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)
if location_result.status_code != 201:
print(
f"Location for {tierheim["properties"]["name"]}:{location_result.status_code} {location_result.json()} not created")
try:
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()

View File

@@ -8,7 +8,7 @@ from django.urls import reverse
from django.utils.http import urlencode
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, \
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, Notification
@@ -100,11 +100,6 @@ class SpeciesSpecificURLInline(admin.StackedInline):
model = SpeciesSpecificURL
class SpeciesSpecializationInline(admin.StackedInline):
model = SpeciesSpecialization
extra = 0
@admin.register(RescueOrganization)
class RescueOrganizationAdmin(admin.ModelAdmin):
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))
inlines = [
SpeciesSpecializationInline,
SpeciesSpecificURLInline,
]
@@ -168,6 +162,9 @@ class LocationAdmin(admin.ModelAdmin):
ImportantLocationInline,
]
@admin.register(SocialMediaPost)
class SocialMediaPostAdmin(admin.ModelAdmin):
list_filter = ("platform",)
admin.site.register(Animal)
admin.site.register(Species)

View File

@@ -360,9 +360,9 @@ class LocationApiView(APIView):
# Log the action
Log.objects.create(
user=request.user_to_notify,
user=request.user,
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

View File

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

View File

@@ -0,0 +1,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 .tools.geo import LocationProxy, Position
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):
@@ -105,6 +107,9 @@ class ImportantLocation(models.Model):
slug = models.SlugField(unique=True)
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):
OSM = "OSM", _("Open Street Map")
@@ -118,10 +123,23 @@ class AllowUseOfMaterialsChices(models.TextChoices):
USE_MATERIALS_NOT_ASKED = "not_asked", _("Not asked")
class RescueOrganization(models.Model):
def __str__(self):
return f"{self.name}"
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 RescueOrganization(models.Model):
name = models.CharField(max_length=200)
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
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'),
help_text=_("Organisation von der manuellen Überprüfung ausschließen, "
"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)
# allows to specify if a rescue organization has a specialization for dedicated species
specializations = models.ManyToManyField(Species, blank=True)
class Meta:
unique_together = ('external_object_identifier', 'external_source_identifier',)
ordering = ['name']
def __str__(self):
return f"{self.name}"
def clean(self):
super().clean()
@@ -166,6 +193,17 @@ class RescueOrganization(models.Model):
def adoption_notices(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
def position(self):
if self.location:
@@ -206,6 +244,10 @@ class RescueOrganization(models.Model):
self.exclude_from_check = True
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
# Moderators can make moderation decisions regarding the deletion of content
@@ -252,14 +294,17 @@ class User(AbstractUser):
def get_absolute_url(self):
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):
return self.get_absolute_url()
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):
return Notification.objects.filter(user=self, read=False).count()
return Notification.objects.filter(user_to_notify=self, read=False).count()
@property
def adoption_notices(self):
@@ -285,22 +330,6 @@ class Image(models.Model):
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 Meta:
permissions = [
@@ -346,7 +375,7 @@ class AdoptionNotice(models.Model):
def num_per_sex(self):
num_per_sex = dict()
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
@property
@@ -455,16 +484,28 @@ class AdoptionNotice(models.Model):
return False
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
def is_disabled_unchecked(self):
if not hasattr(self, 'adoptionnoticestatus'):
return False
return self.adoptionnoticestatus.is_disabled_unchecked
def set_closed(self):
def set_closed(self, minor_status=None):
self.last_checked = timezone.now()
self.save()
self.adoptionnoticestatus.set_closed()
self.adoptionnoticestatus.set_closed(minor_status)
def set_active(self):
self.last_checked = timezone.now()
@@ -489,6 +530,14 @@ class AdoptionNotice(models.Model):
text=text,
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):
"""
@@ -552,6 +601,14 @@ class AdoptionNoticeStatus(models.Model):
def is_active(self):
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
def is_disabled_unchecked(self):
return self.major_status == self.DISABLED and self.minor_status == "unchecked"
@@ -569,9 +626,12 @@ class AdoptionNoticeStatus(models.Model):
minor_status=minor_status,
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.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()
def set_unchecked(self):
@@ -727,6 +787,9 @@ class Report(models.Model):
"""Returns the url to access a detailed page for the report."""
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):
return self.reported_broken_rules.all()
@@ -910,33 +973,24 @@ class Comment(models.Model):
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):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
read_at = models.DateTimeField(blank=True, null=True, verbose_name=_("Gelesen am"))
notification_type = models.CharField(max_length=200,
choices=NotificationTypeChoices.choices,
verbose_name=_('Benachrichtigungsgrund'))
title = models.CharField(max_length=100, verbose_name=_("Titel"))
text = models.TextField(verbose_name="Inhalt")
user_to_notify = models.ForeignKey(User,
on_delete=models.CASCADE,
verbose_name=_('Nutzer*in'),
verbose_name=_('Empfänger*in'),
help_text=_("Useraccount der Benachrichtigt wird"),
related_name='user')
title = models.CharField(max_length=100, verbose_name=_("Titel"))
text = models.TextField(verbose_name="Inhalt")
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'))
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,
blank=True, null=True,
on_delete=models.CASCADE, verbose_name=_('Verwandter Useraccount'),
@@ -958,6 +1012,9 @@ class Notification(models.Model):
self.read_at = timezone.now()
self.save()
def get_body_part(self):
return NotificationDisplayMapping[self.notification_type].web_partial
class Subscriptions(models.Model):
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"))
class SpeciesSpecialization(models.Model):
"""
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"))
rescue_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE,
verbose_name=_("Tierschutzorganisation"))
class PlatformChoices(models.TextChoices):
FEDIVERSE = "fediverse", _("Fediverse")
class SocialMediaPost(models.Model):
created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
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):
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.urls import reverse
from .models import AdoptionNotice, RescueOrganization
from .models import AdoptionNotice, RescueOrganization, ImportantLocation, Animal
class StaticViewSitemap(Sitemap):
@@ -8,7 +8,8 @@ class StaticViewSitemap(Sitemap):
changefreq = "weekly"
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):
return reverse(item)
@@ -25,17 +26,6 @@ class AdoptionNoticeSitemap(Sitemap):
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):
priority = 0.3
changefreq = "weekly"
@@ -45,3 +35,11 @@ class RescueOrganizationSitemap(Sitemap):
def lastmod(self, obj):
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);
}
.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 .mail import send_notification_email
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 .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.search import notify_search_subscribers
@@ -38,6 +39,13 @@ def task_deactivate_unchecked():
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")
def post_adoption_notice_save(pk):
instance = AdoptionNotice.objects.get(pk=pk)

