Compare commits

...

44 Commits

Author SHA1 Message Date
2589f1c703 fix: Make sure rescue orgs with ans only in hierarch show correctly
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-07-22 13:25:47 +02:00
0edb9094c4 feat: Show parent org 2025-07-21 17:11:13 +02:00
bc8feba701 feat: Show all adoption notices of rescue org and children 2025-07-20 16:58:18 +02:00
f37d74a7d1 feat: Add child orgs to org detail page 2025-07-20 16:29:56 +02:00
fa8612ad1a fix: ensure location is displayed 2025-07-20 16:29:27 +02:00
1d8a054b06 feat: Add number of animals per sex to metrics 2025-07-20 16:09:59 +02:00
5898fbf86d feat: Add number of animals per sex to metrics 2025-07-20 15:43:59 +02:00
cd1cdd2e0b feat: Add link to admin interface 2025-07-20 15:32:13 +02:00
c0f920544b feat: Add automatic post in the evening 2025-07-20 13:49:24 +02:00
36c90531a8 feat: Add option to switch between normal and dq 2025-07-20 13:42:56 +02:00
7f7c5a3b04 fix: post all available pictures 2025-07-20 08:50:00 +02:00
c084e56ad8 chore: Bump version to 1.1.0 2025-07-20 07:59:51 +02:00
84acc3c76e feat: format posts in markdown 2025-07-20 07:58:36 +02:00
e1f0014898 feat: Add "Post to Fediverse" 2025-07-20 07:07:33 +02:00
05b3a470f3 feat: Add warning about deactivated ANs 2025-07-19 09:29:15 +02:00
ebe060646a trans: Add a few translations 2025-07-17 15:57:22 +02:00
bb412be8d3 feat: Add view for specialized rescues 2025-07-14 07:31:08 +02:00
e3c48eac24 feat: fail more gracefully 2025-07-14 07:16:17 +02:00
da89cdceda feat: use simpler m2m relationship for specialization 2025-07-14 07:15:44 +02:00
5a6c2c99e5 feat: add important locations and buying to sitemap and fix 2025-07-14 06:33:12 +02:00
9f53836ce8 feat: add page dedicated to buying animals 2025-07-14 06:18:56 +02:00
5d53d1a1dc feat: add a default order for rescue orgs (very useful when adding ANs to it) 2025-07-13 13:01:58 +02:00
e00dda1dc2 feat: add option to mark a rescue org to be in active communication
That enables to filter them out from a check without forgetting there are to-dos. Most often this will be used when you want to call a rescue org but they can currently not be reached
2025-07-13 12:58:14 +02:00
a93e0c819f fix: use correct user 2025-07-13 12:08:42 +02:00
c87733b37a feat: Open shelters in new tab 2025-07-13 11:02:00 +02:00
9aa964bf05 feat: style external site warning 2025-07-13 10:30:51 +02:00
dcb1d3ec15 feat: Add functionality to deactivate AN with reason 2025-07-13 10:06:13 +02:00
5d9b8f3213 feat: Re-add functionality to set AN as checked 2025-07-13 09:12:24 +02:00
d12989d195 feat: Add display of when last checked 2025-07-13 09:11:51 +02:00
a9f384b50e feat: add subscribe functionality again 2025-07-13 01:08:43 +02:00
afedf2d0bd feat: add button to mark all notifications as read and fix action 2025-07-13 00:31:21 +02:00
a4b8486bd4 feat: make use of new notification mapping 2025-07-13 00:00:30 +02:00
d8bcb8ece6 refactor: remove redundant imports 2025-07-12 17:45:21 +02:00
b01ac219a3 feat: Add notification partials including new mapping system for templates 2025-07-12 17:43:40 +02:00
42320866c4 feat: Show nicer time of creating the notification 2025-07-12 16:46:56 +02:00
e2e6c14d57 feat: Show newest notification first 2025-07-12 16:46:17 +02:00
4761c38cd2 feat: Move cards to bulma notifications 2025-07-12 16:31:15 +02:00
e2bef3efe2 feat: Add dedicated notification page 2025-07-12 14:01:40 +02:00
bbfd4c3800 feat: re-add notification badge 2025-07-12 13:34:18 +02:00
b671d8fbb4 fix: fix filters 2025-07-12 13:34:02 +02:00
1ea04e98e8 fix: fucking wired bug where the url is not displayed in plaintext
that how an e-mail looked before:
Moin,

es wurde ein neuer Useraccount erstellt.

[admin] sgsfg (2025-07-12 09:44:27.251212+00:00)
Related:

http://localhost:8000/user/9/
User anzeigen:

---
2025-07-12 11:46:47 +02:00
c1a7d6790b feat: Reorder notification fields for nicer display in admin 2025-07-12 11:25:08 +02:00
f519f78922 feat: Add unsubscribe link 2025-07-12 11:24:44 +02:00
551b5ed6be feat: add plaintext versions of the e-mail 2025-07-12 09:17:58 +02:00
57 changed files with 1094 additions and 219 deletions

View File

@@ -178,8 +178,13 @@ def create_location(tierheim, instance, headers):
location_result = requests.post(f"{instance}/api/locations/", json=location_data, headers=headers) location_result = requests.post(f"{instance}/api/locations/", json=location_data, headers=headers)
if location_result.status_code != 201: if location_result.status_code != 201:
print( try:
f"Location for {tierheim["properties"]["name"]}:{location_result.status_code} {location_result.json()} not created") print(
f"Location for {tierheim["properties"]["name"]}:{location_result.status_code} {location_result.json()} not created")
except requests.exceptions.JSONDecodeError:
print(f"Location for {tierheim["properties"]["name"]} could not be created")
exit()
return location_result.json() return location_result.json()

View File

@@ -8,7 +8,7 @@ from django.urls import reverse
from django.utils.http import urlencode from django.utils.http import urlencode
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \ from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
SpeciesSpecificURL, ImportantLocation, SpeciesSpecialization SpeciesSpecificURL, ImportantLocation, SocialMediaPost
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, Notification Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, Notification
@@ -100,11 +100,6 @@ class SpeciesSpecificURLInline(admin.StackedInline):
model = SpeciesSpecificURL model = SpeciesSpecificURL
class SpeciesSpecializationInline(admin.StackedInline):
model = SpeciesSpecialization
extra = 0
@admin.register(RescueOrganization) @admin.register(RescueOrganization)
class RescueOrganizationAdmin(admin.ModelAdmin): class RescueOrganizationAdmin(admin.ModelAdmin):
search_fields = ("name", "description", "internal_comment", "location_string", "location__city") search_fields = ("name", "description", "internal_comment", "location_string", "location__city")
@@ -112,7 +107,6 @@ class RescueOrganizationAdmin(admin.ModelAdmin):
list_filter = ("allows_using_materials", "trusted", ("external_source_identifier", EmptyFieldListFilter)) list_filter = ("allows_using_materials", "trusted", ("external_source_identifier", EmptyFieldListFilter))
inlines = [ inlines = [
SpeciesSpecializationInline,
SpeciesSpecificURLInline, SpeciesSpecificURLInline,
] ]
@@ -168,6 +162,9 @@ class LocationAdmin(admin.ModelAdmin):
ImportantLocationInline, ImportantLocationInline,
] ]
@admin.register(SocialMediaPost)
class SocialMediaPostAdmin(admin.ModelAdmin):
list_filter = ("platform",)
admin.site.register(Animal) admin.site.register(Animal)
admin.site.register(Species) admin.site.register(Species)

View File

@@ -360,9 +360,9 @@ class LocationApiView(APIView):
# Log the action # Log the action
Log.objects.create( Log.objects.create(
user=request.user_to_notify, user=request.user,
action="add_location", action="add_location",
text=f"{request.user_to_notify} added adoption notice {location.pk} via API", text=f"{request.user} 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

@@ -7,29 +7,27 @@ 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, Notification, TrustLevel, NotificationTypeChoices from fellchensammlung.models import User, Notification, TrustLevel, NotificationTypeChoices
from notfellchen.settings import base_url from fellchensammlung.tools.model_helpers import ndm
NEWLINE = "\r\n"
def mail_admins_new_report(report): def notify_mods_new_report(report, notification_type):
""" """
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 = base_url + report.get_absolute_url() if notification_type == NotificationTypeChoices.NEW_REPORT_AN:
context = {"report_url": report_url, title = _("Vermittlung gemeldet")
"user_comment": report.user_comment, } elif notification_type == NotificationTypeChoices.NEW_COMMENT:
title = _("Kommentar gemeldet")
subject = _("Neue Meldung") else:
html_message = render_to_string('fellchensammlung/mail/notifications/report.html', context) raise NotImplementedError
plain_message = strip_tags(html_message) notification = Notification.objects.create(
notification_type=notification_type,
mail.send_mail(subject, user_to_notify=moderator,
plain_message, report=report,
from_email="info@notfellchen.org", title=title,
recipient_list=[moderator.email], )
html_message=html_message) notification.save()
def send_notification_email(notification_pk): def send_notification_email(notification_pk):
@@ -37,24 +35,9 @@ def send_notification_email(notification_pk):
subject = f"{notification.title}" subject = f"{notification.title}"
context = {"notification": notification, } context = {"notification": notification, }
if notification.notification_type == NotificationTypeChoices.NEW_REPORT_COMMENT or notification.notification_type == NotificationTypeChoices.NEW_REPORT_AN: html_message = render_to_string(ndm[notification.notification_type].email_html_template, context)
context["user_comment"] = notification.report.user_comment plain_message = render_to_string(ndm[notification.notification_type].email_plain_template, context)
context["report_url"] = f"{base_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/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, mail.send_mail(subject, plain_message, settings.DEFAULT_FROM_EMAIL,
[notification.user_to_notify.email], [notification.user_to_notify.email],
html_message=html_message) html_message=html_message)

View File

@@ -0,0 +1,25 @@
# Generated by Django 5.2.1 on 2025-07-13 10:54
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0054_alter_notification_comment'),
]
operations = [
migrations.AddField(
model_name='rescueorganization',
name='ongoing_communication',
field=models.BooleanField(default=False, help_text='Es findet gerade Kommunikation zwischen Notfellchen und der Organisation statt.', verbose_name='In aktiver Kommunikation'),
),
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='Empfänger*in'),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.2.1 on 2025-07-14 05:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0055_rescueorganization_ongoing_communication_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='rescueorganization',
options={'ordering': ['name']},
),
migrations.AddField(
model_name='rescueorganization',
name='specializations',
field=models.ManyToManyField(to='fellchensammlung.species'),
),
]

View File

@@ -0,0 +1,16 @@
# Generated by Django 5.2.1 on 2025-07-14 05:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0056_alter_rescueorganization_options_and_more'),
]
operations = [
migrations.DeleteModel(
name='SpeciesSpecialization',
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 5.2.1 on 2025-07-19 17:48
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0057_delete_speciesspecialization'),
]
operations = [
migrations.CreateModel(
name='SocialMediaPost',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateField(default=django.utils.timezone.now, verbose_name='Erstellt am')),
('platform', models.CharField(choices=[('fediverse', 'Fediverse')], max_length=255, verbose_name='Social Media Platform')),
('url', models.URLField(verbose_name='URL')),
('adoption_notice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')),
],
),
]

View File

