Compare commits

75 Commits

Author SHA1 Message Date
a372be4af2 fix: don't use search when checking specialized rescue orgs 2025-08-12 06:16:27 +02:00
5d333b28ab feat: Fix pagination when searching 2025-08-12 06:12:27 +02:00
84ad047c01 feat: Add search for rescue orgs 2025-08-12 00:06:42 +02:00
c93b2631cb feat: Add shortcut to open rescue org website 2025-08-11 22:16:26 +02:00
15dd06a91f feat: Add shortcut to mark rescue org as checked 2025-08-11 21:51:30 +02:00
30ff26c7ef feat: Divide adoption notices of org by active and inactive 2025-08-11 12:58:02 +02:00
1434e7502a fix: Limit upload of fediverse images to 6 2025-08-11 12:43:15 +02:00
93b21fb7d0 fix: Only try to access trust level when authenticated 2025-08-10 18:32:15 +02:00
e5c82f392c feat(test): Add test for map and metrics 2025-08-10 18:31:53 +02:00
0626964461 feat(test): Add test for token showing 2025-08-10 18:11:10 +02:00
23a724e390 fix: Ensure users of higher trust level are also allowed 2025-08-10 17:51:25 +02:00
2a9c7cf854 feat(test): Add basic tests for user views 2025-08-10 17:51:05 +02:00
335630e16d feat(test): Add test for search by location 2025-08-10 17:50:17 +02:00
6051f7c294 feat(test): Exclude from coverage check 2025-08-10 10:17:59 +02:00
c1ea6cd211 feat(test): Add AN form to basic check 2025-08-10 08:44:28 +02:00
6c43b46007 refactor: break out search test into own file 2025-08-10 08:43:49 +02:00
dc9e68c4b9 refactor: remoive print 2025-08-10 08:22:57 +02:00
4b03f99971 feat(test): Add rss feed test 2025-08-09 16:46:07 +02:00
426f4b3d8b fix: Make sure e-mail is sent when comment is reported 2025-08-09 12:30:42 +02:00
3604233507 fix (test): Rules are now shown on terms of service page 2025-08-09 12:30:22 +02:00
8c5099f14a fix (test): Notification framework changed 2025-08-09 12:16:13 +02:00
d5bc348453 feat: allow marking read with very ugly double delete class 2025-08-03 10:40:42 +02:00
bce98cb439 trans: Translate models 2025-08-03 10:29:47 +02:00
1ed3d27533 feat: add option to sync to twenty 2025-08-03 10:00:42 +02:00
39a098af8e feat: Add option to mask e-mails and phone numbers
This is a prerequisite to do tests on DEv and UAT systems
2025-08-01 20:28:51 +02:00
62491b84c1 feat(seo): Add basic description 2025-08-01 19:32:02 +02:00
81f7f5bb5d fix: Use correct heading hierarchy 2025-08-01 19:31:37 +02:00
8ce4122160 feat: raise 404 when AN not found 2025-07-30 08:01:34 +02:00
370ad2ce66 feat: add warning when an is waiting for review 2025-07-30 06:57:31 +02:00
f25c425d85 feat: add warning when someone is interested 2025-07-30 06:51:29 +02:00
d921623f31 fix: Make sure content class is used when rendering markdown 2025-07-25 22:38:49 +02:00
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
85 changed files with 1966 additions and 416 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

@@ -147,3 +147,11 @@ class AdoptionNoticeSearchForm(forms.Form):
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED,
label=_("Suchradius"))
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
class RescueOrgSearchForm(forms.Form):
template_name = "fellchensammlung/forms/form_snippets.html"
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED,
label=_("Suchradius"))

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_REPORT_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,13 @@
from django.core.management import BaseCommand
from fellchensammlung.tools.admin import mask_organization_contact_data
class Command(BaseCommand):
help = 'Mask e-mail addresses and phone numbers of organizations for testing purposes.'
def add_arguments(self, parser):
parser.add_argument("domain", type=str)
def handle(self, *args, **options):
domain = options["domain"]
mask_organization_contact_data(domain)

View File

@@ -0,0 +1,19 @@
from django.core.management import BaseCommand
from tqdm import tqdm
from fellchensammlung.models import RescueOrganization
from fellchensammlung.tools.twenty import sync_rescue_org_to_twenty
class Command(BaseCommand):
help = 'Send rescue organizations as companies to twenty'
def add_arguments(self, parser):
parser.add_argument("base_url", type=str)
parser.add_argument("token", type=str)
def handle(self, *args, **options):
base_url = options["base_url"]
token = options["token"]
for rescue_org in tqdm(RescueOrganization.objects.all()):
sync_rescue_org_to_twenty(rescue_org, base_url, token)

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

