Merge branch 'notification_rework' into develop
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2025-07-11 22:12:57 +02:00
21 changed files with 346 additions and 98 deletions

View File

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

View File

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

View File

@@ -6,8 +6,8 @@ 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 base_url
NEWLINE = "\r\n" NEWLINE = "\r\n"
@@ -17,12 +17,12 @@ def mail_admins_new_report(report):
Sends an e-mail to all users that should handle the report. Sends an e-mail to all users that should handle the 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 = base_url + 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: subject = f"{notification.title}"
notification = BaseNotification.objects.get(pk=notification_pk) context = {"notification": notification, }
subject = f"🔔 {notification.title}" if notification.notification_type == NotificationTypeChoices.NEW_REPORT_COMMENT or notification.notification_type == NotificationTypeChoices.NEW_REPORT_AN:
body_text = notification.text context["user_comment"] = notification.report.user_comment
message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [notification.user.email]) context["report_url"] = f"{base_url}{notification.report.get_absolute_url()}"
message.send() 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/an-for-search-found.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)

View File

@@ -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',
),
]

View File

@@ -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(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'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.2.1 on 2025-07-11 11:47
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0053_alter_notification_adoption_notice_and_more'),
]
operations = [
migrations.AlterField(
model_name='notification',
name='comment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.comment', verbose_name='Antwort'),
),
]

View File

@@ -14,7 +14,7 @@ from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from .tools import misc, geo from .tools import misc, geo
from notfellchen.settings import MEDIA_URL from notfellchen.settings import MEDIA_URL, base_url
from .tools.geo import LocationProxy, Position from .tools.geo import LocationProxy, Position
from .tools.misc import age_as_hr_string, time_since_as_hr_string from .tools.misc import age_as_hr_string, time_since_as_hr_string
@@ -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):
@@ -389,6 +389,10 @@ class AdoptionNotice(models.Model):
"""Returns the url to access a detailed page for the adoption notice.""" """Returns the url to access a detailed page for the adoption notice."""
return reverse('adoption-notice-detail', args=[str(self.id)]) return reverse('adoption-notice-detail', args=[str(self.id)])
def get_full_url(self):
"""Returns the url including protocol and domain"""
return f"{base_url}{self.get_absolute_url()}"
def get_report_url(self): def get_report_url(self):
"""Returns the url to report an adoption notice.""" """Returns the url to report an adoption notice."""
return reverse('report-adoption-notice', args=[str(self.id)]) return reverse('report-adoption-notice', args=[str(self.id)])
@@ -479,7 +483,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 +910,48 @@ 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,
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 +959,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'))

View File

@@ -1,23 +1,20 @@
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) 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:
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 +37,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()

View File

@@ -48,6 +48,7 @@ def post_adoption_notice_save(pk):
notify_search_subscribers(instance, only_if_active=True) notify_search_subscribers(instance, only_if_active=True)
notify_of_AN_to_be_checked(instance) notify_of_AN_to_be_checked(instance)
@celery_app.task(name="tools.healthcheck") @celery_app.task(name="tools.healthcheck")
def task_healthcheck(): def task_healthcheck():
healthcheck_ok() healthcheck_ok()
@@ -58,9 +59,10 @@ def task_healthcheck():
def task_send_notification_email(notification_pk): def task_send_notification_email(notification_pk):
send_notification_email(notification_pk) send_notification_email(notification_pk)
@celery_app.task(name="commit.post_rescue_org_save") @celery_app.task(name="commit.post_rescue_org_save")
def post_rescue_org_save(pk): def post_rescue_org_save(pk):
instance = RescueOrganization.objects.get(pk=pk) instance = RescueOrganization.objects.get(pk=pk)
Location.add_location_to_object(instance) Location.add_location_to_object(instance)
set_timestamp("add_rescue_org_location") set_timestamp("add_rescue_org_location")
logging.info(f"Location was added to Rescue Organization {pk}") logging.info(f"Location was added to Rescue Organization {pk}")

View File

@@ -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_full_url }}" class="cta-button">{% translate 'Vermittlung anzeigen' %}</a>
</p>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% 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_full_url }}" class="cta-button">{% translate 'Vermittlung anzeigen' %}</a>
</p>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% 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_full_url }}" class="cta-button">{% translate 'Vermittlung anzeigen' %}</a>
</p>
{% endblock %}

View File

@@ -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_full_url }}" class="cta-button">{% translate 'Vermittlung anzeigen' %}</a>
</p>
{% endblock %}

View File

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

View File

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

View File

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

View File

@@ -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.",
)

View File

@@ -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.")

View File

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

View File

@@ -145,6 +145,10 @@ MEDIA_URL = config.get("urls", "media", fallback="/media/")
# Take all three into account when modifying # Take all three into account when modifying
host = config.get("notfellchen", "host", fallback='*') host = config.get("notfellchen", "host", fallback='*')
# The base URL will be used to build URLS
# See https://forum.djangoproject.com/t/putting-full-url-link-on-email-how-to-get-current-domain-name-to-put-on-url/13806/3
base_url = config.get("notfellchen", "base_url", fallback=f"https://{host}")
# see https://docs.djangoproject.com/en/3.2/ref/settings/#std-setting-ALLOWED_HOSTS # see https://docs.djangoproject.com/en/3.2/ref/settings/#std-setting-ALLOWED_HOSTS
ALLOWED_HOSTS = [host] ALLOWED_HOSTS = [host]
CSRF_TRUSTED_ORIGINS = [f"https://{host}"] CSRF_TRUSTED_ORIGINS = [f"https://{host}"]

View File

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