Change notification backend

This commit is contained in:
cuom1999 2023-10-10 17:38:48 -05:00
parent 5f97491f0d
commit 7f854c40dd
15 changed files with 188 additions and 134 deletions

View file

@ -18,6 +18,9 @@ class Room(models.Model):
Profile, related_name="user_two", verbose_name="user 2", on_delete=CASCADE Profile, related_name="user_two", verbose_name="user 2", on_delete=CASCADE
) )
class Meta:
app_label = "chat_box"
@cache_wrapper(prefix="Rc") @cache_wrapper(prefix="Rc")
def contain(self, profile): def contain(self, profile):
return self.user_one == profile or self.user_two == profile return self.user_one == profile or self.user_two == profile
@ -58,6 +61,7 @@ class Message(models.Model):
indexes = [ indexes = [
models.Index(fields=["hidden", "room", "-id"]), models.Index(fields=["hidden", "room", "-id"]),
] ]
app_label = "chat_box"
class UserRoom(models.Model): class UserRoom(models.Model):
@ -70,6 +74,7 @@ class UserRoom(models.Model):
class Meta: class Meta:
unique_together = ("user", "room") unique_together = ("user", "room")
app_label = "chat_box"
class Ignore(models.Model): class Ignore(models.Model):
@ -82,6 +87,9 @@ class Ignore(models.Model):
) )
ignored_users = models.ManyToManyField(Profile) ignored_users = models.ManyToManyField(Profile)
class Meta:
app_label = "chat_box"
@classmethod @classmethod
def is_ignored(self, current_user, new_friend): def is_ignored(self, current_user, new_friend):
try: try:

View file

@ -25,6 +25,7 @@ from judge.models import (
Solution, Solution,
Notification, Notification,
) )
from judge.models.notification import make_notification
from judge.widgets import ( from judge.widgets import (
AdminHeavySelect2MultipleWidget, AdminHeavySelect2MultipleWidget,
AdminSelect2MultipleWidget, AdminSelect2MultipleWidget,
@ -381,14 +382,7 @@ class ProblemAdmin(CompareVersionAdmin):
category = "Problem public: " + str(obj.is_public) category = "Problem public: " + str(obj.is_public)
if orgs: if orgs:
category += " (" + ", ".join(orgs) + ")" category += " (" + ", ".join(orgs) + ")"
for user in users: make_notification(users, html, category, request.profile)
notification = Notification(
owner=user,
html_link=html,
category=category,
author=request.profile,
)
notification.save()
def construct_change_message(self, request, form, *args, **kwargs): def construct_change_message(self, request, form, *args, **kwargs):
if form.cleaned_data.get("change_message"): if form.cleaned_data.get("change_message"):

View file

@ -26,21 +26,20 @@ from judge.dblock import LockModel
from judge.models import Comment, Notification from judge.models import Comment, Notification
from judge.widgets import HeavyPreviewPageDownWidget from judge.widgets import HeavyPreviewPageDownWidget
from judge.jinja2.reference import get_user_from_text from judge.jinja2.reference import get_user_from_text
from judge.models.notification import make_notification
DEFAULT_OFFSET = 10 DEFAULT_OFFSET = 10
def _get_html_link_notification(comment):
return f'<a href="{comment.get_absolute_url()}">{comment.page_title}</a>'
def add_mention_notifications(comment): def add_mention_notifications(comment):
user_referred = get_user_from_text(comment.body).exclude(id=comment.author.id) users_mentioned = get_user_from_text(comment.body).exclude(id=comment.author.id)
for user in user_referred: link = _get_html_link_notification(comment)
notification_ref = Notification(owner=user, comment=comment, category="Mention") make_notification(users_mentioned, "Mention", link, comment.author)
notification_ref.save()
def del_mention_notifications(comment):
query = {"comment": comment, "category": "Mention"}
Notification.objects.filter(**query).delete()
class CommentForm(ModelForm): class CommentForm(ModelForm):
@ -124,23 +123,17 @@ class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
comment.save() comment.save()
# add notification for reply # add notification for reply
comment_notif_link = _get_html_link_notification(comment)
if comment.parent and comment.parent.author != comment.author: if comment.parent and comment.parent.author != comment.author:
notification_reply = Notification( make_notification(
owner=comment.parent.author, comment=comment, category="Reply" [comment.parent.author], "Reply", comment_notif_link, comment.author
) )
notification_reply.save()
# add notification for page authors # add notification for page authors
page_authors = comment.linked_object.authors.all() page_authors = comment.linked_object.authors.all()
for user in page_authors: make_notification(
if user == comment.author: page_authors, "Comment", comment_notif_link, comment.author
continue )
notification = Notification(
owner=user, comment=comment, category="Comment"
)
notification.save()
# except Exception:
# pass
add_mention_notifications(comment) add_mention_notifications(comment)

View file

@ -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'<a href="{c.comment.get_absolute_url()}">{c.comment.page_title}</a>'
)
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"
),
),
],
),
]

