From d10173df5daae18fbd48c44b9b2f1195db21f4d7 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Mon, 21 Mar 2022 16:09:16 -0500 Subject: [PATCH] New home UI --- dmoj/urls.py | 6 +- judge/models/comment.py | 6 +- judge/views/blog.py | 149 +++++++++++++++--------- judge/views/problem.py | 67 ++++++++--- resources/base.scss | 4 +- resources/blog.scss | 100 +++++++++++++++- templates/blog/content.html | 22 ++-- templates/blog/list.html | 197 ++++++++++++++++---------------- templates/chat/chat_css.html | 2 +- templates/comments/feed.html | 18 +++ templates/problem/comments.html | 28 ----- templates/problem/feed.html | 28 +++++ templates/problem/problem.html | 50 +++++++- templates/ticket/feed.html | 23 ++++ 14 files changed, 478 insertions(+), 222 deletions(-) create mode 100644 templates/comments/feed.html delete mode 100644 templates/problem/comments.html create mode 100644 templates/problem/feed.html create mode 100644 templates/ticket/feed.html diff --git a/dmoj/urls.py b/dmoj/urls.py index 4869ab6..c691074 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -110,13 +110,17 @@ urlpatterns = [ url(r'^accounts/', include(register_patterns)), url(r'^', include('social_django.urls')), + url(r'^feed/', include([ + url(r'^problems/$', problem.ProblemFeed.as_view(), name='problem_feed'), + url(r'^tickets/$', blog.TicketFeed.as_view(), name='ticket_feed'), + url(r'^comments/$', blog.CommentFeed.as_view(), name='comment_feed'), + ])), url(r'^problems/$', problem.ProblemList.as_view(), name='problem_list'), url(r'^problems/random/$', problem.RandomProblem.as_view(), name='problem_random'), url(r'^problem/(?P[^/]+)', include([ url(r'^$', problem.ProblemDetail.as_view(), name='problem_detail'), url(r'^/editorial$', problem.ProblemSolution.as_view(), name='problem_editorial'), - url(r'^/comments$', problem.ProblemComments.as_view(), name='problem_comments'), url(r'^/raw$', problem.ProblemRaw.as_view(), name='problem_raw'), url(r'^/pdf$', problem.ProblemPdfView.as_view(), name='problem_pdf'), url(r'^/pdf/(?P[a-z-]+)$', problem.ProblemPdfView.as_view(), name='problem_pdf'), diff --git a/judge/models/comment.py b/judge/models/comment.py index a58ab1d..5d26500 100644 --- a/judge/models/comment.py +++ b/judge/models/comment.py @@ -65,7 +65,9 @@ class Comment(MPTTModel): 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)) - + + if n == -1: + n = len(queryset) if user.is_superuser: return queryset[:n] if batch is None: @@ -105,7 +107,7 @@ class Comment(MPTTModel): try: link = None if self.page.startswith('p:'): - link = reverse('problem_comments', args=(self.page[2:],)) + 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:'): diff --git a/judge/views/blog.py b/judge/views/blog.py index c16aa6e..4a734d5 100644 --- a/judge/views/blog.py +++ b/judge/views/blog.py @@ -17,10 +17,8 @@ from judge.utils.tickets import filter_visible_tickets from judge.utils.views import TitleMixin -class PostList(ListView): - model = BlogPost - paginate_by = 10 - context_object_name = 'posts' +# General view for all content list on home feed +class FeedView(ListView): template_name = 'blog/list.html' title = None @@ -29,6 +27,56 @@ class PostList(ListView): 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['has_clarifications'] = False + if self.request.user.is_authenticated: + participation = self.request.profile.current_contest + if participation: + clarifications = ProblemClarification.objects.filter(problem__in=participation.contest.problems.all()) + context['has_clarifications'] = clarifications.count() > 0 + context['clarifications'] = clarifications.order_by('-date') + 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() + + # Dashboard stuff + # if self.request.user.is_authenticated: + # user = self.request.profile + # context['recently_attempted_problems'] = (Submission.objects.filter(user=user) + # .exclude(problem__in=user_completed_ids(user)) + # .values_list('problem__code', 'problem__name', 'problem__points') + # .annotate(points=Max('points'), latest=Max('date')) + # .order_by('-latest') + # [:settings.DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT]) + + visible_contests = Contest.get_visible_contests(self.request.user).filter(is_visible=True) \ + .order_by('start_time') + + context['current_contests'] = visible_contests.filter(start_time__lte=now, end_time__gt=now) + context['future_contests'] = visible_contests.filter(start_time__gt=now) + + visible_contests = Contest.get_visible_contests(self.request.user).filter(is_visible=True) + + context['top_rated'] = Profile.objects.order_by('-rating')[:10] + context['top_scorer'] = Profile.objects.order_by('-performance_points')[:10] + + return context + + +class PostList(FeedView): + model = BlogPost + paginate_by = 10 + context_object_name = 'posts' + def get_queryset(self): queryset = BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()) \ .order_by('-sticky', '-publish_on') \ @@ -45,25 +93,7 @@ class PostList(ListView): 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['comments'] = Comment.most_recent(self.request.user, 25) - context['new_problems'] = Problem.objects.filter(is_public=True, is_organization_private=False) \ - .order_by('-date', '-id')[:settings.DMOJ_BLOG_NEW_PROBLEM_COUNT] - context['page_titles'] = CacheDict(lambda page: Comment.get_page_title(page)) - - context['has_clarifications'] = False - if self.request.user.is_authenticated: - participation = self.request.profile.current_contest - if participation: - clarifications = ProblemClarification.objects.filter(problem__in=participation.contest.problems.all()) - context['has_clarifications'] = clarifications.count() > 0 - context['clarifications'] = clarifications.order_by('-date') - if participation.contest.is_editable_by(self.request.user): - context['can_edit_contest'] = True - 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) - + context['feed_type'] = 'blog' context['post_comment_counts'] = { int(page[2:]): count for page, count in Comment.objects @@ -71,40 +101,55 @@ class PostList(ListView): .values_list('page').annotate(count=Count('page')).order_by() } - now = timezone.now() + return context - # Dashboard stuff - if self.request.user.is_authenticated: - user = self.request.profile - context['recently_attempted_problems'] = (Submission.objects.filter(user=user) - .exclude(problem__in=user_completed_ids(user)) - .values_list('problem__code', 'problem__name', 'problem__points') - .annotate(points=Max('points'), latest=Max('date')) - .order_by('-latest') - [:settings.DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT]) - - visible_contests = Contest.get_visible_contests(self.request.user).filter(is_visible=True) \ - .order_by('start_time') - context['current_contests'] = visible_contests.filter(start_time__lte=now, end_time__gt=now) - context['future_contests'] = visible_contests.filter(start_time__gt=now) +class TicketFeed(FeedView): + model = Ticket + context_object_name = 'tickets' + paginate_by = 30 - visible_contests = Contest.get_visible_contests(self.request.user).filter(is_visible=True) - if self.request.user.is_authenticated: - profile = self.request.profile - context['own_open_tickets'] = (Ticket.objects.filter(Q(user=profile) | Q(assignees__in=[profile]), is_open=True).order_by('-id') - .prefetch_related('linked_item').select_related('user__user')) + def get_queryset(self, is_own=False): + profile = self.request.profile + if is_own: + if self.request.user.is_authenticated: + return (Ticket.objects.filter(Q(user=profile) | Q(assignees__in=[profile]), is_open=True).order_by('-id') + .prefetch_related('linked_item').select_related('user__user')) + else: + return [] else: - profile = None - context['own_open_tickets'] = [] + # Superusers better be staffs, not the spell-casting kind either. + if self.request.user.is_staff: + tickets = (Ticket.objects.order_by('-id').filter(is_open=True).prefetch_related('linked_item') + .select_related('user__user')) + return filter_visible_tickets(tickets, self.request.user, profile) + else: + return [] - # Superusers better be staffs, not the spell-casting kind either. - if self.request.user.is_staff: - tickets = (Ticket.objects.order_by('-id').filter(is_open=True).prefetch_related('linked_item') - .select_related('user__user')) - context['open_tickets'] = filter_visible_tickets(tickets, self.request.user, profile)[:10] - else: - context['open_tickets'] = [] + def get_context_data(self, **kwargs): + context = super(TicketFeed, self).get_context_data(**kwargs) + context['feed_type'] = 'ticket' + context['first_page_href'] = self.request.path + context['page_prefix'] = '?page=' + context['title'] = _('Ticket feed') + + return context + + +class CommentFeed(FeedView): + model = Comment + context_object_name = 'comments' + paginate_by = 50 + + def get_queryset(self): + return Comment.most_recent(self.request.user, 1000) + + def get_context_data(self, **kwargs): + context = super(CommentFeed, self).get_context_data(**kwargs) + context['feed_type'] = 'comment' + context['first_page_href'] = self.request.path + context['page_prefix'] = '?page=' + context['title'] = _('Comment feed') return context diff --git a/judge/views/problem.py b/judge/views/problem.py index 9149511..995c14e 100644 --- a/judge/views/problem.py +++ b/judge/views/problem.py @@ -21,7 +21,7 @@ from django.utils.functional import cached_property from django.utils.html import escape, format_html from django.utils.safestring import mark_safe from django.utils.translation import gettext as _, gettext_lazy -from django.views.generic import DetailView, ListView, View +from django.views.generic import ListView, View from django.views.generic.base import TemplateResponseMixin from django.views.generic.detail import SingleObjectMixin @@ -38,6 +38,7 @@ from judge.utils.problems import contest_attempted_ids, contest_completed_ids, h from judge.utils.strings import safe_float_or_none, safe_int_or_none from judge.utils.tickets import own_ticket_filter from judge.utils.views import QueryStringSortMixin, SingleObjectFormView, TitleMixin, generic_message +from judge.views.blog import FeedView def get_contest_problem(problem, profile): @@ -155,10 +156,13 @@ class ProblemRaw(ProblemMixin, TitleMixin, TemplateResponseMixin, SingleObjectMi )) -class ProblemDetail(ProblemMixin, SolvedProblemMixin, DetailView): +class ProblemDetail(ProblemMixin, SolvedProblemMixin, CommentedDetailView): context_object_name = 'problem' template_name = 'problem/problem.html' + def get_comment_page(self): + return 'p:%s' % self.object.code + def get_context_data(self, **kwargs): context = super(ProblemDetail, self).get_context_data(**kwargs) user = self.request.user @@ -235,6 +239,7 @@ class ProblemDetail(ProblemMixin, SolvedProblemMixin, DetailView): context['min_possible_vote'] = 100 return context + class DeleteVote(ProblemMixin, SingleObjectMixin, View): def get(self, request, *args, **kwargs): return HttpResponseForbidden(status=405, content_type='text/plain') @@ -273,21 +278,6 @@ class Vote(ProblemMixin, SingleObjectMixin, View): return JsonResponse(form.errors, status=400) -class ProblemComments(ProblemMixin, TitleMixin, CommentedDetailView): - context_object_name = 'problem' - template_name = 'problem/comments.html' - - def get_title(self): - return _('Disscuss {0}').format(self.object.name) - - def get_content_title(self): - return format_html(_(u'Discuss {0}'), self.object.name, - reverse('problem_detail', args=[self.object.code])) - - def get_comment_page(self): - return 'p:%s' % self.object.code - - class LatexError(Exception): pass @@ -592,6 +582,49 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView return HttpResponseRedirect(request.get_full_path()) +class ProblemFeed(FeedView): + model = Problem + context_object_name = 'problems' + paginate_by = 50 + title = _('Problem feed') + + @cached_property + def profile(self): + if not self.request.user.is_authenticated: + return None + return self.request.profile + + def get_unsolved_queryset(self): + filter = Q(is_public=True) + if self.profile is not None: + filter |= Q(authors=self.profile) + filter |= Q(curators=self.profile) + filter |= Q(testers=self.profile) + queryset = Problem.objects.filter(filter).select_related('group').defer('description') + if not self.request.user.has_perm('see_organization_problem'): + filter = Q(is_organization_private=False) + if self.profile is not None: + filter |= Q(organizations__in=self.profile.organizations.all()) + queryset = queryset.filter(filter) + if self.profile is not None: + queryset = queryset.exclude(id__in=Submission.objects.filter(user=self.profile, points=F('problem__points')) + .values_list('problem__id', flat=True)) + return queryset.distinct() + + def get_queryset(self): + queryset = self.get_unsolved_queryset() + return queryset.order_by('?') + + def get_context_data(self, **kwargs): + context = super(ProblemFeed, self).get_context_data(**kwargs) + context['first_page_href'] = self.request.path + context['page_prefix'] = '?page=' + context['feed_type'] = 'problem' + context['title'] = self.title + + return context + + class LanguageTemplateAjax(View): def get(self, request, *args, **kwargs): try: diff --git a/resources/base.scss b/resources/base.scss index 369b91b..9e21b4c 100644 --- a/resources/base.scss +++ b/resources/base.scss @@ -232,7 +232,7 @@ header { } #navigation { - position: relative; + position: fixed; top: 0; left: 0; right: 0; @@ -382,7 +382,7 @@ hr { } #content { - margin: 1em auto auto; + margin: 4.5em auto 1em auto; // Header width: 90%; diff --git a/resources/blog.scss b/resources/blog.scss index cd6aeb2..8cd8693 100644 --- a/resources/blog.scss +++ b/resources/blog.scss @@ -2,9 +2,9 @@ .blog-content { padding-right: 0em; - flex: 73.5%; vertical-align: top; margin-right: 0; + width: 100%; .post { border: 1px dotted grey; @@ -33,7 +33,8 @@ } .blog-sidebar { - flex: 26.5%; + width: 100%; + margin-left: auto; } .blog-sidebox { @@ -88,6 +89,19 @@ color: #555; } +@media (max-width: 799px) { + .left-sidebar-header { + display: none; + } + .left-sidebar-item { + display: inline-block; + } + .blog-left-sidebar { + text-align: right; + padding-right: 1em; + margin-bottom: 1em; + } +} @media (min-width: 800px) { .blog-content, .blog-sidebar { display: block !important; @@ -104,6 +118,28 @@ #blog-container { display: flex; } + + .blog-content { + max-width: 71.5%; + margin-left: 10%; + } + + .blog-sidebar { + width: 18%; + } + + .blog-left-sidebar { + width: 8%; + margin-right: 1em; + position: fixed; + height: 100%; + margin-top: -4em; + padding-top: 4em; + } + + .feed-table { + font-size: small; + } } #mobile.tabs { @@ -135,3 +171,63 @@ } } } + +.blog-box { + border-bottom: 1px solid black; + width: 90%; + margin-bottom: 2.5em; + padding: 0.5em 1.25em; + background-color: white; + margin-left: auto; + margin-right: auto; +} + +.blog-description { + max-height: 20em; + overflow: hidden; + overflow-wrap: anywhere; + padding-bottom: 1em; +} +.problem-feed-name { + display: inline; + font-weight: bold; +} +.problem-feed-name a { + color: #0645ad; +} +.problem-feed-info-entry { + display: inline; + float: right; +} +.problem-feed-types { + color: gray; +} + +.blog-left-sidebar { + background-color: #f0f1f3; + color: #616161; +} + +.left-sidebar-item { + padding: 1em 0.5em; + text-align: center; +} +.left-sidebar-item:hover { + background-color: lightgray; + cursor: pointer; +} +.sidebar-icon { + font-size: x-large; + margin-bottom: 0.1em; + color: black; +} +.left-sidebar-header { + text-align: center; + padding-bottom: 1em; + border-bottom: 1px solid black; + color: black; + border-radius: 0; +} +.feed-table { + margin: 0; +} \ No newline at end of file diff --git a/templates/blog/content.html b/templates/blog/content.html index fd8804d..753e36e 100644 --- a/templates/blog/content.html +++ b/templates/blog/content.html @@ -1,18 +1,16 @@ -
-

- {{ post.title }} -

- +
+
- {%- if post.sticky %}{% endif -%} {% with authors=post.authors.all() %} {%- if authors -%} + {%- endif -%} {% endwith %} - {{_('posted')}} {{ relative_time(post.publish_on, abs=_('on {time}'), rel=_('{time}')) -}} + • {{ relative_time(post.publish_on, abs=_('on {time}'), rel=_('{time}')) -}} + {%- if post.sticky %} • {% endif -%} - + @@ -20,8 +18,10 @@ - -
+
+

+ {{ post.title }} +

{% if post.is_organization_private and show_organization_private_icon %}
{% for org in post.organizations.all() %} @@ -33,7 +33,7 @@ {% endfor %}
{% endif %} -
+
{% cache 86400 'post_summary' post.id %} {{ post.summary|default(post.content, true)|markdown('blog', 'svg', lazy_load=True)|reference|str|safe }} {% endcache %} diff --git a/templates/blog/list.html b/templates/blog/list.html index cd5b65d..f9a2f7b 100644 --- a/templates/blog/list.html +++ b/templates/blog/list.html @@ -16,30 +16,10 @@ clear: both; } } - .post { - margin: 0 2%; - } .time { margin-left: 0; } - .post:first-child { - margin-top: 0.6em; - } - - .own-open-tickets .title a, .open-tickets .title a { - display: block; - } - - .own-open-tickets .object, .open-tickets .object { - margin-left: 1em; - font-style: italic; - } - - .open-tickets .user { - margin-left: 1em; - } - .no-clarifications-message { font-style: italic; text-align: center; @@ -56,6 +36,11 @@ #add-clarification:hover { color: cyan; } + + #content { + width: 99%; + margin-left: 0; + } {% endblock %} @@ -81,6 +66,35 @@ $('.blog-content').hide(); $('.blog-sidebar').show(); }); + $('.blog-description').on('click', function() { + var max_height = $(this).css('max-height'); + if (max_height !== 'fit-content') { + $(this).css('max-height', 'fit-content'); + $(this).parent().css('background-image', 'inherit') + .css('padding-bottom', '0.5em'); + $(this).css('cursor', 'auto'); + } + }) + $('.blog-description').each(function() { + if ($(this).prop('scrollHeight') > $(this).height() ) { + $(this).parent().css('background-image', '-webkit-linear-gradient(bottom, lightgray, lightgray 3%, transparent 8%, transparent 100%)'); + $(this).parent().css('padding-bottom', '0'); + $(this).css('cursor', 'pointer'); + } + }); + $('.left-sidebar-item').on('click', function() { + var url = $(this).attr('data-href'); + window.location.replace(url); + }); + {% if feed_type == 'blog' %} + $('#news-icon').css('color', 'green'); + {% elif feed_type == 'problem' %} + $('#problems-icon').css('color', 'green'); + {% elif feed_type == 'ticket' %} + $('#tickets-icon').css('color', 'green'); + {% elif feed_type == 'comment' %} + $('#comments-icon').css('color', 'green'); + {% endif %} }); {% endblock %} @@ -90,20 +104,50 @@
- {% endif %} - - {% if own_open_tickets %} - - {% endif %} - - {% if open_tickets %} - - {% endif %} - + {% else %} +
+ {% include "comments/list.html" %} +
{% endif %} {% endblock %} diff --git a/templates/ticket/feed.html b/templates/ticket/feed.html new file mode 100644 index 0000000..a3f2af3 --- /dev/null +++ b/templates/ticket/feed.html @@ -0,0 +1,23 @@ +
+

+ + {{ ticket.title }} + +

+ {% with author=ticket.user %} + {% if author %} +
+ + {{ link_user(author) }} +
+ {% endif %} + {% endwith %} + +
+ {{ ticket.messages.last().body |markdown("ticket", MATH_ENGINE)|reference|str|safe }} +
+
\ No newline at end of file