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

@ -1,230 +0,0 @@
from django import forms
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models import Count, FilteredRelation, Q
from django.db.models.expressions import F, Value
from django.db.models.functions import Coalesce
from django.forms import ModelForm
from django.http import (
HttpResponseForbidden,
HttpResponseNotFound,
HttpResponseRedirect,
Http404,
)
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.views.generic import View
from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin
from reversion import revisions
from reversion.models import Revision, Version
from django_ratelimit.decorators import ratelimit
from judge.models import Comment, Notification
from judge.widgets import HeavyPreviewPageDownWidget
from judge.jinja2.reference import get_user_from_text
from judge.models.notification import make_notification
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)
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)
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()
context["target_comment"] = -1
if target_comment != None:
context["target_comment"] = target_comment.id
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
return context

View file

@ -22,6 +22,7 @@ from . import (
social,
spaceless,
timedelta,
comment,
)
from . import registry

12
judge/jinja2/comment.py Normal file
View file

@ -0,0 +1,12 @@
from . import registry
from django.contrib.contenttypes.models import ContentType
from judge.models.comment import get_visible_comment_count
from judge.caching import cache_wrapper
@registry.function
def comment_count(obj):
content_type = ContentType.objects.get_for_model(obj)
return get_visible_comment_count(content_type, obj.pk)

View file

@ -155,16 +155,14 @@ def item_title(item):
@registry.function
@registry.render_with("user/link.html")
def link_user(user):
def link_user(user, show_image=False):
if isinstance(user, Profile):
profile = user
elif isinstance(user, AbstractUser):
profile = user.profile
elif type(user).__name__ == "ContestRankingProfile":
profile = user
else:
raise ValueError("Expected profile or user, got %s" % (type(user),))
return {"profile": profile}
return {"profile": profile, "show_image": show_image}
@registry.function

View file

@ -6,6 +6,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from judge.models.profile import Profile
from judge.caching import cache_wrapper
__all__ = ["BookMark"]
@ -21,12 +22,9 @@ class BookMark(models.Model):
object_id = models.PositiveIntegerField()
linked_object = GenericForeignKey("content_type", "object_id")
@cache_wrapper(prefix="BMgb")
def get_bookmark(self, user):
userqueryset = MakeBookMark.objects.filter(bookmark=self, user=user)
if userqueryset.exists():
return True
else:
return False
return MakeBookMark.objects.filter(bookmark=self, user=user).exists()
class Meta:
verbose_name = _("bookmark")
@ -55,11 +53,22 @@ class MakeBookMark(models.Model):
verbose_name_plural = _("make bookmarks")
@cache_wrapper(prefix="gocb")
def _get_or_create_bookmark(content_type, object_id):
bookmark, created = BookMark.objects.get_or_create(
content_type=content_type,
object_id=object_id,
)
return bookmark
class Bookmarkable:
def get_or_create_bookmark(self):
if self.bookmark.count():
return self.bookmark.first()
new_bookmark = BookMark()
new_bookmark.linked_object = self
new_bookmark.save()
return new_bookmark
content_type = ContentType.objects.get_for_model(self)
object_id = self.pk
return _get_or_create_bookmark(content_type, object_id)
def dirty_bookmark(bookmark, profile):
bookmark.get_bookmark.dirty(bookmark, profile)
_get_or_create_bookmark.dirty(bookmark.content_type, bookmark.object_id)

View file

@ -20,6 +20,7 @@ from judge.models.interface import BlogPost
from judge.models.problem import Problem, Solution
from judge.models.profile import Profile
from judge.utils.cachedict import CacheDict
from judge.caching import cache_wrapper
__all__ = ["Comment", "CommentLock", "CommentVote", "Notification"]
@ -75,16 +76,16 @@ class Comment(MPTTModel):
queryset = (
cls.objects.filter(hidden=False)
.select_related("author__user")
.defer("author__about", "body")
.defer("author__about")
.order_by("-id")
)
if organization:
queryset = queryset.filter(author__in=organization.members.all())
problem_access = CacheDict(lambda p: p.is_accessible_by(user))
object_access = CacheDict(lambda p: p.is_accessible_by(user))
contest_access = CacheDict(lambda c: c.is_accessible_by(user))
blog_access = CacheDict(lambda b: b.can_see(user))
blog_access = CacheDict(lambda b: b.is_accessible_by(user))
if n == -1:
n = len(queryset)
@ -174,3 +175,10 @@ class CommentLock(models.Model):
def __str__(self):
return str(self.page)
@cache_wrapper(prefix="gcc")
def get_visible_comment_count(content_type, object_id):
return Comment.objects.filter(
content_type=content_type, object_id=object_id, hidden=False
).count()

