From c535ae4415e974e9a0eca02ef2ed1a8b93fb4cff Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 26 Sep 2023 13:05:00 -0500 Subject: [PATCH 01/51] Fix bug for without href --- resources/common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/common.js b/resources/common.js index d34e259..185eb7c 100644 --- a/resources/common.js +++ b/resources/common.js @@ -387,7 +387,7 @@ function onWindowReady() { }); $('a').click(function() { var href = $(this).attr('href'); - if (href === '#' || href.startsWith("javascript")) { + if (!href || href === '#' || href.startsWith("javascript")) { return; } From cbcbbc5277829b32eb7b4c40cfdec6b4d230495f Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 27 Sep 2023 18:07:24 -0500 Subject: [PATCH 02/51] Add chunk upload dir to settings --- dmoj/settings.py | 3 +++ judge/utils/fine_uploader.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dmoj/settings.py b/dmoj/settings.py index d5cfa38..e136a4d 100644 --- a/dmoj/settings.py +++ b/dmoj/settings.py @@ -485,6 +485,9 @@ META_REMOTE_ADDRESS_KEY = "REMOTE_ADDR" DEFAULT_AUTO_FIELD = "django.db.models.AutoField" +# Chunk upload +CHUNK_UPLOAD_DIR = "/tmp/chunk_upload_tmp" + try: with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f: exec(f.read(), globals()) diff --git a/judge/utils/fine_uploader.py b/judge/utils/fine_uploader.py index a9909a7..2a22ccf 100644 --- a/judge/utils/fine_uploader.py +++ b/judge/utils/fine_uploader.py @@ -35,7 +35,7 @@ def save_upload(f, path): # pass callback function to post_upload def handle_upload(f, fileattrs, upload_dir, post_upload=None): - chunks_dir = os.path.join(tempfile.gettempdir(), "chunk_upload_tmp") + chunks_dir = settings.CHUNK_UPLOAD_DIR if not os.path.exists(os.path.dirname(chunks_dir)): os.makedirs(os.path.dirname(chunks_dir)) chunked = False From 0e1a3992ebcec6343f371f80143282f4b422f250 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 28 Sep 2023 18:23:39 -0500 Subject: [PATCH 03/51] Comment page speed --- judge/comments.py | 7 ++++--- judge/views/comment.py | 15 ++++----------- templates/comments/content-list.html | 9 +++------ templates/comments/media-js.html | 2 +- 4 files changed, 12 insertions(+), 21 deletions(-) diff --git a/judge/comments.py b/judge/comments.py index be33a4e..65d8f7e 100644 --- a/judge/comments.py +++ b/judge/comments.py @@ -168,7 +168,7 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View): ) def _get_queryset(self, target_comment): - if target_comment != None: + if target_comment: queryset = target_comment.get_descendants(include_self=True) queryset = ( queryset.select_related("author__user") @@ -217,11 +217,11 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View): context["has_comments"] = queryset.exists() context["comment_lock"] = self.is_comment_locked() - context["comment_list"] = queryset + context["comment_list"] = list(queryset) context["vote_hide_threshold"] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD if queryset.exists(): - context["comment_root_id"] = queryset[0].id + context["comment_root_id"] = context["comment_list"][0].id else: context["comment_root_id"] = 0 context["comment_parent_none"] = 1 @@ -234,4 +234,5 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View): context["limit"] = DEFAULT_OFFSET context["comment_count"] = comment_count + context["profile"] = self.request.profile return context diff --git a/judge/views/comment.py b/judge/views/comment.py index 03285ad..58ba3fc 100644 --- a/judge/views/comment.py +++ b/judge/views/comment.py @@ -15,12 +15,11 @@ from django.http import ( HttpResponseBadRequest, HttpResponseForbidden, ) -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, render from django.utils.translation import gettext as _ from django.views.decorators.http import require_POST from django.views.generic import DetailView, UpdateView from django.urls import reverse_lazy -from django.template import loader from reversion import revisions from reversion.models import Version @@ -42,11 +41,6 @@ __all__ = [ @login_required - -# def get_more_reply(request, id): -# queryset = Comment.get_pk(id) - - def vote_comment(request, delta): if abs(delta) != 1: return HttpResponseBadRequest( @@ -164,10 +158,11 @@ def get_comments(request, limit=10): new_offset = offset + min(len(queryset), limit) - comment_html = loader.render_to_string( + return render( + request, "comments/content-list.html", { - "request": request, + "profile": profile, "comment_root_id": comment_root_id, "comment_list": queryset, "vote_hide_threshold": settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD, @@ -181,8 +176,6 @@ def get_comments(request, limit=10): }, ) - return HttpResponse(comment_html) - def get_show_more(request): return get_comments(request) diff --git a/templates/comments/content-list.html b/templates/comments/content-list.html index c08fd46..ebf0c31 100644 --- a/templates/comments/content-list.html +++ b/templates/comments/content-list.html @@ -1,13 +1,10 @@ -{% set logged_in = request.user.is_authenticated %} -{% set profile = request.profile if logged_in else None %} - {% for node in mptt_tree(comment_list) recursive %}
  • - {% if logged_in %} + {% if profile %} {% else %} @@ -16,7 +13,7 @@ {% endif %}
    {{ node.score }}
    - {% if logged_in %} + {% if profile %} {% else %} @@ -55,7 +52,7 @@ - {% if logged_in and not comment_lock %} + {% if profile and not comment_lock %} {% set can_edit = node.author.id == profile.id and not profile.mute %} {% if can_edit %} $(document).ready(function () { - let loading_gif = ""; + let loading_gif = ""; window.reply_comment = function (parent) { var $comment_reply = $('#comment-' + parent + '-reply'); var reply_id = 'reply-' + parent; From 21905fd1db60c3e4a6503f5ee2acd09ef1218477 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 29 Sep 2023 00:37:28 -0500 Subject: [PATCH 04/51] Fix local var error --- judge/views/comment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/judge/views/comment.py b/judge/views/comment.py index 58ba3fc..6965201 100644 --- a/judge/views/comment.py +++ b/judge/views/comment.py @@ -150,6 +150,7 @@ def get_comments(request, limit=10): revisions=Count("versions", distinct=True), )[offset : offset + limit] ) + profile = None if request.user.is_authenticated: profile = request.profile queryset = queryset.annotate( From 067214b587670e508f52cc5968ca0c4b7a5fcd05 Mon Sep 17 00:00:00 2001 From: Bao Le <127121163+BaoLe106@users.noreply.github.com> Date: Tue, 3 Oct 2023 02:30:46 +0800 Subject: [PATCH 05/51] Markdown Editor (#85) --- dmoj/urls.py | 6 + judge/views/markdown_editor.py | 14 ++ locale/vi/LC_MESSAGES/django.po | 13 ++ locale/vi/LC_MESSAGES/dmoj-user.po | 7 + .../markdown_editor/markdown_editor.html | 134 ++++++++++++++++++ 5 files changed, 174 insertions(+) create mode 100644 judge/views/markdown_editor.py create mode 100644 templates/markdown_editor/markdown_editor.html diff --git a/dmoj/urls.py b/dmoj/urls.py index 402a569..4e0691e 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -44,6 +44,7 @@ from judge.views import ( language, license, mailgun, + markdown_editor, notification, organization, preview, @@ -405,6 +406,11 @@ urlpatterns = [ ] ), ), + url( + r"^markdown_editor/", + markdown_editor.MarkdownEditor.as_view(), + name="markdown_editor", + ), url( r"^submission_source_file/(?P(\w|\.)+)", submission.SubmissionSourceFileView.as_view(), diff --git a/judge/views/markdown_editor.py b/judge/views/markdown_editor.py new file mode 100644 index 0000000..a014ba6 --- /dev/null +++ b/judge/views/markdown_editor.py @@ -0,0 +1,14 @@ +from django.views import View +from django.shortcuts import render +from django.utils.translation import gettext_lazy as _ + + +class MarkdownEditor(View): + def get(self, request): + return render( + request, + "markdown_editor/markdown_editor.html", + { + "title": _("Markdown Editor"), + }, + ) diff --git a/locale/vi/LC_MESSAGES/django.po b/locale/vi/LC_MESSAGES/django.po index a4dde83..4497f98 100644 --- a/locale/vi/LC_MESSAGES/django.po +++ b/locale/vi/LC_MESSAGES/django.po @@ -130,6 +130,18 @@ msgstr "Thể thức" msgid "Rating" msgstr "" +#: templates/markdown_editor/markdown_editor.html:107 +msgid "Insert Image" +msgstr "Chèn hình ảnh" + +#: templates/markdown_editor/markdown_editor.html:110 +msgid "From the web" +msgstr "Từ web" + +#: templates/markdown_editor/markdown_editor.html:116 +msgid "From your computer" +msgstr "Từ máy tính của bạn" + #: judge/admin/contest.py:201 msgid "Access" msgstr "Truy cập" @@ -6255,3 +6267,4 @@ msgstr "Chọn tất cả" #~ msgid "Hard" #~ msgstr "Khó" + diff --git a/locale/vi/LC_MESSAGES/dmoj-user.po b/locale/vi/LC_MESSAGES/dmoj-user.po index ffd36e9..4ea605c 100644 --- a/locale/vi/LC_MESSAGES/dmoj-user.po +++ b/locale/vi/LC_MESSAGES/dmoj-user.po @@ -39,6 +39,12 @@ msgstr "Đăng ký tên" msgid "Report" msgstr "Báo cáo" +msgid "Insert Image" +msgstr "Chèn hình ảnh" + +msgid "Save" +msgstr "Lưu" + msgid "2sat" msgstr "" @@ -593,3 +599,4 @@ msgstr "" msgid "z-function" msgstr "" + diff --git a/templates/markdown_editor/markdown_editor.html b/templates/markdown_editor/markdown_editor.html new file mode 100644 index 0000000..04418e4 --- /dev/null +++ b/templates/markdown_editor/markdown_editor.html @@ -0,0 +1,134 @@ +{% extends "base.html" %} + +{% block media %} + +{% endblock %} + +{% block js_media %} + + + + + + + + + + + + + + +{% endblock %} + +{% block title_row %} +{% endblock %} + +{% block body %} + + +
    +
    +
    +{% endblock %} + From c64baad181232dd4baacd17f490545fa708f7f5a Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 5 Oct 2023 01:09:09 -0500 Subject: [PATCH 06/51] Fix xss --- templates/contest/list.html | 96 ++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 49 deletions(-) diff --git a/templates/contest/list.html b/templates/contest/list.html index 226b219..184a70e 100644 --- a/templates/contest/list.html +++ b/templates/contest/list.html @@ -112,56 +112,54 @@ {% endblock %} {% macro contest_head(contest) %} - {% spaceless %} - - {{- contest.name -}} - -
    -
    - {% if not contest.is_visible %} - - {{ _('hidden') }} - + + {{contest.name}} + +
    +
    + {% if not contest.is_visible %} + + {{ _('hidden') }} + + {% endif %} + {% if contest.is_editable %} + + + {{ _('Edit') }} + + + {% endif %} + {% if contest.is_private %} + + {{ _('private') }} + + {% endif %} + {% if not hide_contest_orgs %} + {% if contest.is_organization_private %} + {% for org in contest.organizations.all() %} + + + {{ org.name }} + + + {% endfor %} {% endif %} - {% if contest.is_editable %} - - - {{ _('Edit') }} - - - {% endif %} - {% if contest.is_private %} - - {{ _('private') }} - - {% endif %} - {% if not hide_contest_orgs %} - {% if contest.is_organization_private %} - {% for org in contest.organizations.all() %} - - - {{ org.name }} - - - {% endfor %} - {% endif %} - {% endif %} - {% if contest.is_rated %} - - {{ _('rated') }} - - {% endif %} - {% for tag in contest.tags.all() %} - - - {{- tag.name -}} - - - {% endfor %} -
    - {% endspaceless %} + {% endif %} + {% if contest.is_rated %} + + {{ _('rated') }} + + {% endif %} + {% for tag in contest.tags.all() %} + + + {{- tag.name -}} + + + {% endfor %} +
    {% endmacro %} {% macro time_left(contest, padding_top = true) %} From 458b9e425efd3e2a23f10dff589c73126b60b186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=B5=20Trung=20Ho=C3=A0ng=20H=C6=B0ng?= <89190591+HungBacktracking@users.noreply.github.com> Date: Fri, 6 Oct 2023 01:06:21 +0700 Subject: [PATCH 07/51] fix color in pagination of contest page (#86) --- resources/widgets.scss | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/resources/widgets.scss b/resources/widgets.scss index 3f2ea95..942b39e 100644 --- a/resources/widgets.scss +++ b/resources/widgets.scss @@ -322,7 +322,7 @@ input { // Bootstrap-y pagination ul.pagination a:hover { color: #FFF; - background: #0aa082; + background: #cc4e17; border: none; } @@ -338,22 +338,6 @@ ul.pagination { li { display: inline; - // &:first-child > { - // a, span { - // margin-left: 0; - // border-top-left-radius: $widget_border_radius; - // border-bottom-left-radius: $widget_border_radius; - // } - // } - - // &:last-child > { - // a, span { - // margin-left: 0; - // border-top-right-radius: $widget_border_radius; - // border-bottom-right-radius: $widget_border_radius; - // } - // } - > { a, span { position: relative; @@ -373,15 +357,15 @@ ul.pagination { .disabled-page > { a { - color: #888; - background-color: #04534380; - border-color: #04534380; + color: #f1efef; + background-color: #ab6247; + border-color: #6a240b; } span { - color: #888; - background-color: #04534380; - border-color: #04534380; + color: #f1efef; + background-color: #ab6247; + border-color: #6a240b; } } From 49d1bc2e1bbf1c7ac20ae970716e7a554b710e68 Mon Sep 17 00:00:00 2001 From: Bao Le <127121163+BaoLe106@users.noreply.github.com> Date: Fri, 6 Oct 2023 02:08:24 +0800 Subject: [PATCH 08/51] Markdown editor improvements (#87) --- .../markdown_editor/markdown_editor.html | 103 ++++++++++-------- 1 file changed, 56 insertions(+), 47 deletions(-) diff --git a/templates/markdown_editor/markdown_editor.html b/templates/markdown_editor/markdown_editor.html index 04418e4..c56a6c5 100644 --- a/templates/markdown_editor/markdown_editor.html +++ b/templates/markdown_editor/markdown_editor.html @@ -2,20 +2,19 @@ {% block media %} {% endblock %} @@ -49,13 +51,15 @@ preview: $(this).val() }, success: function(data) { - $('#display').html(data) + $('#display').html(data); + MathJax.typeset(); }, error: function(error) { alert(error); console.log(error.message) } }) + }); }); @@ -90,45 +94,50 @@ {% block title_row %} {% endblock %} -{% block body %} -
    - -
    -
    -
    -
    - -
    -
    -
    {{_('Update Preview')}}
    -
    -
    -
    -

    {{_('Insert Image')}}

    -
    -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    -
    +{% block title_ruler %} +{% endblock %} -
    -
    +{% block body %} +
    +
    + +
    +
    +
    +
    + +
    +
    +
    {{_('Update Preview')}}
    +
    +
    +
    +

    {{_('Insert Image')}}

    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + +
    +
    +
    {% endblock %} From 44aca3c2e56ab568badd264e077653eda857b2b9 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 5 Oct 2023 13:11:58 -0500 Subject: [PATCH 09/51] Add darkmode --- resources/darkmode.css | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/resources/darkmode.css b/resources/darkmode.css index 4947452..f9e4d3a 100644 --- a/resources/darkmode.css +++ b/resources/darkmode.css @@ -2552,7 +2552,7 @@ input[type="text"]:focus, input[type="password"]:focus { ul.pagination a:hover { color: rgb(232, 230, 227); background-image: initial; - background-color: rgb(8, 128, 104); + background-color: rgb(163, 62, 18); border-color: initial; } ul.pagination > li > a, @@ -2563,14 +2563,14 @@ ul.pagination > li > span { border-color: rgb(199, 70, 8); } ul.pagination > .disabled-page > a { - color: rgb(157, 148, 136); - background-color: rgba(3, 66, 54, 0.5); - border-color: rgba(126, 117, 103, 0.5); + color: rgb(223, 220, 215); + background-color: rgb(137, 78, 57); + border-color: rgb(199, 68, 21); } ul.pagination > .disabled-page > span { - color: rgb(157, 148, 136); - background-color: rgba(3, 66, 54, 0.5); - border-color: rgba(126, 117, 103, 0.5); + color: rgb(223, 220, 215); + background-color: rgb(137, 78, 57); + border-color: rgb(199, 68, 21); } ul.pagination > .active-page > a { color: rgb(232, 230, 227); @@ -3868,14 +3868,9 @@ mjx-merror { mjx-assistive-mml { border-color: initial !important; } -mjx-stretchy-v > mjx-ext { - border-color: transparent; -} -.recently-attempted ul { - list-style-image: initial; -} -.organization-row:last-child { - border-bottom-color: initial; +#users-table td:nth-child(2), +#users-table th:nth-child(2) { + border-right-color: initial; } /* Override Style */ From 9decd112186fdc662407920946c4e5cc221ec39d Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Thu, 5 Oct 2023 14:00:20 -0500 Subject: [PATCH 10/51] Change textarea css --- resources/comments.scss | 2 +- resources/darkmode.css | 20 ++++++------- resources/dmmd-preview.css | 5 ++-- resources/pagedown_widget.css | 2 +- resources/pagedown_widget.scss | 54 +++++++++++++++++----------------- resources/widgets.scss | 10 +++---- 6 files changed, 46 insertions(+), 47 deletions(-) diff --git a/resources/comments.scss b/resources/comments.scss index c606f26..42dc90b 100644 --- a/resources/comments.scss +++ b/resources/comments.scss @@ -214,7 +214,7 @@ a { input, textarea { min-width: 100%; max-width: 100%; - font-size: 1em; + font-size: 15px; } } diff --git a/resources/darkmode.css b/resources/darkmode.css index f9e4d3a..f9a4884 100644 --- a/resources/darkmode.css +++ b/resources/darkmode.css @@ -2471,16 +2471,13 @@ input[type="text"], input[type="password"], input[type="email"], input[type="num box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 1px inset; } textarea { - color: rgb(178, 172, 162); background-image: none; background-color: rgb(24, 26, 27); border-color: rgb(62, 68, 70); box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 1px inset; } textarea:hover { - border-color: rgba(16, 87, 144, 0.8); - box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 1px inset, - rgba(16, 91, 150, 0.6) 0px 0px 4px; + border-color: rgb(140, 130, 115); } input[type="text"]:hover, input[type="password"]:hover { border-color: rgba(16, 87, 144, 0.8); @@ -2488,9 +2485,7 @@ input[type="text"]:hover, input[type="password"]:hover { rgba(16, 91, 150, 0.6) 0px 0px 4px; } textarea:focus { - border-color: rgba(16, 87, 144, 0.8); - box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 1px inset, - rgba(16, 91, 150, 0.6) 0px 0px 8px; outline-color: initial; + border-color: rgb(140, 130, 115); outline-color: initial; } input[type="text"]:focus, input[type="password"]:focus { border-color: rgba(16, 87, 144, 0.8); @@ -3868,9 +3863,14 @@ mjx-merror { mjx-assistive-mml { border-color: initial !important; } -#users-table td:nth-child(2), -#users-table th:nth-child(2) { - border-right-color: initial; +mjx-stretchy-v > mjx-ext { + border-color: transparent; +} +.recently-attempted ul { + list-style-image: initial; +} +.organization-row:last-child { + border-bottom-color: initial; } /* Override Style */ diff --git a/resources/dmmd-preview.css b/resources/dmmd-preview.css index 7a7fd1d..c768a0e 100644 --- a/resources/dmmd-preview.css +++ b/resources/dmmd-preview.css @@ -13,7 +13,7 @@ div.dmmd-preview-update { } div.dmmd-preview-content { - padding: 0 7px; + padding: 0 8px; } div.dmmd-preview.dmmd-preview-has-content div.dmmd-preview-update { @@ -21,7 +21,8 @@ div.dmmd-preview.dmmd-preview-has-content div.dmmd-preview-update { } div.dmmd-preview-has-content div.dmmd-preview-content { - padding-bottom: 7px; + padding-bottom: 8px; + padding-top: 8px; } div.dmmd-no-button div.dmmd-preview-update { diff --git a/resources/pagedown_widget.css b/resources/pagedown_widget.css index f20c5e2..3601240 100644 --- a/resources/pagedown_widget.css +++ b/resources/pagedown_widget.css @@ -14,7 +14,7 @@ width: 100%; background: #fff; border: 1px solid DarkGray; - font-family: Consolas, "Liberation Mono", Monaco, "Courier New", monospace !important; + font-family: "Noto Sans",Arial,"Lucida Grande",sans-serif !important; } .wmd-preview { diff --git a/resources/pagedown_widget.scss b/resources/pagedown_widget.scss index b6d2333..fa97abe 100644 --- a/resources/pagedown_widget.scss +++ b/resources/pagedown_widget.scss @@ -14,7 +14,7 @@ width: 100%; background: #fff; border: 1px solid DarkGray; - font-family: Consolas, "Liberation Mono", Monaco, "Courier New", monospace !important; + font-family: "Noto Sans",Arial,"Lucida Grande",sans-serif !important; } .wmd-preview { @@ -174,51 +174,51 @@ /* Extra styles to allow for image upload */ .pagedown-image-upload { - display: none; - z-index: 10001; - position: fixed; - background: white; - top: 50%; - left: 50%; - padding: 10px; - width: 400px; - max-width: 90%; - transform: translate3d(-50%, -50%, 0); - box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5); + display: none; + z-index: 10001; + position: fixed; + background: white; + top: 50%; + left: 50%; + padding: 10px; + width: 400px; + max-width: 90%; + transform: translate3d(-50%, -50%, 0); + box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.5); } .pagedown-image-upload .submit-row { - margin: 10px 0 0 0; + margin: 10px 0 0 0; } .pagedown-image-upload.show { - display: block; + display: block; } .pagedown-image-upload .submit-loading { - display: none; - vertical-align: middle; - border: 4px solid #f3f3f3; /* Light grey */ - border-top: 4px solid #79aec8; /* Blue */ - border-radius: 50%; - width: 24px; - height: 24px; - animation: spin 1s linear infinite; + display: none; + vertical-align: middle; + border: 4px solid #f3f3f3; /* Light grey */ + border-top: 4px solid #79aec8; /* Blue */ + border-radius: 50%; + width: 24px; + height: 24px; + animation: spin 1s linear infinite; } .pagedown-image-upload .submit-loading.show { - display: inline-block; + display: inline-block; } .pagedown-image-upload .submit-input { - display: none; + display: none; } .pagedown-image-upload .submit-input.show { - display: inline-block; + display: inline-block; } @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } } \ No newline at end of file diff --git a/resources/widgets.scss b/resources/widgets.scss index 942b39e..42e8c8e 100644 --- a/resources/widgets.scss +++ b/resources/widgets.scss @@ -161,8 +161,7 @@ input { } textarea { - padding: 4px 8px; - color: #555; + padding: 8px; background: #FFF none; border: 1px solid $border_gray; border-radius: $widget_border_radius; @@ -172,8 +171,7 @@ textarea { } textarea:hover { - border-color: rgba(82, 168, 236, 0.8); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 4px rgba(82, 168, 236, 0.6); + border-color: black; } input { @@ -184,8 +182,8 @@ input { } textarea:focus { - border-color: rgba(82, 168, 236, 0.8); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + border-color: black; + border-width: unset; outline: 0; } From b4c162049786c348cbe908eade4b35fde71e7c12 Mon Sep 17 00:00:00 2001 From: Phuoc Anh Kha Le <76896393+anhkha2003@users.noreply.github.com> Date: Fri, 6 Oct 2023 03:54:37 -0500 Subject: [PATCH 11/51] GP Ranking (#90) --- dmoj/urls.py | 5 ++ judge/admin/__init__.py | 9 ++- judge/admin/contest.py | 16 +++++ judge/caching.py | 5 ++ judge/migrations/0170_contests_summary.py | 30 ++++++++++ judge/models/__init__.py | 1 + judge/models/contest.py | 22 +++++++ judge/views/contests.py | 65 +++++++++++++++++++++ templates/contest/contests_summary.html | 71 +++++++++++++++++++++++ 9 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 judge/migrations/0170_contests_summary.py create mode 100644 templates/contest/contests_summary.html diff --git a/dmoj/urls.py b/dmoj/urls.py index 4e0691e..e6610f8 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -517,6 +517,11 @@ urlpatterns = [ ), ), url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")), + url( + r"^contests/summary/(?P\w+)$", + contests.contests_summary_view, + name="contests_summary", + ), url(r"^course/", paged_list_view(course.CourseList, "course_list")), url( r"^contests/(?P\d+)/(?P\d+)/$", diff --git a/judge/admin/__init__.py b/judge/admin/__init__.py index 94af5aa..05032d6 100644 --- a/judge/admin/__init__.py +++ b/judge/admin/__init__.py @@ -3,7 +3,12 @@ from django.contrib.admin.models import LogEntry from django.contrib.auth.models import User from judge.admin.comments import CommentAdmin -from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin +from judge.admin.contest import ( + ContestAdmin, + ContestParticipationAdmin, + ContestTagAdmin, + ContestsSummaryAdmin, +) from judge.admin.interface import ( BlogPostAdmin, LicenseAdmin, @@ -41,6 +46,7 @@ from judge.models import ( Ticket, VolunteerProblemVote, Course, + ContestsSummary, ) @@ -69,3 +75,4 @@ admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin) admin.site.register(Course) admin.site.unregister(User) admin.site.register(User, UserAdmin) +admin.site.register(ContestsSummary, ContestsSummaryAdmin) diff --git a/judge/admin/contest.py b/judge/admin/contest.py index a126e83..b7b5457 100644 --- a/judge/admin/contest.py +++ b/judge/admin/contest.py @@ -502,3 +502,19 @@ class ContestParticipationAdmin(admin.ModelAdmin): show_virtual.short_description = _("virtual") show_virtual.admin_order_field = "virtual" + + +class ContestsSummaryForm(ModelForm): + class Meta: + widgets = { + "contests": AdminHeavySelect2MultipleWidget( + data_view="contest_select2", attrs={"style": "width: 100%"} + ), + } + + +class ContestsSummaryAdmin(admin.ModelAdmin): + fields = ("key", "contests", "scores") + list_display = ("key",) + search_fields = ("key", "contests__key") + form = ContestsSummaryForm diff --git a/judge/caching.py b/judge/caching.py index 42e9311..70b5613 100644 --- a/judge/caching.py +++ b/judge/caching.py @@ -1,6 +1,7 @@ from inspect import signature from django.core.cache import cache from django.db.models.query import QuerySet +from django.core.handlers.wsgi import WSGIRequest import hashlib @@ -18,10 +19,14 @@ def cache_wrapper(prefix, timeout=None): return str(arg)[:MAX_NUM_CHAR] return str(arg) + def filter_args(args_list): + return [x for x in args_list if not isinstance(x, WSGIRequest)] + def get_key(func, *args, **kwargs): args_list = list(args) signature_args = list(signature(func).parameters.keys()) args_list += [kwargs.get(k) for k in signature_args[len(args) :]] + args_list = filter_args(args_list) args_list = [arg_to_str(i) for i in args_list] key = prefix + ":" + ":".join(args_list) key = key.replace(" ", "_") diff --git a/judge/migrations/0170_contests_summary.py b/judge/migrations/0170_contests_summary.py new file mode 100644 index 0000000..a2b19de --- /dev/null +++ b/judge/migrations/0170_contests_summary.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.21 on 2023-10-02 03:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("judge", "0169_public_scoreboard"), + ] + + operations = [ + migrations.CreateModel( + name="ContestsSummary", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("scores", models.JSONField(blank=True, null=True)), + ("key", models.CharField(max_length=20, unique=True)), + ("contests", models.ManyToManyField(to="judge.Contest")), + ], + ), + ] diff --git a/judge/models/__init__.py b/judge/models/__init__.py index ee9d364..8226c96 100644 --- a/judge/models/__init__.py +++ b/judge/models/__init__.py @@ -16,6 +16,7 @@ from judge.models.contest import ( ContestTag, Rating, ContestProblemClarification, + ContestsSummary, ) from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex from judge.models.message import PrivateMessage, PrivateMessageThread diff --git a/judge/models/contest.py b/judge/models/contest.py index a724f1a..5052399 100644 --- a/judge/models/contest.py +++ b/judge/models/contest.py @@ -33,6 +33,7 @@ __all__ = [ "ContestSubmission", "Rating", "ContestProblemClarification", + "ContestsSummary", ] @@ -900,3 +901,24 @@ class ContestProblemClarification(models.Model): date = models.DateTimeField( verbose_name=_("clarification timestamp"), auto_now_add=True ) + + +class ContestsSummary(models.Model): + contests = models.ManyToManyField( + Contest, + ) + scores = models.JSONField( + null=True, + blank=True, + ) + key = models.CharField( + max_length=20, + unique=True, + ) + + class Meta: + verbose_name = _("contests summary") + verbose_name_plural = _("contests summaries") + + def __str__(self): + return self.key diff --git a/judge/views/contests.py b/judge/views/contests.py index 97ff251..b6f7242 100644 --- a/judge/views/contests.py +++ b/judge/views/contests.py @@ -27,6 +27,8 @@ from django.db.models import ( Value, When, ) +from django.db.models.signals import post_save, post_delete +from django.dispatch import receiver from django.db.models.expressions import CombinedExpression from django.http import ( Http404, @@ -67,6 +69,7 @@ from judge.models import ( Profile, Submission, ContestProblemClarification, + ContestsSummary, ) from judge.tasks import run_moss from judge.utils.celery import redirect_to_task_status @@ -1380,3 +1383,65 @@ def update_contest_mode(request): old_mode = request.session.get("contest_mode", True) request.session["contest_mode"] = not old_mode return HttpResponse() + + +ContestsSummaryData = namedtuple( + "ContestsSummaryData", + "user points point_contests css_class", +) + + +def contests_summary_view(request, key): + try: + contests_summary = ContestsSummary.objects.get(key=key) + except: + raise Http404() + + cache_key = "csv:" + key + context = cache.get(cache_key) + if context: + return render(request, "contest/contests_summary.html", context) + + scores_system = contests_summary.scores + contests = contests_summary.contests.all() + total_points = defaultdict(int) + result_per_contest = defaultdict(lambda: [(0, 0)] * len(contests)) + user_css_class = {} + + for i in range(len(contests)): + contest = contests[i] + users, problems = get_contest_ranking_list(request, contest) + for rank, user in users: + curr_score = 0 + if rank - 1 < len(scores_system): + curr_score = scores_system[rank - 1] + total_points[user.user] += curr_score + result_per_contest[user.user][i] = (curr_score, rank) + user_css_class[user.user] = user.css_class + + sorted_total_points = [ + ContestsSummaryData( + user=user, + points=total_points[user], + point_contests=result_per_contest[user], + css_class=user_css_class[user], + ) + for user in total_points + ] + + sorted_total_points.sort(key=lambda x: x.points, reverse=True) + total_rank = ranker(sorted_total_points) + + context = { + "total_rank": list(total_rank), + "title": _("Contests Summary"), + "contests": contests, + } + cache.set(cache_key, context) + + return render(request, "contest/contests_summary.html", context) + + +@receiver([post_save, post_delete], sender=ContestsSummary) +def clear_cache(sender, instance, **kwargs): + cache.delete("csv:" + instance.key) diff --git a/templates/contest/contests_summary.html b/templates/contest/contests_summary.html new file mode 100644 index 0000000..a7fbe12 --- /dev/null +++ b/templates/contest/contests_summary.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} + +{% block title_row %}{% endblock %} +{% block title_ruler %}{% endblock %} + +{% block media %} + +{% endblock %} + +{% block body %} + + + + + + {% for contest in contests %} + + {% endfor %} + + + + + {% for rank, item in total_rank %} + + + + {% for point_contest, rank_contest in item.point_contests %} + + {% endfor %} + + + {% endfor %} + +
    {{_('Rank')}}{{_('Name')}}{{ loop.index }}{{_('Points')}}
    + {{ rank }} + +
    + {{item.user.username}} +
    +
    {{item.user.first_name}}
    +
    +
    {{ point_contest }}
    + {% if rank_contest %} +
    (#{{ rank_contest }})
    + {% endif %} +
    + {{ item.points }} +
    +{% endblock %} From 5f97491f0db22d706a473e62926ec1bb263d5102 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Fri, 6 Oct 2023 13:04:12 -0500 Subject: [PATCH 12/51] Add contest summary url --- judge/models/contest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/judge/models/contest.py b/judge/models/contest.py index 5052399..db1b3e6 100644 --- a/judge/models/contest.py +++ b/judge/models/contest.py @@ -922,3 +922,6 @@ class ContestsSummary(models.Model): def __str__(self): return self.key + + def get_absolute_url(self): + return reverse("contests_summary", args=[self.key]) From 7f854c40ddb5798bca43ae3aef554c9195ed4f21 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 10 Oct 2023 17:38:48 -0500 Subject: [PATCH 13/51] Change notification backend --- chat_box/models.py | 8 +++ judge/admin/problem.py | 10 +-- judge/comments.py | 35 ++++------ judge/migrations/0171_update_notification.py | 68 ++++++++++++++++++++ judge/models/__init__.py | 3 +- judge/models/comment.py | 26 -------- judge/models/notification.py | 61 ++++++++++++++++++ judge/models/pagevote.py | 2 + judge/models/profile.py | 9 +-- judge/views/comment.py | 3 +- judge/views/notification.py | 32 +++------ judge/views/organization.py | 28 ++------ judge/views/pagevote.py | 6 ++ judge/views/ticket.py | 8 +-- templates/notification/list.html | 23 ++----- 15 files changed, 188 insertions(+), 134 deletions(-) create mode 100644 judge/migrations/0171_update_notification.py create mode 100644 judge/models/notification.py diff --git a/chat_box/models.py b/chat_box/models.py index fb3fd7b..fb6de76 100644 --- a/chat_box/models.py +++ b/chat_box/models.py @@ -18,6 +18,9 @@ class Room(models.Model): Profile, related_name="user_two", verbose_name="user 2", on_delete=CASCADE ) + class Meta: + app_label = "chat_box" + @cache_wrapper(prefix="Rc") def contain(self, profile): return self.user_one == profile or self.user_two == profile @@ -58,6 +61,7 @@ class Message(models.Model): indexes = [ models.Index(fields=["hidden", "room", "-id"]), ] + app_label = "chat_box" class UserRoom(models.Model): @@ -70,6 +74,7 @@ class UserRoom(models.Model): class Meta: unique_together = ("user", "room") + app_label = "chat_box" class Ignore(models.Model): @@ -82,6 +87,9 @@ class Ignore(models.Model): ) ignored_users = models.ManyToManyField(Profile) + class Meta: + app_label = "chat_box" + @classmethod def is_ignored(self, current_user, new_friend): try: diff --git a/judge/admin/problem.py b/judge/admin/problem.py index 49a145d..b5cd56f 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -25,6 +25,7 @@ from judge.models import ( Solution, Notification, ) +from judge.models.notification import make_notification from judge.widgets import ( AdminHeavySelect2MultipleWidget, AdminSelect2MultipleWidget, @@ -381,14 +382,7 @@ class ProblemAdmin(CompareVersionAdmin): category = "Problem public: " + str(obj.is_public) if orgs: category += " (" + ", ".join(orgs) + ")" - for user in users: - notification = Notification( - owner=user, - html_link=html, - category=category, - author=request.profile, - ) - notification.save() + make_notification(users, html, category, request.profile) def construct_change_message(self, request, form, *args, **kwargs): if form.cleaned_data.get("change_message"): diff --git a/judge/comments.py b/judge/comments.py index 65d8f7e..dde97a0 100644 --- a/judge/comments.py +++ b/judge/comments.py @@ -26,21 +26,20 @@ from judge.dblock import LockModel from judge.models import Comment, Notification from judge.widgets import HeavyPreviewPageDownWidget from judge.jinja2.reference import get_user_from_text +from judge.models.notification import make_notification DEFAULT_OFFSET = 10 +def _get_html_link_notification(comment): + return f'{comment.page_title}' + + def add_mention_notifications(comment): - user_referred = get_user_from_text(comment.body).exclude(id=comment.author.id) - for user in user_referred: - notification_ref = Notification(owner=user, comment=comment, category="Mention") - notification_ref.save() - - -def del_mention_notifications(comment): - query = {"comment": comment, "category": "Mention"} - Notification.objects.filter(**query).delete() + users_mentioned = get_user_from_text(comment.body).exclude(id=comment.author.id) + link = _get_html_link_notification(comment) + make_notification(users_mentioned, "Mention", link, comment.author) class CommentForm(ModelForm): @@ -124,23 +123,17 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View): comment.save() # add notification for reply + comment_notif_link = _get_html_link_notification(comment) if comment.parent and comment.parent.author != comment.author: - notification_reply = Notification( - owner=comment.parent.author, comment=comment, category="Reply" + make_notification( + [comment.parent.author], "Reply", comment_notif_link, comment.author ) - notification_reply.save() # add notification for page authors page_authors = comment.linked_object.authors.all() - for user in page_authors: - if user == comment.author: - continue - notification = Notification( - owner=user, comment=comment, category="Comment" - ) - notification.save() - # except Exception: - # pass + make_notification( + page_authors, "Comment", comment_notif_link, comment.author + ) add_mention_notifications(comment) diff --git a/judge/migrations/0171_update_notification.py b/judge/migrations/0171_update_notification.py new file mode 100644 index 0000000..7803e4b --- /dev/null +++ b/judge/migrations/0171_update_notification.py @@ -0,0 +1,68 @@ +# Generated by Django 3.2.18 on 2023-10-10 21:17 + +from django.db import migrations, models +import django.db.models.deletion +from django.urls import reverse + +from collections import defaultdict + + +# Run this in shell +def migrate_notif(apps, schema_editor): + Notification = apps.get_model("judge", "Notification") + Profile = apps.get_model("judge", "Profile") + NotificationProfile = apps.get_model("judge", "NotificationProfile") + + unread_count = defaultdict(int) + for c in Notification.objects.all(): + if c.comment: + c.html_link = ( + f'{c.comment.page_title}' + ) + c.author = c.comment.author + c.save() + if c.read is False: + unread_count[c.author] += 1 + + for user in unread_count: + np = NotificationProfile(user=user) + np.unread_count = unread_count[user] + np.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("judge", "0170_contests_summary"), + ] + + operations = [ + migrations.AlterModelOptions( + name="contestssummary", + options={ + "verbose_name": "contests summary", + "verbose_name_plural": "contests summaries", + }, + ), + migrations.CreateModel( + name="NotificationProfile", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("unread_count", models.IntegerField(default=0)), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to="judge.profile" + ), + ), + ], + ), + ] diff --git a/judge/models/__init__.py b/judge/models/__init__.py index 8226c96..2b1d706 100644 --- a/judge/models/__init__.py +++ b/judge/models/__init__.py @@ -6,7 +6,7 @@ from judge.models.choices import ( MATH_ENGINES_CHOICES, TIMEZONE, ) -from judge.models.comment import Comment, CommentLock, CommentVote, Notification +from judge.models.comment import Comment, CommentLock, CommentVote from judge.models.contest import ( Contest, ContestMoss, @@ -58,6 +58,7 @@ from judge.models.volunteer import VolunteerProblemVote from judge.models.pagevote import PageVote, PageVoteVoter from judge.models.bookmark import BookMark, MakeBookMark from judge.models.course import Course +from judge.models.notification import Notification, NotificationProfile revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"]) revisions.register(Problem, follow=["language_limits"]) diff --git a/judge/models/comment.py b/judge/models/comment.py index 6058cf8..2cbe20a 100644 --- a/judge/models/comment.py +++ b/judge/models/comment.py @@ -177,29 +177,3 @@ class CommentLock(models.Model): def __str__(self): return str(self.page) - - -class Notification(models.Model): - owner = models.ForeignKey( - Profile, - verbose_name=_("owner"), - related_name="notifications", - on_delete=CASCADE, - ) - time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True) - comment = models.ForeignKey( - Comment, null=True, verbose_name=_("comment"), on_delete=CASCADE - ) - read = models.BooleanField(verbose_name=_("read"), default=False) - category = models.CharField(verbose_name=_("category"), max_length=1000) - html_link = models.TextField( - default="", - verbose_name=_("html link to comments, used for non-comments"), - max_length=1000, - ) - author = models.ForeignKey( - Profile, - null=True, - verbose_name=_("who trigger, used for non-comment"), - on_delete=CASCADE, - ) diff --git a/judge/models/notification.py b/judge/models/notification.py new file mode 100644 index 0000000..01c5b0d --- /dev/null +++ b/judge/models/notification.py @@ -0,0 +1,61 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django.db.models import CASCADE, F +from django.core.exceptions import ObjectDoesNotExist + +from judge.models import Profile, Comment +from judge.caching import cache_wrapper + + +class Notification(models.Model): + owner = models.ForeignKey( + Profile, + verbose_name=_("owner"), + related_name="notifications", + on_delete=CASCADE, + ) + time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True) + category = models.CharField(verbose_name=_("category"), max_length=1000) + html_link = models.TextField( + default="", + verbose_name=_("html link to comments, used for non-comments"), + max_length=1000, + ) + author = models.ForeignKey( + Profile, + null=True, + verbose_name=_("who trigger, used for non-comment"), + on_delete=CASCADE, + ) + comment = models.ForeignKey( + Comment, null=True, verbose_name=_("comment"), on_delete=CASCADE + ) # deprecated + read = models.BooleanField(verbose_name=_("read"), default=False) # deprecated + + +class NotificationProfile(models.Model): + unread_count = models.IntegerField(default=0) + user = models.OneToOneField(Profile, on_delete=CASCADE) + + +def make_notification(to_users, category, html_link, author): + for user in to_users: + if user == author: + continue + notif = Notification( + owner=user, category=category, html_link=html_link, author=author + ) + notif.save() + NotificationProfile.objects.get_or_create(user=user) + NotificationProfile.objects.filter(user=user).update( + unread_count=F("unread_count") + 1 + ) + unseen_notifications_count.dirty(user) + + +@cache_wrapper(prefix="unc") +def unseen_notifications_count(profile): + try: + return NotificationProfile.objects.get(user=profile).unread_count + except ObjectDoesNotExist: + return 0 diff --git a/judge/models/pagevote.py b/judge/models/pagevote.py index 5e74c95..7accd01 100644 --- a/judge/models/pagevote.py +++ b/judge/models/pagevote.py @@ -5,6 +5,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from judge.models.profile import Profile +from judge.caching import cache_wrapper __all__ = ["PageVote", "PageVoteVoter"] @@ -28,6 +29,7 @@ class PageVote(models.Model): ] unique_together = ("content_type", "object_id") + @cache_wrapper(prefix="PVvs") def vote_score(self, user): page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user) if page_vote.exists(): diff --git a/judge/models/profile.py b/judge/models/profile.py index 96f3cfd..83bfc32 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -16,6 +16,7 @@ from sortedm2m.fields import SortedManyToManyField from judge.models.choices import ACE_THEMES, MATH_ENGINES_CHOICES, TIMEZONE from judge.models.runtime import Language from judge.ratings import rating_class +from judge.caching import cache_wrapper __all__ = ["Organization", "Profile", "OrganizationRequest", "Friend"] @@ -142,6 +143,7 @@ class Organization(models.Model): ) verbose_name = _("organization") verbose_name_plural = _("organizations") + app_label = "judge" class Profile(models.Model): @@ -266,10 +268,9 @@ class Profile(models.Model): @cached_property def count_unseen_notifications(self): - query = { - "read": False, - } - return self.notifications.filter(**query).count() + from judge.models.notification import unseen_notifications_count + + return unseen_notifications_count(self) @cached_property def count_unread_chat_boxes(self): diff --git a/judge/views/comment.py b/judge/views/comment.py index 6965201..caa4eb6 100644 --- a/judge/views/comment.py +++ b/judge/views/comment.py @@ -27,7 +27,7 @@ from judge.dblock import LockModel from judge.models import Comment, CommentVote, Notification, BlogPost from judge.utils.views import TitleMixin from judge.widgets import MathJaxPagedownWidget, HeavyPreviewPageDownWidget -from judge.comments import add_mention_notifications, del_mention_notifications +from judge.comments import add_mention_notifications import json @@ -240,7 +240,6 @@ class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView): def form_valid(self, form): # update notifications comment = form.instance - del_mention_notifications(comment) add_mention_notifications(comment) with transaction.atomic(), revisions.create_revision(): diff --git a/judge/views/notification.py b/judge/views/notification.py index 63f38c2..bb79317 100644 --- a/judge/views/notification.py +++ b/judge/views/notification.py @@ -2,10 +2,9 @@ from django.contrib.auth.decorators import login_required from django.views.generic import ListView from django.utils.translation import ugettext as _ from django.utils.timezone import now -from django.db.models import BooleanField, Value -from judge.utils.cachedict import CacheDict -from judge.models import Profile, Comment, Notification +from judge.models import Profile, Notification, NotificationProfile +from judge.models.notification import unseen_notifications_count __all__ = ["NotificationList"] @@ -16,24 +15,11 @@ class NotificationList(ListView): template_name = "notification/list.html" def get_queryset(self): - self.unseen_cnt = self.request.profile.count_unseen_notifications + self.unseen_cnt = unseen_notifications_count(self.request.profile) - query = { - "owner": self.request.profile, - } - - self.queryset = ( - Notification.objects.filter(**query) - .order_by("-time")[:100] - .annotate(seen=Value(True, output_field=BooleanField())) - ) - - # Mark the several first unseen - for cnt, q in enumerate(self.queryset): - if cnt < self.unseen_cnt: - q.seen = False - else: - break + self.queryset = Notification.objects.filter( + owner=self.request.profile + ).order_by("-id")[:100] return self.queryset @@ -46,8 +32,6 @@ class NotificationList(ListView): def get(self, request, *args, **kwargs): ret = super().get(request, *args, **kwargs) - - # update after rendering - Notification.objects.filter(owner=self.request.profile).update(read=True) - + NotificationProfile.objects.filter(user=request.profile).update(unread_count=0) + unseen_notifications_count.dirty(self.request.profile) return ret diff --git a/judge/views/organization.py b/judge/views/organization.py index 0576afe..be80203 100644 --- a/judge/views/organization.py +++ b/judge/views/organization.py @@ -56,10 +56,10 @@ from judge.models import ( Problem, Profile, Contest, - Notification, ContestProblem, OrganizationProfile, ) +from judge.models.notification import make_notification from judge import event_poster as event from judge.utils.ranker import ranker from judge.utils.views import ( @@ -1019,16 +1019,9 @@ class AddOrganizationBlog( html = ( f'{self.object.title} - {self.organization.name}' ) - for user in self.organization.admins.all(): - if user.id == self.request.profile.id: - continue - notification = Notification( - owner=user, - author=self.request.profile, - category="Add blog", - html_link=html, - ) - notification.save() + make_notification( + self.organization.admins.all(), "Add blog", html, self.request.profile + ) return res @@ -1104,17 +1097,8 @@ class EditOrganizationBlog( ) html = f'{blog.title} - {self.organization.name}' post_authors = blog.authors.all() - posible_user = self.organization.admins.all() | post_authors - for user in posible_user: - if user.id == self.request.profile.id: - continue - notification = Notification( - owner=user, - author=self.request.profile, - category=action, - html_link=html, - ) - notification.save() + posible_users = self.organization.admins.all() | post_authors + make_notification(posible_users, action, html, self.request.profile) def form_valid(self, form): with transaction.atomic(), revisions.create_revision(): diff --git a/judge/views/pagevote.py b/judge/views/pagevote.py index 9ae6d5b..988d355 100644 --- a/judge/views/pagevote.py +++ b/judge/views/pagevote.py @@ -80,6 +80,7 @@ def vote_page(request, delta): else: PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta) break + _dirty_vote_score(pagevote_id, request.profile) return HttpResponse("success", content_type="text/plain") @@ -103,3 +104,8 @@ class PageVoteDetailView(TemplateResponseMixin, SingleObjectMixin, View): context = super(PageVoteDetailView, self).get_context_data(**kwargs) context["pagevote"] = self.object.get_or_create_pagevote() return context + + +def _dirty_vote_score(pagevote_id, profile): + pv = PageVote(id=pagevote_id) + pv.vote_score.dirty(pv, profile) diff --git a/judge/views/ticket.py b/judge/views/ticket.py index 3d62235..f4bf97a 100644 --- a/judge/views/ticket.py +++ b/judge/views/ticket.py @@ -49,16 +49,10 @@ ticket_widget = ( def add_ticket_notifications(users, author, link, ticket): html = f'{ticket.linked_item}' - users = set(users) if author in users: users.remove(author) - - for user in users: - notification = Notification( - owner=user, html_link=html, category="Ticket", author=author - ) - notification.save() + make_notification(users, "Ticket", html, author) class TicketForm(forms.Form): diff --git a/templates/notification/list.html b/templates/notification/list.html index cd50e32..6aa7351 100644 --- a/templates/notification/list.html +++ b/templates/notification/list.html @@ -1,11 +1,8 @@ {% extends "base.html" %} {% block body %} - {% if not has_notifications %} -

    {{ _('You have no notifications') }}

    - {% else %} @@ -17,24 +14,15 @@ {% for notification in notifications %}
    - {% if notification.comment %} - {{ link_user(notification.comment.author) }} - {% else %} - {{ link_user(notification.author) }} - {% endif %} - + {{ link_user(notification.author) }} {{ notification.category }} - {% if notification.comment %} - {{ notification.comment.page_title }} - {% else %} - {% autoescape off %} - {{notification.html_link}} - {% endautoescape %} - {% endif %} + {% autoescape off %} + {{notification.html_link}} + {% endautoescape %} {{ relative_time(notification.time) }} @@ -43,8 +31,5 @@ {% endfor %}
    {% endif %} - {% endblock %} - \ No newline at end of file From ed287b6ff360ff687b296497ea7a0d1840cf8dd0 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 10 Oct 2023 19:37:36 -0500 Subject: [PATCH 14/51] Add caching for user basic info --- chat_box/utils.py | 3 ++ chat_box/views.py | 6 ++- judge/admin/profile.py | 2 +- judge/caching.py | 2 +- judge/jinja2/gravatar.py | 8 ++-- judge/jinja2/reference.py | 6 +-- judge/migrations/0172_index_rating.py | 18 ++++++++ judge/models/profile.py | 57 ++++++++++++++++++++++++- judge/views/resolver.py | 7 ++- resources/comments.scss | 2 + templates/contest/contests_summary.html | 4 +- templates/contest/ranking-table.html | 8 ++-- templates/user/link.html | 2 +- templates/user/user-about.html | 8 ++-- templates/user/users-table.html | 10 ++--- 15 files changed, 110 insertions(+), 33 deletions(-) create mode 100644 judge/migrations/0172_index_rating.py diff --git a/chat_box/utils.py b/chat_box/utils.py index dd59d98..f3f27b4 100644 --- a/chat_box/utils.py +++ b/chat_box/utils.py @@ -8,6 +8,8 @@ from django.db.models.functions import Coalesce from chat_box.models import Ignore, Message, UserRoom, Room +from judge.caching import cache_wrapper + secret_key = settings.CHAT_SECRET_KEY fernet = Fernet(secret_key) @@ -37,6 +39,7 @@ def encrypt_channel(channel): ) +@cache_wrapper(prefix="gub") def get_unread_boxes(profile): ignored_rooms = Ignore.get_ignored_rooms(profile) unread_boxes = ( diff --git a/chat_box/views.py b/chat_box/views.py index 5a15f74..48fa37a 100644 --- a/chat_box/views.py +++ b/chat_box/views.py @@ -36,7 +36,7 @@ from judge.jinja2.gravatar import gravatar from judge.models import Friend from chat_box.models import Message, Profile, Room, UserRoom, Ignore -from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel +from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel, get_unread_boxes import json @@ -208,6 +208,7 @@ def post_message(request): ) else: Room.last_message_body.dirty(room) + for user in room.users(): event.post( encrypt_channel("chat_" + str(user.id)), @@ -223,6 +224,7 @@ def post_message(request): UserRoom.objects.filter(user=user, room=room).update( unread_count=F("unread_count") + 1 ) + get_unread_boxes.dirty(user) return JsonResponse(ret) @@ -285,6 +287,8 @@ def update_last_seen(request, **kwargs): user_room.unread_count = 0 user_room.save() + get_unread_boxes.dirty(profile) + return JsonResponse({"msg": "updated"}) diff --git a/judge/admin/profile.py b/judge/admin/profile.py index 422fc5d..68ecee1 100644 --- a/judge/admin/profile.py +++ b/judge/admin/profile.py @@ -126,7 +126,7 @@ class ProfileAdmin(VersionAdmin): admin_user_admin.short_description = _("User") def email(self, obj): - return obj.user.email + return obj.email email.admin_order_field = "user__email" email.short_description = _("Email") diff --git a/judge/caching.py b/judge/caching.py index 70b5613..b8fb810 100644 --- a/judge/caching.py +++ b/judge/caching.py @@ -40,9 +40,9 @@ def cache_wrapper(prefix, timeout=None): if result == NONE_RESULT: result = None return result + result = func(*args, **kwargs) if result is None: result = NONE_RESULT - result = func(*args, **kwargs) cache.set(cache_key, result, timeout) return result diff --git a/judge/jinja2/gravatar.py b/judge/jinja2/gravatar.py index cffa413..373f685 100644 --- a/judge/jinja2/gravatar.py +++ b/judge/jinja2/gravatar.py @@ -12,12 +12,12 @@ from . import registry def gravatar(profile, size=80, default=None, profile_image=None, email=None): if profile_image: return profile_image - if profile and profile.profile_image: - return profile.profile_image.url + if profile and profile.cached_profile_image: + return profile.cached_profile_image.url if profile: - email = email or profile.user.email + email = email or profile.email if default is None: - default = profile.mute + default = profile.is_muted gravatar_url = ( "//www.gravatar.com/avatar/" + hashlib.md5(utf8bytes(email.strip().lower())).hexdigest() diff --git a/judge/jinja2/reference.py b/judge/jinja2/reference.py index 49a6ef2..312ac4f 100644 --- a/judge/jinja2/reference.py +++ b/judge/jinja2/reference.py @@ -157,14 +157,14 @@ def item_title(item): @registry.render_with("user/link.html") def link_user(user): if isinstance(user, Profile): - user, profile = user.user, user + profile = user elif isinstance(user, AbstractUser): profile = user.profile elif type(user).__name__ == "ContestRankingProfile": - user, profile = user.user, user + profile = user else: raise ValueError("Expected profile or user, got %s" % (type(user),)) - return {"user": user, "profile": profile} + return {"profile": profile} @registry.function diff --git a/judge/migrations/0172_index_rating.py b/judge/migrations/0172_index_rating.py new file mode 100644 index 0000000..157c005 --- /dev/null +++ b/judge/migrations/0172_index_rating.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2023-10-10 23:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("judge", "0171_update_notification"), + ] + + operations = [ + migrations.AlterField( + model_name="profile", + name="rating", + field=models.IntegerField(db_index=True, default=None, null=True), + ), + ] diff --git a/judge/models/profile.py b/judge/models/profile.py index 83bfc32..bcb6621 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -10,6 +10,9 @@ from django.urls import reverse from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ +from django.dispatch import receiver +from django.db.models.signals import post_save, pre_save + from fernet_fields import EncryptedCharField from sortedm2m.fields import SortedManyToManyField @@ -202,7 +205,7 @@ class Profile(models.Model): help_text=_("User will not be able to vote on problems' point values."), default=False, ) - rating = models.IntegerField(null=True, default=None) + rating = models.IntegerField(null=True, default=None, db_index=True) user_script = models.TextField( verbose_name=_("user script"), default="", @@ -256,6 +259,21 @@ class Profile(models.Model): max_length=300, ) + @cache_wrapper(prefix="Pgbi") + def _get_basic_info(self): + return { + "first_name": self.user.first_name, + "last_name": self.user.last_name, + "email": self.user.email, + "username": self.user.username, + "mute": self.mute, + "profile_image": self.profile_image, + } + + @cached_property + def _cached_info(self): + return self._get_basic_info() + @cached_property def organization(self): # We do this to take advantage of prefetch_related @@ -264,7 +282,27 @@ class Profile(models.Model): @cached_property def username(self): - return self.user.username + return self._cached_info["username"] + + @cached_property + def first_name(self): + return self._cached_info["first_name"] + + @cached_property + def last_name(self): + return self._cached_info["last_name"] + + @cached_property + def email(self): + return self._cached_info["email"] + + @cached_property + def is_muted(self): + return self._cached_info["mute"] + + @cached_property + def cached_profile_image(self): + return self._cached_info["profile_image"] @cached_property def count_unseen_notifications(self): @@ -499,3 +537,18 @@ class OrganizationProfile(models.Model): @classmethod def get_most_recent_organizations(self, users): return self.objects.filter(users=users).order_by("-last_visit")[:5] + + +@receiver([post_save], sender=User) +def on_user_save(sender, instance, **kwargs): + profile = instance.profile + profile._get_user.dirty(profile) + + +@receiver([pre_save], sender=Profile) +def on_profile_save(sender, instance, **kwargs): + if instance.id is None: + return + prev = sender.objects.get(id=instance.id) + if prev.mute != instance.mute or prev.profile_image != instance.profile_image: + instance._get_user.dirty(instance) diff --git a/judge/views/resolver.py b/judge/views/resolver.py index eade951..cf8a193 100644 --- a/judge/views/resolver.py +++ b/judge/views/resolver.py @@ -31,10 +31,9 @@ class Resolver(TemplateView): for participation in self.contest.users.filter(virtual=0): cnt_user += 1 users[str(cnt_user)] = { - "username": participation.user.user.username, - "name": participation.user.user.first_name - or participation.user.user.username, - "school": participation.user.user.last_name, + "username": participation.user.username, + "name": participation.user.first_name or participation.user.username, + "school": participation.user.last_name, "last_submission": participation.cumtime_final, "problems": {}, } diff --git a/resources/comments.scss b/resources/comments.scss index 42dc90b..05ef84f 100644 --- a/resources/comments.scss +++ b/resources/comments.scss @@ -40,6 +40,8 @@ a { .comment-img { display: flex; margin-right: 4px; + height: 1.5em; + width: 1.5em; } .new-comments .comment-display { diff --git a/templates/contest/contests_summary.html b/templates/contest/contests_summary.html index a7fbe12..a43fa65 100644 --- a/templates/contest/contests_summary.html +++ b/templates/contest/contests_summary.html @@ -49,9 +49,9 @@
    - {{item.user.username}} + {{item.username}}
    -
    {{item.user.first_name}}
    +
    {{item.first_name}}
    {% for point_contest, rank_contest in item.point_contests %} diff --git a/templates/contest/ranking-table.html b/templates/contest/ranking-table.html index e182717..18d64dd 100644 --- a/templates/contest/ranking-table.html +++ b/templates/contest/ranking-table.html @@ -13,14 +13,14 @@ {% endblock %} {% block user_footer %} - {% if user.user.first_name %} + {% if user.first_name %} {% endif %} - {% if user.user.last_name %} + {% if user.last_name %} {% endif %} {% endblock %} diff --git a/templates/user/link.html b/templates/user/link.html index f0a73bb..ea821fa 100644 --- a/templates/user/link.html +++ b/templates/user/link.html @@ -1 +1 @@ -{{ user.username }} +{{ profile.username }} diff --git a/templates/user/user-about.html b/templates/user/user-about.html index ee7f395..23c53b1 100644 --- a/templates/user/user-about.html +++ b/templates/user/user-about.html @@ -54,9 +54,9 @@
    - {% if user.user.first_name %} + {% if user.first_name %}

    - {{user.user.first_name}}{% if user.user.last_name %} ({{user.user.last_name}}){% endif %} + {{user.first_name}}{% if user.last_name %} ({{user.last_name}}){% endif %}

    {% endif %} {% if perms.judge.change_profile %} @@ -70,9 +70,7 @@ {% endif%} {% if user.about %}

    {{ _('About') }}

    - {% cache 86400 'user_about' user.id MATH_ENGINE %} - {{ user.about|markdown(lazy_load=True)|reference|str|safe }} - {% endcache %} + {{ user.about|markdown(lazy_load=True)|reference|str|safe }} {% else %} {% if user.user == request.user %} diff --git a/templates/user/users-table.html b/templates/user/users-table.html index 0eb95e0..606e374 100644 --- a/templates/user/users-table.html +++ b/templates/user/users-table.html @@ -10,7 +10,7 @@ {% block user_footer %} {% if user.rating %}{{ rating_number(user) }}{% endif %} -
    {{ user.user.first_name if user.user.first_name else ''}}
    +
    {{ user.first_name or ''}}
    {% endblock %} {% block after_point_head %} @@ -39,11 +39,11 @@ {{ user.problem_count }}
    - {% if user.about %} - {% cache 86400 'user_about' user.id MATH_ENGINE %} + {% cache 86400 'user_about' user.id MATH_ENGINE %} + {% if user.about %} {{ user.about|markdown(lazy_load=True)|reference|str|safe }} - {% endcache %} - {% endif %} + {% endif %} + {% endcache %}
    {% endblock %} From 56c2b6d9b9b972252cd8d5a853291a144aabebe4 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 10 Oct 2023 19:46:48 -0500 Subject: [PATCH 15/51] Fix contest summary --- templates/contest/contests_summary.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/contest/contests_summary.html b/templates/contest/contests_summary.html index a43fa65..a7fbe12 100644 --- a/templates/contest/contests_summary.html +++ b/templates/contest/contests_summary.html @@ -49,9 +49,9 @@
    - {{item.username}} + {{item.user.username}}
    -
    {{item.first_name}}
    +
    {{item.user.first_name}}
    {% for point_contest, rank_contest in item.point_contests %} From 9940d9cc4c509668fedca42c0a4767fee7bb1e19 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 10 Oct 2023 19:54:55 -0500 Subject: [PATCH 16/51] Add a temp fix --- judge/caching.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/judge/caching.py b/judge/caching.py index b8fb810..b8fec83 100644 --- a/judge/caching.py +++ b/judge/caching.py @@ -43,7 +43,10 @@ def cache_wrapper(prefix, timeout=None): result = func(*args, **kwargs) if result is None: result = NONE_RESULT - cache.set(cache_key, result, timeout) + try: + cache.set(cache_key, result, timeout) + except: + pass return result def dirty(*args, **kwargs): From 801738fea9bf9e86981a47b443eb48833f17ee78 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 10 Oct 2023 20:09:05 -0500 Subject: [PATCH 17/51] Add another fix --- templates/problem/related_problems.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/problem/related_problems.html b/templates/problem/related_problems.html index 95ed05f..a92a75b 100644 --- a/templates/problem/related_problems.html +++ b/templates/problem/related_problems.html @@ -1,4 +1,4 @@ -{% if related_problems %} +{% if request.user.is_authenticated and related_problems %}

    {{_('Recommended problems')}}:

      From 5741866c07fd960212f6017543c7b7ea1a49f550 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 10 Oct 2023 20:23:52 -0500 Subject: [PATCH 18/51] Remove try except --- judge/caching.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/judge/caching.py b/judge/caching.py index b8fec83..b8fb810 100644 --- a/judge/caching.py +++ b/judge/caching.py @@ -43,10 +43,7 @@ def cache_wrapper(prefix, timeout=None): result = func(*args, **kwargs) if result is None: result = NONE_RESULT - try: - cache.set(cache_key, result, timeout) - except: - pass + cache.set(cache_key, result, timeout) return result def dirty(*args, **kwargs): From 94395ae71aa6c91e07df4960401615e4f8db766a Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 10 Oct 2023 20:28:16 -0500 Subject: [PATCH 19/51] Fix registration --- judge/models/profile.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/judge/models/profile.py b/judge/models/profile.py index bcb6621..8647206 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -541,6 +541,8 @@ class OrganizationProfile(models.Model): @receiver([post_save], sender=User) def on_user_save(sender, instance, **kwargs): + if instance.id is None: + return profile = instance.profile profile._get_user.dirty(profile) From a377f45e0bc2853430287c71089f0bcddad7442f Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 10 Oct 2023 20:31:25 -0500 Subject: [PATCH 20/51] Another fix --- judge/models/profile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/judge/models/profile.py b/judge/models/profile.py index 8647206..05c5770 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -544,7 +544,7 @@ def on_user_save(sender, instance, **kwargs): if instance.id is None: return profile = instance.profile - profile._get_user.dirty(profile) + profile._get_basic_info.dirty(profile) @receiver([pre_save], sender=Profile) @@ -553,4 +553,4 @@ def on_profile_save(sender, instance, **kwargs): return prev = sender.objects.get(id=instance.id) if prev.mute != instance.mute or prev.profile_image != instance.profile_image: - instance._get_user.dirty(instance) + instance._get_basic_info.dirty(instance) From 8da03aebb0e2ff339800cbb28027c48c08f0ec20 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 10 Oct 2023 20:49:23 -0500 Subject: [PATCH 21/51] Fix new notif --- judge/admin/problem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/judge/admin/problem.py b/judge/admin/problem.py index b5cd56f..1a47d37 100644 --- a/judge/admin/problem.py +++ b/judge/admin/problem.py @@ -382,7 +382,7 @@ class ProblemAdmin(CompareVersionAdmin): category = "Problem public: " + str(obj.is_public) if orgs: category += " (" + ", ".join(orgs) + ")" - make_notification(users, html, category, request.profile) + make_notification(users, category, html, request.profile) def construct_change_message(self, request, form, *args, **kwargs): if form.cleaned_data.get("change_message"): From 67a3c7274e5dbef218683905731ad4469932304a Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 10 Oct 2023 21:01:06 -0500 Subject: [PATCH 22/51] Try fixing memcache error --- judge/jinja2/gravatar.py | 4 ++-- judge/models/profile.py | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/judge/jinja2/gravatar.py b/judge/jinja2/gravatar.py index 373f685..b6e8a83 100644 --- a/judge/jinja2/gravatar.py +++ b/judge/jinja2/gravatar.py @@ -12,8 +12,8 @@ from . import registry def gravatar(profile, size=80, default=None, profile_image=None, email=None): if profile_image: return profile_image - if profile and profile.cached_profile_image: - return profile.cached_profile_image.url + if profile and profile.profile_image_url: + return profile.profile_image_url if profile: email = email or profile.email if default is None: diff --git a/judge/models/profile.py b/judge/models/profile.py index 05c5770..9f7b931 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -259,15 +259,18 @@ class Profile(models.Model): max_length=300, ) - @cache_wrapper(prefix="Pgbi") + @cache_wrapper(prefix="Pgbi2") def _get_basic_info(self): + profile_image_url = None + if self.profile_image: + profile_image_url = self.profile_image.url return { "first_name": self.user.first_name, "last_name": self.user.last_name, "email": self.user.email, "username": self.user.username, "mute": self.mute, - "profile_image": self.profile_image, + "profile_image_url": profile_image_url, } @cached_property @@ -301,8 +304,8 @@ class Profile(models.Model): return self._cached_info["mute"] @cached_property - def cached_profile_image(self): - return self._cached_info["profile_image"] + def profile_image_url(self): + return self._cached_info["profile_image_url"] @cached_property def count_unseen_notifications(self): From d4e0c5ca86406904d72c4bf3555e1fb00e2fa332 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 11 Oct 2023 00:08:26 -0500 Subject: [PATCH 23/51] Fix register --- judge/models/profile.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/judge/models/profile.py b/judge/models/profile.py index 9f7b931..00dbe3f 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -12,6 +12,8 @@ from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from django.dispatch import receiver from django.db.models.signals import post_save, pre_save +from django.core.exceptions import RelatedObjectDoesNotExist + from fernet_fields import EncryptedCharField from sortedm2m.fields import SortedManyToManyField @@ -544,10 +546,11 @@ class OrganizationProfile(models.Model): @receiver([post_save], sender=User) def on_user_save(sender, instance, **kwargs): - if instance.id is None: - return - profile = instance.profile - profile._get_basic_info.dirty(profile) + try: + profile = instance.profile + profile._get_basic_info.dirty(profile) + except RelatedObjectDoesNotExist: + pass @receiver([pre_save], sender=Profile) From 130c96a2fe17f1f50f0bf722a742d6c374dabffa Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 11 Oct 2023 00:21:21 -0500 Subject: [PATCH 24/51] Fix register --- judge/models/profile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/judge/models/profile.py b/judge/models/profile.py index 00dbe3f..3692e39 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -12,7 +12,6 @@ from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from django.dispatch import receiver from django.db.models.signals import post_save, pre_save -from django.core.exceptions import RelatedObjectDoesNotExist from fernet_fields import EncryptedCharField @@ -549,7 +548,7 @@ def on_user_save(sender, instance, **kwargs): try: profile = instance.profile profile._get_basic_info.dirty(profile) - except RelatedObjectDoesNotExist: + except: pass From c3cecb3f583a02cfe27828648ad369ea06b2c934 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Wed, 11 Oct 2023 20:33:48 -0500 Subject: [PATCH 25/51] Add more caching --- judge/caching.py | 55 ++++++++++++++++++++--------- judge/jinja2/__init__.py | 1 - judge/jinja2/submission.py | 41 --------------------- judge/models/submission.py | 41 +++++++++++++++++++++ judge/utils/problems.py | 46 +++++++++++++++++------- judge/views/submission.py | 52 ++------------------------- resources/blog.scss | 1 - templates/feed/has_next.html | 2 +- templates/submission/row.html | 2 +- templates/submission/user-ajax.html | 6 ++-- 10 files changed, 121 insertions(+), 126 deletions(-) delete mode 100644 judge/jinja2/submission.py diff --git a/judge/caching.py b/judge/caching.py index b8fb810..8f42d24 100644 --- a/judge/caching.py +++ b/judge/caching.py @@ -1,27 +1,32 @@ from inspect import signature -from django.core.cache import cache +from django.core.cache import cache, caches from django.db.models.query import QuerySet from django.core.handlers.wsgi import WSGIRequest import hashlib -MAX_NUM_CHAR = 15 +MAX_NUM_CHAR = 50 NONE_RESULT = "__None__" +def arg_to_str(arg): + if hasattr(arg, "id"): + return str(arg.id) + if isinstance(arg, list) or isinstance(arg, QuerySet): + return hashlib.sha1(str(list(arg)).encode()).hexdigest()[:MAX_NUM_CHAR] + if len(str(arg)) > MAX_NUM_CHAR: + return str(arg)[:MAX_NUM_CHAR] + return str(arg) + + +def filter_args(args_list): + return [x for x in args_list if not isinstance(x, WSGIRequest)] + + +l0_cache = caches["l0"] if "l0" in caches else None + + def cache_wrapper(prefix, timeout=None): - def arg_to_str(arg): - if hasattr(arg, "id"): - return str(arg.id) - if isinstance(arg, list) or isinstance(arg, QuerySet): - return hashlib.sha1(str(list(arg)).encode()).hexdigest()[:MAX_NUM_CHAR] - if len(str(arg)) > MAX_NUM_CHAR: - return str(arg)[:MAX_NUM_CHAR] - return str(arg) - - def filter_args(args_list): - return [x for x in args_list if not isinstance(x, WSGIRequest)] - def get_key(func, *args, **kwargs): args_list = list(args) signature_args = list(signature(func).parameters.keys()) @@ -32,23 +37,41 @@ def cache_wrapper(prefix, timeout=None): key = key.replace(" ", "_") return key + def _get(key): + if not l0_cache: + return cache.get(key) + print("GET", key, l0_cache.get(key)) + return l0_cache.get(key) or cache.get(key) + + def _set_l0(key, value): + if l0_cache: + print("SET", key, value) + l0_cache.set(key, value, 30) + + def _set(key, value, timeout): + _set_l0(key, value) + cache.set(key, value, timeout) + def decorator(func): def wrapper(*args, **kwargs): cache_key = get_key(func, *args, **kwargs) - result = cache.get(cache_key) + result = _get(cache_key) if result is not None: + _set_l0(cache_key, result) if result == NONE_RESULT: result = None return result result = func(*args, **kwargs) if result is None: result = NONE_RESULT - cache.set(cache_key, result, timeout) + _set(cache_key, result, timeout) return result def dirty(*args, **kwargs): cache_key = get_key(func, *args, **kwargs) cache.delete(cache_key) + if l0_cache: + l0_cache.delete(cache_key) wrapper.dirty = dirty diff --git a/judge/jinja2/__init__.py b/judge/jinja2/__init__.py index 93d0ed5..93ab0ad 100644 --- a/judge/jinja2/__init__.py +++ b/judge/jinja2/__init__.py @@ -21,7 +21,6 @@ from . import ( render, social, spaceless, - submission, timedelta, ) from . import registry diff --git a/judge/jinja2/submission.py b/judge/jinja2/submission.py deleted file mode 100644 index cdb3634..0000000 --- a/judge/jinja2/submission.py +++ /dev/null @@ -1,41 +0,0 @@ -from . import registry - - -@registry.function -def submission_layout( - submission, - profile_id, - user, - editable_problem_ids, - completed_problem_ids, - tester_problem_ids, -): - problem_id = submission.problem_id - - if problem_id in editable_problem_ids: - return True - - if problem_id in tester_problem_ids: - return True - - if profile_id == submission.user_id: - return True - - if user.has_perm("judge.change_submission"): - return True - - if user.has_perm("judge.view_all_submission"): - return True - - if submission.problem.is_public and user.has_perm("judge.view_public_submission"): - return True - - if hasattr(submission, "contest"): - contest = submission.contest.participation.contest - if contest.is_editable_by(user): - return True - - if submission.problem_id in completed_problem_ids and submission.problem.is_public: - return True - - return False diff --git a/judge/models/submission.py b/judge/models/submission.py index fea9b64..842b627 100644 --- a/judge/models/submission.py +++ b/judge/models/submission.py @@ -220,6 +220,47 @@ class Submission(models.Model): def id_secret(self): return self.get_id_secret(self.id) + def is_accessible_by(self, profile): + from judge.utils.problems import ( + user_completed_ids, + user_tester_ids, + user_editable_ids, + ) + + if not profile: + return False + + problem_id = self.problem_id + user = profile.user + + if profile.id == self.user_id: + return True + + if problem_id in user_editable_ids(profile): + return True + + if self.problem_id in user_completed_ids(profile): + if self.problem.is_public: + return True + if problem_id in user_tester_ids(profile): + return True + + if user.has_perm("judge.change_submission"): + return True + + if user.has_perm("judge.view_all_submission"): + return True + + if self.problem.is_public and user.has_perm("judge.view_public_submission"): + return True + + if hasattr( + self, "contest" + ) and self.contest.participation.contest.is_editable_by(user): + return True + + return False + class Meta: permissions = ( ("abort_any_submission", "Abort any submission"), diff --git a/judge/utils/problems.py b/judge/utils/problems.py index 6c351aa..5861fa7 100644 --- a/judge/utils/problems.py +++ b/judge/utils/problems.py @@ -10,6 +10,8 @@ from django.db.models import Case, Count, ExpressionWrapper, F, Max, Q, When from django.db.models.fields import FloatField from django.utils import timezone from django.utils.translation import gettext as _, gettext_noop +from django.db.models.signals import pre_save +from django.dispatch import receiver from judge.models import Problem, Submission from judge.ml.collab_filter import CollabFilter @@ -24,6 +26,7 @@ __all__ = [ ] +@cache_wrapper(prefix="user_tester") def user_tester_ids(profile): return set( Problem.testers.through.objects.filter(profile=profile).values_list( @@ -32,6 +35,7 @@ def user_tester_ids(profile): ) +@cache_wrapper(prefix="user_editable") def user_editable_ids(profile): result = set( ( @@ -42,22 +46,19 @@ def user_editable_ids(profile): return result +@cache_wrapper(prefix="contest_complete") def contest_completed_ids(participation): - key = "contest_complete:%d" % participation.id - result = cache.get(key) - if result is None: - result = set( - participation.submissions.filter( - submission__result="AC", points=F("problem__points") - ) - .values_list("problem__problem__id", flat=True) - .distinct() + result = set( + participation.submissions.filter( + submission__result="AC", points=F("problem__points") ) - cache.set(key, result, 86400) + .values_list("problem__problem__id", flat=True) + .distinct() + ) return result -@cache_wrapper(prefix="user_complete", timeout=86400) +@cache_wrapper(prefix="user_complete") def user_completed_ids(profile): result = set( Submission.objects.filter( @@ -69,7 +70,7 @@ def user_completed_ids(profile): return result -@cache_wrapper(prefix="contest_attempted", timeout=86400) +@cache_wrapper(prefix="contest_attempted") def contest_attempted_ids(participation): result = { id: {"achieved_points": points, "max_points": max_points} @@ -84,7 +85,7 @@ def contest_attempted_ids(participation): return result -@cache_wrapper(prefix="user_attempted", timeout=86400) +@cache_wrapper(prefix="user_attempted") def user_attempted_ids(profile): result = { id: { @@ -248,3 +249,22 @@ def finished_submission(sub): keys += ["contest_complete:%d" % participation.id] keys += ["contest_attempted:%d" % participation.id] cache.delete_many(keys) + + +@receiver([pre_save], sender=Problem) +def on_problem_save(sender, instance, **kwargs): + if instance.id is None: + return + prev_editors = list(sender.objects.get(id=instance.id).editor_ids) + new_editors = list(instance.editor_ids) + if prev_editors != new_editors: + all_editors = set(prev_editors + new_editors) + for profile_id in all_editors: + user_editable_ids.dirty(profile_id) + + prev_testers = list(sender.objects.get(id=instance.id).tester_ids) + new_testers = list(instance.tester_ids) + if prev_testers != new_testers: + all_testers = set(prev_testers + new_testers) + for profile_id in all_testers: + user_tester_ids.dirty(profile_id) diff --git a/judge/views/submission.py b/judge/views/submission.py index 1556dd9..eae961b 100644 --- a/judge/views/submission.py +++ b/judge/views/submission.py @@ -84,31 +84,7 @@ class SubmissionMixin(object): class SubmissionDetailBase(LoginRequiredMixin, TitleMixin, SubmissionMixin, DetailView): def get_object(self, queryset=None): submission = super(SubmissionDetailBase, self).get_object(queryset) - profile = self.request.profile - problem = submission.problem - if self.request.user.has_perm("judge.view_all_submission"): - return submission - if problem.is_public and self.request.user.has_perm( - "judge.view_public_submission" - ): - return submission - if submission.user_id == profile.id: - return submission - if problem.is_editor(profile): - return submission - if problem.is_public or problem.testers.filter(id=profile.id).exists(): - if Submission.objects.filter( - user_id=profile.id, - result="AC", - problem_id=problem.id, - points=problem.points, - ).exists(): - return submission - if hasattr( - submission, "contest" - ) and submission.contest.participation.contest.is_editable_by( - self.request.user - ): + if submission.is_accessible_by(self.request.profile): return submission raise PermissionDenied() @@ -483,19 +459,9 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView): authenticated = self.request.user.is_authenticated context["dynamic_update"] = False context["show_problem"] = self.show_problem - context["completed_problem_ids"] = ( - user_completed_ids(self.request.profile) if authenticated else [] - ) - context["editable_problem_ids"] = ( - user_editable_ids(self.request.profile) if authenticated else [] - ) - context["tester_problem_ids"] = ( - user_tester_ids(self.request.profile) if authenticated else [] - ) - + context["profile"] = self.request.profile context["all_languages"] = Language.objects.all().values_list("key", "name") context["selected_languages"] = self.selected_languages - context["all_statuses"] = self.get_searchable_status_codes() context["selected_statuses"] = self.selected_statuses @@ -779,19 +745,10 @@ def single_submission(request, submission_id, show_problem=True): "submission/row.html", { "submission": submission, - "completed_problem_ids": user_completed_ids(request.profile) - if authenticated - else [], - "editable_problem_ids": user_editable_ids(request.profile) - if authenticated - else [], - "tester_problem_ids": user_tester_ids(request.profile) - if authenticated - else [], "show_problem": show_problem, "problem_name": show_problem and submission.problem.translated_name(request.LANGUAGE_CODE), - "profile_id": request.profile.id if authenticated else 0, + "profile": request.profile if authenticated else None, }, ) @@ -1010,9 +967,6 @@ class UserContestSubmissionsAjax(UserContestSubmissions): context["contest"] = self.contest context["problem"] = self.problem context["profile"] = self.profile - context["profile_id"] = ( - self.request.profile.id if self.request.profile else None - ) contest_problem = self.contest.contest_problems.get(problem=self.problem) filtered_submissions = [] diff --git a/resources/blog.scss b/resources/blog.scss index ad21448..4c57cfe 100644 --- a/resources/blog.scss +++ b/resources/blog.scss @@ -229,7 +229,6 @@ .show-more { display: flex; color: black; - font-style: italic; font-size: 16px; font-weight: 700; padding: 0px 12px; diff --git a/templates/feed/has_next.html b/templates/feed/has_next.html index 9cc45c9..f26caea 100644 --- a/templates/feed/has_next.html +++ b/templates/feed/has_next.html @@ -1,4 +1,4 @@ {% if has_next_page %} - + {% endif %} \ No newline at end of file diff --git a/templates/submission/row.html b/templates/submission/row.html index 1c4d8d7..73523f6 100644 --- a/templates/submission/row.html +++ b/templates/submission/row.html @@ -1,4 +1,4 @@ -{% set can_view = submission_layout(submission, profile_id, request.user, editable_problem_ids, completed_problem_ids, tester_problem_ids) %} +{% set can_view = submission.is_accessible_by(profile) %}
      {%- if submission.is_graded -%} diff --git a/templates/submission/user-ajax.html b/templates/submission/user-ajax.html index b03248a..d060ede 100644 --- a/templates/submission/user-ajax.html +++ b/templates/submission/user-ajax.html @@ -1,5 +1,5 @@

      - {{_('Contest submissions of')}} {{link_user(profile)}} # + {{_('Contest submissions of')}} {{link_user(profile)}} #


      {% if best_subtasks and subtasks %} @@ -19,7 +19,7 @@ {% endif %} {% if cur_subtask.submission %} - {% set can_view = submission_layout(cur_subtask.submission, profile_id, request.user, editable_problem_ids, completed_problem_ids, tester_problem_ids) %} + {% set can_view = cur_subtask.submission.is_accessible_by(profile) %} {% if can_view %} → {{ cur_subtask.submission.id }} @@ -43,7 +43,7 @@ {% for submission in submissions %} - {% set can_view = submission_layout(submission, profile_id, request.user, editable_problem_ids, completed_problem_ids, tester_problem_ids) %} + {% set can_view = submission.is_accessible_by(profile) %}