@@ -17,6 +17,8 @@ from .tools import misc, geo
from notfellchen.settings import MEDIA_URL, base_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
from .tools.model_helpers import NotificationTypeChoices
from .tools.model_helpers import ndm as NotificationDisplayMapping
class Language(models.Model): class Language(models.Model):
@@ -105,6 +107,9 @@ class ImportantLocation(models.Model):
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
def get_absolute_url(self):
return reverse('search-by-location', kwargs={'important_location_slug': self.slug})
class ExternalSourceChoices(models.TextChoices): class ExternalSourceChoices(models.TextChoices):
OSM = "OSM", _("Open Street Map") OSM = "OSM", _("Open Street Map")
@@ -118,10 +123,23 @@ class AllowUseOfMaterialsChices(models.TextChoices):
USE_MATERIALS_NOT_ASKED = "not_asked", _("Not asked") USE_MATERIALS_NOT_ASKED = "not_asked", _("Not asked")
class RescueOrganization(models.Model): class Species(models.Model):
def __str__(self): """Model representing a species of animal."""
return f"{self.name}" name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
verbose_name=_('Name'))
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
"""String for representing the Model object."""
return self.name
class Meta:
verbose_name = _('Tierart')
verbose_name_plural = _('Tierarten')
class RescueOrganization(models.Model):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig')) trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
allows_using_materials = models.CharField(max_length=200, allows_using_materials = models.CharField(max_length=200,
@@ -149,10 +167,19 @@ class RescueOrganization(models.Model):
exclude_from_check = models.BooleanField(default=False, verbose_name=_('Von Prüfung ausschließen'), exclude_from_check = models.BooleanField(default=False, verbose_name=_('Von Prüfung ausschließen'),
help_text=_("Organisation von der manuellen Überprüfung ausschließen, " help_text=_("Organisation von der manuellen Überprüfung ausschließen, "
"z.B. weil Tiere nicht online geführt werden")) "z.B. weil Tiere nicht online geführt werden"))
ongoing_communication = models.BooleanField(default=False, verbose_name=_('In aktiver Kommunikation'),
help_text=_(
"Es findet gerade Kommunikation zwischen Notfellchen und der Organisation statt."))
parent_org = models.ForeignKey("RescueOrganization", on_delete=models.PROTECT, blank=True, null=True) parent_org = models.ForeignKey("RescueOrganization", on_delete=models.PROTECT, blank=True, null=True)
# allows to specify if a rescue organization has a specialization for dedicated species
specializations = models.ManyToManyField(Species, blank=True)
class Meta: class Meta:
unique_together = ('external_object_identifier', 'external_source_identifier',) unique_together = ('external_object_identifier', 'external_source_identifier',)
ordering = ['name']
def __str__(self):
return f"{self.name}"
def clean(self): def clean(self):
super().clean() super().clean()
@@ -166,6 +193,17 @@ class RescueOrganization(models.Model):
def adoption_notices(self): def adoption_notices(self):
return AdoptionNotice.objects.filter(organization=self) return AdoptionNotice.objects.filter(organization=self)
@property
def adoption_notices_in_hierarchy(self):
"""
Shows all adoption notices of this rescue organization and all child organizations.
"""
adoption_notices_discovered = list(self.adoption_notices)
if self.child_organizations:
for child in self.child_organizations:
adoption_notices_discovered.extend(child.adoption_notices_in_hierarchy)
return adoption_notices_discovered
@property @property
def position(self): def position(self):
if self.location: if self.location:
@@ -206,6 +244,10 @@ class RescueOrganization(models.Model):
self.exclude_from_check = True self.exclude_from_check = True
self.save() self.save()
@property
def child_organizations(self):
return RescueOrganization.objects.filter(parent_org=self)
# Admins can perform all actions and have the highest trust associated with them # Admins can perform all actions and have the highest trust associated with them
# Moderators can make moderation decisions regarding the deletion of content # Moderators can make moderation decisions regarding the deletion of content
@@ -252,14 +294,17 @@ class User(AbstractUser):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("user-detail", args=[str(self.pk)]) return reverse("user-detail", args=[str(self.pk)])
def get_full_url(self):
return f"{base_url}{self.get_absolute_url()}"
def get_notifications_url(self): def get_notifications_url(self):
return self.get_absolute_url() return self.get_absolute_url()
def get_unread_notifications(self): def get_unread_notifications(self):
return Notification.objects.filter(user=self, read=False) return Notification.objects.filter(user_to_notify=self, read=False)
def get_num_unread_notifications(self): def get_num_unread_notifications(self):
return Notification.objects.filter(user=self, read=False).count() return Notification.objects.filter(user_to_notify=self, read=False).count()
@property @property
def adoption_notices(self): def adoption_notices(self):
@@ -285,22 +330,6 @@ class Image(models.Model):
return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">' return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">'
class Species(models.Model):
"""Model representing a species of animal."""
name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
verbose_name=_('Name'))
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
"""String for representing the Model object."""
return self.name
class Meta:
verbose_name = _('Tierart')
verbose_name_plural = _('Tierarten')
class AdoptionNotice(models.Model): class AdoptionNotice(models.Model):
class Meta: class Meta:
permissions = [ permissions = [
@@ -346,7 +375,7 @@ class AdoptionNotice(models.Model):
def num_per_sex(self): def num_per_sex(self):
num_per_sex = dict() num_per_sex = dict()
for sex in SexChoices: for sex in SexChoices:
num_per_sex[sex] = self.animals.filter(sex=sex).count num_per_sex[sex] = self.animals.filter(sex=sex).count()
return num_per_sex return num_per_sex
@property @property
@@ -455,16 +484,28 @@ class AdoptionNotice(models.Model):
return False return False
return self.adoptionnoticestatus.is_active return self.adoptionnoticestatus.is_active
@property
def is_disabled(self):
if not hasattr(self, 'adoptionnoticestatus'):
return False
return self.adoptionnoticestatus.is_disabled
@property
def is_closed(self):
if not hasattr(self, 'adoptionnoticestatus'):
return False
return self.adoptionnoticestatus.is_closed
@property @property
def is_disabled_unchecked(self): def is_disabled_unchecked(self):
if not hasattr(self, 'adoptionnoticestatus'): if not hasattr(self, 'adoptionnoticestatus'):
return False return False
return self.adoptionnoticestatus.is_disabled_unchecked return self.adoptionnoticestatus.is_disabled_unchecked
def set_closed(self): def set_closed(self, minor_status=None):
self.last_checked = timezone.now() self.last_checked = timezone.now()
self.save() self.save()
self.adoptionnoticestatus.set_closed() self.adoptionnoticestatus.set_closed(minor_status)
def set_active(self): def set_active(self):
self.last_checked = timezone.now() self.last_checked = timezone.now()
@@ -489,6 +530,14 @@ class AdoptionNotice(models.Model):
text=text, text=text,
title=notification_title) title=notification_title)
def last_posted(self, platform=None):
if platform is None:
last_post = SocialMediaPost.objects.filter(adoption_notice=self).order_by('-created_at').first()
else:
last_post = SocialMediaPost.objects.filter(adoption_notice=self, platform=platform).order_by(
'-created_at').first()
return last_post.created_at
class AdoptionNoticeStatus(models.Model): class AdoptionNoticeStatus(models.Model):
""" """
@@ -552,6 +601,14 @@ class AdoptionNoticeStatus(models.Model):
def is_active(self): def is_active(self):
return self.major_status == self.ACTIVE return self.major_status == self.ACTIVE
@property
def is_disabled(self):
return self.major_status == self.DISABLED
@property
def is_closed(self):
return self.major_status == self.CLOSED
@property @property
def is_disabled_unchecked(self): def is_disabled_unchecked(self):
return self.major_status == self.DISABLED and self.minor_status == "unchecked" return self.major_status == self.DISABLED and self.minor_status == "unchecked"
@@ -569,9 +626,12 @@ class AdoptionNoticeStatus(models.Model):
minor_status=minor_status, minor_status=minor_status,
adoption_notice=an_instance) adoption_notice=an_instance)
def set_closed(self): def set_closed(self, minor_status=None):
self.major_status = self.MAJOR_STATUS_CHOICES[self.CLOSED] self.major_status = self.MAJOR_STATUS_CHOICES[self.CLOSED]
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"] if minor_status is None:
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"]
else:
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED][minor_status]
self.save() self.save()
def set_unchecked(self): def set_unchecked(self):
@@ -727,6 +787,9 @@ class Report(models.Model):
"""Returns the url to access a detailed page for the report.""" """Returns the url to access a detailed page for the report."""
return reverse('report-detail', args=[str(self.id)]) return reverse('report-detail', args=[str(self.id)])
def get_full_url(self):
return f"{base_url}{self.get_absolute_url()}"
def get_reported_rules(self): def get_reported_rules(self):
return self.reported_broken_rules.all() return self.reported_broken_rules.all()
@@ -910,33 +973,24 @@ class Comment(models.Model):
return self.adoption_notice.get_absolute_url() return self.adoption_notice.get_absolute_url()
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): 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"))
notification_type = models.CharField(max_length=200, notification_type = models.CharField(max_length=200,
choices=NotificationTypeChoices.choices, choices=NotificationTypeChoices.choices,
verbose_name=_('Benachrichtigungsgrund')) verbose_name=_('Benachrichtigungsgrund'))
title = models.CharField(max_length=100, verbose_name=_("Titel"))
text = models.TextField(verbose_name="Inhalt")
user_to_notify = models.ForeignKey(User, user_to_notify = models.ForeignKey(User,
on_delete=models.CASCADE, on_delete=models.CASCADE,
verbose_name=_('Nutzer*in'), verbose_name=_('Empfänger*in'),
help_text=_("Useraccount der Benachrichtigt wird"), help_text=_("Useraccount der Benachrichtigt wird"),
related_name='user') related_name='user')
title = models.CharField(max_length=100, verbose_name=_("Titel"))
text = models.TextField(verbose_name="Inhalt")
read = models.BooleanField(default=False) read = models.BooleanField(default=False)
read_at = models.DateTimeField(blank=True, null=True, verbose_name=_("Gelesen am"))
comment = models.ForeignKey(Comment, blank=True, null=True, on_delete=models.CASCADE, verbose_name=_('Antwort')) 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')) adoption_notice = models.ForeignKey(AdoptionNotice, blank=True, null=True, on_delete=models.CASCADE,
verbose_name=_('Vermittlung'))
user_related = models.ForeignKey(User, user_related = models.ForeignKey(User,
blank=True, null=True, blank=True, null=True,
on_delete=models.CASCADE, verbose_name=_('Verwandter Useraccount'), on_delete=models.CASCADE, verbose_name=_('Verwandter Useraccount'),
@@ -958,6 +1012,9 @@ class Notification(models.Model):
self.read_at = timezone.now() self.read_at = timezone.now()
self.save() self.save()
def get_body_part(self):
return NotificationDisplayMapping[self.notification_type].web_partial
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'))
@@ -1005,13 +1062,22 @@ class SpeciesSpecificURL(models.Model):
url = models.URLField(verbose_name=_("Tierartspezifische URL")) url = models.URLField(verbose_name=_("Tierartspezifische URL"))
class SpeciesSpecialization(models.Model): class PlatformChoices(models.TextChoices):
""" FEDIVERSE = "fediverse", _("Fediverse")
Model that allows to specify if a rescue organization has a specialization for dedicated species
"""
species = models.ForeignKey(Species, on_delete=models.CASCADE, verbose_name=_("Tierart")) class SocialMediaPost(models.Model):
rescue_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE, created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
verbose_name=_("Tierschutzorganisation")) platform = models.CharField(max_length=255, verbose_name=_("Social Media Platform"),
choices=PlatformChoices.choices)
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
url = models.URLField(verbose_name=_("URL"))
@staticmethod
def get_an_to_post():
adoption_notices_without_post = AdoptionNotice.objects.filter(socialmediapost__isnull=True,
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE)
return adoption_notices_without_post.first()
def __str__(self): def __str__(self):
return f"{_('Spezialisierung')} {self.species}" return f"{self.platform} - {self.adoption_notice}"

View File

@@ -1,6 +1,6 @@
from django.contrib.sitemaps import Sitemap from django.contrib.sitemaps import Sitemap
from django.urls import reverse from django.urls import reverse
from .models import AdoptionNotice, RescueOrganization from .models import AdoptionNotice, RescueOrganization, ImportantLocation, Animal
class StaticViewSitemap(Sitemap): class StaticViewSitemap(Sitemap):
@@ -8,7 +8,8 @@ class StaticViewSitemap(Sitemap):
changefreq = "weekly" changefreq = "weekly"
def items(self): def items(self):
return ["index", "search", "map", "about", "rescue-organizations"] return ["index", "search", "map", "about", "rescue-organizations", "buying", "imprint", "terms-of-service",
"privacy"]
def location(self, item): def location(self, item):
return reverse(item) return reverse(item)
@@ -25,17 +26,6 @@ class AdoptionNoticeSitemap(Sitemap):
return obj.updated_at return obj.updated_at
class AnimalSitemap(Sitemap):
priority = 0.2
changefreq = "daily"
def items(self):
return AdoptionNotice.objects.all()
def lastmod(self, obj):
return obj.updated_at
class RescueOrganizationSitemap(Sitemap): class RescueOrganizationSitemap(Sitemap):
priority = 0.3 priority = 0.3
changefreq = "weekly" changefreq = "weekly"
@@ -45,3 +35,11 @@ class RescueOrganizationSitemap(Sitemap):
def lastmod(self, obj): def lastmod(self, obj):
return obj.updated_at return obj.updated_at
class SearchSitemap(Sitemap):
priority = 0.5
chanfreq = "daily"
def items(self):
return ImportantLocation.objects.all()

View File

@@ -320,3 +320,29 @@ AN Cards
background-color: var(--bulma-success-on-scheme); background-color: var(--bulma-success-on-scheme);
} }
.notification-container {
display: inline-block;
position: relative;
padding: 0;
}
.notification-label {
padding: 2px 5px;
}
/* Make the badge float in the top right corner of the button */
.notification-badge {
background-color: #fa3e3e;
border-radius: 2px;
color: white;
padding: 1px 3px;
font-size: 8px;
position: absolute; /* Position the badge within the relatively positioned button */
top: 0;
right: 0;
}

