Infinite scrolling and comment migration

This commit is contained in:
cuom1999 2023-02-20 17:15:13 -06:00
parent 4b558bd656
commit 799ff5f8f8
33 changed files with 639 additions and 556 deletions

View file

@ -231,18 +231,19 @@ urlpatterns = [
url(r"^problems/", paged_list_view(problem.ProblemList, "problem_list")), url(r"^problems/", paged_list_view(problem.ProblemList, "problem_list")),
url(r"^problems/random/$", problem.RandomProblem.as_view(), name="problem_random"), url(r"^problems/random/$", problem.RandomProblem.as_view(), name="problem_random"),
url( url(
r"^problems/feed/", r"^problems/feed/$",
paged_list_view(problem.ProblemFeed, "problem_feed", feed_type="for_you"), problem.ProblemFeed.as_view(feed_type="for_you"),
name="problem_feed",
), ),
url( url(
r"^problems/feed/new/", r"^problems/feed/new/$",
paged_list_view(problem.ProblemFeed, "problem_feed_new", feed_type="new"), problem.ProblemFeed.as_view(feed_type="new"),
name="problem_feed_new",
), ),
url( url(
r"^problems/feed/volunteer/", r"^problems/feed/volunteer/$",
paged_list_view( problem.ProblemFeed.as_view(feed_type="volunteer"),
problem.ProblemFeed, "problem_feed_volunteer", feed_type="volunteer" name="problem_feed_volunteer",
),
), ),
url( url(
r"^problem/(?P<problem>[^/]+)", r"^problem/(?P<problem>[^/]+)",
@ -750,7 +751,7 @@ urlpatterns = [
] ]
), ),
), ),
url(r"^blog/", paged_list_view(blog.PostList, "blog_post_list")), url(r"^blog/", blog.PostList.as_view(), name="blog_post_list"),
url(r"^post/(?P<id>\d+)-(?P<slug>.*)$", blog.PostView.as_view(), name="blog_post"), url(r"^post/(?P<id>\d+)-(?P<slug>.*)$", blog.PostView.as_view(), name="blog_post"),
url(r"^license/(?P<key>[-\w.]+)$", license.LicenseDetail.as_view(), name="license"), url(r"^license/(?P<key>[-\w.]+)$", license.LicenseDetail.as_view(), name="license"),
url( url(

View file

@ -22,11 +22,23 @@ class CommentForm(ModelForm):
class CommentAdmin(VersionAdmin): class CommentAdmin(VersionAdmin):
fieldsets = ( fieldsets = (
(None, {"fields": ("author", "page", "parent", "score", "hidden")}), (
None,
{
"fields": (
"author",
"parent",
"score",
"hidden",
"content_type",
"object_id",
)
},
),
("Content", {"fields": ("body",)}), ("Content", {"fields": ("body",)}),
) )
list_display = ["author", "linked_page", "time"] list_display = ["author", "linked_object", "time"]
search_fields = ["author__user__username", "page", "body"] search_fields = ["author__user__username", "body"]
readonly_fields = ["score"] readonly_fields = ["score"]
actions = ["hide_comment", "unhide_comment"] actions = ["hide_comment", "unhide_comment"]
list_filter = ["hidden"] list_filter = ["hidden"]
@ -66,16 +78,6 @@ class CommentAdmin(VersionAdmin):
unhide_comment.short_description = _("Unhide comments") unhide_comment.short_description = _("Unhide comments")
def linked_page(self, obj):
link = obj.link
if link is not None:
return format_html('<a href="{0}">{1}</a>', link, obj.page)
else:
return format_html("{0}", obj.page)
linked_page.short_description = _("Associated page")
linked_page.admin_order_field = "page"
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
super(CommentAdmin, self).save_model(request, obj, form, change) super(CommentAdmin, self).save_model(request, obj, form, change)
if obj.hidden: if obj.hidden:

View file

@ -22,7 +22,7 @@ from reversion import revisions
from reversion.models import Revision, Version from reversion.models import Revision, Version
from judge.dblock import LockModel from judge.dblock import LockModel
from judge.models import Comment, CommentLock, Notification from judge.models import Comment, Notification
from judge.widgets import HeavyPreviewPageDownWidget from judge.widgets import HeavyPreviewPageDownWidget
from judge.jinja2.reference import get_user_from_text from judge.jinja2.reference import get_user_from_text
@ -90,7 +90,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
def is_comment_locked(self): def is_comment_locked(self):
if self.request.user.has_perm("judge.override_comment_lock"): if self.request.user.has_perm("judge.override_comment_lock"):
return False return False
return CommentLock.objects.filter(page=self.get_comment_page()).exists() or ( return (
self.request.in_contest self.request.in_contest
and self.request.participation.contest.use_clarifications and self.request.participation.contest.use_clarifications
) )
@ -99,7 +99,6 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
page = self.get_comment_page() page = self.get_comment_page()
if self.is_comment_locked(): if self.is_comment_locked():
return HttpResponseForbidden() return HttpResponseForbidden()
@ -110,9 +109,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
except ValueError: except ValueError:
return HttpResponseNotFound() return HttpResponseNotFound()
else: else:
if not Comment.objects.filter( if not self.object.comments.filter(hidden=False, id=parent).exists():
hidden=False, id=parent, page=page
).exists():
return HttpResponseNotFound() return HttpResponseNotFound()
form = CommentForm(request, request.POST) form = CommentForm(request, request.POST)
@ -120,6 +117,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
comment = form.save(commit=False) comment = form.save(commit=False)
comment.author = request.profile comment.author = request.profile
comment.page = page comment.page = page
comment.linked_object = self.object
with LockModel( with LockModel(
write=(Comment, Revision, Version), read=(ContentType,) write=(Comment, Revision, Version), read=(ContentType,)
@ -136,7 +134,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
notification_reply.save() notification_reply.save()
# add notification for page authors # add notification for page authors
page_authors = comment.page_object.authors.all() page_authors = comment.linked_object.authors.all()
for user in page_authors: for user in page_authors:
if user == comment.author: if user == comment.author:
continue continue
@ -149,7 +147,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
add_mention_notifications(comment) add_mention_notifications(comment)
return HttpResponseRedirect(request.path) return HttpResponseRedirect(comment.get_absolute_url())
context = self.get_context_data(object=self.object, comment_form=form) context = self.get_context_data(object=self.object, comment_form=form)
return self.render_to_response(context) return self.render_to_response(context)
@ -159,15 +157,13 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
return self.render_to_response( return self.render_to_response(
self.get_context_data( self.get_context_data(
object=self.object, object=self.object,
comment_form=CommentForm( comment_form=CommentForm(request, initial={"parent": None}),
request, initial={"page": self.get_comment_page(), "parent": None}
),
) )
) )
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CommentedDetailView, self).get_context_data(**kwargs) context = super(CommentedDetailView, self).get_context_data(**kwargs)
queryset = Comment.objects.filter(hidden=False, page=self.get_comment_page()) queryset = self.object.comments
context["has_comments"] = queryset.exists() context["has_comments"] = queryset.exists()
context["comment_lock"] = self.is_comment_locked() context["comment_lock"] = self.is_comment_locked()
queryset = ( queryset = (

View file

@ -0,0 +1,50 @@
# Generated by Django 3.2.18 on 2023-02-20 21:26
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("judge", "0150_alter_profile_timezone"),
]
operations = [
migrations.AddField(
model_name="comment",
name="content_type",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
preserve_default=False,
),
migrations.AddField(
model_name="comment",
name="object_id",
field=models.PositiveIntegerField(null=True),
preserve_default=False,
),
migrations.AlterField(
model_name="solution",
name="problem",
field=models.OneToOneField(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="solution",
to="judge.problem",
verbose_name="associated problem",
),
),
migrations.AddIndex(
model_name="comment",
index=models.Index(
fields=["content_type", "object_id"],
name="judge_comme_content_2dce05_idx",
),
),
]

View file

@ -0,0 +1,54 @@
from django.db import migrations, models
import django.db.models.deletion
from django.core.exceptions import ObjectDoesNotExist
def migrate_comments(apps, schema_editor):
Comment = apps.get_model("judge", "Comment")
Problem = apps.get_model("judge", "Problem")
Solution = apps.get_model("judge", "Solution")
BlogPost = apps.get_model("judge", "BlogPost")
Contest = apps.get_model("judge", "Contest")
for comment in Comment.objects.all():
page = comment.page
try:
if page.startswith("p:"):
code = page[2:]
comment.linked_object = Problem.objects.get(code=code)
elif page.startswith("s:"):
code = page[2:]
comment.linked_object = Solution.objects.get(problem__code=code)
elif page.startswith("c:"):
key = page[2:]
comment.linked_object = Contest.objects.get(key=key)
elif page.startswith("b:"):
blog_id = page[2:]
comment.linked_object = BlogPost.objects.get(id=blog_id)
comment.save()
except ObjectDoesNotExist:
comment.delete()
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("judge", "0151_comment_content_type"),
]
operations = [
migrations.RunPython(migrate_comments, migrations.RunPython.noop, atomic=True),
migrations.AlterField(
model_name="comment",
name="content_type",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
migrations.AlterField(
model_name="comment",
name="object_id",
field=models.PositiveIntegerField(),
),
]

View file

@ -3,10 +3,7 @@ from django.db.models import CASCADE
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from judge.models import Profile from judge.models.profile import Profile
from judge.models.contest import Contest
from judge.models.interface import BlogPost
from judge.models.problem import Problem, Solution
__all__ = ["BookMark"] __all__ = ["BookMark"]
@ -26,6 +23,10 @@ class BookMark(models.Model):
return False return False
def page_object(self): def page_object(self):
from judge.models.contest import Contest
from judge.models.interface import BlogPost
from judge.models.problem import Problem, Solution
try: try:
page = self.page page = self.page
if page.startswith("p:"): if page.startswith("p:"):

View file

@ -9,6 +9,8 @@ from django.db.models import CASCADE
from django.urls import reverse from django.urls import reverse
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from mptt.fields import TreeForeignKey from mptt.fields import TreeForeignKey
from mptt.models import MPTTModel from mptt.models import MPTTModel
from reversion.models import Version from reversion.models import Version
@ -44,6 +46,9 @@ class VersionRelation(GenericRelation):
class Comment(MPTTModel): class Comment(MPTTModel):
author = models.ForeignKey(Profile, verbose_name=_("commenter"), on_delete=CASCADE) author = models.ForeignKey(Profile, verbose_name=_("commenter"), on_delete=CASCADE)
time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True) time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
linked_object = GenericForeignKey("content_type", "object_id")
page = models.CharField( page = models.CharField(
max_length=30, max_length=30,
verbose_name=_("associated page"), verbose_name=_("associated page"),
@ -66,6 +71,9 @@ class Comment(MPTTModel):
class Meta: class Meta:
verbose_name = _("comment") verbose_name = _("comment")
verbose_name_plural = _("comments") verbose_name_plural = _("comments")
indexes = [
models.Index(fields=["content_type", "object_id"]),
]
class MPTTMeta: class MPTTMeta:
order_insertion_by = ["-time"] order_insertion_by = ["-time"]
@ -82,13 +90,9 @@ class Comment(MPTTModel):
if organization: if organization:
queryset = queryset.filter(author__in=organization.members.all()) queryset = queryset.filter(author__in=organization.members.all())
problem_access = CacheDict( problem_access = CacheDict(lambda p: p.is_accessible_by(user))
lambda code: Problem.objects.get(code=code).is_accessible_by(user) contest_access = CacheDict(lambda c: c.is_accessible_by(user))
) blog_access = CacheDict(lambda b: b.can_see(user))
contest_access = CacheDict(
lambda key: Contest.objects.get(key=key).is_accessible_by(user)
)
blog_access = CacheDict(lambda id: BlogPost.objects.get(id=id).can_see(user))
if n == -1: if n == -1:
n = len(queryset) n = len(queryset)
@ -102,112 +106,53 @@ class Comment(MPTTModel):
if not slice: if not slice:
break break
for comment in slice: for comment in slice:
if comment.page.startswith("p:") or comment.page.startswith("s:"): if isinstance(comment.linked_object, Problem):
try: if problem_access[comment.linked_object]:
if problem_access[comment.page[2:]]: output.append(comment)
output.append(comment) elif isinstance(comment.linked_object, Contest):
except Problem.DoesNotExist: if contest_access[comment.linked_object]:
pass output.append(comment)
elif comment.page.startswith("c:"): elif isinstance(comment.linked_object, BlogPost):
try: if blog_access[comment.linked_object]:
if contest_access[comment.page[2:]]: output.append(comment)
output.append(comment) elif isinstance(comment.linked_object, Solution):
except Contest.DoesNotExist: if problem_access[comment.linked_object.problem]:
pass output.append(comment)
elif comment.page.startswith("b:"):
try:
if blog_access[comment.page[2:]]:
output.append(comment)
except BlogPost.DoesNotExist:
pass
else:
output.append(comment)
if len(output) >= n: if len(output) >= n:
return output return output
return output return output
@cached_property @cached_property
def link(self): def page_title(self):
try: if isinstance(self.linked_object, Problem):
link = None return self.linked_object.name
if self.page.startswith("p:"): elif isinstance(self.linked_object, Contest):
link = reverse("problem_detail", args=(self.page[2:],)) return self.linked_object.name
elif self.page.startswith("c:"): elif isinstance(self.linked_object, Solution):
link = reverse("contest_view", args=(self.page[2:],)) return _("Editorial for ") + self.linked_object.problem.name
elif self.page.startswith("b:"): elif isinstance(self.linked_object, BlogPost):
key = "blog_slug:%s" % self.page[2:] return self.linked_object.title
slug = cache.get(key)
if slug is None:
try:
slug = BlogPost.objects.get(id=self.page[2:]).slug
except ObjectDoesNotExist:
slug = ""
cache.set(key, slug, 3600)
link = reverse("blog_post", args=(self.page[2:], slug))
elif self.page.startswith("s:"):
link = reverse("problem_editorial", args=(self.page[2:],))
except Exception:
link = "invalid"
return link
@classmethod
def get_page_title(cls, page):
try:
if page.startswith("p:"):
return Problem.objects.values_list("name", flat=True).get(code=page[2:])
elif page.startswith("c:"):
return Contest.objects.values_list("name", flat=True).get(key=page[2:])
elif page.startswith("b:"):
return BlogPost.objects.values_list("title", flat=True).get(id=page[2:])
elif page.startswith("s:"):
return _("Editorial for %s") % Problem.objects.values_list(
"name", flat=True
).get(code=page[2:])
return "<unknown>"
except ObjectDoesNotExist:
return "<deleted>"
@cached_property @cached_property
def page_title(self): def link(self):
return self.get_page_title(self.page) if isinstance(self.linked_object, Problem):
return reverse("problem_detail", args=(self.linked_object.code,))
elif isinstance(self.linked_object, Contest):
return reverse("contest_view", args=(self.linked_object.key,))
elif isinstance(self.linked_object, Solution):
return reverse("problem_editorial", args=(self.linked_object.problem.code,))
elif isinstance(self.linked_object, BlogPost):
return reverse(
"blog_post",
args=(
self.object_id,
self.linked_object.slug,
),
)
def get_absolute_url(self): def get_absolute_url(self):
return "%s#comment-%d" % (self.link, self.id) return "%s#comment-%d" % (self.link, self.id)
@cached_property
def page_object(self):
try:
page = self.page
if page.startswith("p:"):
return Problem.objects.get(code=page[2:])
elif page.startswith("c:"):
return Contest.objects.get(key=page[2:])
elif page.startswith("b:"):
return BlogPost.objects.get(id=page[2:])
elif page.startswith("s:"):
return Solution.objects.get(problem__code=page[2:])
return None
except ObjectDoesNotExist:
return None
def __str__(self):
return "%(page)s by %(user)s" % {
"page": self.page,
"user": self.author.user.username,
}
# Only use this when queried with
# .prefetch_related(Prefetch('votes', queryset=CommentVote.objects.filter(voter_id=profile_id)))
# It's rather stupid to put a query specific property on the model, but the alternative requires
# digging Django internals, and could not be guaranteed to work forever.
# Hence it is left here for when the alternative breaks.
# @property
# def vote_score(self):
# queryset = self.votes.all()
# if not queryset:
# return 0
# return queryset[0].score
class CommentVote(models.Model): class CommentVote(models.Model):
voter = models.ForeignKey(Profile, related_name="voted_comments", on_delete=CASCADE) voter = models.ForeignKey(Profile, related_name="voted_comments", on_delete=CASCADE)

View file

@ -6,6 +6,7 @@ from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import gettext, gettext_lazy as _ from django.utils.translation import gettext, gettext_lazy as _
from django.contrib.contenttypes.fields import GenericRelation
from jsonfield import JSONField from jsonfield import JSONField
from lupa import LuaRuntime from lupa import LuaRuntime
from moss import ( from moss import (
@ -297,6 +298,7 @@ class Contest(models.Model):
validators=[MinValueValidator(0), MaxValueValidator(10)], validators=[MinValueValidator(0), MaxValueValidator(10)],
help_text=_("Number of digits to round points to."), help_text=_("Number of digits to round points to."),
) )
comments = GenericRelation("Comment")
@cached_property @cached_property
def format_class(self): def format_class(self):

View file

@ -5,10 +5,14 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.functional import cached_property
from django.contrib.contenttypes.fields import GenericRelation
from mptt.fields import TreeForeignKey from mptt.fields import TreeForeignKey
from mptt.models import MPTTModel from mptt.models import MPTTModel
from judge.models.profile import Organization, Profile from judge.models.profile import Organization, Profile
from judge.models.pagevote import PageVote
from judge.models.bookmark import BookMark
__all__ = ["MiscConfig", "validate_regex", "NavigationBar", "BlogPost"] __all__ = ["MiscConfig", "validate_regex", "NavigationBar", "BlogPost"]
@ -91,6 +95,7 @@ class BlogPost(models.Model):
is_organization_private = models.BooleanField( is_organization_private = models.BooleanField(
verbose_name=_("private to organizations"), default=False verbose_name=_("private to organizations"), default=False
) )
comments = GenericRelation("Comment")
def __str__(self): def __str__(self):
return self.title return self.title
@ -125,6 +130,18 @@ class BlogPost(models.Model):
and self.authors.filter(id=user.profile.id).exists() and self.authors.filter(id=user.profile.id).exists()
) )
@cached_property
def pagevote(self):
page = "b:%s" % self.id
pagevote, _ = PageVote.objects.get_or_create(page=page)
return pagevote
@cached_property
def bookmark(self):
page = "b:%s" % self.id
bookmark, _ = BookMark.objects.get_or_create(page=page)
return bookmark
class Meta: class Meta:
permissions = (("edit_all_post", _("Edit all posts")),) permissions = (("edit_all_post", _("Edit all posts")),)
verbose_name = _("blog post") verbose_name = _("blog post")

View file

@ -2,7 +2,7 @@ from django.db import models
from django.db.models import CASCADE from django.db.models import CASCADE
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from judge.models import Profile from judge.models.profile import Profile
__all__ = ["PageVote", "PageVoteVoter"] __all__ = ["PageVote", "PageVoteVoter"]

View file

@ -13,6 +13,8 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from judge.fulltext import SearchQuerySet from judge.fulltext import SearchQuerySet
from judge.models.pagevote import PageVote
from judge.models.bookmark import BookMark
from judge.models.profile import Organization, Profile from judge.models.profile import Organization, Profile
from judge.models.runtime import Language from judge.models.runtime import Language
from judge.user_translations import gettext as user_gettext from judge.user_translations import gettext as user_gettext
@ -268,6 +270,7 @@ class Problem(models.Model):
objects = TranslatedProblemQuerySet.as_manager() objects = TranslatedProblemQuerySet.as_manager()
tickets = GenericRelation("Ticket") tickets = GenericRelation("Ticket")
comments = GenericRelation("Comment")
organizations = models.ManyToManyField( organizations = models.ManyToManyField(
Organization, Organization,
@ -444,6 +447,18 @@ class Problem(models.Model):
def usable_common_names(self): def usable_common_names(self):
return set(self.usable_languages.values_list("common_name", flat=True)) return set(self.usable_languages.values_list("common_name", flat=True))
@cached_property
def pagevote(self):
page = "p:%s" % self.code
pagevote, _ = PageVote.objects.get_or_create(page=page)
return pagevote
@cached_property
def bookmark(self):
page = "p:%s" % self.code
bookmark, _ = BookMark.objects.get_or_create(page=page)
return bookmark
@property @property
def usable_languages(self): def usable_languages(self):
return self.allowed_languages.filter( return self.allowed_languages.filter(
@ -644,7 +659,7 @@ class LanguageTemplate(models.Model):
class Solution(models.Model): class Solution(models.Model):
problem = models.OneToOneField( problem = models.OneToOneField(
Problem, Problem,
on_delete=SET_NULL, on_delete=CASCADE,
verbose_name=_("associated problem"), verbose_name=_("associated problem"),
null=True, null=True,
blank=True, blank=True,
@ -654,6 +669,7 @@ class Solution(models.Model):
publish_on = models.DateTimeField(verbose_name=_("publish date")) publish_on = models.DateTimeField(verbose_name=_("publish date"))
authors = models.ManyToManyField(Profile, verbose_name=_("authors"), blank=True) authors = models.ManyToManyField(Profile, verbose_name=_("authors"), blank=True)
content = models.TextField(verbose_name=_("editorial content")) content = models.TextField(verbose_name=_("editorial content"))
comments = GenericRelation("Comment")
def get_absolute_url(self): def get_absolute_url(self):
problem = self.problem problem = self.problem

View file

@ -7,8 +7,8 @@ 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.comments import CommentedDetailView
from judge.views.pagevote import PageVoteDetailView, PageVoteListView from judge.views.pagevote import PageVoteDetailView
from judge.views.bookmark import BookMarkDetailView, BookMarkListView from judge.views.bookmark import BookMarkDetailView
from judge.models import ( from judge.models import (
BlogPost, BlogPost,
Comment, Comment,
@ -26,28 +26,16 @@ from judge.utils.diggpaginator import DiggPaginator
from judge.utils.problems import user_completed_ids from judge.utils.problems import user_completed_ids
from judge.utils.tickets import filter_visible_tickets from judge.utils.tickets import filter_visible_tickets
from judge.utils.views import TitleMixin from judge.utils.views import TitleMixin
from judge.views.feed import FeedView
# General view for all content list on home feed # General view for all content list on home feed
class FeedView(ListView): class HomeFeedView(FeedView):
template_name = "blog/list.html" template_name = "blog/list.html"
title = None title = None
def get_paginator(
self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
):
return DiggPaginator(
queryset,
per_page,
body=6,
padding=2,
orphans=orphans,
allow_empty_first_page=allow_empty_first_page,
**kwargs
)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(FeedView, self).get_context_data(**kwargs) context = super(HomeFeedView, self).get_context_data(**kwargs)
context["has_clarifications"] = False context["has_clarifications"] = False
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
participation = self.request.profile.current_contest participation = self.request.profile.current_contest
@ -60,17 +48,7 @@ class FeedView(ListView):
if participation.contest.is_editable_by(self.request.user): if participation.contest.is_editable_by(self.request.user):
context["can_edit_contest"] = True context["can_edit_contest"] = True
context["page_titles"] = CacheDict(lambda page: Comment.get_page_title(page))
context["user_count"] = lazy(Profile.objects.count, int, int)
context["problem_count"] = lazy(
Problem.objects.filter(is_public=True).count, int, int
)
context["submission_count"] = lazy(Submission.objects.count, int, int)
context["language_count"] = lazy(Language.objects.count, int, int)
now = timezone.now() now = timezone.now()
visible_contests = ( visible_contests = (
Contest.get_visible_contests(self.request.user, show_own_contests_only=True) Contest.get_visible_contests(self.request.user, show_own_contests_only=True)
.filter(is_visible=True) .filter(is_visible=True)
@ -102,10 +80,12 @@ class FeedView(ListView):
return context return context
class PostList(FeedView, PageVoteListView, BookMarkListView): class PostList(HomeFeedView):
model = BlogPost model = BlogPost
paginate_by = 10 paginate_by = 4
context_object_name = "posts" context_object_name = "posts"
feed_content_template_name = "blog/content.html"
url_name = "blog_post_list"
def get_queryset(self): def get_queryset(self):
queryset = ( queryset = (
@ -121,13 +101,23 @@ class PostList(FeedView, PageVoteListView, BookMarkListView):
queryset = queryset.filter(filter) queryset = queryset.filter(filter)
return queryset return queryset
def get_feed_context(self, object_list):
post_comment_counts = {
int(page[2:]): count
for page, count in Comment.objects.filter(
page__in=["b:%d" % post.id for post in object_list], hidden=False
)
.values_list("page")
.annotate(count=Count("page"))
.order_by()
}
return {"post_comment_counts": post_comment_counts}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(PostList, self).get_context_data(**kwargs) context = super(PostList, self).get_context_data(**kwargs)
context["title"] = ( context["title"] = (
self.title or _("Page %d of Posts") % context["page_obj"].number self.title or _("Page %d of Posts") % context["page_obj"].number
) )
context["first_page_href"] = reverse("home")
context["page_prefix"] = reverse("blog_post_list")
context["page_type"] = "blog" context["page_type"] = "blog"
context["post_comment_counts"] = { context["post_comment_counts"] = {
int(page[2:]): count int(page[2:]): count
@ -138,18 +128,17 @@ class PostList(FeedView, PageVoteListView, BookMarkListView):
.annotate(count=Count("page")) .annotate(count=Count("page"))
.order_by() .order_by()
} }
context = self.add_pagevote_context_data(context)
context = self.add_bookmark_context_data(context)
return context return context
def get_comment_page(self, post): def get_comment_page(self, post):
return "b:%s" % post.id return "b:%s" % post.id
class TicketFeed(FeedView): class TicketFeed(HomeFeedView):
model = Ticket model = Ticket
context_object_name = "tickets" context_object_name = "tickets"
paginate_by = 30 paginate_by = 8
feed_content_template_name = "ticket/feed.html"
def get_queryset(self, is_own=True): def get_queryset(self, is_own=True):
profile = self.request.profile profile = self.request.profile
@ -181,30 +170,25 @@ class TicketFeed(FeedView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(TicketFeed, self).get_context_data(**kwargs) context = super(TicketFeed, self).get_context_data(**kwargs)
context["page_type"] = "ticket" context["page_type"] = "ticket"
context["first_page_href"] = self.request.path
context["page_prefix"] = "?page="
context["title"] = _("Ticket feed") context["title"] = _("Ticket feed")
return context return context
class CommentFeed(FeedView): class CommentFeed(HomeFeedView):
model = Comment model = Comment
context_object_name = "comments" context_object_name = "comments"
paginate_by = 50 paginate_by = 8
feed_content_template_name = "comments/feed.html"
def get_queryset(self): def get_queryset(self):
return Comment.most_recent( return Comment.most_recent(
self.request.user, 1000, organization=self.request.organization self.request.user, 100, organization=self.request.organization
) )
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(CommentFeed, self).get_context_data(**kwargs) context = super(CommentFeed, self).get_context_data(**kwargs)
context["page_type"] = "comment"
context["first_page_href"] = self.request.path
context["page_prefix"] = "?page="
context["title"] = _("Comment feed") context["title"] = _("Comment feed")
context["page_type"] = "comment"
return context return context

View file

@ -74,13 +74,3 @@ class BookMarkDetailView(TemplateResponseMixin, SingleObjectMixin, View):
queryset = BookMark.objects.get_or_create(page=self.get_comment_page()) queryset = BookMark.objects.get_or_create(page=self.get_comment_page())
context["bookmark"] = queryset[0] context["bookmark"] = queryset[0]
return context return context
class BookMarkListView:
def add_bookmark_context_data(self, context, obj_list="object_list"):
for item in context[obj_list]:
bookmark, _ = BookMark.objects.get_or_create(
page=self.get_comment_page(item)
)
setattr(item, "bookmark", bookmark)
return context

34
judge/views/feed.py Normal file
View file

@ -0,0 +1,34 @@
from django.views.generic import ListView
from django.shortcuts import render
from django.urls import reverse
from judge.utils.infinite_paginator import InfinitePaginationMixin
class FeedView(InfinitePaginationMixin, ListView):
def get_feed_context(selfl, object_list):
return {}
def get(self, request, *args, **kwargs):
only_content = request.GET.get("only_content", None)
if only_content and self.feed_content_template_name:
queryset = self.get_queryset()
paginator, page, object_list, _ = self.paginate_queryset(
queryset, self.paginate_by
)
context = {
self.context_object_name: object_list,
"has_next_page": page.has_next(),
}
context.update(self.get_feed_context(object_list))
return render(request, self.feed_content_template_name, context)
return super(FeedView, self).get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
try:
context["feed_content_url"] = reverse(self.url_name)
except Exception as e:
context["feed_content_url"] = self.request.path
return context

View file

@ -42,7 +42,6 @@ class NotificationList(ListView):
context["unseen_count"] = self.unseen_cnt context["unseen_count"] = self.unseen_cnt
context["title"] = _("Notifications (%d unseen)" % context["unseen_count"]) context["title"] = _("Notifications (%d unseen)" % context["unseen_count"])
context["has_notifications"] = self.queryset.exists() context["has_notifications"] = self.queryset.exists()
context["page_titles"] = CacheDict(lambda page: Comment.get_page_title(page))
return context return context
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):

View file

@ -72,9 +72,7 @@ from judge.utils.problems import user_attempted_ids, user_completed_ids
from judge.views.problem import ProblemList from judge.views.problem import ProblemList
from judge.views.contests import ContestList from judge.views.contests import ContestList
from judge.views.submission import AllSubmissions, SubmissionsListBase from judge.views.submission import AllSubmissions, SubmissionsListBase
from judge.views.pagevote import PageVoteListView from judge.views.feed import FeedView
from judge.views.bookmark import BookMarkListView
__all__ = [ __all__ = [
"OrganizationList", "OrganizationList",
@ -194,7 +192,7 @@ class MemberOrganizationMixin(OrganizationMixin):
) )
class OrganizationHomeViewContext: class OrganizationHomeView(OrganizationMixin):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
if not hasattr(self, "organization"): if not hasattr(self, "organization"):
@ -221,28 +219,6 @@ class OrganizationHomeViewContext:
return context return context
class OrganizationDetailView(
OrganizationMixin, OrganizationHomeViewContext, DetailView
):
context_object_name = "organization"
model = Organization
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.slug != kwargs["slug"]:
return HttpResponsePermanentRedirect(
request.get_full_path().replace(kwargs["slug"], self.object.slug)
)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["can_edit"] = self.can_edit_organization()
context["is_member"] = self.is_member()
return context
class OrganizationList(TitleMixin, ListView, OrganizationBase): class OrganizationList(TitleMixin, ListView, OrganizationBase):
model = Organization model = Organization
context_object_name = "organizations" context_object_name = "organizations"
@ -272,51 +248,50 @@ class OrganizationList(TitleMixin, ListView, OrganizationBase):
return context return context
class OrganizationHome(OrganizationDetailView, PageVoteListView, BookMarkListView): class OrganizationHome(OrganizationHomeView, FeedView):
template_name = "organization/home.html" template_name = "organization/home.html"
pagevote_object_name = "posts" paginate_by = 4
context_object_name = "posts"
feed_content_template_name = "blog/content.html"
def get_posts_and_page_obj(self): def get_queryset(self):
posts = ( return (
BlogPost.objects.filter( BlogPost.objects.filter(
visible=True, visible=True,
publish_on__lte=timezone.now(), publish_on__lte=timezone.now(),
is_organization_private=True, is_organization_private=True,
organizations=self.object, organizations=self.organization,
) )
.order_by("-sticky", "-publish_on") .order_by("-sticky", "-publish_on")
.prefetch_related("authors__user", "organizations") .prefetch_related("authors__user", "organizations")
) )
paginator = Paginator(posts, 10)
page_number = self.request.GET.get("page", 1)
posts = paginator.get_page(page_number)
return posts, paginator.page(page_number)
def get_comment_page(self, post): def get_comment_page(self, post):
return "b:%s" % post.id return "b:%s" % post.id
def get_feed_context(self, object_list):
post_comment_counts = {
int(page[2:]): count
for page, count in Comment.objects.filter(
page__in=["b:%d" % post.id for post in object_list], hidden=False
)
.values_list("page")
.annotate(count=Count("page"))
.order_by()
}
return {"post_comment_counts": post_comment_counts}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(OrganizationHome, self).get_context_data(**kwargs) context = super(OrganizationHome, self).get_context_data(**kwargs)
context["title"] = self.object.name context["title"] = self.organization.name
http = "http" if settings.DMOJ_SSL == 0 else "https" http = "http" if settings.DMOJ_SSL == 0 else "https"
context["organization_subdomain"] = ( context["organization_subdomain"] = (
http http
+ "://" + "://"
+ self.object.slug + self.organization.slug
+ "." + "."
+ get_current_site(self.request).domain + get_current_site(self.request).domain
) )
context["posts"], context["page_obj"] = self.get_posts_and_page_obj()
context = self.add_pagevote_context_data(context, "posts")
context = self.add_bookmark_context_data(context, "posts")
# Hack: This allows page_obj to have page_range for non-ListView class
setattr(
context["page_obj"], "page_range", context["posts"].paginator.page_range
)
context["first_page_href"] = self.request.path
context["page_prefix"] = "?page="
context["post_comment_counts"] = { context["post_comment_counts"] = {
int(page[2:]): count int(page[2:]): count
for page, count in Comment.objects.filter( for page, count in Comment.objects.filter(
@ -331,7 +306,9 @@ class OrganizationHome(OrganizationDetailView, PageVoteListView, BookMarkListVie
visible_contests = ( visible_contests = (
Contest.get_visible_contests(self.request.user) Contest.get_visible_contests(self.request.user)
.filter( .filter(
is_visible=True, is_organization_private=True, organizations=self.object is_visible=True,
is_organization_private=True,
organizations=self.organization,
) )
.order_by("start_time") .order_by("start_time")
) )
@ -344,11 +321,27 @@ class OrganizationHome(OrganizationDetailView, PageVoteListView, BookMarkListVie
return context return context
class OrganizationUsers(QueryStringSortMixin, OrganizationDetailView): class OrganizationUsers(QueryStringSortMixin, OrganizationMixin, FeedView):
template_name = "organization/users.html" template_name = "organization/users.html"
all_sorts = frozenset(("points", "problem_count", "rating", "performance_points")) all_sorts = frozenset(("points", "problem_count", "rating", "performance_points"))
default_desc = all_sorts default_desc = all_sorts
default_sort = "-performance_points" default_sort = "-performance_points"
context_object_name = "users"
def get_queryset(self):
return ranker(
self.organization.members.filter(is_unlisted=False)
.order_by(self.order, "id")
.select_related("user")
.only(
"display_rank",
"user__username",
"points",
"rating",
"performance_points",
"problem_count",
)
)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
res = super(OrganizationUsers, self).dispatch(request, *args, **kwargs) res = super(OrganizationUsers, self).dispatch(request, *args, **kwargs)
@ -363,26 +356,13 @@ class OrganizationUsers(QueryStringSortMixin, OrganizationDetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(OrganizationUsers, self).get_context_data(**kwargs) context = super(OrganizationUsers, self).get_context_data(**kwargs)
context["title"] = _("%s Members") % self.object.name context["title"] = _("%s Members") % self.organization.name
context["partial"] = True context["partial"] = True
context["kick_url"] = reverse( context["kick_url"] = reverse(
"organization_user_kick", args=[self.object.id, self.object.slug] "organization_user_kick",
args=[self.organization.id, self.organization.slug],
) )
context["users"] = ranker(
self.get_object()
.members.filter(is_unlisted=False)
.order_by(self.order, "id")
.select_related("user")
.only(
"display_rank",
"user__username",
"points",
"rating",
"performance_points",
"problem_count",
)
)
context["first_page_href"] = "." context["first_page_href"] = "."
context["page_type"] = "users" context["page_type"] = "users"
context.update(self.get_sort_context()) context.update(self.get_sort_context())
@ -421,8 +401,7 @@ class OrganizationProblems(LoginRequiredMixin, MemberOrganizationMixin, ProblemL
class OrganizationContestMixin( class OrganizationContestMixin(
LoginRequiredMixin, LoginRequiredMixin,
TitleMixin, TitleMixin,
OrganizationMixin, OrganizationHomeView,
OrganizationHomeViewContext,
): ):
model = Contest model = Contest
@ -613,8 +592,7 @@ class RequestJoinOrganization(LoginRequiredMixin, SingleObjectMixin, FormView):
class OrganizationRequestDetail( class OrganizationRequestDetail(
LoginRequiredMixin, LoginRequiredMixin,
TitleMixin, TitleMixin,
OrganizationMixin, OrganizationHomeView,
OrganizationHomeViewContext,
DetailView, DetailView,
): ):
model = OrganizationRequest model = OrganizationRequest
@ -639,7 +617,8 @@ OrganizationRequestFormSet = modelformset_factory(
class OrganizationRequestBaseView( class OrganizationRequestBaseView(
OrganizationDetailView, DetailView,
OrganizationHomeView,
TitleMixin, TitleMixin,
LoginRequiredMixin, LoginRequiredMixin,
SingleObjectTemplateResponseMixin, SingleObjectTemplateResponseMixin,
@ -760,7 +739,7 @@ class AddOrganizationMember(
LoginRequiredMixin, LoginRequiredMixin,
TitleMixin, TitleMixin,
AdminOrganizationMixin, AdminOrganizationMixin,
OrganizationDetailView, OrganizationHomeView,
UpdateView, UpdateView,
): ):
template_name = "organization/add-member.html" template_name = "organization/add-member.html"
@ -822,7 +801,7 @@ class EditOrganization(
LoginRequiredMixin, LoginRequiredMixin,
TitleMixin, TitleMixin,
AdminOrganizationMixin, AdminOrganizationMixin,
OrganizationDetailView, OrganizationHomeView,
UpdateView, UpdateView,
): ):
template_name = "organization/edit.html" template_name = "organization/edit.html"
@ -1023,7 +1002,7 @@ class EditOrganizationContest(
class AddOrganizationBlog( class AddOrganizationBlog(
LoginRequiredMixin, LoginRequiredMixin,
TitleMixin, TitleMixin,
OrganizationHomeViewContext, OrganizationHomeView,
MemberOrganizationMixin, MemberOrganizationMixin,
CreateView, CreateView,
): ):
@ -1074,7 +1053,7 @@ class AddOrganizationBlog(
class EditOrganizationBlog( class EditOrganizationBlog(
LoginRequiredMixin, LoginRequiredMixin,
TitleMixin, TitleMixin,
OrganizationHomeViewContext, OrganizationHomeView,
MemberOrganizationMixin, MemberOrganizationMixin,
UpdateView, UpdateView,
): ):
@ -1168,7 +1147,7 @@ class PendingBlogs(
LoginRequiredMixin, LoginRequiredMixin,
TitleMixin, TitleMixin,
MemberOrganizationMixin, MemberOrganizationMixin,
OrganizationHomeViewContext, OrganizationHomeView,
ListView, ListView,
): ):
model = BlogPost model = BlogPost

View file

@ -104,13 +104,3 @@ class PageVoteDetailView(TemplateResponseMixin, SingleObjectMixin, View):
queryset = PageVote.objects.get_or_create(page=self.get_comment_page()) queryset = PageVote.objects.get_or_create(page=self.get_comment_page())
context["pagevote"] = queryset[0] context["pagevote"] = queryset[0]
return context return context
class PageVoteListView:
def add_pagevote_context_data(self, context, obj_list="object_list"):
for item in context[obj_list]:
pagevote, _ = PageVote.objects.get_or_create(
page=self.get_comment_page(item)
)
setattr(item, "pagevote", pagevote)
return context

View file

@ -87,8 +87,9 @@ from judge.utils.views import (
generic_message, generic_message,
) )
from judge.ml.collab_filter import CollabFilter from judge.ml.collab_filter import CollabFilter
from judge.views.pagevote import PageVoteDetailView, PageVoteListView from judge.views.pagevote import PageVoteDetailView
from judge.views.bookmark import BookMarkDetailView, BookMarkListView from judge.views.bookmark import BookMarkDetailView
from judge.views.feed import FeedView
def get_contest_problem(problem, profile): def get_contest_problem(problem, profile):
@ -197,31 +198,34 @@ class ProblemSolution(
template_name = "problem/editorial.html" template_name = "problem/editorial.html"
def get_title(self): def get_title(self):
return _("Editorial for {0}").format(self.object.name) return _("Editorial for {0}").format(self.problem.name)
def get_content_title(self): def get_content_title(self):
return format_html( return format_html(
_('Editorial for <a href="{1}">{0}</a>'), _('Editorial for <a href="{1}">{0}</a>'),
self.object.name, self.problem.name,
reverse("problem_detail", args=[self.object.code]), reverse("problem_detail", args=[self.problem.code]),
) )
def get_object(self):
self.problem = super().get_object()
solution = get_object_or_404(Solution, problem=self.problem)
return solution
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ProblemSolution, self).get_context_data(**kwargs) context = super(ProblemSolution, self).get_context_data(**kwargs)
solution = self.get_object()
solution = get_object_or_404(Solution, problem=self.object)
if ( if (
not solution.is_public or solution.publish_on > timezone.now() not solution.is_public or solution.publish_on > timezone.now()
) and not self.request.user.has_perm("judge.see_private_solution"): ) and not self.request.user.has_perm("judge.see_private_solution"):
raise Http404() raise Http404()
context["solution"] = solution context["solution"] = solution
context["has_solved_problem"] = self.object.id in self.get_completed_problems() context["has_solved_problem"] = self.problem.id in self.get_completed_problems()
return context return context
def get_comment_page(self): def get_comment_page(self):
return "s:" + self.object.code return "s:" + self.problem.code
class ProblemRaw( class ProblemRaw(
@ -830,11 +834,12 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
return HttpResponseRedirect(request.get_full_path()) return HttpResponseRedirect(request.get_full_path())
class ProblemFeed(ProblemList, PageVoteListView, BookMarkListView): 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"
paginate_by = 20 feed_content_template_name = "problem/feed/problems.html"
paginate_by = 4
title = _("Problem feed") title = _("Problem feed")
feed_type = None feed_type = None
@ -843,19 +848,6 @@ class ProblemFeed(ProblemList, PageVoteListView, BookMarkListView):
return request.session.get(key, key == "hide_solved") return request.session.get(key, key == "hide_solved")
return request.GET.get(key, None) == "1" return request.GET.get(key, None) == "1"
def get_paginator(
self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
):
return DiggPaginator(
queryset,
per_page,
body=6,
padding=2,
orphans=orphans,
allow_empty_first_page=allow_empty_first_page,
**kwargs
)
def get_comment_page(self, problem): def get_comment_page(self, problem):
return "p:%s" % problem.code return "p:%s" % problem.code
@ -962,8 +954,6 @@ class ProblemFeed(ProblemList, PageVoteListView, BookMarkListView):
context["feed_type"] = self.feed_type context["feed_type"] = self.feed_type
context["has_show_editorial_option"] = False context["has_show_editorial_option"] = False
context["has_have_editorial_option"] = False context["has_have_editorial_option"] = False
context = self.add_pagevote_context_data(context)
context = self.add_bookmark_context_data(context)
return context return context

View file

@ -12,6 +12,6 @@ function mathjax_pagedown($) {
window.mathjax_pagedown = mathjax_pagedown; window.mathjax_pagedown = mathjax_pagedown;
$(window).load(function () { $(function () {
(mathjax_pagedown)('$' in window ? $ : django.jQuery); (mathjax_pagedown)('$' in window ? $ : django.jQuery);
}); });

View file

@ -45,7 +45,7 @@
</span> </span>
<span class="actionbar-block" style="justify-content: flex-end;"> <span class="actionbar-block" style="justify-content: flex-end;">
<span class="actionbar-button actionbar-share" style="position: relative" <span class="actionbar-button actionbar-share" style="position: relative"
{{"share-url=" + share_url if share_url else ""}}> {{"share-url=" + share_url if share_url else ""}} onclick="javascript:actionbar_share(this, event)">
<i class=" fa fa-share" style="font-size: large;"></i> <i class=" fa fa-share" style="font-size: large;"></i>
<span class="actionbar-text">{{_("Share")}}</span> <span class="actionbar-text">{{_("Share")}}</span>
</span> </span>

View file

@ -107,15 +107,15 @@
} }
} }
}; };
$(".actionbar-share").click(function(e) { window.actionbar_share = function(element, e) {
e.stopPropagation(); e.stopPropagation();
link = $(this).attr("share-url") || window.location.href; link = $(element).attr("share-url") || window.location.href;
navigator.clipboard navigator.clipboard
.writeText(link) .writeText(link)
.then(() => { .then(() => {
showTooltip(this, "Copied link", 'n'); showTooltip(element, "Copied link", 'n');
}); });
}); };
$('.actionbar-comment').on('click', function() { $('.actionbar-comment').on('click', function() {
$('#comment-section').show(); $('#comment-section').show();

View file

@ -1,55 +1,58 @@
<section class="{% if post.sticky %}sticky {% endif %}blog-box"> {% for post in posts%}
<div style="margin-bottom: 0.5em"> <section class="{% if post.sticky %}sticky {% endif %}blog-box">
<span class="time"> <div style="margin-bottom: 0.5em">
{% with authors=post.authors.all() %} <span class="time">
{%- if authors -%} {% with authors=post.authors.all() %}
<img src="{{gravatar(authors[0])}}" style="width: 1.5em; border-radius: 50%; margin-bottom: -0.3em"> {%- if authors -%}
<span class="post-authors">{{ link_users(authors) }}</span> <img src="{{gravatar(authors[0])}}" style="width: 1.5em; border-radius: 50%; margin-bottom: -0.3em">
{%- endif -%} <span class="post-authors">{{ link_users(authors) }}</span>
{% endwith %} {%- endif -%}
&#8226; {% endwith %}
{{ relative_time(post.publish_on, abs=_('on {time}'), rel=_('{time}')) -}} &#8226;
{%- if post.sticky %} &#8226; {{ relative_time(post.publish_on, abs=_('on {time}'), rel=_('{time}')) -}}
<i title="Sticky" class="fa fa-star fa-fw"></i>{% endif -%} {%- if post.sticky %} &#8226;
{% if post.is_organization_private and show_organization_private_icon %} <i title="Sticky" class="fa fa-star fa-fw"></i>{% endif -%}
&#8226; {% if post.is_organization_private and show_organization_private_icon %}
<span> &#8226;
{% for org in post.organizations.all() %} <span>
<span class="organization-tag" style="display: inherit;"> {% for org in post.organizations.all() %}
<a href="{{ org.get_absolute_url() }}"> <span class="organization-tag" style="display: inherit;">
<i class="fa fa-lock"></i> {{ org.name }} <a href="{{ org.get_absolute_url() }}">
</a> <i class="fa fa-lock"></i> {{ org.name }}
</span> </a>
{% endfor %} </span>
{% endfor %}
</span>
{% endif %}
</span>
<span style="float: right">
<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_comment_counts[post.id] or 0 -}}
</span>
</a>
</span> </span>
{% endif %}
</span>
<span style="float: right">
<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_comment_counts[post.id] or 0 -}}
</span>
</a>
</span>
</div>
<h2 class="title">
<a href="{{ url('blog_post', post.id, post.slug) }}">{{ post.title }}</a>
</h2>
<div class="blog-description">
<div class="summary content-description">
{% cache 86400 'post_summary' post.id %}
{{ post.summary|default(post.content, true)|markdown(lazy_load=True)|reference|str|safe }}
{% endcache %}
</div> </div>
<div class="show-more"> {{_("...More")}} </div> <h2 class="title">
</div> <a href="{{ url('blog_post', post.id, post.slug) }}">{{ post.title }}</a>
<div class="actionbar-box"> </h2>
{% set pagevote = post.pagevote %} <div class="blog-description">
{% set bookmark = post.bookmark %} <div class="summary content-description">
{% set hide_actionbar_comment = True %} {% cache 86400 'post_summary' post.id %}
{% set include_hr = False %} {{ post.summary|default(post.content, true)|markdown(lazy_load=True)|reference|str|safe }}
{% set share_url = request.build_absolute_uri(post.get_absolute_url()) %} {% endcache %}
{% include "actionbar/list.html" %} </div>
</div> <div class="show-more"> {{_("...More")}} </div>
</section> </div>
<div class="actionbar-box">
{% set pagevote = post.pagevote %}
{% set bookmark = post.bookmark %}
{% set hide_actionbar_comment = True %}
{% set include_hr = False %}
{% set share_url = request.build_absolute_uri(post.get_absolute_url()) %}
{% include "actionbar/list.html" %}
</div>
</section>
{% endfor %}
{% include "feed/has_next.html" %}

View file

@ -20,6 +20,7 @@
{% block three_col_js %} {% block three_col_js %}
{% include "actionbar/media-js.html" %} {% include "actionbar/media-js.html" %}
{% include "feed/feed_js.html" %}
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function () { $(document).ready(function () {
$('.time-remaining').each(function () { $('.time-remaining').each(function () {
@ -50,24 +51,15 @@
{% block middle_content %} {% block middle_content %}
{% set show_organization_private_icon=True %} {% set show_organization_private_icon=True %}
{% if page_type == 'blog' %} {% if page_type == 'blog' %}
{% for post in posts %} {% include "blog/content.html" %}
{% include "blog/content.html" %}
{% endfor %}
{% elif page_type == 'ticket' %} {% elif page_type == 'ticket' %}
{% if tickets %} {% if tickets %}
{% for ticket in tickets %} {% include "ticket/feed.html" %}
{% include "ticket/feed.html" %}
{% endfor %}
{% else %} {% else %}
<h3 style="text-align: center">{{_('You have no ticket')}}</h3> <h3 style="text-align: center">{{_('You have no ticket')}}</h3>
{% endif %} {% endif %}
{% elif page_type == 'comment' %} {% elif page_type == 'comment' %}
{% for comment in comments %} {% include "comments/feed.html" %}
{% include "comments/feed.html" %}
{% endfor %}
{% endif %}
{% if page_obj.num_pages > 1 %}
<div style="margin-bottom:10px;margin-top:10px">{% include "list-pages.html" %}</div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -1,19 +1,22 @@
<div class="blog-box"> {% for comment in comments %}
<h3 class="problem-feed-name"> <div class="blog-box">
<a href="{{ comment.link }}#comment-{{ comment.id }}"> <h3 class="problem-feed-name">
{{ page_titles[comment.page] }} <a href="{{ comment.get_absolute_url() }}">
</a> {{ comment.page_title }}
</h3> </a>
{% with author=comment.author %} </h3>
{% if author %} {% with author=comment.author %}
<div class="problem-feed-info-entry"> {% if author %}
<i class="fa fa-pencil-square-o fa-fw"></i> <div class="problem-feed-info-entry">
<span class="pi-value">{{ link_user(author) }}</span> <i class="fa fa-pencil-square-o fa-fw"></i>
<span class="pi-value">{{ link_user(author) }}</span>
</div>
{% endif %}
{% endwith %}
<div class='blog-description content-description'>
{{ comment.body|markdown(lazy_load=True)|reference|str|safe }}
<div class="show-more"> {{_("...More")}} </div>
</div>
</div> </div>
{% endif %} {% endfor %}
{% endwith %} {% include "feed/has_next.html" %}
<div class='blog-description content-description'>
{{ comment.body|markdown(lazy_load=True)|reference|str|safe }}
<div class="show-more"> {{_("...More")}} </div>
</div>
</div>

View file

@ -0,0 +1,30 @@
<script>
window.page = {{page_obj.number}};
window.has_next_page = {{1 if page_obj.has_next() else 0}};
window.loading_page = false;
$(function() {
$(window).on("scroll", function() {
if (window.loading_page || !window.has_next_page) return;
var distanceFromBottom = $(document).height() - ($(window).scrollTop() + $(window).height());
if (distanceFromBottom < 500) {
window.loading_page = true;
var params = {
"only_content": 1,
"page": window.page + 1,
};
$.get("{{feed_content_url}}", params)
.done(function(data) {
$(".has_next").remove();
$(".middle-content").append(data);
window.loading_page = false;
window.has_next_page = parseInt($(".has_next").attr("value"));
window.page++;
MathJax.typeset($('.middle-content')[0]);
onWindowReady();
activateBlogBoxOnClick();
})
}
});
});
</script>

View file

@ -0,0 +1 @@
<div class="has_next" style="display: none;" value="{{1 if has_next_page else 0}}"></div>

View file

@ -29,7 +29,7 @@
</td> </td>
<td> <td>
{% if notification.comment %} {% if notification.comment %}
<a href="{{ notification.comment.link }}#comment-{{ notification.comment.id }}">{{ page_titles[notification.comment.page] }}</a> <a href="{{ notification.comment.link }}#comment-{{ notification.comment.id }}">{{ notification.comment.page_title }}</a>
{% else %} {% else %}
{% autoescape off %} {% autoescape off %}
{{notification.html_link}} {{notification.html_link}}

View file

@ -4,6 +4,7 @@
{% block org_js %} {% block org_js %}
{% include "actionbar/media-js.html" %} {% include "actionbar/media-js.html" %}
{% include "feed/feed_js.html" %}
{% endblock %} {% endblock %}
{% block middle_title %} {% block middle_title %}
@ -40,12 +41,7 @@
{% block middle_content %} {% block middle_content %}
{% block before_posts %}{% endblock %} {% block before_posts %}{% endblock %}
{% if is_member or can_edit %} {% if is_member or can_edit %}
{% for post in posts %} {% include "blog/content.html" %}
{% include "blog/content.html" %}
{% endfor %}
{% if posts.paginator.num_pages > 1 %}
<div style="margin-bottom:10px;margin-top:10px">{% include "list-pages.html" %}</div>
{% endif %}
{% else %} {% else %}
<div class="blog-sidebox sidebox"> <div class="blog-sidebox sidebox">
<h3>{{ _('About') }}<i class="fa fa-info-circle"></i></h3> <h3>{{ _('About') }}<i class="fa fa-info-circle"></i></h3>

View file

@ -2,6 +2,9 @@
{% block left_sidebar %} {% block left_sidebar %}
{% include "problem/left-sidebar.html" %} {% include "problem/left-sidebar.html" %}
{% endblock %} {% endblock %}
{% block problem_list_js %}
{% include "feed/feed_js.html" %}
{% endblock %}
{% block middle_content %} {% block middle_content %}
<div class="problem-feed-option"> <div class="problem-feed-option">
@ -24,127 +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 %}
{% for problem in problems %} {% include "problem/feed/problems.html" %}
<div class="blog-box">
<h3 class="problem-feed-name">
<a href="{{ url('problem_detail', problem.code) }}">
{{ problem.i18n_name }}
</a>
{% if problem.id in completed_problem_ids %}
<i class="solved-problem-color fa fa-check-circle"></i>
{% elif problem.id in attempted_problems %}
<i class="attempted-problem-color fa fa-minus-circle"></i>
{% else %}
<i class="unsolved-problem-color fa fa-minus-circle"></i>
{% endif %}
</h3>
{% with authors=problem.authors.all() %}
{% if authors %}
<div class="problem-feed-info-entry">
<i class="fa fa-pencil-square-o fa-fw"></i>
<span class="pi-value">{{ link_users(authors) }}</span>
</div>
{% endif %}
{% endwith %}
{% if show_types %}
<div class="problem-feed-types">
<i class="fa fa-tag"></i>
{% for type in problem.types_list %}
<span class="type-tag">{{ type }}</span>{% if not loop.last %}, {% endif %}
{% endfor %}, *{{problem.points | int}}
</div>
{% endif %}
<div class="blog-description">
<div class='content-description'>
{% cache 86400 'problem_html' problem.id MATH_ENGINE LANGUAGE_CODE %}
{{ problem.description|markdown(lazy_load=True)|reference|str|safe }}
{% endcache %}
{% if problem.pdf_description %}
<embed src="{{url('problem_pdf_description', problem.code)}}" width="100%" height="500" type="application/pdf"
style="margin-top: 0.5em">
{% endif %}
</div>
{% if feed_type=='volunteer' and request.user.has_perm('judge.suggest_problem_changes') %}
<br>
<a href="#" class="view-statement-src">{{ _('View source') }}</a>
<pre class="statement-src" style="display: none">{{ problem.description|str }}</pre>
<hr>
<center>
<h3>{{_('Volunteer form')}}</h3>
</center>
<br>
<button class="edit-btn" id="edit-{{problem.id}}" pid="{{problem.id}}" style="float: right">{{_('Edit')}}</button>
<form class="volunteer-form" id="form-{{problem.id}}" pid="{{problem.id}}" style="display: none;" method="POST">
<input type="submit" class="volunteer-submit-btn" id="submit-{{problem.id}}" pid="{{problem.id}}"
pcode="{{problem.code}}" style="float: right" value="{{_('Submit')}}">
<table class="table">
<thead>
<tr>
<th>
</th>
<th>
{{_('Value')}}
</th>
</tr>
</thead>
<tbody>
<tr>
<td width="30%">
<label for="knowledge_point-{{problem.id}}"><i>{{ _('Knowledge point') }}</i></label>
</td>
<td>
<input id="knowledge_point-{{problem.id}}" type="number" class="point-input" required>
</td>
</tr>
<tr>
<td width="30%">
<label for="thinking_point-{{problem.id}}"><i>{{ _('Thinking point') }}</i></label>
</td>
<td>
<input id="thinking_point-{{problem.id}}" type="number" class="point-input" required>
</td>
</tr>
<tr>
<td width="30%">
<label for="types"><i>{{ _('Problem types') }}</i></label>
</td>
<td>
<select id="volunteer-types-{{problem.id}}" name="types" multiple>
{% for type in problem_types %}
<option value="{{ type.id }}" {% if type in problem.types.all() %} selected{% endif %}>
{{ type.full_name }}
</option>
{% endfor %}
</select>
</td>
</tr>
<tr>
<td width="30%">
<label for="feedback"><i>{{ _('Feedback') }}</i></label>
</td>
<td>
<textarea id="feedback-{{problem.id}}" rows="2" style="width: 100%"
placeholder="{{_('Any additional note here')}}"></textarea>
</td>
</tr>
</tbody>
</table>
</form>
<center id="thank-{{problem.id}}" style="display: none; margin-top: 3em"></center>
{% endif %}
<div class="show-more"> {{_("...More")}} </div>
</div>
<div class="actionbar-box">
{% set pagevote = problem.pagevote %}
{% set bookmark = problem.bookmark %}
{% set hide_actionbar_comment = True %}
{% set include_hr = False %}
{% set share_url = request.build_absolute_uri(problem.get_absolute_url()) %}
{% include "actionbar/list.html" %}
</div>
</div>
{% endfor %}
{% if page_obj.num_pages > 1 %}
<div style="margin-top:10px;">{% include "list-pages.html" %}</div>
{% endif %}
{% endblock %} {% endblock %}

View file

@ -0,0 +1,121 @@
{% for problem in problems %}
<div class="blog-box">
<h3 class="problem-feed-name">
<a href="{{ url('problem_detail', problem.code) }}">
{{ problem.i18n_name }}
</a>
{% if problem.id in completed_problem_ids %}
<i class="solved-problem-color fa fa-check-circle"></i>
{% elif problem.id in attempted_problems %}
<i class="attempted-problem-color fa fa-minus-circle"></i>
{% else %}
<i class="unsolved-problem-color fa fa-minus-circle"></i>
{% endif %}
</h3>
{% with authors=problem.authors.all() %}
{% if authors %}
<div class="problem-feed-info-entry">
<i class="fa fa-pencil-square-o fa-fw"></i>
<span class="pi-value">{{ link_users(authors) }}</span>
</div>
{% endif %}
{% endwith %}
{% if show_types %}
<div class="problem-feed-types">
<i class="fa fa-tag"></i>
{% for type in problem.types_list %}
<span class="type-tag">{{ type }}</span>{% if not loop.last %}, {% endif %}
{% endfor %}, *{{problem.points | int}}
</div>
{% endif %}
<div class="blog-description">
<div class='content-description'>
{% cache 86400 'problem_html' problem.id MATH_ENGINE LANGUAGE_CODE %}
{{ problem.description|markdown(lazy_load=True)|reference|str|safe }}
{% endcache %}
{% if problem.pdf_description %}
<embed src="{{url('problem_pdf_description', problem.code)}}" width="100%" height="500" type="application/pdf"
style="margin-top: 0.5em">
{% endif %}
</div>
{% if feed_type=='volunteer' and request.user.has_perm('judge.suggest_problem_changes') %}
<br>
<a href="#" class="view-statement-src">{{ _('View source') }}</a>
<pre class="statement-src" style="display: none">{{ problem.description|str }}</pre>
<hr>
<center>
<h3>{{_('Volunteer form')}}</h3>
</center>
<br>
<button class="edit-btn" id="edit-{{problem.id}}" pid="{{problem.id}}" style="float: right">{{_('Edit')}}</button>
<form class="volunteer-form" id="form-{{problem.id}}" pid="{{problem.id}}" style="display: none;" method="POST">
<input type="submit" class="volunteer-submit-btn" id="submit-{{problem.id}}" pid="{{problem.id}}"
pcode="{{problem.code}}" style="float: right" value="{{_('Submit')}}">
<table class="table">
<thead>
<tr>
<th>
</th>
<th>
{{_('Value')}}
</th>
</tr>
</thead>
<tbody>
<tr>
<td width="30%">
<label for="knowledge_point-{{problem.id}}"><i>{{ _('Knowledge point') }}</i></label>
</td>
<td>
<input id="knowledge_point-{{problem.id}}" type="number" class="point-input" required>
</td>
</tr>
<tr>
<td width="30%">
<label for="thinking_point-{{problem.id}}"><i>{{ _('Thinking point') }}</i></label>
</td>
<td>
<input id="thinking_point-{{problem.id}}" type="number" class="point-input" required>
</td>
</tr>
<tr>
<td width="30%">
<label for="types"><i>{{ _('Problem types') }}</i></label>
</td>
<td>
<select id="volunteer-types-{{problem.id}}" name="types" multiple>
{% for type in problem_types %}
<option value="{{ type.id }}" {% if type in problem.types.all() %} selected{% endif %}>
{{ type.full_name }}
</option>
{% endfor %}
</select>
</td>
</tr>
<tr>
<td width="30%">
<label for="feedback"><i>{{ _('Feedback') }}</i></label>
</td>
<td>
<textarea id="feedback-{{problem.id}}" rows="2" style="width: 100%"
placeholder="{{_('Any additional note here')}}"></textarea>
</td>
</tr>
</tbody>
</table>
</form>
<center id="thank-{{problem.id}}" style="display: none; margin-top: 3em"></center>
{% endif %}
<div class="show-more"> {{_("...More")}} </div>
</div>
<div class="actionbar-box">
{% set pagevote = problem.pagevote %}
{% set bookmark = problem.bookmark %}
{% set hide_actionbar_comment = True %}
{% set include_hr = False %}
{% set share_url = request.build_absolute_uri(problem.get_absolute_url()) %}
{% include "actionbar/list.html" %}
</div>
</div>
{% endfor %}
{% include "feed/has_next.html" %}

View file

@ -55,6 +55,7 @@
{% block three_col_js %} {% block three_col_js %}
{% include "actionbar/media-js.html" %} {% include "actionbar/media-js.html" %}
{% block problem_list_js %}{% endblock %}
<script> <script>
window.point_start = {{point_start}}; window.point_start = {{point_start}};
window.point_end = {{point_end}}; window.point_end = {{point_end}};

View file

@ -67,12 +67,13 @@
$('.left-sidebar-item').removeClass('active'); $('.left-sidebar-item').removeClass('active');
$elem.addClass('active'); $elem.addClass('active');
} }
$(window).off("scroll");
$('.middle-right-content').html(loading_page); $('.middle-right-content').html(loading_page);
$.get(url, function (data) { $.get(url, function (data) {
var reload_content = $(data).find('.middle-right-content'); var reload_content = $(data).find('.middle-right-content');
if (reload_content.length) { if (reload_content.length) {
window.history.pushState("", "", url); window.history.pushState("", "", url);
$('html, body').animate({scrollTop: 0}, 'fast');
$('.middle-right-content').html(reload_content.first().html()); $('.middle-right-content').html(reload_content.first().html());
if (reload_content.hasClass("wrapper")) { if (reload_content.hasClass("wrapper")) {
$('.middle-right-content').addClass("wrapper"); $('.middle-right-content').addClass("wrapper");
@ -86,6 +87,7 @@
activateBlogBoxOnClick(); activateBlogBoxOnClick();
$('.xdsoft_datetimepicker').hide(); $('.xdsoft_datetimepicker').hide();
registerNavigation(); registerNavigation();
} }
else { else {
window.location.href = url; window.location.href = url;

View file

@ -1,26 +1,29 @@
<div class="blog-box"> {% for ticket in tickets %}
<h3 class="problem-feed-name"> <div class="blog-box">
<a href="{{ ticket.linked_item.get_absolute_url() }}"> <h3 class="problem-feed-name">
{{ ticket.linked_item|item_title }}</a> <a href="{{ ticket.linked_item.get_absolute_url() }}">
&#183 {{ ticket.linked_item|item_title }}</a>
<a href="{{ url('ticket', ticket.id) }}"> &#183
{{ ticket.title }} <a href="{{ url('ticket', ticket.id) }}">
</a> {{ ticket.title }}
</h3> </a>
{% with author=ticket.user %} </h3>
{% if author %} {% with author=ticket.user %}
<div class="problem-feed-info-entry"> {% if author %}
<i class="fa fa-pencil-square-o fa-fw"></i> <div class="problem-feed-info-entry">
<span class="pi-value">{{ link_user(author) }}</span> <i class="fa fa-pencil-square-o fa-fw"></i>
<span class="pi-value">{{ link_user(author) }}</span>
</div>
{% endif %}
{% endwith %}
<div class="problem-feed-types">
<i class="fa fa-tag"></i>
{{link_user(ticket.messages.last().user)}} {{_(' replied')}}
</div>
<div class='blog-description content-description'>
{{ ticket.messages.last().body|markdown(lazy_load=True)|reference|str|safe }}
<div class="show-more"> {{_("...More")}} </div>
</div>
</div> </div>
{% endif %} {% endfor %}
{% endwith %} {% include "feed/has_next.html" %}
<div class="problem-feed-types">
<i class="fa fa-tag"></i>
{{link_user(ticket.messages.last().user)}} {{_(' replied')}}
</div>
<div class='blog-description content-description'>
{{ ticket.messages.last().body|markdown(lazy_load=True)|reference|str|safe }}
<div class="show-more"> {{_("...More")}} </div>
</div>
</div>