Initial subdomain implementation

This commit is contained in:
cuom1999 2023-01-23 20:36:44 -06:00
parent dea24f7f71
commit 1628e63084
17 changed files with 194 additions and 46 deletions

View file

@ -262,6 +262,7 @@ MIDDLEWARE = (
"judge.middleware.DMOJImpersonationMiddleware",
"judge.middleware.ContestMiddleware",
"judge.middleware.DarkModeMiddleware",
"judge.middleware.SubdomainMiddleware",
"django.contrib.flatpages.middleware.FlatpageFallbackMiddleware",
"judge.social_auth.SocialAuthExceptionMiddleware",
"django.contrib.redirects.middleware.RedirectFallbackMiddleware",

View file

@ -2,6 +2,10 @@ from django.conf import settings
from django.http import HttpResponseRedirect
from django.urls import Resolver404, resolve, reverse
from django.utils.http import urlquote
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ObjectDoesNotExist
from judge.models import Organization
class ShortCircuitMiddleware:
@ -82,3 +86,30 @@ class DarkModeMiddleware(object):
reverse("toggle_darkmode") + "?next=" + urlquote(request.path)
)
return self.get_response(request)
class SubdomainMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
domain = request.get_host()
site = get_current_site(request).domain
subdomain = domain[: len(domain) - len(site)]
request.organization = None
if len(subdomain) > 1:
subdomain = subdomain[:-1]
try:
organization = Organization.objects.get(slug=subdomain)
if (
request.profile
and organization in request.profile.organizations.all()
):
request.organization = organization
elif not request.GET.get("next", None):
return HttpResponseRedirect(
reverse("auth_login") + "?next=" + urlquote(request.path)
)
except ObjectDoesNotExist:
pass
return self.get_response(request)

View file

@ -0,0 +1,36 @@
# Generated by Django 3.2.16 on 2023-01-23 23:39
from django.db import migrations, models
def make_slug_unique(apps, schema_editor):
Organization = apps.get_model("judge", "Organization")
slugs = Organization.objects.values_list("slug", flat=True)
slugs = set([i.lower() for i in slugs])
for slug in slugs:
orgs = Organization.objects.filter(slug=slug)
if len(orgs) > 1:
for org in orgs:
org.slug += "-" + str(org.id)
org.save()
class Migration(migrations.Migration):
dependencies = [
("judge", "0144_auto_20230103_0523"),
]
operations = [
migrations.RunPython(make_slug_unique, migrations.RunPython.noop, atomic=True),
migrations.AlterField(
model_name="organization",
name="slug",
field=models.SlugField(
help_text="Organization name shown in URL",
max_length=128,
unique=True,
verbose_name="organization slug",
),
),
]

View file

@ -71,7 +71,7 @@ class Comment(MPTTModel):
order_insertion_by = ["-time"]
@classmethod
def most_recent(cls, user, n, batch=None):
def most_recent(cls, user, n, batch=None, organization=None):
queryset = (
cls.objects.filter(hidden=False)
.select_related("author__user")
@ -79,6 +79,9 @@ class Comment(MPTTModel):
.order_by("-id")
)
if organization:
queryset = queryset.filter(author__in=organization.members.all())
problem_access = CacheDict(
lambda code: Problem.objects.get(code=code).is_accessible_by(user)
)

View file