View File

@@ -5,8 +5,9 @@ from django.utils import timezone
from notfellchen.celery import app as celery_app from notfellchen.celery import app as celery_app
from .mail import send_notification_email from .mail import send_notification_email
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices
from .tools.fedi import post_an_to_fedi
from .tools.misc import healthcheck_ok from .tools.misc import healthcheck_ok
from .models import Location, AdoptionNotice, Timestamp, RescueOrganization from .models import Location, AdoptionNotice, Timestamp, RescueOrganization, SocialMediaPost
from .tools.notifications import notify_of_AN_to_be_checked from .tools.notifications import notify_of_AN_to_be_checked
from .tools.search import notify_search_subscribers from .tools.search import notify_search_subscribers
@@ -38,6 +39,13 @@ def task_deactivate_unchecked():
set_timestamp("task_deactivate_404_adoption_notices") set_timestamp("task_deactivate_404_adoption_notices")
@celery_app.task(name="social_media.post_fedi")
def task_post_to_fedi():
adoption_notice = SocialMediaPost.get_an_to_post()
post_an_to_fedi(adoption_notice)
set_timestamp("task_social_media.post_fedi")
@celery_app.task(name="commit.post_an_save") @celery_app.task(name="commit.post_an_save")
def post_adoption_notice_save(pk): def post_adoption_notice_save(pk):
instance = AdoptionNotice.objects.get(pk=pk) instance = AdoptionNotice.objects.get(pk=pk)

View File

@@ -1,5 +1,6 @@
{% extends "fellchensammlung/base.html" %} {% extends "fellchensammlung/base.html" %}
{% load custom_tags %} {% load custom_tags %}
{% load admin_urls %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
@@ -25,6 +26,20 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if adoption_notice.is_closed %}
<article class="message is-warning">
<div class="message-header">
<p>{% translate 'Vermittlung deaktiviert' %}</p>
</div>
<div class="message-body content">
{% blocktranslate %}
Diese Vermittlung wurde deaktiviert. Typischerweise passiert das, wenn die Tiere erfolgreich
vermittelt wurden.
In den Kommentaren findest du ggf. mehr Informationen.
{% endblocktranslate %}
</div>
</article>
{% endif %}
<div class="columns"> <div class="columns">
<div class="column is-two-thirds"> <div class="column is-two-thirds">
<!--- Title level (including action dropdown) --> <!--- Title level (including action dropdown) -->
@@ -49,30 +64,59 @@
<!--- Action menu (dropdown) ---> <!--- Action menu (dropdown) --->
<div class="dropdown-menu" role="menu"> <div class="dropdown-menu" role="menu">
<div class="dropdown-content"> <div class="dropdown-content">
{% if is_subscribed %}
<form class="dropdown-item" method="POST">
{% csrf_token %}
<input type="hidden" name="action" value="unsubscribe">
<button type="submit" id="submit">
<i class="fas fa-bell-slash fa-fw"
aria-hidden="true"></i> {% trans 'Deabonnieren' %}
</button>
</form>
{% else %}
<form class="dropdown-item" method="POST">
{% csrf_token %}
<input type="hidden" name="action" value="subscribe">
<button type="submit" id="submit">
<i class="fas fa-bell fa-fw"
aria-hidden="true"></i> {% trans 'Abonnieren' %}
</button>
</form>
{% endif %}
<hr class="dropdown-divider">
{% if has_edit_permission %} {% if has_edit_permission %}
<a class="dropdown-item"> <form class="dropdown-item" method="POST">
<i class="fas fa-check" {% csrf_token %}
aria-hidden="true"></i> {% trans 'Als aktiv bestätigen' %} <input type="hidden" name="action" value="checked_active">
</a> <button type="submit" id="submit">
<i class="fas fa-check fa-fw"
aria-hidden="true"></i> {% trans 'Als aktiv bestätigen' %}
</button>
</form>
<a class="dropdown-item" <a class="dropdown-item"
href="{% url 'adoption-notice-edit' adoption_notice_id=adoption_notice.pk %}"> href="{% url 'adoption-notice-edit' adoption_notice_id=adoption_notice.pk %}">
<i class="fas fa-pencil" <i class="fas fa-pencil fa-fw"
aria-hidden="true"></i> {% translate 'Bearbeiten' %} aria-hidden="true"></i> {% translate 'Bearbeiten' %}
</a> </a>
<a class="dropdown-item" <a class="dropdown-item"
href="{% url 'adoption-notice-add-photo' adoption_notice.pk %}"> href="{% url 'adoption-notice-add-photo' adoption_notice.pk %}">
<i class="fas fa-image" <i class="fas fa-image fa-fw"
aria-hidden="true"></i> {% trans 'Bilder hinzufügen' %} aria-hidden="true"></i> {% trans 'Bilder hinzufügen' %}
</a> </a>
<a class="dropdown-item" <a class="dropdown-item"
href="{% url 'adoption-notice-add-animal' adoption_notice.pk %}"> href="{% url 'adoption-notice-add-animal' adoption_notice.pk %}">
<i class="fas fa-plus" <i class="fas fa-plus fa-fw"
aria-hidden="true"></i> {% trans 'Tier hinzufügen' %} aria-hidden="true"></i> {% trans 'Tier hinzufügen' %}
</a> </a>
<a class="dropdown-item"> <a class="dropdown-item"
<i class="fas fa-circle-xmark" href="{% url 'adoption-notice-close' adoption_notice_id=adoption_notice.pk %}">
<i class="fas fa-circle-xmark fa-fw"
aria-hidden="true"></i> {% trans 'Deaktivieren' %} aria-hidden="true"></i> {% trans 'Deaktivieren' %}
</a> </a>
<hr class="dropdown-divider"> <hr class="dropdown-divider">
@@ -81,6 +125,13 @@
<i class="fas fa-flag" <i class="fas fa-flag"
aria-hidden="true"></i> {% trans 'Melden' %} aria-hidden="true"></i> {% trans 'Melden' %}
</a> </a>
{% if request.user.is_superuser %}
<hr class="dropdown-divider">
<a class="dropdown-item is-warning"
href="{% url adoption_notice_meta|admin_urlname:'change' adoption_notice.pk %}">
<i class="fa-solid fa-tools fa-fw"></i> Admin interface
</a>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -169,6 +220,13 @@
{% translate "Keine Beschreibung angegeben" %} {% translate "Keine Beschreibung angegeben" %}
{% endif %} {% endif %}
</p> </p>
<hr>
<p>
<strong>
{% translate 'Zuletzt auf Aktualität überprüft:' %}
</strong>
{{ adoption_notice.last_checked|time_since_hr }}
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -35,6 +35,29 @@
<p>{{ org.description | render_markdown }}</p> <p>{{ org.description | render_markdown }}</p>
{% endif %} {% endif %}
</div> </div>
{% if org.specializations %}
<div class="block">
<h3 class="title is-5">{% translate 'Spezialisierung' %}</h3>
<div class="content">
<ul>
{% for specialization in org.specializations.all %}
<li>{{ specialization }}</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% if org.parent_org %}
<div class="block">
<h3 class="title is-5">{% translate 'Übergeordnete Organisation' %}</h3>
<p>
<span>
<i class="fa-solid fa-building fa-fw" aria-label="{% trans 'Tierschutzorganisation' %}"></i>
<a href="{{ org.parent_org.get_absolute_url }}"> {{ org.parent_org }}</a>
</span>
</p>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -42,7 +65,9 @@
{% include "fellchensammlung/partials/partial-rescue-organization-contact.html" %} {% include "fellchensammlung/partials/partial-rescue-organization-contact.html" %}
</div> </div>
<div class="block"> <div class="block">
<a class="button is-warning is-fullwidth" href="{% url org_meta|admin_urlname:'change' org.pk %}"><i class="fa-solid fa-tools fa-fw"></i> Admin interface</a> <a class="button is-warning is-fullwidth" href="{% url org_meta|admin_urlname:'change' org.pk %}">
<i class="fa-solid fa-tools fa-fw"></i> Admin interface
</a>
</div> </div>
</div> </div>
<div class="column"> <div class="column">
@@ -50,11 +75,20 @@
</div> </div>
</div> </div>
{% if org.child_organizations %}
<div class="block">
<h2 class="title is-2">{% translate 'Unterorganisationen' %}</h2>
{% with rescue_organizations=org.child_organizations %}
{% include "fellchensammlung/lists/list-animal-shelters.html" %}
{% endwith %}
</div>
{% endif %}
<h2 class="title is-2">{% translate 'Vermittlungen der Organisation' %}</h2> <h2 class="title is-2">{% translate 'Vermittlungen der Organisation' %}</h2>
<div class="container-cards"> <div class="container-cards">
{% if org.adoption_notices %} {% if org.adoption_notices_in_hierarchy %}
{% for adoption_notice in org.adoption_notices %} {% for adoption_notice in org.adoption_notices_in_hierarchy %}
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %} {% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
{% endfor %} {% endfor %}
{% else %} {% else %}

View File

@@ -0,0 +1,14 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load custom_tags %}
{% block title %}<title>{% translate "403 Forbidden" %}</title>{% endblock %}
{% block content %}
<h1 class="title is-1">404 Not Found</h1>
<p>
{% blocktranslate %}
Diese Seite existiert nicht.
{% endblocktranslate %}
</p>
{% endblock %}