@@ -0,0 +1,23 @@
# Generated by Django 5.2.1 on 2025-08-02 09:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0058_socialmediapost'),
]
operations = [
migrations.AddField(
model_name='rescueorganization',
name='twenty_id',
field=models.UUIDField(blank=True, help_text='ID der der Organisation in Twenty', null=True, verbose_name='Twenty-ID'),
),
migrations.AlterField(
model_name='rescueorganization',
name='specializations',
field=models.ManyToManyField(blank=True, to='fellchensammlung.species'),
),
]

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):
@@ -58,6 +60,10 @@ class Location(models.Model):
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = _("Standort")
verbose_name_plural = _("Standorte")
def __str__(self):
if self.city and self.postcode:
return f"{self.city} ({self.postcode})"
@@ -101,10 +107,17 @@ class Location(models.Model):
class ImportantLocation(models.Model):
class Meta:
verbose_name = _("Wichtiger Standort")
verbose_name_plural = _("Wichtige Standorte")
location = models.OneToOneField(Location, on_delete=models.CASCADE)
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 +131,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 +175,23 @@ 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)
twenty_id = models.UUIDField(verbose_name=_("Twenty-ID"), null=True, blank=True,
help_text=_("ID der der Organisation in Twenty"))
class Meta:
unique_together = ('external_object_identifier', 'external_source_identifier',)
ordering = ['name']
verbose_name = _("Tierschutzorganisation")
verbose_name_plural = _("Tierschutzorganisationen")
def __str__(self):
return f"{self.name}"
def clean(self):
super().clean()
@@ -166,6 +205,29 @@ 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 adoption_notices_in_hierarchy_divided_by_status(self):
"""Returns two lists of adoption notices, the first active, the other inactive."""
active_adoption_notices = []
inactive_adoption_notices = []
for an in self.adoption_notices_in_hierarchy:
if an.is_active:
active_adoption_notices.append(an)
else:
inactive_adoption_notices.append(an)
return active_adoption_notices, inactive_adoption_notices
@property
def position(self):
if self.location:
@@ -206,6 +268,18 @@ class RescueOrganization(models.Model):
self.exclude_from_check = True
self.save()
@property
def child_organizations(self):
return RescueOrganization.objects.filter(parent_org=self)
def in_distance(self, position, max_distance, unknown_true=True):
"""
Returns a boolean indicating if the Location of the adoption notice is within a given distance to the position
If the location is none, we by default return that the location is within the given distance
"""
return geo.object_in_distance(self, position, max_distance, unknown_true)
# 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 +326,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):
@@ -280,37 +357,25 @@ class Image(models.Model):
def __str__(self):
return self.alt_text
class Meta:
verbose_name = _("Bild")
verbose_name_plural = _("Bilder")
@property
def as_html(self):
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 = [
("create_active_adoption_notice", "Can create an active adoption notice"),
]
verbose_name = _("Vermittlung")
verbose_name_plural = _("Vermittlungen")
def __str__(self):
if not hasattr(self, 'adoptionnoticestatus'):
return self.name
return f"[{self.adoptionnoticestatus.as_string()}] {self.name}"
return self.name
created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
updated_at = models.DateTimeField(auto_now=True)
@@ -346,7 +411,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
@@ -443,11 +508,7 @@ class AdoptionNotice(models.Model):
If the location is none, we by default return that the location is within the given distance
"""
if unknown_true and self.position is None:
return True
distance = geo.calculate_distance_between_coordinates(self.position, position)
return distance < max_distance
return geo.object_in_distance(self, position, max_distance, unknown_true)
@property
def is_active(self):
@@ -455,16 +516,40 @@ 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_interested(self):
if not hasattr(self, 'adoptionnoticestatus'):
return False
return self.adoptionnoticestatus.is_interested
@property
def is_awaiting_action(self):
if not hasattr(self, 'adoptionnoticestatus'):
return False
return self.adoptionnoticestatus.is_awaiting_action
@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 +574,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):
"""
@@ -496,6 +589,10 @@ class AdoptionNoticeStatus(models.Model):
whereas the minor status is used for reporting
"""
class Meta:
verbose_name = _('Vermittlungsstatus')
verbose_name_plural = _('Vermittlungsstati')
ACTIVE = "active"
AWAITING_ACTION = "awaiting_action"
CLOSED = "closed"
@@ -552,6 +649,22 @@ 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_awaiting_action(self):
return self.major_status == self.AWAITING_ACTION
@property
def is_interested(self):
return self.major_status == self.ACTIVE and self.minor_status == "interested"
@property
def is_disabled_unchecked(self):
return self.major_status == self.DISABLED and self.minor_status == "unchecked"
@@ -569,9 +682,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):
@@ -603,6 +719,10 @@ class SexChoicesWithAll(models.TextChoices):
class Animal(models.Model):
class Meta:
verbose_name = _('Tier')
verbose_name_plural = _('Tiere')
date_of_birth = models.DateField(verbose_name=_('Geburtsdatum'))
name = models.CharField(max_length=200)
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
@@ -668,6 +788,11 @@ class SearchSubscription(models.Model):
- On new AdoptionNotice: Check all existing SearchSubscriptions for matches
- For matches: Send notification to user of the SearchSubscription
"""
class Meta:
verbose_name = _("Abonnierte Suche")
verbose_name_plural = _("Abonnierte Suchen")
owner = models.ForeignKey(User, on_delete=models.CASCADE)
location = models.ForeignKey(Location, on_delete=models.PROTECT, null=True)
sex = models.CharField(max_length=20, choices=SexChoicesWithAll.choices)
@@ -686,6 +811,11 @@ class Rule(models.Model):
"""
Class to store rules
"""
class Meta:
verbose_name = _("Regel")
verbose_name_plural = _("Regeln")
title = models.CharField(max_length=200)
# Markdown is allowed in rule text
@@ -702,7 +832,8 @@ class Rule(models.Model):
class Report(models.Model):
class Meta:
permissions = []
verbose_name = _("Meldung")
verbose_name_plural = _("Meldungen")
ACTION_TAKEN = "action taken"
NO_ACTION_TAKEN = "no action taken"
@@ -727,6 +858,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()
@@ -779,6 +913,10 @@ class ReportComment(Report):
class ModerationAction(models.Model):
class Meta:
verbose_name = _("Moderationsaktion")
verbose_name_plural = _("Moderationsaktionen")
BAN = "user_banned"
DELETE = "content_deleted"
COMMENT = "comment"
@@ -843,6 +981,11 @@ class Announcement(Text):
"""
Class to store announcements that should be displayed for all users
"""
class Meta:
verbose_name = _("Banner")
verbose_name_plural = _("Banner")
logged_in_only = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -892,6 +1035,11 @@ class Comment(models.Model):
"""
Class to store comments in markdown content
"""
class Meta:
verbose_name = _("Kommentar")
verbose_name_plural = _("Kommentare")
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -910,33 +1058,28 @@ 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):
class Meta:
verbose_name = _("Benachrichtigung")
verbose_name_plural = _("Benachrichtigungen")
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,8 +1101,17 @@ 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):
"""Subscription to a AdoptionNotice"""
class Meta:
verbose_name = _("Abonnement")
verbose_name_plural = _("Abonnements")
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
created_at = models.DateTimeField(auto_now_add=True)
@@ -987,6 +1139,11 @@ class Timestamp(models.Model):
"""
Class to store timestamps based on keys
"""
class Meta:
verbose_name = _("Zeitstempel")
verbose_name_plural = _("Zeitstempel")
key = models.CharField(max_length=255, verbose_name=_("Schlüssel"), primary_key=True)
timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("Zeitstempel"))
data = models.CharField(max_length=2000, blank=True, null=True)
@@ -999,19 +1156,33 @@ class SpeciesSpecificURL(models.Model):
"""
Model that allows to specify a URL for a rescue organization where a certain species can be found
"""
class Meta:
verbose_name = _("Tierartspezifische URL")
verbose_name_plural = _("Tierartspezifische URLs")
species = models.ForeignKey(Species, on_delete=models.CASCADE, verbose_name=_("Tierart"))
rescue_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE,
verbose_name=_("Tierschutzorganisation"))
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