View File

@@ -1,5 +1,6 @@
{% extends "fellchensammlung/base.html" %}
{% load custom_tags %}
{% load admin_urls %}
{% load i18n %}
{% load static %}
@@ -25,6 +26,20 @@
{% endblock %}
{% 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="column is-two-thirds">
<!--- Title level (including action dropdown) -->
@@ -49,30 +64,59 @@
<!--- Action menu (dropdown) --->
<div class="dropdown-menu" role="menu">
<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 %}
<a class="dropdown-item">
<i class="fas fa-check"
aria-hidden="true"></i> {% trans 'Als aktiv bestätigen' %}
</a>
<form class="dropdown-item" method="POST">
{% csrf_token %}
<input type="hidden" name="action" value="checked_active">
<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"
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' %}
</a>
<a class="dropdown-item"
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' %}
</a>
<a class="dropdown-item"
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' %}
</a>
<a class="dropdown-item">
<i class="fas fa-circle-xmark"
<a class="dropdown-item"
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' %}
</a>
<hr class="dropdown-divider">
@@ -81,6 +125,13 @@
<i class="fas fa-flag"
aria-hidden="true"></i> {% trans 'Melden' %}
</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>
@@ -169,6 +220,13 @@
{% translate "Keine Beschreibung angegeben" %}
{% endif %}
</p>
<hr>
<p>
<strong>
{% translate 'Zuletzt auf Aktualität überprüft:' %}
</strong>
{{ adoption_notice.last_checked|time_since_hr }}
</p>
</div>
</div>
</div>

View File

