Compare commits

63 Commits

Author SHA1 Message Date
ef824d6b82 feat: Download templates when acessing page 2026-02-07 21:40:46 +01:00
153dc8c048 feat: Add overview page for adoption notice social media actions 2026-02-07 21:36:22 +01:00
00e32e0f7c refactor: Abstract post to fedi component 2026-02-07 21:35:51 +01:00
75789f4247 feat: Add instagram story photo generator 2026-02-07 20:19:34 +01:00
96221a1232 fix: also exclude ongoing communication from check statistics 2026-02-07 20:19:05 +01:00
c230020fc8 fix: Translate Allow material usage 2026-01-20 22:32:53 +01:00
e5c822dbb4 feat: exclude orgs that denied usage from check 2026-01-20 22:27:52 +01:00
9457b19e83 feat: Make date of birt optional, fixes #30 2026-01-20 22:15:16 +01:00
0e6a9c7c7d fix: adjust a bit of styling in darkmode 2026-01-20 21:17:06 +01:00
c324394949 feat: add species specific url to api 2026-01-20 21:03:08 +01:00
dfa44ea0c6 feat: add script to export shelters with websites for #29 2026-01-20 21:02:40 +01:00
c309ea9ed4 feat: add meerschweinchen 2026-01-06 18:54:36 +01:00
f3f7131912 feat: add rescue org create and change form 2026-01-06 17:15:52 +01:00
9a3cbffa42 feat: Add basic name based search for rescue orgs 2025-11-30 09:41:33 +01:00
f2bad5f171 feat: buttons span whole footer 2025-11-30 08:37:38 +01:00
515d9453d1 fix: use correct db value for migration 2025-11-29 13:17:42 +01:00
d7797ab02e feat: Add nicer display for location and adoption notice 2025-11-29 11:53:44 +01:00
ad3511c086 feat: Flatten status and use django form 2025-11-29 11:27:30 +01:00
f9f35f9104 feat: Add exclude shelters with different purpose 2025-11-26 23:41:03 +01:00
c4da3318c2 feat: Add history tracking 2025-11-16 18:49:01 +01:00
c529153373 feat: Add mod notes and user activation and deavtivation 2025-11-16 18:07:13 +01:00
8d0e4c62f7 feat: Hide user action buttons 2025-11-16 17:23:20 +01:00
4ab546bd8e feat: add cover for images and adoption process if inactive 2025-11-16 16:17:19 +01:00
cab9010aff feat: make status of disabled ANs more clear 2025-11-16 13:40:38 +01:00
97df7e3ed6 fix: remove caching 2025-11-12 20:30:16 +01:00
e64cc4bd5f Merge branch 'develop'
# Conflicts:
#	src/locale/en/LC_MESSAGES/django.po
2025-11-09 17:55:11 +01:00
a498971d66 feat: create directory in one go, don't use cache dir and use more standard entrypoint.sh 2025-11-09 17:36:48 +01:00
527ab07b6f trans: fis various 2025-11-08 00:33:35 +01:00
e2e236d650 feat: add button to download all logs 2025-11-08 00:00:54 +01:00
c4100a9ade trans: fix various 2025-11-07 18:17:53 +01:00
9511b46af0 feat: deactivate cache in dev config 2025-11-07 18:13:30 +01:00
5b906a7708 feat: add clean method 2025-11-07 18:12:53 +01:00
d68e836b57 trans: add various english translations 2025-11-07 18:12:37 +01:00
fe77f1da8d trans: add many translations 2025-11-07 17:08:13 +01:00
78b71690c0 feat: Allow toggling ongoing communication status 2025-11-07 14:02:22 +01:00
3b9ee95abc feat: Add log export functionality 2025-11-07 14:01:51 +01:00
b4e50364de fix: dependency 2025-11-03 22:22:22 +01:00
b014b3b227 fix: quotes 2025-11-03 22:22:12 +01:00
99bfe460ee fix: remove debug statement 2025-11-03 18:24:45 +01:00
d4c7caa42d fix: allow empty internal IP 2025-11-03 18:24:20 +01:00
32c8fc88cf feat: add debug command that prints settings 2025-11-03 18:22:11 +01:00
151ce0d88e fix: massively reduce number of db queries by caching num_per_sex #27
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-11-03 17:00:16 +01:00
e07e633651 style: explicitly return none 2025-11-03 16:53:59 +01:00
dd3b1fde9d feat: Add logs for checking rescue orgs and remove deprecated exclusion 2025-11-03 16:15:11 +01:00
2ffc9b4ba1 feat: Ad django debug toolbar 2025-11-03 16:14:51 +01:00
22eebd4586 feat: Add simple profiler capability 2025-11-03 16:14:05 +01:00
e589a048d3 feat: Make logs in Admin more usable 2025-11-03 16:11:48 +01:00
392eb5a7a8 feat: Use unified explanation for reason for signup 2025-11-02 08:15:11 +01:00
44fa4d4880 feat: Remove requirement to retype password 2025-11-02 08:14:41 +01:00
9b97cc4cb1 fix: Ensure javascript for login is loaded 2025-11-02 08:14:03 +01:00
656a24ef02 feat: Make settings configurable
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-10-21 03:37:36 +02:00
74643db087 feat: Add nicer display of passkeys based on panels 2025-10-21 03:27:08 +02:00
3a6fd3cee1 feat: Add nicer badge 2025-10-21 03:24:19 +02:00
29e9d1bd8c feat: Don't make input for radio button 2025-10-21 03:24:07 +02:00
3c5ca9ae00 feat: Fix display of 2fa options 2025-10-21 02:12:34 +02:00
3d1ad6112d feat: Add link to 2fa options 2025-10-21 02:12:17 +02:00
b843e67e9b feat: put buttons in group 2025-10-21 01:47:17 +02:00
4cab71e8fb feat: Style allauth templates 2025-10-21 01:28:31 +02:00
969339a95f feat: Use allauth and add passkey support 2025-10-21 00:40:10 +02:00
e06efa1539 feat: limit to 10 2025-10-21 14:47:13 +02:00
2fb6d2782f fix: Align button description with function 2025-10-20 21:56:04 +02:00
f69eccd0e4 feat: add page for updating the exclusion reason where it's not set yet 2025-10-20 18:33:15 +02:00
e20e6d4b1d fix: typo 2025-10-20 18:31:10 +02:00
77 changed files with 5320 additions and 1179 deletions

View File

@@ -9,15 +9,14 @@ RUN apt install gettext -y
RUN apt install libpq-dev gcc -y RUN apt install libpq-dev gcc -y
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app
RUN mkdir /app/data RUN mkdir /app/data/static -p
RUN mkdir /app/data/static
RUN mkdir /app/data/media RUN mkdir /app/data/media
RUN pip install -e . # Without the -e the library static folder will not be copied by collectstatic! RUN pip install --no-cache-dir -e . # Without the -e the library static folder will not be copied by collectstatic!
RUN nf collectstatic --noinput RUN nf collectstatic --noinput
RUN nf compilemessages --ignore venv RUN nf compilemessages --ignore venv
COPY docker/notfellchen.bash /bin/notfellchen COPY docker/entrypoint.sh /bin/notfellchen
EXPOSE 7345 EXPOSE 7345
CMD ["notfellchen"] CMD ["notfellchen"]

View File

@@ -0,0 +1,19 @@
Tierschutzorganisation hinzufügen
=================================
Notfellchen führt eine Liste von Tierheime und anderer Tierschutzorganisationen. Die meisten Tierheime wurden von
OpenStreetMap importiert.
Ausnahmen stellen nicht-ortsgebunden Organisationen wie die Rattenhilfe Süd dar. Sie existieren nicht auf der Karte und
können dort auch nicht sinnvoll verzeichnet werden, daher werden sie nur in Notfellchen geführt.
.. warning::
Generell ist es besser eine Tierschutzorganisation in OpenStreetMap anzulegen, anstatt bei Notfellchen. Das
ermöglichtes anderen die Daten ebenfalls zu nutzen und sie werden ggf. durch die OpenStreetMap Community aktuell gehalten.
Hier erklären wir aber trotzdem wie Tierheime direkt in Notfellchen hinzugefügt werden können, damit es schneller geht
diese Anzulegen und ihnen Vermittlungen zuzuweisen.
Organisationen in Notfellchen hinzufügen
----------------------------------------

View File

@@ -8,6 +8,8 @@ host=localhost
[django] [django]
secret=CHANGE-ME secret=CHANGE-ME
debug=True debug=True
internal_ips=["127.0.0.1"]
cache=False
[database] [database]
backend=sqlite3 backend=sqlite3
@@ -28,3 +30,6 @@ django_log_level=INFO
api_url=https://photon.hyteck.de/api api_url=https://photon.hyteck.de/api
api_format=photon api_format=photon
[security]
totp_issuer="NF Localhost"
webauth_allow_insecure_origin=True

View File

@@ -38,7 +38,11 @@ dependencies = [
"celery[redis]", "celery[redis]",
"drf-spectacular[sidecar]", "drf-spectacular[sidecar]",
"django-widget-tweaks", "django-widget-tweaks",
"django-super-deduper" "django-super-deduper",
"django-allauth[mfa]",
"django_debug_toolbar",
"django-admin-extra-buttons",
"django-simple-history"
] ]
dynamic = ["version", "readme"] dynamic = ["version", "readme"]
@@ -54,6 +58,10 @@ docs = [
"sphinx-rtd-theme", "sphinx-rtd-theme",
"sphinx-autobuild" "sphinx-autobuild"
] ]
shelter-upload = [
"osm2geojson",
"tqdm"
]
[project.urls] [project.urls]
homepage = "https://notfellchen.org" homepage = "https://notfellchen.org"

View File

@@ -0,0 +1,66 @@
import argparse
import csv
import os
import requests
OUTPUT_FILE = "export.csv"
def parse_args():
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="Download animal shelter data from the Overpass API to the Notfellchen API.")
parser.add_argument("--api-token", type=str, help="API token for authentication.")
parser.add_argument("--instance", type=str, help="API instance URL.")
parser.add_argument("--output-file", type=str, help="Path output file.")
return parser.parse_args()
def get_config():
"""Get configuration from environment variables or command-line arguments."""
args = parse_args()
api_token = args.api_token or os.getenv("NOTFELLCHEN_API_TOKEN")
instance = args.instance or os.getenv("NOTFELLCHEN_INSTANCE")
output_file = args.output_file or OUTPUT_FILE
return api_token, instance, output_file
def rat_specific_url_or_none(org):
try:
urls = org["species_specific_urls"]
for url in urls:
# 1 is the key for rats
if url["species"] == 1:
return url["url"]
# Return none if no url for this species is found
return None
except KeyError:
return None
def main():
api_token, instance, output_file = get_config()
# Set headers and endpoint
endpoint = f"{instance}/api/organizations/"
h = {'Authorization': f'Token {api_token}', "content-type": "application/json"}
rescue_orgs_result = requests.get(endpoint, headers=h)
with open(output_file, 'w') as csvfile:
fieldnames = ['id', 'name', 'website', 'rat_specific_website']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for org in rescue_orgs_result.json():
writer.writerow({'id': org["id"],
'name': org["name"],
'website': org["website"],
"rat_specific_website": rat_specific_url_or_none(org)})
if __name__ == "__main__":
main()

View File

