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 %}