View file

@ -13,6 +13,7 @@ from mptt.models import MPTTModel
from judge.models.profile import Organization, Profile
from judge.models.pagevote import PageVotable
from judge.models.bookmark import Bookmarkable
from judge.caching import cache_wrapper
__all__ = ["MiscConfig", "validate_regex", "NavigationBar", "BlogPost"]
@ -105,7 +106,7 @@ class BlogPost(models.Model, PageVotable, Bookmarkable):
def get_absolute_url(self):
return reverse("blog_post", args=(self.id, self.slug))
def can_see(self, user):
def is_accessible_by(self, user):
if self.visible and self.publish_on <= timezone.now():
if not self.is_organization_private:
return True
@ -132,6 +133,10 @@ class BlogPost(models.Model, PageVotable, Bookmarkable):
and self.authors.filter(id=user.profile.id).exists()
)
@cache_wrapper(prefix="BPga")
def get_authors(self):
return self.authors.only("id")
class Meta:
permissions = (("edit_all_post", _("Edit all posts")),)
verbose_name = _("blog post")

View file

@ -31,11 +31,8 @@ class PageVote(models.Model):
@cache_wrapper(prefix="PVvs")
def vote_score(self, user):
page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user)
if page_vote.exists():
return page_vote.first().score
else:
return 0
page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user).first()
return page_vote.score if page_vote else 0
def __str__(self):
return f"pagevote for {self.linked_object}"
@ -52,11 +49,22 @@ class PageVoteVoter(models.Model):
verbose_name_plural = _("pagevote votes")
@cache_wrapper(prefix="gocp")
def _get_or_create_pagevote(content_type, object_id):
pagevote, created = PageVote.objects.get_or_create(
content_type=content_type,
object_id=object_id,
)
return pagevote
class PageVotable:
def get_or_create_pagevote(self):
if self.pagevote.count():
return self.pagevote.first()
new_pagevote = PageVote()
new_pagevote.linked_object = self
new_pagevote.save()
return new_pagevote
content_type = ContentType.objects.get_for_model(self)
object_id = self.pk
return _get_or_create_pagevote(content_type, object_id)
def dirty_pagevote(pagevote, profile):
pagevote.vote_score.dirty(pagevote, profile)
_get_or_create_pagevote.dirty(pagevote.content_type, pagevote.object_id)

View file

@ -24,6 +24,7 @@ from judge.models.problem_data import (
problem_data_storage,
problem_directory_file_helper,
)
from judge.caching import cache_wrapper
__all__ = [
"ProblemGroup",
@ -439,6 +440,10 @@ class Problem(models.Model, PageVotable, Bookmarkable):
"profile_id", flat=True
)
@cache_wrapper(prefix="Pga")
def get_authors(self):
return self.authors.only("id")
@cached_property
def editor_ids(self):
return self.author_ids.union(

View file

@ -141,6 +141,14 @@ class Organization(models.Model):
def get_submissions_url(self):
return reverse("organization_submissions", args=(self.id, self.slug))
@cache_wrapper("Oia")
def is_admin(self, profile):
return self.admins.filter(id=profile.id).exists()
@cache_wrapper("Oim")
def is_member(self, profile):
return profile in self
class Meta:
ordering = ["name"]
permissions = (
@ -280,6 +288,14 @@ class Profile(models.Model):
def is_muted(self):
return self._cached_info["mute"]
@cached_property
def cached_display_rank(self):
return self._cached_info.get("display_rank")
@cached_property
def cached_rating(self):
return self._cached_info.get("rating")
@cached_property
def profile_image_url(self):
return self._cached_info.get("profile_image_url")
@ -374,7 +390,7 @@ class Profile(models.Model):
@cached_property
def css_class(self):
return self.get_user_css_class(self.display_rank, self.rating)
return self.get_user_css_class(self.cached_display_rank, self.cached_rating)
def get_friends(self): # list of ids, including you
friend_obj = self.following_users.prefetch_related("users")
@ -388,11 +404,7 @@ class Profile(models.Model):
if not self.user.is_authenticated:
return False
profile_id = self.id
return (
org.admins.filter(id=profile_id).exists()
or org.registrant_id == profile_id
or self.user.is_superuser
)
return org.is_admin(self) or self.user.is_superuser
@classmethod
def prefetch_profile_cache(self, profile_ids):
@ -544,7 +556,7 @@ def on_profile_save(sender, instance, **kwargs):
_get_basic_info.dirty(instance.id)
@cache_wrapper(prefix="Pgbi2")
@cache_wrapper(prefix="Pgbi3")
def _get_basic_info(profile_id):
profile = (
Profile.objects.select_related("user")
@ -556,6 +568,8 @@ def _get_basic_info(profile_id):
"user__email",
"user__first_name",
"user__last_name",
"display_rank",
"rating",
)
.get(id=profile_id)
)
@ -569,6 +583,8 @@ def _get_basic_info(profile_id):
"profile_image_url": profile.profile_image.url
if profile.profile_image
else None,
"display_rank": profile.display_rank,
"rating": profile.rating,
}
res = {k: v for k, v in res.items() if v is not None}
return res

