From e99798ba5ce950fb4610fa11fd098207b83d0f0f Mon Sep 17 00:00:00 2001 From: moanos Date: Fri, 11 Jul 2025 11:39:55 +0200 Subject: [PATCH] feat: Completely rework notification framework This change is needed as a) there are many different types of notifications. A notification about a new comment does not only relate the comment but also adoption notice and a user that made the comment. Therefore, I introduce a new general notification that has optional links to other objects. b) this change allows to define a different representation of e-mails vs. display on the website. The new notification type is in charge of representation from now on. Title and text will be deprecated and eventually removed --- src/fellchensammlung/admin.py | 6 +- src/fellchensammlung/api/views.py | 16 ++--- src/fellchensammlung/mail.py | 37 ++++++++--- ...2_remove_basenotification_user_and_more.py | 54 ++++++++++++++++ ...r_notification_adoption_notice_and_more.py | 40 ++++++++++++ src/fellchensammlung/models.py | 63 ++++++++++++------- src/fellchensammlung/receivers.py | 23 ++++--- .../mail/notifications/an-deactivated.html | 16 +++++ .../notifications/an-for-search-found.html | 16 +++++ .../mail/notifications/an-to-be-checked.html | 16 +++++ .../mail/notifications/new-comment.html | 19 ++++++ .../mail/notifications/new-user.html | 19 ++++++ .../mail/{ => notifications}/report.html | 4 +- src/fellchensammlung/tools/admin.py | 11 ++-- src/fellchensammlung/tools/notifications.py | 13 ++-- src/fellchensammlung/tools/search.py | 12 ++-- src/fellchensammlung/views.py | 37 ++++++----- src/tests/test_models.py | 8 +-- 18 files changed, 317 insertions(+), 93 deletions(-) create mode 100644 src/fellchensammlung/migrations/0052_remove_basenotification_user_and_more.py create mode 100644 src/fellchensammlung/migrations/0053_alter_notification_adoption_notice_and_more.py create mode 100644 src/fellchensammlung/templates/fellchensammlung/mail/notifications/an-deactivated.html create mode 100644 src/fellchensammlung/templates/fellchensammlung/mail/notifications/an-for-search-found.html create mode 100644 src/fellchensammlung/templates/fellchensammlung/mail/notifications/an-to-be-checked.html create mode 100644 src/fellchensammlung/templates/fellchensammlung/mail/notifications/new-comment.html create mode 100644 src/fellchensammlung/templates/fellchensammlung/mail/notifications/new-user.html rename src/fellchensammlung/templates/fellchensammlung/mail/{ => notifications}/report.html (86%) diff --git a/src/fellchensammlung/admin.py b/src/fellchensammlung/admin.py index 0553052..827968d 100644 --- a/src/fellchensammlung/admin.py +++ b/src/fellchensammlung/admin.py @@ -11,7 +11,7 @@ from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, L SpeciesSpecificURL, ImportantLocation, SpeciesSpecialization from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \ - Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, BaseNotification + Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, Notification from django.utils.translation import gettext_lazy as _ @@ -127,9 +127,9 @@ class CommentAdmin(admin.ModelAdmin): list_filter = ("user",) -@admin.register(BaseNotification) +@admin.register(Notification) class BaseNotificationAdmin(admin.ModelAdmin): - list_filter = ("user", "read") + list_filter = ("user_to_notify", "read") @admin.register(SearchSubscription) diff --git a/src/fellchensammlung/api/views.py b/src/fellchensammlung/api/views.py index 3e0bfb2..29e9bd7 100644 --- a/src/fellchensammlung/api/views.py +++ b/src/fellchensammlung/api/views.py @@ -66,22 +66,22 @@ class AdoptionNoticeApiView(APIView): if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - adoption_notice = serializer.save(owner=request.user) + adoption_notice = serializer.save(owner=request.user_to_notify) # Add the location post_adoption_notice_save.delay_on_commit(adoption_notice.pk) # Only set active when user has trust level moderator or higher - if request.user.trust_level >= TrustLevel.MODERATOR: + if request.user_to_notify.trust_level >= TrustLevel.MODERATOR: adoption_notice.set_active() else: adoption_notice.set_unchecked() # Log the action Log.objects.create( - user=request.user, + user=request.user_to_notify, action="add_adoption_notice", - text=f"{request.user} added adoption notice {adoption_notice.pk} via API", + text=f"{request.user_to_notify} added adoption notice {adoption_notice.pk} via API", ) # Return success response with new adoption notice details @@ -130,7 +130,7 @@ class AnimalApiView(APIView): """ serializer = AnimalCreateSerializer(data=request.data, context={"request": request}) if serializer.is_valid(): - animal = serializer.save(owner=request.user) + animal = serializer.save(owner=request.user_to_notify) return Response( {"message": "Animal created successfully!", "id": animal.id}, status=status.HTTP_201_CREATED, @@ -289,7 +289,7 @@ class AddImageApiView(APIView): raise ValueError("Unknown attach_to_type given, should not happen. Check serializer") serializer.validated_data.pop('attach_to_type', None) serializer.validated_data.pop('attach_to', None) - image = serializer.save(owner=request.user) + image = serializer.save(owner=request.user_to_notify) object_to_attach_to.photos.add(image) return Response( {"message": "Image added successfully!", "id": image.id}, @@ -360,9 +360,9 @@ class LocationApiView(APIView): # Log the action Log.objects.create( - user=request.user, + user=request.user_to_notify, action="add_location", - text=f"{request.user} added adoption notice {location.pk} via API", + text=f"{request.user_to_notify} added adoption notice {location.pk} via API", ) # Return success response with new adoption notice details diff --git a/src/fellchensammlung/mail.py b/src/fellchensammlung/mail.py index 126c4bb..e376fac 100644 --- a/src/fellchensammlung/mail.py +++ b/src/fellchensammlung/mail.py @@ -6,7 +6,7 @@ from django.utils.html import strip_tags from django.utils.translation import gettext_lazy as _ from django.conf import settings from django.core import mail -from fellchensammlung.models import User, CommentNotification, BaseNotification, TrustLevel +from fellchensammlung.models import User, Notification, TrustLevel, NotificationTypeChoices from notfellchen.settings import host NEWLINE = "\r\n" @@ -19,10 +19,10 @@ def mail_admins_new_report(report): for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR): report_url = "https://" + host + report.get_absolute_url() context = {"report_url": report_url, - "user_comment": report.user_comment,} + "user_comment": report.user_comment, } subject = _("Neue Meldung") - html_message = render_to_string('fellchensammlung/mail/report.html', context) + html_message = render_to_string('fellchensammlung/mail/notifications/report.html', context) plain_message = strip_tags(html_message) mail.send_mail(subject, @@ -33,11 +33,28 @@ def mail_admins_new_report(report): def send_notification_email(notification_pk): - try: - notification = CommentNotification.objects.get(pk=notification_pk) - except CommentNotification.DoesNotExist: - notification = BaseNotification.objects.get(pk=notification_pk) + notification = Notification.objects.get(pk=notification_pk) + subject = f"🔔 {notification.title}" - body_text = notification.text - message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [notification.user.email]) - message.send() + context = {"notification": notification, } + if notification.notification_type == NotificationTypeChoices.NEW_REPORT_COMMENT or notification.notification_type == NotificationTypeChoices.NEW_REPORT_AN: + context["user_comment"] = notification.report.user_comment + context["report_url"] = notification.report.get_absolute_url() + html_message = render_to_string('fellchensammlung/mail/notifications/report.html', context) + elif notification.notification_type == NotificationTypeChoices.NEW_USER: + html_message = render_to_string('fellchensammlung/mail/notifications/new-user.html', context) + elif notification.notification_type == NotificationTypeChoices.AN_IS_TO_BE_CHECKED: + html_message = render_to_string('fellchensammlung/mail/notifications/an-to-be-checked.html', context) + elif notification.notification_type == NotificationTypeChoices.AN_WAS_DEACTIVATED: + html_message = render_to_string('fellchensammlung/mail/notifications/an-deactivated.html', context) + elif notification.notification_type == NotificationTypeChoices.AN_FOR_SEARCH_FOUND: + html_message = render_to_string('fellchensammlung/mail/notifications/report.html', context) + elif notification.notification_type == NotificationTypeChoices.NEW_COMMENT: + html_message = render_to_string('fellchensammlung/mail/notifications/new-comment.html', context) + else: + raise NotImplementedError("Unknown notification type") + + plain_message = strip_tags(html_message) + mail.send_mail(subject, plain_message, settings.DEFAULT_FROM_EMAIL, + [notification.user_to_notify.email], + html_message=html_message) diff --git a/src/fellchensammlung/migrations/0052_remove_basenotification_user_and_more.py b/src/fellchensammlung/migrations/0052_remove_basenotification_user_and_more.py new file mode 100644 index 0000000..13b580c --- /dev/null +++ b/src/fellchensammlung/migrations/0052_remove_basenotification_user_and_more.py @@ -0,0 +1,54 @@ +# Generated by Django 5.2.1 on 2025-07-11 09:01 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fellchensammlung', '0051_rescueorganization_parent_org_speciesspecialization'), + ] + + operations = [ + migrations.RemoveField( + model_name='basenotification', + name='user', + ), + migrations.RemoveField( + model_name='commentnotification', + name='basenotification_ptr', + ), + migrations.RemoveField( + model_name='commentnotification', + name='comment', + ), + migrations.CreateModel( + name='Notification', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('read_at', models.DateTimeField(blank=True, null=True, verbose_name='Gelesen am')), + ('notification_type', models.CharField(choices=[('new_user', 'Useraccount wurde erstellt'), ('new_report_an', 'Vermittlung wurde gemeldet'), ('new_report_comment', 'Kommentar wurde gemeldet'), ('an_is_to_be_checked', 'Vermittlung muss überprüft werden'), ('an_was_deactivated', 'Vermittlung wurde deaktiviert'), ('an_for_search_found', 'Vermittlung für Suche gefunden')], max_length=200, verbose_name='Benachrichtigungsgrund')), + ('title', models.CharField(max_length=100, verbose_name='Titel')), + ('text', models.TextField(verbose_name='Inhalt')), + ('read', models.BooleanField(default=False)), + ('adoption_notice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')), + ('comment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.comment', verbose_name='Antwort')), + ('report', models.ForeignKey(help_text='Report auf den sich die Benachrichtigung bezieht.', on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.report', verbose_name='Report')), + ('user_related', models.ForeignKey(help_text='Useraccount auf den sich die Benachrichtigung bezieht.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Verwandter Useraccount')), + ('user_to_notify', models.ForeignKey(help_text='Useraccount der Benachrichtigt wird', on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in')), + ], + ), + migrations.DeleteModel( + name='AdoptionNoticeNotification', + ), + migrations.DeleteModel( + name='BaseNotification', + ), + migrations.DeleteModel( + name='CommentNotification', + ), + ] diff --git a/src/fellchensammlung/migrations/0053_alter_notification_adoption_notice_and_more.py b/src/fellchensammlung/migrations/0053_alter_notification_adoption_notice_and_more.py new file mode 100644 index 0000000..6534fcc --- /dev/null +++ b/src/fellchensammlung/migrations/0053_alter_notification_adoption_notice_and_more.py @@ -0,0 +1,40 @@ +# Generated by Django 5.2.1 on 2025-07-11 09:10 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fellchensammlung', '0052_remove_basenotification_user_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='notification', + name='adoption_notice', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung'), + ), + migrations.AlterField( + model_name='notification', + name='notification_type', + field=models.CharField(choices=[('new_user', 'Useraccount wurde erstellt'), ('new_report_an', 'Vermittlung wurde gemeldet'), ('new_report_comment', 'Kommentar wurde gemeldet'), ('an_is_to_be_checked', 'Vermittlung muss überprüft werden'), ('an_was_deactivated', 'Vermittlung wurde deaktiviert'), ('an_for_search_found', 'Vermittlung für Suche gefunden'), ('new_comment', 'Neuer Kommentar')], max_length=200, verbose_name='Benachrichtigungsgrund'), + ), + migrations.AlterField( + model_name='notification', + name='report', + field=models.ForeignKey(blank=True, help_text='Report auf den sich die Benachrichtigung bezieht.', null=True, on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.report', verbose_name='Report'), + ), + migrations.AlterField( + model_name='notification', + name='user_related', + field=models.ForeignKey(blank=True, help_text='Useraccount auf den sich die Benachrichtigung bezieht.', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Verwandter Useraccount'), + ), + migrations.AlterField( + model_name='notification', + name='user_to_notify', + field=models.ForeignKey(blank=True, help_text='Useraccount der Benachrichtigt wird', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in'), + ), + ] diff --git a/src/fellchensammlung/models.py b/src/fellchensammlung/models.py index 07e8c18..b2d60b0 100644 --- a/src/fellchensammlung/models.py +++ b/src/fellchensammlung/models.py @@ -256,10 +256,10 @@ class User(AbstractUser): return self.get_absolute_url() def get_unread_notifications(self): - return BaseNotification.objects.filter(user=self, read=False) + return Notification.objects.filter(user=self, read=False) def get_num_unread_notifications(self): - return BaseNotification.objects.filter(user=self, read=False).count() + return Notification.objects.filter(user=self, read=False).count() @property def adoption_notices(self): @@ -479,7 +479,11 @@ class AdoptionNotice(models.Model): for subscription in self.get_subscriptions(): notification_title = _("Vermittlung deaktiviert:") + f" {self.name}" text = _("Die folgende Vermittlung wurde deaktiviert: ") + f"[{self.name}]({self.get_absolute_url()})" - BaseNotification.objects.create(user=subscription.owner, text=text, title=notification_title) + Notification.objects.create(user_to_notify=subscription.owner, + notification_type=NotificationTypeChoices.AN_WAS_DEACTIVATED, + adoption_notice=self, + text=text, + title=notification_title) class AdoptionNoticeStatus(models.Model): @@ -902,20 +906,49 @@ class Comment(models.Model): return self.adoption_notice.get_absolute_url() -class BaseNotification(models.Model): +class NotificationTypeChoices(models.TextChoices): + NEW_USER = "new_user", _("Useraccount wurde erstellt") + NEW_REPORT_AN = "new_report_an", _("Vermittlung wurde gemeldet") + NEW_REPORT_COMMENT = "new_report_comment", _("Kommentar wurde gemeldet") + AN_IS_TO_BE_CHECKED = "an_is_to_be_checked", _("Vermittlung muss überprüft werden") + AN_WAS_DEACTIVATED = "an_was_deactivated", _("Vermittlung wurde deaktiviert") + AN_FOR_SEARCH_FOUND = "an_for_search_found", _("Vermittlung für Suche gefunden") + NEW_COMMENT = "new_comment", _("Neuer Kommentar") + + +class Notification(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) read_at = models.DateTimeField(blank=True, null=True, verbose_name=_("Gelesen am")) + notification_type = models.CharField(max_length=200, + choices=NotificationTypeChoices.choices, + verbose_name=_('Benachrichtigungsgrund')) title = models.CharField(max_length=100, verbose_name=_("Titel")) text = models.TextField(verbose_name="Inhalt") - user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in')) + user_to_notify = models.ForeignKey(User, + on_delete=models.CASCADE, + blank=True, null=True, + verbose_name=_('Nutzer*in'), + help_text=_("Useraccount der Benachrichtigt wird"), + related_name='user') read = models.BooleanField(default=False) + comment = models.ForeignKey(Comment, blank=True, null=True, on_delete=models.CASCADE, verbose_name=_('Antwort')) + adoption_notice = models.ForeignKey(AdoptionNotice, blank=True, null=True, on_delete=models.CASCADE, verbose_name=_('Vermittlung')) + user_related = models.ForeignKey(User, + blank=True, null=True, + on_delete=models.CASCADE, verbose_name=_('Verwandter Useraccount'), + help_text=_("Useraccount auf den sich die Benachrichtigung bezieht.")) + report = models.ForeignKey(Report, + blank=True, null=True, + on_delete=models.CASCADE, + verbose_name=_('Report'), + help_text=_("Report auf den sich die Benachrichtigung bezieht.")) def __str__(self): - return f"[{self.user}] {self.title} ({self.created_at})" + return f"[{self.user_to_notify}] {self.title} ({self.created_at})" def get_absolute_url(self): - self.user.get_notifications_url() + self.user_to_notify.get_notifications_url() def mark_read(self): self.read = True @@ -923,22 +956,6 @@ class BaseNotification(models.Model): self.save() -class CommentNotification(BaseNotification): - comment = models.ForeignKey(Comment, on_delete=models.CASCADE, verbose_name=_('Antwort')) - - @property - def url(self): - return self.comment.get_absolute_url - - -class AdoptionNoticeNotification(BaseNotification): - adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung')) - - @property - def url(self): - return self.adoption_notice.get_absolute_url - - class Subscriptions(models.Model): owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in')) adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice')) diff --git a/src/fellchensammlung/receivers.py b/src/fellchensammlung/receivers.py index ba61a73..db5e426 100644 --- a/src/fellchensammlung/receivers.py +++ b/src/fellchensammlung/receivers.py @@ -1,23 +1,22 @@ from django.db.models.signals import post_save from django.dispatch import receiver -from fellchensammlung.models import BaseNotification, CommentNotification, User, TrustLevel, RescueOrganization +from fellchensammlung.models import Notification, User, TrustLevel, RescueOrganization, \ + NotificationTypeChoices from .tasks import task_send_notification_email from notfellchen.settings import host from django.utils.translation import gettext_lazy as _ -@receiver(post_save, sender=CommentNotification) -def comment_notification_receiver(sender, instance: BaseNotification, created: bool, **kwargs): - base_notification_receiver(sender, instance, created, **kwargs) - - -@receiver(post_save, sender=BaseNotification) -def base_notification_receiver(sender, instance: BaseNotification, created: bool, **kwargs): - if not created or not instance.user.email_notifications: +@receiver(post_save, sender=Notification) +def base_notification_receiver(sender, instance: Notification, created: bool, **kwargs): + print("Dada") + if not created or not instance.user_to_notify.email_notifications: return else: + print("Dodo") task_send_notification_email.delay(instance.pk) + @receiver(post_save, sender=RescueOrganization) def rescue_org_receiver(sender, instance: RescueOrganization, created: bool, **kwargs): if instance.location: @@ -40,5 +39,9 @@ def notification_new_user(sender, instance: User, created: bool, **kwargs): link_text = f"Um alle Details zu sehen, geh bitte auf: {user_url}" body_text = new_user_text + user_detail_text + link_text for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR): - notification = BaseNotification.objects.create(title=subject, text=body_text, user=moderator) + notification = Notification.objects.create(title=subject, + text=body_text, + notification_type=NotificationTypeChoices.NEW_USER, + user_to_notify=moderator, + user_related=instance) notification.save() diff --git a/src/fellchensammlung/templates/fellchensammlung/mail/notifications/an-deactivated.html b/src/fellchensammlung/templates/fellchensammlung/mail/notifications/an-deactivated.html new file mode 100644 index 0000000..9ec7f28 --- /dev/null +++ b/src/fellchensammlung/templates/fellchensammlung/mail/notifications/an-deactivated.html @@ -0,0 +1,16 @@ +{% extends "fellchensammlung/mail/base.html" %} +{% load i18n %} +{% block title %} + {% translate 'Vermittlung wurde deaktiviert' %} +{% endblock %} + +{% block content %} +

