NDOJ/judge/views/user.py

605 lines
20 KiB
Python
Raw Normal View History

2020-01-21 06:35:58 +00:00
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
2020-12-26 01:02:14 +00:00
from django.db.models.fields import DateField
from django.db.models.functions import Cast, ExtractYear
2022-11-17 21:21:32 +00:00
from judge.models.bookmark import MakeBookMark
2021-07-28 22:58:42 +00:00
from django.forms import Form
2022-05-14 17:57:27 +00:00
from django.http import (
Http404,
HttpResponseRedirect,
JsonResponse,
HttpResponseForbidden,
HttpResponseBadRequest,
HttpResponse,
)
2020-01-21 06:35:58 +00:00
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
2021-07-28 22:58:42 +00:00
from django.views import View
2020-01-21 06:35:58 +00:00
from django.views.generic import DetailView, ListView, TemplateView
2021-07-28 22:58:42 +00:00
from django.template.loader import render_to_string
2020-01-21 06:35:58 +00:00
from reversion import revisions
2024-04-27 03:51:16 +00:00
from judge.forms import UserForm, ProfileForm, ProfileInfoForm
2024-05-29 05:14:42 +00:00
from judge.models import (
Profile,
Rating,
Submission,
Friend,
ProfileInfo,
BlogPost,
Problem,
Contest,
Solution,
)
2020-01-21 06:35:58 +00:00
from judge.performance_points import get_pp_breakdown
from judge.ratings import rating_class, rating_progress
2021-07-28 22:58:42 +00:00
from judge.tasks import import_users
2020-01-21 06:35:58 +00:00
from judge.utils.problems import contest_completed_ids, user_completed_ids
from judge.utils.ranker import ranker
from judge.utils.unicode import utf8text
2024-05-22 04:09:22 +00:00
from judge.utils.users import (
get_rating_rank,
get_points_rank,
get_awards,
get_contest_ratings,
)
2022-05-14 17:57:27 +00:00
from judge.utils.views import (
QueryStringSortMixin,
TitleMixin,
generic_message,
SingleObjectFormView,
2024-05-29 05:14:42 +00:00
DiggPaginatorMixin,
2022-05-14 17:57:27 +00:00
)
2023-02-18 21:12:33 +00:00
from judge.utils.infinite_paginator import InfinitePaginationMixin
2024-05-29 05:14:42 +00:00
from judge.views.problem import ProblemList
2020-01-21 06:35:58 +00:00
from .contests import ContestRanking
2024-05-29 05:14:42 +00:00
2022-11-17 22:11:47 +00:00
__all__ = [
"UserPage",
"UserAboutPage",
"UserProblemsPage",
"UserBookMarkPage",
"users",
"edit_profile",
]
2020-01-21 06:35:58 +00:00
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
2022-05-14 17:57:27 +00:00
slug_field = "user__username"
slug_url_kwarg = "user"
context_object_name = "user"
2020-01-21 06:35:58 +00:00
def render_to_response(self, context, **response_kwargs):
return super(UserMixin, self).render_to_response(context, **response_kwargs)
class UserPage(TitleMixin, UserMixin, DetailView):
2022-05-14 17:57:27 +00:00
template_name = "user/user-base.html"
2020-01-21 06:35:58 +00:00
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:
2022-05-14 17:57:27 +00:00
return generic_message(
request,
_("No such user"),
_('No user handle "%s".') % self.kwargs.get(self.slug_url_kwarg, None),
)
2020-01-21 06:35:58 +00:00
def get_title(self):
2022-05-14 17:57:27 +00:00
return (
_("My account")
2023-11-24 03:52:19 +00:00
if self.request.profile == self.object
else _("User %s") % self.object.username
2022-05-14 17:57:27 +00:00
)
2020-01-21 06:35:58 +00:00
2021-07-23 03:54:48 +00:00
def get_content_title(self):
2023-11-24 03:52:19 +00:00
username = self.object.username
2021-07-23 03:54:48 +00:00
css_class = self.object.css_class
return mark_safe(f'<span class="{css_class}">{username}</span>')
2020-01-21 06:35:58 +00:00
# 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):
2022-05-14 17:57:27 +00:00
return (
self.profile is not None
and self.profile.current_contest is not None
2022-01-10 11:13:46 +00:00
and self.request.in_contest_mode
2022-05-14 17:57:27 +00:00
)
2020-01-21 06:35:58 +00:00
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)
2022-05-14 17:57:27 +00:00
context["followed"] = Friend.is_friend(self.request.profile, self.object)
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
2024-05-22 04:09:22 +00:00
context["points_rank"] = get_points_rank(self.object)
2020-01-21 06:35:58 +00:00
if rating:
2024-05-22 04:09:22 +00:00
context["rating_rank"] = get_rating_rank(self.object)
2022-05-14 17:57:27 +00:00
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"),
)
)
2020-01-21 06:35:58 +00:00
return context
def get(self, request, *args, **kwargs):
2022-05-14 17:57:27 +00:00
self.hide_solved = (
request.GET.get("hide_solved") == "1"
if "hide_solved" in request.GET
else False
)
2020-01-21 06:35:58 +00:00
return super(UserPage, self).get(request, *args, **kwargs)
EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
class UserAboutPage(UserPage):
2022-05-14 17:57:27 +00:00
template_name = "user/user-about.html"
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super(UserAboutPage, self).get_context_data(**kwargs)
2024-05-22 04:09:22 +00:00
ratings = context["ratings"] = get_contest_ratings(self.object)
2022-05-14 17:57:27 +00:00
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(
timezone.localtime(rating.contest.end_time),
_("M j, Y, G:i"),
),
"class": rating_class(rating.rating),
"height": "%.3fem" % rating_progress(rating.rating),
}
for rating in ratings
]
)
)
2024-05-22 04:09:22 +00:00
context["awards"] = get_awards(self.object)
2021-07-23 03:54:48 +00:00
2020-01-21 06:35:58 +00:00
if ratings:
2022-05-14 17:57:27 +00:00
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"]
2020-01-21 06:35:58 +00:00
delta = max_user - min_user
2022-05-14 17:57:27 +00:00
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
2020-12-26 01:02:14 +00:00
submissions = (
2022-05-14 17:57:27 +00:00
self.object.submission_set.annotate(date_only=Cast("date", DateField()))
.values("date_only")
.annotate(cnt=Count("id"))
)
context["submission_data"] = mark_safe(
json.dumps(
{
date_counts["date_only"].isoformat(): date_counts["cnt"]
for date_counts in submissions
}
)
)
context["submission_metadata"] = mark_safe(
json.dumps(
{
"min_year": (
self.object.submission_set.annotate(
year_only=ExtractYear("date")
).aggregate(min_year=Min("year_only"))["min_year"]
),
}
)
2020-12-26 01:02:14 +00:00
)
2020-01-21 06:35:58 +00:00
return context
class UserProblemsPage(UserPage):
2022-05-14 17:57:27 +00:00
template_name = "user/user-problems.html"
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super(UserProblemsPage, self).get_context_data(**kwargs)
2022-05-14 17:57:27 +00:00
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")
)
2020-01-21 06:35:58 +00:00
def process_group(group, problems_iter):
problems = list(problems_iter)
2022-05-14 17:57:27 +00:00
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"),
)
2020-01-21 06:35:58 +00:00
]
breakdown, has_more = get_pp_breakdown(self.object, start=0, end=10)
2022-05-14 17:57:27 +00:00
context["pp_breakdown"] = breakdown
context["pp_has_more"] = has_more
2020-01-21 06:35:58 +00:00
return context
2022-11-17 22:11:47 +00:00
2024-05-29 05:14:42 +00:00
class UserBookMarkPage(DiggPaginatorMixin, ListView, UserPage):
2022-11-17 21:21:32 +00:00
template_name = "user/user-bookmarks.html"
2024-05-29 05:14:42 +00:00
context_object_name = "bookmarks"
paginate_by = 10
def get(self, request, *args, **kwargs):
self.current_tab = self.request.GET.get("tab", "problems")
self.user = self.object = self.get_object()
return super(UserBookMarkPage, self).get(request, *args, **kwargs)
def get_queryset(self):
model = None
if self.current_tab == "posts":
model = BlogPost
elif self.current_tab == "contests":
model = Contest
elif self.current_tab == "editorials":
model = Solution
else:
model = Problem
q = MakeBookMark.objects.filter(user=self.user).select_related("bookmark")
q = q.filter(bookmark__content_type=ContentType.objects.get_for_model(model))
object_ids = q.values_list("bookmark__object_id", flat=True)
res = model.objects.filter(id__in=object_ids)
if self.current_tab == "contests":
res = res.prefetch_related("organizations", "tags")
elif self.current_tab == "editorials":
res = res.select_related("problem")
return res
2022-11-17 21:21:32 +00:00
def get_context_data(self, **kwargs):
context = super(UserBookMarkPage, self).get_context_data(**kwargs)
2024-05-29 05:14:42 +00:00
context["current_tab"] = self.current_tab
context["user"] = self.user
context["page_prefix"] = (
self.request.path + "?tab=" + self.current_tab + "&page="
)
context["first_page_href"] = self.request.path
2022-11-17 22:11:47 +00:00
2022-11-17 21:21:32 +00:00
return context
2020-01-21 06:35:58 +00:00
class UserPerformancePointsAjax(UserProblemsPage):
2022-05-14 17:57:27 +00:00
template_name = "user/pp-table-body.html"
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super(UserPerformancePointsAjax, self).get_context_data(**kwargs)
try:
2022-05-14 17:57:27 +00:00
start = int(self.request.GET.get("start", 0))
end = int(self.request.GET.get("end", settings.DMOJ_PP_ENTRIES))
2020-01-21 06:35:58 +00:00
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)
2022-05-14 17:57:27 +00:00
context["pp_breakdown"] = breakdown
2020-01-21 06:35:58 +00:00
return context
def get(self, request, *args, **kwargs):
httpresp = super(UserPerformancePointsAjax, self).get(request, *args, **kwargs)
httpresp.render()
2022-05-14 17:57:27 +00:00
return JsonResponse(
{
"results": utf8text(httpresp.content),
"has_more": self.has_more,
}
)
2020-01-21 06:35:58 +00:00
@login_required
def edit_profile(request):
2023-08-24 03:14:09 +00:00
profile = request.profile
2024-04-27 03:51:16 +00:00
profile_info, created = ProfileInfo.objects.get_or_create(profile=profile)
2022-05-14 17:57:27 +00:00
if request.method == "POST":
2022-10-15 16:23:50 +00:00
form_user = UserForm(request.POST, instance=request.user)
2023-08-24 03:14:09 +00:00
form = ProfileForm(
request.POST, request.FILES, instance=profile, user=request.user
)
2024-04-27 03:51:16 +00:00
form_info = ProfileInfoForm(request.POST, instance=profile_info)
2022-10-15 16:23:50 +00:00
if form_user.is_valid() and form.is_valid():
2024-02-19 23:00:44 +00:00
with revisions.create_revision():
2022-10-15 16:23:50 +00:00
form_user.save()
2020-01-21 06:35:58 +00:00
form.save()
2024-04-27 03:51:16 +00:00
form_info.save()
2020-01-21 06:35:58 +00:00
revisions.set_user(request.user)
2022-05-14 17:57:27 +00:00
revisions.set_comment(_("Updated on site"))
2020-01-21 06:35:58 +00:00
return HttpResponseRedirect(request.path)
else:
2022-10-15 16:23:50 +00:00
form_user = UserForm(instance=request.user)
2020-01-21 06:35:58 +00:00
form = ProfileForm(instance=profile, user=request.user)
2024-04-27 03:51:16 +00:00
form_info = ProfileInfoForm(instance=profile_info)
2020-01-21 06:35:58 +00:00
tzmap = settings.TIMEZONE_MAP
2022-05-14 17:57:27 +00:00
return render(
request,
2022-10-15 17:11:20 +00:00
"user/edit-profile.html",
2022-05-14 17:57:27 +00:00
{
2022-10-15 17:11:20 +00:00
"require_staff_2fa": settings.DMOJ_REQUIRE_STAFF_2FA,
"form_user": form_user,
2022-05-14 17:57:27 +00:00
"form": form,
2024-04-27 03:51:16 +00:00
"form_info": form_info,
2022-05-14 17:57:27 +00:00
"title": _("Edit profile"),
"profile": profile,
"TIMEZONE_MAP": tzmap or "http://momentjs.com/static/img/world.png",
"TIMEZONE_BG": settings.TIMEZONE_BG if tzmap else "#4E7CAD",
},
)
2020-01-21 06:35:58 +00:00
2023-02-18 21:12:33 +00:00
class UserList(QueryStringSortMixin, InfinitePaginationMixin, TitleMixin, ListView):
2020-01-21 06:35:58 +00:00
model = Profile
2022-05-14 17:57:27 +00:00
title = gettext_lazy("Leaderboard")
context_object_name = "users"
template_name = "user/list.html"
2020-01-21 06:35:58 +00:00
paginate_by = 100
2022-05-14 17:57:27 +00:00
all_sorts = frozenset(("points", "problem_count", "rating", "performance_points"))
2020-01-21 06:35:58 +00:00
default_desc = all_sorts
2022-05-14 17:57:27 +00:00
default_sort = "-performance_points"
2022-06-06 16:36:35 +00:00
filter_friend = False
2020-01-21 06:35:58 +00:00
2020-11-24 19:17:13 +00:00
def filter_friend_queryset(self, queryset):
2023-02-13 03:35:48 +00:00
friends = self.request.profile.get_friends()
ret = queryset.filter(id__in=friends)
2020-11-24 19:17:13 +00:00
return ret
2020-01-21 06:35:58 +00:00
def get_queryset(self):
2023-01-24 02:36:44 +00:00
queryset = (
2022-05-14 17:57:27 +00:00
Profile.objects.filter(is_unlisted=False)
.order_by(self.order, "id")
.only(
"display_rank",
"points",
"rating",
"performance_points",
"problem_count",
2024-02-23 23:07:34 +00:00
"about",
2022-05-14 17:57:27 +00:00
)
)
2023-01-24 02:36:44 +00:00
if self.request.organization:
queryset = queryset.filter(organizations=self.request.organization)
2022-05-14 17:57:27 +00:00
if (self.request.GET.get("friend") == "true") and self.request.profile:
2023-01-24 02:36:44 +00:00
queryset = self.filter_friend_queryset(queryset)
2022-06-06 16:36:35 +00:00
self.filter_friend = True
2023-01-24 02:36:44 +00:00
return queryset
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super(UserList, self).get_context_data(**kwargs)
2024-02-23 23:07:34 +00:00
Profile.prefetch_profile_cache([u.id for u in context["users"]])
2022-05-14 17:57:27 +00:00
context["users"] = ranker(
context["users"], rank=self.paginate_by * (context["page_obj"].number - 1)
)
context["first_page_href"] = "."
2022-06-06 16:36:35 +00:00
context["page_type"] = "friends" if self.filter_friend else "list"
2020-01-21 06:35:58 +00:00
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:
2022-01-10 11:13:46 +00:00
if request.in_contest_mode:
participation = request.profile.current_contest
2020-01-21 06:35:58 +00:00
contest = participation.contest
2022-05-14 17:57:27 +00:00
return FixedContestRanking.as_view(contest=contest)(
request, contest=contest.key
)
2020-01-21 06:35:58 +00:00
return user_list_view(request)
def user_ranking_redirect(request):
try:
2022-05-14 17:57:27 +00:00
username = request.GET["handle"]
2020-01-21 06:35:58 +00:00
except KeyError:
raise Http404()
user = get_object_or_404(Profile, user__username=username)
2022-05-14 17:57:27 +00:00
rank = Profile.objects.filter(
is_unlisted=False, performance_points__gt=user.performance_points
).count()
2020-01-21 06:35:58 +00:00
rank += Profile.objects.filter(
2022-05-14 17:57:27 +00:00
is_unlisted=False,
performance_points__exact=user.performance_points,
id__lt=user.id,
2020-01-21 06:35:58 +00:00
).count()
page = rank // UserList.paginate_by
2022-05-14 17:57:27 +00:00
return HttpResponseRedirect(
"%s%s#!%s"
% (reverse("user_list"), "?page=%d" % (page + 1) if page else "", username)
)
2020-01-21 06:35:58 +00:00
class UserLogoutView(TitleMixin, TemplateView):
2022-05-14 17:57:27 +00:00
template_name = "registration/logout.html"
title = "You have been successfully logged out."
2020-01-21 06:35:58 +00:00
def post(self, request, *args, **kwargs):
auth_logout(request)
return HttpResponseRedirect(request.get_full_path())
2021-07-28 22:58:42 +00:00
class ImportUsersView(TitleMixin, TemplateView):
2022-05-14 17:57:27 +00:00
template_name = "user/import/index.html"
title = _("Import Users")
2021-07-28 22:58:42 +00:00
def get(self, *args, **kwargs):
if self.request.user.is_superuser:
return super().get(self, *args, **kwargs)
return HttpResponseForbidden()
def import_users_post_file(request):
2022-05-14 17:57:27 +00:00
if not request.user.is_superuser or request.method != "POST":
2021-07-28 22:58:42 +00:00
return HttpResponseForbidden()
2022-05-14 17:57:27 +00:00
users = import_users.csv_to_dict(request.FILES["csv_file"])
2021-07-28 22:58:42 +00:00
if not users:
2022-05-14 17:57:27 +00:00
return JsonResponse(
{
"done": False,
"msg": "No valid row found. Make sure row containing username.",
}
)
table_html = render_to_string("user/import/table_csv.html", {"data": users})
return JsonResponse({"done": True, "html": table_html, "data": users})
2021-07-28 22:58:42 +00:00
def import_users_submit(request):
import json
2022-05-14 17:57:27 +00:00
if not request.user.is_superuser or request.method != "POST":
2021-07-28 22:58:42 +00:00
return HttpResponseForbidden()
2022-05-14 17:57:27 +00:00
users = json.loads(request.body)["users"]
2021-07-28 22:58:42 +00:00
log = import_users.import_users(users)
2022-05-14 17:57:27 +00:00
return JsonResponse({"msg": log})
2021-07-28 22:58:42 +00:00
def sample_import_users(request):
2022-05-14 17:57:27 +00:00
if not request.user.is_superuser or request.method != "GET":
2021-07-28 22:58:42 +00:00
return HttpResponseForbidden()
2022-05-14 17:57:27 +00:00
filename = "import_sample.csv"
content = ",".join(import_users.fields) + "\n" + ",".join(import_users.descriptions)
response = HttpResponse(content, content_type="text/plain")
response["Content-Disposition"] = "attachment; filename={0}".format(filename)
return response
2022-12-18 09:31:31 +00:00
def toggle_darkmode(request):
path = request.GET.get("next")
if not path:
return HttpResponseBadRequest()
request.session["darkmode"] = not request.session.get("darkmode", False)
2022-12-20 08:24:24 +00:00
return HttpResponseRedirect(path)
2024-04-12 04:56:58 +00:00
@login_required
def toggle_follow(request, user):
if request.method != "POST":
raise Http404()
profile_to_follow = get_object_or_404(Profile, user__username=user)
if request.profile.id == profile_to_follow.id:
raise Http404()
Friend.toggle_friend(request.profile, profile_to_follow)
return HttpResponseRedirect(reverse("user_page", args=(user,)))