Compare commits

...

41 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
50 changed files with 4760 additions and 1054 deletions

View File

@@ -9,15 +9,14 @@ RUN apt install gettext -y
RUN apt install libpq-dev gcc -y
COPY . /app
WORKDIR /app
RUN mkdir /app/data
RUN mkdir /app/data/static
RUN mkdir /app/data/static -p
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 compilemessages --ignore venv
COPY docker/notfellchen.bash /bin/notfellchen
COPY docker/entrypoint.sh /bin/notfellchen
EXPOSE 7345
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

@@ -9,6 +9,7 @@ host=localhost
secret=CHANGE-ME
debug=True
internal_ips=["127.0.0.1"]
cache=False
[database]
backend=sqlite3

View File

@@ -40,6 +40,9 @@ dependencies = [
"django-widget-tweaks",
"django-super-deduper",
"django-allauth[mfa]",
"django_debug_toolbar",
"django-admin-extra-buttons",
"django-simple-history"
]
dynamic = ["version", "readme"]
@@ -49,13 +52,16 @@ develop = [
"pytest",
"coverage",
"model_bakery",
"debug_toolbar",
]
docs = [
"sphinx",
"sphinx-rtd-theme",
"sphinx-autobuild"
]
shelter-upload = [
"osm2geojson",
"tqdm"
]
[project.urls]
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}")
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
def get_overpass_result(area, data_file):
"""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"),
external_object_identifier=tierheim["id"],
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
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)
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
# Rescue organization does not exist
else:
@@ -268,7 +288,8 @@ def main():
if result.status_code != 201:
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__":

View File

@@ -7,6 +7,9 @@ from django.utils.html import format_html
from django.urls import reverse
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, \
SpeciesSpecificURL, ImportantLocation, SocialMediaPost
@@ -17,12 +20,34 @@ from django.utils.translation import gettext_lazy as _
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)
class AdoptionNoticeAdmin(admin.ModelAdmin):
search_fields = ("name__icontains", "description__icontains")
list_filter = ("owner",)
search_fields = ("name__icontains", "description__icontains", "location__icontains")
list_display = ["name", "adoption_notice_status", "owner", "organization", "last_checked_hr"]
list_filter = ("adoption_notice_status", "owner")
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):
for obj in queryset:
obj.adoption_notice_status = AdoptionNoticeStatusChoices.Active.SEARCHING
@@ -33,7 +58,7 @@ class AdoptionNoticeAdmin(admin.ModelAdmin):
# Re-register UserAdmin
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
class UserAdmin(SimpleHistoryAdmin):
search_fields = ("usernamname__icontains", "first_name__icontains", "last_name__icontains", "email__icontains")
list_display = ("username", "email", "trust_level", "is_active", "view_adoption_notices")
list_filter = ("is_active", "trust_level",)
@@ -49,17 +74,7 @@ class UserAdmin(admin.ModelAdmin):
return format_html('<a href="{}">{} Adoption Notices</a>', url, count)
def export_as_csv(self, request, queryset):
meta = self.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])
response = export_to_csv_generic(self.model, queryset)
return response
export_as_csv.short_description = _("Ausgewählte User exportieren")
@@ -71,7 +86,7 @@ def _reported_content_link(obj):
@admin.register(ReportComment)
class ReportCommentAdmin(admin.ModelAdmin):
class ReportCommentAdmin(SimpleHistoryAdmin):
list_display = ["user_comment", "reported_content_link"]
date_hierarchy = "created_at"
@@ -82,7 +97,7 @@ class ReportCommentAdmin(admin.ModelAdmin):
@admin.register(ReportAdoptionNotice)
class ReportAdoptionNoticeAdmin(admin.ModelAdmin):
class ReportAdoptionNoticeAdmin(SimpleHistoryAdmin):
list_display = ["user_comment", "reported_content_link"]
date_hierarchy = "created_at"
@@ -97,7 +112,7 @@ class SpeciesSpecificURLInline(admin.StackedInline):
@admin.register(RescueOrganization)
class RescueOrganizationAdmin(admin.ModelAdmin):
class RescueOrganizationAdmin(SimpleHistoryAdmin):
search_fields = ("name", "description", "internal_comment", "location_string", "location__city")
list_display = ("name", "trusted", "allows_using_materials", "website")
list_filter = ("allows_using_materials", "trusted", ("external_source_identifier", EmptyFieldListFilter))
@@ -108,12 +123,12 @@ class RescueOrganizationAdmin(admin.ModelAdmin):
@admin.register(Text)
class TextAdmin(admin.ModelAdmin):
class TextAdmin(SimpleHistoryAdmin):
search_fields = ("title__icontains", "text_code__icontains",)
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
class CommentAdmin(SimpleHistoryAdmin):
list_filter = ("user",)
@@ -123,7 +138,7 @@ class BaseNotificationAdmin(admin.ModelAdmin):
@admin.register(SearchSubscription)
class SearchSubscriptionAdmin(admin.ModelAdmin):
class SearchSubscriptionAdmin(SimpleHistoryAdmin):
list_filter = ("owner",)
@@ -151,8 +166,17 @@ class IsImportantListFilter(admin.SimpleListFilter):
@admin.register(Location)
class LocationAdmin(admin.ModelAdmin):
class LocationAdmin(SimpleHistoryAdmin):
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]
inlines = [
ImportantLocationInline,
@@ -160,23 +184,39 @@ class LocationAdmin(admin.ModelAdmin):
@admin.register(SocialMediaPost)
class SocialMediaPostAdmin(admin.ModelAdmin):
class SocialMediaPostAdmin(SimpleHistoryAdmin):
list_filter = ("platform",)
@admin.register(Log)
class LogAdmin(admin.ModelAdmin):
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)
admin.site.register(Animal, SimpleHistoryAdmin)
admin.site.register(Species)
admin.site.register(Rule)
admin.site.register(Rule, SimpleHistoryAdmin)
admin.site.register(Image)
admin.site.register(ModerationAction)
admin.site.register(ModerationAction, SimpleHistoryAdmin)
admin.site.register(Language)
admin.site.register(Announcement)
admin.site.register(Subscriptions)
admin.site.register(Announcement, SimpleHistoryAdmin)
admin.site.register(Subscriptions, SimpleHistoryAdmin)
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
import math
@@ -144,10 +144,26 @@ class AnimalGetSerializer(serializers.ModelSerializer):
fields = "__all__"
class SpeciesSpecificURLSerializer(serializers.ModelSerializer):
class Meta:
model = SpeciesSpecificURL
fields = "__all__"
class RescueOrganizationSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField()
species_specific_urls = SpeciesSpecificURLSerializer(many=True, read_only=True)
class Meta:
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):

View File

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

View File

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

View File

@@ -5,18 +5,17 @@ from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportC
Comment, SexChoicesWithAll, DistanceChoices, SpeciesSpecificURL, RescueOrganization
from django_registration.forms import RegistrationForm
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 notfellchen.settings import MEDIA_URL
from crispy_forms.layout import Div
from .tools.model_helpers import reason_for_signup_label, reason_for_signup_help_text
from .tools.model_helpers import reason_for_signup_label, reason_for_signup_help_text, AdoptionNoticeStatusChoices
def animal_validator(value: str):
value = value.lower()
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:
raise forms.ValidationError(_("Dieses Tier kenne ich nicht. Probier ein anderes"))
@@ -116,6 +115,12 @@ class ReportCommentForm(forms.ModelForm):
fields = ('reported_broken_rules', 'user_comment')
class UserModCommentForm(forms.ModelForm):
class Meta:
model = User
fields = ('mod_notes',)
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
@@ -178,3 +183,31 @@ class RescueOrgSearchForm(forms.Form):
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.TWENTY,
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,6 +7,7 @@ from django.contrib.auth.models import Group
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
import base64
from simple_history.models import HistoricalRecords
from .tools import misc, geo
from notfellchen.settings import MEDIA_URL, base_url
@@ -121,11 +122,11 @@ class ExternalSourceChoices(models.TextChoices):
class AllowUseOfMaterialsChices(models.TextChoices):
USE_MATERIALS_ALLOWED = "allowed", _("Usage allowed")
USE_MATERIALS_REQUESTED = "requested", _("Usage requested")
USE_MATERIALS_DENIED = "denied", _("Usage denied")
USE_MATERIALS_OTHER = "other", _("It's complicated")
USE_MATERIALS_NOT_ASKED = "not_asked", _("Not asked")
USE_MATERIALS_ALLOWED = "allowed", _("Nutzung erlaubt")
USE_MATERIALS_REQUESTED = "requested", _("Nutzung angefragt")
USE_MATERIALS_DENIED = "denied", _("Nutzung verweigert")
USE_MATERIALS_OTHER = "other", _("Es ist kompliziert")
USE_MATERIALS_NOT_ASKED = "not_asked", _("Nutzung noch nicht angefragt")
class Species(models.Model):
@@ -166,10 +167,14 @@ class RescueOrganization(models.Model):
internal_comment = models.TextField(verbose_name=_("Interner Kommentar"), null=True, blank=True, )
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung')) # Markdown allowed
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,
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'),
help_text=_("Organisation von der manuellen Überprüfung ausschließen, "
"z.B. weil Tiere nicht online geführt werden"))
@@ -182,11 +187,13 @@ class RescueOrganization(models.Model):
ongoing_communication = models.BooleanField(default=False, verbose_name=_('In aktiver Kommunikation'),
help_text=_(
"Es findet gerade Kommunikation zwischen Notfellchen und der Organisation statt."))
parent_org = models.ForeignKey("RescueOrganization", on_delete=models.PROTECT, blank=True, null=True)
parent_org = models.ForeignKey("RescueOrganization", on_delete=models.PROTECT, blank=True, null=True,
verbose_name=_("Übergeordnete Organisation"))
# allows to specify if a rescue organization has a specialization for dedicated species
specializations = models.ManyToManyField(Species, blank=True)
twenty_id = models.UUIDField(verbose_name=_("Twenty-ID"), null=True, blank=True,
help_text=_("ID der der Organisation in Twenty"))
history = HistoricalRecords()
class Meta:
unique_together = ('external_object_identifier', 'external_source_identifier',)
@@ -250,6 +257,7 @@ class RescueOrganization(models.Model):
def set_checked(self):
self.last_checked = timezone.now()
self._change_reason = 'Organization checked'
self.save()
@property
@@ -309,7 +317,9 @@ class User(AbstractUser):
organization_affiliation = models.ForeignKey(RescueOrganization, on_delete=models.PROTECT, null=True, blank=True,
verbose_name=_('Organisation'))
reason_for_signup = models.TextField(verbose_name=reason_for_signup_label, help_text=reason_for_signup_help_text)
mod_notes = models.TextField(verbose_name=_("Moderationsnotizen"), null=True, blank=True)
email_notifications = models.BooleanField(verbose_name=_("Benachrichtigung per E-Mail"), default=True)
history = HistoricalRecords()
REQUIRED_FIELDS = ["reason_for_signup", "email"]
class Meta:
@@ -405,6 +415,7 @@ class AdoptionNotice(models.Model):
adoption_process = models.TextField(null=True, blank=True,
max_length=64, verbose_name=_('Adoptionsprozess'),
choices=AdoptionProcess)
history = HistoricalRecords()
@property
def animals(self):
@@ -419,7 +430,6 @@ class AdoptionNotice(models.Model):
@property
def num_per_sex(self):
print(f"{self.pk} x")
num_per_sex = dict()
for sex in SexChoices:
num_per_sex[sex] = len([animal for animal in self.animals if animal.sex == sex])
@@ -542,6 +552,13 @@ class AdoptionNotice(models.Model):
def _values_of(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
def is_active(self):
return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.Active.choices)
@@ -558,6 +575,19 @@ class AdoptionNotice(models.Model):
def is_awaiting_action(self):
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
def status_description(self):
return AdoptionNoticeStatusChoicesDescriptions.mapping[self.adoption_notice_status]
@@ -607,7 +637,7 @@ class Animal(models.Model):
verbose_name = _('Tier')
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'))
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
species = models.ForeignKey(Species, on_delete=models.PROTECT, verbose_name=_("Tierart"))
@@ -621,18 +651,25 @@ class Animal(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
history = HistoricalRecords()
def __str__(self):
return f"{self.name}"
@property
def age(self):
if self.date_of_birth:
return timezone.now().today().date() - self.date_of_birth
else:
return None
@property
def hr_age(self):
"""Returns a human-readable age based on the date of birth."""
if self.date_of_birth:
return misc.age_as_hr_string(self.age)
else:
return _("Unbekannt")
def get_photo(self):
"""
@@ -684,6 +721,7 @@ class SearchSubscription(models.Model):
max_distance = models.IntegerField(choices=DistanceChoices.choices, null=True)
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"))
history = HistoricalRecords()
def __str__(self):
if self.location and self.max_distance:
@@ -714,6 +752,7 @@ class Rule(models.Model):
"Identifikator haben"))
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"))
history = HistoricalRecords()
def __str__(self):
return self.title
@@ -739,6 +778,7 @@ class Report(models.Model):
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"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am"))
history = HistoricalRecords()
def __str__(self):
return f"[{self.status}]: {self.user_comment:.20}"
@@ -825,6 +865,7 @@ class ModerationAction(models.Model):
# Only visible to moderator
private_comment = models.TextField(blank=True)
report = models.ForeignKey(Report, on_delete=models.CASCADE)
history = HistoricalRecords()
def __str__(self):
return f"[{self.action}]: {self.public_comment}"
@@ -846,6 +887,7 @@ class Text(models.Model):
content = models.TextField(verbose_name="Inhalt")
language = models.ForeignKey(Language, verbose_name="Sprache", on_delete=models.PROTECT)
text_code = models.CharField(max_length=24, verbose_name="Text code", blank=True)
history = HistoricalRecords()
class Meta:
verbose_name = "Text"
@@ -889,6 +931,7 @@ class Announcement(Text):
INFO: "info",
}
type = models.CharField(choices=TYPES, max_length=100, default=INFO)
history = HistoricalRecords()
@property
def is_active(self):
@@ -935,6 +978,7 @@ class Comment(models.Model):
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
text = models.TextField(verbose_name="Inhalt")
reply_to = models.ForeignKey("self", verbose_name="Antwort auf", blank=True, null=True, on_delete=models.CASCADE)
history = HistoricalRecords()
def __str__(self):
return f"{self.user} at {self.created_at.strftime('%H:%M %d.%m.%y')}: {self.text:.10}"
@@ -1006,6 +1050,7 @@ class Subscriptions(models.Model):
help_text=_("Vermittlung die abonniert wurde"))
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
history = HistoricalRecords()
def __str__(self):
return f"{self.owner} - {self.adoption_notice}"
@@ -1052,8 +1097,10 @@ class SpeciesSpecificURL(models.Model):
verbose_name_plural = _("Tierartspezifische URLs")
species = models.ForeignKey(Species, on_delete=models.CASCADE, verbose_name=_("Tierart"))
rescue_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE,
verbose_name=_("Tierschutzorganisation"))
rescue_organization = models.ForeignKey(RescueOrganization,
on_delete=models.CASCADE,
verbose_name=_("Tierschutzorganisation"),
related_name="species_specific_urls")
url = models.URLField(verbose_name=_("Tierartspezifische URL"))
@@ -1067,6 +1114,7 @@ class SocialMediaPost(models.Model):
choices=PlatformChoices.choices)
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
url = models.URLField(verbose_name=_("URL"))
history = HistoricalRecords()
@staticmethod
def get_an_to_post():