View file

@ -6,7 +6,7 @@ from judge.models.choices import (
MATH_ENGINES_CHOICES, MATH_ENGINES_CHOICES,
TIMEZONE, TIMEZONE,
) )
from judge.models.comment import Comment, CommentLock, CommentVote, Notification from judge.models.comment import Comment, CommentLock, CommentVote
from judge.models.contest import ( from judge.models.contest import (
Contest, Contest,
ContestMoss, ContestMoss,
@ -58,6 +58,7 @@ from judge.models.volunteer import VolunteerProblemVote
from judge.models.pagevote import PageVote, PageVoteVoter from judge.models.pagevote import PageVote, PageVoteVoter
from judge.models.bookmark import BookMark, MakeBookMark from judge.models.bookmark import BookMark, MakeBookMark
from judge.models.course import Course from judge.models.course import Course
from judge.models.notification import Notification, NotificationProfile
revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"]) revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"])
revisions.register(Problem, follow=["language_limits"]) revisions.register(Problem, follow=["language_limits"])

View file

@ -177,29 +177,3 @@ class CommentLock(models.Model):
def __str__(self): def __str__(self):
return str(self.page) 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,
)

View file

@ -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

View file

@ -5,6 +5,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from judge.models.profile import Profile from judge.models.profile import Profile
from judge.caching import cache_wrapper
__all__ = ["PageVote", "PageVoteVoter"] __all__ = ["PageVote", "PageVoteVoter"]
@ -28,6 +29,7 @@ class PageVote(models.Model):
] ]
unique_together = ("content_type", "object_id") unique_together = ("content_type", "object_id")
@cache_wrapper(prefix="PVvs")
def vote_score(self, user): def vote_score(self, user):
page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user) page_vote = PageVoteVoter.objects.filter(pagevote=self, voter=user)
if page_vote.exists(): if page_vote.exists():

View file

@ -16,6 +16,7 @@ from sortedm2m.fields import SortedManyToManyField
from judge.models.choices import ACE_THEMES, MATH_ENGINES_CHOICES, TIMEZONE from judge.models.choices import ACE_THEMES, MATH_ENGINES_CHOICES, TIMEZONE
from judge.models.runtime import Language from judge.models.runtime import Language
from judge.ratings import rating_class from judge.ratings import rating_class
from judge.caching import cache_wrapper
__all__ = ["Organization", "Profile", "OrganizationRequest", "Friend"] __all__ = ["Organization", "Profile", "OrganizationRequest", "Friend"]
@ -142,6 +143,7 @@ class Organization(models.Model):
) )
verbose_name = _("organization") verbose_name = _("organization")
verbose_name_plural = _("organizations") verbose_name_plural = _("organizations")
app_label = "judge"
class Profile(models.Model): class Profile(models.Model):
@ -266,10 +268,9 @@ class Profile(models.Model):
@cached_property @cached_property
def count_unseen_notifications(self): def count_unseen_notifications(self):
query = { from judge.models.notification import unseen_notifications_count
"read": False,
} return unseen_notifications_count(self)
return self.notifications.filter(**query).count()
@cached_property @cached_property
def count_unread_chat_boxes(self): def count_unread_chat_boxes(self):

View file

@ -27,7 +27,7 @@ from judge.dblock import LockModel
from judge.models import Comment, CommentVote, Notification, BlogPost from judge.models import Comment, CommentVote, Notification, BlogPost
from judge.utils.views import TitleMixin from judge.utils.views import TitleMixin
from judge.widgets import MathJaxPagedownWidget, HeavyPreviewPageDownWidget 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 import json
@ -240,7 +240,6 @@ class CommentEditAjax(LoginRequiredMixin, CommentMixin, UpdateView):
def form_valid(self, form): def form_valid(self, form):
# update notifications # update notifications
comment = form.instance comment = form.instance
del_mention_notifications(comment)
add_mention_notifications(comment) add_mention_notifications(comment)
with transaction.atomic(), revisions.create_revision(): with transaction.atomic(), revisions.create_revision():

View file

