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)
|
location_result = requests.post(f"{instance}/api/locations/", json=location_data, headers=headers)
|
||||||
|
|
||||||
if location_result.status_code != 201:
|
if location_result.status_code != 201:
|
||||||
print(
|
try:
|
||||||
f"Location for {tierheim["properties"]["name"]}:{location_result.status_code} {location_result.json()} not created")
|
print(
|
||||||
|
f"Location for {tierheim["properties"]["name"]}:{location_result.status_code} {location_result.json()} not created")
|
||||||
|
except requests.exceptions.JSONDecodeError:
|
||||||
|
print(f"Location for {tierheim["properties"]["name"]} could not be created")
|
||||||
|
exit()
|
||||||
|
|
||||||
return location_result.json()
|
return location_result.json()
|
||||||
|
|
||||||
|
|
||||||
|
@@ -8,7 +8,7 @@ from django.urls import reverse
|
|||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
|
|
||||||
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
|
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
|
||||||
SpeciesSpecificURL, ImportantLocation, SpeciesSpecialization
|
SpeciesSpecificURL, ImportantLocation, SocialMediaPost
|
||||||
|
|
||||||
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
|
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
|
||||||
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, Notification
|
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, Notification
|
||||||
@@ -100,11 +100,6 @@ class SpeciesSpecificURLInline(admin.StackedInline):
|
|||||||
model = SpeciesSpecificURL
|
model = SpeciesSpecificURL
|
||||||
|
|
||||||
|
|
||||||
class SpeciesSpecializationInline(admin.StackedInline):
|
|
||||||
model = SpeciesSpecialization
|
|
||||||
extra = 0
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(RescueOrganization)
|
@admin.register(RescueOrganization)
|
||||||
class RescueOrganizationAdmin(admin.ModelAdmin):
|
class RescueOrganizationAdmin(admin.ModelAdmin):
|
||||||
search_fields = ("name", "description", "internal_comment", "location_string", "location__city")
|
search_fields = ("name", "description", "internal_comment", "location_string", "location__city")
|
||||||
@@ -112,7 +107,6 @@ class RescueOrganizationAdmin(admin.ModelAdmin):
|
|||||||
list_filter = ("allows_using_materials", "trusted", ("external_source_identifier", EmptyFieldListFilter))
|
list_filter = ("allows_using_materials", "trusted", ("external_source_identifier", EmptyFieldListFilter))
|
||||||
|
|
||||||
inlines = [
|
inlines = [
|
||||||
SpeciesSpecializationInline,
|
|
||||||
SpeciesSpecificURLInline,
|
SpeciesSpecificURLInline,
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -168,6 +162,9 @@ class LocationAdmin(admin.ModelAdmin):
|
|||||||
ImportantLocationInline,
|
ImportantLocationInline,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@admin.register(SocialMediaPost)
|
||||||
|
class SocialMediaPostAdmin(admin.ModelAdmin):
|
||||||
|
list_filter = ("platform",)
|
||||||
|
|
||||||
admin.site.register(Animal)
|
admin.site.register(Animal)
|
||||||
admin.site.register(Species)
|
admin.site.register(Species)
|
||||||
|
@@ -360,9 +360,9 @@ class LocationApiView(APIView):
|
|||||||
|
|
||||||
# Log the action
|
# Log the action
|
||||||
Log.objects.create(
|
Log.objects.create(
|
||||||
user=request.user_to_notify,
|
user=request.user,
|
||||||
action="add_location",
|
action="add_location",
|
||||||
text=f"{request.user_to_notify} added adoption notice {location.pk} via API",
|
text=f"{request.user} added adoption notice {location.pk} via API",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return success response with new adoption notice details
|
# Return success response with new adoption notice details
|
||||||
|
@@ -147,3 +147,11 @@ class AdoptionNoticeSearchForm(forms.Form):
|
|||||||
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED,
|
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED,
|
||||||
label=_("Suchradius"))
|
label=_("Suchradius"))
|
||||||
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
|
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.conf import settings
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from fellchensammlung.models import User, Notification, TrustLevel, NotificationTypeChoices
|
from fellchensammlung.models import User, Notification, TrustLevel, NotificationTypeChoices
|
||||||
from notfellchen.settings import base_url
|
from fellchensammlung.tools.model_helpers import ndm
|
||||||
|
|
||||||
NEWLINE = "\r\n"
|
|
||||||
|
|
||||||
|
|
||||||
def mail_admins_new_report(report):
|
def notify_mods_new_report(report, notification_type):
|
||||||
"""
|
"""
|
||||||
Sends an e-mail to all users that should handle the report.
|
Sends an e-mail to all users that should handle the report.
|
||||||
"""
|
"""
|
||||||
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
|
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
|
||||||
report_url = base_url + report.get_absolute_url()
|
if notification_type == NotificationTypeChoices.NEW_REPORT_AN:
|
||||||
context = {"report_url": report_url,
|
title = _("Vermittlung gemeldet")
|
||||||
"user_comment": report.user_comment, }
|
elif notification_type == NotificationTypeChoices.NEW_REPORT_COMMENT:
|
||||||
|
title = _("Kommentar gemeldet")
|
||||||
subject = _("Neue Meldung")
|
else:
|
||||||
html_message = render_to_string('fellchensammlung/mail/notifications/report.html', context)
|
raise NotImplementedError
|
||||||
plain_message = strip_tags(html_message)
|
notification = Notification.objects.create(
|
||||||
|
notification_type=notification_type,
|
||||||
mail.send_mail(subject,
|
user_to_notify=moderator,
|
||||||
plain_message,
|
report=report,
|
||||||
from_email="info@notfellchen.org",
|
title=title,
|
||||||
recipient_list=[moderator.email],
|
)
|
||||||
html_message=html_message)
|
notification.save()
|
||||||
|
|
||||||
|
|
||||||
def send_notification_email(notification_pk):
|
def send_notification_email(notification_pk):
|
||||||
@@ -37,24 +35,9 @@ def send_notification_email(notification_pk):
|
|||||||
|
|
||||||
subject = f"{notification.title}"
|
subject = f"{notification.title}"
|
||||||
context = {"notification": notification, }
|
context = {"notification": notification, }
|
||||||
if notification.notification_type == NotificationTypeChoices.NEW_REPORT_COMMENT or notification.notification_type == NotificationTypeChoices.NEW_REPORT_AN:
|
html_message = render_to_string(ndm[notification.notification_type].email_html_template, context)
|
||||||
context["user_comment"] = notification.report.user_comment
|
plain_message = render_to_string(ndm[notification.notification_type].email_plain_template, context)
|
||||||
context["report_url"] = f"{base_url}{notification.report.get_absolute_url()}"
|
|
||||||
html_message = render_to_string('fellchensammlung/mail/notifications/report.html', context)
|
|
||||||
elif notification.notification_type == NotificationTypeChoices.NEW_USER:
|
|
||||||
html_message = render_to_string('fellchensammlung/mail/notifications/new-user.html', context)
|
|
||||||
elif notification.notification_type == NotificationTypeChoices.AN_IS_TO_BE_CHECKED:
|
|
||||||
html_message = render_to_string('fellchensammlung/mail/notifications/an-to-be-checked.html', context)
|
|
||||||
elif notification.notification_type == NotificationTypeChoices.AN_WAS_DEACTIVATED:
|
|
||||||
html_message = render_to_string('fellchensammlung/mail/notifications/an-deactivated.html', context)
|
|
||||||
elif notification.notification_type == NotificationTypeChoices.AN_FOR_SEARCH_FOUND:
|
|
||||||
html_message = render_to_string('fellchensammlung/mail/notifications/an-for-search-found.html', context)
|
|
||||||
elif notification.notification_type == NotificationTypeChoices.NEW_COMMENT:
|
|
||||||
html_message = render_to_string('fellchensammlung/mail/notifications/new-comment.html', context)
|
|
||||||
else:
|
|
||||||
raise NotImplementedError("Unknown notification type")
|
|
||||||
|
|
||||||
plain_message = strip_tags(html_message)
|
|
||||||
mail.send_mail(subject, plain_message, settings.DEFAULT_FROM_EMAIL,
|
mail.send_mail(subject, plain_message, settings.DEFAULT_FROM_EMAIL,
|
||||||
[notification.user_to_notify.email],
|
[notification.user_to_notify.email],
|
||||||
html_message=html_message)
|
html_message=html_message)
|
||||||
|
@@ -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 notfellchen.settings import MEDIA_URL, base_url
|
||||||
from .tools.geo import LocationProxy, Position
|
from .tools.geo import LocationProxy, Position
|
||||||
from .tools.misc import age_as_hr_string, time_since_as_hr_string
|
from .tools.misc import age_as_hr_string, time_since_as_hr_string
|
||||||
|
from .tools.model_helpers import NotificationTypeChoices
|
||||||
|
from .tools.model_helpers import ndm as NotificationDisplayMapping
|
||||||
|
|
||||||
|
|
||||||
class Language(models.Model):
|
class Language(models.Model):
|
||||||
@@ -58,6 +60,10 @@ class Location(models.Model):
|
|||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Standort")
|
||||||
|
verbose_name_plural = _("Standorte")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.city and self.postcode:
|
if self.city and self.postcode:
|
||||||
return f"{self.city} ({self.postcode})"
|
return f"{self.city} ({self.postcode})"
|
||||||
@@ -101,10 +107,17 @@ class Location(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class ImportantLocation(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)
|
location = models.OneToOneField(Location, on_delete=models.CASCADE)
|
||||||
slug = models.SlugField(unique=True)
|
slug = models.SlugField(unique=True)
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('search-by-location', kwargs={'important_location_slug': self.slug})
|
||||||
|
|
||||||
|
|
||||||
class ExternalSourceChoices(models.TextChoices):
|
class ExternalSourceChoices(models.TextChoices):
|
||||||
OSM = "OSM", _("Open Street Map")
|
OSM = "OSM", _("Open Street Map")
|
||||||
@@ -118,10 +131,23 @@ class AllowUseOfMaterialsChices(models.TextChoices):
|
|||||||
USE_MATERIALS_NOT_ASKED = "not_asked", _("Not asked")
|
USE_MATERIALS_NOT_ASKED = "not_asked", _("Not asked")
|
||||||
|
|
||||||
|
|
||||||
class RescueOrganization(models.Model):
|
class Species(models.Model):
|
||||||
def __str__(self):
|
"""Model representing a species of animal."""
|
||||||
return f"{self.name}"
|
name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
|
||||||
|
verbose_name=_('Name'))
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""String for representing the Model object."""
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Tierart')
|
||||||
|
verbose_name_plural = _('Tierarten')
|
||||||
|
|
||||||
|
|
||||||
|
class RescueOrganization(models.Model):
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
|
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
|
||||||
allows_using_materials = models.CharField(max_length=200,
|
allows_using_materials = models.CharField(max_length=200,
|
||||||
@@ -149,10 +175,23 @@ class RescueOrganization(models.Model):
|
|||||||
exclude_from_check = models.BooleanField(default=False, verbose_name=_('Von Prüfung ausschließen'),
|
exclude_from_check = models.BooleanField(default=False, verbose_name=_('Von Prüfung ausschließen'),
|
||||||
help_text=_("Organisation von der manuellen Überprüfung ausschließen, "
|
help_text=_("Organisation von der manuellen Überprüfung ausschließen, "
|
||||||
"z.B. weil Tiere nicht online geführt werden"))
|
"z.B. weil Tiere nicht online geführt werden"))
|
||||||
|
ongoing_communication = models.BooleanField(default=False, verbose_name=_('In aktiver Kommunikation'),
|
||||||
|
help_text=_(
|
||||||
|
"Es findet gerade Kommunikation zwischen Notfellchen und der Organisation statt."))
|
||||||
parent_org = models.ForeignKey("RescueOrganization", on_delete=models.PROTECT, blank=True, null=True)
|
parent_org = models.ForeignKey("RescueOrganization", on_delete=models.PROTECT, blank=True, null=True)
|
||||||
|
# allows to specify if a rescue organization has a specialization for dedicated species
|
||||||
|
specializations = models.ManyToManyField(Species, blank=True)
|
||||||
|
twenty_id = models.UUIDField(verbose_name=_("Twenty-ID"), null=True, blank=True,
|
||||||
|
help_text=_("ID der der Organisation in Twenty"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('external_object_identifier', 'external_source_identifier',)
|
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):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
@@ -166,6 +205,29 @@ class RescueOrganization(models.Model):
|
|||||||
def adoption_notices(self):
|
def adoption_notices(self):
|
||||||
return AdoptionNotice.objects.filter(organization=self)
|
return AdoptionNotice.objects.filter(organization=self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def adoption_notices_in_hierarchy(self):
|
||||||
|
"""
|
||||||
|
Shows all adoption notices of this rescue organization and all child organizations.
|
||||||
|
"""
|
||||||
|
adoption_notices_discovered = list(self.adoption_notices)
|
||||||
|
if self.child_organizations:
|
||||||
|
for child in self.child_organizations:
|
||||||
|
adoption_notices_discovered.extend(child.adoption_notices_in_hierarchy)
|
||||||
|
return adoption_notices_discovered
|
||||||
|
|
||||||
|
@property
|
||||||
|
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
|
@property
|
||||||
def position(self):
|
def position(self):
|
||||||
if self.location:
|
if self.location:
|
||||||
@@ -206,6 +268,18 @@ class RescueOrganization(models.Model):
|
|||||||
self.exclude_from_check = True
|
self.exclude_from_check = True
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def child_organizations(self):
|
||||||
|
return RescueOrganization.objects.filter(parent_org=self)
|
||||||
|
|
||||||
|
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
|
# Admins can perform all actions and have the highest trust associated with them
|
||||||
# Moderators can make moderation decisions regarding the deletion of content
|
# Moderators can make moderation decisions regarding the deletion of content
|
||||||
@@ -252,14 +326,17 @@ class User(AbstractUser):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("user-detail", args=[str(self.pk)])
|
return reverse("user-detail", args=[str(self.pk)])
|
||||||
|
|
||||||
|
def get_full_url(self):
|
||||||
|
return f"{base_url}{self.get_absolute_url()}"
|
||||||
|
|
||||||
def get_notifications_url(self):
|
def get_notifications_url(self):
|
||||||
return self.get_absolute_url()
|
return self.get_absolute_url()
|
||||||
|
|
||||||
def get_unread_notifications(self):
|
def get_unread_notifications(self):
|
||||||
return Notification.objects.filter(user=self, read=False)
|
return Notification.objects.filter(user_to_notify=self, read=False)
|
||||||
|
|
||||||
def get_num_unread_notifications(self):
|
def get_num_unread_notifications(self):
|
||||||
return Notification.objects.filter(user=self, read=False).count()
|
return Notification.objects.filter(user_to_notify=self, read=False).count()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def adoption_notices(self):
|
def adoption_notices(self):
|
||||||
@@ -280,37 +357,25 @@ class Image(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.alt_text
|
return self.alt_text
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Bild")
|
||||||
|
verbose_name_plural = _("Bilder")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def as_html(self):
|
def as_html(self):
|
||||||
return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">'
|
return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">'
|
||||||
|
|
||||||
|
|
||||||
class Species(models.Model):
|
|
||||||
"""Model representing a species of animal."""
|
|
||||||
name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
|
|
||||||
verbose_name=_('Name'))
|
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
"""String for representing the Model object."""
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _('Tierart')
|
|
||||||
verbose_name_plural = _('Tierarten')
|
|
||||||
|
|
||||||
|
|
||||||
class AdoptionNotice(models.Model):
|
class AdoptionNotice(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
permissions = [
|
permissions = [
|
||||||
("create_active_adoption_notice", "Can create an active adoption notice"),
|
("create_active_adoption_notice", "Can create an active adoption notice"),
|
||||||
]
|
]
|
||||||
|
verbose_name = _("Vermittlung")
|
||||||
|
verbose_name_plural = _("Vermittlungen")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if not hasattr(self, 'adoptionnoticestatus'):
|
return self.name
|
||||||
return self.name
|
|
||||||
return f"[{self.adoptionnoticestatus.as_string()}] {self.name}"
|
|
||||||
|
|
||||||
created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
|
created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
@@ -346,7 +411,7 @@ class AdoptionNotice(models.Model):
|
|||||||
def num_per_sex(self):
|
def num_per_sex(self):
|
||||||
num_per_sex = dict()
|
num_per_sex = dict()
|
||||||
for sex in SexChoices:
|
for sex in SexChoices:
|
||||||
num_per_sex[sex] = self.animals.filter(sex=sex).count
|
num_per_sex[sex] = self.animals.filter(sex=sex).count()
|
||||||
return num_per_sex
|
return num_per_sex
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -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 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 geo.object_in_distance(self, position, max_distance, unknown_true)
|
||||||
return True
|
|
||||||
|
|
||||||
distance = geo.calculate_distance_between_coordinates(self.position, position)
|
|
||||||
return distance < max_distance
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
@@ -455,16 +516,40 @@ class AdoptionNotice(models.Model):
|
|||||||
return False
|
return False
|
||||||
return self.adoptionnoticestatus.is_active
|
return self.adoptionnoticestatus.is_active
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_disabled(self):
|
||||||
|
if not hasattr(self, 'adoptionnoticestatus'):
|
||||||
|
return False
|
||||||
|
return self.adoptionnoticestatus.is_disabled
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self):
|
||||||
|
if not hasattr(self, 'adoptionnoticestatus'):
|
||||||
|
return False
|
||||||
|
return self.adoptionnoticestatus.is_closed
|
||||||
|
|
||||||
|
@property
|
||||||
|
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
|
@property
|
||||||
def is_disabled_unchecked(self):
|
def is_disabled_unchecked(self):
|
||||||
if not hasattr(self, 'adoptionnoticestatus'):
|
if not hasattr(self, 'adoptionnoticestatus'):
|
||||||
return False
|
return False
|
||||||
return self.adoptionnoticestatus.is_disabled_unchecked
|
return self.adoptionnoticestatus.is_disabled_unchecked
|
||||||
|
|
||||||
def set_closed(self):
|
def set_closed(self, minor_status=None):
|
||||||
self.last_checked = timezone.now()
|
self.last_checked = timezone.now()
|
||||||
self.save()
|
self.save()
|
||||||
self.adoptionnoticestatus.set_closed()
|
self.adoptionnoticestatus.set_closed(minor_status)
|
||||||
|
|
||||||
def set_active(self):
|
def set_active(self):
|
||||||
self.last_checked = timezone.now()
|
self.last_checked = timezone.now()
|
||||||
@@ -489,6 +574,14 @@ class AdoptionNotice(models.Model):
|
|||||||
text=text,
|
text=text,
|
||||||
title=notification_title)
|
title=notification_title)
|
||||||
|
|
||||||
|
def last_posted(self, platform=None):
|
||||||
|
if platform is None:
|
||||||
|
last_post = SocialMediaPost.objects.filter(adoption_notice=self).order_by('-created_at').first()
|
||||||
|
else:
|
||||||
|
last_post = SocialMediaPost.objects.filter(adoption_notice=self, platform=platform).order_by(
|
||||||
|
'-created_at').first()
|
||||||
|
return last_post.created_at
|
||||||
|
|
||||||
|
|
||||||
class AdoptionNoticeStatus(models.Model):
|
class AdoptionNoticeStatus(models.Model):
|
||||||
"""
|
"""
|
||||||
@@ -496,6 +589,10 @@ class AdoptionNoticeStatus(models.Model):
|
|||||||
whereas the minor status is used for reporting
|
whereas the minor status is used for reporting
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Vermittlungsstatus')
|
||||||
|
verbose_name_plural = _('Vermittlungsstati')
|
||||||
|
|
||||||
ACTIVE = "active"
|
ACTIVE = "active"
|
||||||
AWAITING_ACTION = "awaiting_action"
|
AWAITING_ACTION = "awaiting_action"
|
||||||
CLOSED = "closed"
|
CLOSED = "closed"
|
||||||
@@ -552,6 +649,22 @@ class AdoptionNoticeStatus(models.Model):
|
|||||||
def is_active(self):
|
def is_active(self):
|
||||||
return self.major_status == self.ACTIVE
|
return self.major_status == self.ACTIVE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_disabled(self):
|
||||||
|
return self.major_status == self.DISABLED
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self):
|
||||||
|
return self.major_status == self.CLOSED
|
||||||
|
|
||||||
|
@property
|
||||||
|
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
|
@property
|
||||||
def is_disabled_unchecked(self):
|
def is_disabled_unchecked(self):
|
||||||
return self.major_status == self.DISABLED and self.minor_status == "unchecked"
|
return self.major_status == self.DISABLED and self.minor_status == "unchecked"
|
||||||
@@ -569,9 +682,12 @@ class AdoptionNoticeStatus(models.Model):
|
|||||||
minor_status=minor_status,
|
minor_status=minor_status,
|
||||||
adoption_notice=an_instance)
|
adoption_notice=an_instance)
|
||||||
|
|
||||||
def set_closed(self):
|
def set_closed(self, minor_status=None):
|
||||||
self.major_status = self.MAJOR_STATUS_CHOICES[self.CLOSED]
|
self.major_status = self.MAJOR_STATUS_CHOICES[self.CLOSED]
|
||||||
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"]
|
if minor_status is None:
|
||||||
|
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"]
|
||||||
|
else:
|
||||||
|
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED][minor_status]
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def set_unchecked(self):
|
def set_unchecked(self):
|
||||||
@@ -603,6 +719,10 @@ class SexChoicesWithAll(models.TextChoices):
|
|||||||
|
|
||||||
|
|
||||||
class Animal(models.Model):
|
class Animal(models.Model):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Tier')
|
||||||
|
verbose_name_plural = _('Tiere')
|
||||||
|
|
||||||
date_of_birth = models.DateField(verbose_name=_('Geburtsdatum'))
|
date_of_birth = models.DateField(verbose_name=_('Geburtsdatum'))
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
|
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
|
- On new AdoptionNotice: Check all existing SearchSubscriptions for matches
|
||||||
- For matches: Send notification to user of the SearchSubscription
|
- 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)
|
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
location = models.ForeignKey(Location, on_delete=models.PROTECT, null=True)
|
location = models.ForeignKey(Location, on_delete=models.PROTECT, null=True)
|
||||||
sex = models.CharField(max_length=20, choices=SexChoicesWithAll.choices)
|
sex = models.CharField(max_length=20, choices=SexChoicesWithAll.choices)
|
||||||
@@ -686,6 +811,11 @@ class Rule(models.Model):
|
|||||||
"""
|
"""
|
||||||
Class to store rules
|
Class to store rules
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Regel")
|
||||||
|
verbose_name_plural = _("Regeln")
|
||||||
|
|
||||||
title = models.CharField(max_length=200)
|
title = models.CharField(max_length=200)
|
||||||
|
|
||||||
# Markdown is allowed in rule text
|
# Markdown is allowed in rule text
|
||||||
@@ -702,7 +832,8 @@ class Rule(models.Model):
|
|||||||
|
|
||||||
class Report(models.Model):
|
class Report(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
permissions = []
|
verbose_name = _("Meldung")
|
||||||
|
verbose_name_plural = _("Meldungen")
|
||||||
|
|
||||||
ACTION_TAKEN = "action taken"
|
ACTION_TAKEN = "action taken"
|
||||||
NO_ACTION_TAKEN = "no 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."""
|
"""Returns the url to access a detailed page for the report."""
|
||||||
return reverse('report-detail', args=[str(self.id)])
|
return reverse('report-detail', args=[str(self.id)])
|
||||||
|
|
||||||
|
def get_full_url(self):
|
||||||
|
return f"{base_url}{self.get_absolute_url()}"
|
||||||
|
|
||||||
def get_reported_rules(self):
|
def get_reported_rules(self):
|
||||||
return self.reported_broken_rules.all()
|
return self.reported_broken_rules.all()
|
||||||
|
|
||||||
@@ -779,6 +913,10 @@ class ReportComment(Report):
|
|||||||
|
|
||||||
|
|
||||||
class ModerationAction(models.Model):
|
class ModerationAction(models.Model):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Moderationsaktion")
|
||||||
|
verbose_name_plural = _("Moderationsaktionen")
|
||||||
|
|
||||||
BAN = "user_banned"
|
BAN = "user_banned"
|
||||||
DELETE = "content_deleted"
|
DELETE = "content_deleted"
|
||||||
COMMENT = "comment"
|
COMMENT = "comment"
|
||||||
@@ -843,6 +981,11 @@ class Announcement(Text):
|
|||||||
"""
|
"""
|
||||||
Class to store announcements that should be displayed for all users
|
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)
|
logged_in_only = models.BooleanField(default=False)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
@@ -892,6 +1035,11 @@ class Comment(models.Model):
|
|||||||
"""
|
"""
|
||||||
Class to store comments in markdown content
|
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'))
|
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
@@ -910,33 +1058,28 @@ class Comment(models.Model):
|
|||||||
return self.adoption_notice.get_absolute_url()
|
return self.adoption_notice.get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
class NotificationTypeChoices(models.TextChoices):
|
|
||||||
NEW_USER = "new_user", _("Useraccount wurde erstellt")
|
|
||||||
NEW_REPORT_AN = "new_report_an", _("Vermittlung wurde gemeldet")
|
|
||||||
NEW_REPORT_COMMENT = "new_report_comment", _("Kommentar wurde gemeldet")
|
|
||||||
AN_IS_TO_BE_CHECKED = "an_is_to_be_checked", _("Vermittlung muss überprüft werden")
|
|
||||||
AN_WAS_DEACTIVATED = "an_was_deactivated", _("Vermittlung wurde deaktiviert")
|
|
||||||
AN_FOR_SEARCH_FOUND = "an_for_search_found", _("Vermittlung für Suche gefunden")
|
|
||||||
NEW_COMMENT = "new_comment", _("Neuer Kommentar")
|
|
||||||
|
|
||||||
|
|
||||||
class Notification(models.Model):
|
class Notification(models.Model):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Benachrichtigung")
|
||||||
|
verbose_name_plural = _("Benachrichtigungen")
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
read_at = models.DateTimeField(blank=True, null=True, verbose_name=_("Gelesen am"))
|
|
||||||
notification_type = models.CharField(max_length=200,
|
notification_type = models.CharField(max_length=200,
|
||||||
choices=NotificationTypeChoices.choices,
|
choices=NotificationTypeChoices.choices,
|
||||||
verbose_name=_('Benachrichtigungsgrund'))
|
verbose_name=_('Benachrichtigungsgrund'))
|
||||||
title = models.CharField(max_length=100, verbose_name=_("Titel"))
|
|
||||||
text = models.TextField(verbose_name="Inhalt")
|
|
||||||
user_to_notify = models.ForeignKey(User,
|
user_to_notify = models.ForeignKey(User,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
verbose_name=_('Nutzer*in'),
|
verbose_name=_('Empfänger*in'),
|
||||||
help_text=_("Useraccount der Benachrichtigt wird"),
|
help_text=_("Useraccount der Benachrichtigt wird"),
|
||||||
related_name='user')
|
related_name='user')
|
||||||
|
title = models.CharField(max_length=100, verbose_name=_("Titel"))
|
||||||
|
text = models.TextField(verbose_name="Inhalt")
|
||||||
read = models.BooleanField(default=False)
|
read = models.BooleanField(default=False)
|
||||||
|
read_at = models.DateTimeField(blank=True, null=True, verbose_name=_("Gelesen am"))
|
||||||
comment = models.ForeignKey(Comment, blank=True, null=True, on_delete=models.CASCADE, verbose_name=_('Antwort'))
|
comment = models.ForeignKey(Comment, blank=True, null=True, on_delete=models.CASCADE, verbose_name=_('Antwort'))
|
||||||
adoption_notice = models.ForeignKey(AdoptionNotice, blank=True, null=True, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
|
adoption_notice = models.ForeignKey(AdoptionNotice, blank=True, null=True, on_delete=models.CASCADE,
|
||||||
|
verbose_name=_('Vermittlung'))
|
||||||
user_related = models.ForeignKey(User,
|
user_related = models.ForeignKey(User,
|
||||||
blank=True, null=True,
|
blank=True, null=True,
|
||||||
on_delete=models.CASCADE, verbose_name=_('Verwandter Useraccount'),
|
on_delete=models.CASCADE, verbose_name=_('Verwandter Useraccount'),
|
||||||
@@ -958,8 +1101,17 @@ class Notification(models.Model):
|
|||||||
self.read_at = timezone.now()
|
self.read_at = timezone.now()
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
def get_body_part(self):
|
||||||
|
return NotificationDisplayMapping[self.notification_type].web_partial
|
||||||
|
|
||||||
|
|
||||||
class Subscriptions(models.Model):
|
class Subscriptions(models.Model):
|
||||||
|
"""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'))
|
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
|
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
@@ -987,6 +1139,11 @@ class Timestamp(models.Model):
|
|||||||
"""
|
"""
|
||||||
Class to store timestamps based on keys
|
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)
|
key = models.CharField(max_length=255, verbose_name=_("Schlüssel"), primary_key=True)
|
||||||
timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("Zeitstempel"))
|
timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("Zeitstempel"))
|
||||||
data = models.CharField(max_length=2000, blank=True, null=True)
|
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
|
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"))
|
species = models.ForeignKey(Species, on_delete=models.CASCADE, verbose_name=_("Tierart"))
|
||||||
rescue_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE,
|
rescue_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE,
|
||||||
verbose_name=_("Tierschutzorganisation"))
|
verbose_name=_("Tierschutzorganisation"))
|
||||||
url = models.URLField(verbose_name=_("Tierartspezifische URL"))
|
url = models.URLField(verbose_name=_("Tierartspezifische URL"))
|
||||||
|
|
||||||
|
|
||||||
class SpeciesSpecialization(models.Model):
|
class PlatformChoices(models.TextChoices):
|
||||||
"""
|
FEDIVERSE = "fediverse", _("Fediverse")
|
||||||
Model that allows to specify if a rescue organization has a specialization for dedicated species
|
|
||||||
"""
|
|
||||||
species = models.ForeignKey(Species, on_delete=models.CASCADE, verbose_name=_("Tierart"))
|
class SocialMediaPost(models.Model):
|
||||||
rescue_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE,
|
created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
|
||||||
verbose_name=_("Tierschutzorganisation"))
|
platform = models.CharField(max_length=255, verbose_name=_("Social Media Platform"),
|
||||||
|
choices=PlatformChoices.choices)
|
||||||
|
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
|
||||||
|
url = models.URLField(verbose_name=_("URL"))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_an_to_post():
|
||||||
|
adoption_notices_without_post = AdoptionNotice.objects.filter(socialmediapost__isnull=True,
|
||||||
|
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE)
|
||||||
|
return adoption_notices_without_post.first()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{_('Spezialisierung')} {self.species}"
|
return f"{self.platform} - {self.adoption_notice}"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
from django.contrib.sitemaps import Sitemap
|
from django.contrib.sitemaps import Sitemap
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from .models import AdoptionNotice, RescueOrganization
|
from .models import AdoptionNotice, RescueOrganization, ImportantLocation, Animal
|
||||||
|
|
||||||
|
|
||||||
class StaticViewSitemap(Sitemap):
|
class StaticViewSitemap(Sitemap):
|
||||||
@@ -8,7 +8,8 @@ class StaticViewSitemap(Sitemap):
|
|||||||
changefreq = "weekly"
|
changefreq = "weekly"
|
||||||
|
|
||||||
def items(self):
|
def items(self):
|
||||||
return ["index", "search", "map", "about", "rescue-organizations"]
|
return ["index", "search", "map", "about", "rescue-organizations", "buying", "imprint", "terms-of-service",
|
||||||
|
"privacy"]
|
||||||
|
|
||||||
def location(self, item):
|
def location(self, item):
|
||||||
return reverse(item)
|
return reverse(item)
|
||||||
@@ -25,17 +26,6 @@ class AdoptionNoticeSitemap(Sitemap):
|
|||||||
return obj.updated_at
|
return obj.updated_at
|
||||||
|
|
||||||
|
|
||||||
class AnimalSitemap(Sitemap):
|
|
||||||
priority = 0.2
|
|
||||||
changefreq = "daily"
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
return AdoptionNotice.objects.all()
|
|
||||||
|
|
||||||
def lastmod(self, obj):
|
|
||||||
return obj.updated_at
|
|
||||||
|
|
||||||
|
|
||||||
class RescueOrganizationSitemap(Sitemap):
|
class RescueOrganizationSitemap(Sitemap):
|
||||||
priority = 0.3
|
priority = 0.3
|
||||||
changefreq = "weekly"
|
changefreq = "weekly"
|
||||||
@@ -45,3 +35,11 @@ class RescueOrganizationSitemap(Sitemap):
|
|||||||
|
|
||||||
def lastmod(self, obj):
|
def lastmod(self, obj):
|
||||||
return obj.updated_at
|
return obj.updated_at
|
||||||
|
|
||||||
|
|
||||||
|
class SearchSitemap(Sitemap):
|
||||||
|
priority = 0.5
|
||||||
|
chanfreq = "daily"
|
||||||
|
|
||||||
|
def items(self):
|
||||||
|
return ImportantLocation.objects.all()
|
||||||
|
@@ -320,3 +320,29 @@ AN Cards
|
|||||||
background-color: var(--bulma-success-on-scheme);
|
background-color: var(--bulma-success-on-scheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.notification-container {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-label {
|
||||||
|
padding: 2px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make the badge float in the top right corner of the button */
|
||||||
|
.notification-badge {
|
||||||
|
background-color: #fa3e3e;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
padding: 1px 3px;
|
||||||
|
font-size: 8px;
|
||||||
|
|
||||||
|
position: absolute; /* Position the badge within the relatively positioned button */
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// Looks for all notifications with a delete and allows closing them when pressing delete
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
|
(document.querySelectorAll('.notification .delete:not(.js-delete-excluded)') || []).forEach(($delete) => {
|
||||||
const $notification = $delete.parentNode;
|
const $notification = $delete.parentNode;
|
||||||
|
|
||||||
$delete.addEventListener('click', () => {
|
$delete.addEventListener('click', () => {
|
||||||
|
@@ -5,8 +5,9 @@ from django.utils import timezone
|
|||||||
from notfellchen.celery import app as celery_app
|
from notfellchen.celery import app as celery_app
|
||||||
from .mail import send_notification_email
|
from .mail import send_notification_email
|
||||||
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices
|
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices
|
||||||
|
from .tools.fedi import post_an_to_fedi
|
||||||
from .tools.misc import healthcheck_ok
|
from .tools.misc import healthcheck_ok
|
||||||
from .models import Location, AdoptionNotice, Timestamp, RescueOrganization
|
from .models import Location, AdoptionNotice, Timestamp, RescueOrganization, SocialMediaPost
|
||||||
from .tools.notifications import notify_of_AN_to_be_checked
|
from .tools.notifications import notify_of_AN_to_be_checked
|
||||||
from .tools.search import notify_search_subscribers
|
from .tools.search import notify_search_subscribers
|
||||||
|
|
||||||
@@ -38,6 +39,13 @@ def task_deactivate_unchecked():
|
|||||||
set_timestamp("task_deactivate_404_adoption_notices")
|
set_timestamp("task_deactivate_404_adoption_notices")
|
||||||
|
|
||||||
|
|
||||||
|
@celery_app.task(name="social_media.post_fedi")
|
||||||
|
def task_post_to_fedi():
|
||||||
|
adoption_notice = SocialMediaPost.get_an_to_post()
|
||||||
|
post_an_to_fedi(adoption_notice)
|
||||||
|
set_timestamp("task_social_media.post_fedi")
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task(name="commit.post_an_save")
|
@celery_app.task(name="commit.post_an_save")
|
||||||
def post_adoption_notice_save(pk):
|
def post_adoption_notice_save(pk):
|
||||||
instance = AdoptionNotice.objects.get(pk=pk)
|
instance = AdoptionNotice.objects.get(pk=pk)
|
||||||
|
@@ -30,7 +30,7 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="card-header-title">{{ faq.title }}</h2>
|
<h2 class="card-header-title">{{ faq.title }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content content">
|
||||||
{{ faq.content | render_markdown }}
|
{{ faq.content | render_markdown }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -16,11 +16,27 @@
|
|||||||
{% block canonical_url %}{% host %}{% url 'rescue-organizations' %}{% endblock %}
|
{% block canonical_url %}{% host %}{% url 'rescue-organizations' %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="block">
|
<div class="columns block">
|
||||||
<div style="height: 70vh">
|
<div class=" column {% if show_search %}is-two-thirds {% endif %}">
|
||||||
{% include "fellchensammlung/partials/partial-map.html" %}
|
<div style="height: 70vh">
|
||||||
|
{% include "fellchensammlung/partials/partial-map.html" %}
|
||||||
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
{% with rescue_organizations=rescue_organizations_to_list %}
|
{% with rescue_organizations=rescue_organizations_to_list %}
|
||||||
{% include "fellchensammlung/lists/list-animal-shelters.html" %}
|
{% include "fellchensammlung/lists/list-animal-shelters.html" %}
|
||||||
@@ -29,16 +45,17 @@
|
|||||||
<nav class="pagination" role="navigation" aria-label="{% trans 'Paginierung' %}">
|
<nav class="pagination" role="navigation" aria-label="{% trans 'Paginierung' %}">
|
||||||
{% if rescue_organizations_to_list.has_previous %}
|
{% if rescue_organizations_to_list.has_previous %}
|
||||||
<a class="pagination-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 %}
|
{% endif %}
|
||||||
{% if rescue_organizations_to_list.has_next %}
|
{% 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 %}
|
{% endif %}
|
||||||
<ul class="pagination-list">
|
<ul class="pagination-list">
|
||||||
{% for page in elided_page_range %}
|
{% for page in elided_page_range %}
|
||||||
{% if page != "…" %}
|
{% if page != "…" %}
|
||||||
<li>
|
<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 %}"
|
class="pagination-link {% if page == rescue_organizations_to_list.number %} is-current{% endif %}"
|
||||||
aria-label="{% blocktranslate %}Gehe zu Seite {{ page }}.{% endblocktranslate %}">
|
aria-label="{% blocktranslate %}Gehe zu Seite {{ page }}.{% endblocktranslate %}">
|
||||||
{{ page }}
|
{{ page }}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
{% extends "fellchensammlung/base.html" %}
|
{% extends "fellchensammlung/base.html" %}
|
||||||
{% load custom_tags %}
|
{% load custom_tags %}
|
||||||
|
{% load admin_urls %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% include "fellchensammlung/partials/partial-adoption-notice-status.html" %}
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-two-thirds">
|
<div class="column is-two-thirds">
|
||||||
<!--- Title level (including action dropdown) -->
|
<!--- Title level (including action dropdown) -->
|
||||||
@@ -49,30 +51,59 @@
|
|||||||
<!--- Action menu (dropdown) --->
|
<!--- Action menu (dropdown) --->
|
||||||
<div class="dropdown-menu" role="menu">
|
<div class="dropdown-menu" role="menu">
|
||||||
<div class="dropdown-content">
|
<div class="dropdown-content">
|
||||||
|
{% if is_subscribed %}
|
||||||
|
<form class="dropdown-item" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="unsubscribe">
|
||||||
|
|
||||||
|
<button type="submit" id="submit">
|
||||||
|
<i class="fas fa-bell-slash fa-fw"
|
||||||
|
aria-hidden="true"></i> {% trans 'Deabonnieren' %}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<form class="dropdown-item" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="subscribe">
|
||||||
|
|
||||||
|
<button type="submit" id="submit">
|
||||||
|
<i class="fas fa-bell fa-fw"
|
||||||
|
aria-hidden="true"></i> {% trans 'Abonnieren' %}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
|
||||||
{% if has_edit_permission %}
|
{% if has_edit_permission %}
|
||||||
|
|
||||||
<a class="dropdown-item">
|
<form class="dropdown-item" method="POST">
|
||||||
<i class="fas fa-check"
|
{% csrf_token %}
|
||||||
aria-hidden="true"></i> {% trans 'Als aktiv bestätigen' %}
|
<input type="hidden" name="action" value="checked_active">
|
||||||
</a>
|
<button type="submit" id="submit">
|
||||||
|
<i class="fas fa-check fa-fw"
|
||||||
|
aria-hidden="true"></i> {% trans 'Als aktiv bestätigen' %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
<a class="dropdown-item"
|
<a class="dropdown-item"
|
||||||
href="{% url 'adoption-notice-edit' adoption_notice_id=adoption_notice.pk %}">
|
href="{% url 'adoption-notice-edit' adoption_notice_id=adoption_notice.pk %}">
|
||||||
<i class="fas fa-pencil"
|
<i class="fas fa-pencil fa-fw"
|
||||||
aria-hidden="true"></i> {% translate 'Bearbeiten' %}
|
aria-hidden="true"></i> {% translate 'Bearbeiten' %}
|
||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item"
|
<a class="dropdown-item"
|
||||||
href="{% url 'adoption-notice-add-photo' adoption_notice.pk %}">
|
href="{% url 'adoption-notice-add-photo' adoption_notice.pk %}">
|
||||||
<i class="fas fa-image"
|
<i class="fas fa-image fa-fw"
|
||||||
aria-hidden="true"></i> {% trans 'Bilder hinzufügen' %}
|
aria-hidden="true"></i> {% trans 'Bilder hinzufügen' %}
|
||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item"
|
<a class="dropdown-item"
|
||||||
href="{% url 'adoption-notice-add-animal' adoption_notice.pk %}">
|
href="{% url 'adoption-notice-add-animal' adoption_notice.pk %}">
|
||||||
<i class="fas fa-plus"
|
<i class="fas fa-plus fa-fw"
|
||||||
aria-hidden="true"></i> {% trans 'Tier hinzufügen' %}
|
aria-hidden="true"></i> {% trans 'Tier hinzufügen' %}
|
||||||
</a>
|
</a>
|
||||||
<a class="dropdown-item">
|
<a class="dropdown-item"
|
||||||
<i class="fas fa-circle-xmark"
|
href="{% url 'adoption-notice-close' adoption_notice_id=adoption_notice.pk %}">
|
||||||
|
<i class="fas fa-circle-xmark fa-fw"
|
||||||
aria-hidden="true"></i> {% trans 'Deaktivieren' %}
|
aria-hidden="true"></i> {% trans 'Deaktivieren' %}
|
||||||
</a>
|
</a>
|
||||||
<hr class="dropdown-divider">
|
<hr class="dropdown-divider">
|
||||||
@@ -81,6 +112,13 @@
|
|||||||
<i class="fas fa-flag"
|
<i class="fas fa-flag"
|
||||||
aria-hidden="true"></i> {% trans 'Melden' %}
|
aria-hidden="true"></i> {% trans 'Melden' %}
|
||||||
</a>
|
</a>
|
||||||
|
{% if request.user.is_superuser %}
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
<a class="dropdown-item is-warning"
|
||||||
|
href="{% url adoption_notice_meta|admin_urlname:'change' adoption_notice.pk %}">
|
||||||
|
<i class="fa-solid fa-tools fa-fw"></i> Admin interface
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,15 +198,22 @@
|
|||||||
<div class="column block">
|
<div class="column block">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<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>
|
||||||
<div class="card-content">
|
<div class="card-content content">
|
||||||
<p class="expandable">{% if adoption_notice.description %}
|
<p class="expandable">{% if adoption_notice.description %}
|
||||||
{{ adoption_notice.description | render_markdown }}
|
{{ adoption_notice.description | render_markdown }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% translate "Keine Beschreibung angegeben" %}
|
{% translate "Keine Beschreibung angegeben" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
<hr>
|
||||||
|
<p>
|
||||||
|
<strong>
|
||||||
|
{% translate 'Zuletzt auf Aktualität überprüft:' %}
|
||||||
|
</strong>
|
||||||
|
{{ adoption_notice.last_checked|time_since_hr }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -32,9 +32,35 @@
|
|||||||
{{ org.location_string }}
|
{{ org.location_string }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if org.description %}
|
{% if org.description %}
|
||||||
<p>{{ org.description | render_markdown }}</p>
|
<div class="block content">
|
||||||
|
<p>{{ org.description | render_markdown }}</p>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if org.specializations %}
|
||||||
|
<div class="block">
|
||||||
|
<h3 class="title is-5">{% translate 'Spezialisierung' %}</h3>
|
||||||
|
<div class="content">
|
||||||
|
<ul>
|
||||||
|
{% for specialization in org.specializations.all %}
|
||||||
|
<li>{{ specialization }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if org.parent_org %}
|
||||||
|
<div class="block">
|
||||||
|
<h3 class="title is-5">{% translate 'Übergeordnete Organisation' %}</h3>
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
<i class="fa-solid fa-building fa-fw"
|
||||||
|
aria-label="{% trans 'Tierschutzorganisation' %}"></i>
|
||||||
|
<a href="{{ org.parent_org.get_absolute_url }}"> {{ org.parent_org }}</a>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,7 +68,9 @@
|
|||||||
{% include "fellchensammlung/partials/partial-rescue-organization-contact.html" %}
|
{% include "fellchensammlung/partials/partial-rescue-organization-contact.html" %}
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<a class="button is-warning is-fullwidth" href="{% url org_meta|admin_urlname:'change' org.pk %}"><i class="fa-solid fa-tools fa-fw"></i> Admin interface</a>
|
<a class="button is-warning is-fullwidth" href="{% url org_meta|admin_urlname:'change' org.pk %}">
|
||||||
|
<i class="fa-solid fa-tools fa-fw"></i> Admin interface
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
@@ -50,15 +78,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if org.child_organizations %}
|
||||||
|
<div class="block">
|
||||||
|
<h2 class="title is-2">{% translate 'Unterorganisationen' %}</h2>
|
||||||
|
{% with rescue_organizations=org.child_organizations %}
|
||||||
|
{% include "fellchensammlung/lists/list-animal-shelters.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<h2 class="title is-2">{% translate 'Vermittlungen der Organisation' %}</h2>
|
<h2 class="title is-2">{% translate 'Vermittlungen der Organisation' %}</h2>
|
||||||
<div class="container-cards">
|
{% with ans_by_status=org.adoption_notices_in_hierarchy_divided_by_status %}
|
||||||
{% if org.adoption_notices %}
|
{% with active_ans=ans_by_status.0 inactive_ans=ans_by_status.1 %}
|
||||||
{% for adoption_notice in org.adoption_notices %}
|
<div class="block">
|
||||||
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
|
<h3 class="title is-3">{% translate 'Aktive Vermittlungen' %}</h3>
|
||||||
{% endfor %}
|
<div class="container-cards">
|
||||||
{% else %}
|
{% if active_ans %}
|
||||||
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
|
{% for adoption_notice in active_ans %}
|
||||||
{% endif %}
|
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
|
||||||
</div>
|
{% 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 %}
|
{% 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" %}
|
{% extends "fellchensammlung/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load custom_tags %}
|
{% load custom_tags %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="content">
|
<div class="block">
|
||||||
{% if external_site_warning %}
|
<div class="message is-warning">
|
||||||
{{ external_site_warning.content | render_markdown }}
|
{% if external_site_warning %}
|
||||||
{% else %}
|
<h1 class="message-header">
|
||||||
{% blocktranslate %}
|
{{ external_site_warning.title }}
|
||||||
<p>Achtung du verlässt notfellchen.org</p>
|
</h1>
|
||||||
{% endblocktranslate %}
|
<div class="message-body content">
|
||||||
{% endif %}
|
{{ external_site_warning.content | render_markdown }}
|
||||||
<a href="{{ url }}" class="button is-primary">{% translate "Weiter" %}</a>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<h1 class="message-header">
|
||||||
|
{% trans 'Achtung du verlässt notfellchen.org' %}
|
||||||
|
</h1>
|
||||||
|
<div class="message-body">
|
||||||
|
{% trans 'Sichere Abgabebedingungen können von uns, trotz vieler Bemühungen, nicht garantiert werden. Nimm Kontakt zu einer Rattenhilfe oder dem VdRD e.V. auf, die dich beraten können.' %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="block">
|
||||||
|
<a href="{{ url }}" class="button is-primary is-fullwidth">{% translate "Weiter" %}<i class="fa fa-arrow-right fa-fw" aria-hidden="true"></i> </a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@@ -34,6 +34,10 @@
|
|||||||
{% translate 'Das Notfellchen Projekt' %}
|
{% translate 'Das Notfellchen Projekt' %}
|
||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
|
<a href="{% url "buying" %}">
|
||||||
|
{% translate 'Ratten kaufen' %}
|
||||||
|
</a>
|
||||||
|
<br/>
|
||||||
<a href="{% url "terms-of-service" %}">
|
<a href="{% url "terms-of-service" %}">
|
||||||
{% translate 'Nutzungsbedingungen' %}
|
{% translate 'Nutzungsbedingungen' %}
|
||||||
</a>
|
</a>
|
||||||
@@ -88,17 +92,19 @@
|
|||||||
{% translate 'Tierheime in der Nähe' %}
|
{% translate 'Tierheime in der Nähe' %}
|
||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
{% trust_level "MODERATOR" as coordinator_trust_level %}
|
{% if request.user.is_authenticated %}
|
||||||
{% if request.user.trust_level >= coordinator_trust_level %}
|
{% trust_level "MODERATOR" as coordinator_trust_level %}
|
||||||
<a class="nav-link " href="{% url "modtools" %}">
|
{% if request.user.trust_level >= coordinator_trust_level %}
|
||||||
{% translate 'Moderationstools' %}
|
<a class="nav-link " href="{% url "modtools" %}">
|
||||||
</a>
|
{% translate 'Moderationstools' %}
|
||||||
{% endif %}
|
</a>
|
||||||
<br/>
|
{% endif %}
|
||||||
{% if request.user.is_superuser %}
|
<br/>
|
||||||
<a class="nav-link " href="{% url "admin:index" %}">
|
{% if request.user.is_superuser %}
|
||||||
<i class="fa-solid fa-screwdriver-wrench fa-fw"></i>{% translate 'Admin-Bereich' %}
|
<a class="nav-link " href="{% url "admin:index" %}">
|
||||||
</a>
|
<i class="fa-solid fa-screwdriver-wrench fa-fw"></i>{% translate 'Admin-Bereich' %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
{% extends "fellchensammlung/base.html" %}
|
{% extends "fellchensammlung/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block description %}
|
||||||
|
<meta name="description" content="{% trans 'Inhalt melden' %}">
|
||||||
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="title is-1">{% translate "Melden" %}</h1>
|
<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.
|
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>
|
<h1 class="title is-4">notfellchen.org</h1>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a role="button" class="navbar-burger" aria-label="{% trans 'Hauptmenü' %}" tabindex="0" aria-expanded="false" data-target="navbarBasicExample">
|
<a role="button" class="navbar-burger" aria-label="{% trans 'Hauptmenü' %}" tabindex="0" aria-expanded="false"
|
||||||
|
data-target="navbarBasicExample">
|
||||||
<span aria-hidden="true"></span>
|
<span aria-hidden="true"></span>
|
||||||
<span aria-hidden="true"></span>
|
<span aria-hidden="true"></span>
|
||||||
<span aria-hidden="true"></span>
|
<span aria-hidden="true"></span>
|
||||||
@@ -30,7 +31,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
|
<div class="navbar-item">
|
||||||
|
<div class="notification-container">
|
||||||
|
<a class="notification-label" href="{% url 'user-notifications' %}">
|
||||||
|
<i class="fas fa-bell fa-fw"></i>
|
||||||
|
</a>
|
||||||
|
{% if request.user.get_num_unread_notifications > 0 %}
|
||||||
|
<span class="notification-badge">{{ request.user.get_num_unread_notifications }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="navbar-item">
|
<div class="navbar-item">
|
||||||
<a href="{% url 'user-me' %}">
|
<a href="{% url 'user-me' %}">
|
||||||
<i class="fas fa-user fa-fw"></i> {{ user }}
|
<i class="fas fa-user fa-fw"></i> {{ user }}
|
||||||
|
@@ -23,7 +23,9 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if introduction %}
|
{% if introduction %}
|
||||||
<h1>{{ introduction.title }}</h1>
|
<h1>{{ introduction.title }}</h1>
|
||||||
{{ introduction.content | render_markdown }}
|
<div class="content">
|
||||||
|
{{ introduction.content | render_markdown }}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h2 class="title is-2">{% translate "Aktuelle Vermittlungen" %}</h2>
|
<h2 class="title is-2">{% translate "Aktuelle Vermittlungen" %}</h2>
|
||||||
@@ -44,7 +46,7 @@
|
|||||||
<h2 class="title is-1">{{ how_to.title }}</h2>
|
<h2 class="title is-1">{{ how_to.title }}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content content">
|
||||||
{{ how_to.content | render_markdown }}
|
{{ how_to.content | render_markdown }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -54,6 +54,11 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.setting-info {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.content, .header, .footer {
|
.content, .header, .footer {
|
||||||
padding: 20px 15px;
|
padding: 20px 15px;
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
|
---
|
||||||
|
{% include "fellchensammlung/mail/footer.txt" %}
|
@@ -1,3 +1,12 @@
|
|||||||
|
{% load i18n %}
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
🐀 notfellchen.org | Für Menschen die Ratten aus dem Tierschutz ein liebendes Zuhause geben wollen.
|
🐀 notfellchen.org | Für Menschen die Ratten aus dem Tierschutz ein liebendes Zuhause geben wollen.
|
||||||
</div>
|
{% if notification %}
|
||||||
|
<div class="setting-info">
|
||||||
|
{% trans "Du bekommst diese Nachricht basierend auf deinen Benachrichtigungseinstellungen." %}<br>
|
||||||
|
<a href="{{ notification.user_to_notify.get_full_url }}">
|
||||||
|
{% trans "Einstellungen ändern" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
@@ -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
|
Details findest du hier
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ notification.user_related.get_absolute_url }}" class="cta-button">{% translate 'User anzeigen' %}</a>
|
<a href="{{ notification.user_related.get_full_url }}" class="cta-button">{% translate 'User anzeigen' %}</a>
|
||||||
</p>
|
</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
@@ -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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p>Moin,</p>
|
<p>{% translate 'Moin' %},</p>
|
||||||
<p>
|
<p>
|
||||||
es gibt eine neue Meldung. Folgende Nachricht wurde zur Meldung hinzugefügt:
|
{% blocktranslate %}
|
||||||
|
es gibt eine neue Meldung.
|
||||||
|
{% endblocktranslate %}
|
||||||
|
{% if notification.report.user_comment %}
|
||||||
|
{% blocktranslate %}
|
||||||
|
Folgende Nachricht wurde zur Meldung hinzugefügt:
|
||||||
|
{% endblocktranslate %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
{% if notification.report.user_comment %}
|
||||||
|
<p>
|
||||||
|
<i>
|
||||||
|
{{ notification.report.user_comment }}
|
||||||
|
</i>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
<p>
|
<p>
|
||||||
<i>
|
{% blocktranslate %}
|
||||||
{{ user_comment }}
|
Bitte bearbeite die Meldung möglichst bald.
|
||||||
</i>
|
{% endblocktranslate %}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
<a href="{{ notification.report.get_full_url }}" class="cta-button">{% translate 'Report bearbeiten' %}</a>
|
||||||
Bitte bearbeite die Meldung möglichst bald.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a href="{{ report_url }}" class="cta-button">{% translate 'Report bearbeiten' %}</a>
|
|
||||||
</p>
|
</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
@@ -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">
|
<div class="block">
|
||||||
<h1 class="title is-1">{% translate 'Moderationstools' %}</h1>
|
<h1 class="title is-1">{% translate 'Moderationstools' %}</h1>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<a class="button is-primary is-fullwidth" href="{% url 'modqueue' %}">{% translate 'Moderationswarteschlange' %}</a>
|
<a class="button is-primary is-fullwidth"
|
||||||
|
href="{% url 'modqueue' %}">{% translate 'Moderationswarteschlange' %}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<a class="button is-primary is-fullwidth" href="{% url 'updatequeue' %}">{% translate 'Up-To-Date Check' %}</a>
|
<a class="button is-primary is-fullwidth"
|
||||||
|
href="{% url 'updatequeue' %}">{% translate 'Up-To-Date Check' %}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<a class="button is-primary is-fullwidth" href="{% url 'organization-check' %}">{% translate 'Organisations Check' %}</a>
|
<a class="button is-primary is-fullwidth"
|
||||||
|
href="{% url 'organization-check' %}">{% translate 'Organisations Check' %}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
{% if action_was_posting %}
|
||||||
|
{% if posted_successfully %}
|
||||||
|
<div class="message is-success">
|
||||||
|
<div class="message-header">
|
||||||
|
{% translate 'Vermittlung gepostet' %}
|
||||||
|
</div>
|
||||||
|
<div class="message-body">
|
||||||
|
{% blocktranslate with post_url=post.url %}
|
||||||
|
Link zum Post: <a href={{ post_url }}>{{ post_url }}</a>
|
||||||
|
{% endblocktranslate %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="message is-danger">
|
||||||
|
<div class="message-header">
|
||||||
|
{% translate 'Vermittlung konnte nicht gepostet werden' %}
|
||||||
|
</div>
|
||||||
|
<div class="message-body">
|
||||||
|
{{ error_message }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form class="cell" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="action" value="post_to_fedi">
|
||||||
|
<button class="button is-fullwidth is-warning is-primary" type="submit" id="submit">
|
||||||
|
<i class="fa-solid fa-bullhorn fa-fw"></i> {% translate "Vermittlung ins Fediverse posten" %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -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">
|
<div class="cell">
|
||||||
|
|
||||||
<p>
|
{% if adoption_notice.organization %}
|
||||||
<i class="fa-solid fa-location-dot fa-fw"></i>
|
<div class="cell">
|
||||||
{% if adoption_notice.location %}
|
<span>
|
||||||
{{ adoption_notice.location }}
|
<i class="fa-solid fa-building fa-fw" aria-label="{% trans 'Tierschutzorganisation' %}"></i>
|
||||||
{% else %}
|
<a href="{{ adoption_notice.organization.get_absolute_url }}"> {{ adoption_notice.organization }}</a>
|
||||||
{{ adoption_notice.location_string }}
|
</span>
|
||||||
{% endif %}</p>
|
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
<i class="fa-solid fa-location-dot fa-fw"></i>
|
||||||
|
{% if adoption_notice.location %}
|
||||||
|
{{ adoption_notice.location }}
|
||||||
|
{% else %}
|
||||||
|
{{ adoption_notice.location_string }}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="cell">
|
<div class="cell">
|
||||||
{% include "fellchensammlung/partials/sex-overview.html" %}
|
{% include "fellchensammlung/partials/sex-overview.html" %}
|
||||||
@@ -60,7 +71,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if adoption_notice.description_short %}
|
{% if adoption_notice.description_short %}
|
||||||
{{ adoption_notice.description_short | render_markdown }}
|
<div class="content">
|
||||||
|
{{ adoption_notice.description_short | render_markdown }}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</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 }}
|
{{ adoption_notice.location_string }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<div class="content">
|
||||||
{% if adoption_notice.description %}
|
{% if adoption_notice.description %}
|
||||||
{{ adoption_notice.description | render_markdown }}
|
{{ adoption_notice.description | render_markdown }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% translate "Keine Beschreibung" %}
|
<p>
|
||||||
|
{% translate "Keine Beschreibung" %}
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</div>
|
||||||
{% if adoption_notice.get_photo %}
|
{% if adoption_notice.get_photo %}
|
||||||
<div class="adoption-notice-img">
|
<div class="adoption-notice-img">
|
||||||
<img src="{{ MEDIA_URL }}/{{ adoption_notice.get_photo.image }}"
|
<img src="{{ MEDIA_URL }}/{{ adoption_notice.get_photo.image }}"
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="card-header-title">
|
<div class="card-header-title">
|
||||||
<h2 class="title is-4">
|
<h2 class="title is-4">
|
||||||
<a href="{{ rescue_org.get_absolute_url }}">{{ rescue_org.name }}</a>
|
<a href="{{ rescue_org.get_absolute_url }}" target="_blank">{{ rescue_org.name }}</a>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -15,18 +15,27 @@
|
|||||||
<strong>{% translate 'Zuletzt geprüft:' %}</strong> {{ rescue_org.last_checked_hr }}
|
<strong>{% translate 'Zuletzt geprüft:' %}</strong> {{ rescue_org.last_checked_hr }}
|
||||||
</p>
|
</p>
|
||||||
<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>
|
</p>
|
||||||
{% if rescue_org.website %}
|
{% 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>
|
<i class="fas fa-globe" aria-label="{% translate "Website" %}"></i>
|
||||||
{{ rescue_org.website|domain }}
|
{{ rescue_org.website|domain }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for species_url in rescue_org.species_urls %}
|
{% with rescue_org_counter=forloop.counter %}
|
||||||
<p>{{ species_url.species }}: <a href="{{ species_url.url }}" target="_blank">{{ species_url.url }}</a>
|
{% for species_url in rescue_org.species_urls %}
|
||||||
</p>
|
<p>{{ species_url.species }}:
|
||||||
{% endfor %}
|
<a href="{{ species_url.url }}"
|
||||||
|
id="species_url_{{ rescue_org_counter }}_{{ forloop.counter }}"
|
||||||
|
target="_blank">
|
||||||
|
{{ species_url.url }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
{% if set_internal_comment_available %}
|
{% if set_internal_comment_available %}
|
||||||
<div class="block">
|
<div class="block">
|
||||||
@@ -55,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<div class="card-footer-item is-confirm">
|
<div class="card-footer-item is-confirm">
|
||||||
<form method="post">
|
<form method="post" id="mark_checked_{{ forloop.counter }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden"
|
<input type="hidden"
|
||||||
name="rescue_organization_id"
|
name="rescue_organization_id"
|
||||||
|
@@ -1,17 +1,19 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load custom_tags %}
|
{% load custom_tags %}
|
||||||
<div class="notification">
|
<div class="notification {% if not notification.read %}is-info is-light{% endif %}">
|
||||||
<div class="notification-header">
|
{% if not notification.read %}
|
||||||
<a href="{{ notification.url }}" ><b>{{ notification.title }}</b></a>
|
<form class="delete js-delete-excluded" method="POST">
|
||||||
<i class="card-timestamp">{{ notification.created_at }}</i>
|
|
||||||
<form class="notification-card-mark-read" method="POST">
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="notification_mark_read">
|
<input type="hidden" name="action" value="notification_mark_read">
|
||||||
<input type="hidden" name="notification_id" value="{{ notification.pk }}">
|
<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>
|
</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>
|
</div>
|
||||||
<p>
|
|
||||||
{{ notification.text | render_markdown }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
@@ -8,18 +8,18 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p>
|
<div class="block">
|
||||||
<b><i class="fa-solid fa-location-dot"></i></b>
|
<b><i class="fa-solid fa-location-dot"></i></b>
|
||||||
{% if rescue_organization.location %}
|
{% if rescue_organization.location %}
|
||||||
{{ rescue_organization.location.str }}
|
{{ rescue_organization.location }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ rescue_organization.location_string }}
|
{{ rescue_organization.location_string }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</div>
|
||||||
<p>
|
<div class="block content">
|
||||||
{% if rescue_organization.description_short %}
|
{% if rescue_organization.description_short %}
|
||||||
{{ rescue_organization.description_short | render_markdown }}
|
{{ rescue_organization.description_short | render_markdown }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@@ -3,7 +3,7 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="card-header-title">{{ rule.title }}</h2>
|
<h2 class="card-header-title">{{ rule.title }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content content">
|
||||||
<p class="content">{{ rule.rule_text | render_markdown }}</p>
|
{{ rule.rule_text | render_markdown }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@@ -1,8 +1,14 @@
|
|||||||
{% extends "fellchensammlung/base.html" %}
|
{% extends "fellchensammlung/base.html" %}
|
||||||
{% load i18n %}
|
{% 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 %}
|
{% block content %}
|
||||||
<h1 class="title is-1">{% translate "Aktualitätscheck" %}</h1>
|
<h1 class="title is-1">{% translate "Aktualitätscheck" %}</h1>
|
||||||
<p class="subtitle is-2">{% translate "Überprüfe ob es in Tierheimen neue Tiere gibt die ein Zuhause suchen" %}</p>
|
<p class="subtitle is-3">{% translate "Überprüfe ob es in Tierheimen neue Tiere gibt die ein Zuhause suchen" %}</p>
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
@@ -15,6 +21,13 @@
|
|||||||
<div class="column">
|
<div class="column">
|
||||||
<strong>Geprüft sind {{ percentage_checked|stringformat:"0.2f" }}%</strong>
|
<strong>Geprüft sind {{ percentage_checked|stringformat:"0.2f" }}%</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
{% if dq %}
|
||||||
|
<a class="button is-info" href="{% url 'organization-check' %}">{% translate 'Datenergänzung deaktivieren' %}</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="button is-info is-light" href="{% url 'organization-check-dq' %}">{% translate 'Datenergänzung aktivieren' %}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -30,6 +43,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<h2 class="title is-3">{% translate "In aktiver Kommunikation" %}</h2>
|
||||||
|
<div class="grid is-col-min-15">
|
||||||
|
{% for rescue_org in rescue_orgs_with_ongoing_communication %}
|
||||||
|
<div class="cell">
|
||||||
|
{% include "fellchensammlung/partials/partial-check-rescue-org.html" %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h2 class="title is-3">{% translate "Zuletzt geprüft" %}</h2>
|
<h2 class="title is-3">{% translate "Zuletzt geprüft" %}</h2>
|
||||||
<div class="grid is-col-min-15">
|
<div class="grid is-col-min-15">
|
||||||
|
@@ -4,7 +4,9 @@ from django import template
|
|||||||
from django.template.defaultfilters import stringfilter
|
from django.template.defaultfilters import stringfilter
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from fellchensammlung.tools.misc import time_since_as_hr_string
|
||||||
from notfellchen import settings
|
from notfellchen import settings
|
||||||
from fellchensammlung.models import TrustLevel
|
from fellchensammlung.models import TrustLevel
|
||||||
|
|
||||||
@@ -114,3 +116,18 @@ def dictkey(d, key):
|
|||||||
def host():
|
def host():
|
||||||
# Will not work for localhost or deployments without https
|
# Will not work for localhost or deployments without https
|
||||||
return f"https://{settings.host}"
|
return f"https://{settings.host}"
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def time_since_hr(timestamp):
|
||||||
|
t_delta = timezone.now() - timestamp
|
||||||
|
return time_since_as_hr_string(t_delta)
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
import logging
|
||||||
|
|
||||||
|
from random import randint
|
||||||
from notfellchen import settings
|
from notfellchen import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@@ -139,3 +140,18 @@ def send_test_email(email):
|
|||||||
to = email
|
to = email
|
||||||
|
|
||||||
mail.send_mail(subject, plain_message, from_email, [to], html_message=html_message)
|
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
|
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:
|
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"]}]'
|
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
|
status_code = 200
|
||||||
|
@@ -8,8 +8,9 @@ def gather_metrics_data():
|
|||||||
|
|
||||||
"""Adoption notices"""
|
"""Adoption notices"""
|
||||||
num_adoption_notices = AdoptionNotice.objects.count()
|
num_adoption_notices = AdoptionNotice.objects.count()
|
||||||
num_adoption_notices_active = AdoptionNotice.objects.filter(
|
adoption_notices_active = AdoptionNotice.objects.filter(
|
||||||
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE).count()
|
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE)
|
||||||
|
num_adoption_notices_active = adoption_notices_active.count()
|
||||||
num_adoption_notices_closed = AdoptionNotice.objects.filter(
|
num_adoption_notices_closed = AdoptionNotice.objects.filter(
|
||||||
adoptionnoticestatus__major_status=AdoptionNoticeStatus.CLOSED).count()
|
adoptionnoticestatus__major_status=AdoptionNoticeStatus.CLOSED).count()
|
||||||
num_adoption_notices_disabled = AdoptionNotice.objects.filter(
|
num_adoption_notices_disabled = AdoptionNotice.objects.filter(
|
||||||
@@ -18,6 +19,19 @@ def gather_metrics_data():
|
|||||||
adoptionnoticestatus__major_status=AdoptionNoticeStatus.AWAITING_ACTION).count()
|
adoptionnoticestatus__major_status=AdoptionNoticeStatus.AWAITING_ACTION).count()
|
||||||
|
|
||||||
adoption_notices_without_location = AdoptionNotice.objects.filter(location__isnull=True).count()
|
adoption_notices_without_location = AdoptionNotice.objects.filter(location__isnull=True).count()
|
||||||
|
|
||||||
|
active_animals = 0
|
||||||
|
active_animals_per_sex = {}
|
||||||
|
for adoption_notice in adoption_notices_active:
|
||||||
|
nps = adoption_notice.num_per_sex
|
||||||
|
for sex in nps:
|
||||||
|
number_of_animals = nps[sex]
|
||||||
|
try:
|
||||||
|
active_animals_per_sex[sex] += number_of_animals
|
||||||
|
except KeyError:
|
||||||
|
active_animals_per_sex[sex] = number_of_animals
|
||||||
|
active_animals += number_of_animals
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'users': num_user,
|
'users': num_user,
|
||||||
'staff': num_staff,
|
'staff': num_staff,
|
||||||
@@ -29,6 +43,8 @@ def gather_metrics_data():
|
|||||||
'disabled': num_adoption_notices_disabled,
|
'disabled': num_adoption_notices_disabled,
|
||||||
'awaiting_action': num_adoption_notices_awaiting_action,
|
'awaiting_action': num_adoption_notices_awaiting_action,
|
||||||
},
|
},
|
||||||
'adoption_notices_without_location': adoption_notices_without_location
|
'adoption_notices_without_location': adoption_notices_without_location,
|
||||||
|
'active_animals': active_animals,
|
||||||
|
'active_animals_per_sex': active_animals_per_sex
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
|
@@ -37,6 +37,8 @@ def time_since_as_hr_string(age: datetime.timedelta) -> str:
|
|||||||
weeks = age.days / 7
|
weeks = age.days / 7
|
||||||
months = age.days / 30
|
months = age.days / 30
|
||||||
years = age.days / 365
|
years = age.days / 365
|
||||||
|
minutes = age.seconds / 60
|
||||||
|
hours = age.seconds / 3600
|
||||||
if years >= 1:
|
if years >= 1:
|
||||||
text = ngettext(
|
text = ngettext(
|
||||||
"vor einem Jahr",
|
"vor einem Jahr",
|
||||||
@@ -49,11 +51,14 @@ def time_since_as_hr_string(age: datetime.timedelta) -> str:
|
|||||||
text = _("vor %(month)d Monaten") % {"month": months}
|
text = _("vor %(month)d Monaten") % {"month": months}
|
||||||
elif weeks >= 3:
|
elif weeks >= 3:
|
||||||
text = _("vor %(weeks)d Wochen") % {"weeks": weeks}
|
text = _("vor %(weeks)d Wochen") % {"weeks": weeks}
|
||||||
|
elif days >= 1:
|
||||||
|
text = ngettext("vor einem Tag","vor %(count)d Tagen", days,) % {"count": days,}
|
||||||
|
elif hours >= 1:
|
||||||
|
text = ngettext("vor einer Stunde", "vor %(count)d Stunden", hours,) % {"count": hours,}
|
||||||
|
elif minutes >= 1:
|
||||||
|
text = ngettext("vor einer Minute", "vor %(count)d Minuten", minutes, ) % {"count": minutes, }
|
||||||
else:
|
else:
|
||||||
if days == 0:
|
text = _("Gerade eben")
|
||||||
text = _("Heute")
|
|
||||||
else:
|
|
||||||
text = ngettext("vor einem Tag","vor %(count)d Tagen", days,) % {"count": days,}
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
def notify_of_AN_to_be_checked(adoption_notice):
|
||||||
@@ -8,7 +9,7 @@ def notify_of_AN_to_be_checked(adoption_notice):
|
|||||||
for user in users_to_notify:
|
for user in users_to_notify:
|
||||||
Notification.objects.create(adoption_notice=adoption_notice,
|
Notification.objects.create(adoption_notice=adoption_notice,
|
||||||
user_to_notify=user,
|
user_to_notify=user,
|
||||||
notification_type=NotificationTypeChoices.AN_IS_TO_BE_CHECKED,
|
notification_type=ntc.AN_IS_TO_BE_CHECKED,
|
||||||
title=f" Prüfe Vermittlung {adoption_notice}",
|
title=f" Prüfe Vermittlung {adoption_notice}",
|
||||||
text=f"{adoption_notice} muss geprüft werden bevor sie veröffentlicht wird.",
|
text=f"{adoption_notice} muss geprüft werden bevor sie veröffentlicht wird.",
|
||||||
)
|
)
|
||||||
|
@@ -2,9 +2,9 @@ import logging
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .geo import LocationProxy, Position
|
from .geo import LocationProxy, Position
|
||||||
from ..forms import AdoptionNoticeSearchForm
|
from ..forms import AdoptionNoticeSearchForm, RescueOrgSearchForm
|
||||||
from ..models import SearchSubscription, AdoptionNotice, SexChoicesWithAll, Location, \
|
from ..models import SearchSubscription, AdoptionNotice, SexChoicesWithAll, Location, \
|
||||||
Notification, NotificationTypeChoices
|
Notification, NotificationTypeChoices, RescueOrganization
|
||||||
|
|
||||||
|
|
||||||
def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: bool = True):
|
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
|
return
|
||||||
for search_subscription in SearchSubscription.objects.all():
|
for search_subscription in SearchSubscription.objects.all():
|
||||||
logging.debug(f"Search subscription {search_subscription} found.")
|
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):
|
if search.adoption_notice_fits_search(adoption_notice):
|
||||||
notification_text = f"{_('Zu deiner Suche')} {search_subscription} wurde eine neue Vermittlung gefunden"
|
notification_text = f"{_('Zu deiner Suche')} {search_subscription} wurde eine neue Vermittlung gefunden"
|
||||||
Notification.objects.create(user_to_notify=search_subscription.owner,
|
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")
|
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):
|
def __init__(self, request=None, search_subscription=None):
|
||||||
self.sex = None
|
self.sex = None
|
||||||
self.area_search = None
|
self.area_search = None
|
||||||
@@ -45,7 +45,7 @@ class Search:
|
|||||||
self.location_string = None
|
self.location_string = None
|
||||||
|
|
||||||
if request:
|
if request:
|
||||||
self.search_from_request(request)
|
self.adoption_notice_search_from_request(request)
|
||||||
elif search_subscription:
|
elif search_subscription:
|
||||||
self.search_from_search_subscription(search_subscription)
|
self.search_from_search_subscription(search_subscription)
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ class Search:
|
|||||||
|
|
||||||
return adoptions
|
return adoptions
|
||||||
|
|
||||||
def search_from_request(self, request):
|
def adoption_notice_search_from_request(self, request):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
self.search_form = AdoptionNoticeSearchForm(request.POST)
|
self.search_form = AdoptionNoticeSearchForm(request.POST)
|
||||||
self.search_form.is_valid()
|
self.search_form.is_valid()
|
||||||
@@ -157,3 +157,75 @@ class Search:
|
|||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
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 drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
||||||
|
|
||||||
from django.contrib.sitemaps.views import sitemap
|
from django.contrib.sitemaps.views import sitemap
|
||||||
from .sitemap import StaticViewSitemap, AdoptionNoticeSitemap, AnimalSitemap
|
from .sitemap import StaticViewSitemap, AdoptionNoticeSitemap, RescueOrganizationSitemap, SearchSitemap
|
||||||
|
|
||||||
sitemaps = {
|
sitemaps = {
|
||||||
"static": StaticViewSitemap,
|
"static": StaticViewSitemap,
|
||||||
"vermittlungen": AdoptionNoticeSitemap,
|
"vermittlungen": AdoptionNoticeSitemap,
|
||||||
"tiere": AnimalSitemap,
|
"tierschutzorganisationen": RescueOrganizationSitemap,
|
||||||
|
"orte": SearchSitemap
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -35,10 +37,14 @@ urlpatterns = [
|
|||||||
# ex: /adoption_notice/2/add-animal
|
# ex: /adoption_notice/2/add-animal
|
||||||
path("vermittlung/<int:adoption_notice_id>/add-animal", views.adoption_notice_add_animal,
|
path("vermittlung/<int:adoption_notice_id>/add-animal", views.adoption_notice_add_animal,
|
||||||
name="adoption-notice-add-animal"),
|
name="adoption-notice-add-animal"),
|
||||||
|
path("vermittlung/<int:adoption_notice_id>/close", views.deactivate_an,
|
||||||
|
name="adoption-notice-close"),
|
||||||
|
|
||||||
path("tierschutzorganisationen/", views.list_rescue_organizations, name="rescue-organizations"),
|
path("tierschutzorganisationen/", views.list_rescue_organizations, name="rescue-organizations"),
|
||||||
path("tierschutzorganisationen/<int:rescue_organization_id>/", views.detail_view_rescue_organization,
|
path("tierschutzorganisationen/<int:rescue_organization_id>/", views.detail_view_rescue_organization,
|
||||||
name="rescue-organization-detail"),
|
name="rescue-organization-detail"),
|
||||||
|
path("tierschutzorganisationen/spezialisierung/<int:species_id>", views.specialized_rescues,
|
||||||
|
name="specialized-rescue-organizations"),
|
||||||
|
|
||||||
# ex: /search/
|
# ex: /search/
|
||||||
path("suchen/", views.search, name="search"),
|
path("suchen/", views.search, name="search"),
|
||||||
@@ -52,6 +58,7 @@ urlpatterns = [
|
|||||||
path("impressum/", views.imprint, name="imprint"),
|
path("impressum/", views.imprint, name="imprint"),
|
||||||
path("terms-of-service/", views.terms_of_service, name="terms-of-service"),
|
path("terms-of-service/", views.terms_of_service, name="terms-of-service"),
|
||||||
path("datenschutz/", views.privacy, name="privacy"),
|
path("datenschutz/", views.privacy, name="privacy"),
|
||||||
|
path("ratten-kaufen/", views.buying, name="buying"),
|
||||||
|
|
||||||
################
|
################
|
||||||
## Moderation ##
|
## Moderation ##
|
||||||
@@ -75,6 +82,7 @@ urlpatterns = [
|
|||||||
# ex: user/1
|
# ex: user/1
|
||||||
path("user/<int:user_id>/", views.user_by_id, name="user-detail"),
|
path("user/<int:user_id>/", views.user_by_id, name="user-detail"),
|
||||||
path("user/me/", views.my_profile, name="user-me"),
|
path("user/me/", views.my_profile, name="user-me"),
|
||||||
|
path("user/notifications/", views.my_notifications, name="user-notifications"),
|
||||||
path('user/me/export/', views.export_own_profile, name='user-me-export'),
|
path('user/me/export/', views.export_own_profile, name='user-me-export'),
|
||||||
|
|
||||||
path('accounts/register/',
|
path('accounts/register/',
|
||||||
|
@@ -14,19 +14,21 @@ from django.contrib.auth.decorators import user_passes_test
|
|||||||
from django.core.serializers import serialize
|
from django.core.serializers import serialize
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
import json
|
import json
|
||||||
|
import requests
|
||||||
|
|
||||||
from .mail import mail_admins_new_report
|
from .mail import notify_mods_new_report
|
||||||
from notfellchen import settings
|
from notfellchen import settings
|
||||||
|
|
||||||
from fellchensammlung import logger
|
from fellchensammlung import logger
|
||||||
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
|
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
|
||||||
User, Location, AdoptionNoticeStatus, Subscriptions, Notification, RescueOrganization, \
|
User, Location, AdoptionNoticeStatus, Subscriptions, Notification, RescueOrganization, \
|
||||||
Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, \
|
Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, \
|
||||||
ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices
|
ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices, SocialMediaPost
|
||||||
from .forms import AdoptionNoticeForm, ImageForm, ReportAdoptionNoticeForm, \
|
from .forms import AdoptionNoticeForm, ImageForm, ReportAdoptionNoticeForm, \
|
||||||
CommentForm, ReportCommentForm, AnimalForm, AdoptionNoticeFormAutoAnimal, SpeciesURLForm, RescueOrgInternalComment
|
CommentForm, ReportCommentForm, AnimalForm, AdoptionNoticeFormAutoAnimal, SpeciesURLForm, RescueOrgInternalComment
|
||||||
from .models import Language, Announcement
|
from .models import Language, Announcement
|
||||||
from .tools import i18n
|
from .tools import i18n
|
||||||
|
from .tools.fedi import post_an_to_fedi
|
||||||
from .tools.geo import GeoAPI, zoom_level_for_radius
|
from .tools.geo import GeoAPI, zoom_level_for_radius
|
||||||
from .tools.metrics import gather_metrics_data
|
from .tools.metrics import gather_metrics_data
|
||||||
from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
|
from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
|
||||||
@@ -34,7 +36,7 @@ from .tools.admin import clean_locations, get_unchecked_adoption_notices, deacti
|
|||||||
from .tasks import post_adoption_notice_save
|
from .tasks import post_adoption_notice_save
|
||||||
from rest_framework.authtoken.models import Token
|
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):
|
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):
|
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 (
|
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):
|
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)
|
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):
|
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:
|
if request.user.is_authenticated:
|
||||||
try:
|
try:
|
||||||
subscription = Subscriptions.objects.get(owner=request.user, adoption_notice=adoption_notice)
|
subscription = Subscriptions.objects.get(owner=request.user, adoption_notice=adoption_notice)
|
||||||
@@ -98,6 +121,7 @@ def adoption_notice_detail(request, adoption_notice_id):
|
|||||||
has_edit_permission = user_is_owner_or_trust_level(request.user, adoption_notice)
|
has_edit_permission = user_is_owner_or_trust_level(request.user, adoption_notice)
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
action = request.POST.get("action")
|
action = request.POST.get("action")
|
||||||
|
handle_an_check_actions(request, action, adoption_notice)
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
if action == "comment":
|
if action == "comment":
|
||||||
comment_form = CommentForm(request.POST)
|
comment_form = CommentForm(request.POST)
|
||||||
@@ -143,7 +167,8 @@ def adoption_notice_detail(request, adoption_notice_id):
|
|||||||
else:
|
else:
|
||||||
comment_form = CommentForm(instance=adoption_notice)
|
comment_form = CommentForm(instance=adoption_notice)
|
||||||
context = {"adoption_notice": adoption_notice, "comment_form": comment_form, "user": request.user,
|
context = {"adoption_notice": adoption_notice, "comment_form": comment_form, "user": request.user,
|
||||||
"has_edit_permission": has_edit_permission, "is_subscribed": is_subscribed}
|
"has_edit_permission": has_edit_permission, "is_subscribed": is_subscribed,
|
||||||
|
"adoption_notice_meta": adoption_notice_meta}
|
||||||
return render(request, 'fellchensammlung/details/detail-adoption-notice.html', context=context)
|
return render(request, 'fellchensammlung/details/detail-adoption-notice.html', context=context)
|
||||||
|
|
||||||
|
|
||||||
@@ -175,7 +200,7 @@ def adoption_notice_edit(request, adoption_notice_id):
|
|||||||
|
|
||||||
def search_important_locations(request, important_location_slug):
|
def search_important_locations(request, important_location_slug):
|
||||||
i_location = get_object_or_404(ImportantLocation, slug=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)
|
search.search_from_predefined_i_location(i_location)
|
||||||
|
|
||||||
site_title = _("Ratten in %(location_name)s") % {"location_name": i_location.name}
|
site_title = _("Ratten in %(location_name)s") % {"location_name": i_location.name}
|
||||||
@@ -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
|
# 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. This will toggle the "subscribe" button
|
||||||
searched = False
|
searched = False
|
||||||
search = Search()
|
search = AdoptionNoticeSearch()
|
||||||
search.search_from_request(request)
|
search.adoption_notice_search_from_request(request)
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
searched = True
|
searched = True
|
||||||
if "subscribe_to_search" in request.POST:
|
if "subscribe_to_search" in request.POST:
|
||||||
@@ -454,6 +479,11 @@ def privacy(request):
|
|||||||
return render_text(request, text)
|
return render_text(request, text)
|
||||||
|
|
||||||
|
|
||||||
|
def buying(request):
|
||||||
|
text = i18n.get_text_by_language("buying")
|
||||||
|
return render_text(request, text)
|
||||||
|
|
||||||
|
|
||||||
def terms_of_service(request):
|
def terms_of_service(request):
|
||||||
text = i18n.get_text_by_language("terms_of_service")
|
text = i18n.get_text_by_language("terms_of_service")
|
||||||
rules = Rule.objects.all()
|
rules = Rule.objects.all()
|
||||||
@@ -478,8 +508,7 @@ def report_adoption(request, adoption_notice_id):
|
|||||||
report_instance.status = Report.WAITING
|
report_instance.status = Report.WAITING
|
||||||
report_instance.save()
|
report_instance.save()
|
||||||
form.save_m2m()
|
form.save_m2m()
|
||||||
mail_admins_new_report(report_instance)
|
notify_mods_new_report(report_instance, NotificationTypeChoices.NEW_REPORT_AN)
|
||||||
print("dada")
|
|
||||||
return redirect(reverse("report-detail-success", args=[report_instance.pk], ))
|
return redirect(reverse("report-detail-success", args=[report_instance.pk], ))
|
||||||
else:
|
else:
|
||||||
form = ReportAdoptionNoticeForm()
|
form = ReportAdoptionNoticeForm()
|
||||||
@@ -499,7 +528,7 @@ def report_comment(request, comment_id):
|
|||||||
report_instance.status = Report.WAITING
|
report_instance.status = Report.WAITING
|
||||||
report_instance.save()
|
report_instance.save()
|
||||||
form.save_m2m()
|
form.save_m2m()
|
||||||
mail_admins_new_report(report_instance)
|
notify_mods_new_report(report_instance, NotificationTypeChoices.NEW_REPORT_COMMENT)
|
||||||
return redirect(reverse("report-detail-success", args=[report_instance.pk], ))
|
return redirect(reverse("report-detail-success", args=[report_instance.pk], ))
|
||||||
else:
|
else:
|
||||||
form = ReportCommentForm()
|
form = ReportCommentForm()
|
||||||
@@ -542,13 +571,34 @@ def user_detail(request, user, token=None):
|
|||||||
def user_by_id(request, user_id):
|
def user_by_id(request, user_id):
|
||||||
user = User.objects.get(id=user_id)
|
user = User.objects.get(id=user_id)
|
||||||
# Only users that are mods or owners of the user are allowed to view
|
# 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:
|
if user == request.user:
|
||||||
return my_profile(request)
|
return my_profile(request)
|
||||||
else:
|
else:
|
||||||
return user_detail(request, user)
|
return user_detail(request, user)
|
||||||
|
|
||||||
|
|
||||||
|
def process_notification_actions(request, action):
|
||||||
|
"""
|
||||||
|
As multiple views allow to mark notifications as read, this function can be used to process these actions
|
||||||
|
|
||||||
|
The function allows users to mark only their own notifications as read.
|
||||||
|
"""
|
||||||
|
if action == "notification_mark_read":
|
||||||
|
notification_id = request.POST.get("notification_id")
|
||||||
|
|
||||||
|
notification = Notification.objects.get(pk=notification_id)
|
||||||
|
# Ensures a user can only mark their own notifications as read
|
||||||
|
if not notification.user_to_notify == request.user:
|
||||||
|
return render(request, "fellchensammlung/errors/403.html", status=403)
|
||||||
|
notification.mark_read()
|
||||||
|
elif action == "notification_mark_all_read":
|
||||||
|
notifications = Notification.objects.filter(user_to_notify=request.user, read=False)
|
||||||
|
for notification in notifications:
|
||||||
|
notification.mark_read()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@login_required()
|
@login_required()
|
||||||
def my_profile(request):
|
def my_profile(request):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
@@ -562,16 +612,8 @@ def my_profile(request):
|
|||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
action = request.POST.get("action")
|
action = request.POST.get("action")
|
||||||
if action == "notification_mark_read":
|
process_notification_actions(request, action)
|
||||||
notification_id = request.POST.get("notification_id")
|
if action == "search_subscription_delete":
|
||||||
|
|
||||||
notification = Notification.objects.get(pk=notification_id)
|
|
||||||
notification.mark_read()
|
|
||||||
elif action == "notification_mark_all_read":
|
|
||||||
notifications = Notification.objects.filter(user=request.user, mark_read=False)
|
|
||||||
for notification in notifications:
|
|
||||||
notification.mark_read()
|
|
||||||
elif action == "search_subscription_delete":
|
|
||||||
search_subscription_id = request.POST.get("search_subscription_id")
|
search_subscription_id = request.POST.get("search_subscription_id")
|
||||||
SearchSubscription.objects.get(pk=search_subscription_id).delete()
|
SearchSubscription.objects.get(pk=search_subscription_id).delete()
|
||||||
logging.info(f"Deleted subscription {search_subscription_id}")
|
logging.info(f"Deleted subscription {search_subscription_id}")
|
||||||
@@ -583,6 +625,19 @@ def my_profile(request):
|
|||||||
return user_detail(request, request.user, token)
|
return user_detail(request, request.user, token)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required()
|
||||||
|
def my_notifications(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
action = request.POST.get("action")
|
||||||
|
process_notification_actions(request, action)
|
||||||
|
|
||||||
|
context = {"notifications_unread": Notification.objects.filter(user_to_notify=request.user, read=False).order_by(
|
||||||
|
"-created_at"),
|
||||||
|
"notifications_read_last": Notification.objects.filter(user_to_notify=request.user,
|
||||||
|
read=True).order_by("-read_at")}
|
||||||
|
return render(request, 'fellchensammlung/notifications.html', context=context)
|
||||||
|
|
||||||
|
|
||||||
@user_passes_test(user_is_trust_level_or_above)
|
@user_passes_test(user_is_trust_level_or_above)
|
||||||
def modqueue(request):
|
def modqueue(request):
|
||||||
open_reports = Report.objects.select_related("reportadoptionnotice", "reportcomment").filter(status=Report.WAITING)
|
open_reports = Report.objects.select_related("reportadoptionnotice", "reportcomment").filter(status=Report.WAITING)
|
||||||
@@ -593,16 +648,11 @@ def modqueue(request):
|
|||||||
@login_required
|
@login_required
|
||||||
def updatequeue(request):
|
def updatequeue(request):
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
adoption_notice = AdoptionNotice.objects.get(id=request.POST.get("adoption_notice_id"))
|
|
||||||
edit_permission = request.user == adoption_notice.owner or user_is_trust_level_or_above(request.user,
|
|
||||||
TrustLevel.MODERATOR)
|
|
||||||
if not edit_permission:
|
|
||||||
return render(request, "fellchensammlung/errors/403.html", status=403)
|
|
||||||
action = request.POST.get("action")
|
action = request.POST.get("action")
|
||||||
if action == "checked_inactive":
|
|
||||||
adoption_notice.set_closed()
|
# This function handles the activation and deactivation of ANs
|
||||||
if action == "checked_active":
|
# Separate function because it's used in multiple places
|
||||||
adoption_notice.set_active()
|
handle_an_check_actions(request, action)
|
||||||
|
|
||||||
if user_is_trust_level_or_above(request.user, TrustLevel.MODERATOR):
|
if user_is_trust_level_or_above(request.user, TrustLevel.MODERATOR):
|
||||||
last_checked_adoption_list = AdoptionNotice.objects.order_by("last_checked")
|
last_checked_adoption_list = AdoptionNotice.objects.order_by("last_checked")
|
||||||
@@ -697,8 +747,16 @@ def external_site_warning(request, template_name='fellchensammlung/external-site
|
|||||||
return render(request, template_name, context=context)
|
return render(request, template_name, context=context)
|
||||||
|
|
||||||
|
|
||||||
def list_rescue_organizations(request, template='fellchensammlung/animal-shelters.html'):
|
def list_rescue_organizations(request, species=None, template='fellchensammlung/animal-shelters.html'):
|
||||||
rescue_organizations = RescueOrganization.objects.all()
|
if species is None:
|
||||||
|
# rescue_organizations = RescueOrganization.objects.all()
|
||||||
|
|
||||||
|
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)
|
paginator = Paginator(rescue_organizations, 10)
|
||||||
|
|
||||||
page_number = request.GET.get("page")
|
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)
|
rescue_organizations_to_list = paginator.get_page(page_number)
|
||||||
context = {"rescue_organizations_to_list": rescue_organizations_to_list,
|
context = {"rescue_organizations_to_list": rescue_organizations_to_list,
|
||||||
"show_rescue_orgs": True,
|
"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)
|
return render(request, template, context=context)
|
||||||
|
|
||||||
|
|
||||||
|
def specialized_rescues(request, species_id):
|
||||||
|
species = get_object_or_404(Species, pk=species_id)
|
||||||
|
return list_rescue_organizations(request, species)
|
||||||
|
|
||||||
|
|
||||||
def detail_view_rescue_organization(request, rescue_organization_id,
|
def detail_view_rescue_organization(request, rescue_organization_id,
|
||||||
template='fellchensammlung/details/detail-rescue-organization.html'):
|
template='fellchensammlung/details/detail-rescue-organization.html'):
|
||||||
org = RescueOrganization.objects.get(pk=rescue_organization_id)
|
org = RescueOrganization.objects.get(pk=rescue_organization_id)
|
||||||
@@ -764,12 +841,17 @@ def rescue_organization_check(request, context=None):
|
|||||||
if comment_form.is_valid():
|
if comment_form.is_valid():
|
||||||
comment_form.save()
|
comment_form.save()
|
||||||
|
|
||||||
rescue_orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=False).order_by("last_checked")[:10]
|
rescue_orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=False,
|
||||||
|
ongoing_communication=False).order_by("last_checked")[:3]
|
||||||
|
rescue_orgs_with_ongoing_communication = RescueOrganization.objects.filter(ongoing_communication=True).order_by(
|
||||||
|
"updated_at")
|
||||||
|
rescue_orgs_last_checked = RescueOrganization.objects.filter().order_by("-last_checked")[:10]
|
||||||
|
rescue_orgs_to_comment = rescue_orgs_to_check | rescue_orgs_with_ongoing_communication | rescue_orgs_last_checked
|
||||||
# Prepare a form for each organization
|
# Prepare a form for each organization
|
||||||
comment_forms = {
|
comment_forms = {
|
||||||
org.id: RescueOrgInternalComment(instance=org) for org in rescue_orgs_to_check
|
org.id: RescueOrgInternalComment(instance=org) for org in rescue_orgs_to_comment
|
||||||
}
|
}
|
||||||
rescue_orgs_last_checked = RescueOrganization.objects.filter().order_by("-last_checked")[:10]
|
|
||||||
timeframe = timezone.now().date() - timedelta(days=14)
|
timeframe = timezone.now().date() - timedelta(days=14)
|
||||||
num_rescue_orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=False).filter(
|
num_rescue_orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=False).filter(
|
||||||
last_checked__lt=timeframe).count()
|
last_checked__lt=timeframe).count()
|
||||||
@@ -787,6 +869,8 @@ def rescue_organization_check(request, context=None):
|
|||||||
context["num_rescue_orgs_to_check"] = num_rescue_orgs_to_check
|
context["num_rescue_orgs_to_check"] = num_rescue_orgs_to_check
|
||||||
context["percentage_checked"] = percentage_checked
|
context["percentage_checked"] = percentage_checked
|
||||||
context["num_rescue_orgs_checked"] = num_rescue_orgs_checked
|
context["num_rescue_orgs_checked"] = num_rescue_orgs_checked
|
||||||
|
context["rescue_orgs_with_ongoing_communication"] = rescue_orgs_with_ongoing_communication
|
||||||
|
context["set_internal_comment_available"] = True
|
||||||
return render(request, 'fellchensammlung/rescue-organization-check.html', context=context)
|
return render(request, 'fellchensammlung/rescue-organization-check.html', context=context)
|
||||||
|
|
||||||
|
|
||||||
@@ -797,7 +881,7 @@ def rescue_organization_check_dq(request):
|
|||||||
DQ = data quality
|
DQ = data quality
|
||||||
"""
|
"""
|
||||||
context = {"set_species_url_available": True,
|
context = {"set_species_url_available": True,
|
||||||
"set_internal_comment_available": True,
|
"dq": True,
|
||||||
"species_url_form": SpeciesURLForm,
|
"species_url_form": SpeciesURLForm,
|
||||||
"internal_comment_form": RescueOrgInternalComment}
|
"internal_comment_form": RescueOrgInternalComment}
|
||||||
return rescue_organization_check(request, context)
|
return rescue_organization_check(request, context)
|
||||||
@@ -805,4 +889,39 @@ def rescue_organization_check_dq(request):
|
|||||||
|
|
||||||
@user_passes_test(user_is_trust_level_or_above)
|
@user_passes_test(user_is_trust_level_or_above)
|
||||||
def moderation_tools_overview(request):
|
def moderation_tools_overview(request):
|
||||||
return render(request, 'fellchensammlung/mod-tool-overview.html')
|
context = None
|
||||||
|
if request.method == "POST":
|
||||||
|
action = request.POST.get("action")
|
||||||
|
if action == "post_to_fedi":
|
||||||
|
adoption_notice = SocialMediaPost.get_an_to_post()
|
||||||
|
if adoption_notice is not None:
|
||||||
|
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"
|
"Project-Id-Version: Notfellchen\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-06-19 15:51+0000\n"
|
"POT-Creation-Date: 2025-06-19 15:51+0000\n"
|
||||||
"PO-Revision-Date: 2025-06-19 17:52+0200\n"
|
"PO-Revision-Date: 2025-07-17 15:55+0200\n"
|
||||||
"Last-Translator: Julian-Samuel <Gebühr>\n"
|
"Last-Translator: Julian-Samuel <Gebühr>\n"
|
||||||
"Language-Team: English\n"
|
"Language-Team: English\n"
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
@@ -343,7 +343,7 @@ msgstr "Adoption Notice deactivated:"
|
|||||||
|
|
||||||
#: src/fellchensammlung/models.py:485
|
#: src/fellchensammlung/models.py:485
|
||||||
msgid "Die folgende Vermittlung wurde deaktiviert: "
|
msgid "Die folgende Vermittlung wurde deaktiviert: "
|
||||||
msgstr "The following adoption notice was deactivated:"
|
msgstr "The following adoption notice was deactivated: "
|
||||||
|
|
||||||
#: src/fellchensammlung/models.py:585 src/fellchensammlung/models.py:593
|
#: src/fellchensammlung/models.py:585 src/fellchensammlung/models.py:593
|
||||||
msgid "Weiblich"
|
msgid "Weiblich"
|
||||||
@@ -562,7 +562,7 @@ msgstr "Adoption notices of Organization"
|
|||||||
#: src/fellchensammlung/templates/fellchensammlung/details/detail-rescue-organization.html:47
|
#: src/fellchensammlung/templates/fellchensammlung/details/detail-rescue-organization.html:47
|
||||||
#: src/fellchensammlung/templates/fellchensammlung/lists/list-adoption-notices.html:11
|
#: src/fellchensammlung/templates/fellchensammlung/lists/list-adoption-notices.html:11
|
||||||
msgid "Keine Vermittlungen gefunden."
|
msgid "Keine Vermittlungen gefunden."
|
||||||
msgstr "No adoption notices found"
|
msgstr "No adoption notices found."
|
||||||
|
|
||||||
#: src/fellchensammlung/templates/fellchensammlung/details/detail-user.html:11
|
#: src/fellchensammlung/templates/fellchensammlung/details/detail-user.html:11
|
||||||
msgid "Profil verwalten"
|
msgid "Profil verwalten"
|
||||||
@@ -680,7 +680,7 @@ msgstr "Report problems"
|
|||||||
|
|
||||||
#: src/fellchensammlung/templates/fellchensammlung/footer.html:77
|
#: src/fellchensammlung/templates/fellchensammlung/footer.html:77
|
||||||
msgid "Code"
|
msgid "Code"
|
||||||
msgstr "code"
|
msgstr "Code"
|
||||||
|
|
||||||
#: src/fellchensammlung/templates/fellchensammlung/footer.html:84
|
#: src/fellchensammlung/templates/fellchensammlung/footer.html:84
|
||||||
msgid "Hilfreiche Links"
|
msgid "Hilfreiche Links"
|
||||||
@@ -903,7 +903,7 @@ msgstr "Modqueue"
|
|||||||
msgid ""
|
msgid ""
|
||||||
"Erlaube oder blockiere Vermittlungsanzeigen die bisher noch zurückgehalten "
|
"Erlaube oder blockiere Vermittlungsanzeigen die bisher noch zurückgehalten "
|
||||||
"werden "
|
"werden "
|
||||||
msgstr ""
|
msgstr "Allow or block adoption notices that are waiting in queue"
|
||||||
|
|
||||||
#: src/fellchensammlung/templates/fellchensammlung/partials/partial-adoption-notice.html:12
|
#: src/fellchensammlung/templates/fellchensammlung/partials/partial-adoption-notice.html:12
|
||||||
msgid "Notfellchen"
|
msgid "Notfellchen"
|
||||||
@@ -956,7 +956,7 @@ msgstr "You need to log in to comment"
|
|||||||
|
|
||||||
#: src/fellchensammlung/templates/fellchensammlung/partials/partial-report.html:4
|
#: src/fellchensammlung/templates/fellchensammlung/partials/partial-report.html:4
|
||||||
msgid "Meldung von "
|
msgid "Meldung von "
|
||||||
msgstr "Report of"
|
msgstr "Report of "
|
||||||
|
|
||||||
#: src/fellchensammlung/templates/fellchensammlung/partials/partial-report.html:7
|
#: src/fellchensammlung/templates/fellchensammlung/partials/partial-report.html:7
|
||||||
msgid "Regeln gegen die Verstoßen wurde"
|
msgid "Regeln gegen die Verstoßen wurde"
|
||||||
@@ -1049,7 +1049,7 @@ msgstr "Deactivated adoption notices to check"
|
|||||||
|
|
||||||
#: src/fellchensammlung/templates/fellchensammlung/updatequeue.html:13
|
#: src/fellchensammlung/templates/fellchensammlung/updatequeue.html:13
|
||||||
msgid "Aktive Vermittlungen zur Überprüfung"
|
msgid "Aktive Vermittlungen zur Überprüfung"
|
||||||
msgstr "Active "
|
msgstr "Active Adoption Notices to check"
|
||||||
|
|
||||||
#: src/fellchensammlung/tools/misc.py:42
|
#: src/fellchensammlung/tools/misc.py:42
|
||||||
#, python-format
|
#, python-format
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
__version__ = "1.0.1"
|
__version__ = "1.1.0"
|
||||||
|
|
||||||
# This will make sure the app is always imported when
|
# This will make sure the app is always imported when
|
||||||
# Django starts so that shared_task will use this app.
|
# Django starts so that shared_task will use this app.
|
||||||
|
@@ -24,6 +24,11 @@ app.conf.beat_schedule = {
|
|||||||
'task': 'admin.deactivate_404_adoption_notices',
|
'task': 'admin.deactivate_404_adoption_notices',
|
||||||
'schedule': crontab(hour=3),
|
'schedule': crontab(hour=3),
|
||||||
},
|
},
|
||||||
|
'daily-fedi-post': {
|
||||||
|
'task': 'social_media.post_fedi',
|
||||||
|
'schedule': crontab(hour=19),
|
||||||
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.HEALTHCHECKS_URL is not None and settings.HEALTHCHECKS_URL != "":
|
if settings.HEALTHCHECKS_URL is not None and settings.HEALTHCHECKS_URL != "":
|
||||||
|
@@ -118,6 +118,12 @@ else:
|
|||||||
EMAIL_USE_TLS = config.getboolean('mail', 'tls', fallback=False)
|
EMAIL_USE_TLS = config.getboolean('mail', 'tls', fallback=False)
|
||||||
EMAIL_USE_SSL = config.getboolean('mail', 'ssl', fallback=False)
|
EMAIL_USE_SSL = config.getboolean('mail', 'ssl', fallback=False)
|
||||||
|
|
||||||
|
""" Fediverse """
|
||||||
|
fediverse_enabled = config.get('fediverse', 'enabled', fallback=False)
|
||||||
|
if fediverse_enabled:
|
||||||
|
fediverse_api_base_url = config.get('fediverse', 'api_base_url')
|
||||||
|
fediverse_access_token = config.get('fediverse', 'access_token')
|
||||||
|
|
||||||
"""USER MANAGEMENT"""
|
"""USER MANAGEMENT"""
|
||||||
AUTH_USER_MODEL = "fellchensammlung.User"
|
AUTH_USER_MODEL = "fellchensammlung.User"
|
||||||
ACCOUNT_ACTIVATION_DAYS = 7 # One-week activation window
|
ACCOUNT_ACTIVATION_DAYS = 7 # One-week activation window
|
||||||
|
@@ -30,6 +30,6 @@ urlpatterns += i18n_patterns(
|
|||||||
prefix_default_language=False
|
prefix_default_language=False
|
||||||
)
|
)
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG: # pragma: no cover
|
||||||
urlpatterns += static(settings.MEDIA_URL,
|
urlpatterns += static(settings.MEDIA_URL,
|
||||||
document_root=settings.MEDIA_ROOT)
|
document_root=settings.MEDIA_ROOT)
|
||||||
|
@@ -1,6 +1,14 @@
|
|||||||
{% extends "fellchensammlung/base.html" %}
|
{% extends "fellchensammlung/base.html" %}
|
||||||
{% load i18n %}
|
{% 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 %}
|
{% block content %}
|
||||||
|
|
||||||
{% if form.errors %}
|
{% if form.errors %}
|
||||||
@@ -14,15 +22,15 @@
|
|||||||
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<p class="is-warning">{% translate "Du bist bereits eingeloggt." %}</p>
|
<p class="is-warning">{% translate "Du bist bereits eingeloggt." %}</p>
|
||||||
{% else %} {% if next %}
|
{% else %}
|
||||||
|
{% if next %}
|
||||||
<div class="notification is-warning">
|
<div class="notification is-warning">
|
||||||
<button class="delete"></button>
|
<button class="delete"></button>
|
||||||
<p>
|
<p>
|
||||||
{% translate "Bitte log dich ein um diese Seite sehen zu können." %}
|
{% translate "Bitte log dich ein um diese Seite sehen zu können." %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not user.is_authenticated %}
|
{% if not user.is_authenticated %}
|
||||||
|
@@ -2,12 +2,12 @@ from datetime import timedelta
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from fellchensammlung.tools.admin import get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
|
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 fellchensammlung.tools.misc import is_404
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from model_bakery import baker
|
from model_bakery import baker
|
||||||
from fellchensammlung.models import AdoptionNotice
|
from fellchensammlung.models import AdoptionNotice, RescueOrganization
|
||||||
|
|
||||||
|
|
||||||
class DeactivationTest(TestCase):
|
class DeactivationTest(TestCase):
|
||||||
@@ -96,3 +96,21 @@ class PingTest(TestCase):
|
|||||||
self.adoption2.refresh_from_db()
|
self.adoption2.refresh_from_db()
|
||||||
self.assertTrue(self.adoption1.is_active)
|
self.assertTrue(self.adoption1.is_active)
|
||||||
self.assertFalse(self.adoption2.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")
|
cls.test_user_1 = User.objects.create(username="Testuser1", password="SUPERSECRET", email="test@example.org")
|
||||||
|
|
||||||
def test_mark_read(self):
|
def test_mark_read(self):
|
||||||
not1 = Notification.objects.create(user=self.test_user_1, text="New rats to adopt", title="🔔 New Rat alert")
|
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=self.test_user_1,
|
not2 = Notification.objects.create(user_to_notify=self.test_user_1,
|
||||||
text="New wombat to adopt", title="🔔 New Wombat alert")
|
text="New wombat to adopt", title="🔔 New Wombat alert")
|
||||||
not1.mark_read()
|
not1.mark_read()
|
||||||
|
|
||||||
|
@@ -3,11 +3,12 @@ from time import sleep
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from fellchensammlung.models import SearchSubscription, User, TrustLevel, AdoptionNotice, Location, SexChoicesWithAll, \
|
from fellchensammlung.models import SearchSubscription, User, TrustLevel, AdoptionNotice, Location, SexChoicesWithAll, \
|
||||||
Animal, Species, AdoptionNoticeNotification, SexChoices
|
Animal, Species, SexChoices, Notification
|
||||||
from model_bakery import baker
|
from model_bakery import baker
|
||||||
|
|
||||||
from fellchensammlung.tools.geo import LocationProxy
|
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):
|
class TestSearch(TestCase):
|
||||||
@@ -71,7 +72,7 @@ class TestSearch(TestCase):
|
|||||||
sex=SexChoicesWithAll.ALL,
|
sex=SexChoicesWithAll.ALL,
|
||||||
max_distance=100
|
max_distance=100
|
||||||
)
|
)
|
||||||
search1 = Search()
|
search1 = AdoptionNoticeSearch()
|
||||||
search1.search_position = LocationProxy("Stuttgart").position
|
search1.search_position = LocationProxy("Stuttgart").position
|
||||||
search1.max_distance = 100
|
search1.max_distance = 100
|
||||||
search1.area_search = True
|
search1.area_search = True
|
||||||
@@ -82,11 +83,11 @@ class TestSearch(TestCase):
|
|||||||
self.assertEqual(search_subscription1, search1)
|
self.assertEqual(search_subscription1, search1)
|
||||||
|
|
||||||
def test_adoption_notice_fits_search(self):
|
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.assertTrue(search1.adoption_notice_fits_search(self.adoption1))
|
||||||
self.assertFalse(search1.adoption_notice_fits_search(self.adoption2))
|
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.assertFalse(search2.adoption_notice_fits_search(self.adoption1))
|
||||||
self.assertTrue(search2.adoption_notice_fits_search(self.adoption2))
|
self.assertTrue(search2.adoption_notice_fits_search(self.adoption2))
|
||||||
|
|
||||||
@@ -100,5 +101,7 @@ class TestSearch(TestCase):
|
|||||||
"""
|
"""
|
||||||
notify_search_subscribers(self.adoption1)
|
notify_search_subscribers(self.adoption1)
|
||||||
|
|
||||||
self.assertTrue(AdoptionNoticeNotification.objects.filter(user=self.test_user1, adoption_notice=self.adoption1).exists())
|
self.assertTrue(Notification.objects.filter(user_to_notify=self.test_user1,
|
||||||
self.assertFalse(AdoptionNoticeNotification.objects.filter(user=self.test_user2).exists())
|
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 django.test import TestCase
|
||||||
from model_bakery import baker
|
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
|
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.trust_level = TrustLevel.ADMIN
|
||||||
cls.test_user0.save()
|
cls.test_user0.save()
|
||||||
|
|
||||||
cls.adoption1 = baker.make(AdoptionNotice, name="TestAdoption1", owner=cls.test_user1,)
|
cls.adoption1 = baker.make(AdoptionNotice, name="TestAdoption1", owner=cls.test_user1, )
|
||||||
cls.adoption1.set_unchecked() # Could also emit notification
|
cls.adoption1.set_unchecked() # Could also emit notification
|
||||||
|
|
||||||
def test_notify_of_AN_to_be_checked(self):
|
def test_notify_of_AN_to_be_checked(self):
|
||||||
notify_of_AN_to_be_checked(self.adoption1)
|
notify_of_AN_to_be_checked(self.adoption1)
|
||||||
self.assertTrue(AdoptionNoticeNotification.objects.filter(user=self.test_user0).exists())
|
self.assertTrue(Notification.objects.filter(user_to_notify=self.test_user0,
|
||||||
self.assertTrue(AdoptionNoticeNotification.objects.filter(user=self.test_user1).exists())
|
adoption_notice=self.adoption1,
|
||||||
self.assertFalse(AdoptionNoticeNotification.objects.filter(user=self.test_user2).exists())
|
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 model_bakery import baker
|
||||||
|
|
||||||
from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus, TrustLevel, \
|
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.geo import LocationProxy
|
||||||
|
from fellchensammlung.tools.model_helpers import NotificationTypeChoices
|
||||||
from fellchensammlung.views import add_adoption_notice
|
from fellchensammlung.views import add_adoption_notice
|
||||||
|
|
||||||
|
|
||||||
@@ -34,16 +35,7 @@ class AnimalAndAdoptionTest(TestCase):
|
|||||||
species=rat,
|
species=rat,
|
||||||
description="Eine unglaublich süße Ratte")
|
description="Eine unglaublich süße Ratte")
|
||||||
|
|
||||||
def test_detail_animal(self):
|
def test_detail_adoption_notice(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):
|
|
||||||
self.client.login(username='testuser0', password='12345')
|
self.client.login(username='testuser0', password='12345')
|
||||||
|
|
||||||
response = self.client.post(reverse('adoption-notice-detail', args="1"))
|
response = self.client.post(reverse('adoption-notice-detail', args="1"))
|
||||||
@@ -101,91 +93,6 @@ class AnimalAndAdoptionTest(TestCase):
|
|||||||
self.assertTrue(an.sexes == set("M", ))
|
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):
|
class UpdateQueueTest(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
@@ -339,8 +246,10 @@ class AdoptionDetailTest(TestCase):
|
|||||||
reverse('adoption-notice-detail', args=str(an1.pk)),
|
reverse('adoption-notice-detail', args=str(an1.pk)),
|
||||||
data={"action": "comment", "text": "Test"})
|
data={"action": "comment", "text": "Test"})
|
||||||
self.assertTrue(Comment.objects.filter(user__username="testuser0").exists())
|
self.assertTrue(Comment.objects.filter(user__username="testuser0").exists())
|
||||||
self.assertFalse(CommentNotification.objects.filter(user__username="testuser0").exists())
|
self.assertFalse(Notification.objects.filter(user_to_notify__username="testuser0",
|
||||||
self.assertTrue(CommentNotification.objects.filter(user__username="testuser1").exists())
|
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):
|
class AdoptionEditTest(TestCase):
|
||||||
|
@@ -2,7 +2,8 @@ from django.test import TestCase
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from docs.conf import language
|
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
|
from model_bakery import baker
|
||||||
|
|
||||||
|
|
||||||
@@ -38,7 +39,11 @@ class BasicViewTest(TestCase):
|
|||||||
report_comment1 = ReportComment.objects.create(reported_comment=comment1,
|
report_comment1 = ReportComment.objects.create(reported_comment=comment1,
|
||||||
user_comment="ReportComment1")
|
user_comment="ReportComment1")
|
||||||
report_comment1.save()
|
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):
|
def test_index_logged_in(self):
|
||||||
self.client.login(username='testuser0', password='12345')
|
self.client.login(username='testuser0', password='12345')
|
||||||
@@ -60,11 +65,19 @@ class BasicViewTest(TestCase):
|
|||||||
self.client.login(username='testuser0', password='12345')
|
self.client.login(username='testuser0', password='12345')
|
||||||
response = self.client.get(reverse('about'))
|
response = self.client.get(reverse('about'))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, "Rule 1")
|
|
||||||
|
|
||||||
def test_about_anonymous(self):
|
def test_about_anonymous(self):
|
||||||
response = self.client.get(reverse('about'))
|
response = self.client.get(reverse('about'))
|
||||||
self.assertEqual(response.status_code, 200)
|
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")
|
self.assertContains(response, "Rule 1")
|
||||||
|
|
||||||
def test_report_adoption_logged_in(self):
|
def test_report_adoption_logged_in(self):
|
||||||
@@ -133,4 +146,55 @@ class BasicViewTest(TestCase):
|
|||||||
self.assertContains(response, "ReportComment1")
|
self.assertContains(response, "ReportComment1")
|
||||||
self.assertContains(response, '<form action="allow" class="">')
|
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