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, social,
spaceless, spaceless,
timedelta, timedelta,
comment,
) )
from . import registry 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.function
@registry.render_with("user/link.html") @registry.render_with("user/link.html")
def link_user(user): def link_user(user, show_image=False):
if isinstance(user, Profile): if isinstance(user, Profile):
profile = user profile = user
elif isinstance(user, AbstractUser): elif isinstance(user, AbstractUser):
profile = user.profile profile = user.profile
elif type(user).__name__ == "ContestRankingProfile":
profile = user
else: else:
raise ValueError("Expected profile or user, got %s" % (type(user),)) raise ValueError("Expected profile or user, got %s" % (type(user),))
return {"profile": profile} return {"profile": profile, "show_image": show_image}
@registry.function @registry.function

View file

@ -6,6 +6,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from judge.models.profile import Profile from judge.models.profile import Profile
from judge.caching import cache_wrapper
__all__ = ["BookMark"] __all__ = ["BookMark"]
@ -21,12 +22,9 @@ class BookMark(models.Model):
object_id = models.PositiveIntegerField() object_id = models.PositiveIntegerField()
linked_object = GenericForeignKey("content_type", "object_id") linked_object = GenericForeignKey("content_type", "object_id")
@cache_wrapper(prefix="BMgb")
def get_bookmark(self, user): def get_bookmark(self, user):
userqueryset = MakeBookMark.objects.filter(bookmark=self, user=user) return MakeBookMark.objects.filter(bookmark=self, user=user).exists()
if userqueryset.exists():
return True
else:
return False
class Meta: class Meta:
verbose_name = _("bookmark") verbose_name = _("bookmark")
@ -55,11 +53,22 @@ class MakeBookMark(models.Model):
verbose_name_plural = _("make bookmarks") 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: class Bookmarkable:
def get_or_create_bookmark(self): def get_or_create_bookmark(self):
if self.bookmark.count(): content_type = ContentType.objects.get_for_model(self)
return self.bookmark.first() object_id = self.pk
new_bookmark = BookMark() return _get_or_create_bookmark(content_type, object_id)
new_bookmark.linked_object = self
new_bookmark.save()
return new_bookmark 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.problem import Problem, Solution
from judge.models.profile import Profile from judge.models.profile import Profile
from judge.utils.cachedict import CacheDict from judge.utils.cachedict import CacheDict
from judge.caching import cache_wrapper
__all__ = ["Comment", "CommentLock", "CommentVote", "Notification"] __all__ = ["Comment", "CommentLock", "CommentVote", "Notification"]
@ -75,16 +76,16 @@ class Comment(MPTTModel):
queryset = ( queryset = (
cls.objects.filter(hidden=False) cls.objects.filter(hidden=False)
.select_related("author__user") .select_related("author__user")
.defer("author__about", "body") .defer("author__about")
.order_by("-id") .order_by("-id")
) )
if organization: if organization:
queryset = queryset.filter(author__in=organization.members.all()) 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)) 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: if n == -1:
n = len(queryset) n = len(queryset)
@ -174,3 +175,10 @@ class CommentLock(models.Model):
def __str__(self): def __str__(self):
return str(self.page) 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.profile import Organization, Profile
from judge.models.pagevote import PageVotable from judge.models.pagevote import PageVotable
from judge.models.bookmark import Bookmarkable from judge.models.bookmark import Bookmarkable
from judge.caching import cache_wrapper
__all__ = ["MiscConfig", "validate_regex", "NavigationBar", "BlogPost"] __all__ = ["MiscConfig", "validate_regex", "NavigationBar", "BlogPost"]
@ -105,7 +106,7 @@ class BlogPost(models.Model, PageVotable, Bookmarkable):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("blog_post", args=(self.id, self.slug)) 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 self.visible and self.publish_on <= timezone.now():
if not self.is_organization_private: if not self.is_organization_private:
return True return True
@ -132,6 +133,10 @@ class BlogPost(models.Model, PageVotable, Bookmarkable):
and self.authors.filter(id=user.profile.id).exists() and self.authors.filter(id=user.profile.id).exists()
) )
@cache_wrapper(prefix="BPga")
def get_authors(self):
return self.authors.only("id")
class Meta: class Meta:
permissions = (("edit_all_post", _("Edit all posts")),) permissions = (("edit_all_post", _("Edit all posts")),)
verbose_name = _("blog post") verbose_name = _("blog post")