View File

@@ -1,15 +1,28 @@
{% extends "fellchensammlung/base.html" %} {% extends "fellchensammlung/base.html" %}
{% load i18n %} {% load i18n %}
{% load custom_tags %} {% load custom_tags %}
{% block content %} {% block content %}
<div class="content"> <div class="block">
{% if external_site_warning %} <div class="message is-warning">
{{ external_site_warning.content | render_markdown }} {% if external_site_warning %}
{% else %} <h1 class="message-header">
{% blocktranslate %} {{ external_site_warning.title }}
<p>Achtung du verlässt notfellchen.org</p> </h1>
{% endblocktranslate %} <div class="message-body">
{% endif %} {{ external_site_warning.content | render_markdown }}
<a href="{{ url }}" class="button is-primary">{% translate "Weiter" %}</a> </div>
{% else %}
<h1 class="message-header">
{% trans 'Achtung du verlässt notfellchen.org' %}
</h1>
<div class="message-body">
{% trans 'Sichere Abgabebedingungen können von uns, trotz vieler Bemühungen, nicht garantiert werden. Nimm Kontakt zu einer Rattenhilfe oder dem VdRD e.V. auf, die dich beraten können.' %}
</div>
{% endif %}
</div>
<div class="block">
<a href="{{ url }}" class="button is-primary is-fullwidth">{% translate "Weiter" %}<i class="fa fa-arrow-right fa-fw" aria-hidden="true"></i> </a>
</div>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@@ -34,6 +34,10 @@
{% translate 'Das Notfellchen Projekt' %} {% translate 'Das Notfellchen Projekt' %}
</a> </a>
<br/> <br/>
<a href="{% url "buying" %}">
{% translate 'Ratten kaufen' %}
</a>
<br/>
<a href="{% url "terms-of-service" %}"> <a href="{% url "terms-of-service" %}">
{% translate 'Nutzungsbedingungen' %} {% translate 'Nutzungsbedingungen' %}
</a> </a>

View File

@@ -9,7 +9,8 @@
<h1 class="title is-4">notfellchen.org</h1> <h1 class="title is-4">notfellchen.org</h1>
</a> </a>
<a role="button" class="navbar-burger" aria-label="{% trans 'Hauptmenü' %}" tabindex="0" aria-expanded="false" data-target="navbarBasicExample"> <a role="button" class="navbar-burger" aria-label="{% trans 'Hauptmenü' %}" tabindex="0" aria-expanded="false"
data-target="navbarBasicExample">
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
@@ -30,7 +31,16 @@
</div> </div>
<div class="navbar-end"> <div class="navbar-end">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<div class="navbar-item">
<div class="notification-container">
<a class="notification-label" href="{% url 'user-notifications' %}">
<i class="fas fa-bell fa-fw"></i>
</a>
{% if request.user.get_num_unread_notifications > 0 %}
<span class="notification-badge">{{ request.user.get_num_unread_notifications }}</span>
{% endif %}
</div>
</div>
<div class="navbar-item"> <div class="navbar-item">
<a href="{% url 'user-me' %}"> <a href="{% url 'user-me' %}">
<i class="fas fa-user fa-fw"></i> {{ user }} <i class="fas fa-user fa-fw"></i> {{ user }}

View File

