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/random/$", problem.RandomProblem.as_view(), name="problem_random"),
url(
r"^problems/feed/",
paged_list_view(problem.ProblemFeed, "problem_feed", feed_type="for_you"),
r"^problems/feed/$",
problem.ProblemFeed.as_view(feed_type="for_you"),
name="problem_feed",
),
url(
r"^problems/feed/new/",
paged_list_view(problem.ProblemFeed, "problem_feed_new", feed_type="new"),
r"^problems/feed/new/$",
problem.ProblemFeed.as_view(feed_type="new"),
name="problem_feed_new",
),
url(
r"^problems/feed/volunteer/",
paged_list_view(
problem.ProblemFeed, "problem_feed_volunteer", feed_type="volunteer"
),
r"^problems/feed/volunteer/$",
problem.ProblemFeed.as_view(feed_type="volunteer"),
name="problem_feed_volunteer",
),
url(
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"^license/(?P<key>[-\w.]+)$", license.LicenseDetail.as_view(), name="license"),
url(

View file

@ -22,11 +22,23 @@ class CommentForm(ModelForm):
class CommentAdmin(VersionAdmin):
fieldsets = (
(None, {"fields": ("author", "page", "parent", "score", "hidden")}),
(
None,
{
"fields": (
"author",
"parent",
"score",
"hidden",
"content_type",
"object_id",
)
},
),
("Content", {"fields": ("body",)}),
)
list_display = ["author", "linked_page", "time"]
search_fields = ["author__user__username", "page", "body"]
list_display = ["author", "linked_object", "time"]
search_fields = ["author__user__username", "body"]
readonly_fields = ["score"]
actions = ["hide_comment", "unhide_comment"]
list_filter = ["hidden"]
@ -66,16 +78,6 @@ class CommentAdmin(VersionAdmin):
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):
super(CommentAdmin, self).save_model(request, obj, form, change)
if obj.hidden:

View file

