Caching and refactors

This commit is contained in:
cuom1999 2024-04-13 17:02:54 -05:00
parent 67b06d7856
commit 8f1c8d6c96
33 changed files with 485 additions and 381 deletions

View file

@ -6,7 +6,7 @@ from django.utils.functional import lazy
from django.utils.translation import ugettext as _
from django.views.generic import ListView
from judge.comments import CommentedDetailView
from judge.views.comment import CommentedDetailView
from judge.views.pagevote import PageVoteDetailView
from judge.views.bookmark import BookMarkDetailView
from judge.models import (
@ -69,13 +69,18 @@ class HomeFeedView(FeedView):
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_queryset.filter(is_unlisted=False).order_by(
"-performance_points"
)[:10]
context["top_rated"] = (
profile_queryset.filter(is_unlisted=False)
.order_by("-rating")
.only("id", "rating")[:10]
)
context["top_scorer"] = (
profile_queryset.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"]])
return context
@ -90,7 +95,7 @@ class PostList(HomeFeedView):
queryset = (
BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now())
.order_by("-sticky", "-publish_on")
.prefetch_related("authors__user", "organizations")
.prefetch_related("organizations")
)
filter = Q(is_organization_private=False)
if self.request.user.is_authenticated:
@ -198,6 +203,6 @@ class PostView(TitleMixin, CommentedDetailView, PageVoteDetailView, BookMarkDeta
def get_object(self, queryset=None):
post = super(PostView, self).get_object(queryset)
if not post.can_see(self.request.user):
if not post.is_accessible_by(self.request.user):
raise Http404()
return post

View file

@ -8,12 +8,12 @@ from django.http import (
HttpResponseForbidden,
)
from django.utils.translation import gettext as _
from judge.models.bookmark import BookMark, MakeBookMark
from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin
from django.views.generic import View, ListView
from judge.models.bookmark import BookMark, MakeBookMark, dirty_bookmark
__all__ = [
"dobookmark_page",
@ -32,30 +32,31 @@ def bookmark_page(request, delta):
try:
bookmark_id = int(request.POST["id"])
bookmark_page = BookMark.objects.filter(id=bookmark_id)
bookmark = BookMark.objects.get(id=bookmark_id)
except ValueError:
return HttpResponseBadRequest()
else:
if not bookmark_page.exists():
raise Http404()
except BookMark.DoesNotExist:
raise Http404()
if delta == 0:
bookmarklist = MakeBookMark.objects.filter(
bookmark=bookmark_page.first(), user=request.profile
bookmark=bookmark, user=request.profile
)
if not bookmarklist.exists():
newbookmark = MakeBookMark(
bookmark=bookmark_page.first(),
bookmark=bookmark,
user=request.profile,
)
newbookmark.save()
else:
bookmarklist = MakeBookMark.objects.filter(
bookmark=bookmark_page.first(), user=request.profile
bookmark=bookmark, user=request.profile
)
if bookmarklist.exists():
bookmarklist.delete()
dirty_bookmark(bookmark, request.profile)
return HttpResponse("success", content_type="text/plain")

View file

@ -1,36 +1,45 @@
from django.conf import settings
import json
from django import forms
from django.conf import settings
from django.contrib.auth.context_processors import PermWrapper
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib.auth.context_processors import PermWrapper
from django.core.exceptions import PermissionDenied
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import IntegrityError
from django.db.models import Q, F, Count, FilteredRelation
from django.db.models import Count, F, FilteredRelation, Q
from django.db.models.expressions import Value
from django.db.models.functions import Coalesce
from django.db.models.expressions import F, Value
from django.forms.models import ModelForm
from django.forms import ModelForm
from django.http import (
Http404,
HttpResponse,
HttpResponseBadRequest,
HttpResponseForbidden,
HttpResponseNotFound,
HttpResponseRedirect,
)
from django.shortcuts import get_object_or_404, render
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.views.decorators.http import require_POST
from django.views.generic import DetailView, UpdateView
from django.urls import reverse_lazy
from reversion import revisions
from reversion.models import Version
from django.conf import settings
from django.views.generic import DetailView, UpdateView, View
from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin
from django_ratelimit.decorators import ratelimit
from django.contrib.contenttypes.models import ContentType
from judge.models import Comment, CommentVote, Notification, BlogPost
from reversion import revisions
from reversion.models import Revision, Version
from judge.jinja2.reference import get_user_from_text
from judge.models import BlogPost, Comment, CommentVote, Notification
from judge.models.notification import make_notification
from judge.models.comment import get_visible_comment_count
from judge.utils.views import TitleMixin
from judge.widgets import HeavyPreviewPageDownWidget
from judge.comments import add_mention_notifications
import json
__all__ = [
"upvote_comment",
@ -40,6 +49,18 @@ __all__ = [
"CommentEdit",
]
DEFAULT_OFFSET = 10
def _get_html_link_notification(comment):
return f'<a href="{comment.get_absolute_url()}">{comment.page_title}</a>'
def add_mention_notifications(comment):
users_mentioned = get_user_from_text(comment.body).exclude(id=comment.author.id)
link = _get_html_link_notification(comment)
make_notification(users_mentioned, "Mention", link, comment.author)
@ratelimit(key="user", rate=settings.RL_VOTE)
@login_required
@ -290,4 +311,204 @@ def comment_hide(request):
comment = get_object_or_404(Comment, id=comment_id)
comment.get_descendants(include_self=True).update(hidden=True)
get_visible_comment_count.dirty(comment.content_type, comment.object_id)
return HttpResponse("ok")
class CommentForm(ModelForm):
class Meta:
model = Comment
fields = ["body", "parent"]
widgets = {
"parent": forms.HiddenInput(),
}
if HeavyPreviewPageDownWidget is not None:
widgets["body"] = HeavyPreviewPageDownWidget(
preview=reverse_lazy("comment_preview"),
preview_timeout=1000,
hide_preview_button=True,
)
def __init__(self, request, *args, **kwargs):
self.request = request
super(CommentForm, self).__init__(*args, **kwargs)
self.fields["body"].widget.attrs.update({"placeholder": _("Comment body")})
def clean(self):
if self.request is not None and self.request.user.is_authenticated:
profile = self.request.profile
if profile.mute:
raise ValidationError(_("Your part is silent, little toad."))
elif (
not self.request.user.is_staff
and not profile.submission_set.filter(
points=F("problem__points")
).exists()
):
raise ValidationError(
_(
"You need to have solved at least one problem "
"before your voice can be heard."
)
)
return super(CommentForm, self).clean()
class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
comment_page = None
def is_comment_locked(self):
if self.request.user.has_perm("judge.override_comment_lock"):
return False
return (
self.request.in_contest
and self.request.participation.contest.use_clarifications
)
@method_decorator(ratelimit(key="user", rate=settings.RL_COMMENT))
@method_decorator(login_required)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if self.is_comment_locked():
return HttpResponseForbidden()
parent = request.POST.get("parent")
if parent:
try:
parent = int(parent)
except ValueError:
return HttpResponseNotFound()
else:
if not self.object.comments.filter(hidden=False, id=parent).exists():
return HttpResponseNotFound()
form = CommentForm(request, request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.author = request.profile
comment.linked_object = self.object
with revisions.create_revision():
revisions.set_user(request.user)
revisions.set_comment(_("Posted comment"))
comment.save()
# add notification for reply
comment_notif_link = _get_html_link_notification(comment)
if comment.parent and comment.parent.author != comment.author:
make_notification(
[comment.parent.author], "Reply", comment_notif_link, comment.author
)
# add notification for page authors
page_authors = comment.linked_object.authors.all()
make_notification(
page_authors, "Comment", comment_notif_link, comment.author
)
add_mention_notifications(comment)
get_visible_comment_count.dirty(comment.content_type, comment.object_id)
return HttpResponseRedirect(comment.get_absolute_url())
context = self.get_context_data(object=self.object, comment_form=form)
return self.render_to_response(context)
def get(self, request, *args, **kwargs):
target_comment = None
self.object = self.get_object()
if "comment-id" in request.GET:
try:
comment_id = int(request.GET["comment-id"])
comment_obj = Comment.objects.get(id=comment_id)
except (Comment.DoesNotExist, ValueError):
raise Http404
if comment_obj.linked_object != self.object:
raise Http404
target_comment = comment_obj.get_root()
return self.render_to_response(
self.get_context_data(
object=self.object,
target_comment=target_comment,
comment_form=CommentForm(request, initial={"parent": None}),
)
)
def _get_queryset(self, target_comment):
if target_comment:
queryset = target_comment.get_descendants(include_self=True)
queryset = (
queryset.select_related("author__user")
.filter(hidden=False)
.defer("author__about")
)
else:
queryset = self.object.comments
queryset = queryset.filter(parent=None, hidden=False)
queryset = (
queryset.select_related("author__user")
.defer("author__about")
.filter(hidden=False)
.annotate(
count_replies=Count("replies", distinct=True),
)[:DEFAULT_OFFSET]
)
if self.request.user.is_authenticated:
profile = self.request.profile
queryset = queryset.annotate(
my_vote=FilteredRelation(
"votes", condition=Q(votes__voter_id=profile.id)
),
).annotate(vote_score=Coalesce(F("my_vote__score"), Value(0)))
return queryset
def get_context_data(self, target_comment=None, **kwargs):
context = super(CommentedDetailView, self).get_context_data(**kwargs)
queryset = self._get_queryset(target_comment)
comment_count = self.object.comments.filter(parent=None, hidden=False).count()
content_type = ContentType.objects.get_for_model(self.object)
all_comment_count = get_visible_comment_count(content_type, self.object.pk)
if target_comment != None:
context["target_comment"] = target_comment.id
else:
context["target_comment"] = -1
if self.request.user.is_authenticated:
context["is_new_user"] = (
not self.request.user.is_staff
and not self.request.profile.submission_set.filter(
points=F("problem__points")
).exists()
)
context["has_comments"] = queryset.exists()
context["comment_lock"] = self.is_comment_locked()
context["comment_list"] = list(queryset)
context["vote_hide_threshold"] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD
if queryset.exists():
context["comment_root_id"] = context["comment_list"][0].id
else:
context["comment_root_id"] = 0
context["comment_parent_none"] = 1
if target_comment != None:
context["offset"] = 0
context["comment_more"] = comment_count - 1
else:
context["offset"] = DEFAULT_OFFSET
context["comment_more"] = comment_count - DEFAULT_OFFSET
context["limit"] = DEFAULT_OFFSET
context["comment_count"] = comment_count
context["profile"] = self.request.profile
context["all_comment_count"] = all_comment_count
return context

View file

@ -55,7 +55,7 @@ from django.views.generic.detail import (
)
from judge import event_poster as event
from judge.comments import CommentedDetailView
from judge.views.comment import CommentedDetailView
from judge.forms import ContestCloneForm
from judge.models import (
Contest,
@ -956,7 +956,7 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
ContestRankingProfile = namedtuple(
"ContestRankingProfile",
"id user css_class username points cumtime tiebreaker organization participation "
"id user points cumtime tiebreaker participation "
"participation_rating problem_cells result_cell",
)
@ -976,13 +976,10 @@ def make_contest_ranking_profile(
user = participation.user
return ContestRankingProfile(
id=user.id,
user=user.user,
css_class=user.css_class,
username=user.username,
user=user,
points=points,
cumtime=cumtime,
tiebreaker=participation.tiebreaker,
organization=user.organization,
participation_rating=participation.rating.rating
if hasattr(participation, "rating")
else None,
@ -1000,12 +997,24 @@ def make_contest_ranking_profile(
def base_contest_ranking_list(contest, problems, queryset, show_final=False):
return [
participation_fields = [
field.name
for field in ContestParticipation._meta.get_fields()
if field.concrete and not field.many_to_many
]
fields_to_fetch = participation_fields + [
"user__id",
"rating__rating",
]
res = [
make_contest_ranking_profile(contest, participation, problems, show_final)
for participation in queryset.select_related("user__user", "rating").defer(
"user__about", "user__organizations__about"
for participation in queryset.select_related("user", "rating").only(
*fields_to_fetch
)
]
Profile.prefetch_profile_cache([p.id for p in res])
return res
def contest_ranking_list(contest, problems, queryset=None, show_final=False):
@ -1016,18 +1025,18 @@ def contest_ranking_list(contest, problems, queryset=None, show_final=False):
return base_contest_ranking_list(
contest,
problems,
queryset.prefetch_related("user__organizations")
.extra(select={"round_score": "round(score, 6)"})
.order_by("is_disqualified", "-round_score", "cumtime", "tiebreaker"),
queryset.extra(select={"round_score": "round(score, 6)"}).order_by(
"is_disqualified", "-round_score", "cumtime", "tiebreaker"
),
show_final,
)
else:
return base_contest_ranking_list(
contest,
problems,
queryset.prefetch_related("user__organizations")
.extra(select={"round_score": "round(score_final, 6)"})
.order_by("is_disqualified", "-round_score", "cumtime_final", "tiebreaker"),
queryset.extra(select={"round_score": "round(score_final, 6)"}).order_by(
"is_disqualified", "-round_score", "cumtime_final", "tiebreaker"
),
show_final,
)

View file

@ -104,15 +104,15 @@ class OrganizationBase(object):
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
)
if self.request.profile:
return org.is_member(self.request.profile)
return False
def is_admin(self, org=None):
if org is None:
org = self.object
if self.request.profile:
return org.admins.filter(id=self.request.profile.id).exists()
return org.is_admin(self.request.profile)
return False
def can_access(self, org):
@ -222,12 +222,19 @@ class OrganizationHomeView(OrganizationMixin):
organizations=self.organization,
authors=self.request.profile,
).count()
context["top_rated"] = self.organization.members.filter(
is_unlisted=False
).order_by("-rating")[:10]
context["top_scorer"] = self.organization.members.filter(
is_unlisted=False
).order_by("-performance_points")[:10]
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"]])
return context
@ -516,6 +523,7 @@ class JoinOrganization(OrganizationMembershipChange):
profile.organizations.add(org)
profile.save()
cache.delete(make_template_fragment_key("org_member_count", (org.id,)))
Organization.is_member.dirty(org, profile)
class LeaveOrganization(OrganizationMembershipChange):
@ -528,6 +536,7 @@ class LeaveOrganization(OrganizationMembershipChange):
)
profile.organizations.remove(org)
cache.delete(make_template_fragment_key("org_member_count", (org.id,)))
Organization.is_member.dirty(org, profile)
class OrganizationRequestForm(Form):

View file

@ -8,14 +8,13 @@ from django.http import (
HttpResponseForbidden,
)
from django.utils.translation import gettext as _
from judge.models.pagevote import PageVote, PageVoteVoter
from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin
from django.views.generic import View, ListView
from django_ratelimit.decorators import ratelimit
from django.conf import settings
from judge.models.pagevote import PageVote, PageVoteVoter, dirty_pagevote
__all__ = [
"upvote_page",
@ -54,9 +53,11 @@ def vote_page(request, delta):
pagevote_id = int(request.POST["id"])
except ValueError:
return HttpResponseBadRequest()
else:
if not PageVote.objects.filter(id=pagevote_id).exists():
raise Http404()
try:
pagevote = PageVote.objects.get(id=pagevote_id)
except PageVote.DoesNotExist:
raise Http404()
vote = PageVoteVoter()
vote.pagevote_id = pagevote_id
@ -76,7 +77,9 @@ def vote_page(request, delta):
PageVote.objects.filter(id=pagevote_id).update(score=F("score") - vote.score)
else:
PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta)
_dirty_vote_score(pagevote_id, request.profile)
dirty_pagevote(pagevote, request.profile)
return HttpResponse("success", content_type="text/plain")
@ -100,8 +103,3 @@ class PageVoteDetailView(TemplateResponseMixin, SingleObjectMixin, View):
context = super(PageVoteDetailView, self).get_context_data(**kwargs)
context["pagevote"] = self.object.get_or_create_pagevote()
return context
def _dirty_vote_score(pagevote_id, profile):
pv = PageVote(id=pagevote_id)
pv.vote_score.dirty(pv, profile)

View file

@ -44,7 +44,7 @@ from django.views.generic import ListView, View
from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin
from judge.comments import CommentedDetailView
from judge.views.comment import CommentedDetailView
from judge.forms import ProblemCloneForm, ProblemSubmitForm, ProblemPointsVoteForm
from judge.models import (
ContestProblem,
@ -820,7 +820,7 @@ class ProblemFeed(ProblemList, FeedView):
model = Problem
context_object_name = "problems"
template_name = "problem/feed.html"
feed_content_template_name = "problem/feed/problems.html"
feed_content_template_name = "problem/feed/items.html"
paginate_by = 4
title = _("Problem feed")
feed_type = None

View file

@ -50,11 +50,9 @@ from judge.utils.timedelta import nice_repr
def submission_related(queryset):
return queryset.select_related("user__user", "problem", "language").only(
return queryset.select_related("user", "problem", "language").only(
"id",
"user__user__username",
"user__display_rank",
"user__rating",
"user_id",
"problem__name",
"problem__code",
"problem__is_public",