NDOJ/judge/views/organization.py

376 lines
17 KiB
Python
Raw Permalink 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
2020-01-21 06:35:58 +00:00
from django.forms import Form, modelformset_factory
from django.http import Http404, HttpResponsePermanentRedirect, HttpResponseRedirect
from django.urls import reverse
2020-12-28 05:45:58 +00:00
from django.utils import timezone
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
from django.views.generic import DetailView, FormView, ListView, UpdateView, View
from django.views.generic.detail import SingleObjectMixin, SingleObjectTemplateResponseMixin
from reversion import revisions
from judge.forms import EditOrganizationForm
2020-12-28 05:45:58 +00:00
from judge.models import BlogPost, Comment, Organization, OrganizationRequest, Problem, Profile, Contest
2020-01-21 06:35:58 +00:00
from judge.utils.ranker import ranker
2020-12-30 02:29:50 +00:00
from judge.utils.views import TitleMixin, generic_message, QueryStringSortMixin, DiggPaginatorMixin
2020-01-21 06:35:58 +00:00
__all__ = ['OrganizationList', 'OrganizationHome', 'OrganizationUsers', 'OrganizationMembershipChange',
'JoinOrganization', 'LeaveOrganization', 'EditOrganization', 'RequestJoinOrganization',
'OrganizationRequestDetail', 'OrganizationRequestView', 'OrganizationRequestLog',
'KickUserWidgetView']
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
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
def is_member(self, org=None):
if org is None:
org = self.object
return self.request.profile in org if self.request.user.is_authenticated else False
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
context_object_name = 'organization'
model = Organization
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['logo_override_image'] = self.object.logo_override_image
return context
def dispatch(self, request, *args, **kwargs):
try:
return super(OrganizationMixin, self).dispatch(request, *args, **kwargs)
except Http404:
key = kwargs.get(self.slug_url_kwarg, None)
if key:
return generic_message(request, _('No such organization'),
_('Could not find an organization with the key "%s".') % key)
else:
return generic_message(request, _('No such organization'),
_('Could not find such organization.'))
2020-12-28 05:45:58 +00:00
2020-01-21 06:35:58 +00:00
class OrganizationDetailView(OrganizationMixin, DetailView):
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.slug != kwargs['slug']:
return HttpResponsePermanentRedirect(request.get_full_path().replace(kwargs['slug'], self.object.slug))
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
2020-12-28 05:45:58 +00:00
class OrganizationList(TitleMixin, ListView, OrganizationBase):
2020-01-21 06:35:58 +00:00
model = Organization
context_object_name = 'organizations'
template_name = 'organization/list.html'
title = gettext_lazy('Organizations')
def get_queryset(self):
return super(OrganizationList, self).get_queryset().annotate(member_count=Count('member'))
2020-12-28 05:45:58 +00:00
def get_context_data(self, **kwargs):
context = super(OrganizationList, self).get_context_data(**kwargs)
context['my_organizations'] = set()
for organization in context['organizations']:
if self.can_edit_organization(organization) or self.is_member(organization):
context['my_organizations'].add(organization)
return context
2020-01-21 06:35:58 +00:00
class OrganizationHome(OrganizationDetailView):
template_name = 'organization/home.html'
def get_context_data(self, **kwargs):
context = super(OrganizationHome, self).get_context_data(**kwargs)
context['title'] = self.object.name
context['can_edit'] = self.can_edit_organization()
2020-12-28 05:45:58 +00:00
context['is_member'] = self.is_member()
context['new_problems'] = Problem.objects.filter(is_public=True, is_organization_private=True,
organizations=self.object) \
.order_by('-date', '-id')[:settings.DMOJ_BLOG_NEW_PROBLEM_COUNT]
context['new_contests'] = Contest.objects.filter(is_visible=True, is_organization_private=True,
organizations=self.object) \
.order_by('-end_time', '-id')[:settings.DMOJ_BLOG_NEW_CONTEST_COUNT]
context['posts'] = BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now(),
is_organization_private=True, organizations=self.object) \
.order_by('-sticky', '-publish_on') \
.prefetch_related('authors__user', 'organizations')
context['post_comment_counts'] = {
int(page[2:]): count for page, count in
Comment.objects.filter(page__in=['b:%d' % post.id for post in context['posts']], hidden=False)
.values_list('page').annotate(count=Count('page')).order_by()
}
2021-10-10 22:37:15 +00:00
context['pending_count'] = OrganizationRequest.objects.filter(state='P', organization=self.object).count()
2020-01-21 06:35:58 +00:00
return context
2020-12-30 02:29:50 +00:00
class OrganizationUsers(QueryStringSortMixin, OrganizationDetailView):
2020-01-21 06:35:58 +00:00
template_name = 'organization/users.html'
2020-12-30 02:29:50 +00:00
all_sorts = frozenset(('points', 'problem_count', 'rating', 'performance_points'))
default_desc = all_sorts
default_sort = '-performance_points'
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.object.name
context['partial'] = True
context['is_admin'] = self.can_edit_organization()
context['kick_url'] = reverse('organization_user_kick', args=[self.object.id, self.object.slug])
2020-12-30 02:29:50 +00:00
context['users'] = ranker(
self.get_object().members.filter(is_unlisted=False).order_by(self.order, 'id').select_related('user') \
.only('display_rank', 'user__username', 'points', 'rating', 'performance_points', 'problem_count')
)
context['first_page_href'] = '.'
context.update(self.get_sort_context())
2020-01-21 06:35:58 +00:00
return context
class OrganizationMembershipChange(LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View):
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():
return generic_message(request, _('Joining organization'), _('You are already in the organization.'))
if not org.is_open:
return generic_message(request, _('Joining organization'), _('This organization is not open.'))
max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT
if profile.organizations.filter(is_open=True).count() >= max_orgs:
return generic_message(
request, _('Joining organization'),
_('You may not be part of more than {count} public organizations.').format(count=max_orgs),
)
profile.organizations.add(org)
profile.save()
cache.delete(make_template_fragment_key('org_member_count', (org.id,)))
class LeaveOrganization(OrganizationMembershipChange):
def handle(self, request, org, profile):
if not profile.organizations.filter(id=org.id).exists():
return generic_message(request, _('Leaving organization'), _('You are not in "%s".') % org.short_name)
profile.organizations.remove(org)
cache.delete(make_template_fragment_key('org_member_count', (org.id,)))
class OrganizationRequestForm(Form):
reason = forms.CharField(widget=forms.Textarea)
class RequestJoinOrganization(LoginRequiredMixin, SingleObjectMixin, FormView):
model = Organization
slug_field = 'key'
slug_url_kwarg = 'key'
template_name = 'organization/requests/request.html'
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()
context['title'] = _('Request to join %s') % self.object.name
return context
def form_valid(self, form):
request = OrganizationRequest()
request.organization = self.get_object()
request.user = self.request.profile
request.reason = form.cleaned_data['reason']
request.state = 'P'
request.save()
return HttpResponseRedirect(reverse('request_organization_detail', args=(
request.organization.id, request.organization.slug, request.id,
)))
class OrganizationRequestDetail(LoginRequiredMixin, TitleMixin, DetailView):
model = OrganizationRequest
template_name = 'organization/requests/detail.html'
title = gettext_lazy('Join request detail')
pk_url_kwarg = 'rpk'
def get_object(self, queryset=None):
object = super(OrganizationRequestDetail, self).get_object(queryset)
profile = self.request.profile
if object.user_id != profile.id and not object.organization.admins.filter(id=profile.id).exists():
raise PermissionDenied()
return object
OrganizationRequestFormSet = modelformset_factory(OrganizationRequest, extra=0, fields=('state',), can_delete=True)
2021-10-10 22:37:15 +00:00
class OrganizationRequestBaseView(TitleMixin, LoginRequiredMixin, SingleObjectTemplateResponseMixin, SingleObjectMixin, View):
2020-01-21 06:35:58 +00:00
model = Organization
slug_field = 'key'
slug_url_kwarg = 'key'
tab = None
def get_object(self, queryset=None):
organization = super(OrganizationRequestBaseView, self).get_object(queryset)
if not (organization.admins.filter(id=self.request.profile.id).exists() or
organization.registrant_id == self.request.profile.id):
raise PermissionDenied()
return organization
2021-10-10 22:37:15 +00:00
def get_content_title(self):
href = reverse('organization_home', args=[self.object.id, self.object.slug])
return mark_safe(f'Manage join requests for <a href="{href}">{self.object.name}</a>')
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super(OrganizationRequestBaseView, self).get_context_data(**kwargs)
context['title'] = _('Managing join requests for %s') % self.object.name
context['tab'] = self.tab
return context
class OrganizationRequestView(OrganizationRequestBaseView):
template_name = 'organization/requests/pending.html'
tab = 'pending'
def get_context_data(self, **kwargs):
context = super(OrganizationRequestView, self).get_context_data(**kwargs)
context['formset'] = self.formset
return context
def get(self, request, *args, **kwargs):
self.object = self.get_object()
self.formset = OrganizationRequestFormSet(
queryset=OrganizationRequest.objects.filter(state='P', organization=self.object),
)
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)
to_approve = sum(form.cleaned_data['state'] == 'A' for form in formset.forms if form not in deleted_set)
can_add = organization.slots - organization.members.count()
if to_approve > can_add:
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))
approved, rejected = 0, 0
for obj in formset.save():
if obj.state == 'A':
obj.user.organizations.add(obj.organization)
approved += 1
elif obj.state == 'R':
rejected += 1
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,)))
return HttpResponseRedirect(request.get_full_path())
return self.render_to_response(self.get_context_data(object=organization))
put = post
class OrganizationRequestLog(OrganizationRequestBaseView):
states = ('A', 'R')
tab = 'log'
template_name = 'organization/requests/log.html'
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)
context['requests'] = self.object.requests.filter(state__in=self.states)
return context
class EditOrganization(LoginRequiredMixin, TitleMixin, OrganizationMixin, UpdateView):
template_name = 'organization/edit.html'
model = Organization
form_class = EditOrganizationForm
def get_title(self):
return _('Editing %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):
with transaction.atomic(), revisions.create_revision():
revisions.set_comment(_('Edited from site'))
revisions.set_user(self.request.user)
return super(EditOrganization, self).form_valid(form)
def dispatch(self, request, *args, **kwargs):
try:
return super(EditOrganization, self).dispatch(request, *args, **kwargs)
except PermissionDenied:
return generic_message(request, _("Can't edit organization"),
_('You are not allowed to edit this organization.'), status=403)
class KickUserWidgetView(LoginRequiredMixin, OrganizationMixin, SingleObjectMixin, View):
def post(self, request, *args, **kwargs):
organization = self.get_object()
if not self.can_edit_organization(organization):
return generic_message(request, _("Can't edit organization"),
_('You are not allowed to kick people from this organization.'), status=403)
try:
user = Profile.objects.get(id=request.POST.get('user', None))
except Profile.DoesNotExist:
return generic_message(request, _("Can't kick user"),
_('The user you are trying to kick does not exist!'), status=400)
if not organization.members.filter(id=user.id).exists():
return generic_message(request, _("Can't kick user"),
_('The user you are trying to kick is not in organization: %s.') %
organization.name, status=400)
organization.members.remove(user)
return HttpResponseRedirect(organization.get_users_url())