@ -22,7 +22,7 @@ from reversion import revisions
from reversion.models import Revision, Version
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.jinja2.reference import get_user_from_text
@ -90,7 +90,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
def is_comment_locked(self):
if self.request.user.has_perm("judge.override_comment_lock"):
return False
return CommentLock.objects.filter(page=self.get_comment_page()).exists() or (
return (
self.request.in_contest
and self.request.participation.contest.use_clarifications
)
@ -99,7 +99,6 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
def post(self, request, *args, **kwargs):
self.object = self.get_object()
page = self.get_comment_page()
if self.is_comment_locked():
return HttpResponseForbidden()
@ -110,9 +109,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
except ValueError:
return HttpResponseNotFound()
else:
if not Comment.objects.filter(
hidden=False, id=parent, page=page
).exists():
if not self.object.comments.filter(hidden=False, id=parent).exists():
return HttpResponseNotFound()
form = CommentForm(request, request.POST)
@ -120,6 +117,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
comment = form.save(commit=False)
comment.author = request.profile
comment.page = page
comment.linked_object = self.object
with LockModel(
write=(Comment, Revision, Version), read=(ContentType,)
@ -136,7 +134,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
notification_reply.save()
# add notification for page authors
page_authors = comment.page_object.authors.all()
page_authors = comment.linked_object.authors.all()
for user in page_authors:
if user == comment.author:
continue
@ -149,7 +147,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
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)
return self.render_to_response(context)
@ -159,15 +157,13 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
return self.render_to_response(
self.get_context_data(
object=self.object,
comment_form=CommentForm(
request, initial={"page": self.get_comment_page(), "parent": None}
),
comment_form=CommentForm(request, initial={"parent": None}),
)
)
def get_context_data(self, **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["comment_lock"] = self.is_comment_locked()
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.core.exceptions import ObjectDoesNotExist
from judge.models import Profile
from judge.models.contest import Contest
from judge.models.interface import BlogPost
from judge.models.problem import Problem, Solution
from judge.models.profile import Profile
__all__ = ["BookMark"]
@ -26,6 +23,10 @@ class BookMark(models.Model):
return False
def page_object(self):
from judge.models.contest import Contest
from judge.models.interface import BlogPost
from judge.models.problem import Problem, Solution
try:
page = self.page
if page.startswith("p:"):

View file

@ -9,6 +9,8 @@ from django.db.models import CASCADE
from django.urls import reverse
from django.utils.functional import cached_property
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.models import MPTTModel
from reversion.models import Version
@ -44,6 +46,9 @@ class VersionRelation(GenericRelation):
class Comment(MPTTModel):
author = models.ForeignKey(Profile, verbose_name=_("commenter"), on_delete=CASCADE)
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(
max_length=30,
verbose_name=_("associated page"),
@ -66,6 +71,9 @@ class Comment(MPTTModel):
class Meta:
verbose_name = _("comment")
verbose_name_plural = _("comments")
indexes = [
models.Index(fields=["content_type", "object_id"]),
]
class MPTTMeta:
order_insertion_by = ["-time"]
@ -82,13 +90,9 @@ class Comment(MPTTModel):
if organization:
queryset = queryset.filter(author__in=organization.members.all())
problem_access = CacheDict(
lambda code: Problem.objects.get(code=code).is_accessible_by(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))
problem_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))
if n == -1:
n = len(queryset)
@ -102,112 +106,53 @@ class Comment(MPTTModel):
if not slice:
break
for comment in slice:
if comment.page.startswith("p:") or comment.page.startswith("s:"):
try:
if problem_access[comment.page[2:]]:
if isinstance(comment.linked_object, Problem):
if problem_access[comment.linked_object]:
output.append(comment)
except Problem.DoesNotExist:
pass
elif comment.page.startswith("c:"):
try:
if contest_access[comment.page[2:]]:
elif isinstance(comment.linked_object, Contest):
if contest_access[comment.linked_object]:
output.append(comment)
except Contest.DoesNotExist:
pass
elif comment.page.startswith("b:"):
try:
if blog_access[comment.page[2:]]:
elif isinstance(comment.linked_object, BlogPost):
if blog_access[comment.linked_object]:
output.append(comment)
except BlogPost.DoesNotExist:
pass
else:
elif isinstance(comment.linked_object, Solution):
if problem_access[comment.linked_object.problem]:
output.append(comment)
if len(output) >= n:
return output
return output
@cached_property
def link(self):
try:
link = None
if self.page.startswith("p:"):
link = reverse("problem_detail", args=(self.page[2:],))
elif self.page.startswith("c:"):
link = reverse("contest_view", args=(self.page[2:],))
elif self.page.startswith("b:"):
key = "blog_slug:%s" % self.page[2:]
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>"
def page_title(self):
if isinstance(self.linked_object, Problem):
return self.linked_object.name
elif isinstance(self.linked_object, Contest):
return self.linked_object.name
elif isinstance(self.linked_object, Solution):
return _("Editorial for ") + self.linked_object.problem.name
elif isinstance(self.linked_object, BlogPost):
return self.linked_object.title
@cached_property
def page_title(self):
return self.get_page_title(self.page)
def link(self):
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):
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):
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.functional import cached_property
from django.utils.translation import gettext, gettext_lazy as _
from django.contrib.contenttypes.fields import GenericRelation
from jsonfield import JSONField
from lupa import LuaRuntime
from moss import (
@ -297,6 +298,7 @@ class Contest(models.Model):
validators=[MinValueValidator(0), MaxValueValidator(10)],
help_text=_("Number of digits to round points to."),
)
comments = GenericRelation("Comment")
@cached_property
def format_class(self):

View file

