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,
|
||||
spaceless,
|
||||
timedelta,
|
||||
comment,
|
||||
)
|
||||
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.render_with("user/link.html")
|
||||
def link_user(user):
|
||||
def link_user(user, show_image=False):
|
||||
if isinstance(user, Profile):
|
||||
profile = user
|
||||
elif isinstance(user, AbstractUser):
|
||||
profile = user.profile
|
||||
elif type(user).__name__ == "ContestRankingProfile":
|
||||
profile = user
|
||||
else:
|
||||
raise ValueError("Expected profile or user, got %s" % (type(user),))
|
||||
return {"profile": profile}
|
||||
return {"profile": profile, "show_image": show_image}
|
||||
|
||||
|
||||
@registry.function
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from judge.models.profile import Profile
|
||||
from judge.caching import cache_wrapper
|
||||
|
||||
__all__ = ["BookMark"]
|
||||
|
||||
|
@ -21,12 +22,9 @@ class BookMark(models.Model):
|
|||
object_id = models.PositiveIntegerField()
|
||||
linked_object = GenericForeignKey("content_type", "object_id")
|
||||
|
||||
@cache_wrapper(prefix="BMgb")
|
||||
def get_bookmark(self, user):
|
||||
userqueryset = MakeBookMark.objects.filter(bookmark=self, user=user)
|
||||
if userqueryset.exists():
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return MakeBookMark.objects.filter(bookmark=self, user=user).exists()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("bookmark")
|
||||
|
@ -55,11 +53,22 @@ class MakeBookMark(models.Model):
|
|||
verbose_name_plural = _("make bookmarks")
|
||||
|
||||
|
||||
@cache_wrapper(prefix="gocb")
|
||||
def _get_or_create_bookmark(content_type, object_id):
|
||||
bookmark, created = BookMark.objects.get_or_create(
|
||||
content_type=content_type,
|
||||
object_id=object_id,
|
||||
)
|
||||
return bookmark
|
||||
|
||||
|
||||
class Bookmarkable:
|
||||
def get_or_create_bookmark(self):
|
||||
if self.bookmark.count():
|
||||
return self.bookmark.first()
|
||||
new_bookmark = BookMark()
|
||||
new_bookmark.linked_object = self
|
||||
new_bookmark.save()
|
||||
return new_bookmark
|
||||
content_type = ContentType.objects.get_for_model(self)
|
||||
object_id = self.pk
|
||||
return _get_or_create_bookmark(content_type, object_id)
|
||||
|
||||
|
||||
def dirty_bookmark(bookmark, profile):
|
||||
bookmark.get_bookmark.dirty(bookmark, profile)
|
||||
_get_or_create_bookmark.dirty(bookmark.content_type, bookmark.object_id)
|
||||
|
|
|
@ -20,6 +20,7 @@ from judge.models.interface import BlogPost
|
|||
from judge.models.problem import Problem, Solution
|
||||
from judge.models.profile import Profile
|
||||
from judge.utils.cachedict import CacheDict
|
||||
from judge.caching import cache_wrapper
|
||||
|
||||
|
||||
__all__ = ["Comment", "CommentLock", "CommentVote", "Notification"]
|
||||
|
@ -75,16 +76,16 @@ class Comment(MPTTModel):
|
|||
queryset = (
|
||||
cls.objects.filter(hidden=False)
|
||||
.select_related("author__user")
|
||||
.defer("author__about", "body")
|
||||
.defer("author__about")
|
||||
.order_by("-id")
|
||||
)
|
||||
|
||||
if organization:
|
||||
queryset = queryset.filter(author__in=organization.members.all())
|
||||
|
||||
problem_access = CacheDict(lambda p: p.is_accessible_by(user))
|
||||
object_access = CacheDict(lambda p: p.is_accessible_by(user))
|
||||
contest_access = CacheDict(lambda c: c.is_accessible_by(user))
|
||||
blog_access = CacheDict(lambda b: b.can_see(user))
|
||||
blog_access = CacheDict(lambda b: b.is_accessible_by(user))
|
||||
|
||||
if n == -1:
|
||||
n = len(queryset)
|
||||
|
@ -174,3 +175,10 @@ class CommentLock(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
return str(self.page)
|
||||
|
||||
|
||||
@cache_wrapper(prefix="gcc")
|
||||
def get_visible_comment_count(content_type, object_id):
|
||||
return Comment.objects.filter(
|
||||
content_type=content_type, object_id=object_id, hidden=False
|
||||
).count()
|
||||
|
|
|
@ -13,6 +13,7 @@ from mptt.models import MPTTModel
|
|||
from judge.models.profile import Organization, Profile
|
||||
from judge.models.pagevote import PageVotable
|
||||
from judge.models.bookmark import Bookmarkable
|
||||
from judge.caching import cache_wrapper
|
||||
|
||||
__all__ = ["MiscConfig", "validate_regex", "NavigationBar", "BlogPost"]
|
||||
|
||||
|
@ -105,7 +106,7 @@ class BlogPost(models.Model, PageVotable, Bookmarkable):
|
|||
def get_absolute_url(self):
|
||||
return reverse("blog_post", args=(self.id, self.slug))
|
||||
|
||||
def can_see(self, user):
|
||||
def is_accessible_by(self, user):
|
||||
if self.visible and self.publish_on <= timezone.now():
|
||||
if not self.is_organization_private:
|
||||
return True
|
||||
|
@ -132,6 +133,10 @@ class BlogPost(models.Model, PageVotable, Bookmarkable):
|
|||
and self.authors.filter(id=user.profile.id).exists()
|
||||
)
|
||||
|
||||
@cache_wrapper(prefix="BPga")
|
||||
def get_authors(self):
|
||||
return self.authors.only("id")
|
||||
|
||||
class Meta:
|
||||
permissions = (("edit_all_post", _("Edit all posts")),)
|
||||
verbose_name = _("blog post")
|
||||
|
|
|
@ -31,11 +31,8 @@ class PageVote(models.Model):
|
|||
|
||||
@cache_wrapper(prefix="PVvs")
|
||||
def vote_score(self, user):
|
||||
page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user)
|
||||
if page_vote.exists():
|
||||
return page_vote.first().score
|
||||
else:
|
||||
return 0
|
||||
page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user).first()
|
||||
return page_vote.score if page_vote else 0
|
||||
|
||||
def __str__(self):
|
||||
return f"pagevote for {self.linked_object}"
|
||||
|
@ -52,11 +49,22 @@ class PageVoteVoter(models.Model):
|
|||
verbose_name_plural = _("pagevote votes")
|
||||
|
||||
|
||||
@cache_wrapper(prefix="gocp")
|
||||
def _get_or_create_pagevote(content_type, object_id):
|
||||
pagevote, created = PageVote.objects.get_or_create(
|
||||
content_type=content_type,
|
||||
object_id=object_id,
|
||||
)
|
||||
return pagevote
|
||||
|
||||
|
||||
class PageVotable:
|
||||
def get_or_create_pagevote(self):
|
||||
if self.pagevote.count():
|
||||
return self.pagevote.first()
|
||||
new_pagevote = PageVote()
|
||||
new_pagevote.linked_object = self
|
||||
new_pagevote.save()
|
||||
return new_pagevote
|
||||
content_type = ContentType.objects.get_for_model(self)
|
||||
object_id = self.pk
|
||||
return _get_or_create_pagevote(content_type, object_id)
|
||||
|
||||
|
||||
def dirty_pagevote(pagevote, profile):
|
||||
pagevote.vote_score.dirty(pagevote, profile)
|
||||
_get_or_create_pagevote.dirty(pagevote.content_type, pagevote.object_id)
|
||||
|
|
|
@ -24,6 +24,7 @@ from judge.models.problem_data import (
|
|||
problem_data_storage,
|
||||
problem_directory_file_helper,
|
||||
)
|
||||
from judge.caching import cache_wrapper
|
||||
|
||||
__all__ = [
|
||||
"ProblemGroup",
|
||||
|
@ -439,6 +440,10 @@ class Problem(models.Model, PageVotable, Bookmarkable):
|
|||
"profile_id", flat=True
|
||||
)
|
||||
|
||||
@cache_wrapper(prefix="Pga")
|
||||
def get_authors(self):
|
||||
return self.authors.only("id")
|
||||
|
||||
@cached_property
|
||||
def editor_ids(self):
|
||||
return self.author_ids.union(
|
||||
|
|
|
@ -141,6 +141,14 @@ class Organization(models.Model):
|
|||
def get_submissions_url(self):
|
||||
return reverse("organization_submissions", args=(self.id, self.slug))
|
||||
|
||||
@cache_wrapper("Oia")
|
||||
def is_admin(self, profile):
|
||||
return self.admins.filter(id=profile.id).exists()
|
||||
|
||||
@cache_wrapper("Oim")
|
||||
def is_member(self, profile):
|
||||
return profile in self
|
||||
|
||||
class Meta:
|
||||
ordering = ["name"]
|
||||
permissions = (
|
||||
|
@ -280,6 +288,14 @@ class Profile(models.Model):
|
|||
def is_muted(self):
|
||||
return self._cached_info["mute"]
|
||||
|
||||
@cached_property
|
||||
def cached_display_rank(self):
|
||||
return self._cached_info.get("display_rank")
|
||||
|
||||
@cached_property
|
||||
def cached_rating(self):
|
||||
return self._cached_info.get("rating")
|
||||
|
||||
@cached_property
|
||||
def profile_image_url(self):
|
||||
return self._cached_info.get("profile_image_url")
|
||||
|
@ -374,7 +390,7 @@ class Profile(models.Model):
|
|||
|
||||
@cached_property
|
||||
def css_class(self):
|
||||
return self.get_user_css_class(self.display_rank, self.rating)
|
||||
return self.get_user_css_class(self.cached_display_rank, self.cached_rating)
|
||||
|
||||
def get_friends(self): # list of ids, including you
|
||||
friend_obj = self.following_users.prefetch_related("users")
|
||||
|
@ -388,11 +404,7 @@ class Profile(models.Model):
|
|||
if not self.user.is_authenticated:
|
||||
return False
|
||||
profile_id = self.id
|
||||
return (
|
||||
org.admins.filter(id=profile_id).exists()
|
||||
or org.registrant_id == profile_id
|
||||
or self.user.is_superuser
|
||||
)
|
||||
return org.is_admin(self) or self.user.is_superuser
|
||||
|
||||
@classmethod
|
||||
def prefetch_profile_cache(self, profile_ids):
|
||||
|
@ -544,7 +556,7 @@ def on_profile_save(sender, instance, **kwargs):
|
|||
_get_basic_info.dirty(instance.id)
|
||||
|
||||
|
||||
@cache_wrapper(prefix="Pgbi2")
|
||||
@cache_wrapper(prefix="Pgbi3")
|
||||
def _get_basic_info(profile_id):
|
||||
profile = (
|
||||
Profile.objects.select_related("user")
|
||||
|
@ -556,6 +568,8 @@ def _get_basic_info(profile_id):
|
|||
"user__email",
|
||||
"user__first_name",
|
||||
"user__last_name",
|
||||
"display_rank",
|
||||
"rating",
|
||||
)
|
||||
.get(id=profile_id)
|
||||
)
|
||||
|
@ -569,6 +583,8 @@ def _get_basic_info(profile_id):
|
|||
"profile_image_url": profile.profile_image.url
|
||||
if profile.profile_image
|
||||
else None,
|
||||
"display_rank": profile.display_rank,
|
||||
"rating": profile.rating,
|
||||
}
|
||||
res = {k: v for k, v in res.items() if v is not None}
|
||||
return res
|
||||
|
|
|
@ -55,18 +55,13 @@ def problem_update(sender, instance, **kwargs):
|
|||
for lang, _ in settings.LANGUAGES
|
||||
]
|
||||
)
|
||||
cache.delete_many(
|
||||
[
|
||||
make_template_fragment_key("problem_authors", (instance.id, lang))
|
||||
for lang, _ in settings.LANGUAGES
|
||||
]
|
||||
)
|
||||
cache.delete_many(
|
||||
[
|
||||
"generated-meta-problem:%s:%d" % (lang, instance.id)
|
||||
for lang, _ in settings.LANGUAGES
|
||||
]
|
||||
)
|
||||
Problem.get_authors.dirty(instance)
|
||||
|
||||
for lang, _ in settings.LANGUAGES:
|
||||
unlink_if_exists(get_pdf_path("%s.%s.pdf" % (instance.code, lang)))
|
||||
|
@ -129,6 +124,7 @@ def post_update(sender, instance, **kwargs):
|
|||
]
|
||||
+ [make_template_fragment_key("post_content", (instance.id,))]
|
||||
)
|
||||
BlogPost.get_authors.dirty(instance)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Submission)
|
||||
|
@ -146,6 +142,8 @@ def contest_submission_delete(sender, instance, **kwargs):
|
|||
@receiver(post_save, sender=Organization)
|
||||
def organization_update(sender, instance, **kwargs):
|
||||
cache.delete_many([make_template_fragment_key("organization_html", (instance.id,))])
|
||||
for admin in instance.admins.all():
|
||||
Organization.is_admin.dirty(instance, admin)
|
||||
|
||||
|
||||
_misc_config_i18n = [code for code, _ in settings.LANGUAGES]
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.utils.functional import lazy
|
|||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import ListView
|
||||
|
||||
from judge.comments import CommentedDetailView
|
||||
from judge.views.comment import CommentedDetailView
|
||||
from judge.views.pagevote import PageVoteDetailView
|
||||
from judge.views.bookmark import BookMarkDetailView
|
||||
from judge.models import (
|
||||
|
@ -69,13 +69,18 @@ class HomeFeedView(FeedView):
|
|||
profile_queryset = Profile.objects
|
||||
if self.request.organization:
|
||||
profile_queryset = self.request.organization.members
|
||||
context["top_rated"] = profile_queryset.filter(is_unlisted=False).order_by(
|
||||
"-rating"
|
||||
)[:10]
|
||||
context["top_scorer"] = profile_queryset.filter(is_unlisted=False).order_by(
|
||||
"-performance_points"
|
||||
)[:10]
|
||||
|
||||
context["top_rated"] = (
|
||||
profile_queryset.filter(is_unlisted=False)
|
||||
.order_by("-rating")
|
||||
.only("id", "rating")[:10]
|
||||
)
|
||||
context["top_scorer"] = (
|
||||
profile_queryset.filter(is_unlisted=False)
|
||||
.order_by("-performance_points")
|
||||
.only("id", "performance_points")[:10]
|
||||
)
|
||||
Profile.prefetch_profile_cache([p.id for p in context["top_rated"]])
|
||||
Profile.prefetch_profile_cache([p.id for p in context["top_scorer"]])
|
||||
return context
|
||||
|
||||
|
||||
|
@ -90,7 +95,7 @@ class PostList(HomeFeedView):
|
|||
queryset = (
|
||||
BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now())
|
||||
.order_by("-sticky", "-publish_on")
|
||||
.prefetch_related("authors__user", "organizations")
|
||||
.prefetch_related("organizations")
|
||||
)
|
||||
filter = Q(is_organization_private=False)
|
||||
if self.request.user.is_authenticated:
|
||||
|
@ -198,6 +203,6 @@ class PostView(TitleMixin, CommentedDetailView, PageVoteDetailView, BookMarkDeta
|
|||
|
||||
def get_object(self, queryset=None):
|
||||
post = super(PostView, self).get_object(queryset)
|
||||
if not post.can_see(self.request.user):
|
||||
if not post.is_accessible_by(self.request.user):
|
||||
raise Http404()
|
||||
return post
|
||||
|
|
|
@ -8,12 +8,12 @@ from django.http import (
|
|||
HttpResponseForbidden,
|
||||
)
|
||||
from django.utils.translation import gettext as _
|
||||
from judge.models.bookmark import BookMark, MakeBookMark
|
||||
from django.views.generic.base import TemplateResponseMixin
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
|
||||
from django.views.generic import View, ListView
|
||||
|
||||
from judge.models.bookmark import BookMark, MakeBookMark, dirty_bookmark
|
||||
|
||||
__all__ = [
|
||||
"dobookmark_page",
|
||||
|
@ -32,30 +32,31 @@ def bookmark_page(request, delta):
|
|||
|
||||
try:
|
||||
bookmark_id = int(request.POST["id"])
|
||||
bookmark_page = BookMark.objects.filter(id=bookmark_id)
|
||||
bookmark = BookMark.objects.get(id=bookmark_id)
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest()
|
||||
else:
|
||||
if not bookmark_page.exists():
|
||||
except BookMark.DoesNotExist:
|
||||
raise Http404()
|
||||
|
||||
if delta == 0:
|
||||
bookmarklist = MakeBookMark.objects.filter(
|
||||
bookmark=bookmark_page.first(), user=request.profile
|
||||
bookmark=bookmark, user=request.profile
|
||||
)
|
||||
if not bookmarklist.exists():
|
||||
newbookmark = MakeBookMark(
|
||||
bookmark=bookmark_page.first(),
|
||||
bookmark=bookmark,
|
||||
user=request.profile,
|
||||
)
|
||||
newbookmark.save()
|
||||
else:
|
||||
bookmarklist = MakeBookMark.objects.filter(
|
||||
bookmark=bookmark_page.first(), user=request.profile
|
||||
bookmark=bookmark, user=request.profile
|
||||
)
|
||||
if bookmarklist.exists():
|
||||
bookmarklist.delete()
|
||||
|
||||
dirty_bookmark(bookmark, request.profile)
|
||||
|
||||
return HttpResponse("success", content_type="text/plain")
|
||||
|
||||
|
||||
|
|
|
@ -1,36 +1,45 @@
|
|||
from django.conf import settings
|
||||
import json
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.context_processors import PermWrapper
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
||||
from django.contrib.auth.context_processors import PermWrapper
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Q, F, Count, FilteredRelation
|
||||
from django.db.models import Count, F, FilteredRelation, Q
|
||||
from django.db.models.expressions import Value
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db.models.expressions import F, Value
|
||||
from django.forms.models import ModelForm
|
||||
from django.forms import ModelForm
|
||||
from django.http import (
|
||||
Http404,
|
||||
HttpResponse,
|
||||
HttpResponseBadRequest,
|
||||
HttpResponseForbidden,
|
||||
HttpResponseNotFound,
|
||||
HttpResponseRedirect,
|
||||
)
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views.generic import DetailView, UpdateView
|
||||
from django.urls import reverse_lazy
|
||||
from reversion import revisions
|
||||
from reversion.models import Version
|
||||
from django.conf import settings
|
||||
from django.views.generic import DetailView, UpdateView, View
|
||||
from django.views.generic.base import TemplateResponseMixin
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django_ratelimit.decorators import ratelimit
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from judge.models import Comment, CommentVote, Notification, BlogPost
|
||||
from reversion import revisions
|
||||
from reversion.models import Revision, Version
|
||||
|
||||
from judge.jinja2.reference import get_user_from_text
|
||||
from judge.models import BlogPost, Comment, CommentVote, Notification
|
||||
from judge.models.notification import make_notification
|
||||
from judge.models.comment import get_visible_comment_count
|
||||
from judge.utils.views import TitleMixin
|
||||
from judge.widgets import HeavyPreviewPageDownWidget
|
||||
from judge.comments import add_mention_notifications
|
||||
|
||||
import json
|
||||
|
||||
__all__ = [
|
||||
"upvote_comment",
|
||||
|
@ -40,6 +49,18 @@ __all__ = [
|
|||
"CommentEdit",
|
||||
]
|
||||
|
||||
DEFAULT_OFFSET = 10
|
||||
|
||||
|
||||
def _get_html_link_notification(comment):
|
||||
return f'<a href="{comment.get_absolute_url()}">{comment.page_title}</a>'
|
||||
|
||||
|
||||
def add_mention_notifications(comment):
|
||||
users_mentioned = get_user_from_text(comment.body).exclude(id=comment.author.id)
|
||||
link = _get_html_link_notification(comment)
|
||||
make_notification(users_mentioned, "Mention", link, comment.author)
|
||||
|
||||
|
||||
@ratelimit(key="user", rate=settings.RL_VOTE)
|
||||
@login_required
|
||||
|
@ -290,4 +311,204 @@ def comment_hide(request):
|
|||
|
||||
comment = get_object_or_404(Comment, id=comment_id)
|
||||
comment.get_descendants(include_self=True).update(hidden=True)
|
||||
get_visible_comment_count.dirty(comment.content_type, comment.object_id)
|
||||
return HttpResponse("ok")
|
||||
|
||||
|
||||
class CommentForm(ModelForm):
|
||||
class Meta:
|
||||
model = Comment
|
||||
fields = ["body", "parent"]
|
||||
widgets = {
|
||||
"parent": forms.HiddenInput(),
|
||||
}
|
||||
|
||||
if HeavyPreviewPageDownWidget is not None:
|
||||
widgets["body"] = HeavyPreviewPageDownWidget(
|
||||
preview=reverse_lazy("comment_preview"),
|
||||
preview_timeout=1000,
|
||||
hide_preview_button=True,
|
||||
)
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
super(CommentForm, self).__init__(*args, **kwargs)
|
||||
self.fields["body"].widget.attrs.update({"placeholder": _("Comment body")})
|
||||
|
||||
def clean(self):
|
||||
if self.request is not None and self.request.user.is_authenticated:
|
||||
profile = self.request.profile
|
||||
if profile.mute:
|
||||
raise ValidationError(_("Your part is silent, little toad."))
|
||||
elif (
|
||||
not self.request.user.is_staff
|
||||
and not profile.submission_set.filter(
|
||||
points=F("problem__points")
|
||||
).exists()
|
||||
):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"You need to have solved at least one problem "
|
||||
"before your voice can be heard."
|
||||
)
|
||||
)
|
||||
return super(CommentForm, self).clean()
|
||||
|
||||
|
||||
class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
|
||||
comment_page = None
|
||||
|
||||
def is_comment_locked(self):
|
||||
if self.request.user.has_perm("judge.override_comment_lock"):
|
||||
return False
|
||||
return (
|
||||
self.request.in_contest
|
||||
and self.request.participation.contest.use_clarifications
|
||||
)
|
||||
|
||||
@method_decorator(ratelimit(key="user", rate=settings.RL_COMMENT))
|
||||
@method_decorator(login_required)
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if self.is_comment_locked():
|
||||
return HttpResponseForbidden()
|
||||
|
||||
parent = request.POST.get("parent")
|
||||
if parent:
|
||||
try:
|
||||
parent = int(parent)
|
||||
except ValueError:
|
||||
return HttpResponseNotFound()
|
||||
else:
|
||||
if not self.object.comments.filter(hidden=False, id=parent).exists():
|
||||
return HttpResponseNotFound()
|
||||
|
||||
form = CommentForm(request, request.POST)
|
||||
if form.is_valid():
|
||||
comment = form.save(commit=False)
|
||||
comment.author = request.profile
|
||||
comment.linked_object = self.object
|
||||
|
||||
with revisions.create_revision():
|
||||
revisions.set_user(request.user)
|
||||
revisions.set_comment(_("Posted comment"))
|
||||
comment.save()
|
||||
|
||||
# add notification for reply
|
||||
comment_notif_link = _get_html_link_notification(comment)
|
||||
if comment.parent and comment.parent.author != comment.author:
|
||||
make_notification(
|
||||
[comment.parent.author], "Reply", comment_notif_link, comment.author
|
||||
)
|
||||
|
||||
# add notification for page authors
|
||||
page_authors = comment.linked_object.authors.all()
|
||||
make_notification(
|
||||
page_authors, "Comment", comment_notif_link, comment.author
|
||||
)
|
||||
|
||||
add_mention_notifications(comment)
|
||||
get_visible_comment_count.dirty(comment.content_type, comment.object_id)
|
||||
|
||||
return HttpResponseRedirect(comment.get_absolute_url())
|
||||
|
||||
context = self.get_context_data(object=self.object, comment_form=form)
|
||||
return self.render_to_response(context)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
target_comment = None
|
||||
self.object = self.get_object()
|
||||
if "comment-id" in request.GET:
|
||||
try:
|
||||
comment_id = int(request.GET["comment-id"])
|
||||
comment_obj = Comment.objects.get(id=comment_id)
|
||||
except (Comment.DoesNotExist, ValueError):
|
||||
raise Http404
|
||||
if comment_obj.linked_object != self.object:
|
||||
raise Http404
|
||||
target_comment = comment_obj.get_root()
|
||||
return self.render_to_response(
|
||||
self.get_context_data(
|
||||
object=self.object,
|
||||
target_comment=target_comment,
|
||||
comment_form=CommentForm(request, initial={"parent": None}),
|
||||
)
|
||||
)
|
||||
|
||||
def _get_queryset(self, target_comment):
|
||||
if target_comment:
|
||||
queryset = target_comment.get_descendants(include_self=True)
|
||||
queryset = (
|
||||
queryset.select_related("author__user")
|
||||
.filter(hidden=False)
|
||||
.defer("author__about")
|
||||
)
|
||||
else:
|
||||
queryset = self.object.comments
|
||||
queryset = queryset.filter(parent=None, hidden=False)
|
||||
queryset = (
|
||||
queryset.select_related("author__user")
|
||||
.defer("author__about")
|
||||
.filter(hidden=False)
|
||||
.annotate(
|
||||
count_replies=Count("replies", distinct=True),
|
||||
)[:DEFAULT_OFFSET]
|
||||
)
|
||||
|
||||
if self.request.user.is_authenticated:
|
||||
profile = self.request.profile
|
||||
queryset = queryset.annotate(
|
||||
my_vote=FilteredRelation(
|
||||
"votes", condition=Q(votes__voter_id=profile.id)
|
||||
),
|
||||
).annotate(vote_score=Coalesce(F("my_vote__score"), Value(0)))
|
||||
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, target_comment=None, **kwargs):
|
||||
context = super(CommentedDetailView, self).get_context_data(**kwargs)
|
||||
queryset = self._get_queryset(target_comment)
|
||||
comment_count = self.object.comments.filter(parent=None, hidden=False).count()
|
||||
|
||||
content_type = ContentType.objects.get_for_model(self.object)
|
||||
all_comment_count = get_visible_comment_count(content_type, self.object.pk)
|
||||
|
||||
if target_comment != None:
|
||||
context["target_comment"] = target_comment.id
|
||||
else:
|
||||
context["target_comment"] = -1
|
||||
|
||||
if self.request.user.is_authenticated:
|
||||
context["is_new_user"] = (
|
||||
not self.request.user.is_staff
|
||||
and not self.request.profile.submission_set.filter(
|
||||
points=F("problem__points")
|
||||
).exists()
|
||||
)
|
||||
|
||||
context["has_comments"] = queryset.exists()
|
||||
context["comment_lock"] = self.is_comment_locked()
|
||||
context["comment_list"] = list(queryset)
|
||||
|
||||
context["vote_hide_threshold"] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD
|
||||
|
||||
if queryset.exists():
|
||||
context["comment_root_id"] = context["comment_list"][0].id
|
||||
else:
|
||||
context["comment_root_id"] = 0
|
||||
|
||||
context["comment_parent_none"] = 1
|
||||
|
||||
if target_comment != None:
|
||||
context["offset"] = 0
|
||||
context["comment_more"] = comment_count - 1
|
||||
else:
|
||||
context["offset"] = DEFAULT_OFFSET
|
||||
context["comment_more"] = comment_count - DEFAULT_OFFSET
|
||||
|
||||
context["limit"] = DEFAULT_OFFSET
|
||||
context["comment_count"] = comment_count
|
||||
context["profile"] = self.request.profile
|
||||
context["all_comment_count"] = all_comment_count
|
||||
|
||||
return context
|
||||
|
|
|
@ -55,7 +55,7 @@ from django.views.generic.detail import (
|
|||
)
|
||||
|
||||
from judge import event_poster as event
|
||||
from judge.comments import CommentedDetailView
|
||||
from judge.views.comment import CommentedDetailView
|
||||
from judge.forms import ContestCloneForm
|
||||
from judge.models import (
|
||||
Contest,
|
||||
|
@ -956,7 +956,7 @@ class ContestStats(TitleMixin, ContestMixin, DetailView):
|
|||
|
||||
ContestRankingProfile = namedtuple(
|
||||
"ContestRankingProfile",
|
||||
"id user css_class username points cumtime tiebreaker organization participation "
|
||||
"id user points cumtime tiebreaker participation "
|
||||
"participation_rating problem_cells result_cell",
|
||||
)
|
||||
|
||||
|
@ -976,13 +976,10 @@ def make_contest_ranking_profile(
|
|||
user = participation.user
|
||||
return ContestRankingProfile(
|
||||
id=user.id,
|
||||
user=user.user,
|
||||
css_class=user.css_class,
|
||||
username=user.username,
|
||||
user=user,
|
||||
points=points,
|
||||
cumtime=cumtime,
|
||||
tiebreaker=participation.tiebreaker,
|
||||
organization=user.organization,
|
||||
participation_rating=participation.rating.rating
|
||||
if hasattr(participation, "rating")
|
||||
else None,
|
||||
|
@ -1000,12 +997,24 @@ def make_contest_ranking_profile(
|
|||
|
||||
|
||||
def base_contest_ranking_list(contest, problems, queryset, show_final=False):
|
||||
return [
|
||||
participation_fields = [
|
||||
field.name
|
||||
for field in ContestParticipation._meta.get_fields()
|
||||
if field.concrete and not field.many_to_many
|
||||
]
|
||||
fields_to_fetch = participation_fields + [
|
||||
"user__id",
|
||||
"rating__rating",
|
||||
]
|
||||
|
||||
res = [
|
||||
make_contest_ranking_profile(contest, participation, problems, show_final)
|
||||
for participation in queryset.select_related("user__user", "rating").defer(
|
||||
"user__about", "user__organizations__about"
|
||||
for participation in queryset.select_related("user", "rating").only(
|
||||
*fields_to_fetch
|
||||
)
|
||||
]
|
||||
Profile.prefetch_profile_cache([p.id for p in res])
|
||||
return res
|
||||
|
||||
|
||||
def contest_ranking_list(contest, problems, queryset=None, show_final=False):
|
||||
|
@ -1016,18 +1025,18 @@ def contest_ranking_list(contest, problems, queryset=None, show_final=False):
|
|||
return base_contest_ranking_list(
|
||||
contest,
|
||||
problems,
|
||||
queryset.prefetch_related("user__organizations")
|
||||
.extra(select={"round_score": "round(score, 6)"})
|
||||
.order_by("is_disqualified", "-round_score", "cumtime", "tiebreaker"),
|
||||
queryset.extra(select={"round_score": "round(score, 6)"}).order_by(
|
||||
"is_disqualified", "-round_score", "cumtime", "tiebreaker"
|
||||
),
|
||||
show_final,
|
||||
)
|
||||
else:
|
||||
return base_contest_ranking_list(
|
||||
contest,
|
||||
problems,
|
||||
queryset.prefetch_related("user__organizations")
|
||||
.extra(select={"round_score": "round(score_final, 6)"})
|
||||
.order_by("is_disqualified", "-round_score", "cumtime_final", "tiebreaker"),
|
||||
queryset.extra(select={"round_score": "round(score_final, 6)"}).order_by(
|
||||
"is_disqualified", "-round_score", "cumtime_final", "tiebreaker"
|
||||
),
|
||||
show_final,
|
||||
)
|
||||
|
||||
|
|
|
@ -104,15 +104,15 @@ class OrganizationBase(object):
|
|||
def is_member(self, org=None):
|
||||
if org is None:
|
||||
org = self.object
|
||||
return (
|
||||
self.request.profile in org if self.request.user.is_authenticated else False
|
||||
)
|
||||
if self.request.profile:
|
||||
return org.is_member(self.request.profile)
|
||||
return False
|
||||
|
||||
def is_admin(self, org=None):
|
||||
if org is None:
|
||||
org = self.object
|
||||
if self.request.profile:
|
||||
return org.admins.filter(id=self.request.profile.id).exists()
|
||||
return org.is_admin(self.request.profile)
|
||||
return False
|
||||
|
||||
def can_access(self, org):
|
||||
|
@ -222,12 +222,19 @@ class OrganizationHomeView(OrganizationMixin):
|
|||
organizations=self.organization,
|
||||
authors=self.request.profile,
|
||||
).count()
|
||||
context["top_rated"] = self.organization.members.filter(
|
||||
is_unlisted=False
|
||||
).order_by("-rating")[:10]
|
||||
context["top_scorer"] = self.organization.members.filter(
|
||||
is_unlisted=False
|
||||
).order_by("-performance_points")[:10]
|
||||
context["top_rated"] = (
|
||||
self.organization.members.filter(is_unlisted=False)
|
||||
.order_by("-rating")
|
||||
.only("id", "rating")[:10]
|
||||
)
|
||||
context["top_scorer"] = (
|
||||
self.organization.members.filter(is_unlisted=False)
|
||||
.order_by("-performance_points")
|
||||
.only("id", "performance_points")[:10]
|
||||
)
|
||||
Profile.prefetch_profile_cache([p.id for p in context["top_rated"]])
|
||||
Profile.prefetch_profile_cache([p.id for p in context["top_scorer"]])
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
@ -516,6 +523,7 @@ class JoinOrganization(OrganizationMembershipChange):
|
|||
profile.organizations.add(org)
|
||||
profile.save()
|
||||
cache.delete(make_template_fragment_key("org_member_count", (org.id,)))
|
||||
Organization.is_member.dirty(org, profile)
|
||||
|
||||
|
||||
class LeaveOrganization(OrganizationMembershipChange):
|
||||
|
@ -528,6 +536,7 @@ class LeaveOrganization(OrganizationMembershipChange):
|
|||
)
|
||||
profile.organizations.remove(org)
|
||||
cache.delete(make_template_fragment_key("org_member_count", (org.id,)))
|
||||
Organization.is_member.dirty(org, profile)
|
||||
|
||||
|
||||
class OrganizationRequestForm(Form):
|
||||
|
|
|
@ -8,14 +8,13 @@ from django.http import (
|
|||
HttpResponseForbidden,
|
||||
)
|
||||
from django.utils.translation import gettext as _
|
||||
from judge.models.pagevote import PageVote, PageVoteVoter
|
||||
from django.views.generic.base import TemplateResponseMixin
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
|
||||
from django.views.generic import View, ListView
|
||||
from django_ratelimit.decorators import ratelimit
|
||||
from django.conf import settings
|
||||
|
||||
from judge.models.pagevote import PageVote, PageVoteVoter, dirty_pagevote
|
||||
|
||||
__all__ = [
|
||||
"upvote_page",
|
||||
|
@ -54,8 +53,10 @@ def vote_page(request, delta):
|
|||
pagevote_id = int(request.POST["id"])
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest()
|
||||
else:
|
||||
if not PageVote.objects.filter(id=pagevote_id).exists():
|
||||
|
||||
try:
|
||||
pagevote = PageVote.objects.get(id=pagevote_id)
|
||||
except PageVote.DoesNotExist:
|
||||
raise Http404()
|
||||
|
||||
vote = PageVoteVoter()
|
||||
|
@ -76,7 +77,9 @@ def vote_page(request, delta):
|
|||
PageVote.objects.filter(id=pagevote_id).update(score=F("score") - vote.score)
|
||||
else:
|
||||
PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta)
|
||||
_dirty_vote_score(pagevote_id, request.profile)
|
||||
|
||||
dirty_pagevote(pagevote, request.profile)
|
||||
|
||||
return HttpResponse("success", content_type="text/plain")
|
||||
|
||||
|
||||
|
@ -100,8 +103,3 @@ class PageVoteDetailView(TemplateResponseMixin, SingleObjectMixin, View):
|
|||
context = super(PageVoteDetailView, self).get_context_data(**kwargs)
|
||||
context["pagevote"] = self.object.get_or_create_pagevote()
|
||||
return context
|
||||
|
||||
|
||||
def _dirty_vote_score(pagevote_id, profile):
|
||||
pv = PageVote(id=pagevote_id)
|
||||
pv.vote_score.dirty(pv, profile)
|
||||
|
|
|
@ -44,7 +44,7 @@ from django.views.generic import ListView, View
|
|||
from django.views.generic.base import TemplateResponseMixin
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
|
||||
from judge.comments import CommentedDetailView
|
||||
from judge.views.comment import CommentedDetailView
|
||||
from judge.forms import ProblemCloneForm, ProblemSubmitForm, ProblemPointsVoteForm
|
||||
from judge.models import (
|
||||
ContestProblem,
|
||||
|
@ -820,7 +820,7 @@ class ProblemFeed(ProblemList, FeedView):
|
|||
model = Problem
|
||||
context_object_name = "problems"
|
||||
template_name = "problem/feed.html"
|
||||
feed_content_template_name = "problem/feed/problems.html"
|
||||
feed_content_template_name = "problem/feed/items.html"
|
||||
paginate_by = 4
|
||||
title = _("Problem feed")
|
||||
feed_type = None
|
||||
|
|
|
@ -50,11 +50,9 @@ from judge.utils.timedelta import nice_repr
|
|||
|
||||
|
||||
def submission_related(queryset):
|
||||
return queryset.select_related("user__user", "problem", "language").only(
|
||||
return queryset.select_related("user", "problem", "language").only(
|
||||
"id",
|
||||
"user__user__username",
|
||||
"user__display_rank",
|
||||
"user__rating",
|
||||
"user_id",
|
||||
"problem__name",
|
||||
"problem__code",
|
||||
"problem__is_public",
|
||||
|
|
|
@ -29,6 +29,17 @@ th.header.rank {
|
|||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.user-with-img {
|
||||
display: inline-flex;
|
||||
gap: 0.5em;
|
||||
align-items: center;
|
||||
|
||||
.user-img {
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
#search-handle {
|
||||
width: 100%;
|
||||
height: 2.3em;
|
||||
|
|
|
@ -27,9 +27,9 @@
|
|||
<span class="actionbar-text">
|
||||
{{_("Comment")}}
|
||||
</span>
|
||||
{% if comment_count %}
|
||||
{% if all_comment_count %}
|
||||
<span style="margin-left: 0.2em">
|
||||
({{ comment_count }})
|
||||
({{ all_comment_count }})
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<div class="post-full">
|
||||
<div class="post-title">{{ title }}</div>
|
||||
<div class="time">
|
||||
{% with authors=post.authors.all() %}
|
||||
{% with authors=post.get_authors() %}
|
||||
{% if authors %}
|
||||
<span class="post-authors">{{ link_users(authors) }}</span>
|
||||
{% endif %}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{% for post in posts%}
|
||||
{% for post in posts %}
|
||||
<section class="{% if post.sticky %}sticky {% endif %}blog-box">
|
||||
<div style="margin-bottom: 0.5em">
|
||||
<span class="post-content-header time">
|
||||
{% with authors=post.authors.all() %}
|
||||
{% with authors=post.get_authors() %}
|
||||
{%- if authors -%}
|
||||
<span class="user-img" style="width: 1.5em; height: 1.5em">
|
||||
<img src="{{gravatar(authors[0])}}">
|
||||
|
@ -31,7 +31,7 @@
|
|||
<a href="{{ url('blog_post', post.id, post.slug) }}#comments" class="blog-comment-count-link">
|
||||
<i class="fa fa-comments blog-comment-icon"></i>
|
||||
<span class="blog-comment-count">
|
||||
{{- post.comments.filter(hidden=False).count() or 0 -}}
|
||||
{{ comment_count(post) }}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
|
|
|
@ -79,7 +79,9 @@
|
|||
{% include 'chat/user_online_status.html' %}
|
||||
</div>
|
||||
<div id="chat-box">
|
||||
<img src="{{static('loading.gif')}}" id="loader" style="display: none;">
|
||||
<span id="loader" style="font-size: 2em; display: none;">
|
||||
<i class="fa fa-spinner fa-pulse"></i>
|
||||
</span>
|
||||
<ul id="chat-log">
|
||||
{% include 'chat/message_list.html' %}
|
||||
</ul>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
{{ comment_form.media.js }}
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
let loading_gif = "<img src=\"{{static('loading.gif')}}\" style=\"height: 3em; margin-bottom: 3px\" class=\"loading\">";
|
||||
let loading_gif = "<i class=\"fa fa-spinner fa-pulse loading\" style=\"font-size: 1.5em\"></i>";
|
||||
window.reply_comment = function (parent) {
|
||||
var $comment_reply = $('#comment-' + parent + '-reply');
|
||||
var reply_id = 'reply-' + parent;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<style>
|
||||
#users-table td {
|
||||
height: 2.5em;
|
||||
min-height: 2.5em;
|
||||
}
|
||||
|
||||
#users-table a {
|
||||
|
|
|
@ -13,18 +13,23 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block user_footer %}
|
||||
{% if user.user.first_name %}
|
||||
{% set profile = user.user %}
|
||||
{% if profile.first_name %}
|
||||
<div style="font-weight: 600; display: none" class="fullname gray">
|
||||
{{ user.user.first_name }}
|
||||
{{ profile.first_name }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if user.user.last_name %}
|
||||
{% if profile.last_name %}
|
||||
<div class="school gray" style="display: none"><div style="font-weight: 600">
|
||||
{{- user.user.last_name -}}
|
||||
{{ profile.last_name }}
|
||||
</div></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block user_link %}
|
||||
{{ link_user(user.user, show_image=True) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block user_data %}
|
||||
{% if user.participation.virtual %}
|
||||
<sub class="gray">[{{user.participation.virtual}}]</sub>
|
||||
|
|
|
@ -150,7 +150,7 @@
|
|||
<label for="show-total-score-checkbox" style="vertical-align: bottom; margin-right: 1em;">{{ _('Total score only') }}</label>
|
||||
<input id="show-virtual-checkbox" type="checkbox" style="vertical-align: bottom;">
|
||||
<label id="show-virtual-label" for="show-virtual-checkbox" style="vertical-align: bottom; margin-right: 1em;">{{ _('Show virtual participation') }}</label>
|
||||
<img src="{{static('loading.gif')}}" style="height: 1em; display:none;" id="loading-gif"></img>
|
||||
<i class="fa fa-spinner fa-pulse" style="display: none" id="loading-gif"></i>
|
||||
<a href="#" onclick="download_ranking_as_csv()">
|
||||
<i class="fa fa-download" aria-hidden="true"></i>
|
||||
{{ _('Download as CSV') }}
|
||||
|
|
|
@ -27,5 +27,5 @@
|
|||
<li><a href="{{url('admin:judge_volunteerproblemvote_changelist')}}">{{_('View your votes')}}</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% include "problem/feed/problems.html" %}
|
||||
{% include "problem/feed/items.html" %}
|
||||
{% endblock %}
|
|
@ -12,7 +12,7 @@
|
|||
<i class="unsolved-problem-color fa fa-minus-circle"></i>
|
||||
{% endif %}
|
||||
</h3>
|
||||
{% with authors=problem.authors.all() %}
|
||||
{% with authors=problem.get_authors() %}
|
||||
{% if authors %}
|
||||
<div class="problem-feed-info-entry">
|
||||
<i class="fa fa-pencil-square-o fa-fw"></i>
|
|
@ -260,8 +260,7 @@
|
|||
|
||||
<hr style="padding-top: 0.7em">
|
||||
|
||||
{% cache 86400 'problem_authors' problem.id LANGUAGE_CODE %}
|
||||
{% with authors=problem.authors.all() %}
|
||||
{% with authors=problem.get_authors() %}
|
||||
{% if authors %}
|
||||
<div class="problem-info-entry">
|
||||
<i class="fa fa-pencil-square-o fa-fw"></i><span
|
||||
|
@ -273,8 +272,7 @@
|
|||
<div class="pi-value authors-value">{{ link_users(authors) }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endcache %}
|
||||
{% endwith %}
|
||||
|
||||
{% if not contest_problem or not contest_problem.contest.hide_problem_tags %}
|
||||
<div id="problem-types">
|
||||
|
|
|
@ -19,7 +19,15 @@
|
|||
<tr id="user-{{ user.username }}" {% block row_extra scoped %}{% endblock %}>
|
||||
<td class="rank-td">{{ rank }}</td>
|
||||
{% block after_rank scoped %}{% endblock %}
|
||||
<td class="user-name"><div style="display: inline-block;">{{ link_user(user) }}{% block user_footer scoped %}{% endblock %}</div> {% block user_data scoped %}{% endblock %}</td>
|
||||
<td class="user-name">
|
||||
<div style="display: inline-block;">
|
||||
{% block user_link scoped %}
|
||||
{{ link_user(user) }}
|
||||
{% endblock %}
|
||||
{% block user_footer scoped %}{% endblock %}
|
||||
</div>
|
||||
{% block user_data scoped %}{% endblock %}
|
||||
</td>
|
||||
{% block before_point scoped %}{% endblock %}
|
||||
{% block point scoped %}
|
||||
<td title="{{ user.performance_points|floatformat(2) }}" class="user-points">
|
||||
|
|
|
@ -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