@@ -0,0 +1,11 @@
/* mousetrap v1.6.5 craig.is/killing/mice */
(function(q,u,c){function v(a,b,g){a.addEventListener?a.addEventListener(b,g,!1):a.attachEvent("on"+b,g)}function z(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return n[a.which]?n[a.which]:r[a.which]?r[a.which]:String.fromCharCode(a.which).toLowerCase()}function F(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function w(a){return"shift"==a||"ctrl"==a||"alt"==a||
"meta"==a}function A(a,b){var g,d=[];var e=a;"+"===e?e=["+"]:(e=e.replace(/\+{2}/g,"+plus"),e=e.split("+"));for(g=0;g<e.length;++g){var m=e[g];B[m]&&(m=B[m]);b&&"keypress"!=b&&C[m]&&(m=C[m],d.push("shift"));w(m)&&d.push(m)}e=m;g=b;if(!g){if(!p){p={};for(var c in n)95<c&&112>c||n.hasOwnProperty(c)&&(p[n[c]]=c)}g=p[e]?"keydown":"keypress"}"keypress"==g&&d.length&&(g="keydown");return{key:m,modifiers:d,action:g}}function D(a,b){return null===a||a===u?!1:a===b?!0:D(a.parentNode,b)}function d(a){function b(a){a=
a||{};var b=!1,l;for(l in p)a[l]?b=!0:p[l]=0;b||(x=!1)}function g(a,b,t,f,g,d){var l,E=[],h=t.type;if(!k._callbacks[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(l=0;l<k._callbacks[a].length;++l){var c=k._callbacks[a][l];if((f||!c.seq||p[c.seq]==c.level)&&h==c.action){var e;(e="keypress"==h&&!t.metaKey&&!t.ctrlKey)||(e=c.modifiers,e=b.sort().join(",")===e.sort().join(","));e&&(e=f&&c.seq==f&&c.level==d,(!f&&c.combo==g||e)&&k._callbacks[a].splice(l,1),E.push(c))}}return E}function c(a,b,c,f){k.stopCallback(b,
b.target||b.srcElement,c,f)||!1!==a(b,c)||(b.preventDefault?b.preventDefault():b.returnValue=!1,b.stopPropagation?b.stopPropagation():b.cancelBubble=!0)}function e(a){"number"!==typeof a.which&&(a.which=a.keyCode);var b=z(a);b&&("keyup"==a.type&&y===b?y=!1:k.handleKey(b,F(a),a))}function m(a,g,t,f){function h(c){return function(){x=c;++p[a];clearTimeout(q);q=setTimeout(b,1E3)}}function l(g){c(t,g,a);"keyup"!==f&&(y=z(g));setTimeout(b,10)}for(var d=p[a]=0;d<g.length;++d){var e=d+1===g.length?l:h(f||
A(g[d+1]).action);n(g[d],e,f,a,d)}}function n(a,b,c,f,d){k._directMap[a+":"+c]=b;a=a.replace(/\s+/g," ");var e=a.split(" ");1<e.length?m(a,e,b,c):(c=A(a,c),k._callbacks[c.key]=k._callbacks[c.key]||[],g(c.key,c.modifiers,{type:c.action},f,a,d),k._callbacks[c.key][f?"unshift":"push"]({callback:b,modifiers:c.modifiers,action:c.action,seq:f,level:d,combo:a}))}var k=this;a=a||u;if(!(k instanceof d))return new d(a);k.target=a;k._callbacks={};k._directMap={};var p={},q,y=!1,r=!1,x=!1;k._handleKey=function(a,
d,e){var f=g(a,d,e),h;d={};var k=0,l=!1;for(h=0;h<f.length;++h)f[h].seq&&(k=Math.max(k,f[h].level));for(h=0;h<f.length;++h)f[h].seq?f[h].level==k&&(l=!0,d[f[h].seq]=1,c(f[h].callback,e,f[h].combo,f[h].seq)):l||c(f[h].callback,e,f[h].combo);f="keypress"==e.type&&r;e.type!=x||w(a)||f||b(d);r=l&&"keydown"==e.type};k._bindMultiple=function(a,b,c){for(var d=0;d<a.length;++d)n(a[d],b,c)};v(a,"keypress",e);v(a,"keydown",e);v(a,"keyup",e)}if(q){var n={8:"backspace",9:"tab",13:"enter",16:"shift",17:"ctrl",
18:"alt",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"ins",46:"del",91:"meta",93:"meta",224:"meta"},r={106:"*",107:"+",109:"-",110:".",111:"/",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},C={"~":"`","!":"1","@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0",_:"-","+":"=",":":";",'"':"'","<":",",">":".","?":"/","|":"\\"},B={option:"alt",command:"meta","return":"enter",
escape:"esc",plus:"+",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p;for(c=1;20>c;++c)n[111+c]="f"+c;for(c=0;9>=c;++c)n[c+96]=c.toString();d.prototype.bind=function(a,b,c){a=a instanceof Array?a:[a];this._bindMultiple.call(this,a,b,c);return this};d.prototype.unbind=function(a,b){return this.bind.call(this,a,function(){},b)};d.prototype.trigger=function(a,b){if(this._directMap[a+":"+b])this._directMap[a+":"+b]({},a);return this};d.prototype.reset=function(){this._callbacks={};
this._directMap={};return this};d.prototype.stopCallback=function(a,b){if(-1<(" "+b.className+" ").indexOf(" mousetrap ")||D(b,this.target))return!1;if("composedPath"in a&&"function"===typeof a.composedPath){var c=a.composedPath()[0];c!==a.target&&(b=c)}return"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable};d.prototype.handleKey=function(){return this._handleKey.apply(this,arguments)};d.addKeycodes=function(a){for(var b in a)a.hasOwnProperty(b)&&(n[b]=a[b]);p=null};
d.init=function(){var a=d(u),b;for(b in a)"_"!==b.charAt(0)&&(d[b]=function(b){return function(){return a[b].apply(a,arguments)}}(b))};d.init();q.Mousetrap=d;"undefined"!==typeof module&&module.exports&&(module.exports=d);"function"===typeof define&&define.amd&&define(function(){return d})}})("undefined"!==typeof window?window:null,"undefined"!==typeof window?document:null);

View File

@@ -0,0 +1,15 @@
function mark_checked(index) {
document.getElementById('mark_checked_'+index).submit();
}
function open_information(index) {
let link = document.getElementById('species_url_'+index+'_1');
if (!link) {
link = document.getElementById('rescue_org_website_'+index);
}
window.open(link.href);
}
Mousetrap.bind('c', function() { mark_checked(1); });
Mousetrap.bind('o', function() { open_information(1); });

View File

@@ -22,7 +22,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Looks for all notifications with a delete and allows closing them when pressing delete
document.addEventListener('DOMContentLoaded', () => {
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
(document.querySelectorAll('.notification .delete:not(.js-delete-excluded)') || []).forEach(($delete) => {
const $notification = $delete.parentNode;
$delete.addEventListener('click', () => {

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

@@ -30,7 +30,7 @@
<div class="card-header">
<h2 class="card-header-title">{{ faq.title }}</h2>
</div>
<div class="card-content">
<div class="card-content content">
{{ faq.content | render_markdown }}
</div>
</div>

View File

@@ -16,11 +16,27 @@
{% block canonical_url %}{% host %}{% url 'rescue-organizations' %}{% endblock %}
{% block content %}
<div class="block">
<div style="height: 70vh">
{% include "fellchensammlung/partials/partial-map.html" %}
<div class="columns block">
<div class=" column {% if show_search %}is-two-thirds {% endif %}">
<div style="height: 70vh">
{% include "fellchensammlung/partials/partial-map.html" %}
</div>
</div>
{% if show_search %}
<div class="column is-one-third">
<form method="GET" autocomplete="off">
<input type="hidden" name="longitude" maxlength="200" id="longitude">
<input type="hidden" name="latitude" maxlength="200" id="latitude">
<!--- https://docs.djangoproject.com/en/5.2/topics/forms/#reusable-form-templates -->
{{ search_form }}
<button class="button is-primary is-fullwidth" type="submit" value="search" name="action">
<i class="fas fa-search fa-fw"></i> {% trans 'Suchen' %}
</button>
</form>
</div>
{% endif %}
</div>
<div class="block">
{% with rescue_organizations=rescue_organizations_to_list %}
{% include "fellchensammlung/lists/list-animal-shelters.html" %}
@@ -29,16 +45,17 @@
<nav class="pagination" role="navigation" aria-label="{% trans 'Paginierung' %}">
{% if rescue_organizations_to_list.has_previous %}
<a class="pagination-previous"
href="?page={{ rescue_organizations_to_list.previous_page_number }}">{% trans 'Vorherige' %}</a>
href="?page={% url_replace request 'page' rescue_organizations_to_list.previous_page_number %}">{% trans 'Vorherige' %}</a>
{% endif %}
{% if rescue_organizations_to_list.has_next %}
<a class="pagination-next" href="?page={{ rescue_organizations_to_list.next_page_number }}">{% trans 'Nächste' %}</a>
<a class="pagination-next"
href="?{% url_replace request 'page' rescue_organizations_to_list.next_page_number %}">{% trans 'Nächste' %}</a>
{% endif %}
<ul class="pagination-list">
{% for page in elided_page_range %}
{% if page != "…" %}
<li>
<a href="?page={{ page }}"
<a href="?{% url_replace request 'page' page %}"
class="pagination-link {% if page == rescue_organizations_to_list.number %} is-current{% endif %}"
aria-label="{% blocktranslate %}Gehe zu Seite {{ page }}.{% endblocktranslate %}">
{{ page }}

View File

@@ -1,5 +1,6 @@
{% extends "fellchensammlung/base.html" %}
{% load custom_tags %}
{% load admin_urls %}
{% load i18n %}
{% load static %}
@@ -25,6 +26,7 @@
{% endblock %}
{% block content %}
{% include "fellchensammlung/partials/partial-adoption-notice-status.html" %}
<div class="columns">
<div class="column is-two-thirds">
<!--- Title level (including action dropdown) -->
@@ -49,30 +51,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 +112,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>
@@ -160,15 +198,22 @@
<div class="column block">
<div class="card">
<div class="card-header">
<h1 class="card-header-title title is-4">{% translate "Beschreibung" %}</h1>
<h4 class="card-header-title title is-4">{% translate "Beschreibung" %}</h4>
</div>
<div class="card-content">
<div class="card-content content">
<p class="expandable">{% if adoption_notice.description %}
{{ adoption_notice.description | render_markdown }}
{% else %}
{% 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

@@ -32,9 +32,35 @@
{{ org.location_string }}
{% endif %}
{% if org.description %}
<p>{{ org.description | render_markdown }}</p>
<div class="block content">
<p>{{ org.description | render_markdown }}</p>
</div>
{% 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 +68,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,15 +78,43 @@
</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 %}
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
{% endfor %}
{% else %}
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
{% endif %}
</div>
{% with ans_by_status=org.adoption_notices_in_hierarchy_divided_by_status %}
{% with active_ans=ans_by_status.0 inactive_ans=ans_by_status.1 %}
<div class="block">
<h3 class="title is-3">{% translate 'Aktive Vermittlungen' %}</h3>
<div class="container-cards">
{% if active_ans %}
{% for adoption_notice in active_ans %}
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
{% endfor %}
{% else %}
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
{% endif %}
</div>
</div>
<div class="block">
<h3 class="title is-3">{% translate 'Inaktive Vermittlungen' %}</h3>
<div class="container-cards">
{% if inactive_ans %}
{% for adoption_notice in inactive_ans %}
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
{% endfor %}
{% else %}
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
{% endif %}
</div>
</div>
{% endwith %}
{% endwith %}
{% endblock %}

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 content">
{{ 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>
@@ -88,17 +92,19 @@
{% translate 'Tierheime in der Nähe' %}
</a>
<br/>
{% trust_level "MODERATOR" as coordinator_trust_level %}
{% if request.user.trust_level >= coordinator_trust_level %}
<a class="nav-link " href="{% url "modtools" %}">
{% translate 'Moderationstools' %}
</a>
{% endif %}
<br/>
{% if request.user.is_superuser %}
<a class="nav-link " href="{% url "admin:index" %}">
<i class="fa-solid fa-screwdriver-wrench fa-fw"></i>{% translate 'Admin-Bereich' %}
</a>
{% if request.user.is_authenticated %}
{% trust_level "MODERATOR" as coordinator_trust_level %}
{% if request.user.trust_level >= coordinator_trust_level %}
<a class="nav-link " href="{% url "modtools" %}">
{% translate 'Moderationstools' %}
</a>
{% endif %}
<br/>
{% if request.user.is_superuser %}
<a class="nav-link " href="{% url "admin:index" %}">
<i class="fa-solid fa-screwdriver-wrench fa-fw"></i>{% translate 'Admin-Bereich' %}
</a>
{% endif %}
{% endif %}
</div>
</div>

View File

@@ -1,6 +1,9 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% block description %}
<meta name="description" content="{% trans 'Inhalt melden' %}">
{% endblock %}
{% block content %}
<h1 class="title is-1">{% translate "Melden" %}</h1>
Wenn dieser Inhalt nicht unseren <a href='{% url "about" %}'>Regeln</a> entspricht, wähle bitte eine der folgenden Regeln aus und hinterlasse einen Kommentar der es detaillierter erklärt, insbesondere wenn der Regelverstoß nicht offensichtlich ist.

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

@@ -23,7 +23,9 @@
{% endfor %}
{% if introduction %}
<h1>{{ introduction.title }}</h1>
{{ introduction.content | render_markdown }}
<div class="content">
{{ introduction.content | render_markdown }}
</div>
{% endif %}
<h2 class="title is-2">{% translate "Aktuelle Vermittlungen" %}</h2>
@@ -44,7 +46,7 @@
<h2 class="title is-1">{{ how_to.title }}</h2>
</div>
</div>
<div class="card-content">
<div class="card-content content">
{{ how_to.content | render_markdown }}
</div>
</div>

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" %}
@@ -60,7 +71,9 @@
</div>
{% else %}
{% if adoption_notice.description_short %}
{{ adoption_notice.description_short | render_markdown }}
<div class="content">
{{ adoption_notice.description_short | render_markdown }}
</div>
{% endif %}
{% endif %}
</div>

View File

@@ -0,0 +1,43 @@
{% load custom_tags %}
{% load i18n %}
{% 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>
{% elif adoption_notice.is_interested %}
<article class="message is-info">
<div class="message-header">
<p>{% translate 'Tiere sind reserviert' %}</p>
</div>
<div class="message-body content">
{% blocktranslate %}
Diese Tiere sind bereits reserviert.
In den Kommentaren findest du ggf. mehr Informationen.
{% endblocktranslate %}
</div>
</article>
{% elif adoption_notice.is_awaiting_action %}
<article class="message is-warning">
<div class="message-header">
<p>{% translate 'Warten auf Aktivierung' %}</p>
</div>
<div class="message-body content">
{% blocktranslate %}
Diese Vermittlung muss noch durch Moderator*innen aktiviert werden und taucht daher nicht auf der
Startseite auf.
Ggf. fehlen noch relevante Informationen.
{% endblocktranslate %}
</div>
</article>
{% endif %}

View File

@@ -19,13 +19,15 @@
{{ adoption_notice.location_string }}
{% endif %}
</p>
<p>
<div class="content">
{% if adoption_notice.description %}
{{ adoption_notice.description | render_markdown }}
{% else %}
{% translate "Keine Beschreibung" %}
<p>
{% translate "Keine Beschreibung" %}
</p>
{% endif %}
</p>
</div>
{% if adoption_notice.get_photo %}
<div class="adoption-notice-img">
<img src="{{ MEDIA_URL }}/{{ adoption_notice.get_photo.image }}"

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>
@@ -15,18 +15,27 @@
<strong>{% translate 'Zuletzt geprüft:' %}</strong> {{ rescue_org.last_checked_hr }}
</p>
<p>
<i class="fas fa-images" aria-label="{% translate "Material verwenden?" %}"></i> {{ rescue_org.get_allows_using_materials_display }}
<i class="fas fa-images"
aria-label="{% translate "Material verwenden?" %}"></i> {{ rescue_org.get_allows_using_materials_display }}
</p>
{% if rescue_org.website %}
<a href="{{ rescue_org.website }}" target="_blank">
<a href="{{ rescue_org.website }}" id="rescue_org_website_{{ forloop.counter }}" target="_blank">
<i class="fas fa-globe" aria-label="{% translate "Website" %}"></i>
{{ rescue_org.website|domain }}
</a>
{% endif %}
{% for species_url in rescue_org.species_urls %}
<p>{{ species_url.species }}: <a href="{{ species_url.url }}" target="_blank">{{ species_url.url }}</a>
</p>
{% endfor %}
{% with rescue_org_counter=forloop.counter %}
{% for species_url in rescue_org.species_urls %}
<p>{{ species_url.species }}:
<a href="{{ species_url.url }}"
id="species_url_{{ rescue_org_counter }}_{{ forloop.counter }}"
target="_blank">
{{ species_url.url }}
</a>
</p>
{% endfor %}
{% endwith %}
</div>
{% if set_internal_comment_available %}
<div class="block">
@@ -55,7 +64,7 @@
</div>
<div class="card-footer">
<div class="card-footer-item is-confirm">
<form method="post">
<form method="post" id="mark_checked_{{ forloop.counter }}">
{% csrf_token %}
<input type="hidden"
name="rescue_organization_id"

View File

@@ -1,17 +1,19 @@
{% load i18n %}
{% load custom_tags %}
<div class="notification">
<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">
<div class="notification {% if not notification.read %}is-info is-light{% endif %}">
{% if not notification.read %}
<form class="delete js-delete-excluded" 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>
<button class="delete js-delete-excluded" type="submit" id="submit"></button>
</form>
{% endif %}
<div class="notification-header">
<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

@@ -8,18 +8,18 @@
</div>
<div class="card-content">
<p>
<div class="block">
<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 %}
</p>
<p>
</div>
<div class="block content">
{% if rescue_organization.description_short %}
{{ rescue_organization.description_short | render_markdown }}
{% endif %}
</p>
</div>
</div>
</div>

View File

@@ -3,7 +3,7 @@
<div class="card-header">
<h2 class="card-header-title">{{ rule.title }}</h2>
</div>
<div class="card-content">
<p class="content">{{ rule.rule_text | render_markdown }}</p>
<div class="card-content content">
{{ rule.rule_text | render_markdown }}
</div>
</div>

View File

@@ -1,8 +1,14 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load static %}
{% block additional_scrips %}
<script src="{% static 'fellchensammlung/js/mousetrap.min.js' %}"></script>
<script src="{% static 'fellchensammlung/js/rescue-org-check-shortcuts.js' %}"></script>
{% endblock %}
{% 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 +21,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 +43,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,18 @@ 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)
@register.simple_tag
def url_replace(request, field, value):
dict_ = request.GET.copy()
dict_[field] = value
return dict_.urlencode()

View File

@@ -1,5 +1,6 @@
import logging
from random import randint
from notfellchen import settings
from django.utils import timezone
from datetime import timedelta
@@ -139,3 +140,18 @@ def send_test_email(email):
to = email
mail.send_mail(subject, plain_message, from_email, [to], html_message=html_message)
def mask_organization_contact_data(catchall_domain="example.org"):
"""
Masks e-mails, so they are all sent to one domain, preferably a catchall domain.
"""
rescue_orgs_with_phone_number = RescueOrganization.objects.filter(phone_number__isnull=False)
for rescue_org_with_phone_number in rescue_orgs_with_phone_number:
rescue_org_with_phone_number.phone_number = randint(100000000000, 1000000000000)
rescue_org_with_phone_number.save()
rescue_orgs_with_email = RescueOrganization.objects.filter(email__isnull=False)
for rescue_org_with_email in rescue_orgs_with_email:
rescue_org_with_email.email = f"{rescue_org_with_email.email.replace('@', '-')}@{catchall_domain}"
rescue_org_with_email.save()

View File

@@ -0,0 +1,103 @@
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
if response.status_code >= 300:
logging.error(f"Request= {response.request.body}")
logging.error(f"Response= {response.json()}")
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.
"""
MAX_NUM_OF_IMAGES = 6
if len(images) > MAX_NUM_OF_IMAGES:
logging.warning(f"Too many images ({len(images)}) to post. Selecting the first {MAX_NUM_OF_IMAGES} images.")
media_ids = []
for image in images[:MAX_NUM_OF_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

@@ -53,6 +53,19 @@ def calculate_distance_between_coordinates(position1, position2):
return distance_in_km
def object_in_distance(obj, position, max_distance, unknown_true=True):
"""
Returns a boolean indicating if the Location of the object is within a given distance to the position
If the location is none, we by default return that the location is within the given distance
"""
if unknown_true and obj.position is None:
return True
distance = calculate_distance_between_coordinates(obj.position, position)
return distance < max_distance
class ResponseMock:
content = b'[{"place_id":138181499,"licence":"Data \xc2\xa9 OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"relation","osm_id":1247237,"lat":"48.4949904","lon":"9.040330235970146","category":"boundary","type":"postal_code","place_rank":21, "importance":0.12006895017929346,"addresstype":"postcode","name":"72072","display_name":"72072, Derendingen, T\xc3\xbcbingen, Landkreis T\xc3\xbcbingen, Baden-W\xc3\xbcrttemberg, Deutschland", "boundingbox":["48.4949404","48.4950404","9.0402802","9.0403802"]}]'
status_code = 200

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

@@ -2,9 +2,9 @@ import logging
from django.utils.translation import gettext_lazy as _
from .geo import LocationProxy, Position
from ..forms import AdoptionNoticeSearchForm
from ..forms import AdoptionNoticeSearchForm, RescueOrgSearchForm
from ..models import SearchSubscription, AdoptionNotice, SexChoicesWithAll, Location, \
Notification, NotificationTypeChoices
Notification, NotificationTypeChoices, RescueOrganization
def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: bool = True):
@@ -18,7 +18,7 @@ def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: b
return
for search_subscription in SearchSubscription.objects.all():
logging.debug(f"Search subscription {search_subscription} found.")
search = Search(search_subscription=search_subscription)
search = AdoptionNoticeSearch(search_subscription=search_subscription)
if search.adoption_notice_fits_search(adoption_notice):
notification_text = f"{_('Zu deiner Suche')} {search_subscription} wurde eine neue Vermittlung gefunden"
Notification.objects.create(user_to_notify=search_subscription.owner,
@@ -33,7 +33,7 @@ def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: b
logging.info(f"Subscribers for AN {adoption_notice.pk} have been notified\n")
class Search:
class AdoptionNoticeSearch:
def __init__(self, request=None, search_subscription=None):
self.sex = None
self.area_search = None
@@ -45,7 +45,7 @@ class Search:
self.location_string = None
if request:
self.search_from_request(request)
self.adoption_notice_search_from_request(request)
elif search_subscription:
self.search_from_search_subscription(search_subscription)
@@ -103,7 +103,7 @@ class Search:
return adoptions
def search_from_request(self, request):
def adoption_notice_search_from_request(self, request):
if request.method == 'POST':
self.search_form = AdoptionNoticeSearchForm(request.POST)
self.search_form.is_valid()
@@ -157,3 +157,75 @@ class Search:
return False
else:
return True
class RescueOrgSearch:
def __init__(self, request):
self.area_search = None
self.max_distance = None
self.location = None # Can either be Location (DjangoModel) or LocationProxy
self.place_not_found = False # Indicates that a location was given but could not be geocoded
self.search_form = None
# Either place_id or location string must be set for area search
self.location_string = None
self.rescue_org_search_from_request(request)
def __str__(self):
return f"{_('Suche')}: {self.location=}, {self.area_search=}, {self.max_distance=}"
def __eq__(self, other):
"""
Custom equals that also supports SearchSubscriptions
Only allowed to be called for located subscriptions
"""
# If both locations are empty check only the max distance
if self.location is None and other.location is None:
return self.max_distance == other.max_distance
# If one location is empty and the other is not, they are not equal
elif self.location is not None and other.location is None or self.location is None and other.location is not None:
return False
return self.location == other.location and self.max_distance == other.max_distance
def _locate(self):
try:
self.location = LocationProxy(self.location_string)
except ValueError:
self.place_not_found = True
@property
def position(self):
if self.area_search and not self.place_not_found:
return Position(latitude=self.location.latitude, longitude=self.location.longitude)
else:
return None
def rescue_org_fits_search(self, rescue_org: RescueOrganization):
# make sure it's an area search and the place is found to check location
if self.area_search and not self.place_not_found:
# If adoption notice is in not in search distance, return false
if not rescue_org.in_distance(self.location.position, self.max_distance):
logging.debug("Area mismatch")
return False
return True
def get_rescue_orgs(self):
rescue_orgs = RescueOrganization.objects.all()
fitting_rescue_orgs = [rescue_org for rescue_org in rescue_orgs if self.rescue_org_fits_search(rescue_org)]
return fitting_rescue_orgs
def rescue_org_search_from_request(self, request):
if request.method == 'GET' and request.GET.get("action", False) == "search":
self.search_form = RescueOrgSearchForm(request.GET)
self.search_form.is_valid()
if self.search_form.cleaned_data["location_string"] != "" and self.search_form.cleaned_data[
"max_distance"] != "":
self.area_search = True
self.location_string = self.search_form.cleaned_data["location_string"]
self.max_distance = int(self.search_form.cleaned_data["max_distance"])
self._locate()
else:
self.search_form = RescueOrgSearchForm()

View File

@@ -0,0 +1,52 @@
import requests
from fellchensammlung.models import RescueOrganization
def sync_rescue_org_to_twenty(rescue_org: RescueOrganization, base_url, token: str):
if rescue_org.twenty_id:
update = True
else:
update = False
payload = {
"eMails": {
"primaryEmail": rescue_org.email,
"additionalEmails": None
},
"domainName": {
"primaryLinkLabel": rescue_org.website,
"primaryLinkUrl": rescue_org.website,
"additionalLinks": []
},
"name": rescue_org.name,
}
if rescue_org.location:
payload["address"] = {
"addressStreet1": f"{rescue_org.location.street} {rescue_org.location.housenumber}",
"addressCity": rescue_org.location.city,
"addressPostcode": rescue_org.location.postcode,
"addressCountry": rescue_org.location.countrycode,
"addressLat": rescue_org.location.latitude,
"addressLng": rescue_org.location.longitude,
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
}
if update:
url = f"{base_url}/rest/companies/{rescue_org.twenty_id}"
response = requests.patch(url, json=payload, headers=headers)
assert response.status_code == 200
else:
url = f"{base_url}/rest/companies"
response = requests.post(url, json=payload, headers=headers)
try:
assert response.status_code == 201
except AssertionError:
print(response.request.body)
return
rescue_org.twenty_id = response.json()["data"]["createCompany"]["id"]
rescue_org.save()

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, \
@@ -34,7 +36,7 @@ from .tools.admin import clean_locations, get_unchecked_adoption_notices, deacti
from .tasks import post_adoption_notice_save
from rest_framework.authtoken.models import Token
from .tools.search import Search
from .tools.search import AdoptionNoticeSearch, RescueOrgSearch
def user_is_trust_level_or_above(user, trust_level=TrustLevel.MODERATOR):
@@ -42,8 +44,11 @@ def user_is_trust_level_or_above(user, trust_level=TrustLevel.MODERATOR):
def user_is_owner_or_trust_level(user, django_object, trust_level=TrustLevel.MODERATOR):
"""
Checks if a user is either the owner of a record or has a trust level equal or higher than the given one
"""
return user.is_authenticated and (
user.trust_level == trust_level or django_object.owner == user)
user.trust_level >= trust_level or django_object.owner == user)
def fail_if_user_not_owner_or_trust_level(user, django_object, trust_level=TrustLevel.MODERATOR):
@@ -85,8 +90,26 @@ 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":
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 = get_object_or_404(AdoptionNotice, 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 +121,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 +167,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)
@@ -175,7 +200,7 @@ def adoption_notice_edit(request, adoption_notice_id):
def search_important_locations(request, important_location_slug):
i_location = get_object_or_404(ImportantLocation, slug=important_location_slug)
search = Search()
search = AdoptionNoticeSearch()
search.search_from_predefined_i_location(i_location)
site_title = _("Ratten in %(location_name)s") % {"location_name": i_location.name}
@@ -206,8 +231,8 @@ def search(request, templatename="fellchensammlung/search.html"):
# A user just visiting the search site did not search, only upon completing the search form a user has really
# searched. This will toggle the "subscribe" button
searched = False
search = Search()
search.search_from_request(request)
search = AdoptionNoticeSearch()
search.adoption_notice_search_from_request(request)
if request.method == 'POST':
searched = True
if "subscribe_to_search" in request.POST:
@@ -454,6 +479,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 +508,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 +528,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()
@@ -542,13 +571,34 @@ def user_detail(request, user, token=None):
def user_by_id(request, user_id):
user = User.objects.get(id=user_id)
# Only users that are mods or owners of the user are allowed to view
fail_if_user_not_owner_or_trust_level(request.user, user)
fail_if_user_not_owner_or_trust_level(user=request.user, django_object=user, trust_level=TrustLevel.MODERATOR)
if user == request.user:
return my_profile(request)
else:
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 +612,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 +625,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 +648,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 +747,16 @@ 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()
org_search = RescueOrgSearch(request)
rescue_organizations = org_search.get_rescue_orgs()
else:
org_search = None
rescue_organizations = RescueOrganization.objects.filter(specializations=species)
paginator = Paginator(rescue_organizations, 10)
page_number = request.GET.get("page")
@@ -713,10 +771,29 @@ def list_rescue_organizations(request, template='fellchensammlung/animal-shelter
rescue_organizations_to_list = paginator.get_page(page_number)
context = {"rescue_organizations_to_list": rescue_organizations_to_list,
"show_rescue_orgs": True,
"elided_page_range": paginator.get_elided_page_range(page_number, on_each_side=2, on_ends=1)}
"elided_page_range": paginator.get_elided_page_range(page_number, on_each_side=2, on_ends=1),
}
if org_search:
additional_context = {
"show_search": True,
"search_form": org_search.search_form,
"place_not_found": org_search.place_not_found,
"map_center": org_search.position,
"search_center": org_search.position,
"map_pins": [org_search],
"location": org_search.location,
"search_radius": org_search.max_distance,
"zoom_level": zoom_level_for_radius(org_search.max_distance),
}
context.update(additional_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,
template='fellchensammlung/details/detail-rescue-organization.html'):
org = RescueOrganization.objects.get(pk=rescue_organization_id)
@@ -764,12 +841,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 +869,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 +881,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 +889,39 @@ 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:
logging.info(f"Posting adoption notice: {adoption_notice} ({adoption_notice.id})")
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

View File

@@ -30,6 +30,6 @@ urlpatterns += i18n_patterns(
prefix_default_language=False
)
if settings.DEBUG:
if settings.DEBUG: # pragma: no cover
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)

View File

@@ -1,6 +1,14 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% block description %}
{% if next %}
<meta name="description" content="{% trans 'Bei Notfellchen.org einloggen' %}">
{% else %}
<meta name="description" content="{% translate "Bitte log dich ein um diese Seite sehen zu können." %}">
{% endif %}
{% endblock %}
{% block content %}
{% if form.errors %}
@@ -14,15 +22,15 @@
{% if user.is_authenticated %}
<p class="is-warning">{% translate "Du bist bereits eingeloggt." %}</p>
{% else %} {% if next %}
<div class="notification is-warning">
<button class="delete"></button>
<p>
{% translate "Bitte log dich ein um diese Seite sehen zu können." %}
</p>
</div>
{% endif %}
{% else %}
{% if next %}
<div class="notification is-warning">
<button class="delete"></button>
<p>
{% translate "Bitte log dich ein um diese Seite sehen zu können." %}
</p>
</div>
{% endif %}
{% endif %}
{% if not user.is_authenticated %}

View File

@@ -2,12 +2,12 @@ from datetime import timedelta
from django.utils import timezone
from fellchensammlung.tools.admin import get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
deactivate_404_adoption_notices
deactivate_404_adoption_notices, mask_organization_contact_data
from fellchensammlung.tools.misc import is_404
from django.test import TestCase
from model_bakery import baker
from fellchensammlung.models import AdoptionNotice
from fellchensammlung.models import AdoptionNotice, RescueOrganization
class DeactivationTest(TestCase):
@@ -96,3 +96,21 @@ class PingTest(TestCase):
self.adoption2.refresh_from_db()
self.assertTrue(self.adoption1.is_active)
self.assertFalse(self.adoption2.is_active)
class MaskingTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.rescue1 = baker.make(RescueOrganization, email="test1@example.com", )
cls.rescue2 = baker.make(RescueOrganization, email="test-2@example.com", phone_number="0123456789", )
def test_masking(self):
mask_organization_contact_data()
self.assertEqual(RescueOrganization.objects.count(), 2)
# Ensure that the rescues are pulled from the database again, otherwise this test will fail
self.rescue1.refresh_from_db()
self.rescue2.refresh_from_db()
self.assertNotEqual(self.rescue1.phone_number, "0123456789")
self.assertEqual(self.rescue1.email, "test1-example.com@example.org")
self.assertEqual(self.rescue2.email, "test-2-example.com@example.org")

View File

@@ -85,8 +85,8 @@ class TestNotifications(TestCase):
cls.test_user_1 = User.objects.create(username="Testuser1", password="SUPERSECRET", email="test@example.org")
def test_mark_read(self):
not1 = Notification.objects.create(user=self.test_user_1, text="New rats to adopt", title="🔔 New Rat alert")
not2 = Notification.objects.create(user=self.test_user_1,
not1 = Notification.objects.create(user_to_notify=self.test_user_1, text="New rats to adopt", title="🔔 New Rat alert")
not2 = Notification.objects.create(user_to_notify=self.test_user_1,
text="New wombat to adopt", title="🔔 New Wombat alert")
not1.mark_read()

View File

@@ -3,11 +3,12 @@ from time import sleep
from django.test import TestCase
from django.urls import reverse
from fellchensammlung.models import SearchSubscription, User, TrustLevel, AdoptionNotice, Location, SexChoicesWithAll, \
Animal, Species, AdoptionNoticeNotification, SexChoices
Animal, Species, SexChoices, Notification
from model_bakery import baker
from fellchensammlung.tools.geo import LocationProxy
from fellchensammlung.tools.search import Search, notify_search_subscribers
from fellchensammlung.tools.model_helpers import NotificationTypeChoices
from fellchensammlung.tools.search import AdoptionNoticeSearch, notify_search_subscribers
class TestSearch(TestCase):
@@ -71,7 +72,7 @@ class TestSearch(TestCase):
sex=SexChoicesWithAll.ALL,
max_distance=100
)
search1 = Search()
search1 = AdoptionNoticeSearch()
search1.search_position = LocationProxy("Stuttgart").position
search1.max_distance = 100
search1.area_search = True
@@ -82,11 +83,11 @@ class TestSearch(TestCase):
self.assertEqual(search_subscription1, search1)
def test_adoption_notice_fits_search(self):
search1 = Search(search_subscription=self.subscription1)
search1 = AdoptionNoticeSearch(search_subscription=self.subscription1)
self.assertTrue(search1.adoption_notice_fits_search(self.adoption1))
self.assertFalse(search1.adoption_notice_fits_search(self.adoption2))
search2 = Search(search_subscription=self.subscription2)
search2 = AdoptionNoticeSearch(search_subscription=self.subscription2)
self.assertFalse(search2.adoption_notice_fits_search(self.adoption1))
self.assertTrue(search2.adoption_notice_fits_search(self.adoption2))
@@ -100,5 +101,7 @@ class TestSearch(TestCase):
"""
notify_search_subscribers(self.adoption1)
self.assertTrue(AdoptionNoticeNotification.objects.filter(user=self.test_user1, adoption_notice=self.adoption1).exists())
self.assertFalse(AdoptionNoticeNotification.objects.filter(user=self.test_user2).exists())
self.assertTrue(Notification.objects.filter(user_to_notify=self.test_user1,
adoption_notice=self.adoption1,
notification_type=NotificationTypeChoices.AN_FOR_SEARCH_FOUND).exists())
self.assertFalse(Notification.objects.filter(user_to_notify=self.test_user2,).exists())

View File

@@ -1,7 +1,8 @@
from django.test import TestCase
from model_bakery import baker
from fellchensammlung.models import User, TrustLevel, Species, Location, AdoptionNotice, AdoptionNoticeNotification
from fellchensammlung.models import User, TrustLevel, Species, Location, AdoptionNotice, Notification
from fellchensammlung.tools.model_helpers import NotificationTypeChoices
from fellchensammlung.tools.notifications import notify_of_AN_to_be_checked
@@ -24,11 +25,17 @@ class TestNotifications(TestCase):
cls.test_user0.trust_level = TrustLevel.ADMIN
cls.test_user0.save()
cls.adoption1 = baker.make(AdoptionNotice, name="TestAdoption1", owner=cls.test_user1,)
cls.adoption1.set_unchecked() # Could also emit notification
cls.adoption1 = baker.make(AdoptionNotice, name="TestAdoption1", owner=cls.test_user1, )
cls.adoption1.set_unchecked() # Could also emit notification
def test_notify_of_AN_to_be_checked(self):
notify_of_AN_to_be_checked(self.adoption1)
self.assertTrue(AdoptionNoticeNotification.objects.filter(user=self.test_user0).exists())
self.assertTrue(AdoptionNoticeNotification.objects.filter(user=self.test_user1).exists())
self.assertFalse(AdoptionNoticeNotification.objects.filter(user=self.test_user2).exists())
self.assertTrue(Notification.objects.filter(user_to_notify=self.test_user0,
adoption_notice=self.adoption1,
notification_type=NotificationTypeChoices.AN_IS_TO_BE_CHECKED).exists())
self.assertTrue(Notification.objects.filter(user_to_notify=self.test_user1,
adoption_notice=self.adoption1,
notification_type=NotificationTypeChoices.AN_IS_TO_BE_CHECKED).exists())
self.assertFalse(Notification.objects.filter(user_to_notify=self.test_user2,
adoption_notice=self.adoption1,
notification_type=NotificationTypeChoices.AN_IS_TO_BE_CHECKED).exists())

View File

@@ -5,8 +5,9 @@ from django.urls import reverse
from model_bakery import baker
from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus, TrustLevel, \
Animal, Subscriptions, Comment, CommentNotification, SearchSubscription
Animal, Subscriptions, Comment, Notification, SearchSubscription
from fellchensammlung.tools.geo import LocationProxy
from fellchensammlung.tools.model_helpers import NotificationTypeChoices
from fellchensammlung.views import add_adoption_notice
@@ -34,16 +35,7 @@ class AnimalAndAdoptionTest(TestCase):
species=rat,
description="Eine unglaublich süße Ratte")
def test_detail_animal(self):
self.client.login(username='testuser0', password='12345')
response = self.client.post(reverse('animal-detail', args="1"))
self.assertEqual(response.status_code, 200)
# Check our user is logged in
self.assertEqual(str(response.context['user']), 'testuser0')
self.assertContains(response, "Rat1")
def test_detail_animal_notice(self):
def test_detail_adoption_notice(self):
self.client.login(username='testuser0', password='12345')
response = self.client.post(reverse('adoption-notice-detail', args="1"))
@@ -101,91 +93,6 @@ class AnimalAndAdoptionTest(TestCase):
self.assertTrue(an.sexes == set("M", ))
class SearchTest(TestCase):
@classmethod
def setUpTestData(cls):
test_user0 = User.objects.create_user(username='testuser0',
first_name="Admin",
last_name="BOFH",
password='12345')
test_user0.save()
# Location of Berlin: lat 52.5170365 lon 13.3888599 PLZ 10115 (Mitte)
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2")
adoption3 = baker.make(AdoptionNotice, name="TestAdoption3")
berlin = Location.get_location_from_string("Berlin")
adoption1.location = berlin
adoption1.save()
stuttgart = Location.get_location_from_string("Stuttgart")
adoption3.location = stuttgart
adoption3.save()
adoption1.set_active()
adoption3.set_active()
adoption2.set_unchecked()
def test_basic_view(self):
response = self.client.get(reverse('search'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "TestAdoption1")
self.assertNotContains(response, "TestAdoption2")
self.assertContains(response, "TestAdoption3")
def test_basic_view_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('search'))
self.assertEqual(response.status_code, 200)
# Check our user is logged in
self.assertEqual(str(response.context['user']), 'testuser0')
self.assertContains(response, "TestAdoption1")
self.assertContains(response, "TestAdoption3")
self.assertNotContains(response, "TestAdoption2")
def test_unauthenticated_subscribe(self):
response = self.client.post(reverse('search'), {"subscribe_to_search": ""})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/suchen/")
def test_unauthenticated_unsubscribe(self):
response = self.client.post(reverse('search'), {"unsubscribe_to_search": 1})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/suchen/")
def test_subscribe(self):
self.client.login(username='testuser0', password='12345')
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A",
"subscribe_to_search": ""})
self.assertEqual(response.status_code, 200)
self.assertTrue(SearchSubscription.objects.filter(owner=User.objects.get(username='testuser0'),
max_distance=50).exists())
def test_unsubscribe(self):
user0 = User.objects.get(username='testuser0')
self.client.login(username='testuser0', password='12345')
location = Location.get_location_from_string("München")
subscription = SearchSubscription.objects.create(owner=user0, max_distance=200, location=location, sex="A")
response = self.client.post(reverse('search'), {"max_distance": 200, "location_string": "München", "sex": "A",
"unsubscribe_to_search": subscription.pk})
self.assertEqual(response.status_code, 200)
self.assertFalse(SearchSubscription.objects.filter(owner=User.objects.get(username='testuser0'),
max_distance=200).exists())
def test_location_search(self):
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A"})
self.assertEqual(response.status_code, 200)
# We can't use assertContains because TestAdoption3 will always be in response at is included in map
# In order to test properly, we need to only care for the context that influences the list display
an_names = [a.name for a in response.context["adoption_notices"]]
self.assertTrue("TestAdoption1" in an_names) # Adoption in Berlin
self.assertFalse("TestAdoption3" in an_names) # Adoption in Stuttgart
class UpdateQueueTest(TestCase):
@classmethod
def setUpTestData(cls):
@@ -339,8 +246,10 @@ class AdoptionDetailTest(TestCase):
reverse('adoption-notice-detail', args=str(an1.pk)),
data={"action": "comment", "text": "Test"})
self.assertTrue(Comment.objects.filter(user__username="testuser0").exists())
self.assertFalse(CommentNotification.objects.filter(user__username="testuser0").exists())
self.assertTrue(CommentNotification.objects.filter(user__username="testuser1").exists())
self.assertFalse(Notification.objects.filter(user_to_notify__username="testuser0",
notification_type=NotificationTypeChoices.NEW_COMMENT).exists())
self.assertTrue(Notification.objects.filter(user_to_notify__username="testuser1",
notification_type=NotificationTypeChoices.NEW_COMMENT).exists())
class AdoptionEditTest(TestCase):

View File

@@ -2,7 +2,8 @@ from django.test import TestCase
from django.urls import reverse
from docs.conf import language
from fellchensammlung.models import User, TrustLevel, AdoptionNotice, Species, Rule, Language, Comment, ReportComment
from fellchensammlung.models import User, TrustLevel, AdoptionNotice, Species, Rule, Language, Comment, ReportComment, \
Location, ImportantLocation
from model_bakery import baker
@@ -38,7 +39,11 @@ class BasicViewTest(TestCase):
report_comment1 = ReportComment.objects.create(reported_comment=comment1,
user_comment="ReportComment1")
report_comment1.save()
report_comment1.reported_broken_rules.set({rule1,})
report_comment1.reported_broken_rules.set({rule1, })
berlin = Location.get_location_from_string("Berlin")
cls.important_berlin = ImportantLocation(location=berlin, slug="berlin", name="Berlin")
cls.important_berlin.save()
def test_index_logged_in(self):
self.client.login(username='testuser0', password='12345')
@@ -60,11 +65,19 @@ class BasicViewTest(TestCase):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('about'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Rule 1")
def test_about_anonymous(self):
response = self.client.get(reverse('about'))
self.assertEqual(response.status_code, 200)
def terms_of_service_logged_in(self):
response = self.client.get(reverse('terms-of-service'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Rule 1")
def terms_of_service_anonymous(self):
response = self.client.get(reverse('terms-of-service'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Rule 1")
def test_report_adoption_logged_in(self):
@@ -133,4 +146,55 @@ class BasicViewTest(TestCase):
self.assertContains(response, "ReportComment1")
self.assertContains(response, '<form action="allow" class="">')
def test_rss_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('rss'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "TestAdoption0")
self.assertNotContains(response, "TestAdoption5") # Should not be active, therefore not shown
def test_rss_anonymous(self):
response = self.client.get(reverse('rss'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "TestAdoption1")
self.assertNotContains(response, "TestAdoption5")
def test_an_form_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('add-adoption'))
self.assertEqual(response.status_code, 200)
def test_an_form_anonymous(self):
response = self.client.get(reverse('add-adoption'))
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/vermitteln/")
def test_important_location_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('search-by-location', args=(self.important_berlin.slug,)))
self.assertEqual(response.status_code, 200)
def test_important_location_anonymous(self):
response = self.client.get(reverse('search-by-location', args=(self.important_berlin.slug,)))
self.assertEqual(response.status_code, 200)
def test_map_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('map'))
self.assertEqual(response.status_code, 200)
def test_map_anonymous(self):
response = self.client.get(reverse('map'))
self.assertEqual(response.status_code, 200)
def test_metrics_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('metrics'))
self.assertEqual(response.status_code, 200)
def test_metrics_anonymous(self):
response = self.client.get(reverse('metrics'))
self.assertEqual(response.status_code, 200)

View File

@@ -0,0 +1,113 @@
from django.test import TestCase
from django.contrib.auth.models import Permission
from django.urls import reverse
from model_bakery import baker
from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus, TrustLevel, \
Animal, Subscriptions, Comment, Notification, SearchSubscription
from fellchensammlung.tools.geo import LocationProxy
from fellchensammlung.tools.model_helpers import NotificationTypeChoices
from fellchensammlung.views import add_adoption_notice
class SearchTest(TestCase):
@classmethod
def setUpTestData(cls):
test_user0 = User.objects.create_user(username='testuser0',
first_name="Max",
last_name="BOFH",
password='12345')
test_user0.save()
test_user1 = User.objects.create_user(username='testuser1',
first_name="Moritz",
last_name="BOFH",
password='12345')
test_user1.save()
# Location of Berlin: lat 52.5170365 lon 13.3888599 PLZ 10115 (Mitte)
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2")
adoption3 = baker.make(AdoptionNotice, name="TestAdoption3")
berlin = Location.get_location_from_string("Berlin")
adoption1.location = berlin
adoption1.save()
stuttgart = Location.get_location_from_string("Stuttgart")
adoption3.location = stuttgart
adoption3.save()
adoption1.set_active()
adoption3.set_active()
adoption2.set_unchecked()
cls.subscription1 = SearchSubscription.objects.create(owner=test_user1,
max_distance=200,
location=stuttgart,
sex="A")
def test_basic_view(self):
response = self.client.get(reverse('search'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "TestAdoption1")
self.assertNotContains(response, "TestAdoption2")
self.assertContains(response, "TestAdoption3")
def test_basic_view_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('search'))
self.assertEqual(response.status_code, 200)
# Check our user is logged in
self.assertEqual(str(response.context['user']), 'testuser0')
self.assertContains(response, "TestAdoption1")
self.assertContains(response, "TestAdoption3")
self.assertNotContains(response, "TestAdoption2")
def test_unauthenticated_subscribe(self):
response = self.client.post(reverse('search'), {"subscribe_to_search": ""})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/suchen/")
def test_unauthenticated_unsubscribe(self):
response = self.client.post(reverse('search'), {"unsubscribe_to_search": 1})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/suchen/")
def test_unauthorized_unsubscribe(self):
self.client.login(username='testuser0', password='12345')
# This should not be allowed as the subscription owner is different than the request user
response = self.client.post(reverse('search'), {"unsubscribe_to_search": self.subscription1.id})
self.assertEqual(response.status_code, 403)
def test_subscribe(self):
self.client.login(username='testuser0', password='12345')
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A",
"subscribe_to_search": ""})
self.assertEqual(response.status_code, 200)
self.assertTrue(SearchSubscription.objects.filter(owner=User.objects.get(username='testuser0'),
max_distance=50).exists())
def test_unsubscribe(self):
user0 = User.objects.get(username='testuser0')
self.client.login(username='testuser0', password='12345')
location = Location.get_location_from_string("München")
subscription = SearchSubscription.objects.create(owner=user0, max_distance=200, location=location, sex="A")
response = self.client.post(reverse('search'), {"max_distance": 200, "location_string": "München", "sex": "A",
"unsubscribe_to_search": subscription.pk})
self.assertEqual(response.status_code, 200)
self.assertFalse(SearchSubscription.objects.filter(owner=User.objects.get(username='testuser0'),
max_distance=200).exists())
def test_location_search(self):
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A"})
self.assertEqual(response.status_code, 200)
# We can't use assertContains because TestAdoption3 will always be in response at is included in map
# In order to test properly, we need to only care for the context that influences the list display
an_names = [a.name for a in response.context["adoption_notices"]]
self.assertTrue("TestAdoption1" in an_names) # Adoption in Berlin
self.assertFalse("TestAdoption3" in an_names) # Adoption in Stuttgart

View File

@@ -0,0 +1,73 @@
from django.test import TestCase
from django.urls import reverse
from rest_framework.authtoken.models import Token
from model_bakery import baker
from fellchensammlung.models import AdoptionNotice, User, TrustLevel, Notification
class UserTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.test_user0 = User.objects.create_user(username='testuser0',
first_name="Admin",
last_name="BOFH",
password='12345')
cls.test_user0.trust_level = TrustLevel.ADMIN
cls.test_user0.save()
cls.test_user1 = User.objects.create_user(username='testuser1',
first_name="Max",
last_name="Müller",
password='12345')
cls.test_user2 = User.objects.create_user(username='testuser2',
first_name="Mira",
last_name="Müller",
password='12345')
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1", owner=cls.test_user0)
notification1 = baker.make(Notification,
title="TestNotification1",
user_to_notify=cls.test_user0,
adoption_notice=adoption1)
notification2 = baker.make(Notification, title="TestNotification1", user_to_notify=cls.test_user1)
notification3 = baker.make(Notification, title="TestNotification1", user_to_notify=cls.test_user2)
token = baker.make(Token, user=cls.test_user0)
def test_detail_self(self):
self.client.login(username='testuser1', password='12345')
response = self.client.post(reverse('user-me'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Max")
def test_detail_self_via_id(self):
self.client.login(username='testuser1', password='12345')
response = self.client.post(reverse('user-detail', args=str(self.test_user1.pk)))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Max")
def test_detail_admin_with_token(self):
self.client.login(username='testuser0', password='12345')
response = self.client.post(reverse('user-me'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, Token.objects.get(user=self.test_user0).key)
def test_detail_unauthenticated(self):
response = self.client.get(reverse('user-me'))
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/user/me/")
def test_detail_unauthorized(self):
self.client.login(username='testuser2', password='12345')
response = self.client.get(reverse('user-detail', args=str(self.test_user1.pk)))
self.assertEqual(response.status_code, 403)
def test_detail_authorized(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('user-detail', args=str(self.test_user1.pk)))
self.assertEqual(response.status_code, 200)