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
This commit is contained in:
@@ -11,7 +11,7 @@ from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, L
|
|||||||
SpeciesSpecificURL, ImportantLocation, SpeciesSpecialization
|
SpeciesSpecificURL, ImportantLocation, SpeciesSpecialization
|
||||||
|
|
||||||
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
|
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 _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
@@ -127,9 +127,9 @@ class CommentAdmin(admin.ModelAdmin):
|
|||||||
list_filter = ("user",)
|
list_filter = ("user",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(BaseNotification)
|
@admin.register(Notification)
|
||||||
class BaseNotificationAdmin(admin.ModelAdmin):
|
class BaseNotificationAdmin(admin.ModelAdmin):
|
||||||
list_filter = ("user", "read")
|
list_filter = ("user_to_notify", "read")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(SearchSubscription)
|
@admin.register(SearchSubscription)
|
||||||
|
@@ -66,22 +66,22 @@ class AdoptionNoticeApiView(APIView):
|
|||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
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
|
# Add the location
|
||||||
post_adoption_notice_save.delay_on_commit(adoption_notice.pk)
|
post_adoption_notice_save.delay_on_commit(adoption_notice.pk)
|
||||||
|
|
||||||
# Only set active when user has trust level moderator or higher
|
# 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()
|
adoption_notice.set_active()
|
||||||
else:
|
else:
|
||||||
adoption_notice.set_unchecked()
|
adoption_notice.set_unchecked()
|
||||||
|
|
||||||
# Log the action
|
# Log the action
|
||||||
Log.objects.create(
|
Log.objects.create(
|
||||||
user=request.user,
|
user=request.user_to_notify,
|
||||||
action="add_adoption_notice",
|
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
|
# Return success response with new adoption notice details
|
||||||
@@ -130,7 +130,7 @@ class AnimalApiView(APIView):
|
|||||||
"""
|
"""
|
||||||
serializer = AnimalCreateSerializer(data=request.data, context={"request": request})
|
serializer = AnimalCreateSerializer(data=request.data, context={"request": request})
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
animal = serializer.save(owner=request.user)
|
animal = serializer.save(owner=request.user_to_notify)
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "Animal created successfully!", "id": animal.id},
|
{"message": "Animal created successfully!", "id": animal.id},
|
||||||
status=status.HTTP_201_CREATED,
|
status=status.HTTP_201_CREATED,
|
||||||
@@ -289,7 +289,7 @@ class AddImageApiView(APIView):
|
|||||||
raise ValueError("Unknown attach_to_type given, should not happen. Check serializer")
|
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_type', None)
|
||||||
serializer.validated_data.pop('attach_to', 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)
|
object_to_attach_to.photos.add(image)
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "Image added successfully!", "id": image.id},
|
{"message": "Image added successfully!", "id": image.id},
|
||||||
@@ -360,9 +360,9 @@ class LocationApiView(APIView):
|
|||||||
|
|
||||||
# Log the action
|
# Log the action
|
||||||
Log.objects.create(
|
Log.objects.create(
|
||||||
user=request.user,
|
user=request.user_to_notify,
|
||||||
action="add_location",
|
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
|
# Return success response with new adoption notice details
|
||||||
|
@@ -6,7 +6,7 @@ from django.utils.html import strip_tags
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import mail
|
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
|
from notfellchen.settings import host
|
||||||
|
|
||||||
NEWLINE = "\r\n"
|
NEWLINE = "\r\n"
|
||||||
@@ -19,10 +19,10 @@ def mail_admins_new_report(report):
|
|||||||
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
|
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
|
||||||
report_url = "https://" + host + report.get_absolute_url()
|
report_url = "https://" + host + report.get_absolute_url()
|
||||||
context = {"report_url": report_url,
|
context = {"report_url": report_url,
|
||||||
"user_comment": report.user_comment,}
|
"user_comment": report.user_comment, }
|
||||||
|
|
||||||
subject = _("Neue Meldung")
|
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)
|
plain_message = strip_tags(html_message)
|
||||||
|
|
||||||
mail.send_mail(subject,
|
mail.send_mail(subject,
|
||||||
@@ -33,11 +33,28 @@ def mail_admins_new_report(report):
|
|||||||
|
|
||||||
|
|
||||||
def send_notification_email(notification_pk):
|
def send_notification_email(notification_pk):
|
||||||
try:
|
notification = Notification.objects.get(pk=notification_pk)
|
||||||
notification = CommentNotification.objects.get(pk=notification_pk)
|
|
||||||
except CommentNotification.DoesNotExist:
|
|
||||||
notification = BaseNotification.objects.get(pk=notification_pk)
|
|
||||||
subject = f"🔔 {notification.title}"
|
subject = f"🔔 {notification.title}"
|
||||||
body_text = notification.text
|
context = {"notification": notification, }
|
||||||
message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [notification.user.email])
|
if notification.notification_type == NotificationTypeChoices.NEW_REPORT_COMMENT or notification.notification_type == NotificationTypeChoices.NEW_REPORT_AN:
|
||||||
message.send()
|
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)
|
||||||
|
@@ -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',
|
||||||
|
),
|
||||||
|
]
|
@@ -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'),
|
||||||
|
),
|
||||||
|
]
|
@@ -256,10 +256,10 @@ class User(AbstractUser):
|
|||||||
return self.get_absolute_url()
|
return self.get_absolute_url()
|
||||||
|
|
||||||
def get_unread_notifications(self):
|
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):
|
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
|
@property
|
||||||
def adoption_notices(self):
|
def adoption_notices(self):
|
||||||
@@ -479,7 +479,11 @@ class AdoptionNotice(models.Model):
|
|||||||
for subscription in self.get_subscriptions():
|
for subscription in self.get_subscriptions():
|
||||||
notification_title = _("Vermittlung deaktiviert:") + f" {self.name}"
|
notification_title = _("Vermittlung deaktiviert:") + f" {self.name}"
|
||||||
text = _("Die folgende Vermittlung wurde deaktiviert: ") + f"[{self.name}]({self.get_absolute_url()})"
|
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):
|
class AdoptionNoticeStatus(models.Model):
|
||||||
@@ -902,20 +906,49 @@ class Comment(models.Model):
|
|||||||
return self.adoption_notice.get_absolute_url()
|
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)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
read_at = models.DateTimeField(blank=True, null=True, verbose_name=_("Gelesen am"))
|
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"))
|
title = models.CharField(max_length=100, verbose_name=_("Titel"))
|
||||||
text = models.TextField(verbose_name="Inhalt")
|
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)
|
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):
|
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):
|
def get_absolute_url(self):
|
||||||
self.user.get_notifications_url()
|
self.user_to_notify.get_notifications_url()
|
||||||
|
|
||||||
def mark_read(self):
|
def mark_read(self):
|
||||||
self.read = True
|
self.read = True
|
||||||
@@ -923,22 +956,6 @@ class BaseNotification(models.Model):
|
|||||||
self.save()
|
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):
|
class Subscriptions(models.Model):
|
||||||
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
|
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
|
||||||
|
@@ -1,23 +1,22 @@
|
|||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
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 .tasks import task_send_notification_email
|
||||||
from notfellchen.settings import host
|
from notfellchen.settings import host
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=CommentNotification)
|
@receiver(post_save, sender=Notification)
|
||||||
def comment_notification_receiver(sender, instance: BaseNotification, created: bool, **kwargs):
|
def base_notification_receiver(sender, instance: Notification, created: bool, **kwargs):
|
||||||
base_notification_receiver(sender, instance, created, **kwargs)
|
print("Dada")
|
||||||
|
if not created or not instance.user_to_notify.email_notifications:
|
||||||
|
|
||||||
@receiver(post_save, sender=BaseNotification)
|
|
||||||
def base_notification_receiver(sender, instance: BaseNotification, created: bool, **kwargs):
|
|
||||||
if not created or not instance.user.email_notifications:
|
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
print("Dodo")
|
||||||
task_send_notification_email.delay(instance.pk)
|
task_send_notification_email.delay(instance.pk)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=RescueOrganization)
|
@receiver(post_save, sender=RescueOrganization)
|
||||||
def rescue_org_receiver(sender, instance: RescueOrganization, created: bool, **kwargs):
|
def rescue_org_receiver(sender, instance: RescueOrganization, created: bool, **kwargs):
|
||||||
if instance.location:
|
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}"
|
link_text = f"Um alle Details zu sehen, geh bitte auf: {user_url}"
|
||||||
body_text = new_user_text + user_detail_text + link_text
|
body_text = new_user_text + user_detail_text + link_text
|
||||||
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
|
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()
|
notification.save()
|
||||||
|
@@ -0,0 +1,16 @@
|
|||||||
|
{% extends "fellchensammlung/mail/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}
|
||||||
|
{% translate 'Vermittlung wurde deaktiviert' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>Moin,</p>
|
||||||
|
<p>
|
||||||
|
die Vermittlung {{ notification.adoption_notice }} wurde deaktiviert.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="{{ notification.adoption_notice.get_absolute_url }}" class="cta-button">{% translate 'Vermittlung anzeigen' %}</a>
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
@@ -0,0 +1,16 @@
|
|||||||
|
{% extends "fellchensammlung/mail/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}
|
||||||
|
{% translate 'Neue Vermittlung gefunden' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>Moin,</p>
|
||||||
|
<p>
|
||||||
|
es wurde eine neue Vermittlung gefunden die deinen Kriterien entspricht: {{ notification.adoption_notice }}
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="{{ notification.adoption_notice.get_absolute_url }}" class="cta-button">{% translate 'Vermittlung anzeigen' %}</a>
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
@@ -0,0 +1,16 @@
|
|||||||
|
{% extends "fellchensammlung/mail/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}
|
||||||
|
{% translate 'Vermittlung muss überprüft werden' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>Moin,</p>
|
||||||
|
<p>
|
||||||
|
die Vermittlung {{ notification.adoption_notice }} muss überprüft werden.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="{{ notification.adoption_notice.get_absolute_url }}" class="cta-button">{% translate 'Vermittlung anzeigen' %}</a>
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
@@ -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 %}
|
||||||
|
<p>Moin,</p>
|
||||||
|
<p>
|
||||||
|
folgender Kommentar wurde zur Vermittlung {{ notification.adoption_notice }} hinzugefügt:
|
||||||
|
</p>
|
||||||
|
<p><i>
|
||||||
|
{{ notification.comment.text | render_markdown }}
|
||||||
|
</i></p>
|
||||||
|
<p>
|
||||||
|
<a href="{{ notification.adoption_notice.get_absolute_url }}" class="cta-button">{% translate 'Vermittlung anzeigen' %}</a>
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
@@ -0,0 +1,19 @@
|
|||||||
|
{% extends "fellchensammlung/mail/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}
|
||||||
|
{% translate 'Neuer User' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<p>Moin,</p>
|
||||||
|
<p>
|
||||||
|
es wurde ein neuer Useraccount erstellt.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Details findest du hier
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="{{ notification.user_related.get_absolute_url }}" class="cta-button">{% translate 'User anzeigen' %}</a>
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
@@ -7,7 +7,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<p>Moin,</p>
|
<p>Moin,</p>
|
||||||
<p>
|
<p>
|
||||||
es gibt eine neue Meldung. Folgende Nachricht wurde zur Meldung hinzugefügt.
|
es gibt eine neue Meldung. Folgende Nachricht wurde zur Meldung hinzugefügt:
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
Bitte bearbeite die Meldung möglichst bald
|
Bitte bearbeite die Meldung möglichst bald.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ report_url }}" class="cta-button">{% translate 'Report bearbeiten' %}</a>
|
<a href="{{ report_url }}" class="cta-button">{% translate 'Report bearbeiten' %}</a>
|
@@ -10,7 +10,7 @@ from django.core import mail
|
|||||||
from django.utils.html import strip_tags
|
from django.utils.html import strip_tags
|
||||||
|
|
||||||
from fellchensammlung.models import AdoptionNotice, Location, RescueOrganization, AdoptionNoticeStatus, Log, \
|
from fellchensammlung.models import AdoptionNotice, Location, RescueOrganization, AdoptionNoticeStatus, Log, \
|
||||||
AdoptionNoticeNotification
|
Notification, NotificationTypeChoices
|
||||||
from fellchensammlung.tools.misc import is_404
|
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.'
|
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():
|
for subscription in adoption_notice.get_subscriptions():
|
||||||
AdoptionNoticeNotification.objects.create(user=subscription.owner,
|
Notification.objects.create(user_to_notify=subscription.owner,
|
||||||
title="Vermittlung deaktiviert",
|
notification_type=NotificationTypeChoices.AN_WAS_DEACTIVATED,
|
||||||
adoption_notice=adoption_notice,
|
title="Vermittlung deaktiviert",
|
||||||
text=deactivation_message)
|
adoption_notice=adoption_notice,
|
||||||
|
text=deactivation_message)
|
||||||
|
|
||||||
|
|
||||||
def dedup_location(location: Location, destructive=False):
|
def dedup_location(location: Location, destructive=False):
|
||||||
|
@@ -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):
|
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 = set(User.objects.filter(trust_level__gt=TrustLevel.MODERATOR))
|
||||||
users_to_notify.add(adoption_notice.owner)
|
users_to_notify.add(adoption_notice.owner)
|
||||||
for user in users_to_notify:
|
for user in users_to_notify:
|
||||||
AdoptionNoticeNotification.objects.create(adoption_notice=adoption_notice,
|
Notification.objects.create(adoption_notice=adoption_notice,
|
||||||
user=user,
|
user_to_notify=user,
|
||||||
title=f" Prüfe Vermittlung {adoption_notice}",
|
notification_type=NotificationTypeChoices.AN_IS_TO_BE_CHECKED,
|
||||||
text=f"{adoption_notice} muss geprüft werden bevor sie veröffentlicht wird.",
|
title=f" Prüfe Vermittlung {adoption_notice}",
|
||||||
)
|
text=f"{adoption_notice} muss geprüft werden bevor sie veröffentlicht wird.",
|
||||||
|
)
|
||||||
|
@@ -3,7 +3,8 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from .geo import LocationProxy, Position
|
from .geo import LocationProxy, Position
|
||||||
from ..forms import AdoptionNoticeSearchForm
|
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):
|
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)
|
search = Search(search_subscription=search_subscription)
|
||||||
if search.adoption_notice_fits_search(adoption_notice):
|
if search.adoption_notice_fits_search(adoption_notice):
|
||||||
notification_text = f"{_('Zu deiner Suche')} {search_subscription} wurde eine neue Vermittlung gefunden"
|
notification_text = f"{_('Zu deiner Suche')} {search_subscription} wurde eine neue Vermittlung gefunden"
|
||||||
AdoptionNoticeNotification.objects.create(user=search_subscription.owner,
|
Notification.objects.create(user_to_notify=search_subscription.owner,
|
||||||
title=f"{_('Neue Vermittlung')}: {adoption_notice}",
|
notification_type=NotificationTypeChoices.AN_FOR_SEARCH_FOUND,
|
||||||
adoption_notice=adoption_notice,
|
title=f"{_('Neue Vermittlung')}: {adoption_notice}",
|
||||||
text=notification_text)
|
adoption_notice=adoption_notice,
|
||||||
|
text=notification_text)
|
||||||
logging.debug(f"Notification for search subscription {search_subscription} was sent.")
|
logging.debug(f"Notification for search subscription {search_subscription} was sent.")
|
||||||
else:
|
else:
|
||||||
logging.debug(f"Adoption notice {adoption_notice} was not fitting the search subscription.")
|
logging.debug(f"Adoption notice {adoption_notice} was not fitting the search subscription.")
|
||||||
|
@@ -20,9 +20,9 @@ from notfellchen import settings
|
|||||||
|
|
||||||
from fellchensammlung import logger
|
from fellchensammlung import logger
|
||||||
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
|
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
|
||||||
User, Location, AdoptionNoticeStatus, Subscriptions, CommentNotification, BaseNotification, RescueOrganization, \
|
User, Location, AdoptionNoticeStatus, Subscriptions, Notification, RescueOrganization, \
|
||||||
Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, AdoptionNoticeNotification, \
|
Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, \
|
||||||
ImportantLocation, SpeciesSpecificURL
|
ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices
|
||||||
from .forms import AdoptionNoticeForm, ImageForm, ReportAdoptionNoticeForm, \
|
from .forms import AdoptionNoticeForm, ImageForm, ReportAdoptionNoticeForm, \
|
||||||
CommentForm, ReportCommentForm, AnimalForm, AdoptionNoticeFormAutoAnimal, SpeciesURLForm, RescueOrgInternalComment
|
CommentForm, ReportCommentForm, AnimalForm, AdoptionNoticeFormAutoAnimal, SpeciesURLForm, RescueOrgInternalComment
|
||||||
from .models import Language, Announcement
|
from .models import Language, Announcement
|
||||||
@@ -121,10 +121,12 @@ def adoption_notice_detail(request, adoption_notice_id):
|
|||||||
for subscription in adoption_notice.get_subscriptions():
|
for subscription in adoption_notice.get_subscriptions():
|
||||||
# Create a notification but only if the user is not the one that posted the comment
|
# Create a notification but only if the user is not the one that posted the comment
|
||||||
if subscription.owner != request.user:
|
if subscription.owner != request.user:
|
||||||
notification = CommentNotification(user=subscription.owner,
|
notification = Notification(user_to_notify=subscription.owner,
|
||||||
title=f"{adoption_notice.name} - Neuer Kommentar",
|
adoption_notice=adoption_notice,
|
||||||
text=f"{request.user}: {comment_instance.text}",
|
notification_type=NotificationTypeChoices.NEW_COMMENT,
|
||||||
comment=comment_instance)
|
title=f"{adoption_notice.name} - Neuer Kommentar",
|
||||||
|
text=f"{request.user}: {comment_instance.text}",
|
||||||
|
comment=comment_instance)
|
||||||
notification.save()
|
notification.save()
|
||||||
else:
|
else:
|
||||||
comment_form = CommentForm(instance=adoption_notice)
|
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)
|
search.search_from_predefined_i_location(i_location)
|
||||||
|
|
||||||
site_title = _("Ratten in %(location_name)s") % {"location_name": i_location.name}
|
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])
|
canonical_url = reverse("search-by-location", args=[i_location.slug])
|
||||||
|
|
||||||
context = {"adoption_notices": search.get_adoption_notices(),
|
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):
|
def user_detail(request, user, token=None):
|
||||||
context = {"user": user,
|
context = {"user": user,
|
||||||
"adoption_notices": AdoptionNotice.objects.filter(owner=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), }
|
"search_subscriptions": SearchSubscription.objects.filter(owner=user), }
|
||||||
if token is not None:
|
if token is not None:
|
||||||
context["token"] = token
|
context["token"] = token
|
||||||
@@ -561,13 +564,11 @@ def my_profile(request):
|
|||||||
action = request.POST.get("action")
|
action = request.POST.get("action")
|
||||||
if action == "notification_mark_read":
|
if action == "notification_mark_read":
|
||||||
notification_id = request.POST.get("notification_id")
|
notification_id = request.POST.get("notification_id")
|
||||||
try:
|
|
||||||
notification = CommentNotification.objects.get(pk=notification_id)
|
notification = Notification.objects.get(pk=notification_id)
|
||||||
except CommentNotification.DoesNotExist:
|
|
||||||
notification = BaseNotification.objects.get(pk=notification_id)
|
|
||||||
notification.mark_read()
|
notification.mark_read()
|
||||||
elif action == "notification_mark_all_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:
|
for notification in notifications:
|
||||||
notification.mark_read()
|
notification.mark_read()
|
||||||
elif action == "search_subscription_delete":
|
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]
|
rescue_orgs_last_checked = RescueOrganization.objects.filter().order_by("-last_checked")[:10]
|
||||||
timeframe = timezone.now().date() - timedelta(days=14)
|
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_to_check = RescueOrganization.objects.filter(exclude_from_check=False).filter(
|
||||||
num_rescue_orgs_checked = RescueOrganization.objects.filter(exclude_from_check=False).filter(last_checked__gte=timeframe).count()
|
last_checked__lt=timeframe).count()
|
||||||
|
num_rescue_orgs_checked = RescueOrganization.objects.filter(exclude_from_check=False).filter(
|
||||||
|
last_checked__gte=timeframe).count()
|
||||||
|
|
||||||
try:
|
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:
|
except ZeroDivisionError:
|
||||||
percentage_checked = 100
|
percentage_checked = 100
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@ from django.utils import timezone
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from model_bakery import baker
|
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):
|
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")
|
cls.test_user_1 = User.objects.create(username="Testuser1", password="SUPERSECRET", email="test@example.org")
|
||||||
|
|
||||||
def test_mark_read(self):
|
def test_mark_read(self):
|
||||||
not1 = BaseNotification.objects.create(user=self.test_user_1, text="New rats to adopt", title="🔔 New Rat alert")
|
not1 = Notification.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,
|
not2 = Notification.objects.create(user=self.test_user_1,
|
||||||
text="New wombat to adopt", title="🔔 New Wombat alert")
|
text="New wombat to adopt", title="🔔 New Wombat alert")
|
||||||
not1.mark_read()
|
not1.mark_read()
|
||||||
|
|
||||||
self.assertTrue(not1.read)
|
self.assertTrue(not1.read)
|
||||||
|
Reference in New Issue
Block a user