@@ -54,6 +54,11 @@
font-size: 14px; font-size: 14px;
} }
.setting-info {
font-size: 10px;
}
@media (max-width: 600px) { @media (max-width: 600px) {
.content, .header, .footer { .content, .header, .footer {
padding: 20px 15px; padding: 20px 15px;

View File

@@ -0,0 +1,4 @@
{% block content %}{% endblock %}
---
{% include "fellchensammlung/mail/footer.txt" %}

View File

@@ -1,3 +1,12 @@
{% load i18n %}
<div class="footer"> <div class="footer">
🐀 notfellchen.org | Für Menschen die Ratten aus dem Tierschutz ein liebendes Zuhause geben wollen. 🐀 notfellchen.org | Für Menschen die Ratten aus dem Tierschutz ein liebendes Zuhause geben wollen.
</div> {% if notification %}
<div class="setting-info">
{% trans "Du bekommst diese Nachricht basierend auf deinen Benachrichtigungseinstellungen." %}<br>
<a href="{{ notification.user_to_notify.get_full_url }}">
{% trans "Einstellungen ändern" %}
</a>
</div>
{% endif %}
</div>

View File

@@ -0,0 +1,7 @@
{% load i18n %}
🐀 notfellchen.org | Für Menschen die Ratten aus dem Tierschutz ein liebendes Zuhause geben wollen.
{% if notification %}
{% trans "Du bekommst diese Nachricht basierend auf deinen Benachrichtigungseinstellungen." %}
{% trans "Einstellungen ändern" %}: {{ notification.user_to_notify.get_full_url }}
{% endif %}

View File

@@ -0,0 +1,7 @@
{% extends "fellchensammlung/mail/base.txt" %}
{% load i18n %}
{% block content %}{% blocktranslate %}Moin,
die Vermittlung {{ notification.adoption_notice }} wurde deaktiviert.
{% endblocktranslate %}
{% translate 'Vermittlung anzeigen' %}: {{ notification.adoption_notice.get_full_url }}{% endblock %}

View File

@@ -0,0 +1,9 @@
{% extends "fellchensammlung/mail/base.txt" %}
{% load i18n %}
{% block content %}{% blocktranslate %}Moin,
es wurde eine neue Vermittlung gefunden, die deinen Kriterien entspricht: {{ notification.adoption_notice }}
Vermittlung anzeigen: {{ notification.adoption_notice.get_full_url }}
{% endblocktranslate %}{% endblock %}

View File

@@ -0,0 +1,8 @@
{% extends "fellchensammlung/mail/base.txt" %}
{% load i18n %}
{% block content %}{% blocktranslate %}Moin,
die Vermittlung {{ notification.adoption_notice }} muss überprüft werden.
Vermittlung anzeigen: {{ notification.adoption_notice.get_full_url }}
{% endblocktranslate %}{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends "fellchensammlung/mail/base.html" %}
{% load i18n %}
{% load custom_tags %}
{% block content %}{% blocktranslate %}Moin,
folgender Kommentar wurde zur Vermittlung {{ notification.adoption_notice }} hinzugefügt:
{{ notification.comment.text }}
Vermittlung anzeigen: {{ notification.adoption_notice.get_full_url }}
{% endblocktranslate %}{% endblock %}

View File

@@ -14,6 +14,6 @@
Details findest du hier Details findest du hier
</p> </p>
<p> <p>
<a href="{{ notification.user_related.get_absolute_url }}" class="cta-button">{% translate 'User anzeigen' %}</a> <a href="{{ notification.user_related.get_full_url }}" class="cta-button">{% translate 'User anzeigen' %}</a>
</p> </p>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,8 @@
{% extends "fellchensammlung/mail/base.txt" %}
{% load i18n %}
{% block content %}{% blocktranslate with new_user_url=notification.user_related.get_full_url %}Moin,
es wurde ein neuer Useraccount erstellt.
User anzeigen: {{ new_user_url }}
{% endblocktranslate %}{% endblock %}

View File

@@ -5,21 +5,31 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<p>Moin,</p> <p>{% translate 'Moin' %},</p>
<p> <p>
es gibt eine neue Meldung. Folgende Nachricht wurde zur Meldung hinzugefügt: {% blocktranslate %}
es gibt eine neue Meldung.
{% endblocktranslate %}
{% if notification.report.user_comment %}
{% blocktranslate %}
Folgende Nachricht wurde zur Meldung hinzugefügt:
{% endblocktranslate %}
{% endif %}
</p> </p>
{% if notification.report.user_comment %}
<p>
<i>
{{ notification.report.user_comment }}
</i>
</p>
{% endif %}
<p> <p>
<i> {% blocktranslate %}
{{ user_comment }} Bitte bearbeite die Meldung möglichst bald.
</i> {% endblocktranslate %}
</p> </p>
<p> <p>
<a href="{{ notification.report.get_full_url }}" class="cta-button">{% translate 'Report bearbeiten' %}</a>
Bitte bearbeite die Meldung möglichst bald.
</p>
<p>
<a href="{{ report_url }}" class="cta-button">{% translate 'Report bearbeiten' %}</a>
</p> </p>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends "fellchensammlung/mail/base.txt" %}
{% load i18n %}
{% block content %}{% blocktranslate %}Moin,
es gibt eine neue Meldung. Folgende Nachricht wurde zur Meldung hinzugefügt:
{{ user_comment }}
Bitte bearbeite die Meldung möglichst bald.
Meldung bearbeiten: {{ report_url }}{% endblocktranslate %}{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load widget_tweaks %}
{% block content %}
<h1 class="title is-1">{% translate 'Vermittlung deaktivieren' %}</h1>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="field">
<label class="label" for="reason_for_closing">{% translate 'Warum schließt du die Vermittlung?' %}</label>
<div class="control">
<div class="select">
<select id="reason_for_closing" name="reason_for_closing">
<option value="successful_with_notfellchen">{% translate 'Vermittelt mit Hilfe von Notfellchen' %}</option>
<option value="successful_without_notfellchen">{% translate 'Vermittelt ohne Hilfe von Notfellchen' %}</option>
<option value="closed_for_other_adoption_notice">{% translate 'Vermittlung zugunsten einer anderen geschlossen' %}</option>
<option value="not_open_for_adoption_anymore">{% translate 'Nicht mehr zu vermitteln (z.B. aufgrund von Krankheit)' %}</option>
<option value="animal_died">{% translate 'Tod des Tiers' %}</option>
<option value="other">{% translate 'Anderer Grund' %}</option>
</select>
</div>
</div>
</div>
<input class="button is-warning" type="submit" value="{% translate "Vermittlung deaktivieren" %}">
</form>
{% endblock %}

View File

@@ -0,0 +1,8 @@
### {{ adoption_notice.name }}
📍 {{ adoption_notice.location }}
{{ adoption_notice.description }}
{{ adoption_notice.get_full_url}}

View File

@@ -7,13 +7,52 @@
<div class="block"> <div class="block">
<h1 class="title is-1">{% translate 'Moderationstools' %}</h1> <h1 class="title is-1">{% translate 'Moderationstools' %}</h1>
<div class="block"> <div class="block">
<a class="button is-primary is-fullwidth" href="{% url 'modqueue' %}">{% translate 'Moderationswarteschlange' %}</a> <a class="button is-primary is-fullwidth"
href="{% url 'modqueue' %}">{% translate 'Moderationswarteschlange' %}</a>
</div> </div>
<div class="block"> <div class="block">
<a class="button is-primary is-fullwidth" href="{% url 'updatequeue' %}">{% translate 'Up-To-Date Check' %}</a> <a class="button is-primary is-fullwidth"
href="{% url 'updatequeue' %}">{% translate 'Up-To-Date Check' %}</a>
</div> </div>
<div class="block"> <div class="block">
<a class="button is-primary is-fullwidth" href="{% url 'organization-check' %}">{% translate 'Organisations Check' %}</a> <a class="button is-primary is-fullwidth"
href="{% url 'organization-check' %}">{% translate 'Organisations Check' %}</a>
</div>
<div class="block">
{% if action_was_posting %}
{% if posted_successfully %}
<div class="message is-success">
<div class="message-header">
{% translate 'Vermittlung gepostet' %}
</div>
<div class="message-body">
{% blocktranslate with post_url=post.url %}
Link zum Post: <a href={{ post_url }}>{{ post_url }}</a>
{% endblocktranslate %}
</div>
</div>
{% else %}
<div class="message is-danger">
<div class="message-header">
{% translate 'Vermittlung konnte nicht gepostet werden' %}
</div>
<div class="message-body">
{{ error_message }}
</div>
</div>
{% endif %}
{% endif %}
<form class="cell" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="post_to_fedi">
<button class="button is-fullwidth is-warning is-primary" type="submit" id="submit">
<i class="fa-solid fa-bullhorn fa-fw"></i> {% translate "Vermittlung ins Fediverse posten" %}
</button>
</form>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,32 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% block title %}<title>{% trans 'Benachrichtigungen' %}</title>{% endblock %}
{% block content %}
<div class="block">
<h1 class="title is-1">{% translate 'Benachrichtigungen' %}</h1>
{% if notifications_unread|length > 0 %}
<div class="block">
<form class="button is-danger" method="POST">
{% csrf_token %}
<input type="hidden" name="action" value="notification_mark_all_read">
<button class="" type="submit" id="submit">
{% trans 'Alle Benachrichtigungen als gelesen markieren' %}
</button>
</form>
</div>
{% endif %}
<div class="block">
{% with notifications=notifications_unread %}
{% include "fellchensammlung/lists/list-notifications.html" %}
{% endwith %}
</div>
</div>
<div class="block">
<h2 class="title is-2">{% translate 'Zuletzt gelesene Benachrichtigungen' %}</h2>
{% with notifications=notifications_read_last %}
{% include "fellchensammlung/lists/list-notifications.html" %}
{% endwith %}
</div>
{% endblock %}

View File

@@ -0,0 +1,5 @@
{% load i18n %}
{% blocktranslate with adoption_notice_title=notification.adoption_notice.name %}
Die Vermittlung <strong>{{ adoption_notice_title }}</strong> wurde deaktiviert.
{% endblocktranslate %}
<a href="{{ notification.adoption_notice.get_full_url }}">{% translate 'Vermittlung anzeigen' %}</a>

View File

@@ -0,0 +1,5 @@
{% load i18n %}
{% blocktranslate %}
Es wurde eine neue Vermittlung gefunden, die deinen Kriterien entspricht:
{% endblocktranslate %}
<a href="{{ notification.adoption_notice.get_full_url }}">{{ notification.adoption_notice.name }}</a>

View File

@@ -0,0 +1,5 @@
{% load i18n %}
{% blocktranslate with adoption_notice_title=notification.adoption_notice.name %}
Die Vermittlung <strong>{{ adoption_notice_title }}</strong> muss überprüft werden.
{% endblocktranslate %}
<a href="{{ notification.adoption_notice.get_full_url }}">{% translate 'Vermittlung anzeigen' %}</a>

View File

@@ -0,0 +1,13 @@
{% load i18n %}
{% load custom_tags %}
<p>
{% blocktranslate with adoption_notice_title=notification.adoption_notice.name %}
Folgender Kommentar wurde zur Vermittlung <strong>{{ adoption_notice_title }}</strong> hinzugefügt:
{% endblocktranslate %}
</p>
<p><i>
{{ notification.comment.text | render_markdown }}
</i></p>
<p>
<a href="{{ notification.adoption_notice.get_full_url }}">{% translate 'Vermittlung anzeigen' %}</a>
</p>

View File

@@ -0,0 +1,13 @@
{% load i18n %}
<p>
{% blocktranslate %}
Es gibt eine neue Meldung. Folgende Nachricht wurde zur Meldung hinzugefügt:
{% endblocktranslate %}
</p>
<p>
<i>
{{ notification.report.user_comment }}
</i>
</p>
<a href="{{ notification.report.get_absolute_url }}">{% translate 'Meldung anzeigen' %}</a>

View File

@@ -0,0 +1,5 @@
{% load i18n %}
{% blocktranslate %}
Es wurde ein neuer Useraccount erstellt:
{% endblocktranslate %}
<a href="{{ notification.user_related.get_full_url }}">{% translate 'User anzeigen' %}</a>

View File

@@ -15,13 +15,24 @@
<div class="cell"> <div class="cell">
<p> {% if adoption_notice.organization %}
<i class="fa-solid fa-location-dot fa-fw"></i> <div class="cell">
{% if adoption_notice.location %} <span>
{{ adoption_notice.location }} <i class="fa-solid fa-building fa-fw" aria-label="{% trans 'Tierschutzorganisation' %}"></i>
{% else %} <a href="{{ adoption_notice.organization.get_absolute_url }}"> {{ adoption_notice.organization }}</a>
{{ adoption_notice.location_string }} </span>
{% endif %}</p>
</div>
{% else %}
<p>
<i class="fa-solid fa-location-dot fa-fw"></i>
{% if adoption_notice.location %}
{{ adoption_notice.location }}
{% else %}
{{ adoption_notice.location_string }}
{% endif %}
</p>
{% endif %}
</div> </div>
<div class="cell"> <div class="cell">
{% include "fellchensammlung/partials/sex-overview.html" %} {% include "fellchensammlung/partials/sex-overview.html" %}

View File

@@ -4,7 +4,7 @@
<div class="card-header"> <div class="card-header">
<div class="card-header-title"> <div class="card-header-title">
<h2 class="title is-4"> <h2 class="title is-4">
<a href="{{ rescue_org.get_absolute_url }}">{{ rescue_org.name }}</a> <a href="{{ rescue_org.get_absolute_url }}" target="_blank">{{ rescue_org.name }}</a>
</h2> </h2>
</div> </div>
</div> </div>

View File

@@ -1,17 +1,17 @@
{% load i18n %} {% load i18n %}
{% load custom_tags %} {% load custom_tags %}
<div class="notification"> <div class="notification">
<form class="delete" method="POST">
{% csrf_token %}
<input type="hidden" name="action" value="notification_mark_read">
<input type="hidden" name="notification_id" value="{{ notification.pk }}">
<button class="" type="submit" id="submit"></button>
</form>
<div class="notification-header"> <div class="notification-header">
<a href="{{ notification.url }}" ><b>{{ notification.title }}</b></a> <a href="{{ notification.url }}"><b>{{ notification.title }}</b></a>
<i class="card-timestamp">{{ notification.created_at }}</i> <i class="card-timestamp">{{ notification.created_at|time_since_hr }}</i>
<form class="notification-card-mark-read" method="POST"> </div>
{% csrf_token %} <div class="notification-body">
<input type="hidden" name="action" value="notification_mark_read"> {% include notification.get_body_part %}
<input type="hidden" name="notification_id" value="{{ notification.pk }}">
<button class="btn2" type="submit" id="submit"><i class="fa-solid fa-check"></i></button>
</form>
</div> </div>
<p>
{{ notification.text | render_markdown }}
</p>
</div> </div>

View File

@@ -11,7 +11,7 @@
<p> <p>
<b><i class="fa-solid fa-location-dot"></i></b> <b><i class="fa-solid fa-location-dot"></i></b>
{% if rescue_organization.location %} {% if rescue_organization.location %}
{{ rescue_organization.location.str }} {{ rescue_organization.location }}
{% else %} {% else %}
{{ rescue_organization.location_string }} {{ rescue_organization.location_string }}
{% endif %} {% endif %}

View File

@@ -2,7 +2,7 @@
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
<h1 class="title is-1">{% translate "Aktualitätscheck" %}</h1> <h1 class="title is-1">{% translate "Aktualitätscheck" %}</h1>
<p class="subtitle is-2">{% translate "Überprüfe ob es in Tierheimen neue Tiere gibt die ein Zuhause suchen" %}</p> <p class="subtitle is-3">{% translate "Überprüfe ob es in Tierheimen neue Tiere gibt die ein Zuhause suchen" %}</p>
<div class="block"> <div class="block">
<div class="columns"> <div class="columns">
@@ -15,6 +15,13 @@
<div class="column"> <div class="column">
<strong>Geprüft sind {{ percentage_checked|stringformat:"0.2f" }}%</strong> <strong>Geprüft sind {{ percentage_checked|stringformat:"0.2f" }}%</strong>
</div> </div>
<div class="column">
{% if dq %}
<a class="button is-info" href="{% url 'organization-check' %}">{% translate 'Datenergänzung deaktivieren' %}</a>
{% else %}
<a class="button is-info is-light" href="{% url 'organization-check-dq' %}">{% translate 'Datenergänzung aktivieren' %}</a>
{% endif %}
</div>
</div> </div>
</div> </div>
@@ -30,6 +37,18 @@
</div> </div>
<hr> <hr>
<div class="block">
<h2 class="title is-3">{% translate "In aktiver Kommunikation" %}</h2>
<div class="grid is-col-min-15">
{% for rescue_org in rescue_orgs_with_ongoing_communication %}
<div class="cell">
{% include "fellchensammlung/partials/partial-check-rescue-org.html" %}
</div>
{% endfor %}
</div>
</div>
<hr>
<div class="block"> <div class="block">
<h2 class="title is-3">{% translate "Zuletzt geprüft" %}</h2> <h2 class="title is-3">{% translate "Zuletzt geprüft" %}</h2>
<div class="grid is-col-min-15"> <div class="grid is-col-min-15">

View File

@@ -4,7 +4,9 @@ from django import template
from django.template.defaultfilters import stringfilter from django.template.defaultfilters import stringfilter
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from urllib.parse import urlparse from urllib.parse import urlparse
from django.utils import timezone
from fellchensammlung.tools.misc import time_since_as_hr_string
from notfellchen import settings from notfellchen import settings
from fellchensammlung.models import TrustLevel from fellchensammlung.models import TrustLevel
@@ -114,3 +116,9 @@ def dictkey(d, key):
def host(): def host():
# Will not work for localhost or deployments without https # Will not work for localhost or deployments without https
return f"https://{settings.host}" return f"https://{settings.host}"
@register.filter
def time_since_hr(timestamp):
t_delta = timezone.now() - timestamp
return time_since_as_hr_string(t_delta)

View File

@@ -0,0 +1,97 @@
import logging
import requests
from django.template.loader import render_to_string
from fellchensammlung.models import SocialMediaPost, PlatformChoices
from notfellchen import settings
class FediClient:
def __init__(self, access_token, api_base_url):
"""
:param access_token: Your server API access token.
:param api_base_url: The base URL of the Fediverse instance (e.g., 'https://gay-pirate-assassins.de').
"""
self.access_token = access_token
self.api_base_url = api_base_url.rstrip('/')
self.headers = {
'Authorization': f'Bearer {self.access_token}',
}
def upload_media(self, image_path, alt_text):
"""
Uploads media (image) to the server and returns the media ID.
:param image_path: Path to the image file to upload.
:param alt_text: Description (alt text) for the image.
:return: The media ID of the uploaded image.
"""
media_endpoint = f'{self.api_base_url}/api/v2/media'
with open(image_path, 'rb') as image_file:
files = {
'file': image_file,
'description': (None, alt_text)
}
response = requests.post(media_endpoint, headers=self.headers, files=files)
# Raise exception if upload fails
response.raise_for_status()
# Parse and return the media ID from the response
media_id = response.json().get('id')
return media_id
def post_status(self, status, media_ids=None):
"""
Posts a status to Mastodon with optional media.
:param status: The text of the status to post.
:param media_ids: A list of media IDs to attach to the status (optional).
:return: The response from the Mastodon API.
"""
status_endpoint = f'{self.api_base_url}/api/v1/statuses'
payload = {
'status': status,
'media_ids[]': media_ids if media_ids else []
}
response = requests.post(status_endpoint, headers=self.headers, data=payload)
# Raise exception if posting fails
response.raise_for_status()
return response.json()
def post_status_with_images(self, status, images):
"""
Uploads one or more image, then posts a status with that images and alt text.
:param status: The text of the status.
:param image_paths: The paths to the image file.
:param alt_text: The alt text for the image.
:return: The response from the Mastodon API.
"""
media_ids = []
for image in images:
# Upload the image and get the media ID
media_ids.append(self.upload_media(f"{settings.MEDIA_ROOT}/{image.image}", image.alt_text))
# Post the status with the uploaded image's media ID
return self.post_status(status, media_ids=media_ids)
def post_an_to_fedi(adoption_notice):
client = FediClient(settings.fediverse_access_token, settings.fediverse_api_base_url)
context = {"adoption_notice": adoption_notice}
status_text = render_to_string("fellchensammlung/misc/fediverse/an-post.md", context)
images = adoption_notice.get_photos()
if images is not None:
response = client.post_status_with_images(status_text, images)
else:
response = client.post_status(status_text)
logging.info(response)
post = SocialMediaPost.objects.create(adoption_notice=adoption_notice,
platform=PlatformChoices.FEDIVERSE,
url=response['url'], )
return post

View File

@@ -8,8 +8,9 @@ def gather_metrics_data():
"""Adoption notices""" """Adoption notices"""
num_adoption_notices = AdoptionNotice.objects.count() num_adoption_notices = AdoptionNotice.objects.count()
num_adoption_notices_active = AdoptionNotice.objects.filter( adoption_notices_active = AdoptionNotice.objects.filter(
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE).count() adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE)
num_adoption_notices_active = adoption_notices_active.count()
num_adoption_notices_closed = AdoptionNotice.objects.filter( num_adoption_notices_closed = AdoptionNotice.objects.filter(
adoptionnoticestatus__major_status=AdoptionNoticeStatus.CLOSED).count() adoptionnoticestatus__major_status=AdoptionNoticeStatus.CLOSED).count()
num_adoption_notices_disabled = AdoptionNotice.objects.filter( num_adoption_notices_disabled = AdoptionNotice.objects.filter(
@@ -18,6 +19,19 @@ def gather_metrics_data():
adoptionnoticestatus__major_status=AdoptionNoticeStatus.AWAITING_ACTION).count() adoptionnoticestatus__major_status=AdoptionNoticeStatus.AWAITING_ACTION).count()
adoption_notices_without_location = AdoptionNotice.objects.filter(location__isnull=True).count() adoption_notices_without_location = AdoptionNotice.objects.filter(location__isnull=True).count()
active_animals = 0
active_animals_per_sex = {}
for adoption_notice in adoption_notices_active:
nps = adoption_notice.num_per_sex
for sex in nps:
number_of_animals = nps[sex]
try:
active_animals_per_sex[sex] += number_of_animals
except KeyError:
active_animals_per_sex[sex] = number_of_animals
active_animals += number_of_animals
data = { data = {
'users': num_user, 'users': num_user,
'staff': num_staff, 'staff': num_staff,
@@ -29,6 +43,8 @@ def gather_metrics_data():
'disabled': num_adoption_notices_disabled, 'disabled': num_adoption_notices_disabled,
'awaiting_action': num_adoption_notices_awaiting_action, 'awaiting_action': num_adoption_notices_awaiting_action,
}, },
'adoption_notices_without_location': adoption_notices_without_location 'adoption_notices_without_location': adoption_notices_without_location,
'active_animals': active_animals,
'active_animals_per_sex': active_animals_per_sex
} }
return data return data