View File

@@ -22,12 +22,24 @@ $confirm: hsl(133deg, 100%, calc(41% + 0%));
background-color: $grey-light !important;
border-radius: 5px;
}
.navbar-burger {
color: white;
}
.card-header {
background-color: $grey-dark;
}
a.card-footer-item.is-danger {
color: black;
div.card-footer-item.is-danger {
background-color: #5a212d;
}
div.card-footer-item.is-warning {
background-color: #523e13;
}
div.card-footer-item.is-confirm {
background-color: #00420f;
}
.tag {
color: $grey-dark;
background-color: $grey-light;
@@ -245,6 +257,34 @@ IMAGES
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
@@ -322,7 +362,6 @@ AN Cards
}
.notification-container {
display: inline-block;
position: relative;

View File

@@ -33,6 +33,11 @@
<i class="fas fa-search fa-fw"></i> {% trans 'Suchen' %}
</button>
</form>
<hr>
<form method="post" autocomplete="off">
{% csrf_token %}
{{ org_name_search_form }}
</form>
</div>
{% endif %}
</div>
@@ -69,4 +74,57 @@
{% endfor %}
</ul>
</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 %}

View File

@@ -114,7 +114,17 @@
<i class="fas fa-chart-line fa-fw"></i> {% trans 'Aufrufe' %}
</a>
{% endif %}
<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"
href="{% url adoption_notice_meta|admin_urlname:'change' adoption_notice.pk %}">
<i class="fa-solid fa-tools fa-fw"></i> Admin interface
@@ -164,6 +174,9 @@
{% if adoption_notice.get_photos %}
<div class="column block">
<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="gallery">
{% with photo=adoption_notice.get_photos.0 %}

View File

@@ -24,11 +24,20 @@
<div class="block">
{% 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 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>
{% endif %}
</div>
<div class="column">
{% include "fellchensammlung/partials/partial-map.html" %}

View File

@@ -27,6 +27,7 @@
<div class="block">
<h2 class="title is-2">{% trans 'Profil verwalten' %}</h2>
<div class="block"><strong>{% translate "E-Mail" %}:</strong> {{ user.email }}</div>
{% if user.id is request.user.id %}
<div class="block">
<div class="field is-grouped is-grouped-multiline">
<div class="control">
@@ -53,7 +54,6 @@
</div>
</div>
{% if user.id is request.user.id %}
<div class="block">
<h2 class="title is-2">{% trans 'Einstellungen' %}</h2>
<form class="block" action="" method="POST">
@@ -98,6 +98,27 @@
</details>
</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>
{% include "fellchensammlung/lists/list-notifications.html" %}
@@ -105,8 +126,7 @@
<h2 class="title is-2">{% translate 'Abonnierte Suchen' %}</h2>
{% 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" %}
{% endif %}
{% endblock %}

View File

@@ -7,9 +7,14 @@
{% block content %}
<h1 class="title is-1">403 Forbidden</h1>
<p>
{% if error_message %}
{{ error_message }}
{% else %}
{% 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!
{% endblocktranslate %}
{% endif %}
</p>
{% endblock %}

View File

@@ -102,6 +102,10 @@
<a class="nav-link " href="{% url "modtools" %}">
{% translate 'Moderationstools' %}
</a>
<br>
<a class="nav-link " href="{% url "rescue-organization-create" %}">
{% translate 'Tierschutzorganisation hinzufügen' %}
</a>
{% endif %}
<br/>
{% 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

@@ -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 %}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -6,21 +6,7 @@
<h1 class="title is-1">{% translate 'Vermittlung deaktivieren' %}</h1>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="field">
<label class="label" for="reason_for_closing">{% translate 'Warum schließt du die Vermittlung?' %}</label>
<div class="control">
<div class="select">
<select id="reason_for_closing" name="reason_for_closing">
<option value="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>
{{ form }}
<input class="button is-warning" type="submit" value="{% translate "Vermittlung deaktivieren" %}">
</form>
{% 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 class="block">
{% if action_was_posting %}
{% if posted_successfully %}
<div class="message is-success">
<div class="message-header">
{% translate 'Vermittlung gepostet' %}
</div>
<div class="message-body">
{% blocktranslate with post_url=post.url %}
Link zum Post: <a href={{ post_url }}>{{ post_url }}</a>
{% endblocktranslate %}
</div>
</div>
{% else %}
<div class="message is-danger">
<div class="message-header">
{% translate 'Vermittlung konnte nicht gepostet werden' %}
</div>
<div class="message-body">
{{ error_message }}
</div>
</div>
{% endif %}
{% endif %}
<form class="cell" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="post_to_fedi">
<button class="button is-fullwidth is-warning is-primary" type="submit" id="submit">
<i class="fa-solid fa-bullhorn fa-fw"></i> {% translate "Vermittlung ins Fediverse posten" %}
</button>
</form>
{% include "fellchensammlung/partials/social_media/post-to-fedi.html" %}
</div>
</div>

View File

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

View File

@@ -31,5 +31,14 @@
{{ adoption_notice.status_description }}
</div>
</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 %}

View File

@@ -1,7 +1,7 @@
{% load i18n %}
{% load custom_tags %}
<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">
{{ adoption_notice.name }}
</div>
@@ -17,25 +17,21 @@
{% endif %}
</div>
<div class="card-footer">
<div class="card-footer-item is-confirm">
<form method="post">
<form class="card-footer-item is-confirm" method="post">
{% csrf_token %}
<input type="hidden"
name="adoption_notice_id"
value="{{ adoption_notice.pk }}">
<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>
</div>
<div class="card-footer-item is-warning">
<form method="post">
<form class="card-footer-item is-warning" method="post">
{% csrf_token %}
<input type="hidden"
name="adoption_notice_id"
value="{{ adoption_notice.pk }}">
<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>
</div>
</div>
</div>

View File

@@ -71,7 +71,22 @@
value="{{ rescue_org.pk }}">
<input type="hidden" name="action" value="checked">
<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>
</div>
<div class="card-footer-item is-danger">

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

@@ -5,6 +5,7 @@ from django.template.defaultfilters import stringfilter
from django.utils.safestring import mark_safe
from urllib.parse import urlparse
from django.utils import timezone
from django.urls import reverse
from fellchensammlung.tools.misc import time_since_as_hr_string
from notfellchen import settings
@@ -54,6 +55,11 @@ def get_oxitraffic_script_if_enabled():
return ""
@register.simple_tag
def api_base_url():
return reverse("api-base-url")
@register.filter
@stringfilter
def pointdecimal(value):

View File

@@ -1,8 +1,9 @@
import logging
import requests
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
@@ -86,7 +87,10 @@ class FediClient:
def post_an_to_fedi(adoption_notice):
try:
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}
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,
url=response['url'], )
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
def export_svg(adoption_notice):
result = render_to_string(template_name="fellchensammlung/images/adoption-notice.svg",
def export_svg(adoption_notice, template_name: str = "fellchensammlung/images/adoption-notice.svg"):
result = render_to_string(template_name=template_name,
context={"adoption_notice": adoption_notice, })
return result

View File

@@ -2,14 +2,19 @@ from datetime import timedelta
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():
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()
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()
try:

View File

@@ -81,6 +81,9 @@ def is_404(url):
class RequestProfiler:
data = []
def clear(self):
self.data = []
def add_status(self, status):
self.data.append((time.time(), status))

View File

@@ -69,14 +69,14 @@ class AdoptionNoticeStatusChoices:
UNCHECKED = "awaiting_action_unchecked", _("Unchecked")
class Closed(TextChoices):
SUCCESSFUL_WITH_NOTFELLCHEN = "closed_successful_with_notfellchen", _("Successful (with Notfellchen)")
SUCCESSFUL_WITHOUT_NOTFELLCHEN = "closed_successful_without_notfellchen", _("Successful (without Notfellchen)")
ANIMAL_DIED = "closed_animal_died", _("Animal died")
FOR_OTHER_ADOPTION_NOTICE = "closed_for_other_adoption_notice", _("Closed for other adoption notice")
NOT_OPEN_ANYMORE = "closed_not_open_for_adoption_anymore", _("Not open for adoption anymore")
SUCCESSFUL = "closed_successfully", _("Erfolgreich vermittelt")
ANIMAL_DIED = "closed_animal_died", _("Tier gestorben")
FOR_OTHER_ADOPTION_NOTICE = ("closed_for_other_adoption_notice",
_("Vermittlung wurde zugunsten einer anderen geschlossen."))
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", _(
"Der Link zu weiteren Informationen ist nicht mehr erreichbar.")
OTHER = "closed_other", _("Other (closed)")
OTHER = "closed_other", _("Anderes")
class Disabled(TextChoices):
AGAINST_RULES = "disabled_against_the_rules", _("Against the rules")
@@ -97,8 +97,7 @@ class AdoptionNoticeStatusChoicesDescriptions:
_ansc = AdoptionNoticeStatusChoices # Mapping for readability
mapping = {_ansc.Active.SEARCHING.value: "",
_ansc.Active.INTERESTED: _("Jemand hat bereits Interesse an den Tieren."),
_ansc.Closed.SUCCESSFUL_WITH_NOTFELLCHEN: _("Vermittlung erfolgreich abgeschlossen."),
_ansc.Closed.SUCCESSFUL_WITHOUT_NOTFELLCHEN: _("Vermittlung erfolgreich abgeschlossen."),
_ansc.Closed.SUCCESSFUL: _("Vermittlung erfolgreich abgeschlossen."),
_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.NOT_OPEN_ANYMORE: _("Tier(e) stehen nicht mehr zur Vermittlung bereit."),
@@ -113,8 +112,8 @@ class AdoptionNoticeStatusChoicesDescriptions:
_ansc.AwaitingAction.UNCHECKED: _(
"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.OTHER: _("Vermittlung deaktiviert.")
_ansc.Disabled.AGAINST_RULES: _("Die Vermittlung wurde gesperrt, da sie gegen die Regeln verstößt."),
_ansc.Disabled.OTHER: _("Vermittlung gesperrt.")
}

View File

@@ -173,6 +173,7 @@ class AdoptionNoticeSearch:
class RescueOrgSearch:
def __init__(self, request):
self.name = None
self.area_search = None
self.max_distance = None
self.location = None # Can either be Location (DjangoModel) or LocationProxy
@@ -229,6 +230,7 @@ class RescueOrgSearch:
return fitting_rescue_orgs
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":
self.search_form = RescueOrgSearchForm(request.GET)
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"),
# ex: /adoption_notice/7/
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
path("vermittlung/<int:adoption_notice_id>/sharepic", views.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
path("vermittlung/<int:adoption_notice_id>/edit", views.adoption_notice_edit, name="adoption-notice-edit"),
# ex: /vermittlung/5/add-photo
@@ -41,12 +47,15 @@ urlpatterns = [
# ex: /adoption_notice/2/add-animal
path("vermittlung/<int:adoption_notice_id>/add-animal", views.adoption_notice_add_animal,
name="adoption-notice-add-animal"),
path("vermittlung/<int:adoption_notice_id>/close", views.deactivate_an,
path("vermittlung/<int:adoption_notice_id>/close", views.close_adoption_notice,
name="adoption-notice-close"),
path("tierschutzorganisationen/", views.list_rescue_organizations, name="rescue-organizations"),
path("tierschutzorganisationen/<int:rescue_organization_id>/", views.detail_view_rescue_organization,
name="rescue-organization-detail"),
path("tierschutzorganisationen/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,
name="rescue-organization-exclude"),
path("tierschutzorganisationen/add-exclusion-reason", views.update_exclusion_reason,
@@ -90,11 +99,12 @@ urlpatterns = [
###########
# ex: user/1
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/notifications/", views.my_notifications, name="user-notifications"),
path('user/me/export/', views.export_own_profile, name='user-me-export'),
path('change-language', views.change_language, name="change-language"),
###########
@@ -120,7 +130,7 @@ urlpatterns = [
###################
## External Site ##
###################
path('bulma/external-site/', views.external_site_warning, name="external-site"),
path('external-site/', views.external_site_warning, name="external-site"),
###############
## TECHNICAL ##

View File

@@ -13,7 +13,6 @@ from django.contrib.auth.decorators import user_passes_test
from django.core.serializers import serialize
from django.utils.translation import gettext_lazy as _
import json
import requests
from .mail import notify_mods_new_report
from notfellchen import settings
@@ -22,13 +21,14 @@ from fellchensammlung import logger
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
User, Location, Subscriptions, Notification, RescueOrganization, \
Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, \
ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices, SocialMediaPost
ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices, SocialMediaPost, AllowUseOfMaterialsChices
from .forms import AdoptionNoticeForm, ImageForm, ReportAdoptionNoticeForm, \
CommentForm, ReportCommentForm, AnimalForm, AdoptionNoticeFormAutoAnimal, SpeciesURLForm, RescueOrgInternalComment, \
UpdateRescueOrgRegularCheckStatus
UpdateRescueOrgRegularCheckStatus, UserModCommentForm, CloseAdoptionNoticeForm, RescueOrgSearchByNameForm, \
RescueOrgForm
from .models import Language, Announcement
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.metrics import gather_metrics_data, get_rescue_org_check_stats
from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
@@ -114,6 +114,13 @@ def handle_an_check_actions(request, action, adoption_notice=None):
def adoption_notice_detail(request, 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
if request.user.is_authenticated:
try:
@@ -604,6 +611,9 @@ def user_detail(request, user, token=None):
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)
@@ -678,6 +688,36 @@ def my_notifications(request):
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)
def modqueue(request):
open_reports = Report.objects.select_related("reportadoptionnotice", "reportcomment").filter(status=Report.WAITING)
@@ -811,6 +851,7 @@ def list_rescue_organizations(request, species=None, template='fellchensammlung/
context = {"rescue_organizations_to_list": rescue_organizations_to_list,
"show_rescue_orgs": True,
"elided_page_range": paginator.get_elided_page_range(page_number, on_each_side=2, on_ends=1),
"org_name_search_form": RescueOrgSearchByNameForm(),
}
if org_search:
additional_context = {
@@ -867,6 +908,9 @@ def rescue_organization_check(request, context=None):
if action == "checked":
rescue_org.set_checked()
Log.objects.create(user=request.user, action="rescue_organization_checked", )
elif action == "toggle_active_communication":
rescue_org.ongoing_communication = not rescue_org.ongoing_communication
rescue_org.save()
elif action == "set_species_url":
species_url_form = SpeciesURLForm(request.POST)
@@ -881,7 +925,9 @@ def rescue_organization_check(request, context=None):
comment_form.save()
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(
"updated_at")
rescue_orgs_last_checked = RescueOrganization.objects.filter().order_by("-last_checked")[:10]
@@ -929,7 +975,7 @@ def exclude_from_regular_check(request, rescue_organization_id, source="organiza
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"]}")
text=f"New status: {form.cleaned_data['regular_check_status']}")
return redirect(reverse(source))
else:
@@ -959,44 +1005,74 @@ def moderation_tools_overview(request):
if request.method == "POST":
action = request.POST.get("action")
if action == "post_to_fedi":
adoption_notice = SocialMediaPost.get_an_to_post()
if adoption_notice is not None:
logging.info(f"Posting adoption notice: {adoption_notice} ({adoption_notice.id})")
try:
post = post_an_to_fedi(adoption_notice)
context = {"action_was_posting": True, "post": post, "posted_successfully": True}
except requests.exceptions.ConnectionError as e:
logging.error(f"Could not post fediverse post: {e}")
context = {"action_was_posting": True,
"posted_successfully": False,
"error_message": _("Verbindungsfehler. Vermittlung wurde nicht gepostet")}
except requests.exceptions.HTTPError as e:
logging.error(f"Could not post fediverse post: {e}")
context = {"action_was_posting": True,
"posted_successfully": False,
"error_message": _("Fehler beim Posten. Vermittlung wurde nicht gepostet. Das kann "
"z.B. an falschen Zugangsdaten liegen. Kontaktieren einen Admin.")}
else:
context = {"action_was_posting": True,
"posted_successfully": False,
"error_message": _("Keine Vermittlung zum Posten gefunden.")}
context = handle_post_fedi_action()
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)
if request.method == "POST":
reason_for_closing = request.POST.get("reason_for_closing")
if reason_for_closing not in AdoptionNoticeStatusChoices.Closed.values:
return render(request, "fellchensammlung/errors/403.html", status=403)
adoption_notice.adoption_notice_status = reason_for_closing
adoption_notice.save()
form = CloseAdoptionNoticeForm(request.POST, instance=adoption_notice)
if form.is_valid():
form.save()
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)
def adoption_notice_sharepic(request, adoption_notice_id):
adoption_notice = get_object_or_404(AdoptionNotice, pk=adoption_notice_id)
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

@@ -75,8 +75,10 @@ except configparser.NoSectionError:
DEBUG = config.getboolean('django', 'debug', fallback=False)
# Internal IPs
raw_config_value = config.get("django", "internal_ips", fallback=[])
INTERNAL_IPS = json.loads(raw_config_value)
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 """
DB_BACKEND = config.get("database", "backend", fallback="sqlite3")
@@ -89,7 +91,6 @@ DB_HOST = config.get("database", "host", fallback='')
BASE_DIR = Path(__file__).resolve().parent.parent
LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale')]
""" CELERY + KEYDB """
CELERY_BROKER_URL = config.get("celery", "broker", fallback="redis://localhost:6379/0")
CELERY_RESULT_BACKEND = config.get("celery", "backend", fallback="redis://localhost:6379/0")
@@ -235,6 +236,8 @@ INSTALLED_APPS = [
'drf_spectacular_sidecar', # required for Django collectstatic discovery
'widget_tweaks',
"debug_toolbar",
'admin_extra_buttons',
'simple_history',
]
MIDDLEWARE = [
@@ -252,6 +255,7 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# allauth middleware, needs to be after message middleware
"allauth.account.middleware.AccountMiddleware",
'simple_history.middleware.HistoryRequestMiddleware',
]
ROOT_URLCONF = 'notfellchen.urls'