325 lines
14 KiB
Python
325 lines
14 KiB
Python
|
import itertools
|
||
|
import json
|
||
|
from datetime import datetime
|
||
|
from operator import itemgetter
|
||
|
|
||
|
from django.conf import settings
|
||
|
from django.contrib.auth import logout as auth_logout
|
||
|
from django.contrib.auth.decorators import login_required
|
||
|
from django.contrib.auth.models import Permission
|
||
|
from django.contrib.auth.views import redirect_to_login
|
||
|
from django.contrib.contenttypes.models import ContentType
|
||
|
from django.db import transaction
|
||
|
from django.db.models import Count, Max, Min
|
||
|
from django.http import Http404, HttpResponseRedirect, JsonResponse
|
||
|
from django.shortcuts import get_object_or_404, render
|
||
|
from django.urls import reverse
|
||
|
from django.utils import timezone
|
||
|
from django.utils.formats import date_format
|
||
|
from django.utils.functional import cached_property
|
||
|
from django.utils.safestring import mark_safe
|
||
|
from django.utils.translation import gettext as _, gettext_lazy
|
||
|
from django.views.generic import DetailView, ListView, TemplateView
|
||
|
from reversion import revisions
|
||
|
|
||
|
from judge.forms import ProfileForm, newsletter_id
|
||
|
from judge.models import Profile, Rating, Submission
|
||
|
from judge.performance_points import get_pp_breakdown
|
||
|
from judge.ratings import rating_class, rating_progress
|
||
|
from judge.utils.problems import contest_completed_ids, user_completed_ids
|
||
|
from judge.utils.ranker import ranker
|
||
|
from judge.utils.subscription import Subscription
|
||
|
from judge.utils.unicode import utf8text
|
||
|
from judge.utils.views import DiggPaginatorMixin, QueryStringSortMixin, TitleMixin, generic_message
|
||
|
from .contests import ContestRanking
|
||
|
|
||
|
__all__ = ['UserPage', 'UserAboutPage', 'UserProblemsPage', 'users', 'edit_profile']
|
||
|
|
||
|
|
||
|
def remap_keys(iterable, mapping):
|
||
|
return [dict((mapping.get(k, k), v) for k, v in item.items()) for item in iterable]
|
||
|
|
||
|
|
||
|
class UserMixin(object):
|
||
|
model = Profile
|
||
|
slug_field = 'user__username'
|
||
|
slug_url_kwarg = 'user'
|
||
|
context_object_name = 'user'
|
||
|
|
||
|
def render_to_response(self, context, **response_kwargs):
|
||
|
return super(UserMixin, self).render_to_response(context, **response_kwargs)
|
||
|
|
||
|
|
||
|
class UserPage(TitleMixin, UserMixin, DetailView):
|
||
|
template_name = 'user/user-base.html'
|
||
|
|
||
|
def get_object(self, queryset=None):
|
||
|
if self.kwargs.get(self.slug_url_kwarg, None) is None:
|
||
|
return self.request.profile
|
||
|
return super(UserPage, self).get_object(queryset)
|
||
|
|
||
|
def dispatch(self, request, *args, **kwargs):
|
||
|
if self.kwargs.get(self.slug_url_kwarg, None) is None:
|
||
|
if not self.request.user.is_authenticated:
|
||
|
return redirect_to_login(self.request.get_full_path())
|
||
|
try:
|
||
|
return super(UserPage, self).dispatch(request, *args, **kwargs)
|
||
|
except Http404:
|
||
|
return generic_message(request, _('No such user'), _('No user handle "%s".') %
|
||
|
self.kwargs.get(self.slug_url_kwarg, None))
|
||
|
|
||
|
def get_title(self):
|
||
|
return (_('My account') if self.request.user == self.object.user else
|
||
|
_('User %s') % self.object.user.username)
|
||
|
|
||
|
# TODO: the same code exists in problem.py, maybe move to problems.py?
|
||
|
@cached_property
|
||
|
def profile(self):
|
||
|
if not self.request.user.is_authenticated:
|
||
|
return None
|
||
|
return self.request.profile
|
||
|
|
||
|
@cached_property
|
||
|
def in_contest(self):
|
||
|
return self.profile is not None and self.profile.current_contest is not None
|
||
|
|
||
|
def get_completed_problems(self):
|
||
|
if self.in_contest:
|
||
|
return contest_completed_ids(self.profile.current_contest)
|
||
|
else:
|
||
|
return user_completed_ids(self.profile) if self.profile is not None else ()
|
||
|
|
||
|
def get_context_data(self, **kwargs):
|
||
|
context = super(UserPage, self).get_context_data(**kwargs)
|
||
|
|
||
|
context['hide_solved'] = int(self.hide_solved)
|
||
|
context['authored'] = self.object.authored_problems.filter(is_public=True, is_organization_private=False) \
|
||
|
.order_by('code')
|
||
|
rating = self.object.ratings.order_by('-contest__end_time')[:1]
|
||
|
context['rating'] = rating[0] if rating else None
|
||
|
|
||
|
context['rank'] = Profile.objects.filter(
|
||
|
is_unlisted=False, performance_points__gt=self.object.performance_points,
|
||
|
).count() + 1
|
||
|
|
||
|
if rating:
|
||
|
context['rating_rank'] = Profile.objects.filter(
|
||
|
is_unlisted=False, rating__gt=self.object.rating,
|
||
|
).count() + 1
|
||
|
context['rated_users'] = Profile.objects.filter(is_unlisted=False, rating__isnull=False).count()
|
||
|
context.update(self.object.ratings.aggregate(min_rating=Min('rating'), max_rating=Max('rating'),
|
||
|
contests=Count('contest')))
|
||
|
return context
|
||
|
|
||
|
def get(self, request, *args, **kwargs):
|
||
|
self.hide_solved = request.GET.get('hide_solved') == '1' if 'hide_solved' in request.GET else False
|
||
|
return super(UserPage, self).get(request, *args, **kwargs)
|
||
|
|
||
|
|
||
|
EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||
|
|
||
|
|
||
|
class UserAboutPage(UserPage):
|
||
|
template_name = 'user/user-about.html'
|
||
|
|
||
|
def get_context_data(self, **kwargs):
|
||
|
context = super(UserAboutPage, self).get_context_data(**kwargs)
|
||
|
ratings = context['ratings'] = self.object.ratings.order_by('-contest__end_time').select_related('contest') \
|
||
|
.defer('contest__description')
|
||
|
|
||
|
context['rating_data'] = mark_safe(json.dumps([{
|
||
|
'label': rating.contest.name,
|
||
|
'rating': rating.rating,
|
||
|
'ranking': rating.rank,
|
||
|
'link': reverse('contest_ranking', args=(rating.contest.key,)),
|
||
|
'timestamp': (rating.contest.end_time - EPOCH).total_seconds() * 1000,
|
||
|
'date': date_format(rating.contest.end_time, _('M j, Y, G:i')),
|
||
|
'class': rating_class(rating.rating),
|
||
|
'height': '%.3fem' % rating_progress(rating.rating),
|
||
|
} for rating in ratings]))
|
||
|
|
||
|
if ratings:
|
||
|
user_data = self.object.ratings.aggregate(Min('rating'), Max('rating'))
|
||
|
global_data = Rating.objects.aggregate(Min('rating'), Max('rating'))
|
||
|
min_ever, max_ever = global_data['rating__min'], global_data['rating__max']
|
||
|
min_user, max_user = user_data['rating__min'], user_data['rating__max']
|
||
|
delta = max_user - min_user
|
||
|
ratio = (max_ever - max_user) / (max_ever - min_ever) if max_ever != min_ever else 1.0
|
||
|
context['max_graph'] = max_user + ratio * delta
|
||
|
context['min_graph'] = min_user + ratio * delta - delta
|
||
|
return context
|
||
|
|
||
|
|
||
|
class UserProblemsPage(UserPage):
|
||
|
template_name = 'user/user-problems.html'
|
||
|
|
||
|
def get_context_data(self, **kwargs):
|
||
|
context = super(UserProblemsPage, self).get_context_data(**kwargs)
|
||
|
|
||
|
result = Submission.objects.filter(user=self.object, points__gt=0, problem__is_public=True,
|
||
|
problem__is_organization_private=False) \
|
||
|
.exclude(problem__in=self.get_completed_problems() if self.hide_solved else []) \
|
||
|
.values('problem__id', 'problem__code', 'problem__name', 'problem__points', 'problem__group__full_name') \
|
||
|
.distinct().annotate(points=Max('points')).order_by('problem__group__full_name', 'problem__code')
|
||
|
|
||
|
def process_group(group, problems_iter):
|
||
|
problems = list(problems_iter)
|
||
|
points = sum(map(itemgetter('points'), problems))
|
||
|
return {'name': group, 'problems': problems, 'points': points}
|
||
|
|
||
|
context['best_submissions'] = [
|
||
|
process_group(group, problems) for group, problems in itertools.groupby(
|
||
|
remap_keys(result, {
|
||
|
'problem__code': 'code', 'problem__name': 'name', 'problem__points': 'total',
|
||
|
'problem__group__full_name': 'group',
|
||
|
}), itemgetter('group'))
|
||
|
]
|
||
|
breakdown, has_more = get_pp_breakdown(self.object, start=0, end=10)
|
||
|
context['pp_breakdown'] = breakdown
|
||
|
context['pp_has_more'] = has_more
|
||
|
|
||
|
return context
|
||
|
|
||
|
|
||
|
class UserPerformancePointsAjax(UserProblemsPage):
|
||
|
template_name = 'user/pp-table-body.html'
|
||
|
|
||
|
def get_context_data(self, **kwargs):
|
||
|
context = super(UserPerformancePointsAjax, self).get_context_data(**kwargs)
|
||
|
try:
|
||
|
start = int(self.request.GET.get('start', 0))
|
||
|
end = int(self.request.GET.get('end', settings.DMOJ_PP_ENTRIES))
|
||
|
if start < 0 or end < 0 or start > end:
|
||
|
raise ValueError
|
||
|
except ValueError:
|
||
|
start, end = 0, 100
|
||
|
breakdown, self.has_more = get_pp_breakdown(self.object, start=start, end=end)
|
||
|
context['pp_breakdown'] = breakdown
|
||
|
return context
|
||
|
|
||
|
def get(self, request, *args, **kwargs):
|
||
|
httpresp = super(UserPerformancePointsAjax, self).get(request, *args, **kwargs)
|
||
|
httpresp.render()
|
||
|
|
||
|
return JsonResponse({
|
||
|
'results': utf8text(httpresp.content),
|
||
|
'has_more': self.has_more,
|
||
|
})
|
||
|
|
||
|
|
||
|
@login_required
|
||
|
def edit_profile(request):
|
||
|
profile = Profile.objects.get(user=request.user)
|
||
|
if profile.mute:
|
||
|
raise Http404()
|
||
|
if request.method == 'POST':
|
||
|
form = ProfileForm(request.POST, instance=profile, user=request.user)
|
||
|
if form.is_valid():
|
||
|
with transaction.atomic(), revisions.create_revision():
|
||
|
form.save()
|
||
|
revisions.set_user(request.user)
|
||
|
revisions.set_comment(_('Updated on site'))
|
||
|
|
||
|
if newsletter_id is not None:
|
||
|
try:
|
||
|
subscription = Subscription.objects.get(user=request.user, newsletter_id=newsletter_id)
|
||
|
except Subscription.DoesNotExist:
|
||
|
if form.cleaned_data['newsletter']:
|
||
|
Subscription(user=request.user, newsletter_id=newsletter_id, subscribed=True).save()
|
||
|
else:
|
||
|
if subscription.subscribed != form.cleaned_data['newsletter']:
|
||
|
subscription.update(('unsubscribe', 'subscribe')[form.cleaned_data['newsletter']])
|
||
|
|
||
|
perm = Permission.objects.get(codename='test_site', content_type=ContentType.objects.get_for_model(Profile))
|
||
|
if form.cleaned_data['test_site']:
|
||
|
request.user.user_permissions.add(perm)
|
||
|
else:
|
||
|
request.user.user_permissions.remove(perm)
|
||
|
|
||
|
return HttpResponseRedirect(request.path)
|
||
|
else:
|
||
|
form = ProfileForm(instance=profile, user=request.user)
|
||
|
if newsletter_id is not None:
|
||
|
try:
|
||
|
subscription = Subscription.objects.get(user=request.user, newsletter_id=newsletter_id)
|
||
|
except Subscription.DoesNotExist:
|
||
|
form.fields['newsletter'].initial = False
|
||
|
else:
|
||
|
form.fields['newsletter'].initial = subscription.subscribed
|
||
|
form.fields['test_site'].initial = request.user.has_perm('judge.test_site')
|
||
|
|
||
|
tzmap = settings.TIMEZONE_MAP
|
||
|
return render(request, 'user/edit-profile.html', {
|
||
|
'require_staff_2fa': settings.DMOJ_REQUIRE_STAFF_2FA,
|
||
|
'form': form, 'title': _('Edit profile'), 'profile': profile,
|
||
|
'has_math_config': bool(settings.MATHOID_URL),
|
||
|
'TIMEZONE_MAP': tzmap or 'http://momentjs.com/static/img/world.png',
|
||
|
'TIMEZONE_BG': settings.TIMEZONE_BG if tzmap else '#4E7CAD',
|
||
|
})
|
||
|
|
||
|
|
||
|
class UserList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ListView):
|
||
|
model = Profile
|
||
|
title = gettext_lazy('Leaderboard')
|
||
|
context_object_name = 'users'
|
||
|
template_name = 'user/list.html'
|
||
|
paginate_by = 100
|
||
|
all_sorts = frozenset(('points', 'problem_count', 'rating', 'performance_points'))
|
||
|
default_desc = all_sorts
|
||
|
default_sort = '-performance_points'
|
||
|
|
||
|
def get_queryset(self):
|
||
|
return (Profile.objects.filter(is_unlisted=False).order_by(self.order, 'id').select_related('user')
|
||
|
.only('display_rank', 'user__username', 'points', 'rating', 'performance_points',
|
||
|
'problem_count'))
|
||
|
|
||
|
def get_context_data(self, **kwargs):
|
||
|
context = super(UserList, self).get_context_data(**kwargs)
|
||
|
context['users'] = ranker(context['users'], rank=self.paginate_by * (context['page_obj'].number - 1))
|
||
|
context['first_page_href'] = '.'
|
||
|
context.update(self.get_sort_context())
|
||
|
context.update(self.get_sort_paginate_context())
|
||
|
return context
|
||
|
|
||
|
|
||
|
user_list_view = UserList.as_view()
|
||
|
|
||
|
|
||
|
class FixedContestRanking(ContestRanking):
|
||
|
contest = None
|
||
|
|
||
|
def get_object(self, queryset=None):
|
||
|
return self.contest
|
||
|
|
||
|
|
||
|
def users(request):
|
||
|
if request.user.is_authenticated:
|
||
|
participation = request.profile.current_contest
|
||
|
if participation is not None:
|
||
|
contest = participation.contest
|
||
|
return FixedContestRanking.as_view(contest=contest)(request, contest=contest.key)
|
||
|
return user_list_view(request)
|
||
|
|
||
|
|
||
|
def user_ranking_redirect(request):
|
||
|
try:
|
||
|
username = request.GET['handle']
|
||
|
except KeyError:
|
||
|
raise Http404()
|
||
|
user = get_object_or_404(Profile, user__username=username)
|
||
|
rank = Profile.objects.filter(is_unlisted=False, performance_points__gt=user.performance_points).count()
|
||
|
rank += Profile.objects.filter(
|
||
|
is_unlisted=False, performance_points__exact=user.performance_points, id__lt=user.id,
|
||
|
).count()
|
||
|
page = rank // UserList.paginate_by
|
||
|
return HttpResponseRedirect('%s%s#!%s' % (reverse('user_list'), '?page=%d' % (page + 1) if page else '', username))
|
||
|
|
||
|
|
||
|
class UserLogoutView(TitleMixin, TemplateView):
|
||
|
template_name = 'registration/logout.html'
|
||
|
title = 'You have been successfully logged out.'
|
||
|
|
||
|
def post(self, request, *args, **kwargs):
|
||
|
auth_logout(request)
|
||
|
return HttpResponseRedirect(request.get_full_path())
|