Moin,

+

+ die Vermittlung {{ notification.adoption_notice }} wurde deaktiviert. + +

+

+ {% translate 'Vermittlung anzeigen' %} +

+{% endblock %} \ No newline at end of file diff --git a/src/fellchensammlung/templates/fellchensammlung/mail/notifications/an-for-search-found.html b/src/fellchensammlung/templates/fellchensammlung/mail/notifications/an-for-search-found.html new file mode 100644 index 0000000..05517f4 --- /dev/null +++ b/src/fellchensammlung/templates/fellchensammlung/mail/notifications/an-for-search-found.html @@ -0,0 +1,16 @@ +{% extends "fellchensammlung/mail/base.html" %} +{% load i18n %} +{% block title %} + {% translate 'Neue Vermittlung gefunden' %} +{% endblock %} + +{% block content %} +

Moin,

+

+ es wurde eine neue Vermittlung gefunden die deinen Kriterien entspricht: {{ notification.adoption_notice }} + +

+

+ {% translate 'Vermittlung anzeigen' %} +

+{% endblock %} \ No newline at end of file diff --git a/src/fellchensammlung/templates/fellchensammlung/mail/notifications/an-to-be-checked.html b/src/fellchensammlung/templates/fellchensammlung/mail/notifications/an-to-be-checked.html new file mode 100644 index 0000000..7c4b8a0 --- /dev/null +++ b/src/fellchensammlung/templates/fellchensammlung/mail/notifications/an-to-be-checked.html @@ -0,0 +1,16 @@ +{% extends "fellchensammlung/mail/base.html" %} +{% load i18n %} +{% block title %} + {% translate 'Vermittlung muss überprüft werden' %} +{% endblock %} + +{% block content %} +