View file

@ -55,18 +55,13 @@ def problem_update(sender, instance, **kwargs):
for lang, _ in settings.LANGUAGES
]
)
cache.delete_many(
[
make_template_fragment_key("problem_authors", (instance.id, lang))
for lang, _ in settings.LANGUAGES
]
)
cache.delete_many(
[
"generated-meta-problem:%s:%d" % (lang, instance.id)
for lang, _ in settings.LANGUAGES
]
)
Problem.get_authors.dirty(instance)
for lang, _ in settings.LANGUAGES:
unlink_if_exists(get_pdf_path("%s.%s.pdf" % (instance.code, lang)))
@ -129,6 +124,7 @@ def post_update(sender, instance, **kwargs):
]
+ [make_template_fragment_key("post_content", (instance.id,))]
)
BlogPost.get_authors.dirty(instance)
@receiver(post_delete, sender=Submission)
@ -146,6 +142,8 @@ def contest_submission_delete(sender, instance, **kwargs):
@receiver(post_save, sender=Organization)
def organization_update(sender, instance, **kwargs):
cache.delete_many([make_template_fragment_key("organization_html", (instance.id,))])
for admin in instance.admins.all():
Organization.is_admin.dirty(instance, admin)
_misc_config_i18n = [code for code, _ in settings.LANGUAGES]

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():
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,8 +53,10 @@ 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():
try:
pagevote = PageVote.objects.get(id=pagevote_id)
except PageVote.DoesNotExist:
raise Http404()
vote = PageVoteVoter()
@ -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",

View file

@ -29,6 +29,17 @@ th.header.rank {
padding-left: 5px;
}
.user-with-img {
display: inline-flex;
gap: 0.5em;
align-items: center;
.user-img {
height: 1.5em;
width: 1.5em;
}
}
#search-handle {
width: 100%;
height: 2.3em;

View file

@ -27,9 +27,9 @@
<span class="actionbar-text">
{{_("Comment")}}
</span>
{% if comment_count %}
{% if all_comment_count %}
<span style="margin-left: 0.2em">
({{ comment_count }})
({{ all_comment_count }})
</span>
{% endif %}
</span>

View file

@ -19,7 +19,7 @@
<div class="post-full">
<div class="post-title">{{ title }}</div>
<div class="time">
{% with authors=post.authors.all() %}
{% with authors=post.get_authors() %}
{% if authors %}
<span class="post-authors">{{ link_users(authors) }}</span>
{% endif %}

View file

@ -1,8 +1,8 @@
{% for post in posts%}
{% for post in posts %}
<section class="{% if post.sticky %}sticky {% endif %}blog-box">
<div style="margin-bottom: 0.5em">
<span class="post-content-header time">
{% with authors=post.authors.all() %}
{% with authors=post.get_authors() %}
{%- if authors -%}
<span class="user-img" style="width: 1.5em; height: 1.5em">
<img src="{{gravatar(authors[0])}}">
@ -31,7 +31,7 @@
<a href="{{ url('blog_post', post.id, post.slug) }}#comments" class="blog-comment-count-link">
<i class="fa fa-comments blog-comment-icon"></i>
<span class="blog-comment-count">
{{- post.comments.filter(hidden=False).count() or 0 -}}
{{ comment_count(post) }}
</span>
</a>
</span>

View file

@ -79,7 +79,9 @@
{% include 'chat/user_online_status.html' %}
</div>
<div id="chat-box">
<img src="{{static('loading.gif')}}" id="loader" style="display: none;">
<span id="loader" style="font-size: 2em; display: none;">
<i class="fa fa-spinner fa-pulse"></i>
</span>
<ul id="chat-log">
{% include 'chat/message_list.html' %}
</ul>

View file