@@ -35,6 +35,29 @@
<p>{{ org.description | render_markdown }}</p>
{% endif %}
</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>
@@ -42,7 +65,9 @@
{% include "fellchensammlung/partials/partial-rescue-organization-contact.html" %}
</div>
<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 class="column">
@@ -50,11 +75,20 @@
</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>
<div class="container-cards">
{% if org.adoption_notices %}
{% for adoption_notice in org.adoption_notices %}
{% if org.adoption_notices_in_hierarchy %}
{% for adoption_notice in org.adoption_notices_in_hierarchy %}
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
{% endfor %}
{% 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" %}
{% load i18n %}
{% load custom_tags %}
{% load custom_tags %}
{% block content %}
<div class="content">
{% if external_site_warning %}
{{ external_site_warning.content | render_markdown }}
{% else %}
{% blocktranslate %}
<p>Achtung du verlässt notfellchen.org</p>
{% endblocktranslate %}
{% endif %}
<a href="{{ url }}" class="button is-primary">{% translate "Weiter" %}</a>
<div class="block">
<div class="message is-warning">
{% if external_site_warning %}
<h1 class="message-header">
{{ external_site_warning.title }}
</h1>
<div class="message-body">
{{ external_site_warning.content | render_markdown }}
</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>
{% endblock content %}

View File

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

View File

@@ -9,7 +9,8 @@
<h1 class="title is-4">notfellchen.org</h1>
</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>
@@ -30,7 +31,16 @@
</div>
<div class="navbar-end">
{% 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">
<a href="{% url 'user-me' %}">
<i class="fas fa-user fa-fw"></i> {{ user }}

View File

@@ -54,6 +54,11 @@
font-size: 14px;
}
.setting-info {
font-size: 10px;
}
@media (max-width: 600px) {
.content, .header, .footer {
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">
🐀 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
</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>
{% 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 %}
{% block content %}
<p>Moin,</p>
<p>{% translate 'Moin' %},</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>
{% if notification.report.user_comment %}
<p>
<i>
{{ notification.report.user_comment }}
</i>
</p>
{% endif %}
<p>
<i>
{{ user_comment }}
</i>
{% blocktranslate %}
Bitte bearbeite die Meldung möglichst bald.
{% endblocktranslate %}
</p>
<p>
Bitte bearbeite die Meldung möglichst bald.
</p>
<p>
<a href="{{ report_url }}" class="cta-button">{% translate 'Report bearbeiten' %}</a>
<a href="{{ notification.report.get_full_url }}" class="cta-button">{% translate 'Report bearbeiten' %}</a>
</p>
{% 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">
<h1 class="title is-1">{% translate 'Moderationstools' %}</h1>
<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 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 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>

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">
<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>
{% if adoption_notice.organization %}
<div class="cell">
<span>
<i class="fa-solid fa-building fa-fw" aria-label="{% trans 'Tierschutzorganisation' %}"></i>
<a href="{{ adoption_notice.organization.get_absolute_url }}"> {{ adoption_notice.organization }}</a>
</span>
</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 class="cell">
{% include "fellchensammlung/partials/sex-overview.html" %}

View File

@@ -4,7 +4,7 @@
<div class="card-header">
<div class="card-header-title">
<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>
</div>
</div>

View File

@@ -1,17 +1,17 @@
{% load i18n %}
{% load custom_tags %}
<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">
<a href="{{ notification.url }}" ><b>{{ notification.title }}</b></a>
<i class="card-timestamp">{{ notification.created_at }}</i>
<form class="notification-card-mark-read" method="POST">
{% csrf_token %}
<input type="hidden" name="action" value="notification_mark_read">
<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>
<a href="{{ notification.url }}"><b>{{ notification.title }}</b></a>
<i class="card-timestamp">{{ notification.created_at|time_since_hr }}</i>
</div>
<div class="notification-body">
{% include notification.get_body_part %}
</div>
<p>
{{ notification.text | render_markdown }}
</p>
</div>

View File

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

View File

@@ -2,7 +2,7 @@
{% load i18n %}
{% block content %}
<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="columns">
@@ -15,6 +15,13 @@
<div class="column">
<strong>Geprüft sind {{ percentage_checked|stringformat:"0.2f" }}%</strong>
</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>
@@ -30,6 +37,18 @@
</div>
<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">
<h2 class="title is-3">{% translate "Zuletzt geprüft" %}</h2>
<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.utils.safestring import mark_safe
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 fellchensammlung.models import TrustLevel
@@ -114,3 +116,9 @@ def dictkey(d, key):
def host():
# Will not work for localhost or deployments without https
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"""
num_adoption_notices = AdoptionNotice.objects.count()
num_adoption_notices_active = AdoptionNotice.objects.filter(
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE).count()
adoption_notices_active = AdoptionNotice.objects.filter(
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE)
num_adoption_notices_active = adoption_notices_active.count()
num_adoption_notices_closed = AdoptionNotice.objects.filter(
adoptionnoticestatus__major_status=AdoptionNoticeStatus.CLOSED).count()
num_adoption_notices_disabled = AdoptionNotice.objects.filter(
@@ -18,6 +19,19 @@ def gather_metrics_data():
adoptionnoticestatus__major_status=AdoptionNoticeStatus.AWAITING_ACTION).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 = {
'users': num_user,
'staff': num_staff,
@@ -29,6 +43,8 @@ def gather_metrics_data():
'disabled': num_adoption_notices_disabled,
'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

View File

@@ -37,6 +37,8 @@ def time_since_as_hr_string(age: datetime.timedelta) -> str:
weeks = age.days / 7
months = age.days / 30
years = age.days / 365
minutes = age.seconds / 60
hours = age.seconds / 3600
if years >= 1:
text = ngettext(
"vor einem Jahr",
@@ -49,11 +51,14 @@ def time_since_as_hr_string(age: datetime.timedelta) -> str:
text = _("vor %(month)d Monaten") % {"month": months}
elif weeks >= 3:
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:
if days == 0:
text = _("Heute")
else:
text = ngettext("vor einem Tag","vor %(count)d Tagen", days,) % {"count": days,}
text = _("Gerade eben")
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):
@@ -8,7 +9,7 @@ def notify_of_AN_to_be_checked(adoption_notice):
for user in users_to_notify:
Notification.objects.create(adoption_notice=adoption_notice,
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}",
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 django.contrib.sitemaps.views import sitemap
from .sitemap import StaticViewSitemap, AdoptionNoticeSitemap, AnimalSitemap
from .sitemap import StaticViewSitemap, AdoptionNoticeSitemap, RescueOrganizationSitemap, SearchSitemap
sitemaps = {
"static": StaticViewSitemap,
"vermittlungen": AdoptionNoticeSitemap,
"tiere": AnimalSitemap,
"tierschutzorganisationen": RescueOrganizationSitemap,
"orte": SearchSitemap
}
urlpatterns = [
@@ -35,10 +37,14 @@ urlpatterns = [
# ex: /adoption_notice/2/add-animal
path("vermittlung/<int:adoption_notice_id>/add-animal", views.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/<int:rescue_organization_id>/", views.detail_view_rescue_organization,
name="rescue-organization-detail"),
path("tierschutzorganisationen/spezialisierung/<int:species_id>", views.specialized_rescues,
name="specialized-rescue-organizations"),
# ex: /search/
path("suchen/", views.search, name="search"),
@@ -52,6 +58,7 @@ urlpatterns = [
path("impressum/", views.imprint, name="imprint"),
path("terms-of-service/", views.terms_of_service, name="terms-of-service"),
path("datenschutz/", views.privacy, name="privacy"),
path("ratten-kaufen/", views.buying, name="buying"),
################
## Moderation ##
@@ -75,6 +82,7 @@ urlpatterns = [
# ex: user/1
path("user/<int:user_id>/", views.user_by_id, name="user-detail"),
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('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.utils.translation import gettext_lazy as _
import json
import requests
from .mail import mail_admins_new_report
from .mail import notify_mods_new_report
from notfellchen import settings
from fellchensammlung import logger
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
User, Location, AdoptionNoticeStatus, Subscriptions, Notification, RescueOrganization, \
Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, \
ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices
ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices, SocialMediaPost
from .forms import AdoptionNoticeForm, ImageForm, ReportAdoptionNoticeForm, \
CommentForm, ReportCommentForm, AnimalForm, AdoptionNoticeFormAutoAnimal, SpeciesURLForm, RescueOrgInternalComment
from .models import Language, Announcement
from .tools import i18n
from .tools.fedi import post_an_to_fedi
from .tools.geo import GeoAPI, zoom_level_for_radius
from .tools.metrics import gather_metrics_data
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)
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):
adoption_notice = AdoptionNotice.objects.get(id=adoption_notice_id)
adoption_notice_meta = adoption_notice._meta
if request.user.is_authenticated:
try:
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)
if request.method == 'POST':
action = request.POST.get("action")
handle_an_check_actions(request, action, adoption_notice)
if request.user.is_authenticated:
if action == "comment":
comment_form = CommentForm(request.POST)
@@ -143,7 +165,8 @@ def adoption_notice_detail(request, adoption_notice_id):
else:
comment_form = CommentForm(instance=adoption_notice)
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)
@@ -454,6 +477,11 @@ def privacy(request):
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):
text = i18n.get_text_by_language("terms_of_service")
rules = Rule.objects.all()
@@ -478,8 +506,7 @@ def report_adoption(request, adoption_notice_id):
report_instance.status = Report.WAITING
report_instance.save()
form.save_m2m()
mail_admins_new_report(report_instance)
print("dada")
notify_mods_new_report(report_instance, NotificationTypeChoices.NEW_REPORT_AN)
return redirect(reverse("report-detail-success", args=[report_instance.pk], ))
else:
form = ReportAdoptionNoticeForm()
@@ -499,7 +526,7 @@ def report_comment(request, comment_id):
report_instance.status = Report.WAITING
report_instance.save()
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], ))
else:
form = ReportCommentForm()
@@ -549,6 +576,27 @@ def user_by_id(request, user_id):
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()
def my_profile(request):
if request.method == 'POST':
@@ -562,16 +610,8 @@ def my_profile(request):
user.save()
action = request.POST.get("action")
if action == "notification_mark_read":
notification_id = request.POST.get("notification_id")
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":
process_notification_actions(request, action)
if action == "search_subscription_delete":
search_subscription_id = request.POST.get("search_subscription_id")
SearchSubscription.objects.get(pk=search_subscription_id).delete()
logging.info(f"Deleted subscription {search_subscription_id}")
@@ -583,6 +623,19 @@ def my_profile(request):
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)
def modqueue(request):
open_reports = Report.objects.select_related("reportadoptionnotice", "reportcomment").filter(status=Report.WAITING)
@@ -593,16 +646,11 @@ def modqueue(request):
@login_required
def updatequeue(request):
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")
if action == "checked_inactive":
adoption_notice.set_closed()
if action == "checked_active":
adoption_notice.set_active()
# This function handles the activation and deactivation of ANs
# Separate function because it's used in multiple places
handle_an_check_actions(request, action)
if user_is_trust_level_or_above(request.user, TrustLevel.MODERATOR):
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)
def list_rescue_organizations(request, template='fellchensammlung/animal-shelters.html'):
rescue_organizations = RescueOrganization.objects.all()
def list_rescue_organizations(request, species=None, template='fellchensammlung/animal-shelters.html'):
if species is None:
rescue_organizations = RescueOrganization.objects.all()
else:
rescue_organizations = RescueOrganization.objects.filter(specializations=species)
paginator = Paginator(rescue_organizations, 10)
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)
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,
template='fellchensammlung/details/detail-rescue-organization.html'):
org = RescueOrganization.objects.get(pk=rescue_organization_id)
@@ -764,12 +821,17 @@ def rescue_organization_check(request, context=None):
if comment_form.is_valid():
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
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)
num_rescue_orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=False).filter(
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["percentage_checked"] = percentage_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)
@@ -797,7 +861,7 @@ def rescue_organization_check_dq(request):
DQ = data quality
"""
context = {"set_species_url_available": True,
"set_internal_comment_available": True,
"dq": True,
"species_url_form": SpeciesURLForm,
"internal_comment_form": RescueOrgInternalComment}
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)
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"
"Report-Msgid-Bugs-To: \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"
"Language-Team: English\n"
"Language: en\n"
@@ -343,7 +343,7 @@ msgstr "Adoption Notice deactivated:"
#: src/fellchensammlung/models.py:485
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
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/lists/list-adoption-notices.html:11
msgid "Keine Vermittlungen gefunden."
msgstr "No adoption notices found"
msgstr "No adoption notices found."
#: src/fellchensammlung/templates/fellchensammlung/details/detail-user.html:11
msgid "Profil verwalten"
@@ -680,7 +680,7 @@ msgstr "Report problems"
#: src/fellchensammlung/templates/fellchensammlung/footer.html:77
msgid "Code"
msgstr "code"
msgstr "Code"
#: src/fellchensammlung/templates/fellchensammlung/footer.html:84
msgid "Hilfreiche Links"
@@ -903,7 +903,7 @@ msgstr "Modqueue"
msgid ""
"Erlaube oder blockiere Vermittlungsanzeigen die bisher noch zurückgehalten "
"werden "
msgstr ""
msgstr "Allow or block adoption notices that are waiting in queue"
#: src/fellchensammlung/templates/fellchensammlung/partials/partial-adoption-notice.html:12
msgid "Notfellchen"
@@ -956,7 +956,7 @@ msgstr "You need to log in to comment"
#: src/fellchensammlung/templates/fellchensammlung/partials/partial-report.html:4
msgid "Meldung von "
msgstr "Report of"
msgstr "Report of "
#: src/fellchensammlung/templates/fellchensammlung/partials/partial-report.html:7
msgid "Regeln gegen die Verstoßen wurde"
@@ -1049,7 +1049,7 @@ msgstr "Deactivated adoption notices to check"
#: src/fellchensammlung/templates/fellchensammlung/updatequeue.html:13
msgid "Aktive Vermittlungen zur Überprüfung"
msgstr "Active "
msgstr "Active Adoption Notices to check"
#: src/fellchensammlung/tools/misc.py:42
#, 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
# 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',
'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 != "":

View File

@@ -118,6 +118,12 @@ else:
EMAIL_USE_TLS = config.getboolean('mail', 'tls', 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"""
AUTH_USER_MODEL = "fellchensammlung.User"
ACCOUNT_ACTIVATION_DAYS = 7 # One-week activation window