@ -5,10 +5,14 @@ from django.db import models
from django.urls import reverse
from django.utils import timezone
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.models import MPTTModel
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"]
@ -91,6 +95,7 @@ class BlogPost(models.Model):
is_organization_private = models.BooleanField(
verbose_name=_("private to organizations"), default=False
)
comments = GenericRelation("Comment")
def __str__(self):
return self.title
@ -125,6 +130,18 @@ class BlogPost(models.Model):
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:
permissions = (("edit_all_post", _("Edit all posts")),)
verbose_name = _("blog post")

View file

@ -2,7 +2,7 @@ from django.db import models
from django.db.models import CASCADE
from django.utils.translation import gettext_lazy as _
from judge.models import Profile
from judge.models.profile import Profile
__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 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.runtime import Language
from judge.user_translations import gettext as user_gettext
@ -268,6 +270,7 @@ class Problem(models.Model):
objects = TranslatedProblemQuerySet.as_manager()
tickets = GenericRelation("Ticket")
comments = GenericRelation("Comment")
organizations = models.ManyToManyField(
Organization,
@ -444,6 +447,18 @@ class Problem(models.Model):
def usable_common_names(self):
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
def usable_languages(self):
return self.allowed_languages.filter(
@ -644,7 +659,7 @@ class LanguageTemplate(models.Model):
class Solution(models.Model):
problem = models.OneToOneField(
Problem,
on_delete=SET_NULL,
on_delete=CASCADE,
verbose_name=_("associated problem"),
null=True,
blank=True,
@ -654,6 +669,7 @@ class Solution(models.Model):
publish_on = models.DateTimeField(verbose_name=_("publish date"))
authors = models.ManyToManyField(Profile, verbose_name=_("authors"), blank=True)
content = models.TextField(verbose_name=_("editorial content"))
comments = GenericRelation("Comment")
def get_absolute_url(self):
problem = self.problem

View file

@ -7,8 +7,8 @@ from django.utils.translation import ugettext as _
from django.views.generic import ListView
from judge.comments import CommentedDetailView
from judge.views.pagevote import PageVoteDetailView, PageVoteListView
from judge.views.bookmark import BookMarkDetailView, BookMarkListView
from judge.views.pagevote import PageVoteDetailView
from judge.views.bookmark import BookMarkDetailView
from judge.models import (
BlogPost,
Comment,
@ -26,28 +26,16 @@ from judge.utils.diggpaginator import DiggPaginator
from judge.utils.problems import user_completed_ids
from judge.utils.tickets import filter_visible_tickets
from judge.utils.views import TitleMixin
from judge.views.feed import FeedView
# General view for all content list on home feed
class FeedView(ListView):
class HomeFeedView(FeedView):
template_name = "blog/list.html"
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):
context = super(FeedView, self).get_context_data(**kwargs)
context = super(HomeFeedView, self).get_context_data(**kwargs)
context["has_clarifications"] = False
if self.request.user.is_authenticated:
participation = self.request.profile.current_contest
@ -60,17 +48,7 @@ class FeedView(ListView):
if participation.contest.is_editable_by(self.request.user):
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()
visible_contests = (
Contest.get_visible_contests(self.request.user, show_own_contests_only=True)
.filter(is_visible=True)
@ -102,10 +80,12 @@ class FeedView(ListView):
return context
class PostList(FeedView, PageVoteListView, BookMarkListView):
class PostList(HomeFeedView):
model = BlogPost
paginate_by = 10
paginate_by = 4
context_object_name = "posts"
feed_content_template_name = "blog/content.html"
url_name = "blog_post_list"
def get_queryset(self):
queryset = (
@ -121,13 +101,23 @@ class PostList(FeedView, PageVoteListView, BookMarkListView):
queryset = queryset.filter(filter)
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):
context = super(PostList, self).get_context_data(**kwargs)
context["title"] = (
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["post_comment_counts"] = {
int(page[2:]): count
@ -138,18 +128,17 @@ class PostList(FeedView, PageVoteListView, BookMarkListView):
.annotate(count=Count("page"))
.order_by()
}
context = self.add_pagevote_context_data(context)
context = self.add_bookmark_context_data(context)
return context
def get_comment_page(self, post):
return "b:%s" % post.id
class TicketFeed(FeedView):
class TicketFeed(HomeFeedView):
model = Ticket
context_object_name = "tickets"
paginate_by = 30
paginate_by = 8
feed_content_template_name = "ticket/feed.html"
def get_queryset(self, is_own=True):
profile = self.request.profile
@ -181,30 +170,25 @@ class TicketFeed(FeedView):
def get_context_data(self, **kwargs):
context = super(TicketFeed, self).get_context_data(**kwargs)
context["page_type"] = "ticket"
context["first_page_href"] = self.request.path
context["page_prefix"] = "?page="
context["title"] = _("Ticket feed")
return context
class CommentFeed(FeedView):
class CommentFeed(HomeFeedView):
model = Comment
context_object_name = "comments"
paginate_by = 50
paginate_by = 8
feed_content_template_name = "comments/feed.html"
def get_queryset(self):
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):
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["page_type"] = "comment"
return context

View file

@ -74,13 +74,3 @@ class BookMarkDetailView(TemplateResponseMixin, SingleObjectMixin, View):
queryset = BookMark.objects.get_or_create(page=self.get_comment_page())
context["bookmark"] = queryset[0]
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["title"] = _("Notifications (%d unseen)" % context["unseen_count"])
context["has_notifications"] = self.queryset.exists()
context["page_titles"] = CacheDict(lambda page: Comment.get_page_title(page))
return context
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.contests import ContestList
from judge.views.submission import AllSubmissions, SubmissionsListBase
from judge.views.pagevote import PageVoteListView
from judge.views.bookmark import BookMarkListView
from judge.views.feed import FeedView
__all__ = [
"OrganizationList",
@ -194,7 +192,7 @@ class MemberOrganizationMixin(OrganizationMixin):
)
class OrganizationHomeViewContext:
class OrganizationHomeView(OrganizationMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if not hasattr(self, "organization"):
@ -221,28 +219,6 @@ class OrganizationHomeViewContext:
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):
model = Organization
context_object_name = "organizations"
@ -272,51 +248,50 @@ class OrganizationList(TitleMixin, ListView, OrganizationBase):
return context
class OrganizationHome(OrganizationDetailView, PageVoteListView, BookMarkListView):
class OrganizationHome(OrganizationHomeView, FeedView):
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):
posts = (
def get_queryset(self):
return (
BlogPost.objects.filter(
visible=True,
publish_on__lte=timezone.now(),
is_organization_private=True,
organizations=self.object,
organizations=self.organization,
)
.order_by("-sticky", "-publish_on")
.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):
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):
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"
context["organization_subdomain"] = (
http
+ "://"
+ self.object.slug
+ self.organization.slug
+ "."
+ 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"] = {
int(page[2:]): count
for page, count in Comment.objects.filter(
@ -331,7 +306,9 @@ class OrganizationHome(OrganizationDetailView, PageVoteListView, BookMarkListVie
visible_contests = (
Contest.get_visible_contests(self.request.user)
.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")
)
@ -344,11 +321,27 @@ class OrganizationHome(OrganizationDetailView, PageVoteListView, BookMarkListVie
return context
class OrganizationUsers(QueryStringSortMixin, OrganizationDetailView):
class OrganizationUsers(QueryStringSortMixin, OrganizationMixin, FeedView):
template_name = "organization/users.html"
all_sorts = frozenset(("points", "problem_count", "rating", "performance_points"))
default_desc = all_sorts
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):
res = super(OrganizationUsers, self).dispatch(request, *args, **kwargs)
@ -363,26 +356,13 @@ class OrganizationUsers(QueryStringSortMixin, OrganizationDetailView):
def get_context_data(self, **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["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["page_type"] = "users"
context.update(self.get_sort_context())
@ -421,8 +401,7 @@ class OrganizationProblems(LoginRequiredMixin, MemberOrganizationMixin, ProblemL
class OrganizationContestMixin(
LoginRequiredMixin,
TitleMixin,
OrganizationMixin,
OrganizationHomeViewContext,
OrganizationHomeView,
):
model = Contest
@ -613,8 +592,7 @@ class RequestJoinOrganization(LoginRequiredMixin, SingleObjectMixin, FormView):
class OrganizationRequestDetail(
LoginRequiredMixin,
TitleMixin,
OrganizationMixin,
OrganizationHomeViewContext,
OrganizationHomeView,
DetailView,
):
model = OrganizationRequest
@ -639,7 +617,8 @@ OrganizationRequestFormSet = modelformset_factory(
class OrganizationRequestBaseView(
OrganizationDetailView,
DetailView,
OrganizationHomeView,
TitleMixin,
LoginRequiredMixin,
SingleObjectTemplateResponseMixin,
@ -760,7 +739,7 @@ class AddOrganizationMember(
LoginRequiredMixin,
TitleMixin,
AdminOrganizationMixin,
OrganizationDetailView,
OrganizationHomeView,
UpdateView,
):
template_name = "organization/add-member.html"
@ -822,7 +801,7 @@ class EditOrganization(
LoginRequiredMixin,
TitleMixin,
AdminOrganizationMixin,
OrganizationDetailView,
OrganizationHomeView,
UpdateView,
):
template_name = "organization/edit.html"
@ -1023,7 +1002,7 @@ class EditOrganizationContest(
class AddOrganizationBlog(
LoginRequiredMixin,
TitleMixin,
OrganizationHomeViewContext,
OrganizationHomeView,
MemberOrganizationMixin,
CreateView,
):
@ -1074,7 +1053,7 @@ class AddOrganizationBlog(
class EditOrganizationBlog(
LoginRequiredMixin,
TitleMixin,
OrganizationHomeViewContext,
OrganizationHomeView,
MemberOrganizationMixin,
UpdateView,
):
@ -1168,7 +1147,7 @@ class PendingBlogs(
LoginRequiredMixin,
TitleMixin,
MemberOrganizationMixin,
OrganizationHomeViewContext,
OrganizationHomeView,
ListView,
):
model = BlogPost

View file

@ -104,13 +104,3 @@ class PageVoteDetailView(TemplateResponseMixin, SingleObjectMixin, View):
queryset = PageVote.objects.get_or_create(page=self.get_comment_page())
context["pagevote"] = queryset[0]
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,
)
from judge.ml.collab_filter import CollabFilter
from judge.views.pagevote import PageVoteDetailView, PageVoteListView
from judge.views.bookmark import BookMarkDetailView, BookMarkListView
from judge.views.pagevote import PageVoteDetailView
from judge.views.bookmark import BookMarkDetailView
from judge.views.feed import FeedView
def get_contest_problem(problem, profile):
@ -197,31 +198,34 @@ class ProblemSolution(
template_name = "problem/editorial.html"
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):
return format_html(
_('Editorial for <a href="{1}">{0}</a>'),
self.object.name,
reverse("problem_detail", args=[self.object.code]),
self.problem.name,
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):
context = super(ProblemSolution, self).get_context_data(**kwargs)
solution = get_object_or_404(Solution, problem=self.object)
solution = self.get_object()
if (
not solution.is_public or solution.publish_on > timezone.now()
) and not self.request.user.has_perm("judge.see_private_solution"):
raise Http404()
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
def get_comment_page(self):
return "s:" + self.object.code
return "s:" + self.problem.code
class ProblemRaw(
@ -830,11 +834,12 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
return HttpResponseRedirect(request.get_full_path())
class ProblemFeed(ProblemList, PageVoteListView, BookMarkListView):
class ProblemFeed(ProblemList, FeedView):
model = Problem
context_object_name = "problems"
template_name = "problem/feed.html"
paginate_by = 20
feed_content_template_name = "problem/feed/problems.html"
paginate_by = 4
title = _("Problem feed")
feed_type = None
@ -843,19 +848,6 @@ class ProblemFeed(ProblemList, PageVoteListView, BookMarkListView):
return request.session.get(key, key == "hide_solved")
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):
return "p:%s" % problem.code
@ -962,8 +954,6 @@ class ProblemFeed(ProblemList, PageVoteListView, BookMarkListView):
context["feed_type"] = self.feed_type
context["has_show_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

View file

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

View file

@ -45,7 +45,7 @@
</span>
<span class="actionbar-block" style="justify-content: flex-end;">
<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>
<span class="actionbar-text">{{_("Share")}}</span>
</span>

View file

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

View file

@ -1,4 +1,5 @@
<section class="{% if post.sticky %}sticky {% endif %}blog-box">
{% for post in posts%}
<section class="{% if post.sticky %}sticky {% endif %}blog-box">
<div style="margin-bottom: 0.5em">
<span class="time">
{% with authors=post.authors.all() %}
@ -52,4 +53,6 @@
{% set share_url = request.build_absolute_uri(post.get_absolute_url()) %}
{% include "actionbar/list.html" %}
</div>
</section>
</section>
{% endfor %}
{% include "feed/has_next.html" %}

View file

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

View file

@ -1,7 +1,8 @@
<div class="blog-box">
{% for comment in comments %}
<div class="blog-box">
<h3 class="problem-feed-name">
<a href="{{ comment.link }}#comment-{{ comment.id }}">
{{ page_titles[comment.page] }}
<a href="{{ comment.get_absolute_url() }}">
{{ comment.page_title }}
</a>
</h3>
{% with author=comment.author %}
@ -16,4 +17,6 @@
{{ comment.body|markdown(lazy_load=True)|reference|str|safe }}
<div class="show-more"> {{_("...More")}} </div>
</div>
</div>
</div>
{% endfor %}
{% include "feed/has_next.html" %}

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>
{% 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 %}
{% autoescape off %}
{{notification.html_link}}

View file

@ -4,6 +4,7 @@
{% block org_js %}
{% include "actionbar/media-js.html" %}
{% include "feed/feed_js.html" %}
{% endblock %}
{% block middle_title %}
@ -40,12 +41,7 @@
{% block middle_content %}
{% block before_posts %}{% endblock %}
{% if is_member or can_edit %}
{% for post in posts %}
{% 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 %}
<div class="blog-sidebox sidebox">
<h3>{{ _('About') }}<i class="fa fa-info-circle"></i></h3>

View file

@ -2,6 +2,9 @@
{% block left_sidebar %}
{% include "problem/left-sidebar.html" %}
{% endblock %}
{% block problem_list_js %}
{% include "feed/feed_js.html" %}
{% endblock %}
{% block middle_content %}
<div class="problem-feed-option">
@ -24,127 +27,5 @@
<li><a href="{{url('admin:judge_volunteerproblemvote_changelist')}}">{{_('View your votes')}}</a></li>
</ul>
{% endif %}
{% 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 %}
{% if page_obj.num_pages > 1 %}
<div style="margin-top:10px;">{% include "list-pages.html" %}</div>
{% endif %}
{% include "problem/feed/problems.html" %}
{% 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 %}
{% include "actionbar/media-js.html" %}
{% block problem_list_js %}{% endblock %}
<script>
window.point_start = {{point_start}};
window.point_end = {{point_end}};

View file

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

View file

@ -1,4 +1,5 @@
<div class="blog-box">
{% for ticket in tickets %}
<div class="blog-box">
<h3 class="problem-feed-name">
<a href="{{ ticket.linked_item.get_absolute_url() }}">
{{ ticket.linked_item|item_title }}</a>
@ -23,4 +24,6 @@
{{ ticket.messages.last().body|markdown(lazy_load=True)|reference|str|safe }}
<div class="show-more"> {{_("...More")}} </div>
</div>
</div>
</div>
{% endfor %}
{% include "feed/has_next.html" %}