@ -3,7 +3,7 @@
{{ comment_form.media.js }}
<script type="text/javascript">
$(document).ready(function () {
let loading_gif = "<img src=\"{{static('loading.gif')}}\" style=\"height: 3em; margin-bottom: 3px\" class=\"loading\">";
let loading_gif = "<i class=\"fa fa-spinner fa-pulse loading\" style=\"font-size: 1.5em\"></i>";
window.reply_comment = function (parent) {
var $comment_reply = $('#comment-' + parent + '-reply');
var reply_id = 'reply-' + parent;

View file

@ -1,6 +1,6 @@
<style>
#users-table td {
height: 2.5em;
min-height: 2.5em;
}
#users-table a {

View file

@ -13,18 +13,23 @@
{% endblock %}
{% block user_footer %}
{% if user.user.first_name %}
{% set profile = user.user %}
{% if profile.first_name %}
<div style="font-weight: 600; display: none" class="fullname gray">
{{ user.user.first_name }}
{{ profile.first_name }}
</div>
{% endif %}
{% if user.user.last_name %}
{% if profile.last_name %}
<div class="school gray" style="display: none"><div style="font-weight: 600">
{{- user.user.last_name -}}
{{ profile.last_name }}
</div></div>
{% endif %}
{% endblock %}
{% block user_link %}
{{ link_user(user.user, show_image=True) }}
{% endblock %}
{% block user_data %}
{% if user.participation.virtual %}
<sub class="gray">[{{user.participation.virtual}}]</sub>

View file

@ -150,7 +150,7 @@
<label for="show-total-score-checkbox" style="vertical-align: bottom; margin-right: 1em;">{{ _('Total score only') }}</label>
<input id="show-virtual-checkbox" type="checkbox" style="vertical-align: bottom;">
<label id="show-virtual-label" for="show-virtual-checkbox" style="vertical-align: bottom; margin-right: 1em;">{{ _('Show virtual participation') }}</label>
<img src="{{static('loading.gif')}}" style="height: 1em; display:none;" id="loading-gif"></img>
<i class="fa fa-spinner fa-pulse" style="display: none" id="loading-gif"></i>
<a href="#" onclick="download_ranking_as_csv()">
<i class="fa fa-download" aria-hidden="true"></i>
{{ _('Download as CSV') }}

View file

@ -27,5 +27,5 @@
<li><a href="{{url('admin:judge_volunteerproblemvote_changelist')}}">{{_('View your votes')}}</a></li>
</ul>
{% endif %}
{% include "problem/feed/problems.html" %}
{% include "problem/feed/items.html" %}
{% endblock %}

View file

@ -12,7 +12,7 @@
<i class="unsolved-problem-color fa fa-minus-circle"></i>
{% endif %}
</h3>
{% with authors=problem.authors.all() %}
{% with authors=problem.get_authors() %}
{% if authors %}
<div class="problem-feed-info-entry">
<i class="fa fa-pencil-square-o fa-fw"></i>

View file

@ -260,8 +260,7 @@
<hr style="padding-top: 0.7em">
{% cache 86400 'problem_authors' problem.id LANGUAGE_CODE %}
{% with authors=problem.authors.all() %}
{% with authors=problem.get_authors() %}
{% if authors %}
<div class="problem-info-entry">
<i class="fa fa-pencil-square-o fa-fw"></i><span
@ -273,8 +272,7 @@
<div class="pi-value authors-value">{{ link_users(authors) }}</div>
</div>
{% endif %}
{% endwith %}
{% endcache %}
{% endwith %}
{% if not contest_problem or not contest_problem.contest.hide_problem_tags %}
<div id="problem-types">

View file

@ -19,7 +19,15 @@
<tr id="user-{{ user.username }}" {% block row_extra scoped %}{% endblock %}>
<td class="rank-td">{{ rank }}</td>
{% block after_rank scoped %}{% endblock %}
<td class="user-name"><div style="display: inline-block;">{{ link_user(user) }}{% block user_footer scoped %}{% endblock %}</div> {% block user_data scoped %}{% endblock %}</td>
<td class="user-name">
<div style="display: inline-block;">
{% block user_link scoped %}
{{ link_user(user) }}
{% endblock %}
{% block user_footer scoped %}{% endblock %}
</div>
{% block user_data scoped %}{% endblock %}
</td>
{% block before_point scoped %}{% endblock %}
{% block point scoped %}
<td title="{{ user.performance_points|floatformat(2) }}" class="user-points">

View file

@ -1 +1,10 @@
<span class="{{ profile.css_class }}"><a href="{{ url('user_page', profile.username) }}">{{ profile.username }}</a></span>
<span class="{{ profile.css_class }} {{'user-with-img' if show_image}}">
{% if show_image %}
<span class="user-img">
<img src="{{gravatar(profile)}}">
</span>
{% endif %}
<a href="{{ url('user_page', profile.username) }}">
{{ profile.username }}
</a>
</span>