Caching and refactors
This commit is contained in:
parent
67b06d7856
commit
8f1c8d6c96
33 changed files with 485 additions and 381 deletions
|
@ -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
|
|
|
@ -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
12
judge/jinja2/comment.py
Normal 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)
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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') }}
|
||||||
|
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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">
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue