Compare commits
75 Commits
20cbb0397a
...
develop
Author | SHA1 | Date | |
---|---|---|---|
a372be4af2 | |||
5d333b28ab | |||
84ad047c01 | |||
c93b2631cb | |||
15dd06a91f | |||
30ff26c7ef | |||
1434e7502a | |||
93b21fb7d0 | |||
e5c82f392c | |||
0626964461 | |||
23a724e390 | |||
2a9c7cf854 | |||
335630e16d | |||
6051f7c294 | |||
c1ea6cd211 | |||
6c43b46007 | |||
dc9e68c4b9 | |||
4b03f99971 | |||
426f4b3d8b | |||
3604233507 | |||
8c5099f14a | |||
d5bc348453 | |||
bce98cb439 | |||
1ed3d27533 | |||
39a098af8e | |||
62491b84c1 | |||
81f7f5bb5d | |||
8ce4122160 | |||
370ad2ce66 | |||
f25c425d85 | |||
d921623f31 | |||
2589f1c703 | |||
0edb9094c4 | |||
bc8feba701 | |||
f37d74a7d1 | |||
fa8612ad1a | |||
1d8a054b06 | |||
5898fbf86d | |||
cd1cdd2e0b | |||
c0f920544b | |||
36c90531a8 | |||
7f7c5a3b04 | |||
c084e56ad8 | |||
84acc3c76e | |||
e1f0014898 | |||
05b3a470f3 | |||
ebe060646a | |||
bb412be8d3 | |||
e3c48eac24 | |||
da89cdceda | |||
5a6c2c99e5 | |||
9f53836ce8 | |||
5d53d1a1dc | |||
e00dda1dc2 | |||
a93e0c819f | |||
c87733b37a | |||
9aa964bf05 | |||
dcb1d3ec15 | |||
5d9b8f3213 | |||
d12989d195 | |||
a9f384b50e | |||
afedf2d0bd | |||
a4b8486bd4 | |||
d8bcb8ece6 | |||
b01ac219a3 | |||
42320866c4 | |||
e2e6c14d57 | |||
4761c38cd2 | |||
e2bef3efe2 | |||
bbfd4c3800 | |||
b671d8fbb4 | |||
1ea04e98e8 | |||
c1a7d6790b | |||
f519f78922 | |||
551b5ed6be |
@@ -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()
|
||||
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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"))
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
19
src/fellchensammlung/management/commands/sync_to_twenty.py
Normal file
19
src/fellchensammlung/management/commands/sync_to_twenty.py
Normal 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)
|
@@ -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'),
|
||||
),
|
||||
]
|
@@ -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'),
|
||||
),
|
||||
]
|
@@ -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',
|
||||
),
|
||||
]
|
25
src/fellchensammlung/migrations/0058_socialmediapost.py
Normal file
25
src/fellchensammlung/migrations/0058_socialmediapost.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
@@ -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'),
|
||||
),
|
||||
]
|
@@ -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}"
|
||||
|
@@ -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()
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
11
src/fellchensammlung/static/fellchensammlung/js/mousetrap.min.js
vendored
Normal file
11
src/fellchensammlung/static/fellchensammlung/js/mousetrap.min.js
vendored
Normal 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);
|
@@ -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); });
|
@@ -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', () => {
|
||||
|
@@ -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)
|
||||
|
@@ -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>
|
||||
|
@@ -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 }}
|
||||
|
@@ -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>
|
||||
|
@@ -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 %}
|
||||
|
@@ -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 %}
|
@@ -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 %}
|
||||
|
@@ -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>
|
||||
|
@@ -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.
|
||||
|
@@ -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 }}
|
||||
|
@@ -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>
|
||||
|
@@ -54,6 +54,11 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
.setting-info {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.content, .header, .footer {
|
||||
padding: 20px 15px;
|
||||
|
@@ -0,0 +1,4 @@
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
---
|
||||
{% include "fellchensammlung/mail/footer.txt" %}
|
@@ -1,3 +1,12 @@
|
||||
{% load i18n %}
|
||||
<div class="footer">
|
||||
🐀 notfellchen.org | Für Menschen die Ratten aus dem Tierschutz ein liebendes Zuhause geben wollen.
|
||||
{% 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>
|
@@ -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 %}
|
@@ -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 %}
|
@@ -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 %}
|
@@ -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 %}
|
@@ -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 %}
|
@@ -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 %}
|
@@ -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 %}
|
@@ -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 %}
|
@@ -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 %}
|
@@ -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 %}
|
@@ -0,0 +1,8 @@
|
||||
### {{ adoption_notice.name }}
|
||||
|
||||
📍 {{ adoption_notice.location }}
|
||||
|
||||
|
||||
{{ adoption_notice.description }}
|
||||
|
||||
{{ adoption_notice.get_full_url}}
|
@@ -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>
|
||||
|
||||
|
@@ -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 %}
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
||||
|
@@ -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 %}
|
||||
|
@@ -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 }}"
|
||||
|
@@ -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"
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
@@ -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>
|
@@ -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">
|
||||
|
@@ -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()
|
||||
|
@@ -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()
|
||||
|
||||
|
103
src/fellchensammlung/tools/fedi.py
Normal file
103
src/fellchensammlung/tools/fedi.py
Normal 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
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
||||
|
57
src/fellchensammlung/tools/model_helpers.py
Normal file
57
src/fellchensammlung/tools/model_helpers.py
Normal 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'
|
||||
)
|
||||
}
|
@@ -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.",
|
||||
)
|
||||
|
@@ -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()
|
||||
|
52
src/fellchensammlung/tools/twenty.py
Normal file
52
src/fellchensammlung/tools/twenty.py
Normal 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()
|
@@ -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/',
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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.
|
||||
|
@@ -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 != "":
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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 %}
|
||||
|
@@ -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")
|
||||
|
@@ -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()
|
||||
|
||||
|
@@ -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())
|
||||
|
@@ -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())
|
||||
|
@@ -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):
|
||||
|
@@ -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)
|
||||
|
113
src/tests/test_views/test_search.py
Normal file
113
src/tests/test_views/test_search.py
Normal 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
|
73
src/tests/test_views/test_user_views.py
Normal file
73
src/tests/test_views/test_user_views.py
Normal 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)
|
Reference in New Issue
Block a user