@@ -133,6 +133,19 @@ def get_center_coordinates(geometry):
raise ValueError(f"Unsupported geometry type: {geom_type}") raise ValueError(f"Unsupported geometry type: {geom_type}")
def is_not_for_adoption(tierheim):
"""
Returns true if the shelter is not for the purpose of adoption
"""
if tierheim.purpose_adoption == "no":
return True
elif (tierheim.purpose_adoption is None and
(tierheim.purpose_sanctuary == "yes" or tierheim.purpose_release == "yes")):
return True
else:
return False
# TODO: take note of new get_overpass_result function which does the bulk of the new overpass query work # TODO: take note of new get_overpass_result function which does the bulk of the new overpass query work
def get_overpass_result(area, data_file): def get_overpass_result(area, data_file):
"""Build the Overpass query for fetching animal shelters in the specified area.""" """Build the Overpass query for fetching animal shelters in the specified area."""
@@ -225,7 +238,13 @@ def main():
description=get_or_none(tierheim, "opening_hours"), description=get_or_none(tierheim, "opening_hours"),
external_object_identifier=tierheim["id"], external_object_identifier=tierheim["id"],
EXTERNAL_SOURCE_IDENTIFIER="OSM", EXTERNAL_SOURCE_IDENTIFIER="OSM",
purpose_adoption=get_or_none(tierheim, "animal_shelter:adoption"),
purpose_sanctuary=get_or_none(tierheim, "animal_shelter:sanctuary"),
purpose_release=get_or_none(tierheim, "animal_shelter:release"),
) )
# Skip the shelter if it is not for adopting animals
if is_not_for_adoption(th_data):
continue
# Define here for later # Define here for later
optional_data = ["email", "phone_number", "website", "description", "fediverse_profile", "facebook", optional_data = ["email", "phone_number", "website", "description", "fediverse_profile", "facebook",
@@ -250,7 +269,8 @@ def main():
result = requests.patch(endpoint, json=org_patch_data, headers=h) result = requests.patch(endpoint, json=org_patch_data, headers=h)
if result.status_code != 200: if result.status_code != 200:
logging.warning(f"Updating {tierheim['properties']['name']} failed:{result.status_code} {result.json()}") logging.warning(
f"Updating {tierheim['properties']['name']} failed:{result.status_code} {result.json()}")
continue continue
# Rescue organization does not exist # Rescue organization does not exist
else: else:
@@ -268,7 +288,8 @@ def main():
if result.status_code != 201: if result.status_code != 201:
print(f"{idx} {tierheim["properties"]["name"]}:{result.status_code} {result.json()}") print(f"{idx} {tierheim["properties"]["name"]}:{result.status_code} {result.json()}")
print(f"Upload finished. Inserted {stats['num_inserted_orgs']} new orgs and updated {stats['num_updated_orgs']} orgs.") print(
f"Upload finished. Inserted {stats['num_inserted_orgs']} new orgs and updated {stats['num_updated_orgs']} orgs.")
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -7,6 +7,9 @@ from django.utils.html import format_html
from django.urls import reverse from django.urls import reverse
from django.utils.http import urlencode from django.utils.http import urlencode
from admin_extra_buttons.api import ExtraButtonsMixin, button, link
from simple_history.admin import SimpleHistoryAdmin
from .models import Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \ from .models import Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
SpeciesSpecificURL, ImportantLocation, SocialMediaPost SpeciesSpecificURL, ImportantLocation, SocialMediaPost
@@ -17,12 +20,34 @@ from django.utils.translation import gettext_lazy as _
from .tools.model_helpers import AdoptionNoticeStatusChoices from .tools.model_helpers import AdoptionNoticeStatusChoices
def export_to_csv_generic(model, queryset):
meta = model._meta
field_names = [field.name for field in meta.fields]
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
writer = csv.writer(response)
writer.writerow(field_names)
for obj in queryset:
row = writer.writerow([getattr(obj, field) for field in field_names])
return response
@admin.register(AdoptionNotice) @admin.register(AdoptionNotice)
class AdoptionNoticeAdmin(admin.ModelAdmin): class AdoptionNoticeAdmin(admin.ModelAdmin):
search_fields = ("name__icontains", "description__icontains") search_fields = ("name__icontains", "description__icontains", "location__icontains")
list_filter = ("owner",) list_display = ["name", "adoption_notice_status", "owner", "organization", "last_checked_hr"]
list_filter = ("adoption_notice_status", "owner")
actions = ("activate",) actions = ("activate",)
# This admin display is only needed to get a translated label of this property
# If not present the column would show up as "last checked hr"
@admin.display(description=_("zuletzt überprüft"))
def last_checked_hr(self, obj):
return obj.last_checked_hr
def activate(self, request, queryset): def activate(self, request, queryset):
for obj in queryset: for obj in queryset:
obj.adoption_notice_status = AdoptionNoticeStatusChoices.Active.SEARCHING obj.adoption_notice_status = AdoptionNoticeStatusChoices.Active.SEARCHING
@@ -33,7 +58,7 @@ class AdoptionNoticeAdmin(admin.ModelAdmin):
# Re-register UserAdmin # Re-register UserAdmin
@admin.register(User) @admin.register(User)
class UserAdmin(admin.ModelAdmin): class UserAdmin(SimpleHistoryAdmin):
search_fields = ("usernamname__icontains", "first_name__icontains", "last_name__icontains", "email__icontains") search_fields = ("usernamname__icontains", "first_name__icontains", "last_name__icontains", "email__icontains")
list_display = ("username", "email", "trust_level", "is_active", "view_adoption_notices") list_display = ("username", "email", "trust_level", "is_active", "view_adoption_notices")
list_filter = ("is_active", "trust_level",) list_filter = ("is_active", "trust_level",)
@@ -49,17 +74,7 @@ class UserAdmin(admin.ModelAdmin):
return format_html('<a href="{}">{} Adoption Notices</a>', url, count) return format_html('<a href="{}">{} Adoption Notices</a>', url, count)
def export_as_csv(self, request, queryset): def export_as_csv(self, request, queryset):
meta = self.model._meta response = export_to_csv_generic(self.model, queryset)
field_names = [field.name for field in meta.fields]
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
writer = csv.writer(response)
writer.writerow(field_names)
for obj in queryset:
row = writer.writerow([getattr(obj, field) for field in field_names])
return response return response
export_as_csv.short_description = _("Ausgewählte User exportieren") export_as_csv.short_description = _("Ausgewählte User exportieren")
@@ -71,7 +86,7 @@ def _reported_content_link(obj):
@admin.register(ReportComment) @admin.register(ReportComment)
class ReportCommentAdmin(admin.ModelAdmin): class ReportCommentAdmin(SimpleHistoryAdmin):
list_display = ["user_comment", "reported_content_link"] list_display = ["user_comment", "reported_content_link"]
date_hierarchy = "created_at" date_hierarchy = "created_at"
@@ -82,7 +97,7 @@ class ReportCommentAdmin(admin.ModelAdmin):
@admin.register(ReportAdoptionNotice) @admin.register(ReportAdoptionNotice)
class ReportAdoptionNoticeAdmin(admin.ModelAdmin): class ReportAdoptionNoticeAdmin(SimpleHistoryAdmin):
list_display = ["user_comment", "reported_content_link"] list_display = ["user_comment", "reported_content_link"]
date_hierarchy = "created_at" date_hierarchy = "created_at"
@@ -97,7 +112,7 @@ class SpeciesSpecificURLInline(admin.StackedInline):
@admin.register(RescueOrganization) @admin.register(RescueOrganization)
class RescueOrganizationAdmin(admin.ModelAdmin): class RescueOrganizationAdmin(SimpleHistoryAdmin):
search_fields = ("name", "description", "internal_comment", "location_string", "location__city") search_fields = ("name", "description", "internal_comment", "location_string", "location__city")
list_display = ("name", "trusted", "allows_using_materials", "website") list_display = ("name", "trusted", "allows_using_materials", "website")
list_filter = ("allows_using_materials", "trusted", ("external_source_identifier", EmptyFieldListFilter)) list_filter = ("allows_using_materials", "trusted", ("external_source_identifier", EmptyFieldListFilter))
@@ -108,12 +123,12 @@ class RescueOrganizationAdmin(admin.ModelAdmin):
@admin.register(Text) @admin.register(Text)
class TextAdmin(admin.ModelAdmin): class TextAdmin(SimpleHistoryAdmin):
search_fields = ("title__icontains", "text_code__icontains",) search_fields = ("title__icontains", "text_code__icontains",)
@admin.register(Comment) @admin.register(Comment)
class CommentAdmin(admin.ModelAdmin): class CommentAdmin(SimpleHistoryAdmin):
list_filter = ("user",) list_filter = ("user",)
@@ -123,7 +138,7 @@ class BaseNotificationAdmin(admin.ModelAdmin):
@admin.register(SearchSubscription) @admin.register(SearchSubscription)
class SearchSubscriptionAdmin(admin.ModelAdmin): class SearchSubscriptionAdmin(SimpleHistoryAdmin):
list_filter = ("owner",) list_filter = ("owner",)
@@ -151,8 +166,17 @@ class IsImportantListFilter(admin.SimpleListFilter):
@admin.register(Location) @admin.register(Location)
class LocationAdmin(admin.ModelAdmin): class LocationAdmin(SimpleHistoryAdmin):
search_fields = ("name__icontains", "city__icontains") search_fields = ("name__icontains", "city__icontains")
list_display = ("name", "city", "slug")
@admin.display(description=_("Slug"))
def slug(self, obj):
if obj.importantlocation:
return obj.importantlocation.slug
else:
return None
list_filter = [IsImportantListFilter] list_filter = [IsImportantListFilter]
inlines = [ inlines = [
ImportantLocationInline, ImportantLocationInline,
@@ -160,17 +184,39 @@ class LocationAdmin(admin.ModelAdmin):
@admin.register(SocialMediaPost) @admin.register(SocialMediaPost)
class SocialMediaPostAdmin(admin.ModelAdmin): class SocialMediaPostAdmin(SimpleHistoryAdmin):
list_filter = ("platform",) list_filter = ("platform",)
admin.site.register(Animal) @admin.register(Log)
class LogAdmin(ExtraButtonsMixin, admin.ModelAdmin):
ordering = ["-created_at"]
list_filter = ("action",)
list_display = ("action", "user", "created_at")
actions = ("export_as_csv",)
@admin.action(description=_("Ausgewählte Logs exportieren"))
def export_as_csv(self, request, queryset):
response = export_to_csv_generic(Log, queryset)
return response
@button()
def export_all_as_csv(self, request):
actual_queryset = Log.objects.all()
response = export_to_csv_generic(Log, actual_queryset)
return response
@link(href="https://www.google.com/", visible=lambda btn: True)
def invisible(self, button):
button.visible = False
admin.site.register(Animal, SimpleHistoryAdmin)
admin.site.register(Species) admin.site.register(Species)
admin.site.register(Rule) admin.site.register(Rule, SimpleHistoryAdmin)
admin.site.register(Image) admin.site.register(Image)
admin.site.register(ModerationAction) admin.site.register(ModerationAction, SimpleHistoryAdmin)
admin.site.register(Language) admin.site.register(Language)
admin.site.register(Announcement) admin.site.register(Announcement, SimpleHistoryAdmin)
admin.site.register(Subscriptions) admin.site.register(Subscriptions, SimpleHistoryAdmin)
admin.site.register(Log)
admin.site.register(Timestamp) admin.site.register(Timestamp)

View File

@@ -1,4 +1,4 @@
from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image, Location from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image, Location, SpeciesSpecificURL
from rest_framework import serializers from rest_framework import serializers
import math import math
@@ -144,10 +144,26 @@ class AnimalGetSerializer(serializers.ModelSerializer):
fields = "__all__" fields = "__all__"
class SpeciesSpecificURLSerializer(serializers.ModelSerializer):
class Meta:
model = SpeciesSpecificURL
fields = "__all__"
class RescueOrganizationSerializer(serializers.ModelSerializer): class RescueOrganizationSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField()
species_specific_urls = SpeciesSpecificURLSerializer(many=True, read_only=True)
class Meta: class Meta:
model = RescueOrganization model = RescueOrganization
exclude = ["internal_comment", "allows_using_materials"] fields = ('id', 'name', 'url', 'trusted', 'location_string', 'instagram', "facebook", "fediverse_profile",
"email", "phone_number", "website", "updated_at", "created_at", "last_checked", "description",
"external_source_identifier", "external_object_identifier", "exclude_from_check",
"regular_check_status", "ongoing_communication", "twenty_id", "location", "parent_org", "specializations",
"species_specific_urls")
def get_url(self, obj):
return obj.get_absolute_url()
class ImageCreateSerializer(serializers.ModelSerializer): class ImageCreateSerializer(serializers.ModelSerializer):

View File

@@ -2,10 +2,11 @@ from django.urls import path
from .views import ( from .views import (
AdoptionNoticeApiView, AdoptionNoticeApiView,
AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView, LocationApiView, AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView, LocationApiView,
AdoptionNoticeGeoJSONView, RescueOrgGeoJSONView, AdoptionNoticePerOrgApiView AdoptionNoticeGeoJSONView, RescueOrgGeoJSONView, AdoptionNoticePerOrgApiView, index
) )
urlpatterns = [ urlpatterns = [
path("", index, name="api-base-url"),
path("adoption_notice", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-list"), path("adoption_notice", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-list"),
path("adoption_notice.geojson", AdoptionNoticeGeoJSONView.as_view(), name="api-adoption-notice-list-geojson"), path("adoption_notice.geojson", AdoptionNoticeGeoJSONView.as_view(), name="api-adoption-notice-list-geojson"),
path("adoption_notice/<int:id>/", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-detail"), path("adoption_notice/<int:id>/", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-detail"),

View File

@@ -1,4 +1,6 @@
from django.db.models import Q from django.db.models import Q
from django.shortcuts import redirect
from django.urls import reverse
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from rest_framework.generics import ListAPIView from rest_framework.generics import ListAPIView
@@ -450,3 +452,7 @@ class AdoptionNoticePerOrgApiView(APIView):
adoption_notices = temporary_an_storage adoption_notices = temporary_an_storage
serializer = AdoptionNoticeSerializer(adoption_notices, many=True, context={"request": request}) serializer = AdoptionNoticeSerializer(adoption_notices, many=True, context={"request": request})
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def index(request):
return redirect(reverse("swagger-ui"))

View File

@@ -1,19 +1,21 @@
from django import forms from django import forms
from django.forms.widgets import Textarea
from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportComment, ModerationAction, User, Species, \ from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportComment, ModerationAction, User, Species, \
Comment, SexChoicesWithAll, DistanceChoices, SpeciesSpecificURL, RescueOrganization Comment, SexChoicesWithAll, DistanceChoices, SpeciesSpecificURL, RescueOrganization
from django_registration.forms import RegistrationForm from django_registration.forms import RegistrationForm
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Layout, Fieldset, HTML, Row, Column, Field, Hidden from crispy_forms.layout import Submit, Layout, Fieldset
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from notfellchen.settings import MEDIA_URL
from crispy_forms.layout import Div from crispy_forms.layout import Div
from .tools.model_helpers import reason_for_signup_label, reason_for_signup_help_text, AdoptionNoticeStatusChoices
def animal_validator(value: str): def animal_validator(value: str):
value = value.lower() value = value.lower()
animal_list = ["ratte", "farbratte", "katze", "hund", "kaninchen", "hase", "kuh", "fuchs", "cow", "rat", "cat", animal_list = ["ratte", "farbratte", "katze", "hund", "kaninchen", "hase", "kuh", "fuchs", "cow", "rat", "cat",
"dog", "rabbit", "fox", "fancy rat"] "dog", "rabbit", "fox", "fancy rat", "meerschweinchen"]
if value not in animal_list: if value not in animal_list:
raise forms.ValidationError(_("Dieses Tier kenne ich nicht. Probier ein anderes")) raise forms.ValidationError(_("Dieses Tier kenne ich nicht. Probier ein anderes"))
@@ -113,6 +115,12 @@ class ReportCommentForm(forms.ModelForm):
fields = ('reported_broken_rules', 'user_comment') fields = ('reported_broken_rules', 'user_comment')
class UserModCommentForm(forms.ModelForm):
class Meta:
model = User
fields = ('mod_notes',)
class CommentForm(forms.ModelForm): class CommentForm(forms.ModelForm):
class Meta: class Meta:
model = Comment model = Comment
@@ -137,6 +145,18 @@ class ModerationActionForm(forms.ModelForm):
fields = ('action', 'public_comment', 'private_comment') fields = ('action', 'public_comment', 'private_comment')
class AddedRegistrationForm(forms.Form):
reason_for_signup = forms.CharField(label=reason_for_signup_label,
help_text=reason_for_signup_help_text,
widget=Textarea)
captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_(
"Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um "
"sicherzustellen, dass du kein Roboter bist."))
def signup(self, request, user):
pass
class CustomRegistrationForm(RegistrationForm): class CustomRegistrationForm(RegistrationForm):
class Meta(RegistrationForm.Meta): class Meta(RegistrationForm.Meta):
model = User model = User
@@ -163,3 +183,31 @@ class RescueOrgSearchForm(forms.Form):
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False) location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.TWENTY, max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.TWENTY,
label=_("Suchradius")) label=_("Suchradius"))
class RescueOrgSearchByNameForm(forms.Form):
template_name = "fellchensammlung/forms/form_snippets.html"
name = forms.CharField(max_length=100, label=_("Name der Organisation"), required=False)
class CloseAdoptionNoticeForm(forms.ModelForm):
template_name = "fellchensammlung/forms/form_snippets.html"
adoption_notice_status = forms.ChoiceField(choices=AdoptionNoticeStatusChoices.Closed,
label=_("Status"),
help_text=_("Gib den neuen Status der Vermittlung an"))
class Meta:
model = AdoptionNotice
fields = ('adoption_notice_status',)
class RescueOrgForm(forms.ModelForm):
template_name = "fellchensammlung/forms/form_snippets.html"
class Meta:
model = RescueOrganization
fields = ('name', 'allows_using_materials', 'location_string', "email", "phone_number", "website", "instagram",
"facebook", "fediverse_profile", "internal_comment", "description", "external_source_identifier",
"external_object_identifier",
"parent_org")

View File

@@ -0,0 +1,13 @@
from django.core.management import BaseCommand
from notfellchen import settings
class Command(BaseCommand):
help = 'Print the current settings'
def handle(self, *args, **options):
for key in settings.__dir__():
if key.startswith("_") or key == "SECRET_KEY":
continue
print(f"{key} = {getattr(settings, key)}")

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.8 on 2025-11-16 16:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0069_rescueorganization_regular_check_status'),
]
operations = [
migrations.AddField(
model_name='user',
name='mod_notes',
field=models.TextField(blank=True, null=True, verbose_name='Moderationsnotizen'),
),
migrations.AlterField(
model_name='user',
name='reason_for_signup',
field=models.TextField(help_text="Wir würden gerne wissen warum du dich registrierst, ob du dich z.B. Tiere eines bestimmten Tierheim einstellen willst 'nur mal gucken' willst. Beides ist toll! Wenn du für ein Tierheim/eine Pflegestelle arbeitest kontaktieren wir dich ggf. um dir erweiterte Rechte zu geben.", verbose_name='Grund für die Registrierung'),
),
]

View File

@@ -0,0 +1,367 @@
# Generated by Django 5.2.8 on 2025-11-16 17:37
import django.contrib.auth.validators
import django.db.models.deletion
import django.utils.timezone
import simple_history.models
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0070_user_mod_notes_alter_user_reason_for_signup'),
]
operations = [
migrations.CreateModel(
name='HistoricalAdoptionNotice',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created_at', models.DateField(default=django.utils.timezone.now, verbose_name='Erstellt am')),
('updated_at', models.DateTimeField(blank=True, editable=False)),
('last_checked', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Zuletzt überprüft am')),
('searching_since', models.DateField(verbose_name='Sucht nach einem Zuhause seit')),
('name', models.CharField(max_length=200, verbose_name='Titel der Vermittlung')),
('description', models.TextField(blank=True, null=True, verbose_name='Beschreibung')),
('further_information', models.URLField(blank=True, help_text='Verlinke hier die Quelle der Vermittlung (z.B. die Website des Tierheims)', null=True, verbose_name='Link zu mehr Informationen')),
('group_only', models.BooleanField(default=False, verbose_name='Ausschließlich Gruppenadoption')),
('location_string', models.CharField(max_length=200, verbose_name='Ortsangabe')),
('adoption_notice_status', models.TextField(choices=[('active_searching', 'Searching'), ('active_interested', 'Interested'), ('awaiting_action_waiting_for_review', 'Waiting for review'), ('awaiting_action_needs_additional_info', 'Needs additional info'), ('awaiting_action_unchecked', 'Unchecked'), ('closed_successful_with_notfellchen', 'Successful (with Notfellchen)'), ('closed_successful_without_notfellchen', 'Successful (without Notfellchen)'), ('closed_animal_died', 'Animal died'), ('closed_for_other_adoption_notice', 'Closed for other adoption notice'), ('closed_not_open_for_adoption_anymore', 'Not open for adoption anymore'), ('closed_link_to_more_info_not_reachable', 'Der Link zu weiteren Informationen ist nicht mehr erreichbar.'), ('closed_other', 'Other (closed)'), ('disabled_against_the_rules', 'Against the rules'), ('disabled_other', 'Other (disabled)')], max_length=64, verbose_name='Status')),
('adoption_process', models.TextField(blank=True, choices=[('contact_person_in_an', 'Kontaktiere die Person im Vermittlungstext')], max_length=64, null=True, verbose_name='Adoptionsprozess')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('location', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.location')),
('organization', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.rescueorganization', verbose_name='Organisation')),
('owner', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Creator')),
],
options={
'verbose_name': 'historical Vermittlung',
'verbose_name_plural': 'historical Vermittlungen',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalAnimal',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('date_of_birth', models.DateField(verbose_name='Geburtsdatum')),
('name', models.CharField(max_length=200, verbose_name='Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Beschreibung')),
('sex', models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich, kastriert'), ('I', 'Intergeschlechtlich')], max_length=20, verbose_name='Geschlecht')),
('updated_at', models.DateTimeField(blank=True, editable=False)),
('created_at', models.DateTimeField(blank=True, editable=False)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('adoption_notice', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.adoptionnotice')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('owner', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
('species', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.species', verbose_name='Tierart')),
],
options={
'verbose_name': 'historical Tier',
'verbose_name_plural': 'historical Tiere',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalAnnouncement',
fields=[
('text_ptr', models.ForeignKey(auto_created=True, blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, parent_link=True, related_name='+', to='fellchensammlung.text')),
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('title', models.CharField(max_length=100, verbose_name='Titel')),
('content', models.TextField(verbose_name='Inhalt')),
('text_code', models.CharField(blank=True, max_length=24, verbose_name='Text code')),
('logged_in_only', models.BooleanField(default=False)),
('created_at', models.DateTimeField(blank=True, editable=False)),
('updated_at', models.DateTimeField(blank=True, editable=False)),
('publish_start_time', models.DateTimeField(verbose_name='Veröffentlichungszeitpunkt')),
('publish_end_time', models.DateTimeField(verbose_name='Veröffentlichungsende')),
('type', models.CharField(choices=[('important', 'important'), ('warning', 'warning'), ('info', 'info')], default='info', max_length=100)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('language', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.language', verbose_name='Sprache')),
],
options={
'verbose_name': 'historical Banner',
'verbose_name_plural': 'historical Banner',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalComment',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created_at', models.DateTimeField(blank=True, editable=False)),
('updated_at', models.DateTimeField(blank=True, editable=False)),
('text', models.TextField(verbose_name='Inhalt')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('adoption_notice', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('reply_to', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.comment', verbose_name='Antwort auf')),
('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in')),
],
options={
'verbose_name': 'historical Kommentar',
'verbose_name_plural': 'historical Kommentare',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalModerationAction',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('action', models.CharField(choices=[('user_banned', 'User was banned'), ('content_deleted', 'Content was deleted'), ('comment', 'Comment was added'), ('other_action_taken', 'Other action was taken'), ('no_action_taken', 'No action was taken')], max_length=30)),
('created_at', models.DateTimeField(blank=True, editable=False)),
('updated_at', models.DateTimeField(blank=True, editable=False)),
('public_comment', models.TextField(blank=True)),
('private_comment', models.TextField(blank=True)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('report', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.report')),
],
options={
'verbose_name': 'historical Moderationsaktion',
'verbose_name_plural': 'historical Moderationsaktionen',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalReport',
fields=[
('id', models.UUIDField(db_index=True, default=uuid.uuid4, help_text='ID dieses reports', verbose_name='ID')),
('status', models.CharField(choices=[('action taken', 'Action was taken'), ('no action taken', 'No action was taken'), ('waiting', 'Waiting for moderator action')], max_length=30)),
('user_comment', models.TextField(blank=True, verbose_name='Kommentar/Zusätzliche Information')),
('updated_at', models.DateTimeField(blank=True, editable=False, verbose_name='Zuletzt geändert am')),
('created_at', models.DateTimeField(blank=True, editable=False, verbose_name='Erstellt am')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'historical Meldung',
'verbose_name_plural': 'historical Meldungen',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalRescueOrganization',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('trusted', models.BooleanField(default=False, verbose_name='Vertrauenswürdig')),
('allows_using_materials', models.CharField(choices=[('allowed', 'Usage allowed'), ('requested', 'Usage requested'), ('denied', 'Usage denied'), ('other', "It's complicated"), ('not_asked', 'Not asked')], default='not_asked', max_length=200, verbose_name='Erlaubt Nutzung von Inhalten')),
('location_string', models.CharField(blank=True, max_length=200, null=True, verbose_name='Ort der Organisation')),
('instagram', models.URLField(blank=True, null=True, verbose_name='Instagram Profil')),
('facebook', models.URLField(blank=True, null=True, verbose_name='Facebook Profil')),
('fediverse_profile', models.URLField(blank=True, null=True, verbose_name='Fediverse Profil')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-Mail')),
('phone_number', models.CharField(blank=True, max_length=15, null=True, verbose_name='Telefonnummer')),
('website', models.URLField(blank=True, null=True, verbose_name='Website')),
('updated_at', models.DateTimeField(blank=True, editable=False)),
('created_at', models.DateTimeField(blank=True, editable=False)),
('last_checked', models.DateTimeField(blank=True, editable=False, verbose_name='Datum der letzten Prüfung')),
('internal_comment', models.TextField(blank=True, null=True, verbose_name='Interner Kommentar')),
('description', models.TextField(blank=True, null=True, verbose_name='Beschreibung')),
('external_object_identifier', models.CharField(blank=True, max_length=200, null=True, verbose_name='External Object Identifier')),
('external_source_identifier', models.CharField(blank=True, choices=[('OSM', 'Open Street Map')], max_length=200, null=True, verbose_name='External Source Identifier')),
('exclude_from_check', models.BooleanField(default=False, help_text='Organisation von der manuellen Überprüfung ausschließen, z.B. weil Tiere nicht online geführt werden', verbose_name='Von Prüfung ausschließen')),
('regular_check_status', models.CharField(choices=[('regular_check', 'Wird regelmäßig geprüft'), ('excluded_no_online_listing', 'Exkludiert: Tiere werden nicht online gelistet'), ('excluded_other_org', 'Exkludiert: Andere Organisation wird geprüft'), ('excluded_scope', 'Exkludiert: Organisation hat nie Notfellchen-relevanten Vermittlungen'), ('excluded_other', 'Exkludiert: Anderer Grund')], default='regular_check', help_text='Organisationen können, durch ändern dieser Einstellung, von der regelmäßigen Prüfung ausgeschlossen werden.', max_length=30, verbose_name='Status der regelmäßigen Prüfung')),
('ongoing_communication', models.BooleanField(default=False, help_text='Es findet gerade Kommunikation zwischen Notfellchen und der Organisation statt.', verbose_name='In aktiver Kommunikation')),
('twenty_id', models.UUIDField(blank=True, help_text='ID der der Organisation in Twenty', null=True, verbose_name='Twenty-ID')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('location', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.location')),
('parent_org', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.rescueorganization')),
],
options={
'verbose_name': 'historical Tierschutzorganisation',
'verbose_name_plural': 'historical Tierschutzorganisationen',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalRule',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('rule_text', models.TextField(verbose_name='Regeltext')),
('rule_identifier', models.CharField(help_text='Ein eindeutiger Identifikator der Regel. Ein Regelobjekt derselben Regel in einer anderen Sprache muss den gleichen Identifikator haben', max_length=24, verbose_name='Regel-ID')),
('updated_at', models.DateTimeField(blank=True, editable=False, verbose_name='Zuletzt geändert am')),
('created_at', models.DateTimeField(blank=True, editable=False, verbose_name='Erstellt am')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('language', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.language', verbose_name='Sprache')),
],
options={
'verbose_name': 'historical Regel',
'verbose_name_plural': 'historical Regeln',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalSearchSubscription',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('sex', models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich Kastriert'), ('I', 'Intergeschlechtlich'), ('A', 'Alle')], max_length=20, verbose_name='Geschlecht')),
('max_distance', models.IntegerField(choices=[(20, '20 km'), (50, '50 km'), (100, '100 km'), (200, '200 km'), (500, '500 km')], null=True)),
('updated_at', models.DateTimeField(blank=True, editable=False, verbose_name='Zuletzt geändert am')),
('created_at', models.DateTimeField(blank=True, editable=False, verbose_name='Erstellt am')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('location', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.location')),
('owner', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'historical Abonnierte Suche',
'verbose_name_plural': 'historical Abonnierte Suchen',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalSocialMediaPost',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, 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')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('adoption_notice', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'historical social media post',
'verbose_name_plural': 'historical social media posts',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalSubscriptions',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created_at', models.DateTimeField(blank=True, editable=False)),
('updated_at', models.DateTimeField(blank=True, editable=False)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('adoption_notice', models.ForeignKey(blank=True, db_constraint=False, help_text='Vermittlung die abonniert wurde', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('owner', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in')),
],
options={
'verbose_name': 'historical Abonnement',
'verbose_name_plural': 'historical Abonnements',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalText',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('title', models.CharField(max_length=100, verbose_name='Titel')),
('content', models.TextField(verbose_name='Inhalt')),
('text_code', models.CharField(blank=True, max_length=24, verbose_name='Text code')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('language', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.language', verbose_name='Sprache')),
],
options={
'verbose_name': 'historical Text',
'verbose_name_plural': 'historical Texte',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalUser',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(db_index=True, error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('trust_level', models.IntegerField(choices=[(1, 'Member'), (2, 'Coordinator'), (3, 'Moderator'), (4, 'Admin')], default=1)),
('updated_at', models.DateTimeField(blank=True, editable=False)),
('reason_for_signup', models.TextField(help_text="Wir würden gerne wissen warum du dich registrierst, ob du dich z.B. Tiere eines bestimmten Tierheim einstellen willst 'nur mal gucken' willst. Beides ist toll! Wenn du für ein Tierheim/eine Pflegestelle arbeitest kontaktieren wir dich ggf. um dir erweiterte Rechte zu geben.", verbose_name='Grund für die Registrierung')),
('mod_notes', models.TextField(blank=True, null=True, verbose_name='Moderationsnotizen')),
('email_notifications', models.BooleanField(default=True, verbose_name='Benachrichtigung per E-Mail')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('organization_affiliation', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.rescueorganization', verbose_name='Organisation')),
('preferred_language', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='fellchensammlung.language', verbose_name='Bevorzugte Sprache')),
],
options={
'verbose_name': 'historical Nutzer*in',
'verbose_name_plural': 'historical Nutzer*innen',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.8 on 2025-11-29 09:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0071_historicaladoptionnotice_historicalanimal_and_more'),
]
operations = [
migrations.AlterField(
model_name='adoptionnotice',
name='adoption_notice_status',
field=models.TextField(choices=[('active_searching', 'Searching'), ('active_interested', 'Interested'), ('awaiting_action_waiting_for_review', 'Waiting for review'), ('awaiting_action_needs_additional_info', 'Needs additional info'), ('awaiting_action_unchecked', 'Unchecked'), ('closed_successfully', 'Erfolgreich vermittelt'), ('closed_animal_died', 'Tier gestorben'), ('closed_for_other_adoption_notice', 'Vermittlung wurde zugunsten einer anderen geschlossen.'), ('closed_not_open_for_adoption_anymore', 'Tier(e) stehen nicht mehr zur Vermittlung bereit.'), ('closed_link_to_more_info_not_reachable', 'Der Link zu weiteren Informationen ist nicht mehr erreichbar.'), ('closed_other', 'Anderes'), ('disabled_against_the_rules', 'Against the rules'), ('disabled_other', 'Other (disabled)')], max_length=64, verbose_name='Status'),
),
migrations.AlterField(
model_name='historicaladoptionnotice',
name='adoption_notice_status',
field=models.TextField(choices=[('active_searching', 'Searching'), ('active_interested', 'Interested'), ('awaiting_action_waiting_for_review', 'Waiting for review'), ('awaiting_action_needs_additional_info', 'Needs additional info'), ('awaiting_action_unchecked', 'Unchecked'), ('closed_successfully', 'Erfolgreich vermittelt'), ('closed_animal_died', 'Tier gestorben'), ('closed_for_other_adoption_notice', 'Vermittlung wurde zugunsten einer anderen geschlossen.'), ('closed_not_open_for_adoption_anymore', 'Tier(e) stehen nicht mehr zur Vermittlung bereit.'), ('closed_link_to_more_info_not_reachable', 'Der Link zu weiteren Informationen ist nicht mehr erreichbar.'), ('closed_other', 'Anderes'), ('disabled_against_the_rules', 'Against the rules'), ('disabled_other', 'Other (disabled)')], max_length=64, verbose_name='Status'),
),
]

View File

@@ -0,0 +1,23 @@
import logging
from django.db import migrations
def migrate_status(apps, schema_editor):
# We can't import the model directly as it may be a newer
# version than this migration expects. We use the historical version.
AdoptionNotice = apps.get_model("fellchensammlung", "AdoptionNotice")
for adoption_notice in AdoptionNotice.objects.filter(
adoption_notice_status__in=("closed_successful_without_notfellchen", "closed_successful_with_notfellchen")):
adoption_notice.adoption_notice_status = "closed_successfully"
adoption_notice.save()
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0072_alter_adoptionnotice_adoption_notice_status_and_more'),
]
operations = [
migrations.RunPython(migrate_status),
]

View File

@@ -0,0 +1,104 @@
# Generated by Django 5.2.8 on 2026-01-20 21:09
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("fellchensammlung", "0073_adoption_notice_status_successful"),
]
operations = [
migrations.AlterField(
model_name="animal",
name="date_of_birth",
field=models.DateField(blank=True, null=True, verbose_name="Geburtsdatum"),
),
migrations.AlterField(
model_name="historicalanimal",
name="date_of_birth",
field=models.DateField(blank=True, null=True, verbose_name="Geburtsdatum"),
),
migrations.AlterField(
model_name="historicalrescueorganization",
name="external_object_identifier",
field=models.CharField(
blank=True,
help_text="Id des Objekts in der externen Datenbank (kann leer gelassen werden)",
max_length=200,
null=True,
verbose_name="External Object Identifier",
),
),
migrations.AlterField(
model_name="historicalrescueorganization",
name="external_source_identifier",
field=models.CharField(
blank=True,
choices=[("OSM", "Open Street Map")],
help_text="Name der Datenbank aus der die Tierschutzorganisation importiert wurde (kann leer gelassen werden)",
max_length=200,
null=True,
verbose_name="External Source Identifier",
),
),
migrations.AlterField(
model_name="historicalrescueorganization",
name="parent_org",
field=models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to="fellchensammlung.rescueorganization",
verbose_name="Übergeordnete Organisation",
),
),
migrations.AlterField(
model_name="rescueorganization",
name="external_object_identifier",
field=models.CharField(
blank=True,
help_text="Id des Objekts in der externen Datenbank (kann leer gelassen werden)",
max_length=200,
null=True,
verbose_name="External Object Identifier",
),
),
migrations.AlterField(
model_name="rescueorganization",
name="external_source_identifier",
field=models.CharField(
blank=True,
choices=[("OSM", "Open Street Map")],
help_text="Name der Datenbank aus der die Tierschutzorganisation importiert wurde (kann leer gelassen werden)",
max_length=200,
null=True,
verbose_name="External Source Identifier",
),
),
migrations.AlterField(
model_name="rescueorganization",
name="parent_org",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="fellchensammlung.rescueorganization",
verbose_name="Übergeordnete Organisation",
),
),
migrations.AlterField(
model_name="speciesspecificurl",
name="rescue_organization",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="species_specific_urls",
to="fellchensammlung.rescueorganization",
verbose_name="Tierschutzorganisation",
),
),
]

View File

@@ -7,13 +7,15 @@ from django.contrib.auth.models import Group
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
import base64 import base64
from simple_history.models import HistoricalRecords
from .tools import misc, geo 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 time_since_as_hr_string from .tools.misc import time_since_as_hr_string
from .tools.model_helpers import NotificationTypeChoices, AdoptionNoticeStatusChoices, AdoptionProcess, \ from .tools.model_helpers import NotificationTypeChoices, AdoptionNoticeStatusChoices, AdoptionProcess, \
AdoptionNoticeStatusChoicesDescriptions, RegularCheckStatusChoices AdoptionNoticeStatusChoicesDescriptions, RegularCheckStatusChoices, reason_for_signup_label, \
reason_for_signup_help_text
from .tools.model_helpers import ndm as NotificationDisplayMapping from .tools.model_helpers import ndm as NotificationDisplayMapping
@@ -120,11 +122,11 @@ class ExternalSourceChoices(models.TextChoices):
class AllowUseOfMaterialsChices(models.TextChoices): class AllowUseOfMaterialsChices(models.TextChoices):
USE_MATERIALS_ALLOWED = "allowed", _("Usage allowed") USE_MATERIALS_ALLOWED = "allowed", _("Nutzung erlaubt")
USE_MATERIALS_REQUESTED = "requested", _("Usage requested") USE_MATERIALS_REQUESTED = "requested", _("Nutzung angefragt")
USE_MATERIALS_DENIED = "denied", _("Usage denied") USE_MATERIALS_DENIED = "denied", _("Nutzung verweigert")
USE_MATERIALS_OTHER = "other", _("It's complicated") USE_MATERIALS_OTHER = "other", _("Es ist kompliziert")
USE_MATERIALS_NOT_ASKED = "not_asked", _("Not asked") USE_MATERIALS_NOT_ASKED = "not_asked", _("Nutzung noch nicht angefragt")
class Species(models.Model): class Species(models.Model):
@@ -165,10 +167,14 @@ class RescueOrganization(models.Model):
internal_comment = models.TextField(verbose_name=_("Interner Kommentar"), null=True, blank=True, ) internal_comment = models.TextField(verbose_name=_("Interner Kommentar"), null=True, blank=True, )
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung')) # Markdown allowed description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung')) # Markdown allowed
external_object_identifier = models.CharField(max_length=200, null=True, blank=True, external_object_identifier = models.CharField(max_length=200, null=True, blank=True,
verbose_name=_('External Object Identifier')) verbose_name=_('External Object Identifier'),
help_text=_(
"Id des Objekts in der externen Datenbank (kann leer gelassen werden)"))
external_source_identifier = models.CharField(max_length=200, null=True, blank=True, external_source_identifier = models.CharField(max_length=200, null=True, blank=True,
choices=ExternalSourceChoices.choices, choices=ExternalSourceChoices.choices,
verbose_name=_('External Source Identifier')) verbose_name=_('External Source Identifier'),
help_text=_(
"Name der Datenbank aus der die Tierschutzorganisation importiert wurde (kann leer gelassen werden)"))
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"))
@@ -181,11 +187,13 @@ class RescueOrganization(models.Model):
ongoing_communication = models.BooleanField(default=False, verbose_name=_('In aktiver Kommunikation'), ongoing_communication = models.BooleanField(default=False, verbose_name=_('In aktiver Kommunikation'),
help_text=_( help_text=_(
"Es findet gerade Kommunikation zwischen Notfellchen und der Organisation statt.")) "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,
verbose_name=_("Übergeordnete Organisation"))
# allows to specify if a rescue organization has a specialization for dedicated species # allows to specify if a rescue organization has a specialization for dedicated species
specializations = models.ManyToManyField(Species, blank=True) specializations = models.ManyToManyField(Species, blank=True)
twenty_id = models.UUIDField(verbose_name=_("Twenty-ID"), null=True, blank=True, twenty_id = models.UUIDField(verbose_name=_("Twenty-ID"), null=True, blank=True,
help_text=_("ID der der Organisation in Twenty")) help_text=_("ID der der Organisation in Twenty"))
history = HistoricalRecords()
class Meta: class Meta:
unique_together = ('external_object_identifier', 'external_source_identifier',) unique_together = ('external_object_identifier', 'external_source_identifier',)
@@ -249,6 +257,7 @@ class RescueOrganization(models.Model):
def set_checked(self): def set_checked(self):
self.last_checked = timezone.now() self.last_checked = timezone.now()
self._change_reason = 'Organization checked'
self.save() self.save()
@property @property
@@ -267,10 +276,6 @@ class RescueOrganization(models.Model):
""" """
return self.instagram or self.facebook or self.website or self.phone_number or self.email or self.fediverse_profile return self.instagram or self.facebook or self.website or self.phone_number or self.email or self.fediverse_profile
def set_exclusion_from_checks(self):
self.exclude_from_check = True
self.save()
@property @property
def child_organizations(self): def child_organizations(self):
return RescueOrganization.objects.filter(parent_org=self) return RescueOrganization.objects.filter(parent_org=self)
@@ -311,9 +316,10 @@ class User(AbstractUser):
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
organization_affiliation = models.ForeignKey(RescueOrganization, on_delete=models.PROTECT, null=True, blank=True, organization_affiliation = models.ForeignKey(RescueOrganization, on_delete=models.PROTECT, null=True, blank=True,
verbose_name=_('Organisation')) verbose_name=_('Organisation'))
reason_for_signup = models.TextField(verbose_name=_("Grund für die Registrierung"), help_text=_( reason_for_signup = models.TextField(verbose_name=reason_for_signup_label, help_text=reason_for_signup_help_text)
"Wir würden gerne wissen warum du dich registriertst, ob du dich z.B. Tiere eines bestimmten Tierheim einstellen willst 'nur mal gucken' willst. Beides ist toll! Wenn du für ein Tierheim/eine Pflegestelle arbeitest kontaktieren wir dich ggf. um dir erweiterte Rechte zu geben.")) mod_notes = models.TextField(verbose_name=_("Moderationsnotizen"), null=True, blank=True)
email_notifications = models.BooleanField(verbose_name=_("Benachrichtigung per E-Mail"), default=True) email_notifications = models.BooleanField(verbose_name=_("Benachrichtigung per E-Mail"), default=True)
history = HistoricalRecords()
REQUIRED_FIELDS = ["reason_for_signup", "email"] REQUIRED_FIELDS = ["reason_for_signup", "email"]
class Meta: class Meta:
@@ -409,6 +415,7 @@ class AdoptionNotice(models.Model):
adoption_process = models.TextField(null=True, blank=True, adoption_process = models.TextField(null=True, blank=True,
max_length=64, verbose_name=_('Adoptionsprozess'), max_length=64, verbose_name=_('Adoptionsprozess'),
choices=AdoptionProcess) choices=AdoptionProcess)
history = HistoricalRecords()
@property @property
def animals(self): def animals(self):
@@ -425,7 +432,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] = len([animal for animal in self.animals if animal.sex == sex])
return num_per_sex return num_per_sex
@property @property
@@ -515,6 +522,7 @@ class AdoptionNotice(models.Model):
photos.extend(animal.photos.all()) photos.extend(animal.photos.all())
if len(photos) > 0: if len(photos) > 0:
return photos return photos
return None
def get_photo(self): def get_photo(self):
""" """
@@ -544,6 +552,13 @@ class AdoptionNotice(models.Model):
def _values_of(list_of_enums): def _values_of(list_of_enums):
return list(map(lambda x: x[0], list_of_enums)) return list(map(lambda x: x[0], list_of_enums))
@property
def status_category(self):
ansc = AdoptionNoticeStatusChoices
for status_category in [ansc.Active, ansc.Disabled, ansc.Closed, ansc.AwaitingAction]:
if self.adoption_notice_status in self._values_of(status_category.choices):
return status_category.__name__.lower()
@property @property
def is_active(self): def is_active(self):
return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.Active.choices) return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.Active.choices)
@@ -560,6 +575,19 @@ class AdoptionNotice(models.Model):
def is_awaiting_action(self): def is_awaiting_action(self):
return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.AwaitingAction.choices) return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.AwaitingAction.choices)
@property
def status_description_short(self):
if self.is_active:
return _("Vermittlung aktiv")
elif self.is_disabled:
return _("Vermittlung gesperrt")
elif self.is_closed:
return _("Vermittlung geschlossen")
elif self.is_awaiting_action:
return _("Wartet auf Freigabe von Moderator*innen")
else:
raise NotImplementedError()
@property @property
def status_description(self): def status_description(self):
return AdoptionNoticeStatusChoicesDescriptions.mapping[self.adoption_notice_status] return AdoptionNoticeStatusChoicesDescriptions.mapping[self.adoption_notice_status]
@@ -609,7 +637,7 @@ class Animal(models.Model):
verbose_name = _('Tier') verbose_name = _('Tier')
verbose_name_plural = _('Tiere') verbose_name_plural = _('Tiere')
date_of_birth = models.DateField(verbose_name=_('Geburtsdatum')) date_of_birth = models.DateField(verbose_name=_('Geburtsdatum'), null=True, blank=True)
name = models.CharField(max_length=200, verbose_name=_('Name')) name = models.CharField(max_length=200, verbose_name=_('Name'))
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung')) description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
species = models.ForeignKey(Species, on_delete=models.PROTECT, verbose_name=_("Tierart")) species = models.ForeignKey(Species, on_delete=models.PROTECT, verbose_name=_("Tierart"))
@@ -623,18 +651,25 @@ class Animal(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE) owner = models.ForeignKey(User, on_delete=models.CASCADE)
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)
history = HistoricalRecords()
def __str__(self): def __str__(self):
return f"{self.name}" return f"{self.name}"
@property @property
def age(self): def age(self):
if self.date_of_birth:
return timezone.now().today().date() - self.date_of_birth return timezone.now().today().date() - self.date_of_birth
else:
return None
@property @property
def hr_age(self): def hr_age(self):
"""Returns a human-readable age based on the date of birth.""" """Returns a human-readable age based on the date of birth."""
if self.date_of_birth:
return misc.age_as_hr_string(self.age) return misc.age_as_hr_string(self.age)
else:
return _("Unbekannt")
def get_photo(self): def get_photo(self):
""" """
@@ -686,6 +721,7 @@ class SearchSubscription(models.Model):
max_distance = models.IntegerField(choices=DistanceChoices.choices, null=True) max_distance = models.IntegerField(choices=DistanceChoices.choices, null=True)
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Zuletzt geändert am")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Zuletzt geändert am"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am"))
history = HistoricalRecords()
def __str__(self): def __str__(self):
if self.location and self.max_distance: if self.location and self.max_distance:
@@ -716,6 +752,7 @@ class Rule(models.Model):
"Identifikator haben")) "Identifikator haben"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Zuletzt geändert am")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Zuletzt geändert am"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am"))
history = HistoricalRecords()
def __str__(self): def __str__(self):
return self.title return self.title
@@ -741,6 +778,7 @@ class Report(models.Model):
user_comment = models.TextField(blank=True, verbose_name=_("Kommentar/Zusätzliche Information")) user_comment = models.TextField(blank=True, verbose_name=_("Kommentar/Zusätzliche Information"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Zuletzt geändert am")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Zuletzt geändert am"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am"))
history = HistoricalRecords()
def __str__(self): def __str__(self):
return f"[{self.status}]: {self.user_comment:.20}" return f"[{self.status}]: {self.user_comment:.20}"
@@ -827,6 +865,7 @@ class ModerationAction(models.Model):
# Only visible to moderator # Only visible to moderator
private_comment = models.TextField(blank=True) private_comment = models.TextField(blank=True)
report = models.ForeignKey(Report, on_delete=models.CASCADE) report = models.ForeignKey(Report, on_delete=models.CASCADE)
history = HistoricalRecords()
def __str__(self): def __str__(self):
return f"[{self.action}]: {self.public_comment}" return f"[{self.action}]: {self.public_comment}"
@@ -848,6 +887,7 @@ class Text(models.Model):
content = models.TextField(verbose_name="Inhalt") content = models.TextField(verbose_name="Inhalt")
language = models.ForeignKey(Language, verbose_name="Sprache", on_delete=models.PROTECT) language = models.ForeignKey(Language, verbose_name="Sprache", on_delete=models.PROTECT)
text_code = models.CharField(max_length=24, verbose_name="Text code", blank=True) text_code = models.CharField(max_length=24, verbose_name="Text code", blank=True)
history = HistoricalRecords()
class Meta: class Meta:
verbose_name = "Text" verbose_name = "Text"
@@ -891,6 +931,7 @@ class Announcement(Text):
INFO: "info", INFO: "info",
} }
type = models.CharField(choices=TYPES, max_length=100, default=INFO) type = models.CharField(choices=TYPES, max_length=100, default=INFO)
history = HistoricalRecords()
@property @property
def is_active(self): def is_active(self):
@@ -937,6 +978,7 @@ class Comment(models.Model):
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung')) adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
text = models.TextField(verbose_name="Inhalt") text = models.TextField(verbose_name="Inhalt")
reply_to = models.ForeignKey("self", verbose_name="Antwort auf", blank=True, null=True, on_delete=models.CASCADE) reply_to = models.ForeignKey("self", verbose_name="Antwort auf", blank=True, null=True, on_delete=models.CASCADE)
history = HistoricalRecords()
def __str__(self): def __str__(self):
return f"{self.user} at {self.created_at.strftime('%H:%M %d.%m.%y')}: {self.text:.10}" return f"{self.user} at {self.created_at.strftime('%H:%M %d.%m.%y')}: {self.text:.10}"
@@ -1008,6 +1050,7 @@ class Subscriptions(models.Model):
help_text=_("Vermittlung die abonniert wurde")) help_text=_("Vermittlung die abonniert wurde"))
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)
history = HistoricalRecords()
def __str__(self): def __str__(self):
return f"{self.owner} - {self.adoption_notice}" return f"{self.owner} - {self.adoption_notice}"
@@ -1054,8 +1097,10 @@ class SpeciesSpecificURL(models.Model):
verbose_name_plural = _("Tierartspezifische URLs") 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,
verbose_name=_("Tierschutzorganisation")) on_delete=models.CASCADE,
verbose_name=_("Tierschutzorganisation"),
related_name="species_specific_urls")
url = models.URLField(verbose_name=_("Tierartspezifische URL")) url = models.URLField(verbose_name=_("Tierartspezifische URL"))
@@ -1069,6 +1114,7 @@ class SocialMediaPost(models.Model):
choices=PlatformChoices.choices) choices=PlatformChoices.choices)
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung')) adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
url = models.URLField(verbose_name=_("URL")) url = models.URLField(verbose_name=_("URL"))
history = HistoricalRecords()
@staticmethod @staticmethod
def get_an_to_post(): def get_an_to_post():

View File

@@ -22,12 +22,24 @@ $confirm: hsl(133deg, 100%, calc(41% + 0%));
background-color: $grey-light !important; background-color: $grey-light !important;
border-radius: 5px; border-radius: 5px;
} }
.navbar-burger {
color: white;
}
.card-header { .card-header {
background-color: $grey-dark; background-color: $grey-dark;
} }
a.card-footer-item.is-danger { div.card-footer-item.is-danger {
color: black; background-color: #5a212d;
} }
div.card-footer-item.is-warning {
background-color: #523e13;
}
div.card-footer-item.is-confirm {
background-color: #00420f;
}
.tag { .tag {
color: $grey-dark; color: $grey-dark;
background-color: $grey-light; background-color: $grey-light;
@@ -245,6 +257,34 @@ IMAGES
flex: 1; flex: 1;
} }
.cover {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(192, 192, 192, 0.75);
z-index: 99;
display: flex;
justify-content: center;
align-items: center;
opacity: 1;
transition: opacity 500ms;
border-radius: inherit; // ensure border radius of cards is followed, see https://css-tricks.com/preventing-child-background-overflow-with-inherited-border-radii/
}
.cover:hover {
transition: opacity 500ms;
opacity: 0;
// For being able to click the image and for accessibility the cover should have display: none;
// This is accepted by W3C: https://front-end.social/@mia/109433817951030826
// However this does not yet seem to be supported to be animated by browsers.
// Chrome claims to have shipped it: https://chromestatus.com/feature/5154958272364544
// However I couldn't get it to work there too.
// display: none;
// transition: opacity 1000ms, display 100ms;
}
/** /**
AN Cards AN Cards
@@ -322,7 +362,6 @@ AN Cards
} }
.notification-container { .notification-container {
display: inline-block; display: inline-block;
position: relative; position: relative;

View File

@@ -0,0 +1,12 @@
{% load allauth %}
{% setvar variant %}
{% if "primary" in attrs.tags %}
is-success
{% elif "secondary" in attrs.tags %}
is-success is-light
{% endif %}
{% endsetvar %}
<span class="tag{% if variant %} {{ variant }}{% endif %}" {% if attrs.title %}title="{{ attrs.title }}"{% endif %}>
{% slot %}
{% endslot %}
</span>

View File

@@ -0,0 +1,15 @@
{% load allauth %}
{% comment %} djlint:off {% endcomment %}
<div class="control">
<{% if attrs.href %}a href="{{ attrs.href }}"{% else %}button{% endif %}
class="button is-primary"
{% if attrs.form %}form="{{ attrs.form }}"{% endif %}
{% if attrs.id %}id="{{ attrs.id }}"{% endif %}
{% if attrs.name %}name="{{ attrs.name }}"{% endif %}
{% if attrs.value %}value="{{ attrs.value }}"{% endif %}
{% if attrs.type %}type="{{ attrs.type }}"{% endif %}
>
{% slot %}
{% endslot %}
</{% if attrs.href %}a{% else %}button{% endif %}>
</div>

View File

@@ -0,0 +1,5 @@
{% load allauth %}
<div class="field is-grouped">
{% slot %}
{% endslot %}
</div>

View File

@@ -0,0 +1,50 @@
{% load allauth %}
<div class="field">
{% if attrs.type == "textarea" %}
<label class="label" for="{{ attrs.id }}">
{% slot label %}
{% endslot %}
</label>
<textarea class="textarea"
{% if attrs.required %}required{% endif %}
{% if attrs.rows %}rows="{{ attrs.rows }}"{% endif %}
{% if attrs.disabled %}disabled{% endif %}
{% if attrs.readonly %}readonly{% endif %}
{% if attrs.checked %}checked{% endif %}
{% if attrs.name %}name="{{ attrs.name }}"{% endif %}
{% if attrs.id %}id="{{ attrs.id }}"{% endif %}
{% if attrs.placeholder %}placeholder="{{ attrs.placeholder }}"{% endif %}>{% slot value %}{% endslot %}</textarea>
{% else %}
{% if attrs.type != "checkbox" and attrs.type != "radio" %}
<label class="label" for="{{ attrs.id }}">
{% slot label %}
{% endslot %}
</label>
{% endif %}
<input {% if attrs.type != "checkbox" and attrs.type != "radio" %}class="input"{% endif %}
{% if attrs.required %}required{% endif %}
{% if attrs.disabled %}disabled{% endif %}
{% if attrs.readonly %}readonly{% endif %}
{% if attrs.checked %}checked{% endif %}
{% if attrs.name %}name="{{ attrs.name }}"{% endif %}
{% if attrs.id %}id="{{ attrs.id }}"{% endif %}
{% if attrs.placeholder %}placeholder="{{ attrs.placeholder }}"{% endif %}
{% if attrs.autocomplete %}autocomplete="{{ attrs.autocomplete }}"{% endif %}
{% if attrs.value is not None %}value="{{ attrs.value }}"{% endif %}
type="{{ attrs.type }}">
{% if attrs.type == "checkbox" or attrs.type == "radio" %}
<label for="{{ attrs.id }}">
{% slot label %}
{% endslot %}
</label>
{% endif %}
{% endif %}
{% if slots.help_text %}
<p class="help is-danger">
{% slot help_text %}
{% endslot %}
</p>
{% endif %}
<p class="help is-danger">{{ attrs.errors }}</p>
</div>

View File

@@ -0,0 +1 @@
{{ attrs.form }}

View File

@@ -0,0 +1,12 @@
{% load allauth %}
<div class="block">
<form method="{{ attrs.method }}"
{% if attrs.action %}action="{{ attrs.action }}"{% endif %}>
{% slot body %}
{% endslot %}
<div class="field is-grouped is-grouped-multiline">
{% slot actions %}
{% endslot %}
</div>
</form>
</div>

View File

@@ -0,0 +1 @@
{% comment %} djlint:off {% endcomment %}{% load allauth %}<h1 class="title is-1">{% slot %}{% endslot %}</h1>

View File

@@ -0,0 +1 @@
{% comment %} djlint:off {% endcomment %}{% load allauth %}<h2 class="title is-2">{% slot %}{% endslot %}</h2>

View File

@@ -0,0 +1 @@
{% comment %} djlint:off {% endcomment %}{% load allauth %}<p class="content">{% slot %}{% endslot %}</p>

View File

@@ -0,0 +1,18 @@
{% load allauth %}
<section class="block">
<h2 class="title is-2">
{% slot title %}
{% endslot %}
</h2>
{% slot body %}
{% endslot %}
{% if slots.actions %}
<div class="field is-grouped is-grouped-multiline">
{% for action in slots.actions %}
<div class="control">
{{ action }}
</div>
{% endfor %}
</div>
{% endif %}
</section>

View File

@@ -0,0 +1 @@
{% extends "fellchensammlung/base.html" %}

View File

@@ -33,6 +33,11 @@
<i class="fas fa-search fa-fw"></i> {% trans 'Suchen' %} <i class="fas fa-search fa-fw"></i> {% trans 'Suchen' %}
</button> </button>
</form> </form>
<hr>
<form method="post" autocomplete="off">
{% csrf_token %}
{{ org_name_search_form }}
</form>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@@ -69,4 +74,57 @@
{% endfor %} {% endfor %}
</ul> </ul>
</nav> </nav>
<script>
$(document).ready(function () {
const $nameInput = $("#id_name");
$nameInput.wrap("<div class='dropdown' id='location-result-list'></div>");
const dropdown = $("#location-result-list");
$nameInput.wrap("<div class='dropdown-trigger'></div>");
$("<div class='dropdown-content' id='results'></div>").insertAfter($nameInput);
const $resultsList = $("#results");
$resultsList.wrap("<div class='dropdown-menu'></div>");
$nameInput.on("input", function () {
const query = $.trim($nameInput.val());
if (query.length < 3) {
dropdown.removeClass("is-active");
return;
}
$.ajax({
url: "{% api_base_url %}organizations/",
data: {
search: query
},
method: "GET",
dataType: "json",
success: function (data) {
$resultsList.empty();
dropdown.addClass("is-active");
if (data) {
const orgs = data.slice(0, 5);
$.each(orgs, function (index, org) {
const $listItem = $("<a>")
.addClass("dropdown-item")
.addClass("result-item")
.attr('href', org.url)
.text(org.name);
$resultsList.append($listItem);
});
}
},
error: function () {
$resultsList.html('<li class="result-item">{% trans 'Error fetching data. Please try again.' %}</li>');
}
});
});
});
</script>
{% endblock %} {% endblock %}

View File

@@ -37,6 +37,26 @@
{% block header %} {% block header %}
{% include "fellchensammlung/header.html" %} {% include "fellchensammlung/header.html" %}
{% endblock %} {% endblock %}
{% if profile %}
<div class="profile">
<table class="table is-bordered is-fullwidth is-hoverable is-striped">
<thead>
<tr>
<td>Timestamp</td>
<td>Status</td>
</tr>
</thead>
<tbody>
{% for status in profile %}
<tr>
<td>{{ status.0 }}</td>
<td>{{ status.1 }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
<div class="main-content"> <div class="main-content">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
@@ -45,5 +65,8 @@
{% block footer %} {% block footer %}
{% include "fellchensammlung/footer.html" %} {% include "fellchensammlung/footer.html" %}
{% endblock %} {% endblock %}
{% block extra_body %}
{% endblock extra_body %}
</body> </body>
</html> </html>

View File

@@ -114,7 +114,17 @@
<i class="fas fa-chart-line fa-fw"></i> {% trans 'Aufrufe' %} <i class="fas fa-chart-line fa-fw"></i> {% trans 'Aufrufe' %}
</a> </a>
{% endif %} {% endif %}
<hr class="dropdown-divider"> <hr class="dropdown-divider">
<a class="dropdown-item"
href="{% url 'adoption-notice-social-media-template-selection' adoption_notice_id=adoption_notice.pk %}">
<i class="fab fa-instagram fa-fw"
aria-hidden="true"></i> {% trans 'Social Media Vorlagen' %}
</a>
<hr class="dropdown-divider">
<a class="dropdown-item is-warning" <a class="dropdown-item is-warning"
href="{% url adoption_notice_meta|admin_urlname:'change' adoption_notice.pk %}"> href="{% url adoption_notice_meta|admin_urlname:'change' adoption_notice.pk %}">
<i class="fa-solid fa-tools fa-fw"></i> Admin interface <i class="fa-solid fa-tools fa-fw"></i> Admin interface
@@ -164,6 +174,9 @@
{% if adoption_notice.get_photos %} {% if adoption_notice.get_photos %}
<div class="column block"> <div class="column block">
<div class="card"> <div class="card">
{% if not adoption_notice.is_active %}
<div class="cover">{{ adoption_notice.status_description_short }}</div>
{% endif %}
<div class="grid card-content"> <div class="grid card-content">
<div class="gallery"> <div class="gallery">
{% with photo=adoption_notice.get_photos.0 %} {% with photo=adoption_notice.get_photos.0 %}

View File

@@ -19,59 +19,25 @@
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<div class="block"> <div class="block">
<div class="card"> {% include "fellchensammlung/partials/rescue_orgs/partial-basic-info-card.html" %}
<div class="card-header">
<h1 class="card-header-title">{{ org.name }}</h1>
</div>
<div class="card-content">
<div class="block">
<b><i class="fa-solid fa-location-dot"></i></b>
{% if org.location %}
{{ org.location }}
{% else %}
{{ org.location_string }}
{% endif %}
{% if org.description %}
<div class="block content">
<p>{{ org.description | render_markdown }}</p>
</div>
{% endif %}
</div>
{% if org.specializations %}
<div class="block">
<h3 class="title is-5">{% translate 'Spezialisierung' %}</h3>
<div class="content">
<ul>
{% for specialization in org.specializations.all %}
<li>{{ specialization }}</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% if org.parent_org %}
<div class="block">
<h3 class="title is-5">{% translate 'Übergeordnete Organisation' %}</h3>
<p>
<span>
<i class="fa-solid fa-building fa-fw"
aria-label="{% trans 'Tierschutzorganisation' %}"></i>
<a href="{{ org.parent_org.get_absolute_url }}"> {{ org.parent_org }}</a>
</span>
</p>
</div>
{% endif %}
</div>
</div>
</div> </div>
<div class="block"> <div class="block">
{% include "fellchensammlung/partials/partial-rescue-organization-contact.html" %} {% include "fellchensammlung/partials/rescue_orgs/partial-rescue-organization-contact.html" %}
</div>
{% trust_level "MODERATOR" as coordinator_trust_level %}
{% if request.user.trust_level >= coordinator_trust_level %}
<div class="block">
<a class="button is-primary is-fullwidth"
href="{% url 'rescue-organization-edit' rescue_organization_id=org.pk %}">
<i class="fa-solid fa-tools fa-fw"></i> {% translate 'Bearbeiten' %}
</a>
</div> </div>
<div class="block"> <div class="block">
<a class="button is-warning is-fullwidth" href="{% url org_meta|admin_urlname:'change' org.pk %}"> <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 <i class="fa-solid fa-tools fa-fw"></i> Admin interface
</a> </a>
</div> </div>
{% endif %}
</div> </div>
<div class="column"> <div class="column">
{% include "fellchensammlung/partials/partial-map.html" %} {% include "fellchensammlung/partials/partial-map.html" %}

View File

@@ -1,7 +1,8 @@
{% extends "fellchensammlung/base.html" %} {% extends "fellchensammlung/base.html" %}
{% load i18n %} {% load i18n %}
{% load account %}
{% block title %}<title>{{ user.get_full_name }}</title>{% endblock %} {% block title %}<title>{% user_display user %}</title>{% endblock %}
{% block content %} {% block content %}
@@ -13,7 +14,7 @@
</div> </div>
<div class="level-right"> <div class="level-right">
<div class="level-item"> <div class="level-item">
<form class="" action="{% url 'logout' %}" method="post"> <form class="" action="{% url 'account_logout' %}" method="post">
{% csrf_token %} {% csrf_token %}
<button class="button" type="submit"> <button class="button" type="submit">
<i aria-hidden="true" class="fas fa-sign-out fa-fw"></i> Logout <i aria-hidden="true" class="fas fa-sign-out fa-fw"></i> Logout
@@ -25,16 +26,34 @@
<div class="block"> <div class="block">
<h2 class="title is-2">{% trans 'Profil verwalten' %}</h2> <h2 class="title is-2">{% trans 'Profil verwalten' %}</h2>
<p><strong>{% translate "E-Mail" %}:</strong> {{ user.email }}</p> <div class="block"><strong>{% translate "E-Mail" %}:</strong> {{ user.email }}</div>
<div class=""> {% if user.id is request.user.id %}
<p> <div class="block">
<a class="button is-warning" href="{% url 'password_change' %}">{% translate "Change password" %}</a> <div class="field is-grouped is-grouped-multiline">
<a class="button is-info" href="{% url 'user-me-export' %}">{% translate "Daten exportieren" %}</a> <div class="control">
</p> <a class="button is-warning"
href="{% url 'account_change_password' %}">{% translate "Passwort ändern" %}</a>
</div>
<div class="control">
<a class="button is-warning"
href="{% url 'account_email' %}">
{% translate "E-Mail Adresse ändern" %}
</a>
</div>
<div class="control">
<a class="button is-warning"
href="{% url 'mfa_index' %}">
{% translate "2-Faktor Authentifizierung" %}
</a>
</div>
<div class="control">
<a class="button is-info" href="{% url 'user-me-export' %}">
{% translate "Daten exportieren" %}
</a>
</div>
</div> </div>
</div> </div>
{% if user.id is request.user.id %}
<div class="block"> <div class="block">
<h2 class="title is-2">{% trans 'Einstellungen' %}</h2> <h2 class="title is-2">{% trans 'Einstellungen' %}</h2>
<form class="block" action="" method="POST"> <form class="block" action="" method="POST">
@@ -79,6 +98,27 @@
</details> </details>
</div> </div>
{% endif %}
{% if show_mod_actions %}
<div class="block">
<h2 class="title is-2">{% trans 'Moderation' %}</h2>
<div class="block">
<p><strong>{% translate 'Moderationsnotizen' %}:</strong> {{ user.mod_notes }}</p>
</div>
<div class="block">
{% if user.is_active %}
<a href="{% url 'user-deactivate' user_id=user.pk %}" class="button is-danger">
{% translate 'User deaktivieren' %}
</a>
{% else %}
<a href="{% url 'user-activate' user_id=user.pk %}" class="button is-info">
{% translate 'User aktivieren' %}
</a>
{% endif %}
</div>
</div>
{% endif %}
<h2 class="title is-2">{% translate 'Benachrichtigungen' %}</h2> <h2 class="title is-2">{% translate 'Benachrichtigungen' %}</h2>
{% include "fellchensammlung/lists/list-notifications.html" %} {% include "fellchensammlung/lists/list-notifications.html" %}
@@ -86,8 +126,7 @@
<h2 class="title is-2">{% translate 'Abonnierte Suchen' %}</h2> <h2 class="title is-2">{% translate 'Abonnierte Suchen' %}</h2>
{% include "fellchensammlung/lists/list-search-subscriptions.html" %} {% include "fellchensammlung/lists/list-search-subscriptions.html" %}
<h2 class="title is-2">{% translate 'Meine Vermittlungen' %}</h2> <h2 class="title is-2">{% translate 'Vermittlungen' %}</h2>
{% include "fellchensammlung/lists/list-adoption-notices.html" %} {% include "fellchensammlung/lists/list-adoption-notices.html" %}
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -7,9 +7,14 @@
{% block content %} {% block content %}
<h1 class="title is-1">403 Forbidden</h1> <h1 class="title is-1">403 Forbidden</h1>
<p> <p>
{% if error_message %}
{{ error_message }}
{% else %}
{% blocktranslate %} {% blocktranslate %}
Diese Aktion ist dir nicht erlaubt. Logge dich ein oder nutze einen anderen Account. Wenn du denkst, dass hier Diese Aktion ist dir nicht erlaubt. Logge dich ein oder nutze einen anderen Account. Wenn du denkst,
dass hier
ein Fehler vorliegt, kontaktiere das Team! ein Fehler vorliegt, kontaktiere das Team!
{% endblocktranslate %} {% endblocktranslate %}
{% endif %}
</p> </p>
{% endblock %} {% endblock %}

View File

@@ -2,7 +2,7 @@
{% load i18n %} {% load i18n %}
{% load custom_tags %} {% load custom_tags %}
{% block title %}<title>{% translate "403 Forbidden" %}</title>{% endblock %} {% block title %}<title>{% translate "404 Forbidden" %}</title>{% endblock %}
{% block content %} {% block content %}
<h1 class="title is-1">404 Not Found</h1> <h1 class="title is-1">404 Not Found</h1>

View File

@@ -102,6 +102,10 @@
<a class="nav-link " href="{% url "modtools" %}"> <a class="nav-link " href="{% url "modtools" %}">
{% translate 'Moderationstools' %} {% translate 'Moderationstools' %}
</a> </a>
<br>
<a class="nav-link " href="{% url "rescue-organization-create" %}">
{% translate 'Tierschutzorganisation hinzufügen' %}
</a>
{% endif %} {% endif %}
<br/> <br/>
{% if request.user.is_superuser %} {% if request.user.is_superuser %}

View File

@@ -0,0 +1,17 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% block description %}
<meta name="description" content="{% trans 'User aktivieren' %}">
{% endblock %}
{% block content %}
<h1 class="title is-1">{% trans 'User aktivieren' %}</h1>
{% blocktranslate %}
Hier kannst du einen User manuell aktivieren und optional eine Notiz hinterlassen.
{% endblocktranslate %}
<form method="post">
{% csrf_token %}
{{ form }}
<button class="button is-info is-fullwidth" type="submit">{% translate "Aktivieren" %}</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% block description %}
<meta name="description" content="{% trans 'User deaktivieren' %}">
{% endblock %}
{% block content %}
<h1 class="title is-1">{% trans 'User deaktivieren' %}</h1>
{% url "about" as rule_url %}
{% blocktranslate %}
Wenn dieser User unseren <a href='{{ rule_url }}'>Regeln</a> zuwider gehandelt hat oder seit langem inaktiv
ist, kannst du ihn deaktivieren.
{% endblocktranslate %}
<form method="post">
{% csrf_token %}
{{ form }}
<button class="button is-danger is-fullwidth" type="submit">{% translate "Deaktivieren" %}</button>
</form>
{% endblock %}

View File

@@ -1,18 +1,35 @@
{% extends "fellchensammlung/base.html" %} {% extends "fellchensammlung/base.html" %}
{% load i18n %} {% load i18n %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% load admin_urls %}
{% block title %} {% block title %}
<title>Organisation {{ rescue_org }} von regelmäßiger Prüfung ausschließen</title> <title>Organisation {{ org }} von regelmäßiger Prüfung ausschließen</title>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1 class="title is-1">Organisation {{ rescue_org }} von regelmäßiger Prüfung ausschließen</h1> <h1 class="title is-1">Organisation {{ org }} von regelmäßiger Prüfung ausschließen</h1>
<div class="columns block">
<div class="column">
{% include "fellchensammlung/partials/rescue_orgs/partial-basic-info-card.html" %}
</div>
<div class="column">
{% include "fellchensammlung/partials/rescue_orgs/partial-rescue-organization-contact.html" %}
</div>
</div>
<div class="block">
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{{ form }} {{ form }}
<a class="button" href="{% url 'organization-check' %}">{% translate "Zurück (nicht exkludieren)" %}</a> <input class="button is-primary" type="submit" name="delete"
<input class="button is-danger" type="submit" name="delete" value="{% translate "Von regelmäßiger Prüfung ausschließen" %}"> value="{% translate "Aktualisieren" %}">
<a class="button" href="{% url 'organization-check' %}">{% translate "Zurück" %}</a>
</form> </form>
</div>
<div class="block">
<a class="button is-warning is-fullwidth" href="{% url org_meta|admin_urlname:'change' org.pk %}">
<i class="fa-solid fa-tools fa-fw"></i> Admin interface
</a>
</div>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load widget_tweaks %}
{% block content %}
<h1 class="title is-1">
{% if rescue_org %}
{{ rescue_org.name }}
{% else %}
{% translate 'Tierschutzorganisation hinzufügen' %}
{% endif %}
</h1>
<form method="post">
{% csrf_token %}
{{ form }}
<input class="button is-primary" type="submit" value="{% translate "Speichern" %}">
</form>
{% endblock %}

View File

@@ -27,7 +27,7 @@
{{ field|add_class:"input" }} {{ field|add_class:"input" }}
{% endif %} {% endif %}
</div> </div>
<div class="help"> <div class="help content">
{{ field.help_text }} {{ field.help_text }}
</div> </div>
<div class="help is-danger"> <div class="help is-danger">

View File

@@ -49,10 +49,10 @@
{% else %} {% else %}
<div class="navbar-item"> <div class="navbar-item">
<div class="buttons"> <div class="buttons">
<a class="button is-primary" href="{% url "django_registration_register" %}"> <a class="button is-primary" href="{% url "account_signup" %}">
<strong>{% translate "Registrieren" %}</strong> <strong>{% translate "Registrieren" %}</strong>
</a> </a>
<a class="button is-light" href="{% url "login" %}"> <a class="button is-light" href="{% url "account_login" %}">
<strong>{% translate "Login" %}</strong> <strong>{% translate "Login" %}</strong>
</a> </a>
</div> </div>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -3,7 +3,7 @@
{% if rescue_organizations %} {% if rescue_organizations %}
{% for rescue_organization in rescue_organizations %} {% for rescue_organization in rescue_organizations %}
<div class="cell"> <div class="cell">
{% include "fellchensammlung/partials/partial-rescue-organization.html" %} {% include "fellchensammlung/partials/rescue_orgs/partial-rescue-organization.html" %}
</div> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}

View File

@@ -6,21 +6,7 @@
<h1 class="title is-1">{% translate 'Vermittlung deaktivieren' %}</h1> <h1 class="title is-1">{% translate 'Vermittlung deaktivieren' %}</h1>
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div class="field"> {{ form }}
<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="closed_successful_with_notfellchen">{% translate 'Vermittelt mit Hilfe von Notfellchen' %}</option>
<option value="closed_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="closed_not_open_for_adoption_anymore">{% translate 'Nicht mehr zu vermitteln (z.B. aufgrund von Krankheit)' %}</option>
<option value="closed_animal_died">{% translate 'Tod des Tiers' %}</option>
<option value="closed_other">{% translate 'Anderer Grund' %}</option>
</select>
</div>
</div>
</div>
<input class="button is-warning" type="submit" value="{% translate "Vermittlung deaktivieren" %}"> <input class="button is-warning" type="submit" value="{% translate "Vermittlung deaktivieren" %}">
</form> </form>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,62 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load widget_tweaks %}
{% load admin_urls %}
{% block title %}
<title>Social Media Post für {{ adoption_notice }}</title>
{% endblock %}
{% block content %}
<h1 class="title is-1">Social Media Post für {{ adoption_notice }}</h1>
<div class="columns block">
<div class="column">
<div class="box">
{% include 'fellchensammlung/partials/social_media/post-to-fedi.html' %}
<p class="block">
{% blocktranslate %}
Die Vermittlung wird auf unserem Fediverse-Account gepostet
{% endblocktranslate %}
</p>
</div>
</div>
<div class="column">
<div class="box block">
<a href="{% url 'adoption-notice-story-pic' adoption_notice.pk %}"
class="button is-primary is-fullwidth">
<i class="fab fa-instagram fa-fw"></i> {% trans 'Instagram Storyvorlage' %}
</a>
<p class="block">
{% blocktranslate %}
Eine Vorlage für eine Instagram-Story im Format 1080x1980px.
{% endblocktranslate %}
</p>
</div>
</div>
<div class="column">
<div class="box block">
<a href="{% url 'adoption-notice-sharepic' adoption_notice.pk %}"
class="button is-primary block is-fullwidth">
<i class="fab fa-instagram fa-fw"></i> {% trans 'Instagram Post' %}
</a>
<p class="block">
{% blocktranslate %}
Eine Vorlage für eine Instagram-Post mit einer Slide pro Tier.
{% endblocktranslate %}
</p>
</div>
</div>
</div>
<div class="block">
<p>
{% blocktranslate %}
Die Vorlagen werden idealerweise im Grafikprogramm Inkscape weiterbearbeitet.
{% endblocktranslate %}
</p>
<a href="https://inkscape.org/" class="button is-link block is-fullwidth">
<i class="fab fa-inkscape fa-fw"></i>
{% translate 'Inkscape herunterladen' %}
</a>
</div>
{% endblock %}

View File

@@ -20,39 +20,7 @@
</div> </div>
<div class="block"> <div class="block">
{% if action_was_posting %} {% include "fellchensammlung/partials/social_media/post-to-fedi.html" %}
{% if posted_successfully %}
<div class="message is-success">
<div class="message-header">
{% translate 'Vermittlung gepostet' %}
</div>
<div class="message-body">
{% blocktranslate with post_url=post.url %}
Link zum Post: <a href={{ post_url }}>{{ post_url }}</a>
{% endblocktranslate %}
</div>
</div>
{% else %}
<div class="message is-danger">
<div class="message-header">
{% translate 'Vermittlung konnte nicht gepostet werden' %}
</div>
<div class="message-body">
{{ error_message }}
</div>
</div>
{% endif %}
{% endif %}
<form class="cell" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="post_to_fedi">
<button class="button is-fullwidth is-warning is-primary" type="submit" id="submit">
<i class="fa-solid fa-bullhorn fa-fw"></i> {% translate "Vermittlung ins Fediverse posten" %}
</button>
</form>
</div> </div>
</div> </div>

View File

@@ -7,6 +7,9 @@
{% translate 'Adoptionsprozess' %} {% translate 'Adoptionsprozess' %}
</h2> </h2>
</div> </div>
{% if not adoption_notice.is_active %}
<div class="cover">{{ adoption_notice.status_description_short }}</div>
{% endif %}
<div class="card-content"> <div class="card-content">
{% include adoption_process_template %} {% include adoption_process_template %}
</div> </div>

View File

@@ -31,5 +31,14 @@
{{ adoption_notice.status_description }} {{ adoption_notice.status_description }}
</div> </div>
</article> </article>
{% elif adoption_notice.is_disabled %}
<article class="message is-warning">
<div class="message-header">
<p>{% translate 'Vermittlung wurde gesperrt' %}</p>
</div>
<div class="message-body content">
{{ adoption_notice.status_description }}
</div>
</article>
{% endif %} {% endif %}

View File

@@ -1,7 +1,7 @@
{% load i18n %} {% load i18n %}
{% load custom_tags %} {% load custom_tags %}
<div class="card"> <div class="card">
<a href="{{ adoption_notice.get_absolute_url }}" class="card-header"> <a href="{{ adoption_notice.get_absolute_url }}" class="card-header" target="_blank">
<div class="card-header-title"> <div class="card-header-title">
{{ adoption_notice.name }} {{ adoption_notice.name }}
</div> </div>
@@ -17,25 +17,21 @@
{% endif %} {% endif %}
</div> </div>
<div class="card-footer"> <div class="card-footer">
<div class="card-footer-item is-confirm"> <form class="card-footer-item is-confirm" method="post">
<form method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" <input type="hidden"
name="adoption_notice_id" name="adoption_notice_id"
value="{{ adoption_notice.pk }}"> value="{{ adoption_notice.pk }}">
<input type="hidden" name="action" value="checked_active"> <input type="hidden" name="action" value="checked_active">
<button class="" type="submit">{% translate "Vermittlung noch aktuell" %}</button> <button style="width: 100%" type="submit">{% translate "Vermittlung noch aktuell" %}</button>
</form> </form>
</div> <form class="card-footer-item is-warning" method="post">
<div class="card-footer-item is-warning">
<form method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" <input type="hidden"
name="adoption_notice_id" name="adoption_notice_id"
value="{{ adoption_notice.pk }}"> value="{{ adoption_notice.pk }}">
<input type="hidden" name="action" value="checked_inactive"> <input type="hidden" name="action" value="checked_inactive">
<button class="" type="submit">{% translate "Vermittlung inaktiv" %}</button> <button style="width:100%" type="submit">{% translate "Vermittlung inaktiv" %}</button>
</form> </form>
</div> </div>
</div> </div>
</div>

View File

@@ -0,0 +1,46 @@
{% load i18n %}
{% load custom_tags %}
<div class="card">
<div class="card-header">
<h1 class="card-header-title">{{ org.name }}</h1>
</div>
<div class="card-content">
<div class="block">
<b><i class="fa-solid fa-location-dot"></i></b>
{% if org.location %}
{{ org.location }}
{% else %}
{{ org.location_string }}
{% endif %}
{% if org.description %}
<div class="block content">
<p>{{ org.description | render_markdown }}</p>
</div>
{% endif %}
</div>
{% if org.specializations %}
<div class="block">
<h3 class="title is-5">{% translate 'Spezialisierung' %}</h3>
<div class="content">
<ul>
{% for specialization in org.specializations.all %}
<li>{{ specialization }}</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% if org.parent_org %}
<div class="block">
<h3 class="title is-5">{% translate 'Übergeordnete Organisation' %}</h3>
<p>
<span>
<i class="fa-solid fa-building fa-fw"
aria-label="{% trans 'Tierschutzorganisation' %}"></i>
<a href="{{ org.parent_org.get_absolute_url }}"> {{ org.parent_org }}</a>
</span>
</p>
</div>
{% endif %}
</div>
</div>

View File

@@ -71,7 +71,22 @@
value="{{ rescue_org.pk }}"> value="{{ rescue_org.pk }}">
<input type="hidden" name="action" value="checked"> <input type="hidden" name="action" value="checked">
<button class="" type="submit">{% translate "Organisation geprüft" %}</button> <button class="" type="submit">{% translate "Organisation geprüft" %}</button>
</form>
</div>
<div class="card-footer-item is-warning">
<form method="post">
{% csrf_token %}
<input type="hidden"
name="rescue_organization_id"
value="{{ rescue_org.pk }}">
<input type="hidden" name="action" value="toggle_active_communication">
<button class="" type="submit">
{% if rescue_org.ongoing_communication %}
{% translate "Aktive Kommunikation beendet" %}
{% else %}
{% translate "Aktive Kommunikation" %}
{% endif %}
</button>
</form> </form>
</div> </div>
<div class="card-footer-item is-danger"> <div class="card-footer-item is-danger">

View File

@@ -1,9 +1,10 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
<div class="grid is-col-min-2"> <div class="grid is-col-min-2">
{% if adoption_notice.num_per_sex.F > 0 %} {% with num_per_sex=adoption_notice.num_per_sex %}
{% if num_per_sex.F > 0 %}
<span class="cell icon-text tag is-medium"> <span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.F }} </span> <span class="has-text-weight-bold is-size-4">{{ num_per_sex.F }}</span>
<span class="icon"> <span class="icon">
<img class="icon" src="{% static 'fellchensammlung/img/sexes/Female.png' %}" <img class="icon" src="{% static 'fellchensammlung/img/sexes/Female.png' %}"
alt="{% translate 'weibliche Tiere' %}"> alt="{% translate 'weibliche Tiere' %}">
@@ -11,9 +12,9 @@
</span> </span>
{% endif %} {% endif %}
{% if adoption_notice.num_per_sex.I > 0 %} {% if num_per_sex.I > 0 %}
<span class="cell icon-text tag is-medium"> <span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.I }}</span> <span class="has-text-weight-bold is-size-4">{{ num_per_sex.I }}</span>
<span class="icon"> <span class="icon">
<img class="icon" <img class="icon"
@@ -22,18 +23,18 @@
</span> </span>
</span> </span>
{% endif %} {% endif %}
{% if adoption_notice.num_per_sex.M > 0 %} {% if num_per_sex.M > 0 %}
<span class="cell icon-text tag is-medium"> <span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.M }}</span> <span class="has-text-weight-bold is-size-4">{{ num_per_sex.M }}</span>
<span class="icon"> <span class="icon">
<img class="icon" src="{% static 'fellchensammlung/img/sexes/Male.png' %}" <img class="icon" src="{% static 'fellchensammlung/img/sexes/Male.png' %}"
alt="{% translate 'männliche Tiere' %}"> alt="{% translate 'männliche Tiere' %}">
</span> </span>
</span> </span>
{% endif %} {% endif %}
{% if adoption_notice.num_per_sex.M_N > 0 %} {% if num_per_sex.M_N > 0 %}
<span class="cell icon-text tag is-medium"> <span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.M_N }}</span> <span class="has-text-weight-bold is-size-4">{{ num_per_sex.M_N }}</span>
<span class="icon"> <span class="icon">
<img class="icon" <img class="icon"
src="{% static 'fellchensammlung/img/sexes/Male Neutered.png' %}" src="{% static 'fellchensammlung/img/sexes/Male Neutered.png' %}"
@@ -41,4 +42,5 @@
</span> </span>
</span> </span>
{% endif %} {% endif %}
{% endwith %}
</div> </div>

View File

@@ -0,0 +1,38 @@
{% load i18n %}
{% 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">
{% if adoption_notice %}
<input type="hidden" name="adoption_notice_pk" value="{{ adoption_notice.pk }}">
{% endif %}
<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>

View File

@@ -38,7 +38,7 @@
<div class="grid is-col-min-15"> <div class="grid is-col-min-15">
{% for rescue_org in rescue_orgs_to_check %} {% for rescue_org in rescue_orgs_to_check %}
<div class="cell"> <div class="cell">
{% include "fellchensammlung/partials/partial-check-rescue-org.html" %} {% include "fellchensammlung/partials/rescue_orgs/partial-check-rescue-org.html" %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@@ -50,7 +50,7 @@
<div class="grid is-col-min-15"> <div class="grid is-col-min-15">
{% for rescue_org in rescue_orgs_with_ongoing_communication %} {% for rescue_org in rescue_orgs_with_ongoing_communication %}
<div class="cell"> <div class="cell">
{% include "fellchensammlung/partials/partial-check-rescue-org.html" %} {% include "fellchensammlung/partials/rescue_orgs/partial-check-rescue-org.html" %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@@ -62,7 +62,7 @@
<div class="grid is-col-min-15"> <div class="grid is-col-min-15">
{% for rescue_org in rescue_orgs_last_checked %} {% for rescue_org in rescue_orgs_last_checked %}
<div class="cell"> <div class="cell">
{% include "fellchensammlung/partials/partial-check-rescue-org.html" %} {% include "fellchensammlung/partials/rescue_orgs/partial-check-rescue-org.html" %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -0,0 +1,43 @@
{% extends "mfa/recovery_codes/base.html" %}
{% load i18n %}
{% load allauth %}
{% block content %}
{% element h1 %}
{% translate "Recovery Codes" %}
{% endelement %}
{% element p %}
{% blocktranslate count unused_count=unused_codes|length %}There is {{ unused_count }} out of {{ total_count }}
recovery codes available.{% plural %}There are {{ unused_count }} out of {{ total_count }} recovery codes
available.{% endblocktranslate %}
{% endelement %}
<div class="block">
{% element field id="recovery_codes" type="textarea" disabled=True rows=unused_codes|length readonly=True %}
{% slot label %}
{% translate "Unused codes" %}
{% endslot %}
{% comment %} djlint:off {% endcomment %}
{% slot value %}{% for code in unused_codes %}{% if forloop.counter0 %}
{% endif %}{{ code }}{% endfor %}{% endslot %}
{% comment %} djlint:on {% endcomment %}
{% endelement %}
</div>
<div class="block">
<div class="field is-grouped is-grouped-multiline">
{% if unused_codes %}
{% url 'mfa_download_recovery_codes' as download_url %}
<div class="control">
{% element button href=download_url %}
{% translate "Download codes" %}
{% endelement %}
</div>
{% endif %}
{% url 'mfa_generate_recovery_codes' as generate_url %}
<div class="control">
{% element button href=generate_url %}
{% translate "Generate new codes" %}
{% endelement %}
</div>
</div>
</div>
{% endblock content %}

View File

@@ -0,0 +1,78 @@
{% extends "mfa/webauthn/base.html" %}
{% load i18n %}
{% load static %}
{% load allauth %}
{% load humanize %}
{% block content %}
{% element h1 %}
{% trans "Security Keys" %}
{% endelement %}
{% if authenticators|length == 0 %}
{% element p %}
{% blocktranslate %}No security keys have been added.{% endblocktranslate %}
{% endelement %}
{% else %}
<article class="panel">
<p class="panel-heading">{% trans "Security Keys" %}</p>
{% for authenticator in authenticators %}
<div class="panel-block">
<div class="level" style="width: 100%;">
<div class="level-left">
<div class="level-item">
{% if authenticator.wrap.is_passwordless is True %}
{% element badge tags="mfa,key,primary" %}
{% translate "Passkey" %}
{% endelement %}
{% elif authenticator.wrap.is_passwordless is False %}
{% element badge tags="mfa,key,secondary" %}
{% translate "Security key" %}
{% endelement %}
{% else %}
{% element badge title=_("This key does not indicate whether it is a passkey.") tags="mfa,key,warning" %}
{% translate "Unspecified" %}
{% endelement %}
{% endif %}
</div>
<div class="level-item">
<strong>
{{ authenticator }}
</strong>
</div>
<div class="level-item">
{% blocktranslate with created_at=authenticator.created_at|date:"SHORT_DATE_FORMAT" %}
Added
on {{ created_at }}{% endblocktranslate %}.
</div>
<div class="level-item">
{% if authenticator.last_used_at %}
{% blocktranslate with last_used=authenticator.last_used_at|naturaltime %}Last used
{{ last_used }}{% endblocktranslate %}
{% else %}
Not used.
{% endif %}
</div>
</div>
<div class="level-right">
<div class="level-item">
{% url 'mfa_edit_webauthn' pk=authenticator.pk as edit_url %}
{% element button tags="mfa,authenticator,edit,tool" href=edit_url %}
{% translate "Edit" %}
{% endelement %}
</div>
<div class="level-item">
{% url 'mfa_remove_webauthn' pk=authenticator.pk as remove_url %}
{% element button tags="mfa,authenticator,danger,delete,tool" href=remove_url %}
{% translate "Remove" %}
{% endelement %}
</div>
</div>
</div>
</div>
{% endfor %}
</article>
{% endif %}
{% url 'mfa_add_webauthn' as add_url %}
{% element button href=add_url %}
{% translate "Add" %}
{% endelement %}
{% endblock %}

View File

@@ -5,6 +5,7 @@ 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 django.utils import timezone
from django.urls import reverse
from fellchensammlung.tools.misc import time_since_as_hr_string from fellchensammlung.tools.misc import time_since_as_hr_string
from notfellchen import settings from notfellchen import settings
@@ -54,6 +55,11 @@ def get_oxitraffic_script_if_enabled():
return "" return ""
@register.simple_tag
def api_base_url():
return reverse("api-base-url")
@register.filter @register.filter
@stringfilter @stringfilter
def pointdecimal(value): def pointdecimal(value):

View File

@@ -1,8 +1,9 @@
import logging import logging
import requests import requests
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
from fellchensammlung.models import SocialMediaPost, PlatformChoices from fellchensammlung.models import SocialMediaPost, PlatformChoices, AdoptionNotice
from notfellchen import settings from notfellchen import settings
@@ -86,7 +87,10 @@ class FediClient:
def post_an_to_fedi(adoption_notice): def post_an_to_fedi(adoption_notice):
try:
client = FediClient(settings.fediverse_access_token, settings.fediverse_api_base_url) client = FediClient(settings.fediverse_access_token, settings.fediverse_api_base_url)
except AttributeError:
raise ConnectionError("Configuration for connecting to a Fediverse account is missing")
context = {"adoption_notice": adoption_notice} context = {"adoption_notice": adoption_notice}
status_text = render_to_string("fellchensammlung/misc/fediverse/an-post.md", context) status_text = render_to_string("fellchensammlung/misc/fediverse/an-post.md", context)
@@ -101,3 +105,33 @@ def post_an_to_fedi(adoption_notice):
platform=PlatformChoices.FEDIVERSE, platform=PlatformChoices.FEDIVERSE,
url=response['url'], ) url=response['url'], )
return post return post
def handle_post_fedi_action(adoption_notice: AdoptionNotice = 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.")}
except ConnectionError as e:
logging.error(f"Could not post fediverse post: {e}")
context = {"action_was_posting": True,
"posted_successfully": False,
"error_message": _(
"Fehler beim Posten, in der Konfiguration fehlen Zugangsdaten zu einem Fediverse Account")}
else:
context = {"action_was_posting": True,
"posted_successfully": False,
"error_message": _("Keine Vermittlung zum Posten gefunden.")}
return context

View File

@@ -3,7 +3,7 @@ from django.template.loader import render_to_string
from fellchensammlung.models import AdoptionNotice from fellchensammlung.models import AdoptionNotice
def export_svg(adoption_notice): def export_svg(adoption_notice, template_name: str = "fellchensammlung/images/adoption-notice.svg"):
result = render_to_string(template_name="fellchensammlung/images/adoption-notice.svg", result = render_to_string(template_name=template_name,
context={"adoption_notice": adoption_notice, }) context={"adoption_notice": adoption_notice, })
return result return result

View File

@@ -2,14 +2,19 @@ from datetime import timedelta
from django.utils import timezone from django.utils import timezone
from fellchensammlung.models import User, AdoptionNotice, AdoptionNoticeStatusChoices, Animal, RescueOrganization from fellchensammlung.models import User, AdoptionNotice, AdoptionNoticeStatusChoices, Animal, RescueOrganization, \
AllowUseOfMaterialsChices
def get_rescue_org_check_stats(): def get_rescue_org_check_stats():
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,
ongoing_communication=False).exclude(
allows_using_materials=AllowUseOfMaterialsChices.USE_MATERIALS_DENIED).filter(
last_checked__lt=timeframe).count() last_checked__lt=timeframe).count()
num_rescue_orgs_checked = RescueOrganization.objects.filter(exclude_from_check=False).filter( num_rescue_orgs_checked = RescueOrganization.objects.filter(exclude_from_check=False,
ongoing_communication=False).exclude(
allows_using_materials=AllowUseOfMaterialsChices.USE_MATERIALS_DENIED).filter(
last_checked__gte=timeframe).count() last_checked__gte=timeframe).count()
try: try:

View File

@@ -1,5 +1,6 @@
import datetime as datetime import datetime as datetime
import logging import logging
import time
from django.utils.translation import ngettext from django.utils.translation import ngettext
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@@ -75,3 +76,23 @@ def is_404(url):
return result.status_code == 404 return result.status_code == 404
except requests.RequestException as e: except requests.RequestException as e:
logging.warning(f"Request to {url} failed: {e}") logging.warning(f"Request to {url} failed: {e}")
class RequestProfiler:
data = []
def clear(self):
self.data = []
def add_status(self, status):
self.data.append((time.time(), status))
@property
def as_relative(self):
first_ts = self.data[0][0]
return [(datum[0] - first_ts, datum[1]) for datum in self.data]
@property
def as_relative_with_ms(self):
first_ts = self.data[0][0]
return [(f"{(datum[0] - first_ts)*1000:.4}ms", datum[1]) for datum in self.data]

View File

@@ -69,14 +69,14 @@ class AdoptionNoticeStatusChoices:
UNCHECKED = "awaiting_action_unchecked", _("Unchecked") UNCHECKED = "awaiting_action_unchecked", _("Unchecked")
class Closed(TextChoices): class Closed(TextChoices):
SUCCESSFUL_WITH_NOTFELLCHEN = "closed_successful_with_notfellchen", _("Successful (with Notfellchen)") SUCCESSFUL = "closed_successfully", _("Erfolgreich vermittelt")
SUCCESSFUL_WITHOUT_NOTFELLCHEN = "closed_successful_without_notfellchen", _("Successful (without Notfellchen)") ANIMAL_DIED = "closed_animal_died", _("Tier gestorben")
ANIMAL_DIED = "closed_animal_died", _("Animal died") FOR_OTHER_ADOPTION_NOTICE = ("closed_for_other_adoption_notice",
FOR_OTHER_ADOPTION_NOTICE = "closed_for_other_adoption_notice", _("Closed for other adoption notice") _("Vermittlung wurde zugunsten einer anderen geschlossen."))
NOT_OPEN_ANYMORE = "closed_not_open_for_adoption_anymore", _("Not open for adoption anymore") NOT_OPEN_ANYMORE = "closed_not_open_for_adoption_anymore", _("Tier(e) stehen nicht mehr zur Vermittlung bereit.")
LINK_TO_MORE_INFO_NOT_REACHABLE = "closed_link_to_more_info_not_reachable", _( LINK_TO_MORE_INFO_NOT_REACHABLE = "closed_link_to_more_info_not_reachable", _(
"Der Link zu weiteren Informationen ist nicht mehr erreichbar.") "Der Link zu weiteren Informationen ist nicht mehr erreichbar.")
OTHER = "closed_other", _("Other (closed)") OTHER = "closed_other", _("Anderes")
class Disabled(TextChoices): class Disabled(TextChoices):
AGAINST_RULES = "disabled_against_the_rules", _("Against the rules") AGAINST_RULES = "disabled_against_the_rules", _("Against the rules")
@@ -97,8 +97,7 @@ class AdoptionNoticeStatusChoicesDescriptions:
_ansc = AdoptionNoticeStatusChoices # Mapping for readability _ansc = AdoptionNoticeStatusChoices # Mapping for readability
mapping = {_ansc.Active.SEARCHING.value: "", mapping = {_ansc.Active.SEARCHING.value: "",
_ansc.Active.INTERESTED: _("Jemand hat bereits Interesse an den Tieren."), _ansc.Active.INTERESTED: _("Jemand hat bereits Interesse an den Tieren."),
_ansc.Closed.SUCCESSFUL_WITH_NOTFELLCHEN: _("Vermittlung erfolgreich abgeschlossen."), _ansc.Closed.SUCCESSFUL: _("Vermittlung erfolgreich abgeschlossen."),
_ansc.Closed.SUCCESSFUL_WITHOUT_NOTFELLCHEN: _("Vermittlung erfolgreich abgeschlossen."),
_ansc.Closed.ANIMAL_DIED: _("Die zu vermittelnden Tiere sind über die Regenbrücke gegangen."), _ansc.Closed.ANIMAL_DIED: _("Die zu vermittelnden Tiere sind über die Regenbrücke gegangen."),
_ansc.Closed.FOR_OTHER_ADOPTION_NOTICE: _("Vermittlung wurde zugunsten einer anderen geschlossen."), _ansc.Closed.FOR_OTHER_ADOPTION_NOTICE: _("Vermittlung wurde zugunsten einer anderen geschlossen."),
_ansc.Closed.NOT_OPEN_ANYMORE: _("Tier(e) stehen nicht mehr zur Vermittlung bereit."), _ansc.Closed.NOT_OPEN_ANYMORE: _("Tier(e) stehen nicht mehr zur Vermittlung bereit."),
@@ -113,8 +112,8 @@ class AdoptionNoticeStatusChoicesDescriptions:
_ansc.AwaitingAction.UNCHECKED: _( _ansc.AwaitingAction.UNCHECKED: _(
"Vermittlung deaktiviert bis sie vom Team auf Aktualität geprüft wurde."), "Vermittlung deaktiviert bis sie vom Team auf Aktualität geprüft wurde."),
_ansc.Disabled.AGAINST_RULES: _("Vermittlung deaktiviert da sie gegen die Regeln verstößt."), _ansc.Disabled.AGAINST_RULES: _("Die Vermittlung wurde gesperrt, da sie gegen die Regeln verstößt."),
_ansc.Disabled.OTHER: _("Vermittlung deaktiviert.") _ansc.Disabled.OTHER: _("Vermittlung gesperrt.")
} }
@@ -134,3 +133,14 @@ class RegularCheckStatusChoices(models.TextChoices):
EXCLUDED_OTHER_ORG = "excluded_other_org", _("Exkludiert: Andere Organisation wird geprüft") EXCLUDED_OTHER_ORG = "excluded_other_org", _("Exkludiert: Andere Organisation wird geprüft")
EXCLUDED_SCOPE = "excluded_scope", _("Exkludiert: Organisation hat nie Notfellchen-relevanten Vermittlungen") EXCLUDED_SCOPE = "excluded_scope", _("Exkludiert: Organisation hat nie Notfellchen-relevanten Vermittlungen")
EXCLUDED_OTHER = "excluded_other", _("Exkludiert: Anderer Grund") EXCLUDED_OTHER = "excluded_other", _("Exkludiert: Anderer Grund")
##########
## USER ##
##########
reason_for_signup_label = _("Grund für die Registrierung")
reason_for_signup_help_text = _(
"Wir würden gerne wissen warum du dich registrierst, ob du dich z.B. Tiere eines bestimmten Tierheim einstellen "
"willst 'nur mal gucken' willst. Beides ist toll! Wenn du für ein Tierheim/eine Pflegestelle arbeitest "
"kontaktieren wir dich ggf. um dir erweiterte Rechte zu geben.")

View File

@@ -173,6 +173,7 @@ class AdoptionNoticeSearch:
class RescueOrgSearch: class RescueOrgSearch:
def __init__(self, request): def __init__(self, request):
self.name = None
self.area_search = None self.area_search = None
self.max_distance = None self.max_distance = None
self.location = None # Can either be Location (DjangoModel) or LocationProxy self.location = None # Can either be Location (DjangoModel) or LocationProxy
@@ -229,6 +230,7 @@ class RescueOrgSearch:
return fitting_rescue_orgs return fitting_rescue_orgs
def rescue_org_search_from_request(self, request): def rescue_org_search_from_request(self, request):
# Only search if request method is get with action search
if request.method == 'GET' and request.GET.get("action", False) == "search": if request.method == 'GET' and request.GET.get("action", False) == "search":
self.search_form = RescueOrgSearchForm(request.GET) self.search_form = RescueOrgSearchForm(request.GET)
self.search_form.is_valid() self.search_form.is_valid()

View File

@@ -30,9 +30,15 @@ urlpatterns = [
path("tier/<int:animal_id>/add-photo", views.add_photo_to_animal, name="animal-add-photo"), path("tier/<int:animal_id>/add-photo", views.add_photo_to_animal, name="animal-add-photo"),
# ex: /adoption_notice/7/ # ex: /adoption_notice/7/
path("vermittlung/<int:adoption_notice_id>/", views.adoption_notice_detail, name="adoption-notice-detail"), path("vermittlung/<int:adoption_notice_id>/", views.adoption_notice_detail, name="adoption-notice-detail"),
# ex: /adoption_notice/7/social-media-templates
path("vermittlung/<int:adoption_notice_id>/social-media-templates", views.adoption_notice_social_media_templates,
name="adoption-notice-social-media-template-selection"),
# ex: /adoption_notice/7/sharepic # ex: /adoption_notice/7/sharepic
path("vermittlung/<int:adoption_notice_id>/sharepic", views.adoption_notice_sharepic, path("vermittlung/<int:adoption_notice_id>/sharepic", views.adoption_notice_sharepic,
name="adoption-notice-sharepic"), name="adoption-notice-sharepic"),
# ex: /adoption_notice/7/story
path("vermittlung/<int:adoption_notice_id>/storypic", views.adoption_notice_story_pic,
name="adoption-notice-story-pic"),
# ex: /adoption_notice/7/edit # ex: /adoption_notice/7/edit
path("vermittlung/<int:adoption_notice_id>/edit", views.adoption_notice_edit, name="adoption-notice-edit"), path("vermittlung/<int:adoption_notice_id>/edit", views.adoption_notice_edit, name="adoption-notice-edit"),
# ex: /vermittlung/5/add-photo # ex: /vermittlung/5/add-photo
@@ -41,14 +47,19 @@ 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, path("vermittlung/<int:adoption_notice_id>/close", views.close_adoption_notice,
name="adoption-notice-close"), 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/add", views.rescue_org_create_or_update, name="rescue-organization-create"),
path("tierschutzorganisationen/<int:rescue_organization_id>/edit", views.rescue_org_create_or_update,
name="rescue-organization-edit"),
path("tierschutzorganisationen/<int:rescue_organization_id>/exkludieren", views.exclude_from_regular_check, path("tierschutzorganisationen/<int:rescue_organization_id>/exkludieren", views.exclude_from_regular_check,
name="rescue-organization-exclude"), name="rescue-organization-exclude"),
path("tierschutzorganisationen/add-exclusion-reason", views.update_exclusion_reason,
name="rescue-organization-add-exclusion-reason"),
path("tierschutzorganisationen/spezialisierung/<slug:species_slug>", views.specialized_rescues, path("tierschutzorganisationen/spezialisierung/<slug:species_slug>", views.specialized_rescues,
name="specialized-rescue-organizations"), name="specialized-rescue-organizations"),
@@ -88,20 +99,12 @@ 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/<int:user_id>/deactivate/", views.user_deactivate, name="user-deactivate"),
path("user/<int:user_id>/activate/", views.user_activate, name="user-activate"),
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/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/',
registration_views.HTMLMailRegistrationView.as_view(
form_class=CustomRegistrationForm,
email_body_template="fellchensammlung/mail/activation_email.html",
),
name='django_registration_register',
),
path('accounts/', include('django_registration.backends.activation.urls')),
path('accounts/', include('django.contrib.auth.urls')),
path('change-language', views.change_language, name="change-language"), path('change-language', views.change_language, name="change-language"),
########### ###########
@@ -127,7 +130,7 @@ urlpatterns = [
################### ###################
## External Site ## ## External Site ##
################### ###################
path('bulma/external-site/', views.external_site_warning, name="external-site"), path('external-site/', views.external_site_warning, name="external-site"),
############### ###############
## TECHNICAL ## ## TECHNICAL ##

View File

@@ -1,5 +1,4 @@
import logging import logging
from datetime import timedelta
from django.contrib.auth.views import redirect_to_login from django.contrib.auth.views import redirect_to_login
from django.core.paginator import Paginator from django.core.paginator import Paginator
@@ -14,7 +13,6 @@ 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 notify_mods_new_report from .mail import notify_mods_new_report
from notfellchen import settings from notfellchen import settings
@@ -23,13 +21,14 @@ 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, Subscriptions, Notification, RescueOrganization, \ User, Location, Subscriptions, Notification, RescueOrganization, \
Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, \ Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, \
ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices, SocialMediaPost ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices, SocialMediaPost, AllowUseOfMaterialsChices
from .forms import AdoptionNoticeForm, ImageForm, ReportAdoptionNoticeForm, \ from .forms import AdoptionNoticeForm, ImageForm, ReportAdoptionNoticeForm, \
CommentForm, ReportCommentForm, AnimalForm, AdoptionNoticeFormAutoAnimal, SpeciesURLForm, RescueOrgInternalComment, \ CommentForm, ReportCommentForm, AnimalForm, AdoptionNoticeFormAutoAnimal, SpeciesURLForm, RescueOrgInternalComment, \
UpdateRescueOrgRegularCheckStatus UpdateRescueOrgRegularCheckStatus, UserModCommentForm, CloseAdoptionNoticeForm, RescueOrgSearchByNameForm, \
RescueOrgForm
from .models import Language, Announcement from .models import Language, Announcement
from .tools import i18n, img from .tools import i18n, img
from .tools.fedi import post_an_to_fedi from .tools.fedi import handle_post_fedi_action
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, get_rescue_org_check_stats from .tools.metrics import gather_metrics_data, get_rescue_org_check_stats
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, \
@@ -37,6 +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.misc import RequestProfiler
from .tools.model_helpers import AdoptionNoticeStatusChoices, AdoptionNoticeProcessTemplates, RegularCheckStatusChoices from .tools.model_helpers import AdoptionNoticeStatusChoices, AdoptionNoticeProcessTemplates, RegularCheckStatusChoices
from .tools.search import AdoptionNoticeSearch, RescueOrgSearch from .tools.search import AdoptionNoticeSearch, RescueOrgSearch
@@ -114,6 +114,13 @@ def handle_an_check_actions(request, action, adoption_notice=None):
def adoption_notice_detail(request, adoption_notice_id): def adoption_notice_detail(request, adoption_notice_id):
adoption_notice = get_object_or_404(AdoptionNotice, id=adoption_notice_id) adoption_notice = get_object_or_404(AdoptionNotice, id=adoption_notice_id)
if adoption_notice.is_disabled and not user_is_owner_or_trust_level(request.user, adoption_notice):
error_message = _("Die Vermittlung wurde versteckt und ist nur Admins zugänglich. Grund dafür kann z.b. ein "
"Regelverstoß sein.")
return render(request,
"fellchensammlung/errors/403.html",
context={"error_message": error_message},
status=403)
adoption_notice_meta = adoption_notice._meta adoption_notice_meta = adoption_notice._meta
if request.user.is_authenticated: if request.user.is_authenticated:
try: try:
@@ -242,11 +249,17 @@ def search_important_locations(request, important_location_slug):
def search(request, templatename="fellchensammlung/search.html"): def search(request, templatename="fellchensammlung/search.html"):
search_profile = RequestProfiler()
search_profile.add_status("Start")
# 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_profile.add_status("Init Search")
search = AdoptionNoticeSearch() search = AdoptionNoticeSearch()
search_profile.add_status("Search from request starting")
search.adoption_notice_search_from_request(request) search.adoption_notice_search_from_request(request)
search_profile.add_status("Search from request finished")
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:
@@ -266,10 +279,12 @@ def search(request, templatename="fellchensammlung/search.html"):
subscribed_search = search.get_subscription_or_none(request.user) subscribed_search = search.get_subscription_or_none(request.user)
else: else:
subscribed_search = None subscribed_search = None
search_profile.add_status("End of POST")
site_title = _("Suche") site_title = _("Suche")
site_description = _("Ratten in Tierheimen und Rattenhilfen in der Nähe suchen.") site_description = _("Ratten in Tierheimen und Rattenhilfen in der Nähe suchen.")
canonical_url = reverse("search") canonical_url = reverse("search")
search_profile.add_status("Start of context")
context = {"adoption_notices": search.get_adoption_notices(), context = {"adoption_notices": search.get_adoption_notices(),
"search_form": search.search_form, "search_form": search.search_form,
"place_not_found": search.place_not_found, "place_not_found": search.place_not_found,
@@ -286,6 +301,10 @@ def search(request, templatename="fellchensammlung/search.html"):
"site_title": site_title, "site_title": site_title,
"site_description": site_description, "site_description": site_description,
"canonical_url": canonical_url} "canonical_url": canonical_url}
search_profile.add_status("End of context")
if request.user.is_superuser:
context["profile"] = search_profile.as_relative_with_ms
search_profile.add_status("Finished - returing render")
return render(request, templatename, context=context) return render(request, templatename, context=context)
@@ -578,12 +597,23 @@ def report_detail_success(request, report_id):
def user_detail(request, user, token=None): def user_detail(request, user, token=None):
user_detail_profile = RequestProfiler()
user_detail_profile.add_status("Start")
adoption_notices = AdoptionNotice.objects.filter(owner=user)
user_detail_profile.add_status("Finished fetching adoption notices")
context = {"user": user, context = {"user": user,
"adoption_notices": AdoptionNotice.objects.filter(owner=user), "adoption_notices": adoption_notices,
"notifications": Notification.objects.filter(user_to_notify=user, read=False), "notifications": Notification.objects.filter(user_to_notify=user, read=False),
"search_subscriptions": SearchSubscription.objects.filter(owner=user), } "search_subscriptions": SearchSubscription.objects.filter(owner=user), }
user_detail_profile.add_status("End of context")
if token is not None: if token is not None:
context["token"] = token context["token"] = token
user_detail_profile.add_status("Finished - returning to renderer")
if request.user.is_superuser:
context["profile"] = user_detail_profile.as_relative_with_ms
if request.user.trust_level > TrustLevel.MODERATOR:
context["show_mod_actions"] = True
return render(request, 'fellchensammlung/details/detail-user.html', context=context) return render(request, 'fellchensammlung/details/detail-user.html', context=context)
@@ -654,10 +684,40 @@ def my_notifications(request):
context = {"notifications_unread": Notification.objects.filter(user_to_notify=request.user, read=False).order_by( context = {"notifications_unread": Notification.objects.filter(user_to_notify=request.user, read=False).order_by(
"-created_at"), "-created_at"),
"notifications_read_last": Notification.objects.filter(user_to_notify=request.user, "notifications_read_last": Notification.objects.filter(user_to_notify=request.user,
read=True).order_by("-read_at")} read=True).order_by("-read_at")[:10]}
return render(request, 'fellchensammlung/notifications.html', context=context) return render(request, 'fellchensammlung/notifications.html', context=context)
def user_activate(request, user_id):
return user_de_activation(request, user_id, True)
def user_deactivate(request, user_id):
return user_de_activation(request, user_id, False)
def user_de_activation(request, user_id, is_to_be_active):
"""
Activates or deactivates a user
"""
user = User.objects.get(id=user_id)
if request.method == 'POST':
form = UserModCommentForm(request.POST, instance=user)
if form.is_valid():
user_instance = form.save(commit=False)
user_instance.is_active = is_to_be_active
user_instance.save()
return redirect(reverse("user-detail", args=[user_instance.pk], ))
else:
form = UserModCommentForm(instance=user)
if is_to_be_active:
template = 'fellchensammlung/forms/form-activate-user.html'
else:
template = 'fellchensammlung/forms/form-deactivate-user.html'
return render(request, template, {'form': form})
@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)
@@ -791,6 +851,7 @@ def list_rescue_organizations(request, species=None, template='fellchensammlung/
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),
"org_name_search_form": RescueOrgSearchByNameForm(),
} }
if org_search: if org_search:
additional_context = { additional_context = {
@@ -846,8 +907,10 @@ def rescue_organization_check(request, context=None):
action = request.POST.get("action") action = request.POST.get("action")
if action == "checked": if action == "checked":
rescue_org.set_checked() rescue_org.set_checked()
elif action == "exclude": Log.objects.create(user=request.user, action="rescue_organization_checked", )
rescue_org.set_exclusion_from_checks() elif action == "toggle_active_communication":
rescue_org.ongoing_communication = not rescue_org.ongoing_communication
rescue_org.save()
elif action == "set_species_url": elif action == "set_species_url":
species_url_form = SpeciesURLForm(request.POST) species_url_form = SpeciesURLForm(request.POST)
@@ -858,10 +921,13 @@ def rescue_organization_check(request, context=None):
elif action == "update_internal_comment": elif action == "update_internal_comment":
comment_form = RescueOrgInternalComment(request.POST, instance=rescue_org) comment_form = RescueOrgInternalComment(request.POST, instance=rescue_org)
if comment_form.is_valid(): if comment_form.is_valid():
Log.objects.create(user=request.user, action="rescue_organization_added_internal_comment", )
comment_form.save() comment_form.save()
rescue_orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=False, rescue_orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=False,
ongoing_communication=False).order_by("last_checked")[:3] ongoing_communication=False).exclude(
allows_using_materials=AllowUseOfMaterialsChices.USE_MATERIALS_DENIED).order_by(
"last_checked")[:3]
rescue_orgs_with_ongoing_communication = RescueOrganization.objects.filter(ongoing_communication=True).order_by( rescue_orgs_with_ongoing_communication = RescueOrganization.objects.filter(ongoing_communication=True).order_by(
"updated_at") "updated_at")
rescue_orgs_last_checked = RescueOrganization.objects.filter().order_by("-last_checked")[:10] rescue_orgs_last_checked = RescueOrganization.objects.filter().order_by("-last_checked")[:10]
@@ -897,67 +963,116 @@ def rescue_organization_check_dq(request):
return rescue_organization_check(request, context) return rescue_organization_check(request, context)
@user_passes_test(user_is_trust_level_or_above) def exclude_from_regular_check(request, rescue_organization_id, source="organization-check"):
def exclude_from_regular_check(request, rescue_organization_id):
rescue_org = get_object_or_404(RescueOrganization, pk=rescue_organization_id) rescue_org = get_object_or_404(RescueOrganization, pk=rescue_organization_id)
if request.method == "POST": if request.method == "POST":
form = UpdateRescueOrgRegularCheckStatus(request.POST, instance=rescue_org) form = UpdateRescueOrgRegularCheckStatus(request.POST, instance=rescue_org)
if form.is_valid(): if form.is_valid():
form.save() form.save()
if form.cleaned_data["regular_check_status"] != RegularCheckStatusChoices.REGULAR_CHECK: to_be_excluded = form.cleaned_data["regular_check_status"] != RegularCheckStatusChoices.REGULAR_CHECK
rescue_org.exclude_from_check = True rescue_org.exclude_from_check = to_be_excluded
rescue_org.save() rescue_org.save()
return redirect(reverse("organization-check")) if to_be_excluded:
Log.objects.create(user=request.user,
action="rescue_organization_excluded_from_check",
text=f"New status: {form.cleaned_data['regular_check_status']}")
return redirect(reverse(source))
else: else:
form = UpdateRescueOrgRegularCheckStatus(instance=rescue_org) form = UpdateRescueOrgRegularCheckStatus(instance=rescue_org)
context = {"form": form, rescue_org: rescue_org} org_meta = rescue_org._meta
context = {"form": form, "org": rescue_org, "org_meta": org_meta}
return render(request, 'fellchensammlung/forms/form-exclude-org-from-check.html', context=context) return render(request, 'fellchensammlung/forms/form-exclude-org-from-check.html', context=context)
@user_passes_test(user_is_trust_level_or_above)
def update_exclusion_reason(request):
"""
This view will redirect to update a rescue org that not yet has an exclusion reason but is excluded
"""
orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=True,
regular_check_status=RegularCheckStatusChoices.REGULAR_CHECK)
if orgs_to_check.count() > 0:
return exclude_from_regular_check(request, orgs_to_check[0].pk,
source="rescue-organization-add-exclusion-reason")
else:
return render(request, "fellchensammlung/errors/404.html", status=404)
@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):
context = None context = None
if request.method == "POST": if request.method == "POST":
action = request.POST.get("action") action = request.POST.get("action")
if action == "post_to_fedi": if action == "post_to_fedi":
adoption_notice = SocialMediaPost.get_an_to_post() context = handle_post_fedi_action()
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) return render(request, 'fellchensammlung/mod-tool-overview.html', context=context)
def deactivate_an(request, adoption_notice_id): def close_adoption_notice(request, adoption_notice_id):
adoption_notice = get_object_or_404(AdoptionNotice, pk=adoption_notice_id) adoption_notice = get_object_or_404(AdoptionNotice, pk=adoption_notice_id)
if request.method == "POST": if request.method == "POST":
reason_for_closing = request.POST.get("reason_for_closing") form = CloseAdoptionNoticeForm(request.POST, instance=adoption_notice)
if reason_for_closing not in AdoptionNoticeStatusChoices.Closed.values: if form.is_valid():
return render(request, "fellchensammlung/errors/403.html", status=403) form.save()
adoption_notice.adoption_notice_status = reason_for_closing
adoption_notice.save()
return redirect(reverse("adoption-notice-detail", args=[adoption_notice.pk], )) return redirect(reverse("adoption-notice-detail", args=[adoption_notice.pk], ))
context = {"adoption_notice": adoption_notice, } else:
form = CloseAdoptionNoticeForm(instance=adoption_notice)
context = {"adoption_notice": adoption_notice, "form": form}
return render(request, 'fellchensammlung/misc/deactivate-an.html', context=context) return render(request, 'fellchensammlung/misc/deactivate-an.html', context=context)
def adoption_notice_sharepic(request, adoption_notice_id): def adoption_notice_sharepic(request, adoption_notice_id):
adoption_notice = get_object_or_404(AdoptionNotice, pk=adoption_notice_id) adoption_notice = get_object_or_404(AdoptionNotice, pk=adoption_notice_id)
svg_data = img.export_svg(adoption_notice) svg_data = img.export_svg(adoption_notice)
return HttpResponse(svg_data, content_type="image/svg+xml") return HttpResponse(svg_data, content_type="image/svg+xml",
headers={"Content-Disposition": f'attachment; filename="{adoption_notice.name}-post.svg"'},)
def adoption_notice_story_pic(request, adoption_notice_id):
adoption_notice = get_object_or_404(AdoptionNotice, pk=adoption_notice_id)
svg_data = img.export_svg(adoption_notice, "fellchensammlung/images/adoption-notice-story.svg")
return HttpResponse(svg_data, content_type="image/svg+xml",
headers={"Content-Disposition": f'attachment; filename="{adoption_notice.name}-story.svg"'})
def adoption_notice_social_media_templates(request, adoption_notice_id):
context = {}
if request.method == "POST":
action = request.POST.get("action")
if action == "post_to_fedi":
context = handle_post_fedi_action()
adoption_notice = get_object_or_404(AdoptionNotice, pk=adoption_notice_id)
context["adoption_notice"] = adoption_notice
return render(request, 'fellchensammlung/misc/social-media-template-selection.html', context=context)
@login_required
def rescue_org_create_or_update(request, rescue_organization_id=None):
"""
Create or update a rescue organization
"""
# Only users that are mods to create or edit it
if not user_is_trust_level_or_above(request.user, TrustLevel.MODERATOR):
return HttpResponseForbidden()
if rescue_organization_id:
rescue_org = get_object_or_404(RescueOrganization, pk=rescue_organization_id)
else:
rescue_org = None
if request.method == 'POST':
form = RescueOrgForm(request.POST, instance=rescue_org)
if form.is_valid():
rescue_org = form.save()
"""Log"""
Log.objects.create(user=request.user, action="add_rescue_org",
text=f"{request.user} hat Tierschutzorganisation {rescue_org.pk} geändert")
return redirect(reverse("rescue-organization-detail", args=[rescue_org.pk], ))
else:
form = RescueOrgForm(instance=rescue_org)
return render(request, 'fellchensammlung/forms/form-rescue-organization.html',
context={"form": form, "rescue_org": rescue_org})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ https://docs.djangoproject.com/en/3.2/topics/settings/
For the full list of settings and their values, see For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/ https://docs.djangoproject.com/en/3.2/ref/settings/
""" """
import json
from pathlib import Path from pathlib import Path
import os import os
import configparser import configparser
@@ -74,6 +74,12 @@ except configparser.NoSectionError:
raise BaseException("No config found or no Django Secret is configured!") raise BaseException("No config found or no Django Secret is configured!")
DEBUG = config.getboolean('django', 'debug', fallback=False) DEBUG = config.getboolean('django', 'debug', fallback=False)
# Internal IPs
internal_ip_raw_config_value = config.get("django", "internal_ips", fallback=None)
if internal_ip_raw_config_value:
INTERNAL_IPS = json.loads(internal_ip_raw_config_value)
""" DATABASE """ """ DATABASE """
DB_BACKEND = config.get("database", "backend", fallback="sqlite3") DB_BACKEND = config.get("database", "backend", fallback="sqlite3")
DB_NAME = config.get("database", "name", fallback="notfellchen.sqlite3") DB_NAME = config.get("database", "name", fallback="notfellchen.sqlite3")
@@ -130,6 +136,37 @@ ACCOUNT_ACTIVATION_DAYS = 7 # One-week activation window
REGISTRATION_OPEN = True REGISTRATION_OPEN = True
REGISTRATION_SALT = "notfellchen" REGISTRATION_SALT = "notfellchen"
AUTHENTICATION_BACKENDS = [
# Needed to login by username in Django admin, regardless of `allauth`
'django.contrib.auth.backends.ModelBackend',
# `allauth` specific authentication methods, such as login by email
'allauth.account.auth_backends.AuthenticationBackend',
]
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_EMAIL_VERIFICATION_BY_CODE_ENABLED = True
ACCOUNT_SIGNUP_FIELDS = ['username*', "email*", "password1*"]
ACCOUNT_SIGNUP_FORM_CLASS = 'fellchensammlung.forms.AddedRegistrationForm'
MFA_SUPPORTED_TYPES = ["totp",
"webauthn",
"recovery_codes"]
MFA_TOTP_TOLERANCE = 1
MFA_TOTP_ISSUER = config.get('security', 'totp_issuer', fallback="Notfellchen")
MFA_PASSKEY_LOGIN_ENABLED = True
MFA_PASSKEY_SIGNUP_ENABLED = True
# Optional -- use for local development only: the WebAuthn uses the
#``fido2`` package, and versions up to including version 1.1.3 do not
# regard localhost as a secure origin, which is problematic during
# local development and testing.
MFA_WEBAUTHN_ALLOW_INSECURE_ORIGIN = config.get('security', 'webauth_allow_insecure_origin', fallback=False)
""" SECURITY.TXT """ """ SECURITY.TXT """
SEC_CONTACT = config.get("security", "Contact", fallback="julian-samuel@gebuehr.net") SEC_CONTACT = config.get("security", "Contact", fallback="julian-samuel@gebuehr.net")
SEC_EXPIRES = config.get("security", "Expires", fallback="2028-03-17T07:00:00.000Z") SEC_EXPIRES = config.get("security", "Expires", fallback="2028-03-17T07:00:00.000Z")
@@ -182,7 +219,11 @@ INSTALLED_APPS = [
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
"django.contrib.humanize",
'django.contrib.messages', 'django.contrib.messages',
'allauth',
'allauth.account',
'allauth.mfa',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
"django.contrib.sitemaps", "django.contrib.sitemaps",
'fontawesomefree', 'fontawesomefree',
@@ -193,11 +234,15 @@ INSTALLED_APPS = [
'rest_framework.authtoken', 'rest_framework.authtoken',
'drf_spectacular', 'drf_spectacular',
'drf_spectacular_sidecar', # required for Django collectstatic discovery 'drf_spectacular_sidecar', # required for Django collectstatic discovery
'widget_tweaks' 'widget_tweaks',
"debug_toolbar",
'admin_extra_buttons',
'simple_history',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
"debug_toolbar.middleware.DebugToolbarMiddleware",
# Static file serving & caching # Static file serving & caching
'whitenoise.middleware.WhiteNoiseMiddleware', 'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
@@ -208,6 +253,9 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
# allauth middleware, needs to be after message middleware
"allauth.account.middleware.AccountMiddleware",
'simple_history.middleware.HistoryRequestMiddleware',
] ]
ROOT_URLCONF = 'notfellchen.urls' ROOT_URLCONF = 'notfellchen.urls'
@@ -222,6 +270,7 @@ TEMPLATES = [
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
'django.template.context_processors.debug', 'django.template.context_processors.debug',
# Needed for allauth
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.template.context_processors.media', 'django.template.context_processors.media',

View File

@@ -18,12 +18,14 @@ from django.conf.urls.i18n import i18n_patterns
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
] path('accounts/', include('allauth.urls')),
] + debug_toolbar_urls()
urlpatterns += i18n_patterns( urlpatterns += i18n_patterns(
path("", include("fellchensammlung.urls")), path("", include("fellchensammlung.urls")),

View File

@@ -47,7 +47,7 @@
<div class="block"> <div class="block">
<a class="button is-warning" href="{% url 'password_reset' %}">{% translate "Passwort vergessen?" %}</a> <a class="button is-warning" href="{% url 'password_reset' %}">{% translate "Passwort vergessen?" %}</a>
<a class="button is-link" <a class="button is-link"
href="{% url 'django_registration_register' %}">{% translate "Registrieren" %}</a> href="{% url 'account_signup' %}">{% translate "Registrieren" %}</a>
</div> </div>
</div> </div>
{% endif %} {% endif %}