@ -33,6 +33,7 @@ class Organization(models.Model):
max_length=128,
verbose_name=_("organization slug"),
help_text=_("Organization name shown in URL"),
unique=True,
)
short_name = models.CharField(
max_length=20,
@ -339,6 +340,16 @@ class Profile(models.Model):
ret.add(self.username)
return ret
def can_edit_organization(self, org):
if not self.user.is_authenticated:
return False
profile_id = self.id
return (
org.admins.filter(id=profile_id).exists()
or org.registrant_id == profile_id
or self.user.is_superuser
)
class Meta:
permissions = (
("test_site", "Shows in-progress development stuff"),

View file

@ -76,6 +76,10 @@ class FeedView(ListView):
.filter(is_visible=True)
.order_by("start_time")
)
if self.request.organization:
visible_contests = visible_contests.filter(
is_organization_private=True, organizations=self.request.organization
)
context["current_contests"] = visible_contests.filter(
start_time__lte=now, end_time__gt=now
@ -84,10 +88,14 @@ class FeedView(ListView):
context[
"recent_organizations"
] = OrganizationProfile.get_most_recent_organizations(self.request.profile)
context["top_rated"] = Profile.objects.filter(is_unlisted=False).order_by(
profile_queryset = Profile.objects
if self.request.organization:
profile_queryset = self.request.organization.members
context["top_rated"] = profile_queryset.filter(is_unlisted=False).order_by(
"-rating"
)[:10]
context["top_scorer"] = Profile.objects.filter(is_unlisted=False).order_by(
context["top_scorer"] = profile_queryset.filter(is_unlisted=False).order_by(
"-performance_points"
)[:10]
@ -108,6 +116,8 @@ class PostList(FeedView, PageVoteListView, BookMarkListView):
filter = Q(is_organization_private=False)
if self.request.user.is_authenticated:
filter |= Q(organizations__in=self.request.profile.organizations.all())
if self.request.organization:
filter &= Q(organizations=self.request.organization)
queryset = queryset.filter(filter)
return queryset
@ -184,7 +194,9 @@ class CommentFeed(FeedView):
paginate_by = 50
def get_queryset(self):
return Comment.most_recent(self.request.user, 1000)
return Comment.most_recent(
self.request.user, 1000, organization=self.request.organization
)
def get_context_data(self, **kwargs):
context = super(CommentFeed, self).get_context_data(**kwargs)

View file

@ -179,6 +179,8 @@ class ContestList(
queryset = queryset.filter(
Q(key__icontains=query) | Q(name__icontains=query)
)
if not self.org_query and self.request.organization:
self.org_query = [self.request.organization.id]
if self.org_query:
queryset = queryset.filter(organizations__in=self.org_query)
@ -404,6 +406,15 @@ class ContestDetail(
def get_title(self):
return self.object.name
def get_editable_organizations(self):
if not self.request.profile:
return []
res = []
for organization in self.object.organizations.all():
if self.request.profile.can_edit_organization(organization):
res.append(organization)
return res
def get_context_data(self, **kwargs):
context = super(ContestDetail, self).get_context_data(**kwargs)
context["contest_problems"] = (
@ -421,6 +432,7 @@ class ContestDetail(
)
.add_i18n_name(self.request.LANGUAGE_CODE)
)
context["editable_organizations"] = self.get_editable_organizations()
return context

View file

@ -35,6 +35,7 @@ from django.views.generic.detail import (
SingleObjectTemplateResponseMixin,
)
from django.core.paginator import Paginator
from django.contrib.sites.shortcuts import get_current_site
from reversion import revisions
from judge.forms import (
@ -68,12 +69,13 @@ from judge.utils.views import (
DiggPaginatorMixin,
)
from judge.utils.problems import user_attempted_ids, user_completed_ids
from judge.views.problem import ProblemList
from judge.views.problem import ProblemList, get_problems_in_organization
from judge.views.contests import ContestList
from judge.views.submission import AllSubmissions, SubmissionsListBase
from judge.views.pagevote import PageVoteListView
from judge.views.bookmark import BookMarkListView
__all__ = [
"OrganizationList",
"OrganizationHome",
@ -96,14 +98,9 @@ class OrganizationBase(object):
def can_edit_organization(self, org=None):
if org is None:
org = self.object
if not self.request.user.is_authenticated:
return False
profile_id = self.request.profile.id
return (
org.admins.filter(id=profile_id).exists()
or org.registrant_id == profile_id
or self.request.user.is_superuser
)
if self.request.profile:
return self.request.profile.can_edit_organization(org)
return False
def is_member(self, org=None):
if org is None:
@ -293,6 +290,9 @@ class OrganizationHome(OrganizationDetailView, PageVoteListView, BookMarkListVie
def get_context_data(self, **kwargs):
context = super(OrganizationHome, self).get_context_data(**kwargs)
context["title"] = self.object.name
context["organization_subdomain"] = (
self.object.slug + "." + get_current_site(self.request).domain
)
context["posts"], context["page_obj"] = self.get_posts_and_page_obj()
context = self.add_pagevote_context_data(context, "posts")
context = self.add_bookmark_context_data(context, "posts")
@ -378,6 +378,7 @@ class OrganizationUsers(QueryStringSortMixin, OrganizationDetailView):
class OrganizationProblems(LoginRequiredMixin, MemberOrganizationMixin, ProblemList):
template_name = "organization/problems.html"
filter_organization = True
def get_queryset(self):
self.org_query = [self.organization_id]
@ -387,17 +388,6 @@ class OrganizationProblems(LoginRequiredMixin, MemberOrganizationMixin, ProblemL
self.setup_problem_list(request)
return super().get(request, *args, **kwargs)
def get_latest_attempted_problems(self, limit=None):
if not self.profile:
return ()
problems = set(self.get_queryset().values_list("code", flat=True))
result = list(user_attempted_ids(self.profile).values())
result = [i for i in result if i["code"] in problems]
result = sorted(result, key=lambda d: -d["last_submission"])
if limit:
result = result[:limit]
return result
def get_completed_problems(self):
return user_completed_ids(self.profile) if self.profile is not None else ()
@ -478,10 +468,11 @@ class OrganizationSubmissions(
return None
def _get_queryset(self):
problems = get_problems_in_organization(self.request, self.organization)
return (
super()
._get_entire_queryset()
.filter(contest_object__organizations=self.organization)
.filter(user__organizations=self.organization, problem__in=problems)
)
def get_context_data(self, **kwargs):

View file

@ -105,6 +105,14 @@ def get_contest_submission_count(problem, profile, virtual):
)
def get_problems_in_organization(request, organization):
problem_list = ProblemList(request=request)
problem_list.setup_problem_list(request)
problem_list.org_query = [organization.id]
problems = problem_list.get_normal_queryset()
return problems
class ProblemMixin(object):
model = Problem
slug_url_kwarg = "problem"
@ -145,10 +153,13 @@ class SolvedProblemMixin(object):
else:
return user_attempted_ids(self.profile) if self.profile is not None else ()
def get_latest_attempted_problems(self, limit=None):
def get_latest_attempted_problems(self, limit=None, queryset=None):
if self.in_contest or not self.profile:
return ()
result = list(user_attempted_ids(self.profile).values())
if queryset:
queryset_ids = set([i.code for i in queryset])
result = filter(lambda i: i["code"] in queryset_ids, result)
result = sorted(result, key=lambda d: -d["last_submission"])
if limit:
result = result[:limit]
@ -454,6 +465,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
default_desc = frozenset(("date", "points", "ac_rate", "user_count"))
default_sort = "-date"
first_page_href = None
filter_organization = False
def get_paginator(
self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
@ -592,6 +604,8 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
user=self.profile, points=F("problem__points")
).values_list("problem__id", flat=True)
)
if not self.org_query and self.request.organization:
self.org_query = [self.request.organization.id]
if self.org_query:
self.org_query = self.get_org_query(self.org_query)
queryset = queryset.filter(
@ -652,6 +666,8 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
def get_context_data(self, **kwargs):
context = super(ProblemList, self).get_context_data(**kwargs)
if self.request.organization:
self.filter_organization = True
context["hide_solved"] = 0 if self.in_contest else int(self.hide_solved)
context["show_types"] = 0 if self.in_contest else int(self.show_types)
context["full_text"] = 0 if self.in_contest else int(self.full_text)
@ -676,7 +692,9 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
context["search_query"] = self.search_query
context["completed_problem_ids"] = self.get_completed_problems()
context["attempted_problems"] = self.get_attempted_problems()
context["last_attempted_problems"] = self.get_latest_attempted_problems(15)
context["last_attempted_problems"] = self.get_latest_attempted_problems(
15, context["problems"] if self.filter_organization else None
)
context["page_type"] = "list"
context.update(self.get_sort_paginate_context())
if not self.in_contest:

View file

@ -49,6 +49,7 @@ from judge.utils.raw_sql import join_sql_subquery, use_straight_join
from judge.utils.views import DiggPaginatorMixin
from judge.utils.views import TitleMixin
from judge.utils.timedelta import nice_repr
from judge.views.problem import get_problems_in_organization
MAX_NUMBER_OF_QUERY_SUBMISSIONS = 50000
@ -414,6 +415,13 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
def _get_queryset(self):
queryset = self._get_entire_queryset()
if not self.in_contest:
if self.request.organization:
problems = get_problems_in_organization(
self.request, self.request.organization
)
queryset = queryset.filter(
user__organizations=self.request.organization, problem__in=problems
)
join_sql_subquery(
queryset,
subquery=str(
@ -785,7 +793,12 @@ class AllSubmissions(SubmissionsListBase):
return context
def _get_result_data(self):
if self.in_contest or self.selected_languages or self.selected_statuses:
if (
self.request.organization
or self.in_contest
or self.selected_languages
or self.selected_statuses
):
return super(AllSubmissions, self)._get_result_data()
key = "global_submission_result_data"

View file

@ -454,7 +454,7 @@ class UserList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ListView):
return ret
def get_queryset(self):
ret = (
queryset = (
Profile.objects.filter(is_unlisted=False)
.order_by(self.order, "id")
.select_related("user")
@ -467,11 +467,13 @@ class UserList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ListView):
"problem_count",
)
)
if self.request.organization:
queryset = queryset.filter(organizations=self.request.organization)
if (self.request.GET.get("friend") == "true") and self.request.profile:
ret = self.filter_friend_queryset(ret)
queryset = self.filter_friend_queryset(queryset)
self.filter_friend = True
return ret
return queryset
def get_context_data(self, **kwargs):
context = super(UserList, self).get_context_data(**kwargs)

View file

@ -1,6 +1,11 @@
{% extends "base.html" %}
{% block body %}
{% if request.organization %}
{% cache 3600 'organization_html' request.organization.id MATH_ENGINE %}
{{ request.organization.about|markdown|reference|str|safe }}
{% endcache %}
{% else %}
<h4>
<a target="_blank" href="">LQDOJ (Le Quy Don Online Judge)</a> là một trang web chấm bài tự động được phát triển dựa trên nền tảng mã nguồn mở <a target="_blank" href="https://dmoj.ca/">DMOJ</a>. Được xây dựng với mục đích ban đầu là tạo ra một môi trường học tập cho học sinh khối chuyên Tin <a target="_blank" href="http://thpt-lequydon-danang.edu.vn/">trường THPT chuyên Lê Quý Đôn (TP Đà Nẵng)</a>, hiện nay LQDOJ đã cho phép đăng ký tự do để trở thành một sân chơi rộng mở cho toàn bộ cộng đồng học sinh yêu Tin học. Trang web cung cấp lượng bài luyện tập đồ sộ từ các kỳ thi HSG Quốc Gia, ACM ICPC, Olympic Duyên Hải Bắc Bộ, etc. cho đến các contest định kỳ để xếp loại khả năng (rating) giúp các bạn có thêm động lực cạnh tranh và khí thế phấn đấu rèn luyện nâng cao trình độ lập trình. Các bạn có thể tham khảo mã nguồn của trang web tại <a target="_blank" href="https://github.com/LQDJudge/online-judge">Github repo chính thức</a>. Mọi ý kiến đóng góp và thắc mắc xin gửi về:
<ul>
@ -9,5 +14,6 @@
<li><a target="_blank" href="https://www.facebook.com/profile.php?id=100011662657075">Lê Phước Định</a> (handle: <span class="rating rate-grandmaster user"><a target="_blank" href="../user/cuom1999">cuom1999</a></span>), email: <a href="mailto:dinh@lqdoj.edu.vn">dinh@lqdoj.edu.vn</a></li>
<li><a target="_blank" href="https://www.facebook.com/doannguyenthanhluong">Đoàn Nguyễn Thành Lương</a> (handle: <span class="rating rate-master user"><a target="_blank" href="../user/CaiWinDao">CaiWinDao</a></span>), email: <a href="mailto:luong@lqdoj.edu.vn">luong@lqdoj.edu.vn</a></li>
</ul>
</h4>
</h4>
{% endif %}
{% endblock %}

View file

@ -31,7 +31,7 @@
{# Allow users to leave the virtual contest #}
{% if in_contest %}
<form action="{{ url('contest_leave', contest.key) }}" method="post"
class="contest-join-pseudotab btn-midnightblue">
class="contest-join-pseudotab btn-red">
{% csrf_token %}
<input type="submit" class="leaving-forever" value="{{ _('Leave contest') }}">
</form>
@ -77,12 +77,19 @@
<input type="submit" class="btn-midnightblue" value="{{ _('Login to participate') }}">
</form>
{% endif %}
<div class="content-description">
{% cache 3600 'contest_html' contest.id MATH_ENGINE %}
{{ contest.description|markdown|reference|str|safe }}
{% endcache %}
</div>
{% if editable_organizations %}
<div>
{% for org in editable_organizations %}
<span> [<a href="{{ url('organization_contest_edit', org.id , org.slug , contest.key) }}">{{ _('Edit in') }} {{org.slug}}</a>]</span>
{% endfor %}
</div>
{% endif %}
{% if contest.ended or request.user.is_superuser or is_editor or is_tester %}
<hr>

View file

@ -13,7 +13,14 @@
{% block middle_title %}
<div class="page-title">
<div class="tabs" style="border: none;">
<h2><img src="{{logo_override_image}}" style="height: 3rem; vertical-align: middle"> <span>{{title}}</span></h2>
<h2><img src="{{logo_override_image}}" style="height: 3rem; vertical-align: middle">
{{title}}
</h2>
{% if is_member %}
<div>
<a href="{{organization_subdomain}}" target="_blank">(Subdomain)</a>
</div>
{% endif %}
<span class="spacer"></span>
{% if request.user.is_authenticated %}

View file

@ -1,7 +1,9 @@
{% if not request.in_contest_mode %}
{% if not show_contest_mode %}
<div class="left-sidebar">
{{ make_tab_item('feed', 'fa fa-pagelines', url('problem_feed'), _('Feed')) }}
{{ make_tab_item('list', 'fa fa-list', url('problem_list'), _('List')) }}
{% if request.user.is_superuser %}
{{ make_tab_item('admin', 'fa fa-edit', url('admin:judge_problem_changelist'), _('Admin')) }}
{% endif %}
</div>
{% endif %}

View file

@ -248,13 +248,7 @@
{% endblock %}
{% block left_sidebar %}
{% if not show_contest_mode %}
<div class="left-sidebar">
{{ make_tab_item('feed', 'fa fa-pagelines', url('problem_feed'), _('Feed')) }}
{{ make_tab_item('list', 'fa fa-list', url('problem_list'), _('List')) }}
{{ make_tab_item('admin', 'fa fa-edit', url('admin:judge_problem_changelist'), _('Admin')) }}
</div>
{% endif %}
{% include "problem/left-sidebar.html" %}
{% endblock %}
{% block right_sidebar %}

View file

@ -1,7 +1,9 @@
{% if request.in_contest_mode and request.participation.contest.logo_override_image %}
<img data-src="{{ request.participation.contest.logo_override_image|camo }}" alt="{{ SITE_NAME }}" height="44" style="border: none">
<img src="{{ request.participation.contest.logo_override_image|camo }}" alt="{{ SITE_NAME }}" height="44" style="border: none">
{% elif request.organization %}
<img src="{{ request.organization.logo_override_image|camo }}" alt="{{ SITE_NAME }}" height="44" style="border: none">
{% elif logo_override_image is defined and logo_override_image %}
<img data-src="{{ logo_override_image|camo }}" alt="{{ SITE_NAME }}" height="44" style="border: none">
<img src="{{ logo_override_image|camo }}" alt="{{ SITE_NAME }}" height="44" style="border: none">
{% else %}
<img src="{{ static('icons/logo.png') }}" alt="{{ SITE_NAME }}" height="44"
onerror="this.src=&quot;{{ static('icons/logo.png') }}&quot;; this.onerror=null" style="border: none">