NDOJ/judge/views/organization.py

1245 lines
42 KiB
Python
Raw Normal View History

2020-01-21 06:35:58 +00:00
from django import forms
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.core.exceptions import PermissionDenied
from django.db import transaction
2020-12-28 05:45:58 +00:00
from django.db.models import Count, Q, Value, BooleanField
2022-05-28 04:28:22 +00:00
from django.db.utils import ProgrammingError
2020-01-21 06:35:58 +00:00
from django.forms import Form, modelformset_factory
2022-05-28 04:28:22 +00:00
from django.http import (
Http404,
HttpResponsePermanentRedirect,
HttpResponseRedirect,
HttpResponseBadRequest,
)
2022-06-05 02:25:23 +00:00
from django.shortcuts import get_object_or_404
2022-10-08 05:15:04 +00:00
from django.urls import reverse
2020-12-28 05:45:58 +00:00
from django.utils import timezone
2022-06-09 05:43:15 +00:00
from django.utils.html import format_html
2022-06-07 22:11:30 +00:00
from django.utils.functional import cached_property
2021-10-10 22:37:15 +00:00
from django.utils.safestring import mark_safe
2020-01-21 06:35:58 +00:00
from django.utils.translation import gettext as _, gettext_lazy, ungettext
2022-05-30 06:59:53 +00:00
from django.views.generic import (
DetailView,
FormView,
ListView,
UpdateView,
View,
CreateView,
)
2022-05-14 17:57:27 +00:00
from django.views.generic.detail import (
SingleObjectMixin,
SingleObjectTemplateResponseMixin,
)
2022-05-28 04:28:22 +00:00
from django.core.paginator import Paginator
2023-01-24 02:36:44 +00:00
from django.contrib.sites.shortcuts import get_current_site
2020-01-21 06:35:58 +00:00
from reversion import revisions
2022-05-30 06:59:53 +00:00
from judge.forms import (
EditOrganizationForm,
AddOrganizationForm,
2022-05-30 06:59:53 +00:00
AddOrganizationMemberForm,
OrganizationBlogForm,
OrganizationAdminBlogForm,
2022-09-16 05:07:27 +00:00
EditOrganizationContestForm,
ContestProblemFormSet,
2022-09-16 05:07:27 +00:00
AddOrganizationContestForm,
2022-05-30 06:59:53 +00:00
)
2022-05-14 17:57:27 +00:00
from judge.models import (
BlogPost,
Comment,
Organization,
OrganizationRequest,
Problem,
Profile,
Contest,
ContestProblem,
2022-10-17 22:08:12 +00:00
OrganizationProfile,
2022-05-14 17:57:27 +00:00
)
2023-10-10 22:38:48 +00:00
from judge.models.notification import make_notification
2022-06-07 22:11:30 +00:00
from judge import event_poster as event
2020-01-21 06:35:58 +00:00
from judge.utils.ranker import ranker
2022-05-14 17:57:27 +00:00
from judge.utils.views import (
TitleMixin,
generic_message,
QueryStringSortMixin,
DiggPaginatorMixin,
)
from judge.utils.problems import user_attempted_ids, user_completed_ids
2024-10-02 20:06:33 +00:00
from judge.utils.contest import maybe_trigger_contest_rescore
2023-01-24 03:00:11 +00:00
from judge.views.problem import ProblemList
2022-05-28 04:28:22 +00:00
from judge.views.contests import ContestList
2024-04-30 02:08:48 +00:00
from judge.views.submission import SubmissionsListBase
from judge.views.feed import FeedView
from judge.tasks import rescore_contest
2023-01-24 02:36:44 +00:00
2022-05-14 17:57:27 +00:00
__all__ = [
"OrganizationList",
"OrganizationHome",
"OrganizationUsers",
2022-05-28 04:28:22 +00:00
"OrganizationProblems",
"OrganizationContests",
2022-05-14 17:57:27 +00:00
"OrganizationMembershipChange",
"JoinOrganization",
"LeaveOrganization",
"EditOrganization",
"RequestJoinOrganization",
"OrganizationRequestDetail",
"OrganizationRequestView",
"OrganizationRequestLog",
"KickUserWidgetView",
]
2020-01-21 06:35:58 +00:00
2020-12-28 05:45:58 +00:00
class OrganizationBase(object):
def can_edit_organization(self, org=None):
if org is None:
org = self.object
2023-01-24 02:36:44 +00:00
if self.request.profile:
return self.request.profile.can_edit_organization(org)
return False
2020-12-28 05:45:58 +00:00
def is_member(self, org=None):
if org is None:
org = self.object
2024-04-13 22:02:54 +00:00
if self.request.profile:
return org.is_member(self.request.profile)
return False
2022-05-14 17:57:27 +00:00
def is_admin(self, org=None):
if org is None:
org = self.object
if self.request.profile:
2024-04-13 22:02:54 +00:00
return org.is_admin(self.request.profile)
return False
2022-05-28 04:33:00 +00:00
def can_access(self, org):
if self.request.user.is_superuser:
return True
if org is None:
org = self.object
return self.is_member(org) or self.can_edit_organization(org)
2020-01-21 06:35:58 +00:00
2020-12-28 05:45:58 +00:00
class OrganizationMixin(OrganizationBase):
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
2022-05-30 06:59:53 +00:00
context["is_member"] = self.is_member(self.organization)
context["is_admin"] = self.is_admin(self.organization)
2022-05-30 06:59:53 +00:00
context["can_edit"] = self.can_edit_organization(self.organization)
context["organization"] = self.organization
context["organization_image"] = self.organization.organization_image
2024-02-19 23:00:44 +00:00
context["organization_subdomain"] = (
("http" if settings.DMOJ_SSL == 0 else "https")
+ "://"
+ self.organization.slug
+ "."
+ get_current_site(self.request).domain
)
2022-05-30 06:59:53 +00:00
if "organizations" in context:
context.pop("organizations")
2020-01-21 06:35:58 +00:00
return context
def dispatch(self, request, *args, **kwargs):
try:
2022-10-08 05:15:04 +00:00
self.organization_id = int(kwargs["pk"])
2022-06-05 02:25:23 +00:00
self.organization = get_object_or_404(Organization, id=self.organization_id)
2020-01-21 06:35:58 +00:00
except Http404:
2023-02-21 20:20:14 +00:00
key = None
if hasattr(self, "slug_url_kwarg"):
key = kwargs.get(self.slug_url_kwarg, None)
2020-01-21 06:35:58 +00:00
if key:
2022-05-14 17:57:27 +00:00
return generic_message(
request,
_("No such organization"),
_('Could not find an organization with the key "%s".') % key,
2023-03-02 03:37:16 +00:00
status=403,
2022-05-14 17:57:27 +00:00
)
2020-01-21 06:35:58 +00:00
else:
2022-05-14 17:57:27 +00:00
return generic_message(
request,
_("No such organization"),
_("Could not find such organization."),
2023-03-02 03:37:16 +00:00
status=403,
2022-05-14 17:57:27 +00:00
)
2022-05-30 06:59:53 +00:00
if self.organization.slug != kwargs["slug"]:
return HttpResponsePermanentRedirect(
2022-05-30 10:54:21 +00:00
request.get_full_path().replace(kwargs["slug"], self.organization.slug)
2022-05-30 06:59:53 +00:00
)
if self.request.user.is_authenticated:
OrganizationProfile.add_organization(
self.request.profile, self.organization
)
2022-05-30 06:59:53 +00:00
return super(OrganizationMixin, self).dispatch(request, *args, **kwargs)
2022-05-14 17:57:27 +00:00
2020-01-21 06:35:58 +00:00
2022-05-30 06:59:53 +00:00
class AdminOrganizationMixin(OrganizationMixin):
def dispatch(self, request, *args, **kwargs):
res = super(AdminOrganizationMixin, self).dispatch(request, *args, **kwargs)
2022-09-25 04:50:26 +00:00
if not hasattr(self, "organization") or self.can_edit_organization(
self.organization
):
2022-05-30 06:59:53 +00:00
return res
return generic_message(
request,
_("Can't edit organization"),
_("You are not allowed to edit this organization."),
status=403,
)
class MemberOrganizationMixin(OrganizationMixin):
def dispatch(self, request, *args, **kwargs):
res = super(MemberOrganizationMixin, self).dispatch(request, *args, **kwargs)
2022-09-25 04:50:26 +00:00
if not hasattr(self, "organization") or self.can_access(self.organization):
2022-05-30 06:59:53 +00:00
return res
return generic_message(
request,
_("Can't access organization"),
_("You are not allowed to access this organization."),
status=403,
)
class OrganizationHomeView(OrganizationMixin):
2022-05-30 06:59:53 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if not hasattr(self, "organization"):
self.organization = self.object
if self.can_edit_organization(self.organization):
context["pending_count"] = OrganizationRequest.objects.filter(
state="P", organization=self.organization
).count()
context["pending_blog_count"] = BlogPost.objects.filter(
visible=False, organizations=self.organization
).count()
else:
context["pending_blog_count"] = BlogPost.objects.filter(
visible=False,
organizations=self.organization,
authors=self.request.profile,
).count()
2024-04-13 22:02:54 +00:00
context["top_rated"] = (
self.organization.members.filter(is_unlisted=False)
.order_by("-rating")
.only("id", "rating")[:10]
)
context["top_scorer"] = (
self.organization.members.filter(is_unlisted=False)
.order_by("-performance_points")
.only("id", "performance_points")[:10]
)
Profile.prefetch_profile_cache([p.id for p in context["top_rated"]])
Profile.prefetch_profile_cache([p.id for p in context["top_scorer"]])
2022-05-30 06:59:53 +00:00
return context
class OrganizationList(
QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ListView, OrganizationBase
):
2020-01-21 06:35:58 +00:00
model = Organization
2022-05-14 17:57:27 +00:00
context_object_name = "organizations"
template_name = "organization/list.html"
2022-05-30 06:59:53 +00:00
title = gettext_lazy("Groups")
paginate_by = 12
all_sorts = frozenset(("name", "member_count"))
default_desc = frozenset(("name", "member_count"))
2020-01-21 06:35:58 +00:00
def get_default_sort_order(self, request):
return "-member_count"
def get(self, request, *args, **kwargs):
default_tab = "mine"
if not self.request.user.is_authenticated:
default_tab = "public"
self.current_tab = self.request.GET.get("tab", default_tab)
self.organization_query = request.GET.get("organization", "")
return super(OrganizationList, self).get(request, *args, **kwargs)
def _get_queryset(self):
queryset = (
2022-05-14 17:57:27 +00:00
super(OrganizationList, self)
.get_queryset()
.annotate(member_count=Count("member"))
2024-04-12 06:51:57 +00:00
.defer("about")
2022-05-14 17:57:27 +00:00
)
2020-01-21 06:35:58 +00:00
if self.organization_query:
queryset = queryset.filter(
Q(slug__icontains=self.organization_query)
| Q(name__icontains=self.organization_query)
| Q(short_name__icontains=self.organization_query)
)
return queryset
def get_queryset(self):
organization_list = self._get_queryset()
my_organizations = []
2022-05-28 07:29:25 +00:00
if self.request.profile:
my_organizations = organization_list.filter(
2022-06-18 07:32:37 +00:00
id__in=self.request.profile.organizations.values("id")
)
if self.current_tab == "public":
queryset = organization_list.exclude(id__in=my_organizations).filter(
is_open=True
)
elif self.current_tab == "private":
queryset = organization_list.exclude(id__in=my_organizations).filter(
is_open=False
)
else:
queryset = my_organizations
if queryset:
queryset = queryset.order_by(self.order)
return queryset
def get_context_data(self, **kwargs):
context = super(OrganizationList, self).get_context_data(**kwargs)
context["first_page_href"] = "."
context["current_tab"] = self.current_tab
context["page_type"] = self.current_tab
context["organization_query"] = self.organization_query
context["selected_order"] = self.request.GET.get("order")
context["all_sort_options"] = [
("name", _("Name (asc.)")),
("-name", _("Name (desc.)")),
("member_count", _("Member count (asc.)")),
("-member_count", _("Member count (desc.)")),
]
context.update(self.get_sort_context())
context.update(self.get_sort_paginate_context())
2020-12-28 05:45:58 +00:00
return context
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
class OrganizationHome(OrganizationHomeView, FeedView):
2022-05-14 17:57:27 +00:00
template_name = "organization/home.html"
paginate_by = 4
context_object_name = "posts"
feed_content_template_name = "blog/content.html"
def get_queryset(self):
return (
2022-05-14 17:57:27 +00:00
BlogPost.objects.filter(
visible=True,
publish_on__lte=timezone.now(),
is_organization_private=True,
organizations=self.organization,
2022-05-14 17:57:27 +00:00
)
.order_by("-sticky", "-publish_on")
.prefetch_related("authors__user", "organizations")
)
2022-11-17 19:10:19 +00:00
2022-05-28 04:28:22 +00:00
def get_context_data(self, **kwargs):
context = super(OrganizationHome, self).get_context_data(**kwargs)
context["title"] = self.organization.name
2022-05-28 04:28:22 +00:00
now = timezone.now()
visible_contests = (
Contest.get_visible_contests(self.request.user)
.filter(
is_visible=True,
is_organization_private=True,
organizations=self.organization,
2022-05-28 04:28:22 +00:00
)
.order_by("start_time")
)
context["current_contests"] = visible_contests.filter(
start_time__lte=now, end_time__gt=now
)
context["future_contests"] = visible_contests.filter(start_time__gt=now)
context["page_type"] = "home"
2022-11-17 19:10:19 +00:00
2020-01-21 06:35:58 +00:00
return context
2023-04-26 09:02:26 +00:00
class OrganizationUsers(
DiggPaginatorMixin, QueryStringSortMixin, OrganizationMixin, ListView
):
2022-05-14 17:57:27 +00:00
template_name = "organization/users.html"
all_sorts = frozenset(("points", "problem_count", "rating", "performance_points"))
2020-12-30 02:29:50 +00:00
default_desc = all_sorts
2022-05-14 17:57:27 +00:00
default_sort = "-performance_points"
2023-04-26 09:02:26 +00:00
paginate_by = 100
context_object_name = "users"
def get_queryset(self):
2023-04-26 09:02:26 +00:00
return (
self.organization.members.filter(is_unlisted=False)
.order_by(self.order, "id")
.select_related("user")
.only(
"display_rank",
"user__username",
"points",
"rating",
"performance_points",
"problem_count",
)
)
2022-05-14 17:57:27 +00:00
2022-06-26 05:07:34 +00:00
def dispatch(self, request, *args, **kwargs):
res = super(OrganizationUsers, self).dispatch(request, *args, **kwargs)
2023-03-02 03:37:16 +00:00
if res.status_code != 200:
return res
2022-06-26 05:07:34 +00:00
if self.can_access(self.organization) or self.organization.is_open:
return res
return generic_message(
request,
_("Can't access organization"),
_("You are not allowed to access this organization."),
status=403,
)
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super(OrganizationUsers, self).get_context_data(**kwargs)
context["title"] = _("%s Members") % self.organization.name
2022-05-14 17:57:27 +00:00
context["partial"] = True
context["kick_url"] = reverse(
"organization_user_kick",
args=[self.organization.id, self.organization.slug],
2022-05-14 17:57:27 +00:00
)
2023-04-26 09:02:26 +00:00
context["users"] = ranker(
context["users"], rank=self.paginate_by * (context["page_obj"].number - 1)
)
2022-05-14 17:57:27 +00:00
context["first_page_href"] = "."
2022-05-28 04:28:22 +00:00
context["page_type"] = "users"
2020-12-30 02:29:50 +00:00
context.update(self.get_sort_context())
2020-01-21 06:35:58 +00:00
return context
2022-05-30 06:59:53 +00:00
class OrganizationProblems(LoginRequiredMixin, MemberOrganizationMixin, ProblemList):
2022-05-28 04:28:22 +00:00
template_name = "organization/problems.html"
2023-01-24 02:36:44 +00:00
filter_organization = True
2022-05-28 04:28:22 +00:00
def get_queryset(self):
self.org_query = [self.organization_id]
return super().get_normal_queryset()
def get(self, request, *args, **kwargs):
self.setup_problem_list(request)
return super().get(request, *args, **kwargs)
def get_completed_problems(self):
return user_completed_ids(self.profile) if self.profile is not None else ()
def get_attempted_problems(self):
return user_attempted_ids(self.profile) if self.profile is not None else ()
@cached_property
def in_contest(self):
return False
2022-05-28 04:28:22 +00:00
def get_context_data(self, **kwargs):
context = super(OrganizationProblems, self).get_context_data(**kwargs)
context["page_type"] = "problems"
context["show_contest_mode"] = False
2022-05-28 04:28:22 +00:00
return context
class OrganizationContestMixin(
LoginRequiredMixin,
TitleMixin,
OrganizationHomeView,
):
model = Contest
2022-10-04 03:33:16 +00:00
def is_contest_editable(self, request, contest):
return contest.is_editable_by(request.user) or self.can_edit_organization(
self.organization
)
class OrganizationContests(
OrganizationContestMixin, MemberOrganizationMixin, ContestList
):
2022-05-28 04:28:22 +00:00
template_name = "organization/contests.html"
def get_queryset(self):
self.org_query = [self.organization_id]
2024-05-30 07:59:22 +00:00
self.hide_organization_contests = False
2022-05-28 04:28:22 +00:00
return super().get_queryset()
def set_editable_contest(self, contest):
if not contest:
return False
contest.is_editable = self.is_contest_editable(self.request, contest)
2022-05-28 04:28:22 +00:00
def get_context_data(self, **kwargs):
context = super(OrganizationContests, self).get_context_data(**kwargs)
context["page_type"] = "contests"
context.pop("organizations")
if self.can_edit_organization(self.organization):
context["create_url"] = reverse(
"organization_contest_add",
args=[self.organization.id, self.organization.slug],
)
if self.current_tab == "active":
for participation in context["contests"]:
self.set_editable_contest(participation.contest)
else:
for contest in context["contests"]:
self.set_editable_contest(contest)
2022-05-28 04:28:22 +00:00
return context
2022-06-07 22:11:30 +00:00
class OrganizationSubmissions(
LoginRequiredMixin, MemberOrganizationMixin, SubmissionsListBase
):
template_name = "organization/submissions.html"
2023-07-06 15:39:16 +00:00
2022-06-07 22:11:30 +00:00
@cached_property
def in_contest(self):
return False
@cached_property
def contest(self):
return None
def get_context_data(self, **kwargs):
context = super(OrganizationSubmissions, self).get_context_data(**kwargs)
2022-06-08 16:00:34 +00:00
# context["dynamic_update"] = context["page_obj"].number == 1
# context["last_msg"] = event.last()
2022-06-07 22:11:30 +00:00
context["stats_update_interval"] = 3600
context["page_type"] = "submissions"
context["page_prefix"] = None
context["page_suffix"] = suffix = (
("?" + self.request.GET.urlencode()) if self.request.GET else ""
)
context["first_page_href"] = (self.first_page_href or ".") + suffix
return context
2022-06-09 05:43:15 +00:00
def get_content_title(self):
return format_html(
_('All submissions in <a href="{1}">{0}</a>'),
self.organization,
reverse(
"organization_home", args=[self.organization.id, self.organization.slug]
),
)
2024-04-25 06:58:47 +00:00
def get_title(self):
return _("Submissions in") + f" {self.organization}"
2022-06-07 22:11:30 +00:00
2022-05-14 17:57:27 +00:00
class OrganizationMembershipChange(
LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View
):
2022-05-30 06:59:53 +00:00
model = Organization
context_object_name = "organization"
2020-01-21 06:35:58 +00:00
def post(self, request, *args, **kwargs):
org = self.get_object()
response = self.handle(request, org, request.profile)
if response is not None:
return response
return HttpResponseRedirect(org.get_absolute_url())
def handle(self, request, org, profile):
raise NotImplementedError()
class JoinOrganization(OrganizationMembershipChange):
def handle(self, request, org, profile):
if profile.organizations.filter(id=org.id).exists():
2022-05-14 17:57:27 +00:00
return generic_message(
request,
2022-05-30 06:59:53 +00:00
_("Joining group"),
_("You are already in the group."),
2022-05-14 17:57:27 +00:00
)
2020-01-21 06:35:58 +00:00
if not org.is_open:
2022-05-14 17:57:27 +00:00
return generic_message(
2022-05-30 06:59:53 +00:00
request, _("Joining group"), _("This group is not open.")
2022-05-14 17:57:27 +00:00
)
2020-01-21 06:35:58 +00:00
max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT
if profile.organizations.filter(is_open=True).count() >= max_orgs:
return generic_message(
2022-05-14 17:57:27 +00:00
request,
2022-05-30 06:59:53 +00:00
_("Joining group"),
_("You may not be part of more than {count} public groups.").format(
count=max_orgs
),
2020-01-21 06:35:58 +00:00
)
profile.organizations.add(org)
profile.save()
2022-05-14 17:57:27 +00:00
cache.delete(make_template_fragment_key("org_member_count", (org.id,)))
2020-01-21 06:35:58 +00:00
class LeaveOrganization(OrganizationMembershipChange):
def handle(self, request, org, profile):
if not profile.organizations.filter(id=org.id).exists():
2022-05-14 17:57:27 +00:00
return generic_message(
request,
2022-05-30 06:59:53 +00:00
_("Leaving group"),
2022-05-14 17:57:27 +00:00
_('You are not in "%s".') % org.short_name,
)
2020-01-21 06:35:58 +00:00
profile.organizations.remove(org)
2022-05-14 17:57:27 +00:00
cache.delete(make_template_fragment_key("org_member_count", (org.id,)))
2020-01-21 06:35:58 +00:00
class OrganizationRequestForm(Form):
reason = forms.CharField(widget=forms.Textarea)
class RequestJoinOrganization(LoginRequiredMixin, SingleObjectMixin, FormView):
model = Organization
2022-05-14 17:57:27 +00:00
slug_field = "key"
slug_url_kwarg = "key"
template_name = "organization/requests/request.html"
2020-01-21 06:35:58 +00:00
form_class = OrganizationRequestForm
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
return super(RequestJoinOrganization, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(RequestJoinOrganization, self).get_context_data(**kwargs)
if self.object.is_open:
raise Http404()
2022-05-14 17:57:27 +00:00
context["title"] = _("Request to join %s") % self.object.name
2020-01-21 06:35:58 +00:00
return context
def form_valid(self, form):
request = OrganizationRequest()
request.organization = self.get_object()
request.user = self.request.profile
2022-05-14 17:57:27 +00:00
request.reason = form.cleaned_data["reason"]
request.state = "P"
2020-01-21 06:35:58 +00:00
request.save()
2022-05-14 17:57:27 +00:00
return HttpResponseRedirect(
reverse(
"request_organization_detail",
args=(
request.organization.id,
request.organization.slug,
request.id,
),
)
)
2020-01-21 06:35:58 +00:00
2022-05-30 06:59:53 +00:00
class OrganizationRequestDetail(
LoginRequiredMixin,
TitleMixin,
OrganizationHomeView,
2022-05-30 06:59:53 +00:00
DetailView,
):
2020-01-21 06:35:58 +00:00
model = OrganizationRequest
2022-05-14 17:57:27 +00:00
template_name = "organization/requests/detail.html"
title = gettext_lazy("Join request detail")
pk_url_kwarg = "rpk"
2020-01-21 06:35:58 +00:00
def get_object(self, queryset=None):
object = super(OrganizationRequestDetail, self).get_object(queryset)
profile = self.request.profile
2022-05-14 17:57:27 +00:00
if (
object.user_id != profile.id
and not object.organization.admins.filter(id=profile.id).exists()
):
2020-01-21 06:35:58 +00:00
raise PermissionDenied()
return object
2022-05-14 17:57:27 +00:00
OrganizationRequestFormSet = modelformset_factory(
OrganizationRequest, extra=0, fields=("state",), can_delete=True
)
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
class OrganizationRequestBaseView(
2024-06-25 01:01:00 +00:00
AdminOrganizationMixin,
DetailView,
OrganizationHomeView,
2022-05-14 17:57:27 +00:00
TitleMixin,
LoginRequiredMixin,
SingleObjectTemplateResponseMixin,
SingleObjectMixin,
):
2020-01-21 06:35:58 +00:00
model = Organization
2022-05-14 17:57:27 +00:00
slug_field = "key"
slug_url_kwarg = "key"
2020-01-21 06:35:58 +00:00
tab = None
2021-10-10 22:37:15 +00:00
def get_content_title(self):
2023-04-03 16:55:13 +00:00
return _("Manage join requests")
2022-05-14 17:57:27 +00:00
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super(OrganizationRequestBaseView, self).get_context_data(**kwargs)
2022-05-14 17:57:27 +00:00
context["title"] = _("Managing join requests for %s") % self.object.name
context["tab"] = self.tab
2020-01-21 06:35:58 +00:00
return context
class OrganizationRequestView(OrganizationRequestBaseView):
2022-05-14 17:57:27 +00:00
template_name = "organization/requests/pending.html"
tab = "pending"
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super(OrganizationRequestView, self).get_context_data(**kwargs)
2022-05-14 17:57:27 +00:00
context["formset"] = self.formset
2020-01-21 06:35:58 +00:00
return context
def get(self, request, *args, **kwargs):
self.object = self.get_object()
self.formset = OrganizationRequestFormSet(
2022-05-14 17:57:27 +00:00
queryset=OrganizationRequest.objects.filter(
state="P", organization=self.object
),
2020-01-21 06:35:58 +00:00
)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
self.object = organization = self.get_object()
self.formset = formset = OrganizationRequestFormSet(request.POST, request.FILES)
if formset.is_valid():
if organization.slots is not None:
deleted_set = set(formset.deleted_forms)
2022-05-14 17:57:27 +00:00
to_approve = sum(
form.cleaned_data["state"] == "A"
for form in formset.forms
if form not in deleted_set
)
2020-01-21 06:35:58 +00:00
can_add = organization.slots - organization.members.count()
if to_approve > can_add:
2022-05-14 17:57:27 +00:00
messages.error(
request,
_(
"Your organization can only receive %d more members. "
"You cannot approve %d users."
)
% (can_add, to_approve),
)
return self.render_to_response(
self.get_context_data(object=organization)
)
2020-01-21 06:35:58 +00:00
approved, rejected = 0, 0
for obj in formset.save():
2022-05-14 17:57:27 +00:00
if obj.state == "A":
2020-01-21 06:35:58 +00:00
obj.user.organizations.add(obj.organization)
approved += 1
2022-05-14 17:57:27 +00:00
elif obj.state == "R":
2020-01-21 06:35:58 +00:00
rejected += 1
2022-05-14 17:57:27 +00:00
messages.success(
request,
ungettext("Approved %d user.", "Approved %d users.", approved)
% approved
+ "\n"
+ ungettext("Rejected %d user.", "Rejected %d users.", rejected)
% rejected,
)
cache.delete(
make_template_fragment_key("org_member_count", (organization.id,))
)
2020-01-21 06:35:58 +00:00
return HttpResponseRedirect(request.get_full_path())
return self.render_to_response(self.get_context_data(object=organization))
put = post
class OrganizationRequestLog(OrganizationRequestBaseView):
2022-05-14 17:57:27 +00:00
states = ("A", "R")
tab = "log"
template_name = "organization/requests/log.html"
2020-01-21 06:35:58 +00:00
def get(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def get_context_data(self, **kwargs):
context = super(OrganizationRequestLog, self).get_context_data(**kwargs)
2022-05-14 17:57:27 +00:00
context["requests"] = self.object.requests.filter(state__in=self.states)
2020-01-21 06:35:58 +00:00
return context
2022-05-30 06:59:53 +00:00
class AddOrganizationMember(
LoginRequiredMixin,
TitleMixin,
AdminOrganizationMixin,
OrganizationHomeView,
2022-05-30 06:59:53 +00:00
UpdateView,
):
template_name = "organization/add-member.html"
2020-01-21 06:35:58 +00:00
model = Organization
2022-05-30 06:59:53 +00:00
form_class = AddOrganizationMemberForm
2020-01-21 06:35:58 +00:00
def get_title(self):
2022-05-30 06:59:53 +00:00
return _("Add member for %s") % self.object.name
2020-01-21 06:35:58 +00:00
def get_object(self, queryset=None):
2022-05-30 06:59:53 +00:00
object = super(AddOrganizationMember, self).get_object()
2020-01-21 06:35:58 +00:00
if not self.can_edit_organization(object):
raise PermissionDenied()
return object
def form_valid(self, form):
2022-05-30 06:59:53 +00:00
new_users = form.cleaned_data["new_users"]
self.object.members.add(*new_users)
link = reverse("organization_home", args=[self.object.id, self.object.slug])
html = f'<a href="{link}">{self.object.name}</a>'
make_notification(new_users, "Added to group", html, self.request.profile)
2024-02-19 23:00:44 +00:00
with revisions.create_revision():
usernames = ", ".join([u.username for u in new_users])
revisions.set_comment(_("Added members from site") + ": " + usernames)
2020-01-21 06:35:58 +00:00
revisions.set_user(self.request.user)
2022-05-30 06:59:53 +00:00
return super(AddOrganizationMember, self).form_valid(form)
2020-01-21 06:35:58 +00:00
2022-05-30 06:59:53 +00:00
def get_success_url(self):
return reverse("organization_users", args=[self.object.id, self.object.slug])
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
class KickUserWidgetView(
2022-05-30 06:59:53 +00:00
LoginRequiredMixin, AdminOrganizationMixin, SingleObjectMixin, View
2022-05-14 17:57:27 +00:00
):
2022-05-30 08:14:02 +00:00
model = Organization
2022-06-01 05:28:56 +00:00
2020-01-21 06:35:58 +00:00
def post(self, request, *args, **kwargs):
organization = self.get_object()
try:
2022-05-14 17:57:27 +00:00
user = Profile.objects.get(id=request.POST.get("user", None))
2020-01-21 06:35:58 +00:00
except Profile.DoesNotExist:
2022-05-14 17:57:27 +00:00
return generic_message(
request,
_("Can't kick user"),
_("The user you are trying to kick does not exist!"),
status=400,
)
2020-01-21 06:35:58 +00:00
if not organization.is_member(user):
2022-05-14 17:57:27 +00:00
return generic_message(
request,
_("Can't kick user"),
_("The user you are trying to kick is not in organization: %s.")
% organization.name,
status=400,
)
2020-01-21 06:35:58 +00:00
if organization.is_admin(user):
return generic_message(
request,
_("Can't kick user"),
_("The user you are trying to kick is an organization admin."),
status=400,
)
with revisions.create_revision():
revisions.set_comment(_("Kicked member") + " " + user.username)
revisions.set_user(self.request.user)
organization.members.remove(user)
organization.save()
2020-01-21 06:35:58 +00:00
return HttpResponseRedirect(organization.get_users_url())
2022-05-30 06:59:53 +00:00
class EditOrganization(
LoginRequiredMixin,
TitleMixin,
AdminOrganizationMixin,
OrganizationHomeView,
2022-05-30 06:59:53 +00:00
UpdateView,
):
template_name = "organization/edit.html"
model = Organization
form_class = EditOrganizationForm
def get_title(self):
return _("Edit %s") % self.object.name
def get_object(self, queryset=None):
object = super(EditOrganization, self).get_object()
if not self.can_edit_organization(object):
raise PermissionDenied()
return object
def get_form(self, form_class=None):
form = super(EditOrganization, self).get_form(form_class)
form.fields["admins"].queryset = Profile.objects.filter(
Q(organizations=self.object) | Q(admin_of=self.object)
).distinct()
return form
def form_valid(self, form):
2024-02-19 23:00:44 +00:00
with revisions.create_revision():
2022-05-30 06:59:53 +00:00
revisions.set_comment(_("Edited from site"))
revisions.set_user(self.request.user)
return super(EditOrganization, self).form_valid(form)
class AddOrganization(LoginRequiredMixin, TitleMixin, CreateView):
template_name = "organization/add.html"
model = Organization
form_class = AddOrganizationForm
def get_title(self):
return _("Create group")
def get_form_kwargs(self):
kwargs = super(AddOrganization, self).get_form_kwargs()
kwargs["request"] = self.request
return kwargs
def form_valid(self, form):
if (
not self.request.user.is_staff
and Organization.objects.filter(registrant=self.request.profile).count()
>= settings.DMOJ_USER_MAX_ORGANIZATION_ADD
):
return generic_message(
self.request,
_("Exceeded limit"),
_("You created too many groups. You can only create at most %d groups")
% settings.DMOJ_USER_MAX_ORGANIZATION_ADD,
status=400,
)
2024-02-19 23:00:44 +00:00
with revisions.create_revision():
revisions.set_comment(_("Added from site"))
revisions.set_user(self.request.user)
res = super(AddOrganization, self).form_valid(form)
self.object.admins.add(self.request.profile)
self.object.members.add(self.request.profile)
self.object.save()
return res
class AddOrganizationContest(
AdminOrganizationMixin, OrganizationContestMixin, CreateView
):
template_name = "organization/contest/add.html"
2022-09-16 05:07:27 +00:00
form_class = AddOrganizationContestForm
def get_title(self):
return _("Add contest")
2022-09-16 05:07:27 +00:00
def get_form_kwargs(self):
kwargs = super(AddOrganizationContest, self).get_form_kwargs()
kwargs["request"] = self.request
return kwargs
def form_valid(self, form):
2024-02-19 23:00:44 +00:00
with revisions.create_revision():
revisions.set_comment(_("Added from site"))
revisions.set_user(self.request.user)
2022-09-16 05:07:27 +00:00
res = super(AddOrganizationContest, self).form_valid(form)
2022-09-16 05:07:27 +00:00
self.object.organizations.add(self.organization)
self.object.is_organization_private = True
2022-09-16 05:07:27 +00:00
self.object.authors.add(self.request.profile)
self.object.save()
return res
def get_success_url(self):
return reverse(
"organization_contest_edit",
args=[self.organization.id, self.organization.slug, self.object.key],
)
class EditOrganizationContest(
OrganizationContestMixin, MemberOrganizationMixin, UpdateView
):
2022-09-16 05:07:27 +00:00
template_name = "organization/contest/edit.html"
form_class = EditOrganizationContestForm
def setup_contest(self, request, *args, **kwargs):
contest_key = kwargs.get("contest", None)
if not contest_key:
raise Http404()
self.contest = get_object_or_404(Contest, key=contest_key)
if self.organization not in self.contest.organizations.all():
raise Http404()
if not self.is_contest_editable(request, self.contest):
return generic_message(
self.request,
_("Permission denied"),
_("You are not allowed to edit this contest"),
status=400,
)
2022-09-16 05:07:27 +00:00
def get_form_kwargs(self):
kwargs = super(EditOrganizationContest, self).get_form_kwargs()
kwargs["org_id"] = self.organization.id
return kwargs
def get(self, request, *args, **kwargs):
res = self.setup_contest(request, *args, **kwargs)
if res:
return res
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
res = self.setup_contest(request, *args, **kwargs)
if res:
return res
problem_formset = self.get_problem_formset(True)
if problem_formset.is_valid():
2024-09-03 15:48:01 +00:00
for problem_form in problem_formset:
if problem_form.cleaned_data.get("DELETE") and problem_form.instance.pk:
problem_form.instance.delete()
for problem_form in problem_formset.save(commit=False):
if problem_form:
problem_form.contest = self.contest
problem_form.save()
2024-09-03 15:48:01 +00:00
2022-10-08 15:12:08 +00:00
super().post(request, *args, **kwargs)
return HttpResponseRedirect(
reverse(
"organization_contest_edit",
args=(
self.organization_id,
self.organization.slug,
self.contest.key,
),
)
)
self.object = self.contest
return self.render_to_response(
self.get_context_data(
problems_form=problem_formset,
)
)
def get_title(self):
return _("Edit %s") % self.contest.key
def get_content_title(self):
href = reverse("contest_view", args=[self.contest.key])
return mark_safe(_("Edit") + f' <a href="{href}">{self.contest.key}</a>')
def get_object(self):
return self.contest
def form_valid(self, form):
2024-02-19 23:00:44 +00:00
with revisions.create_revision():
revisions.set_comment(_("Edited from site"))
revisions.set_user(self.request.user)
res = super(EditOrganizationContest, self).form_valid(form)
self.object.organizations.add(self.organization)
self.object.is_organization_private = True
self.object.save()
2024-10-02 20:06:33 +00:00
maybe_trigger_contest_rescore(form, self.object)
return res
def get_problem_formset(self, post=False):
return ContestProblemFormSet(
data=self.request.POST if post else None,
prefix="problems",
queryset=ContestProblem.objects.filter(contest=self.contest).order_by(
"order"
),
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if "problems_form" not in context:
context["problems_form"] = self.get_problem_formset()
return context
def get_success_url(self):
return self.request.path
2022-05-30 06:59:53 +00:00
class AddOrganizationBlog(
LoginRequiredMixin,
TitleMixin,
OrganizationHomeView,
2022-05-30 06:59:53 +00:00
MemberOrganizationMixin,
CreateView,
):
template_name = "organization/blog/add.html"
model = BlogPost
form_class = OrganizationBlogForm
2022-05-31 04:35:30 +00:00
def get_form_class(self):
if self.can_edit_organization(self.organization):
return OrganizationAdminBlogForm
return OrganizationBlogForm
2022-05-30 06:59:53 +00:00
def get_title(self):
return _("Add blog for %s") % self.organization.name
def form_valid(self, form):
2024-02-19 23:00:44 +00:00
with revisions.create_revision():
2022-05-30 06:59:53 +00:00
res = super(AddOrganizationBlog, self).form_valid(form)
self.object.is_organization_private = True
self.object.authors.add(self.request.profile)
self.object.slug = self.organization.slug + "-" + self.request.user.username
self.object.organizations.add(self.organization)
self.object.save()
revisions.set_comment(_("Added from site"))
revisions.set_user(self.request.user)
2022-06-26 07:24:38 +00:00
link = reverse(
"edit_organization_blog",
args=[self.organization.id, self.organization.slug, self.object.id],
)
html = (
f'<a href="{link}">{self.object.title} - {self.organization.name}</a>'
)
2023-10-10 22:38:48 +00:00
make_notification(
self.organization.admins.all(), "Add blog", html, self.request.profile
)
2022-05-30 06:59:53 +00:00
return res
2024-02-19 23:00:44 +00:00
def get_success_url(self):
return reverse(
"organization_home", args=[self.organization.id, self.organization.slug]
)
2022-05-30 06:59:53 +00:00
class EditOrganizationBlog(
LoginRequiredMixin,
TitleMixin,
OrganizationHomeView,
AdminOrganizationMixin,
2022-05-30 06:59:53 +00:00
UpdateView,
):
2022-10-08 02:35:21 +00:00
template_name = "organization/blog/edit.html"
2022-05-30 06:59:53 +00:00
model = BlogPost
def get_form_class(self):
if self.can_edit_organization(self.organization):
return OrganizationAdminBlogForm
return OrganizationBlogForm
def setup_blog(self, request, *args, **kwargs):
try:
self.blog_id = kwargs["blog_pk"]
self.blog = BlogPost.objects.get(id=self.blog_id)
if self.organization not in self.blog.organizations.all():
raise Exception(_("This blog does not belong to this organization"))
if not self.request.profile.can_edit_organization(self.organization):
raise Exception(_("Not allowed to edit this blog"))
except Exception as e:
2022-05-30 06:59:53 +00:00
return generic_message(
request,
_("Permission denied"),
e,
2022-05-30 06:59:53 +00:00
)
def publish_blog(self, request, *args, **kwargs):
self.blog_id = kwargs["blog_pk"]
BlogPost.objects.filter(pk=self.blog_id).update(visible=True)
def delete_blog(self, request, *args, **kwargs):
2022-10-08 02:35:21 +00:00
self.blog_id = kwargs["blog_pk"]
BlogPost.objects.get(pk=self.blog_id).delete()
2022-10-08 02:35:21 +00:00
2022-05-30 06:59:53 +00:00
def get(self, request, *args, **kwargs):
res = self.setup_blog(request, *args, **kwargs)
if res:
return res
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
res = self.setup_blog(request, *args, **kwargs)
if res:
return res
if request.POST["action"] == "Delete":
2022-10-08 02:35:21 +00:00
self.create_notification("Delete blog")
self.delete_blog(request, *args, **kwargs)
cur_url = reverse(
"organization_home",
args=(self.organization_id, self.organization.slug),
)
return HttpResponseRedirect(cur_url)
elif request.POST["action"] == "Reject":
self.create_notification("Reject blog")
self.delete_blog(request, *args, **kwargs)
cur_url = reverse(
"organization_pending_blogs",
args=(self.organization_id, self.organization.slug),
)
return HttpResponseRedirect(cur_url)
elif request.POST["action"] == "Approve":
self.create_notification("Approve blog")
self.publish_blog(request, *args, **kwargs)
cur_url = reverse(
"organization_pending_blogs",
args=(self.organization_id, self.organization.slug),
)
2022-10-08 02:35:21 +00:00
return HttpResponseRedirect(cur_url)
else:
return super().post(request, *args, **kwargs)
2022-05-30 06:59:53 +00:00
def get_object(self):
return self.blog
def get_title(self):
return _("Edit blog %s") % self.object.title
def create_notification(self, action):
2022-10-08 02:35:21 +00:00
blog = BlogPost.objects.get(pk=self.blog_id)
link = reverse(
"edit_organization_blog",
args=[self.organization.id, self.organization.slug, self.blog_id],
)
html = f'<a href="{link}">{blog.title} - {self.organization.name}</a>'
to_users = (self.organization.admins.all() | blog.get_authors()).distinct()
make_notification(to_users, action, html, self.request.profile)
2022-05-30 06:59:53 +00:00
def form_valid(self, form):
2024-02-19 23:00:44 +00:00
with revisions.create_revision():
2022-05-30 06:59:53 +00:00
res = super(EditOrganizationBlog, self).form_valid(form)
revisions.set_comment(_("Edited from site"))
revisions.set_user(self.request.user)
2022-10-08 02:35:21 +00:00
self.create_notification("Edit blog")
2022-05-30 06:59:53 +00:00
return res
2024-02-19 23:00:44 +00:00
def get_success_url(self):
return reverse(
"organization_home", args=[self.organization.id, self.organization.slug]
)
2022-05-30 06:59:53 +00:00
class PendingBlogs(
LoginRequiredMixin,
TitleMixin,
MemberOrganizationMixin,
OrganizationHomeView,
2022-05-30 06:59:53 +00:00
ListView,
):
model = BlogPost
template_name = "organization/blog/pending.html"
context_object_name = "blogs"
def get_queryset(self):
queryset = BlogPost.objects.filter(
organizations=self.organization, visible=False
)
if not self.can_edit_organization(self.organization):
queryset = queryset.filter(authors=self.request.profile)
return queryset.order_by("publish_on")
def get_title(self):
return _("Pending blogs in %s") % self.organization.name
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["org"] = self.organization
return context