From 1628e63084c242a957b34e04e8861bbdc10f533d Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Mon, 23 Jan 2023 20:36:44 -0600 Subject: [PATCH] Initial subdomain implementation --- dmoj/settings.py | 1 + judge/middleware.py | 31 ++++++++++++++++ .../0145_alter_organization_slug.py | 36 +++++++++++++++++++ judge/models/comment.py | 5 ++- judge/models/profile.py | 11 ++++++ judge/views/blog.py | 18 ++++++++-- judge/views/contests.py | 12 +++++++ judge/views/organization.py | 33 +++++++---------- judge/views/problem.py | 22 ++++++++++-- judge/views/submission.py | 15 +++++++- judge/views/user.py | 10 +++--- templates/about/about.html | 8 ++++- templates/contest/contest.html | 11 ++++-- templates/organization/home.html | 9 ++++- templates/problem/left-sidebar.html | 4 ++- templates/problem/list-base.html | 8 +---- templates/site-logo-fragment.html | 6 ++-- 17 files changed, 194 insertions(+), 46 deletions(-) create mode 100644 judge/migrations/0145_alter_organization_slug.py diff --git a/dmoj/settings.py b/dmoj/settings.py index 076ab39..80f42bc 100644 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -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", diff --git a/judge/middleware.py b/judge/middleware.py index 940b486..cf2d37f 100644 --- a/judge/middleware.py +++ b/judge/middleware.py @@ -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) diff --git a/judge/migrations/0145_alter_organization_slug.py b/judge/migrations/0145_alter_organization_slug.py new file mode 100644 index 0000000..1cadbe2 --- /dev/null +++ b/judge/migrations/0145_alter_organization_slug.py @@ -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", + ), + ), + ] diff --git a/judge/models/comment.py b/judge/models/comment.py index 9361434..64a94db 100644 --- a/judge/models/comment.py +++ b/judge/models/comment.py @@ -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) ) diff --git a/judge/models/profile.py b/judge/models/profile.py index c8704cd..d14f9b8 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -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"), diff --git a/judge/views/blog.py b/judge/views/blog.py index 2bcc0a6..66e55a9 100644 --- a/judge/views/blog.py +++ b/judge/views/blog.py @@ -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) diff --git a/judge/views/contests.py b/judge/views/contests.py index 65a8b36..381dfc4 100644 --- a/judge/views/contests.py +++ b/judge/views/contests.py @@ -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 diff --git a/judge/views/organization.py b/judge/views/organization.py index dd3eee7..1ea810b 100644 --- a/judge/views/organization.py +++ b/judge/views/organization.py @@ -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): diff --git a/judge/views/problem.py b/judge/views/problem.py index 2b309a1..320113d 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -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: diff --git a/judge/views/submission.py b/judge/views/submission.py index 8ae921b..9f30fb2 100644 --- a/judge/views/submission.py +++ b/judge/views/submission.py @@ -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" diff --git a/judge/views/user.py b/judge/views/user.py index bd0d34b..80f9add 100644 --- a/judge/views/user.py +++ b/judge/views/user.py @@ -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) diff --git a/templates/about/about.html b/templates/about/about.html index 1fc305e..9547ce1 100644 --- a/templates/about/about.html +++ b/templates/about/about.html @@ -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 %}

LQDOJ (Le Quy Don Online Judge) 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ở DMOJ. Đượ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 trường THPT chuyên Lê Quý Đôn (TP Đà Nẵng), 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 Github repo chính thức. Mọi ý kiến đóng góp và thắc mắc xin gửi về: -

+ +{% endif %} {% endblock %} \ No newline at end of file diff --git a/templates/contest/contest.html b/templates/contest/contest.html index 79c4ce3..c2242c6 100644 --- a/templates/contest/contest.html +++ b/templates/contest/contest.html @@ -31,7 +31,7 @@ {# Allow users to leave the virtual contest #} {% if in_contest %}
+ class="contest-join-pseudotab btn-red"> {% csrf_token %}
@@ -77,12 +77,19 @@ {% endif %} - +
{% cache 3600 'contest_html' contest.id MATH_ENGINE %} {{ contest.description|markdown|reference|str|safe }} {% endcache %}
+ {% if editable_organizations %} +
+ {% for org in editable_organizations %} + [{{ _('Edit in') }} {{org.slug}}] + {% endfor %} +
+ {% endif %} {% if contest.ended or request.user.is_superuser or is_editor or is_tester %}
diff --git a/templates/organization/home.html b/templates/organization/home.html index 8a0d5e5..5d481b9 100644 --- a/templates/organization/home.html +++ b/templates/organization/home.html @@ -13,7 +13,14 @@ {% block middle_title %}
-

{{title}}

+

+ {{title}} +

+ {% if is_member %} +
+ (Subdomain) +
+ {% endif %} {% if request.user.is_authenticated %} diff --git a/templates/problem/left-sidebar.html b/templates/problem/left-sidebar.html index 0083b41..97f5f7b 100644 --- a/templates/problem/left-sidebar.html +++ b/templates/problem/left-sidebar.html @@ -1,7 +1,9 @@ -{% if not request.in_contest_mode %} +{% if not show_contest_mode %} {% endif %} \ No newline at end of file diff --git a/templates/problem/list-base.html b/templates/problem/list-base.html index d6cee15..0e44083 100644 --- a/templates/problem/list-base.html +++ b/templates/problem/list-base.html @@ -248,13 +248,7 @@ {% endblock %} {% block left_sidebar %} - {% if not show_contest_mode %} - - {% endif %} + {% include "problem/left-sidebar.html" %} {% endblock %} {% block right_sidebar %} diff --git a/templates/site-logo-fragment.html b/templates/site-logo-fragment.html index f2099fc..b6a7698 100644 --- a/templates/site-logo-fragment.html +++ b/templates/site-logo-fragment.html @@ -1,7 +1,9 @@ {% if request.in_contest_mode and request.participation.contest.logo_override_image %} - {{ SITE_NAME }} + {{ SITE_NAME }} +{% elif request.organization %} + {{ SITE_NAME }} {% elif logo_override_image is defined and logo_override_image %} - {{ SITE_NAME }} + {{ SITE_NAME }} {% else %} {{ SITE_NAME }}