@ -2,10 +2,9 @@ from django.contrib.auth.decorators import login_required
from django.views.generic import ListView from django.views.generic import ListView
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.timezone import now from django.utils.timezone import now
from django.db.models import BooleanField, Value
from judge.utils.cachedict import CacheDict from judge.models import Profile, Notification, NotificationProfile
from judge.models import Profile, Comment, Notification from judge.models.notification import unseen_notifications_count
__all__ = ["NotificationList"] __all__ = ["NotificationList"]
@ -16,24 +15,11 @@ class NotificationList(ListView):
template_name = "notification/list.html" template_name = "notification/list.html"
def get_queryset(self): def get_queryset(self):
self.unseen_cnt = self.request.profile.count_unseen_notifications self.unseen_cnt = unseen_notifications_count(self.request.profile)
query = { self.queryset = Notification.objects.filter(
"owner": self.request.profile, owner=self.request.profile
} ).order_by("-id")[:100]
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
return self.queryset return self.queryset
@ -46,8 +32,6 @@ class NotificationList(ListView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
ret = super().get(request, *args, **kwargs) ret = super().get(request, *args, **kwargs)
NotificationProfile.objects.filter(user=request.profile).update(unread_count=0)
# update after rendering unseen_notifications_count.dirty(self.request.profile)
Notification.objects.filter(owner=self.request.profile).update(read=True)
return ret return ret

View file

@ -56,10 +56,10 @@ from judge.models import (
Problem, Problem,
Profile, Profile,
Contest, Contest,
Notification,
ContestProblem, ContestProblem,
OrganizationProfile, OrganizationProfile,
) )
from judge.models.notification import make_notification
from judge import event_poster as event from judge import event_poster as event
from judge.utils.ranker import ranker from judge.utils.ranker import ranker
from judge.utils.views import ( from judge.utils.views import (
@ -1019,16 +1019,9 @@ class AddOrganizationBlog(
html = ( html = (
f'<a href="{link}">{self.object.title} - {self.organization.name}</a>' f'<a href="{link}">{self.object.title} - {self.organization.name}</a>'
) )
for user in self.organization.admins.all(): make_notification(
if user.id == self.request.profile.id: self.organization.admins.all(), "Add blog", html, self.request.profile
continue )
notification = Notification(
owner=user,
author=self.request.profile,
category="Add blog",
html_link=html,
)
notification.save()
return res return res
@ -1104,17 +1097,8 @@ class EditOrganizationBlog(
) )
html = f'<a href="{link}">{blog.title} - {self.organization.name}</a>' html = f'<a href="{link}">{blog.title} - {self.organization.name}</a>'
post_authors = blog.authors.all() post_authors = blog.authors.all()
posible_user = self.organization.admins.all() | post_authors posible_users = self.organization.admins.all() | post_authors
for user in posible_user: make_notification(posible_users, action, html, self.request.profile)
if user.id == self.request.profile.id:
continue
notification = Notification(
owner=user,
author=self.request.profile,
category=action,
html_link=html,
)
notification.save()
def form_valid(self, form): def form_valid(self, form):
with transaction.atomic(), revisions.create_revision(): with transaction.atomic(), revisions.create_revision():

View file

@ -80,6 +80,7 @@ def vote_page(request, delta):
else: else:
PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta) PageVote.objects.filter(id=pagevote_id).update(score=F("score") + delta)
break break
_dirty_vote_score(pagevote_id, request.profile)
return HttpResponse("success", content_type="text/plain") 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 = super(PageVoteDetailView, self).get_context_data(**kwargs)
context["pagevote"] = self.object.get_or_create_pagevote() context["pagevote"] = self.object.get_or_create_pagevote()
return context return context
def _dirty_vote_score(pagevote_id, profile):
pv = PageVote(id=pagevote_id)
pv.vote_score.dirty(pv, profile)

View file

@ -49,16 +49,10 @@ ticket_widget = (
def add_ticket_notifications(users, author, link, ticket): def add_ticket_notifications(users, author, link, ticket):
html = f'<a href="{link}">{ticket.linked_item}</a>' html = f'<a href="{link}">{ticket.linked_item}</a>'
users = set(users) users = set(users)
if author in users: if author in users:
users.remove(author) users.remove(author)
make_notification(users, "Ticket", html, author)
for user in users:
notification = Notification(
owner=user, html_link=html, category="Ticket", author=author
)
notification.save()
class TicketForm(forms.Form): class TicketForm(forms.Form):

View file

@ -1,11 +1,8 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block body %} {% block body %}
{% if not has_notifications %} {% if not has_notifications %}
<h2 style="text-align: center;">{{ _('You have no notifications') }}</h2> <h2 style="text-align: center;">{{ _('You have no notifications') }}</h2>
{% else %} {% else %}
<table class="table"> <table class="table">
<tr> <tr>
@ -17,24 +14,15 @@
{% for notification in notifications %} {% for notification in notifications %}
<tr class="{{ 'highlight' if not notification.seen }}"> <tr class="{{ 'highlight' if not notification.seen }}">
<td> <td>
{% if notification.comment %} {{ link_user(notification.author) }}
{{ link_user(notification.comment.author) }}
{% else %}
{{ link_user(notification.author) }}
{% endif %}
</td> </td>
<td> <td>
{{ notification.category }} {{ notification.category }}
</td> </td>
<td> <td>
{% if notification.comment %} {% autoescape off %}
<a href="{{ notification.comment.link }}#comment-{{ notification.comment.id }}">{{ notification.comment.page_title }}</a> {{notification.html_link}}
{% else %} {% endautoescape %}
{% autoescape off %}
{{notification.html_link}}
{% endautoescape %}
{% endif %}
</td> </td>
<td> <td>
{{ relative_time(notification.time) }} {{ relative_time(notification.time) }}
@ -43,8 +31,5 @@
{% endfor %} {% endfor %}
</table> </table>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
<!--
-->