View file

@ -31,11 +31,8 @@ class PageVote(models.Model):
@cache_wrapper(prefix="PVvs") @cache_wrapper(prefix="PVvs")
def vote_score(self, user): def vote_score(self, user):
page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user) page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user).first()
if page_vote.exists(): return page_vote.score if page_vote else 0
return page_vote.first().score
else:
return 0
def __str__(self): def __str__(self):
return f"pagevote for {self.linked_object}" return f"pagevote for {self.linked_object}"
@ -52,11 +49,22 @@ class PageVoteVoter(models.Model):
verbose_name_plural = _("pagevote votes") 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: class PageVotable:
def get_or_create_pagevote(self): def get_or_create_pagevote(self):
if self.pagevote.count(): content_type = ContentType.objects.get_for_model(self)
return self.pagevote.first() object_id = self.pk
new_pagevote = PageVote() return _get_or_create_pagevote(content_type, object_id)
new_pagevote.linked_object = self
new_pagevote.save()
return new_pagevote 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_data_storage,
problem_directory_file_helper, problem_directory_file_helper,
) )
from judge.caching import cache_wrapper
__all__ = [ __all__ = [
"ProblemGroup", "ProblemGroup",
@ -439,6 +440,10 @@ class Problem(models.Model, PageVotable, Bookmarkable):
"profile_id", flat=True "profile_id", flat=True
) )
@cache_wrapper(prefix="Pga")
def get_authors(self):
return self.authors.only("id")
@cached_property @cached_property
def editor_ids(self): def editor_ids(self):
return self.author_ids.union( return self.author_ids.union(

View file

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

View file

@ -55,18 +55,13 @@ def problem_update(sender, instance, **kwargs):
for lang, _ in settings.LANGUAGES 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( cache.delete_many(
[ [
"generated-meta-problem:%s:%d" % (lang, instance.id) "generated-meta-problem:%s:%d" % (lang, instance.id)
for lang, _ in settings.LANGUAGES for lang, _ in settings.LANGUAGES
] ]
) )
Problem.get_authors.dirty(instance)
for lang, _ in settings.LANGUAGES: for lang, _ in settings.LANGUAGES:
unlink_if_exists(get_pdf_path("%s.%s.pdf" % (instance.code, lang))) 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,))] + [make_template_fragment_key("post_content", (instance.id,))]
) )
BlogPost.get_authors.dirty(instance)
@receiver(post_delete, sender=Submission) @receiver(post_delete, sender=Submission)
@ -146,6 +142,8 @@ def contest_submission_delete(sender, instance, **kwargs):
@receiver(post_save, sender=Organization) @receiver(post_save, sender=Organization)
def organization_update(sender, instance, **kwargs): def organization_update(sender, instance, **kwargs):
cache.delete_many([make_template_fragment_key("organization_html", (instance.id,))]) 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] _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.utils.translation import ugettext as _
from django.views.generic import ListView 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.pagevote import PageVoteDetailView
from judge.views.bookmark import BookMarkDetailView from judge.views.bookmark import BookMarkDetailView
from judge.models import ( from judge.models import (
@ -69,13 +69,18 @@ class HomeFeedView(FeedView):
profile_queryset = Profile.objects profile_queryset = Profile.objects
if self.request.organization: if self.request.organization:
profile_queryset = self.request.organization.members profile_queryset = self.request.organization.members
context["top_rated"] = profile_queryset.filter(is_unlisted=False).order_by( context["top_rated"] = (
"-rating" profile_queryset.filter(is_unlisted=False)
)[:10] .order_by("-rating")
context["top_scorer"] = profile_queryset.filter(is_unlisted=False).order_by( .only("id", "rating")[:10]
"-performance_points" )
)[: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 return context
@ -90,7 +95,7 @@ class PostList(HomeFeedView):
queryset = ( queryset = (
BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()) BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now())
.order_by("-sticky", "-publish_on") .order_by("-sticky", "-publish_on")
.prefetch_related("authors__user", "organizations") .prefetch_related("organizations")
) )
filter = Q(is_organization_private=False) filter = Q(is_organization_private=False)
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
@ -198,6 +203,6 @@ class PostView(TitleMixin, CommentedDetailView, PageVoteDetailView, BookMarkDeta
def get_object(self, queryset=None): def get_object(self, queryset=None):
post = super(PostView, self).get_object(queryset) 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() raise Http404()
return post return post

View file

@ -8,12 +8,12 @@ from django.http import (
HttpResponseForbidden, HttpResponseForbidden,
) )
from django.utils.translation import gettext as _ 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.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.views.generic import View, ListView from django.views.generic import View, ListView
from judge.models.bookmark import BookMark, MakeBookMark, dirty_bookmark
__all__ = [ __all__ = [
"dobookmark_page", "dobookmark_page",
@ -32,30 +32,31 @@ def bookmark_page(request, delta):
try: try:
bookmark_id = int(request.POST["id"]) bookmark_id = int(request.POST["id"])
bookmark_page = BookMark.objects.filter(id=bookmark_id) bookmark = BookMark.objects.get(id=bookmark_id)
except ValueError: except ValueError:
return HttpResponseBadRequest() return HttpResponseBadRequest()
else: except BookMark.DoesNotExist:
if not bookmark_page.exists(): raise Http404()
raise Http404()
if delta == 0: if delta == 0:
bookmarklist = MakeBookMark.objects.filter( bookmarklist = MakeBookMark.objects.filter(
bookmark=bookmark_page.first(), user=request.profile bookmark=bookmark, user=request.profile
) )
if not bookmarklist.exists(): if not bookmarklist.exists():
newbookmark = MakeBookMark( newbookmark = MakeBookMark(
bookmark=bookmark_page.first(), bookmark=bookmark,
user=request.profile, user=request.profile,
) )
newbookmark.save() newbookmark.save()
else: else:
bookmarklist = MakeBookMark.objects.filter( bookmarklist = MakeBookMark.objects.filter(
bookmark=bookmark_page.first(), user=request.profile bookmark=bookmark, user=request.profile
) )
if bookmarklist.exists(): if bookmarklist.exists():
bookmarklist.delete() bookmarklist.delete()
dirty_bookmark(bookmark, request.profile)
return HttpResponse("success", content_type="text/plain") 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.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib.auth.context_processors import PermWrapper from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied, ValidationError
from django.db import IntegrityError 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.functions import Coalesce
from django.db.models.expressions import F, Value from django.forms import ModelForm
from django.forms.models import ModelForm
from django.http import ( from django.http import (
Http404, Http404,
HttpResponse, HttpResponse,
HttpResponseBadRequest, HttpResponseBadRequest,
HttpResponseForbidden, HttpResponseForbidden,
HttpResponseNotFound,
HttpResponseRedirect,
) )
from django.shortcuts import get_object_or_404, render 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.utils.translation import gettext as _
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django.views.generic import DetailView, UpdateView from django.views.generic import DetailView, UpdateView, View
from django.urls import reverse_lazy from django.views.generic.base import TemplateResponseMixin
from reversion import revisions from django.views.generic.detail import SingleObjectMixin
from reversion.models import Version
from django.conf import settings
from django_ratelimit.decorators import ratelimit 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.utils.views import TitleMixin
from judge.widgets import HeavyPreviewPageDownWidget from judge.widgets import HeavyPreviewPageDownWidget
from judge.comments import add_mention_notifications
import json
__all__ = [ __all__ = [
"upvote_comment", "upvote_comment",
@ -40,6 +49,18 @@ __all__ = [
"CommentEdit", "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) @ratelimit(key="user", rate=settings.RL_VOTE)
@login_required @login_required
@ -290,4 +311,204 @@ def comment_hide(request):
comment = get_object_or_404(Comment, id=comment_id) comment = get_object_or_404(Comment, id=comment_id)
comment.get_descendants(include_self=True).update(hidden=True) comment.get_descendants(include_self=True).update(hidden=True)
get_visible_comment_count.dirty(comment.content_type, comment.object_id)
return HttpResponse("ok") 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 import event_poster as event
from judge.comments import CommentedDetailView from judge.views.comment import CommentedDetailView
from judge.forms import ContestCloneForm from judge.forms import ContestCloneForm
from judge.models import ( from judge.models import (
Contest, Contest,
@ -956,7 +956,7 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
ContestRankingProfile = namedtuple( ContestRankingProfile = namedtuple(
"ContestRankingProfile", "ContestRankingProfile",
"id user css_class username points cumtime tiebreaker organization participation " "id user points cumtime tiebreaker participation "
"participation_rating problem_cells result_cell", "participation_rating problem_cells result_cell",
) )
@ -976,13 +976,10 @@ def make_contest_ranking_profile(
user = participation.user user = participation.user
return ContestRankingProfile( return ContestRankingProfile(
id=user.id, id=user.id,
user=user.user, user=user,
css_class=user.css_class,
username=user.username,
points=points, points=points,
cumtime=cumtime, cumtime=cumtime,
tiebreaker=participation.tiebreaker, tiebreaker=participation.tiebreaker,
organization=user.organization,
participation_rating=participation.rating.rating participation_rating=participation.rating.rating
if hasattr(participation, "rating") if hasattr(participation, "rating")
else None, else None,
@ -1000,12 +997,24 @@ def make_contest_ranking_profile(
def base_contest_ranking_list(contest, problems, queryset, show_final=False): 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) make_contest_ranking_profile(contest, participation, problems, show_final)
for participation in queryset.select_related("user__user", "rating").defer( for participation in queryset.select_related("user", "rating").only(
"user__about", "user__organizations__about" *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): 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( return base_contest_ranking_list(
contest, contest,
problems, problems,
queryset.prefetch_related("user__organizations") queryset.extra(select={"round_score": "round(score, 6)"}).order_by(
.extra(select={"round_score": "round(score, 6)"}) "is_disqualified", "-round_score", "cumtime", "tiebreaker"
.order_by("is_disqualified", "-round_score", "cumtime", "tiebreaker"), ),
show_final, show_final,
) )
else: else:
return base_contest_ranking_list( return base_contest_ranking_list(
contest, contest,
problems, problems,
queryset.prefetch_related("user__organizations") queryset.extra(select={"round_score": "round(score_final, 6)"}).order_by(
.extra(select={"round_score": "round(score_final, 6)"}) "is_disqualified", "-round_score", "cumtime_final", "tiebreaker"
.order_by("is_disqualified", "-round_score", "cumtime_final", "tiebreaker"), ),
show_final, show_final,
) )

View file

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

View file

@ -8,14 +8,13 @@ from django.http import (
HttpResponseForbidden, HttpResponseForbidden,
) )
from django.utils.translation import gettext as _ 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.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.views.generic import View, ListView from django.views.generic import View, ListView
from django_ratelimit.decorators import ratelimit from django_ratelimit.decorators import ratelimit
from django.conf import settings from django.conf import settings
from judge.models.pagevote import PageVote, PageVoteVoter, dirty_pagevote
__all__ = [ __all__ = [
"upvote_page", "upvote_page",
@ -54,9 +53,11 @@ def vote_page(request, delta):
pagevote_id = int(request.POST["id"]) pagevote_id = int(request.POST["id"])
except ValueError: except ValueError:
return HttpResponseBadRequest() return HttpResponseBadRequest()
else:
if not PageVote.objects.filter(id=pagevote_id).exists(): try:
raise Http404() pagevote = PageVote.objects.get(id=pagevote_id)
except PageVote.DoesNotExist:
raise Http404()
vote = PageVoteVoter() vote = PageVoteVoter()
vote.pagevote_id = pagevote_id 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) PageVote.objects.filter(id=pagevote_id).update(score=F("score") - vote.score)
else: else:
PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta) 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") 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 = super(PageVoteDetailView, self).get_context_data(**kwargs)
context["pagevote"] = self.object.get_or_create_pagevote() context["pagevote"] = self.object.get_or_create_pagevote()
return context 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.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin 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.forms import ProblemCloneForm, ProblemSubmitForm, ProblemPointsVoteForm
from judge.models import ( from judge.models import (
ContestProblem, ContestProblem,
@ -820,7 +820,7 @@ class ProblemFeed(ProblemList, FeedView):
model = Problem model = Problem
context_object_name = "problems" context_object_name = "problems"
template_name = "problem/feed.html" template_name = "problem/feed.html"
feed_content_template_name = "problem/feed/problems.html" feed_content_template_name = "problem/feed/items.html"
paginate_by = 4 paginate_by = 4
title = _("Problem feed") title = _("Problem feed")
feed_type = None feed_type = None

View file

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

View file

@ -29,6 +29,17 @@ th.header.rank {
padding-left: 5px; 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 { #search-handle {
width: 100%; width: 100%;
height: 2.3em; height: 2.3em;

View file

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

View file

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

View file

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

View file

@ -79,7 +79,9 @@
{% include 'chat/user_online_status.html' %} {% include 'chat/user_online_status.html' %}
</div> </div>
<div id="chat-box"> <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"> <ul id="chat-log">
{% include 'chat/message_list.html' %} {% include 'chat/message_list.html' %}
</ul> </ul>

View file

@ -3,7 +3,7 @@
{{ comment_form.media.js }} {{ comment_form.media.js }}
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function () { $(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) { window.reply_comment = function (parent) {
var $comment_reply = $('#comment-' + parent + '-reply'); var $comment_reply = $('#comment-' + parent + '-reply');
var reply_id = 'reply-' + parent; var reply_id = 'reply-' + parent;

View file

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

View file

@ -13,18 +13,23 @@
{% endblock %} {% endblock %}
{% block user_footer %} {% 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"> <div style="font-weight: 600; display: none" class="fullname gray">
{{ user.user.first_name }} {{ profile.first_name }}
</div> </div>
{% endif %} {% endif %}
{% if user.user.last_name %} {% if profile.last_name %}
<div class="school gray" style="display: none"><div style="font-weight: 600"> <div class="school gray" style="display: none"><div style="font-weight: 600">
{{- user.user.last_name -}} {{ profile.last_name }}
</div></div> </div></div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block user_link %}
{{ link_user(user.user, show_image=True) }}
{% endblock %}
{% block user_data %} {% block user_data %}
{% if user.participation.virtual %} {% if user.participation.virtual %}
<sub class="gray">[{{user.participation.virtual}}]</sub> <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> <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;"> <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> <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()"> <a href="#" onclick="download_ranking_as_csv()">
<i class="fa fa-download" aria-hidden="true"></i> <i class="fa fa-download" aria-hidden="true"></i>
{{ _('Download as CSV') }} {{ _('Download as CSV') }}

View file

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

View file

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

View file

@ -260,21 +260,19 @@
<hr style="padding-top: 0.7em"> <hr style="padding-top: 0.7em">
{% cache 86400 'problem_authors' problem.id LANGUAGE_CODE %} {% with authors=problem.get_authors() %}
{% with authors=problem.authors.all() %} {% if authors %}
{% if authors %} <div class="problem-info-entry">
<div class="problem-info-entry"> <i class="fa fa-pencil-square-o fa-fw"></i><span
<i class="fa fa-pencil-square-o fa-fw"></i><span class="pi-name">{% trans trimmed count=authors|length %}
class="pi-name">{% trans trimmed count=authors|length %} Author:
Author: {% pluralize count %}
{% pluralize count %} Authors:
Authors: {% endtrans %}</span>
{% endtrans %}</span> <div class="pi-value authors-value">{{ link_users(authors) }}</div>
<div class="pi-value authors-value">{{ link_users(authors) }}</div> </div>
</div> {% endif %}
{% endif %} {% endwith %}
{% endwith %}
{% endcache %}
{% if not contest_problem or not contest_problem.contest.hide_problem_tags %} {% if not contest_problem or not contest_problem.contest.hide_problem_tags %}
<div id="problem-types"> <div id="problem-types">

View file

@ -19,7 +19,15 @@
<tr id="user-{{ user.username }}" {% block row_extra scoped %}{% endblock %}> <tr id="user-{{ user.username }}" {% block row_extra scoped %}{% endblock %}>
<td class="rank-td">{{ rank }}</td> <td class="rank-td">{{ rank }}</td>
{% block after_rank scoped %}{% endblock %} {% 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 before_point scoped %}{% endblock %}
{% block point scoped %} {% block point scoped %}
<td title="{{ user.performance_points|floatformat(2) }}" class="user-points"> <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>