Infinite scrolling and comment migration
This commit is contained in:
parent
4b558bd656
commit
799ff5f8f8
33 changed files with 639 additions and 556 deletions
19
dmoj/urls.py
19
dmoj/urls.py
|
@ -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(
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
50
judge/migrations/0151_comment_content_type.py
Normal file
50
judge/migrations/0151_comment_content_type.py
Normal 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
54
judge/migrations/0152_migrate_comments.py
Normal file
54
judge/migrations/0152_migrate_comments.py
Normal 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(),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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:"):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
34
judge/views/feed.py
Normal 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
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 -%}
|
||||||
•
|
{% endwith %}
|
||||||
{{ relative_time(post.publish_on, abs=_('on {time}'), rel=_('{time}')) -}}
|
•
|
||||||
{%- if post.sticky %} •
|
{{ relative_time(post.publish_on, abs=_('on {time}'), rel=_('{time}')) -}}
|
||||||
<i title="Sticky" class="fa fa-star fa-fw"></i>{% endif -%}
|
{%- if post.sticky %} •
|
||||||
{% if post.is_organization_private and show_organization_private_icon %}
|
<i title="Sticky" class="fa fa-star fa-fw"></i>{% endif -%}
|
||||||
•
|
{% if post.is_organization_private and show_organization_private_icon %}
|
||||||
<span>
|
•
|
||||||
{% 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" %}
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
30
templates/feed/feed_js.html
Normal file
30
templates/feed/feed_js.html
Normal 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>
|
1
templates/feed/has_next.html
Normal file
1
templates/feed/has_next.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<div class="has_next" style="display: none;" value="{{1 if has_next_page else 0}}"></div>
|
|
@ -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}}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
121
templates/problem/feed/problems.html
Normal file
121
templates/problem/feed/problems.html
Normal 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" %}
|
|
@ -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}};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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() }}">
|
||||||
·
|
{{ ticket.linked_item|item_title }}</a>
|
||||||
<a href="{{ url('ticket', ticket.id) }}">
|
·
|
||||||
{{ 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>
|
|
Loading…
Reference in a new issue