View File

@@ -37,6 +37,8 @@ def time_since_as_hr_string(age: datetime.timedelta) -> str:
weeks = age.days / 7 weeks = age.days / 7
months = age.days / 30 months = age.days / 30
years = age.days / 365 years = age.days / 365
minutes = age.seconds / 60
hours = age.seconds / 3600
if years >= 1: if years >= 1:
text = ngettext( text = ngettext(
"vor einem Jahr", "vor einem Jahr",
@@ -49,11 +51,14 @@ def time_since_as_hr_string(age: datetime.timedelta) -> str:
text = _("vor %(month)d Monaten") % {"month": months} text = _("vor %(month)d Monaten") % {"month": months}
elif weeks >= 3: elif weeks >= 3:
text = _("vor %(weeks)d Wochen") % {"weeks": weeks} text = _("vor %(weeks)d Wochen") % {"weeks": weeks}
elif days >= 1:
text = ngettext("vor einem Tag","vor %(count)d Tagen", days,) % {"count": days,}
elif hours >= 1:
text = ngettext("vor einer Stunde", "vor %(count)d Stunden", hours,) % {"count": hours,}
elif minutes >= 1:
text = ngettext("vor einer Minute", "vor %(count)d Minuten", minutes, ) % {"count": minutes, }
else: else:
if days == 0: text = _("Gerade eben")
text = _("Heute")
else:
text = ngettext("vor einem Tag","vor %(count)d Tagen", days,) % {"count": days,}
return text return text

View File

@@ -0,0 +1,57 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
"""
Helpers that MUST NOT DEPEND ON MODELS to avoid circular imports
"""
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 NotificationDisplayMapping:
def __init__(self, email_html_template, email_plain_template, web_partial):
self.email_html_template = email_html_template
self.email_plain_template = email_plain_template
self.web_partial = web_partial
report_mapping = NotificationDisplayMapping(
email_html_template='fellchensammlung/mail/notifications/report.html',
email_plain_template='fellchensammlung/mail/notifications/report.txt',
web_partial="fellchensammlung/partials/notifications/body-new-report.html"
)
# ndm = notification display mapping
ndm = {NotificationTypeChoices.NEW_USER: NotificationDisplayMapping(
email_html_template='fellchensammlung/mail/notifications/report.html',
email_plain_template="fellchensammlung/mail/notifications/report.txt",
web_partial="fellchensammlung/partials/notifications/body-new-user.html"),
NotificationTypeChoices.NEW_COMMENT: NotificationDisplayMapping(
email_html_template='fellchensammlung/mail/notifications/new-comment.html',
email_plain_template='fellchensammlung/mail/notifications/new-comment.txt',
web_partial="fellchensammlung/partials/notifications/body-new-comment.html"),
NotificationTypeChoices.NEW_REPORT_AN: report_mapping,
NotificationTypeChoices.NEW_REPORT_COMMENT: report_mapping,
NotificationTypeChoices.AN_IS_TO_BE_CHECKED: NotificationDisplayMapping(
email_html_template='fellchensammlung/mail/notifications/an-to-be-checked.html',
email_plain_template='fellchensammlung/mail/notifications/an-to-be-checked.txt',
web_partial='fellchensammlung/partials/notifications/body-an-to-be-checked.html'
),
NotificationTypeChoices.AN_WAS_DEACTIVATED: NotificationDisplayMapping(
email_html_template='fellchensammlung/mail/notifications/an-deactivated.html',
email_plain_template='fellchensammlung/mail/notifications/an-deactivated.txt',
web_partial='fellchensammlung/partials/notifications/body-an-deactivated.html'
),
NotificationTypeChoices.AN_FOR_SEARCH_FOUND: NotificationDisplayMapping(
email_html_template='fellchensammlung/mail/notifications/an-for-search-found.html',
email_plain_template='fellchensammlung/mail/notifications/an-for-search-found.txt',
web_partial='fellchensammlung/partials/notifications/body-an-for-search.html'
)
}

View File

@@ -1,4 +1,5 @@
from fellchensammlung.models import User, Notification, TrustLevel, NotificationTypeChoices from fellchensammlung.models import User, Notification, TrustLevel
from fellchensammlung.models import NotificationTypeChoices as ntc
def notify_of_AN_to_be_checked(adoption_notice): def notify_of_AN_to_be_checked(adoption_notice):
@@ -8,7 +9,7 @@ def notify_of_AN_to_be_checked(adoption_notice):
for user in users_to_notify: for user in users_to_notify:
Notification.objects.create(adoption_notice=adoption_notice, Notification.objects.create(adoption_notice=adoption_notice,
user_to_notify=user, user_to_notify=user,
notification_type=NotificationTypeChoices.AN_IS_TO_BE_CHECKED, notification_type=ntc.AN_IS_TO_BE_CHECKED,
title=f" Prüfe Vermittlung {adoption_notice}", title=f" Prüfe Vermittlung {adoption_notice}",
text=f"{adoption_notice} muss geprüft werden bevor sie veröffentlicht wird.", text=f"{adoption_notice} muss geprüft werden bevor sie veröffentlicht wird.",
) )

View File

@@ -7,12 +7,14 @@ from . import views, registration_views
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
from django.contrib.sitemaps.views import sitemap from django.contrib.sitemaps.views import sitemap
from .sitemap import StaticViewSitemap, AdoptionNoticeSitemap, AnimalSitemap from .sitemap import StaticViewSitemap, AdoptionNoticeSitemap, RescueOrganizationSitemap, SearchSitemap
sitemaps = { sitemaps = {
"static": StaticViewSitemap, "static": StaticViewSitemap,
"vermittlungen": AdoptionNoticeSitemap, "vermittlungen": AdoptionNoticeSitemap,
"tiere": AnimalSitemap, "tierschutzorganisationen": RescueOrganizationSitemap,
"orte": SearchSitemap
} }
urlpatterns = [ urlpatterns = [
@@ -35,10 +37,14 @@ urlpatterns = [
# ex: /adoption_notice/2/add-animal # ex: /adoption_notice/2/add-animal
path("vermittlung/<int:adoption_notice_id>/add-animal", views.adoption_notice_add_animal, path("vermittlung/<int:adoption_notice_id>/add-animal", views.adoption_notice_add_animal,
name="adoption-notice-add-animal"), name="adoption-notice-add-animal"),
path("vermittlung/<int:adoption_notice_id>/close", views.deactivate_an,
name="adoption-notice-close"),
path("tierschutzorganisationen/", views.list_rescue_organizations, name="rescue-organizations"), path("tierschutzorganisationen/", views.list_rescue_organizations, name="rescue-organizations"),
path("tierschutzorganisationen/<int:rescue_organization_id>/", views.detail_view_rescue_organization, path("tierschutzorganisationen/<int:rescue_organization_id>/", views.detail_view_rescue_organization,
name="rescue-organization-detail"), name="rescue-organization-detail"),
path("tierschutzorganisationen/spezialisierung/<int:species_id>", views.specialized_rescues,
name="specialized-rescue-organizations"),
# ex: /search/ # ex: /search/
path("suchen/", views.search, name="search"), path("suchen/", views.search, name="search"),
@@ -52,6 +58,7 @@ urlpatterns = [
path("impressum/", views.imprint, name="imprint"), path("impressum/", views.imprint, name="imprint"),
path("terms-of-service/", views.terms_of_service, name="terms-of-service"), path("terms-of-service/", views.terms_of_service, name="terms-of-service"),
path("datenschutz/", views.privacy, name="privacy"), path("datenschutz/", views.privacy, name="privacy"),
path("ratten-kaufen/", views.buying, name="buying"),
################ ################
## Moderation ## ## Moderation ##
@@ -75,6 +82,7 @@ urlpatterns = [
# ex: user/1 # ex: user/1
path("user/<int:user_id>/", views.user_by_id, name="user-detail"), path("user/<int:user_id>/", views.user_by_id, name="user-detail"),
path("user/me/", views.my_profile, name="user-me"), path("user/me/", views.my_profile, name="user-me"),
path("user/notifications/", views.my_notifications, name="user-notifications"),
path('user/me/export/', views.export_own_profile, name='user-me-export'), path('user/me/export/', views.export_own_profile, name='user-me-export'),
path('accounts/register/', path('accounts/register/',

View File

@@ -14,19 +14,21 @@ from django.contrib.auth.decorators import user_passes_test
from django.core.serializers import serialize from django.core.serializers import serialize
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import json import json
import requests
from .mail import mail_admins_new_report from .mail import notify_mods_new_report
from notfellchen import settings 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, Notification, RescueOrganization, \ User, Location, AdoptionNoticeStatus, Subscriptions, Notification, RescueOrganization, \
Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, \ Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, \
ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices, SocialMediaPost
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
from .tools import i18n from .tools import i18n
from .tools.fedi import post_an_to_fedi
from .tools.geo import GeoAPI, zoom_level_for_radius from .tools.geo import GeoAPI, zoom_level_for_radius
from .tools.metrics import gather_metrics_data from .tools.metrics import gather_metrics_data
from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \ from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
@@ -85,8 +87,27 @@ def change_language(request):
return render(request, "fellchensammlung/errors/403.html", status=403) return render(request, "fellchensammlung/errors/403.html", status=403)
def handle_an_check_actions(request, action, adoption_notice=None):
if adoption_notice is None:
adoption_notice = AdoptionNotice.objects.get(id=request.POST.get("adoption_notice_id"))
edit_permission = request.user == adoption_notice.owner or user_is_trust_level_or_above(request.user,
TrustLevel.MODERATOR)
# Check if the user is permitted to perform the actions
if action in ("checked_inactive", "checked_active") and not request.user.is_authenticated or not edit_permission:
return render(request, "fellchensammlung/errors/403.html", status=403)
if action == "checked_inactive":
adoption_notice.set_closed()
elif action == "checked_active":
print("dads")
adoption_notice.set_active()
return None
def adoption_notice_detail(request, adoption_notice_id): def adoption_notice_detail(request, adoption_notice_id):
adoption_notice = AdoptionNotice.objects.get(id=adoption_notice_id) adoption_notice = AdoptionNotice.objects.get(id=adoption_notice_id)
adoption_notice_meta = adoption_notice._meta
if request.user.is_authenticated: if request.user.is_authenticated:
try: try:
subscription = Subscriptions.objects.get(owner=request.user, adoption_notice=adoption_notice) subscription = Subscriptions.objects.get(owner=request.user, adoption_notice=adoption_notice)
@@ -98,6 +119,7 @@ def adoption_notice_detail(request, adoption_notice_id):
has_edit_permission = user_is_owner_or_trust_level(request.user, adoption_notice) has_edit_permission = user_is_owner_or_trust_level(request.user, adoption_notice)
if request.method == 'POST': if request.method == 'POST':
action = request.POST.get("action") action = request.POST.get("action")
handle_an_check_actions(request, action, adoption_notice)
if request.user.is_authenticated: if request.user.is_authenticated:
if action == "comment": if action == "comment":
comment_form = CommentForm(request.POST) comment_form = CommentForm(request.POST)
@@ -143,7 +165,8 @@ def adoption_notice_detail(request, adoption_notice_id):
else: else:
comment_form = CommentForm(instance=adoption_notice) comment_form = CommentForm(instance=adoption_notice)
context = {"adoption_notice": adoption_notice, "comment_form": comment_form, "user": request.user, context = {"adoption_notice": adoption_notice, "comment_form": comment_form, "user": request.user,
"has_edit_permission": has_edit_permission, "is_subscribed": is_subscribed} "has_edit_permission": has_edit_permission, "is_subscribed": is_subscribed,
"adoption_notice_meta": adoption_notice_meta}
return render(request, 'fellchensammlung/details/detail-adoption-notice.html', context=context) return render(request, 'fellchensammlung/details/detail-adoption-notice.html', context=context)
@@ -454,6 +477,11 @@ def privacy(request):
return render_text(request, text) return render_text(request, text)
def buying(request):
text = i18n.get_text_by_language("buying")
return render_text(request, text)
def terms_of_service(request): def terms_of_service(request):
text = i18n.get_text_by_language("terms_of_service") text = i18n.get_text_by_language("terms_of_service")
rules = Rule.objects.all() rules = Rule.objects.all()
@@ -478,8 +506,7 @@ def report_adoption(request, adoption_notice_id):
report_instance.status = Report.WAITING report_instance.status = Report.WAITING
report_instance.save() report_instance.save()
form.save_m2m() form.save_m2m()
mail_admins_new_report(report_instance) notify_mods_new_report(report_instance, NotificationTypeChoices.NEW_REPORT_AN)
print("dada")
return redirect(reverse("report-detail-success", args=[report_instance.pk], )) return redirect(reverse("report-detail-success", args=[report_instance.pk], ))
else: else:
form = ReportAdoptionNoticeForm() form = ReportAdoptionNoticeForm()
@@ -499,7 +526,7 @@ def report_comment(request, comment_id):
report_instance.status = Report.WAITING report_instance.status = Report.WAITING
report_instance.save() report_instance.save()
form.save_m2m() form.save_m2m()
mail_admins_new_report(report_instance) notify_mods_new_report(report_instance, NotificationTypeChoices.NEW_REPORT_COMMENT)
return redirect(reverse("report-detail-success", args=[report_instance.pk], )) return redirect(reverse("report-detail-success", args=[report_instance.pk], ))
else: else:
form = ReportCommentForm() form = ReportCommentForm()
@@ -549,6 +576,27 @@ def user_by_id(request, user_id):
return user_detail(request, user) return user_detail(request, user)
def process_notification_actions(request, action):
"""
As multiple views allow to mark notifications as read, this function can be used to process these actions
The function allows users to mark only their own notifications as read.
"""
if action == "notification_mark_read":
notification_id = request.POST.get("notification_id")
notification = Notification.objects.get(pk=notification_id)
# Ensures a user can only mark their own notifications as read
if not notification.user_to_notify == request.user:
return render(request, "fellchensammlung/errors/403.html", status=403)
notification.mark_read()
elif action == "notification_mark_all_read":
notifications = Notification.objects.filter(user_to_notify=request.user, read=False)
for notification in notifications:
notification.mark_read()
return None
@login_required() @login_required()
def my_profile(request): def my_profile(request):
if request.method == 'POST': if request.method == 'POST':
@@ -562,16 +610,8 @@ def my_profile(request):
user.save() user.save()
action = request.POST.get("action") action = request.POST.get("action")
if action == "notification_mark_read": process_notification_actions(request, action)
notification_id = request.POST.get("notification_id") if action == "search_subscription_delete":
notification = Notification.objects.get(pk=notification_id)
notification.mark_read()
elif action == "notification_mark_all_read":
notifications = Notification.objects.filter(user=request.user, mark_read=False)
for notification in notifications:
notification.mark_read()
elif action == "search_subscription_delete":
search_subscription_id = request.POST.get("search_subscription_id") search_subscription_id = request.POST.get("search_subscription_id")
SearchSubscription.objects.get(pk=search_subscription_id).delete() SearchSubscription.objects.get(pk=search_subscription_id).delete()
logging.info(f"Deleted subscription {search_subscription_id}") logging.info(f"Deleted subscription {search_subscription_id}")
@@ -583,6 +623,19 @@ def my_profile(request):
return user_detail(request, request.user, token) return user_detail(request, request.user, token)
@login_required()
def my_notifications(request):
if request.method == 'POST':
action = request.POST.get("action")
process_notification_actions(request, action)
context = {"notifications_unread": Notification.objects.filter(user_to_notify=request.user, read=False).order_by(
"-created_at"),
"notifications_read_last": Notification.objects.filter(user_to_notify=request.user,
read=True).order_by("-read_at")}
return render(request, 'fellchensammlung/notifications.html', context=context)
@user_passes_test(user_is_trust_level_or_above) @user_passes_test(user_is_trust_level_or_above)
def modqueue(request): def modqueue(request):
open_reports = Report.objects.select_related("reportadoptionnotice", "reportcomment").filter(status=Report.WAITING) open_reports = Report.objects.select_related("reportadoptionnotice", "reportcomment").filter(status=Report.WAITING)
@@ -593,16 +646,11 @@ def modqueue(request):
@login_required @login_required
def updatequeue(request): def updatequeue(request):
if request.method == "POST": if request.method == "POST":
adoption_notice = AdoptionNotice.objects.get(id=request.POST.get("adoption_notice_id"))
edit_permission = request.user == adoption_notice.owner or user_is_trust_level_or_above(request.user,
TrustLevel.MODERATOR)
if not edit_permission:
return render(request, "fellchensammlung/errors/403.html", status=403)
action = request.POST.get("action") action = request.POST.get("action")
if action == "checked_inactive":
adoption_notice.set_closed() # This function handles the activation and deactivation of ANs
if action == "checked_active": # Separate function because it's used in multiple places
adoption_notice.set_active() handle_an_check_actions(request, action)
if user_is_trust_level_or_above(request.user, TrustLevel.MODERATOR): if user_is_trust_level_or_above(request.user, TrustLevel.MODERATOR):
last_checked_adoption_list = AdoptionNotice.objects.order_by("last_checked") last_checked_adoption_list = AdoptionNotice.objects.order_by("last_checked")
@@ -697,8 +745,12 @@ def external_site_warning(request, template_name='fellchensammlung/external-site
return render(request, template_name, context=context) return render(request, template_name, context=context)
def list_rescue_organizations(request, template='fellchensammlung/animal-shelters.html'): def list_rescue_organizations(request, species=None, template='fellchensammlung/animal-shelters.html'):
rescue_organizations = RescueOrganization.objects.all() if species is None:
rescue_organizations = RescueOrganization.objects.all()
else:
rescue_organizations = RescueOrganization.objects.filter(specializations=species)
paginator = Paginator(rescue_organizations, 10) paginator = Paginator(rescue_organizations, 10)
page_number = request.GET.get("page") page_number = request.GET.get("page")
@@ -717,6 +769,11 @@ def list_rescue_organizations(request, template='fellchensammlung/animal-shelter
return render(request, template, context=context) return render(request, template, context=context)
def specialized_rescues(request, species_id):
species = get_object_or_404(Species, pk=species_id)
return list_rescue_organizations(request, species)
def detail_view_rescue_organization(request, rescue_organization_id, def detail_view_rescue_organization(request, rescue_organization_id,
template='fellchensammlung/details/detail-rescue-organization.html'): template='fellchensammlung/details/detail-rescue-organization.html'):
org = RescueOrganization.objects.get(pk=rescue_organization_id) org = RescueOrganization.objects.get(pk=rescue_organization_id)
@@ -764,12 +821,17 @@ def rescue_organization_check(request, context=None):
if comment_form.is_valid(): if comment_form.is_valid():
comment_form.save() comment_form.save()
rescue_orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=False).order_by("last_checked")[:10] rescue_orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=False,
ongoing_communication=False).order_by("last_checked")[:3]
rescue_orgs_with_ongoing_communication = RescueOrganization.objects.filter(ongoing_communication=True).order_by(
"updated_at")
rescue_orgs_last_checked = RescueOrganization.objects.filter().order_by("-last_checked")[:10]
rescue_orgs_to_comment = rescue_orgs_to_check | rescue_orgs_with_ongoing_communication | rescue_orgs_last_checked
# Prepare a form for each organization # Prepare a form for each organization
comment_forms = { comment_forms = {
org.id: RescueOrgInternalComment(instance=org) for org in rescue_orgs_to_check org.id: RescueOrgInternalComment(instance=org) for org in rescue_orgs_to_comment
} }
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( num_rescue_orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=False).filter(
last_checked__lt=timeframe).count() last_checked__lt=timeframe).count()
@@ -787,6 +849,8 @@ def rescue_organization_check(request, context=None):
context["num_rescue_orgs_to_check"] = num_rescue_orgs_to_check context["num_rescue_orgs_to_check"] = num_rescue_orgs_to_check
context["percentage_checked"] = percentage_checked context["percentage_checked"] = percentage_checked
context["num_rescue_orgs_checked"] = num_rescue_orgs_checked context["num_rescue_orgs_checked"] = num_rescue_orgs_checked
context["rescue_orgs_with_ongoing_communication"] = rescue_orgs_with_ongoing_communication
context["set_internal_comment_available"] = True
return render(request, 'fellchensammlung/rescue-organization-check.html', context=context) return render(request, 'fellchensammlung/rescue-organization-check.html', context=context)
@@ -797,7 +861,7 @@ def rescue_organization_check_dq(request):
DQ = data quality DQ = data quality
""" """
context = {"set_species_url_available": True, context = {"set_species_url_available": True,
"set_internal_comment_available": True, "dq": True,
"species_url_form": SpeciesURLForm, "species_url_form": SpeciesURLForm,
"internal_comment_form": RescueOrgInternalComment} "internal_comment_form": RescueOrgInternalComment}
return rescue_organization_check(request, context) return rescue_organization_check(request, context)
@@ -805,4 +869,38 @@ def rescue_organization_check_dq(request):
@user_passes_test(user_is_trust_level_or_above) @user_passes_test(user_is_trust_level_or_above)
def moderation_tools_overview(request): def moderation_tools_overview(request):
return render(request, 'fellchensammlung/mod-tool-overview.html') context = None
if request.method == "POST":
action = request.POST.get("action")
if action == "post_to_fedi":
adoption_notice = SocialMediaPost.get_an_to_post()
if adoption_notice is not None:
try:
post = post_an_to_fedi(adoption_notice)
context = {"action_was_posting": True, "post": post, "posted_successfully": True}
except requests.exceptions.ConnectionError as e:
logging.error(f"Could not post fediverse post: {e}")
context = {"action_was_posting": True,
"posted_successfully": False,
"error_message": _("Verbindungsfehler. Vermittlung wurde nicht gepostet")}
except requests.exceptions.HTTPError as e:
logging.error(f"Could not post fediverse post: {e}")
context = {"action_was_posting": True,
"posted_successfully": False,
"error_message": _("Fehler beim Posten. Vermittlung wurde nicht gepostet. Das kann "
"z.B. an falschen Zugangsdaten liegen. Kontaktieren einen Admin.")}
else:
context = {"action_was_posting": True,
"posted_successfully": False,
"error_message": _("Keine Vermittlung zum Posten gefunden.")}
return render(request, 'fellchensammlung/mod-tool-overview.html', context=context)
def deactivate_an(request, adoption_notice_id):
adoption_notice = get_object_or_404(AdoptionNotice, pk=adoption_notice_id)
if request.method == "POST":
reason_for_closing = request.POST.get("reason_for_closing")
adoption_notice.set_closed(reason_for_closing)
return redirect(reverse("adoption-notice-detail", args=[adoption_notice.pk], ))
context = {"adoption_notice": adoption_notice, }
return render(request, 'fellchensammlung/misc/deactivate-an.html', context=context)

View File

@@ -9,7 +9,7 @@ msgstr ""
"Project-Id-Version: Notfellchen\n" "Project-Id-Version: Notfellchen\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-06-19 15:51+0000\n" "POT-Creation-Date: 2025-06-19 15:51+0000\n"
"PO-Revision-Date: 2025-06-19 17:52+0200\n" "PO-Revision-Date: 2025-07-17 15:55+0200\n"
"Last-Translator: Julian-Samuel <Gebühr>\n" "Last-Translator: Julian-Samuel <Gebühr>\n"
"Language-Team: English\n" "Language-Team: English\n"
"Language: en\n" "Language: en\n"
@@ -343,7 +343,7 @@ msgstr "Adoption Notice deactivated:"
#: src/fellchensammlung/models.py:485 #: src/fellchensammlung/models.py:485
msgid "Die folgende Vermittlung wurde deaktiviert: " msgid "Die folgende Vermittlung wurde deaktiviert: "
msgstr "The following adoption notice was deactivated:" msgstr "The following adoption notice was deactivated: "
#: src/fellchensammlung/models.py:585 src/fellchensammlung/models.py:593 #: src/fellchensammlung/models.py:585 src/fellchensammlung/models.py:593
msgid "Weiblich" msgid "Weiblich"
@@ -562,7 +562,7 @@ msgstr "Adoption notices of Organization"
#: src/fellchensammlung/templates/fellchensammlung/details/detail-rescue-organization.html:47 #: src/fellchensammlung/templates/fellchensammlung/details/detail-rescue-organization.html:47
#: src/fellchensammlung/templates/fellchensammlung/lists/list-adoption-notices.html:11 #: src/fellchensammlung/templates/fellchensammlung/lists/list-adoption-notices.html:11
msgid "Keine Vermittlungen gefunden." msgid "Keine Vermittlungen gefunden."
msgstr "No adoption notices found" msgstr "No adoption notices found."
#: src/fellchensammlung/templates/fellchensammlung/details/detail-user.html:11 #: src/fellchensammlung/templates/fellchensammlung/details/detail-user.html:11
msgid "Profil verwalten" msgid "Profil verwalten"
@@ -680,7 +680,7 @@ msgstr "Report problems"
#: src/fellchensammlung/templates/fellchensammlung/footer.html:77 #: src/fellchensammlung/templates/fellchensammlung/footer.html:77
msgid "Code" msgid "Code"
msgstr "code" msgstr "Code"
#: src/fellchensammlung/templates/fellchensammlung/footer.html:84 #: src/fellchensammlung/templates/fellchensammlung/footer.html:84
msgid "Hilfreiche Links" msgid "Hilfreiche Links"
@@ -903,7 +903,7 @@ msgstr "Modqueue"
msgid "" msgid ""
"Erlaube oder blockiere Vermittlungsanzeigen die bisher noch zurückgehalten " "Erlaube oder blockiere Vermittlungsanzeigen die bisher noch zurückgehalten "
"werden " "werden "
msgstr "" msgstr "Allow or block adoption notices that are waiting in queue"
#: src/fellchensammlung/templates/fellchensammlung/partials/partial-adoption-notice.html:12 #: src/fellchensammlung/templates/fellchensammlung/partials/partial-adoption-notice.html:12
msgid "Notfellchen" msgid "Notfellchen"
@@ -956,7 +956,7 @@ msgstr "You need to log in to comment"
#: src/fellchensammlung/templates/fellchensammlung/partials/partial-report.html:4 #: src/fellchensammlung/templates/fellchensammlung/partials/partial-report.html:4
msgid "Meldung von " msgid "Meldung von "
msgstr "Report of" msgstr "Report of "
#: src/fellchensammlung/templates/fellchensammlung/partials/partial-report.html:7 #: src/fellchensammlung/templates/fellchensammlung/partials/partial-report.html:7
msgid "Regeln gegen die Verstoßen wurde" msgid "Regeln gegen die Verstoßen wurde"
@@ -1049,7 +1049,7 @@ msgstr "Deactivated adoption notices to check"
#: src/fellchensammlung/templates/fellchensammlung/updatequeue.html:13 #: src/fellchensammlung/templates/fellchensammlung/updatequeue.html:13
msgid "Aktive Vermittlungen zur Überprüfung" msgid "Aktive Vermittlungen zur Überprüfung"
msgstr "Active " msgstr "Active Adoption Notices to check"
#: src/fellchensammlung/tools/misc.py:42 #: src/fellchensammlung/tools/misc.py:42
#, python-format #, python-format

View File

@@ -1,4 +1,4 @@
__version__ = "1.0.1" __version__ = "1.1.0"
# This will make sure the app is always imported when # This will make sure the app is always imported when
# Django starts so that shared_task will use this app. # Django starts so that shared_task will use this app.

View File

@@ -24,6 +24,11 @@ app.conf.beat_schedule = {
'task': 'admin.deactivate_404_adoption_notices', 'task': 'admin.deactivate_404_adoption_notices',
'schedule': crontab(hour=3), 'schedule': crontab(hour=3),
}, },
'daily-fedi-post': {
'task': 'social_media.post_fedi',
'schedule': crontab(hour=19),
},
} }
if settings.HEALTHCHECKS_URL is not None and settings.HEALTHCHECKS_URL != "": if settings.HEALTHCHECKS_URL is not None and settings.HEALTHCHECKS_URL != "":

View File

@@ -118,6 +118,12 @@ else:
EMAIL_USE_TLS = config.getboolean('mail', 'tls', fallback=False) EMAIL_USE_TLS = config.getboolean('mail', 'tls', fallback=False)
EMAIL_USE_SSL = config.getboolean('mail', 'ssl', fallback=False) EMAIL_USE_SSL = config.getboolean('mail', 'ssl', fallback=False)
""" Fediverse """
fediverse_enabled = config.get('fediverse', 'enabled', fallback=False)
if fediverse_enabled:
fediverse_api_base_url = config.get('fediverse', 'api_base_url')
fediverse_access_token = config.get('fediverse', 'access_token')
"""USER MANAGEMENT""" """USER MANAGEMENT"""
AUTH_USER_MODEL = "fellchensammlung.User" AUTH_USER_MODEL = "fellchensammlung.User"
ACCOUNT_ACTIVATION_DAYS = 7 # One-week activation window ACCOUNT_ACTIVATION_DAYS = 7 # One-week activation window