From 7f854c40ddb5798bca43ae3aef554c9195ed4f21 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Tue, 10 Oct 2023 17:38:48 -0500 Subject: [PATCH] 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