Moin,

+

+ die Vermittlung {{ notification.adoption_notice }} muss überprüft werden. + +

+

+ {% translate 'Vermittlung anzeigen' %} +

+{% endblock %} \ No newline at end of file diff --git a/src/fellchensammlung/templates/fellchensammlung/mail/notifications/new-comment.html b/src/fellchensammlung/templates/fellchensammlung/mail/notifications/new-comment.html new file mode 100644 index 0000000..708b374 --- /dev/null +++ b/src/fellchensammlung/templates/fellchensammlung/mail/notifications/new-comment.html @@ -0,0 +1,19 @@ +{% extends "fellchensammlung/mail/base.html" %} +{% load i18n %} +{% load custom_tags %} +{% block title %} + {% translate 'Vermittlung muss überprüft werden' %} +{% endblock %} + +{% block content %} +

Moin,

+

+ folgender Kommentar wurde zur Vermittlung {{ notification.adoption_notice }} hinzugefügt: +

+

+ {{ notification.comment.text | render_markdown }} +

+

+ {% translate 'Vermittlung anzeigen' %} +

+{% endblock %} \ No newline at end of file diff --git a/src/fellchensammlung/templates/fellchensammlung/mail/notifications/new-user.html b/src/fellchensammlung/templates/fellchensammlung/mail/notifications/new-user.html new file mode 100644 index 0000000..3bffbac --- /dev/null +++ b/src/fellchensammlung/templates/fellchensammlung/mail/notifications/new-user.html @@ -0,0 +1,19 @@ +{% extends "fellchensammlung/mail/base.html" %} +{% load i18n %} +{% block title %} + {% translate 'Neuer User' %} +{% endblock %} + +{% block content %} +

Moin,

+

+ es wurde ein neuer Useraccount erstellt. + +

+

+ Details findest du hier +

+

+ {% translate 'User anzeigen' %} +

+{% endblock %} \ No newline at end of file diff --git a/src/fellchensammlung/templates/fellchensammlung/mail/report.html b/src/fellchensammlung/templates/fellchensammlung/mail/notifications/report.html similarity index 86% rename from src/fellchensammlung/templates/fellchensammlung/mail/report.html rename to src/fellchensammlung/templates/fellchensammlung/mail/notifications/report.html index f9cdd14..8bfc26b 100644 --- a/src/fellchensammlung/templates/fellchensammlung/mail/report.html +++ b/src/fellchensammlung/templates/fellchensammlung/mail/notifications/report.html @@ -7,7 +7,7 @@ {% block content %}

Moin,

- es gibt eine neue Meldung. Folgende Nachricht wurde zur Meldung hinzugefügt. + es gibt eine neue Meldung. Folgende Nachricht wurde zur Meldung hinzugefügt:

@@ -17,7 +17,7 @@

- Bitte bearbeite die Meldung möglichst bald + Bitte bearbeite die Meldung möglichst bald.

{% translate 'Report bearbeiten' %} diff --git a/src/fellchensammlung/tools/admin.py b/src/fellchensammlung/tools/admin.py index 7f37cef..11b890a 100644 --- a/src/fellchensammlung/tools/admin.py +++ b/src/fellchensammlung/tools/admin.py @@ -10,7 +10,7 @@ from django.core import mail from django.utils.html import strip_tags from fellchensammlung.models import AdoptionNotice, Location, RescueOrganization, AdoptionNoticeStatus, Log, \ - AdoptionNoticeNotification + Notification, NotificationTypeChoices from fellchensammlung.tools.misc import is_404 @@ -92,10 +92,11 @@ def deactivate_404_adoption_notices(): deactivation_message = f'Die Vermittlung [{adoption_notice.name}]({adoption_notice.get_absolute_url()}) wurde automatisch deaktiviert, da die Website unter "Mehr Informationen" nicht mehr online ist.' for subscription in adoption_notice.get_subscriptions(): - AdoptionNoticeNotification.objects.create(user=subscription.owner, - title="Vermittlung deaktiviert", - adoption_notice=adoption_notice, - text=deactivation_message) + Notification.objects.create(user_to_notify=subscription.owner, + notification_type=NotificationTypeChoices.AN_WAS_DEACTIVATED, + title="Vermittlung deaktiviert", + adoption_notice=adoption_notice, + text=deactivation_message) def dedup_location(location: Location, destructive=False): diff --git a/src/fellchensammlung/tools/notifications.py b/src/fellchensammlung/tools/notifications.py index 63fd1f0..3430d0d 100644 --- a/src/fellchensammlung/tools/notifications.py +++ b/src/fellchensammlung/tools/notifications.py @@ -1,4 +1,4 @@ -from fellchensammlung.models import User, AdoptionNoticeNotification, TrustLevel +from fellchensammlung.models import User, Notification, TrustLevel, NotificationTypeChoices def notify_of_AN_to_be_checked(adoption_notice): @@ -6,8 +6,9 @@ def notify_of_AN_to_be_checked(adoption_notice): users_to_notify = set(User.objects.filter(trust_level__gt=TrustLevel.MODERATOR)) users_to_notify.add(adoption_notice.owner) for user in users_to_notify: - AdoptionNoticeNotification.objects.create(adoption_notice=adoption_notice, - user=user, - title=f" Prüfe Vermittlung {adoption_notice}", - text=f"{adoption_notice} muss geprüft werden bevor sie veröffentlicht wird.", - ) \ No newline at end of file + Notification.objects.create(adoption_notice=adoption_notice, + user_to_notify=user, + notification_type=NotificationTypeChoices.AN_IS_TO_BE_CHECKED, + title=f" Prüfe Vermittlung {adoption_notice}", + text=f"{adoption_notice} muss geprüft werden bevor sie veröffentlicht wird.", + ) diff --git a/src/fellchensammlung/tools/search.py b/src/fellchensammlung/tools/search.py index 49237da..ad36b4c 100644 --- a/src/fellchensammlung/tools/search.py +++ b/src/fellchensammlung/tools/search.py @@ -3,7 +3,8 @@ from django.utils.translation import gettext_lazy as _ from .geo import LocationProxy, Position from ..forms import AdoptionNoticeSearchForm -from ..models import SearchSubscription, AdoptionNotice, AdoptionNoticeNotification, SexChoicesWithAll, Location +from ..models import SearchSubscription, AdoptionNotice, SexChoicesWithAll, Location, \ + Notification, NotificationTypeChoices def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: bool = True): @@ -20,10 +21,11 @@ def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: b search = Search(search_subscription=search_subscription) if search.adoption_notice_fits_search(adoption_notice): notification_text = f"{_('Zu deiner Suche')} {search_subscription} wurde eine neue Vermittlung gefunden" - AdoptionNoticeNotification.objects.create(user=search_subscription.owner, - title=f"{_('Neue Vermittlung')}: {adoption_notice}", - adoption_notice=adoption_notice, - text=notification_text) + Notification.objects.create(user_to_notify=search_subscription.owner, + notification_type=NotificationTypeChoices.AN_FOR_SEARCH_FOUND, + title=f"{_('Neue Vermittlung')}: {adoption_notice}", + adoption_notice=adoption_notice, + text=notification_text) logging.debug(f"Notification for search subscription {search_subscription} was sent.") else: logging.debug(f"Adoption notice {adoption_notice} was not fitting the search subscription.") diff --git a/src/fellchensammlung/views.py b/src/fellchensammlung/views.py index 9b69c78..f368824 100644 --- a/src/fellchensammlung/views.py +++ b/src/fellchensammlung/views.py @@ -20,9 +20,9 @@ from notfellchen import settings from fellchensammlung import logger from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \ - User, Location, AdoptionNoticeStatus, Subscriptions, CommentNotification, BaseNotification, RescueOrganization, \ - Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, AdoptionNoticeNotification, \ - ImportantLocation, SpeciesSpecificURL + User, Location, AdoptionNoticeStatus, Subscriptions, Notification, RescueOrganization, \ + Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, \ + ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices from .forms import AdoptionNoticeForm, ImageForm, ReportAdoptionNoticeForm, \ CommentForm, ReportCommentForm, AnimalForm, AdoptionNoticeFormAutoAnimal, SpeciesURLForm, RescueOrgInternalComment from .models import Language, Announcement @@ -121,10 +121,12 @@ def adoption_notice_detail(request, adoption_notice_id): for subscription in adoption_notice.get_subscriptions(): # Create a notification but only if the user is not the one that posted the comment if subscription.owner != request.user: - notification = CommentNotification(user=subscription.owner, - title=f"{adoption_notice.name} - Neuer Kommentar", - text=f"{request.user}: {comment_instance.text}", - comment=comment_instance) + notification = Notification(user_to_notify=subscription.owner, + adoption_notice=adoption_notice, + notification_type=NotificationTypeChoices.NEW_COMMENT, + title=f"{adoption_notice.name} - Neuer Kommentar", + text=f"{request.user}: {comment_instance.text}", + comment=comment_instance) notification.save() else: comment_form = CommentForm(instance=adoption_notice) @@ -177,7 +179,8 @@ def search_important_locations(request, important_location_slug): search.search_from_predefined_i_location(i_location) site_title = _("Ratten in %(location_name)s") % {"location_name": i_location.name} - site_description = _("Ratten in Tierheimen und Rattenhilfen in der Nähe von %(location_name)s suchen.") % {"location_name": i_location.name} + site_description = _("Ratten in Tierheimen und Rattenhilfen in der Nähe von %(location_name)s suchen.") % { + "location_name": i_location.name} canonical_url = reverse("search-by-location", args=[i_location.slug]) context = {"adoption_notices": search.get_adoption_notices(), @@ -528,7 +531,7 @@ def report_detail_success(request, report_id): def user_detail(request, user, token=None): context = {"user": user, "adoption_notices": AdoptionNotice.objects.filter(owner=user), - "notifications": BaseNotification.objects.filter(user=user, read=False), + "notifications": Notification.objects.filter(user_to_notify=user, read=False), "search_subscriptions": SearchSubscription.objects.filter(owner=user), } if token is not None: context["token"] = token @@ -561,13 +564,11 @@ def my_profile(request): action = request.POST.get("action") if action == "notification_mark_read": notification_id = request.POST.get("notification_id") - try: - notification = CommentNotification.objects.get(pk=notification_id) - except CommentNotification.DoesNotExist: - notification = BaseNotification.objects.get(pk=notification_id) + + notification = Notification.objects.get(pk=notification_id) notification.mark_read() elif action == "notification_mark_all_read": - notifications = CommentNotification.objects.filter(user=request.user, mark_read=False) + notifications = Notification.objects.filter(user=request.user, mark_read=False) for notification in notifications: notification.mark_read() elif action == "search_subscription_delete": @@ -770,11 +771,13 @@ def rescue_organization_check(request, context=None): } rescue_orgs_last_checked = RescueOrganization.objects.filter().order_by("-last_checked")[:10] timeframe = timezone.now().date() - timedelta(days=14) - num_rescue_orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=False).filter(last_checked__lt=timeframe).count() - num_rescue_orgs_checked = RescueOrganization.objects.filter(exclude_from_check=False).filter(last_checked__gte=timeframe).count() + num_rescue_orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=False).filter( + last_checked__lt=timeframe).count() + num_rescue_orgs_checked = RescueOrganization.objects.filter(exclude_from_check=False).filter( + last_checked__gte=timeframe).count() try: - percentage_checked = 100*num_rescue_orgs_checked/(num_rescue_orgs_to_check+num_rescue_orgs_checked) + percentage_checked = 100 * num_rescue_orgs_checked / (num_rescue_orgs_to_check + num_rescue_orgs_checked) except ZeroDivisionError: percentage_checked = 100 diff --git a/src/tests/test_models.py b/src/tests/test_models.py index 9aff0fe..68882af 100644 --- a/src/tests/test_models.py +++ b/src/tests/test_models.py @@ -4,7 +4,7 @@ from django.utils import timezone from django.test import TestCase from model_bakery import baker -from fellchensammlung.models import Announcement, Language, User, TrustLevel, BaseNotification +from fellchensammlung.models import Announcement, Language, User, TrustLevel, Notification class UserTest(TestCase): @@ -85,9 +85,9 @@ class TestNotifications(TestCase): cls.test_user_1 = User.objects.create(username="Testuser1", password="SUPERSECRET", email="test@example.org") def test_mark_read(self): - not1 = BaseNotification.objects.create(user=self.test_user_1, text="New rats to adopt", title="🔔 New Rat alert") - not2 = BaseNotification.objects.create(user=self.test_user_1, - text="New wombat to adopt", title="🔔 New Wombat alert") + not1 = Notification.objects.create(user=self.test_user_1, text="New rats to adopt", title="🔔 New Rat alert") + not2 = Notification.objects.create(user=self.test_user_1, + text="New wombat to adopt", title="🔔 New Wombat alert") not1.mark_read() self.assertTrue(not1.read)