Compare commits

3 Commits

Author SHA1 Message Date
c9f46d7547 ci: fix?
All checks were successful
ci/woodpecker/push/docs Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
2025-01-19 07:12:09 +01:00
9f23f5768c ci: restructure
Some checks failed
ci/woodpecker/push/docs Pipeline failed
ci/woodpecker/push/test Pipeline failed
2025-01-19 07:07:53 +01:00
19210f90cd ci: try teests 2025-01-19 07:04:22 +01:00
80 changed files with 1010 additions and 5987 deletions

2
.gitignore vendored
View File

@@ -4,7 +4,7 @@
notfellchen notfellchen
# Media storage # Media storage
/static static
media media

View File

@@ -6,6 +6,9 @@ steps:
commands: commands:
- cd docs && make html - cd docs && make html
when:
event: [ tag, push ]
deploy: deploy:
image: appleboy/drone-scp image: appleboy/drone-scp
settings: settings:
@@ -19,6 +22,8 @@ steps:
source: docs/_build/html/ source: docs/_build/html/
key: key:
from_secret: ssh_key from_secret: ssh_key
when:
event: [ tag, push ]

14
.woodpecker/test.yml Normal file
View File

@@ -0,0 +1,14 @@
---
steps:
test:
image: python
commands:
- python -m pip install '.[develop]'
- coverage run --source='.' src/manage.py test src && coverage html
- coverage html
- cat htmlcov/index.html
when:
event: [tag, push]

View File

@@ -1,105 +0,0 @@
import argparse
import json
import os
import requests
from tqdm import tqdm
DEFAULT_OSM_DATA_FILE = "export.geojson"
def parse_args():
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(description="Upload animal shelter data 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("--data-file", type=str, help="Path to the GeoJSON file containing (only) animal shelters.")
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")
data_file = args.data_file or os.getenv("NOTFELLCHEN_DATA_FILE", DEFAULT_OSM_DATA_FILE)
if not api_token or not instance:
raise ValueError("API token and instance URL must be provided via environment variables or CLI arguments.")
return api_token, instance, data_file
def get_or_none(data, key):
if key in data["properties"].keys():
return data["properties"][key]
else:
return ""
def choose(keys, data, replace=False):
for key in keys:
if key in data.keys():
if replace:
return data[key].replace(" ", "").replace("-", "").replace("(", "").replace(")", "")
else:
return data[key]
return None
def add(value, platform):
if value != "":
if value.find(platform) == -1:
return f"https://www.{platform}.com/{value}"
else:
return value
else:
return None
def https(value):
if value is not None and value != "":
value = value.replace("http://", "")
if value.find("https") == -1:
return f"https://{value}"
else:
return value
else:
return None
def main():
api_token, instance, data_file = get_config()
# Set headers and endpoint
endpoint = f"{instance}/api/organizations/"
h = {'Authorization': f'Token {api_token}', "content-type": "application/json"}
with open(data_file, encoding="utf8") as f:
d = json.load(f)
for idx, tierheim in tqdm(enumerate(d["features"])):
if "name" not in tierheim["properties"].keys() or "addr:city" not in tierheim["properties"].keys():
continue
data = {"name": tierheim["properties"]["name"],
"location_string": f"{get_or_none(tierheim, "addr:street")} {get_or_none(tierheim, "addr:housenumber")}, {get_or_none(tierheim, "addr:postcode")} {tierheim["properties"]["addr:city"]}",
"phone_number": choose(("contact:phone", "phone"), tierheim["properties"], replace=True),
"fediverse_profile": get_or_none(tierheim, "contact:mastodon"),
"facebook": https(add(get_or_none(tierheim, "contact:facebook"), "facebook")),
"instagram": https(add(get_or_none(tierheim, "contact:instagram"), "instagram")),
"website": https(choose(("contact:website", "website"), tierheim["properties"])),
"email": choose(("contact:email", "email"), tierheim["properties"]),
"description": get_or_none(tierheim, "opening_hours"),
"external_object_identifier": f"{tierheim["id"]}",
"external_source_identifier": "OSM"
}
result = requests.post(endpoint, json=data, headers=h)
if result.status_code != 201:
print(f"{idx} {tierheim["properties"]["name"]}:{result.status_code} {result.json()}")
if __name__ == "__main__":
main()

View File

@@ -7,7 +7,7 @@ from django.urls import reverse
from django.utils.http import urlencode from django.utils.http import urlencode
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \ from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
SpeciesSpecificURL, ImportantLocation SpeciesSpecificURL
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \ from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, BaseNotification Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, BaseNotification
@@ -94,14 +94,12 @@ class ReportAdoptionNoticeAdmin(admin.ModelAdmin):
reported_content_link.short_description = "Reported Content" reported_content_link.short_description = "Reported Content"
class SpeciesSpecificURLInline(admin.StackedInline): class SpeciesSpecificURLInline(admin.StackedInline):
model = SpeciesSpecificURL model = SpeciesSpecificURL
@admin.register(RescueOrganization) @admin.register(RescueOrganization)
class RescueOrganizationAdmin(admin.ModelAdmin): class RescueOrganizationAdmin(admin.ModelAdmin):
search_fields = ("name", "description", "internal_comment", "location_string") search_fields = ("name","description", "internal_comment", "location_string")
list_display = ("name", "trusted", "allows_using_materials", "website") list_display = ("name", "trusted", "allows_using_materials", "website")
list_filter = ("allows_using_materials", "trusted",) list_filter = ("allows_using_materials", "trusted",)
@@ -124,46 +122,14 @@ class CommentAdmin(admin.ModelAdmin):
class BaseNotificationAdmin(admin.ModelAdmin): class BaseNotificationAdmin(admin.ModelAdmin):
list_filter = ("user", "read") list_filter = ("user", "read")
@admin.register(SearchSubscription) @admin.register(SearchSubscription)
class SearchSubscriptionAdmin(admin.ModelAdmin): class SearchSubscriptionAdmin(admin.ModelAdmin):
list_filter = ("owner",) list_filter = ("owner",)
class ImportantLocationInline(admin.StackedInline):
model = ImportantLocation
class IsImportantListFilter(admin.SimpleListFilter):
# See https://docs.djangoproject.com/en/5.1/ref/contrib/admin/filters/#modeladmin-list-filters
title = _('Is Important Location?')
parameter_name = 'important'
def lookups(self, request, model_admin):
return (
('is_important', _('Important Location')),
('is_normal', _('Normal Location')),
)
def queryset(self, request, queryset):
if self.value() == 'is_important':
return queryset.filter(importantlocation__isnull=False)
else:
return queryset.filter(importantlocation__isnull=True)
@admin.register(Location)
class LocationAdmin(admin.ModelAdmin):
search_fields = ("name__icontains", "city__icontains")
list_filter = [IsImportantListFilter]
inlines = [
ImportantLocationInline,
]
admin.site.register(Animal) admin.site.register(Animal)
admin.site.register(Species) admin.site.register(Species)
admin.site.register(Location)
admin.site.register(Rule) admin.site.register(Rule)
admin.site.register(Image) admin.site.register(Image)
admin.site.register(ModerationAction) admin.site.register(ModerationAction)

View File

@@ -1,35 +1,12 @@
from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image, Location from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image
from rest_framework import serializers from rest_framework import serializers
class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer): class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
location = serializers.PrimaryKeyRelatedField(
queryset=Location.objects.all(),
required=False,
allow_null=True
)
location_details = serializers.StringRelatedField(source='location', read_only=True)
organization = serializers.PrimaryKeyRelatedField(
queryset=RescueOrganization.objects.all(),
required=False,
allow_null=True
)
organization = serializers.PrimaryKeyRelatedField(
queryset=RescueOrganization.objects.all(),
required=False,
allow_null=True
)
photos = serializers.PrimaryKeyRelatedField(
queryset=Image.objects.all(),
many=True,
required=False
)
class Meta: class Meta:
model = AdoptionNotice model = AdoptionNotice
fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information", fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information",
"group_only", "location", "location_details", "organization", "photos"] "group_only"]
class AnimalCreateSerializer(serializers.ModelSerializer): class AnimalCreateSerializer(serializers.ModelSerializer):
@@ -37,14 +14,12 @@ class AnimalCreateSerializer(serializers.ModelSerializer):
model = Animal model = Animal
fields = ["name", "date_of_birth", "description", "species", "sex", "adoption_notice"] fields = ["name", "date_of_birth", "description", "species", "sex", "adoption_notice"]
class RescueOrgSerializer(serializers.ModelSerializer): class RescueOrgSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = RescueOrganization model = RescueOrganization
fields = ["name", "location_string", "instagram", "facebook", "fediverse_profile", "email", "phone_number", fields = ["name", "location_string", "instagram", "facebook", "fediverse_profile", "email", "phone_number",
"website", "description", "external_object_identifier", "external_source_identifier"] "website", "description", "external_object_identifier", "external_source_identifier"]
class AnimalGetSerializer(serializers.ModelSerializer): class AnimalGetSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Animal model = Animal
@@ -76,9 +51,3 @@ class SpeciesSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Species model = Species
fields = "__all__" fields = "__all__"
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Location
fields = "__all__"

View File

@@ -1,7 +1,7 @@
from django.urls import path from django.urls import path
from .views import ( from .views import (
AdoptionNoticeApiView, AdoptionNoticeApiView,
AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView, LocationApiView AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView
) )
urlpatterns = [ urlpatterns = [
@@ -13,5 +13,4 @@ urlpatterns = [
path("organizations/<int:id>/", RescueOrganizationApiView.as_view(), name="api-organization-detail"), path("organizations/<int:id>/", RescueOrganizationApiView.as_view(), name="api-organization-detail"),
path("images/", AddImageApiView.as_view(), name="api-add-image"), path("images/", AddImageApiView.as_view(), name="api-add-image"),
path("species/", SpeciesApiView.as_view(), name="api-species-list"), path("species/", SpeciesApiView.as_view(), name="api-species-list"),
path("locations/", LocationApiView.as_view(), name="api-locations-list"),
] ]

View File

@@ -1,11 +1,8 @@
from django.db.models import Q
from fellchensammlung.api.serializers import LocationSerializer
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from django.db import transaction from django.db import transaction
from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel, Location from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel
from fellchensammlung.tasks import post_adoption_notice_save, post_rescue_org_save from fellchensammlung.tasks import post_adoption_notice_save
from rest_framework import status from rest_framework import status
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from .serializers import ( from .serializers import (
@@ -19,7 +16,6 @@ from .serializers import (
from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image
from drf_spectacular.utils import extend_schema from drf_spectacular.utils import extend_schema
class AdoptionNoticeApiView(APIView): class AdoptionNoticeApiView(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@@ -88,6 +84,7 @@ class AdoptionNoticeApiView(APIView):
) )
class AnimalApiView(APIView): class AnimalApiView(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@@ -121,7 +118,6 @@ class AnimalApiView(APIView):
) )
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class RescueOrganizationApiView(APIView): class RescueOrganizationApiView(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@@ -133,44 +129,14 @@ class RescueOrganizationApiView(APIView):
'description': 'ID of the rescue organization to retrieve.', 'description': 'ID of the rescue organization to retrieve.',
'type': int 'type': int
}, },
{
'name': 'trusted',
'required': False,
'description': 'Filter by trusted status (true/false).',
'type': bool
},
{
'name': 'external_object_identifier',
'required': False,
'description': 'Filter by external object identifier. Use "None" to filter for an empty field',
'type': str
},
{
'name': 'external_source_identifier',
'required': False,
'description': 'Filter by external source identifier. Use "None" to filter for an empty field',
'type': str
},
{
'name': 'search',
'required': False,
'description': 'Search by organization name or location name/city.',
'type': str
},
], ],
responses={200: RescueOrganizationSerializer(many=True)} responses={200: RescueOrganizationSerializer(many=True)}
) )
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
""" """
Get list of rescue organizations or a specific organization by ID or get a list with available filters for Get list of rescue organizations or a specific organization by ID.
- external_object_identifier
- external_source_identifier
""" """
org_id = request.query_params.get("id") org_id = kwargs.get("id")
external_object_identifier = request.query_params.get("external_object_identifier")
external_source_identifier = request.query_params.get("external_source_identifier")
search_query = request.query_params.get("search")
if org_id: if org_id:
try: try:
organization = RescueOrganization.objects.get(pk=org_id) organization = RescueOrganization.objects.get(pk=org_id)
@@ -178,33 +144,14 @@ class RescueOrganizationApiView(APIView):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
except RescueOrganization.DoesNotExist: except RescueOrganization.DoesNotExist:
return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND) return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
organizations = RescueOrganization.objects.all() organizations = RescueOrganization.objects.all()
if external_object_identifier:
if external_object_identifier == "None":
external_object_identifier = None
organizations = organizations.filter(external_object_identifier=external_object_identifier)
if external_source_identifier:
if external_source_identifier == "None":
external_source_identifier = None
organizations = organizations.filter(external_source_identifier=external_source_identifier)
if search_query:
organizations = organizations.filter(
Q(name__icontains=search_query) |
Q(location_string__icontains=search_query) |
Q(location__name__icontains=search_query) |
Q(location__city__icontains=search_query)
)
serializer = RescueOrganizationSerializer(organizations, many=True, context={"request": request}) serializer = RescueOrganizationSerializer(organizations, many=True, context={"request": request})
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
@transaction.atomic @transaction.atomic
@extend_schema( @extend_schema(
request=RescueOrgSerializer, request=RescueOrgSerializer, # Document the request body
responses={201: 'Rescue organization created successfully!'} responses={201: 'Rescue organization created/updated successfully!'}
) )
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
""" """
@@ -212,39 +159,11 @@ class RescueOrganizationApiView(APIView):
""" """
serializer = RescueOrgSerializer(data=request.data, context={"request": request}) serializer = RescueOrgSerializer(data=request.data, context={"request": request})
if serializer.is_valid(): if serializer.is_valid():
rescue_org = serializer.save() rescue_org = serializer.save(owner=request.user)
# Add the location
post_rescue_org_save.delay_on_commit(rescue_org.pk)
return Response( return Response(
{"message": "Rescue organization created successfully!", "id": rescue_org.id}, {"message": "Rescue organization created/updated successfully!", "id": rescue_org.id},
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
) )
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@transaction.atomic
@extend_schema(
request=RescueOrgSerializer,
responses={200: 'Rescue organization updated successfully!'}
)
def patch(self, request, *args, **kwargs):
"""
Partially update a rescue organization.
"""
org_id = kwargs.get("id")
if not org_id:
return Response({"error": "ID is required for updating."}, status=status.HTTP_400_BAD_REQUEST)
try:
organization = RescueOrganization.objects.get(pk=org_id)
except RescueOrganization.DoesNotExist:
return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
serializer = RescueOrgSerializer(organization, data=request.data, partial=True, context={"request": request})
if serializer.is_valid():
serializer.save()
return Response({"message": "Rescue organization updated successfully!"}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class AddImageApiView(APIView): class AddImageApiView(APIView):
@@ -291,63 +210,3 @@ class SpeciesApiView(APIView):
species = Species.objects.all() species = Species.objects.all()
serializer = SpeciesSerializer(species, many=True, context={"request": request}) serializer = SpeciesSerializer(species, many=True, context={"request": request})
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
class LocationApiView(APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
parameters=[
{
'name': 'id',
'required': False,
'description': 'ID of the location to retrieve.',
'type': int
},
],
responses={200: LocationSerializer(many=True)}
)
def get(self, request, *args, **kwargs):
"""
Retrieve a location
"""
location_id = kwargs.get("id")
if location_id:
try:
location = Location.objects.get(pk=location_id)
serializer = LocationSerializer(location, context={"request": request})
return Response(serializer.data, status=status.HTTP_200_OK)
except Location.DoesNotExist:
return Response({"error": "Location not found."}, status=status.HTTP_404_NOT_FOUND)
locations = Location.objects.all()
serializer = LocationSerializer(locations, many=True, context={"request": request})
return Response(serializer.data, status=status.HTTP_200_OK)
@transaction.atomic
@extend_schema(
request=LocationSerializer,
responses={201: 'Location created successfully!'}
)
def post(self, request, *args, **kwargs):
"""
API view to add a location
"""
serializer = LocationSerializer(data=request.data, context={'request': request})
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
location = serializer.save()
# Log the action
Log.objects.create(
user=request.user,
action="add_location",
text=f"{request.user} added adoption notice {location.pk} via API",
)
# Return success response with new adoption notice details
return Response(
{"message": "Location created successfully!", "id": location.pk},
status=status.HTTP_201_CREATED,
)

View File

@@ -22,15 +22,6 @@ class DateInput(forms.DateInput):
input_type = 'date' input_type = 'date'
class BulmaAdoptionNoticeForm(forms.ModelForm):
template_name = "fellchensammlung/forms/form_snippets.html"
class Meta:
model = AdoptionNotice
fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string",
"organization"]
class AdoptionNoticeForm(forms.ModelForm): class AdoptionNoticeForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if 'in_adoption_notice_creation_flow' in kwargs: if 'in_adoption_notice_creation_flow' in kwargs:
@@ -136,9 +127,8 @@ class ImageForm(forms.ModelForm):
self.helper.form_method = 'post' self.helper.form_method = 'post'
if in_flow: if in_flow:
submits = Div(Submit('submit', _('Speichern')), submits= Div(Submit('submit', _('Speichern')),
Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')), Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')), css_class="container-edit-buttons")
css_class="container-edit-buttons")
else: else:
submits = Fieldset(Submit('submit', _('Speichern')), css_class="container-edit-buttons") submits = Fieldset(Submit('submit', _('Speichern')), css_class="container-edit-buttons")
self.helper.layout = Layout( self.helper.layout = Layout(
@@ -150,6 +140,7 @@ class ImageForm(forms.ModelForm):
submits submits
) )
class Meta: class Meta:
model = Image model = Image
fields = ('image', 'alt_text') fields = ('image', 'alt_text')
@@ -173,7 +164,7 @@ class CommentForm(forms.ModelForm):
self.helper = FormHelper() self.helper = FormHelper()
self.helper.form_class = 'form-comments' self.helper.form_class = 'form-comments'
self.helper.add_input(Hidden('action', 'comment')) self.helper.add_input(Hidden('action', 'comment'))
self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="button is-primary")) self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="btn2"))
class Meta: class Meta:
model = Comment model = Comment
@@ -190,8 +181,7 @@ class CustomRegistrationForm(RegistrationForm):
class Meta(RegistrationForm.Meta): class Meta(RegistrationForm.Meta):
model = User model = User
captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_( captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_("Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um sicherzustellen, dass du kein Roboter bist."))
"Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um sicherzustellen, dass du kein Roboter bist."))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -203,10 +193,7 @@ class CustomRegistrationForm(RegistrationForm):
class AdoptionNoticeSearchForm(forms.Form): class AdoptionNoticeSearchForm(forms.Form):
template_name = "fellchensammlung/forms/form_snippets.html"
sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False, sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False,
initial=SexChoicesWithAll.ALL) initial=SexChoicesWithAll.ALL)
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED, max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED, label=_("Suchradius"))
label=_("Suchradius"))
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False) location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)

View File

@@ -1,20 +0,0 @@
# Generated by Django 5.1.4 on 2025-03-09 08:31
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0037_alter_basenotification_title'),
]
operations = [
migrations.AddField(
model_name='rescueorganization',
name='last_checked',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Datum der letzten Prüfung'),
preserve_default=False,
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2025-03-09 16:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0038_rescueorganization_last_checked'),
]
operations = [
migrations.AlterField(
model_name='rescueorganization',
name='last_checked',
field=models.DateTimeField(auto_now_add=True, verbose_name='Datum der letzten Prüfung'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2025-03-20 23:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0039_alter_rescueorganization_last_checked'),
]
operations = [
migrations.AlterField(
model_name='rescueorganization',
name='allows_using_materials',
field=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'),
),
]

View File

@@ -1,42 +0,0 @@
# Generated by Django 5.1.4 on 2025-04-06 06:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0040_alter_rescueorganization_allows_using_materials'),
]
operations = [
migrations.AddField(
model_name='location',
name='city',
field=models.CharField(blank=True, max_length=200, null=True),
),
migrations.AddField(
model_name='location',
name='country',
field=models.CharField(blank=True, help_text='Standardisierter Ländercode nach ISO 3166-1 ALPHA-2', max_length=2, null=True, verbose_name='Ländercode'),
),
migrations.AddField(
model_name='location',
name='housenumber',
field=models.CharField(blank=True, max_length=20, null=True),
),
migrations.AddField(
model_name='location',
name='postcode',
field=models.CharField(blank=True, max_length=20, null=True),
),
migrations.AddField(
model_name='location',
name='street',
field=models.CharField(blank=True, max_length=200, null=True),
),
migrations.AlterUniqueTogether(
name='rescueorganization',
unique_together={('external_object_identifier', 'external_source_identifier')},
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2025-04-24 17:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0041_location_city_location_country_location_housenumber_and_more'),
]
operations = [
migrations.AddField(
model_name='location',
name='county',
field=models.CharField(blank=True, max_length=200, null=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2025-04-24 17:41
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0042_location_county'),
]
operations = [
migrations.RenameField(
model_name='location',
old_name='country',
new_name='countrycode',
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2025-04-26 22:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0043_rename_country_location_countrycode'),
]
operations = [
migrations.AlterField(
model_name='location',
name='place_id',
field=models.CharField(max_length=200),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 5.1.4 on 2025-04-27 11:31
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0044_alter_location_place_id'),
]
operations = [
migrations.CreateModel(
name='ImportantLocation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.SlugField(unique=True)),
('name', models.CharField(max_length=200)),
('location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.location')),
],
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 5.1.4 on 2025-04-27 11:51
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0045_importantlocation'),
]
operations = [
migrations.AlterField(
model_name='importantlocation',
name='location',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.location'),
),
]

View File

@@ -3,7 +3,6 @@ from random import choices
from tabnanny import verbose from tabnanny import verbose
from django.db import models from django.db import models
from django.template.defaultfilters import slugify
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone from django.utils import timezone
@@ -40,28 +39,15 @@ class Language(models.Model):
class Location(models.Model): class Location(models.Model):
place_id = models.CharField(max_length=200) # OSM id place_id = models.IntegerField() # OSM id
latitude = models.FloatField() latitude = models.FloatField()
longitude = models.FloatField() longitude = models.FloatField()
name = models.CharField(max_length=2000) name = models.CharField(max_length=2000)
city = models.CharField(max_length=200, blank=True, null=True)
housenumber = models.CharField(max_length=20, blank=True, null=True)
postcode = models.CharField(max_length=20, blank=True, null=True)
street = models.CharField(max_length=200, blank=True, null=True)
county = models.CharField(max_length=200, blank=True, null=True)
# Country code as per ISO 3166-1 alpha-2
# https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes
countrycode = models.CharField(max_length=2, verbose_name=_("Ländercode"),
help_text=_("Standardisierter Ländercode nach ISO 3166-1 ALPHA-2"),
blank=True, null=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
if self.city and self.postcode: return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
return f"{self.city} ({self.postcode})"
else:
return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
@property @property
def position(self): def position(self):
@@ -87,11 +73,6 @@ class Location(models.Model):
latitude=proxy.latitude, latitude=proxy.latitude,
longitude=proxy.longitude, longitude=proxy.longitude,
name=proxy.name, name=proxy.name,
postcode=proxy.postcode,
city=proxy.city,
street=proxy.street,
county=proxy.county,
countrycode=proxy.countrycode,
) )
return location return location
@@ -103,33 +84,33 @@ class Location(models.Model):
instance.save() instance.save()
class ImportantLocation(models.Model):
location = models.OneToOneField(Location, on_delete=models.CASCADE)
slug = models.SlugField(unique=True)
name = models.CharField(max_length=200)
class ExternalSourceChoices(models.TextChoices): class ExternalSourceChoices(models.TextChoices):
OSM = "OSM", _("Open Street Map") OSM = "OSM", _("Open Street Map")
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")
class RescueOrganization(models.Model): class RescueOrganization(models.Model):
def __str__(self): def __str__(self):
return f"{self.name}" return f"{self.name}"
USE_MATERIALS_ALLOWED = "allowed"
USE_MATERIALS_REQUESTED = "requested"
USE_MATERIALS_DENIED = "denied"
USE_MATERIALS_OTHER = "other"
USE_MATERIALS_NOT_ASKED = "not_asked"
ALLOW_USE_MATERIALS_CHOICE = {
USE_MATERIALS_ALLOWED: "Usage allowed",
USE_MATERIALS_REQUESTED: "Usage requested",
USE_MATERIALS_DENIED: "Usage denied",
USE_MATERIALS_OTHER: "It's complicated",
USE_MATERIALS_NOT_ASKED: "Not asked"
}
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig')) trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
allows_using_materials = models.CharField(max_length=200, allows_using_materials = models.CharField(max_length=200,
default=AllowUseOfMaterialsChices.USE_MATERIALS_NOT_ASKED, default=ALLOW_USE_MATERIALS_CHOICE[USE_MATERIALS_NOT_ASKED],
choices=AllowUseOfMaterialsChices.choices, choices=ALLOW_USE_MATERIALS_CHOICE,
verbose_name=_('Erlaubt Nutzung von Inhalten')) verbose_name=_('Erlaubt Nutzung von Inhalten'))
location_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation")) location_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation"))
location = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True) location = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True)
@@ -141,7 +122,6 @@ class RescueOrganization(models.Model):
website = models.URLField(null=True, blank=True, verbose_name=_('Website')) website = models.URLField(null=True, blank=True, verbose_name=_('Website'))
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
last_checked = models.DateTimeField(auto_now_add=True, verbose_name=_('Datum der letzten Prüfung'))
internal_comment = models.TextField(verbose_name=_("Interner Kommentar"), null=True, blank=True, ) internal_comment = models.TextField(verbose_name=_("Interner Kommentar"), null=True, blank=True, )
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung')) # Markdown allowed description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung')) # Markdown allowed
external_object_identifier = models.CharField(max_length=200, null=True, blank=True, external_object_identifier = models.CharField(max_length=200, null=True, blank=True,
@@ -150,9 +130,6 @@ class RescueOrganization(models.Model):
choices=ExternalSourceChoices.choices, choices=ExternalSourceChoices.choices,
verbose_name=_('External Source Identifier')) verbose_name=_('External Source Identifier'))
class Meta:
unique_together = ('external_object_identifier', 'external_source_identifier',)
def get_absolute_url(self): def get_absolute_url(self):
return reverse("rescue-organization-detail", args=[str(self.pk)]) return reverse("rescue-organization-detail", args=[str(self.pk)])
@@ -172,20 +149,7 @@ class RescueOrganization(models.Model):
if self.description is None: if self.description is None:
return "" return ""
if len(self.description) > 200: if len(self.description) > 200:
return self.description[:200] + _(f" ... [weiterlesen]({self.get_absolute_url()})") return self.description[:200] + f" ... [weiterlesen]({self.get_absolute_url()})"
def set_checked(self):
self.last_checked = timezone.now()
self.save()
@property
def last_checked_hr(self):
time_since_last_checked = timezone.now() - self.last_checked
return time_since_as_hr_string(time_since_last_checked)
@property
def species_urls(self):
return SpeciesSpecificURL.objects.filter(organization=self)
# Admins can perform all actions and have the highest trust associated with them # Admins can perform all actions and have the highest trust associated with them
@@ -297,13 +261,11 @@ class AdoptionNotice(models.Model):
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=timezone.now) last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=timezone.now)
searching_since = models.DateField(verbose_name=_('Sucht nach einem Zuhause seit')) searching_since = models.DateField(verbose_name=_('Sucht nach einem Zuhause seit'))
name = models.CharField(max_length=200, verbose_name=_('Titel der Vermittlung')) name = models.CharField(max_length=200)
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung')) description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
organization = models.ForeignKey(RescueOrganization, blank=True, null=True, on_delete=models.SET_NULL, organization = models.ForeignKey(RescueOrganization, blank=True, null=True, on_delete=models.SET_NULL,
verbose_name=_('Organisation')) verbose_name=_('Organisation'))
further_information = models.URLField(null=True, blank=True, further_information = models.URLField(null=True, blank=True, verbose_name=_('Link zu mehr Informationen'))
verbose_name=_('Link zu mehr Informationen'),
help_text=_("Verlinke hier die Quelle der Vermittlung (z.B. die Website des Tierheims"))
group_only = models.BooleanField(default=False, verbose_name=_('Ausschließlich Gruppenadoption')) group_only = models.BooleanField(default=False, verbose_name=_('Ausschließlich Gruppenadoption'))
photos = models.ManyToManyField(Image, blank=True) photos = models.ManyToManyField(Image, blank=True)
location_string = models.CharField(max_length=200, verbose_name=_("Ortsangabe")) location_string = models.CharField(max_length=200, verbose_name=_("Ortsangabe"))
@@ -321,13 +283,6 @@ class AdoptionNotice(models.Model):
sexes.add(animal.sex) sexes.add(animal.sex)
return sexes return sexes
@property
def num_per_sex(self):
num_per_sex = dict()
for sex in SexChoices:
num_per_sex[sex] = self.animals.filter(sex=sex).count
return num_per_sex
@property @property
def last_checked_hr(self): def last_checked_hr(self):
time_since_last_checked = timezone.now() - self.last_checked time_since_last_checked = timezone.now() - self.last_checked
@@ -368,10 +323,6 @@ class AdoptionNotice(models.Model):
"""Returns the url to access a detailed page for the adoption notice.""" """Returns the url to access a detailed page for the adoption notice."""
return reverse('adoption-notice-detail', args=[str(self.id)]) return reverse('adoption-notice-detail', args=[str(self.id)])
def get_absolute_url_bulma(self):
"""Returns the url to access a detailed page for the adoption notice."""
return reverse('adoption-notice-detail-bulma', args=[str(self.id)])
def get_report_url(self): def get_report_url(self):
"""Returns the url to report an adoption notice.""" """Returns the url to report an adoption notice."""
return reverse('report-adoption-notice', args=[str(self.id)]) return reverse('report-adoption-notice', args=[str(self.id)])
@@ -715,31 +666,6 @@ class Report(models.Model):
def get_moderation_actions(self): def get_moderation_actions(self):
return ModerationAction.objects.filter(report=self) return ModerationAction.objects.filter(report=self)
@property
def reported_content(self):
"""
Dynamically fetch the reported content based on subclass.
The alternative would be to use the ContentType framework:
https://docs.djangoproject.com/en/5.1/ref/contrib/contenttypes/
"""
if hasattr(self, "reportadoptionnotice"):
return self.reportadoptionnotice.adoption_notice
elif hasattr(self, "reportcomment"):
return self.reportcomment.reported_comment
return None
@property
def reported_content_url(self):
"""
Same as reported_content, just for url
"""
if hasattr(self, "reportadoptionnotice"):
print(self.reportadoptionnotice.adoption_notice.get_absolute_url)
return self.reportadoptionnotice.adoption_notice.get_absolute_url
elif hasattr(self, "reportcomment"):
return self.reportcomment.reported_comment.get_absolute_url
return None
class ReportAdoptionNotice(Report): class ReportAdoptionNotice(Report):
adoption_notice = models.ForeignKey("AdoptionNotice", on_delete=models.CASCADE) adoption_notice = models.ForeignKey("AdoptionNotice", on_delete=models.CASCADE)
@@ -748,9 +674,6 @@ class ReportAdoptionNotice(Report):
def reported_content(self): def reported_content(self):
return self.adoption_notice return self.adoption_notice
def __str__(self):
return f"Report der Vermittlung {self.adoption_notice}"
class ReportComment(Report): class ReportComment(Report):
reported_comment = models.ForeignKey("Comment", on_delete=models.CASCADE) reported_comment = models.ForeignKey("Comment", on_delete=models.CASCADE)

View File

@@ -1,6 +1,6 @@
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from fellchensammlung.models import BaseNotification, CommentNotification, User, TrustLevel, RescueOrganization from fellchensammlung.models import BaseNotification, CommentNotification, User, TrustLevel
from .tasks import task_send_notification_email from .tasks import task_send_notification_email
from notfellchen.settings import host from notfellchen.settings import host
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -18,13 +18,6 @@ def base_notification_receiver(sender, instance: BaseNotification, created: bool
else: else:
task_send_notification_email.delay(instance.pk) task_send_notification_email.delay(instance.pk)
@receiver(post_save, sender=RescueOrganization)
def rescue_org_receiver(sender, instance: RescueOrganization, created: bool, **kwargs):
if instance.location:
return
else:
task_send_notification_email.delay(instance.pk)
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def notification_new_user(sender, instance: User, created: bool, **kwargs): def notification_new_user(sender, instance: User, created: bool, **kwargs):

View File

@@ -1,47 +0,0 @@
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
from .models import AdoptionNotice, RescueOrganization
class StaticViewSitemap(Sitemap):
priority = 0.8
changefreq = "weekly"
def items(self):
return ["index", "search", "map", "about", "rescue-organizations"]
def location(self, item):
return reverse(item)
class AdoptionNoticeSitemap(Sitemap):
priority = 0.5
changefreq = "daily"
def items(self):
return AdoptionNotice.get_active_ANs()
def lastmod(self, obj):
return obj.updated_at
class AnimalSitemap(Sitemap):
priority = 0.2
changefreq = "daily"
def items(self):
return AdoptionNotice.objects.all()
def lastmod(self, obj):
return obj.updated_at
class RescueOrganizationSitemap(Sitemap):
priority = 0.3
changefreq = "weekly"
def items(self):
return RescueOrganization.objects.all()
def lastmod(self, obj):
return obj.updated_at

View File

@@ -1,60 +0,0 @@
/***************/
/* MAIN COLORS */
/***************/
:root {
--primary-light-one: #5daa68;
--primary-light-two: #4a9455;
--primary-semidark-one: #356c3c;
--primary-dark-one: #17311b;
--secondary-light-one: #faf1cf;
--secondary-light-two: #e1d7b5;
--background-one: var(--primary-light-one);
--background-two: var(--primary-light-two);
--background-three: var(--secondary-light-one);
--background-four: var(--primary-dark-one);
--highlight-one: var(--primary-dark-one);
--highlight-one-text: var(--secondary-light-one);
--highlight-two: var(--primary-semidark-one);
--text-one: var(--secondary-light-one);
--shadow-one: var(--primary-dark-one);
--text-two: var(--primary-dark-one);
--text-three: var(--primary-light-one);
--shadow-three: var(--primary-dark-one);
}
.content {
padding: 10px;
}
/*******/
/* MAP */
/*******/
.map {
border-radius: 8px;
width:100%;
height:100%
}
.marker {
background-image: url('../img/logo_transparent.png');
background-size: cover;
width: 50px;
height: 50px;
cursor: pointer;
}
.animal-shelter-marker {
background-image: url('../img/animal_shelter.png');
!important;
}
.maplibregl-popup {
max-width: 600px !important;
}
.map-in-content #map {
max-height: 500px;
width: 90%;
}

File diff suppressed because one or more lines are too long

View File

@@ -1,420 +0,0 @@
/*! PhotoSwipe main CSS by Dmytro Semenov | photoswipe.com */
.pswp {
--pswp-bg: #000;
--pswp-placeholder-bg: #222;
--pswp-root-z-index: 100000;
--pswp-preloader-color: rgba(79, 79, 79, 0.4);
--pswp-preloader-color-secondary: rgba(255, 255, 255, 0.9);
/* defined via js:
--pswp-transition-duration: 333ms; */
--pswp-icon-color: #fff;
--pswp-icon-color-secondary: #4f4f4f;
--pswp-icon-stroke-color: #4f4f4f;
--pswp-icon-stroke-width: 2px;
--pswp-error-text-color: var(--pswp-icon-color);
}
/*
Styles for basic PhotoSwipe (pswp) functionality (sliding area, open/close transitions)
*/
.pswp {
position: fixed;
z-index: var(--pswp-root-z-index);
display: none;
touch-action: none;
outline: 0;
opacity: 0.003;
contain: layout style size;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* Prevents focus outline on the root element,
(it may be focused initially) */
.pswp:focus {
outline: 0;
}
.pswp * {
box-sizing: border-box;
}
.pswp img {
max-width: none;
}
.pswp--open {
display: block;
}
.pswp,
.pswp__bg {
transform: translateZ(0);
will-change: opacity;
}
.pswp__bg {
opacity: 0.005;
background: var(--pswp-bg);
}
.pswp,
.pswp__scroll-wrap {
overflow: hidden;
}
.pswp,
.pswp__scroll-wrap,
.pswp__bg,
.pswp__container,
.pswp__item,
.pswp__content,
.pswp__img,
.pswp__zoom-wrap {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.pswp {
position: fixed;
}
.pswp__img,
.pswp__zoom-wrap {
width: auto;
height: auto;
}
.pswp--click-to-zoom.pswp--zoom-allowed .pswp__img {
cursor: -webkit-zoom-in;
cursor: -moz-zoom-in;
cursor: zoom-in;
}
.pswp--click-to-zoom.pswp--zoomed-in .pswp__img {
cursor: move;
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.pswp--click-to-zoom.pswp--zoomed-in .pswp__img:active {
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* :active to override grabbing cursor */
.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img,
.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img:active,
.pswp__img {
cursor: -webkit-zoom-out;
cursor: -moz-zoom-out;
cursor: zoom-out;
}
/* Prevent selection and tap highlights */
.pswp__container,
.pswp__img,
.pswp__button,
.pswp__counter {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.pswp__item {
/* z-index for fade transition */
z-index: 1;
overflow: hidden;
}
.pswp__hidden {
display: none !important;
}
/* Allow to click through pswp__content element, but not its children */
.pswp__content {
pointer-events: none;
}
.pswp__content > * {
pointer-events: auto;
}
/*
PhotoSwipe UI
*/
/*
Error message appears when image is not loaded
(JS option errorMsg controls markup)
*/
.pswp__error-msg-container {
display: grid;
}
.pswp__error-msg {
margin: auto;
font-size: 1em;
line-height: 1;
color: var(--pswp-error-text-color);
}
/*
class pswp__hide-on-close is applied to elements that
should hide (for example fade out) when PhotoSwipe is closed
and show (for example fade in) when PhotoSwipe is opened
*/
.pswp .pswp__hide-on-close {
opacity: 0.005;
will-change: opacity;
transition: opacity var(--pswp-transition-duration) cubic-bezier(0.4, 0, 0.22, 1);
z-index: 10; /* always overlap slide content */
pointer-events: none; /* hidden elements should not be clickable */
}
/* class pswp--ui-visible is added when opening or closing transition starts */
.pswp--ui-visible .pswp__hide-on-close {
opacity: 1;
pointer-events: auto;
}
/* <button> styles, including css reset */
.pswp__button {
position: relative;
display: block;
width: 50px;
height: 60px;
padding: 0;
margin: 0;
overflow: hidden;
cursor: pointer;
background: none;
border: 0;
box-shadow: none;
opacity: 0.85;
-webkit-appearance: none;
-webkit-touch-callout: none;
}
.pswp__button:hover,
.pswp__button:active,
.pswp__button:focus {
transition: none;
padding: 0;
background: none;
border: 0;
box-shadow: none;
opacity: 1;
}
.pswp__button:disabled {
opacity: 0.3;
cursor: auto;
}
.pswp__icn {
fill: var(--pswp-icon-color);
color: var(--pswp-icon-color-secondary);
}
.pswp__icn {
position: absolute;
top: 14px;
left: 9px;
width: 32px;
height: 32px;
overflow: hidden;
pointer-events: none;
}
.pswp__icn-shadow {
stroke: var(--pswp-icon-stroke-color);
stroke-width: var(--pswp-icon-stroke-width);
fill: none;
}
.pswp__icn:focus {
outline: 0;
}
/*
div element that matches size of large image,
large image loads on top of it,
used when msrc is not provided
*/
div.pswp__img--placeholder,
.pswp__img--with-bg {
background: var(--pswp-placeholder-bg);
}
.pswp__top-bar {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 60px;
display: flex;
flex-direction: row;
justify-content: flex-end;
z-index: 10;
/* allow events to pass through top bar itself */
pointer-events: none !important;
}
.pswp__top-bar > * {
pointer-events: auto;
/* this makes transition significantly more smooth,
even though inner elements are not animated */
will-change: opacity;
}
/*
Close button
*/
.pswp__button--close {
margin-right: 6px;
}
/*
Arrow buttons
*/
.pswp__button--arrow {
position: absolute;
top: 0;
width: 75px;
height: 100px;
top: 50%;
margin-top: -50px;
}
.pswp__button--arrow:disabled {
display: none;
cursor: default;
}
.pswp__button--arrow .pswp__icn {
top: 50%;
margin-top: -30px;
width: 60px;
height: 60px;
background: none;
border-radius: 0;
}
.pswp--one-slide .pswp__button--arrow {
display: none;
}
/* hide arrows on touch screens */
.pswp--touch .pswp__button--arrow {
visibility: hidden;
}
/* show arrows only after mouse was used */
.pswp--has_mouse .pswp__button--arrow {
visibility: visible;
}
.pswp__button--arrow--prev {
right: auto;
left: 0px;
}
.pswp__button--arrow--next {
right: 0px;
}
.pswp__button--arrow--next .pswp__icn {
left: auto;
right: 14px;
/* flip horizontally */
transform: scale(-1, 1);
}
/*
Zoom button
*/
.pswp__button--zoom {
display: none;
}
.pswp--zoom-allowed .pswp__button--zoom {
display: block;
}
/* "+" => "-" */
.pswp--zoomed-in .pswp__zoom-icn-bar-v {
display: none;
}
/*
Loading indicator
*/
.pswp__preloader {
position: relative;
overflow: hidden;
width: 50px;
height: 60px;
margin-right: auto;
}
.pswp__preloader .pswp__icn {
opacity: 0;
transition: opacity 0.2s linear;
animation: pswp-clockwise 600ms linear infinite;
}
.pswp__preloader--active .pswp__icn {
opacity: 0.85;
}
@keyframes pswp-clockwise {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/*
"1 of 10" counter
*/
.pswp__counter {
height: 30px;
margin: 15px 0 0 20px;
font-size: 14px;
line-height: 30px;
color: var(--pswp-icon-color);
text-shadow: 1px 1px 3px var(--pswp-icon-color-secondary);
opacity: 0.85;
}
.pswp--one-slide .pswp__counter {
display: none;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -1,11 +0,0 @@
import PhotoSwipeLightbox from 'https://unpkg.com/photoswipe/dist/photoswipe-lightbox.esm.js';
const lightbox = new PhotoSwipeLightbox({
gallery: '#my-gallery',
children: 'a',
pswpModule: () => import('https://unpkg.com/photoswipe'),
});
lightbox.init();

View File

@@ -1,32 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Add a click event on each of them
$navbarBurgers.forEach( el => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target;
const $target = document.getElementById(target);
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
});
});
});
// Looks for all notifications with a delete and allows closing them when pressing delete
document.addEventListener('DOMContentLoaded', () => {
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
const $notification = $delete.parentNode;
$delete.addEventListener('click', () => {
$notification.parentNode.removeChild($notification);
});
});
});

View File

@@ -1,7 +0,0 @@
User-agent: *
Disallow: /admin/
User-agent: OpenAI
Disallow: /
Sitemap: https://notfellchen.org/sitemap.xml

View File

@@ -6,7 +6,7 @@ from notfellchen.celery import app as celery_app
from .mail import send_notification_email from .mail import send_notification_email
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices
from .tools.misc import healthcheck_ok from .tools.misc import healthcheck_ok
from .models import Location, AdoptionNotice, Timestamp, RescueOrganization from .models import Location, AdoptionNotice, Timestamp
from .tools.notifications import notify_of_AN_to_be_checked from .tools.notifications import notify_of_AN_to_be_checked
from .tools.search import notify_search_subscribers from .tools.search import notify_search_subscribers
@@ -57,10 +57,3 @@ def task_healthcheck():
@shared_task @shared_task
def task_send_notification_email(notification_pk): def task_send_notification_email(notification_pk):
send_notification_email(notification_pk) send_notification_email(notification_pk)
@celery_app.task(name="commit.post_rescue_org_save")
def post_rescue_org_save(pk):
instance = RescueOrganization.objects.get(pk=pk)
Location.add_location_to_object(instance)
set_timestamp("add_rescue_org_location")
logging.info(f"Location was added to Rescue Organization {pk}")

View File

@@ -1,43 +0,0 @@
{% load custom_tags %}
{% load i18n %}
{% load static %}
{% get_current_language as LANGUAGE_CODE%}
<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE }}">
<head>
{% block title %}{% endblock %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{% translate "Farbratten aus dem Tierschutz finden und adoptieren" %}">
<!-- Add additional CSS in static file -->
<link rel="stylesheet" href="{% static 'fellchensammlung/css/bulma-styles.css' %}">
<link rel="stylesheet" href="{% static 'fellchensammlung/css/bulma.min.css' %}">
<link rel="stylesheet" href="https://unpkg.com/photoswipe@5.2.2/dist/photoswipe.css">
<link href="{% static 'fontawesomefree/css/fontawesome.css' %}" rel="stylesheet" type="text/css">
<link href="{% static 'fontawesomefree/css/brands.css' %}" rel="stylesheet" type="text/css">
<link href="{% static 'fontawesomefree/css/solid.css' %}" rel="stylesheet" type="text/css">
<script src="{% static 'fellchensammlung/js/custom.js' %}"></script>
<script src="{% static 'fellchensammlung/js/toggles.js' %}"></script>
<script type="module" src="{% static 'fellchensammlung/js/photoswipe.js' %}"></script>
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'fellchensammlung/favicon/apple-touch-icon.png' %}">
<link rel="icon" type="image/png" sizes="32x32" href="{% static 'fellchensammlung/favicon/favicon-32x32.png' %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% static 'fellchensammlung/favicon/favicon-16x16.png' %}">
{% get_oxitraffic_script_if_enabled %}
</head>
<body>
{% block header %}
{% include "fellchensammlung/bulma-header.html" %}
{% endblock %}
<div class="content">
{% block content %}{% endblock %}
</div>
{% block footer %}
{% include "fellchensammlung/bulma-footer.html" %}
{% endblock %}
</body>
</html>

View File

@@ -1,8 +1,7 @@
{% load custom_tags %} {% load custom_tags %}
{% load i18n %} {% load i18n %}
{% get_current_language as LANGUAGE_CODE%}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE }}"> <html lang="en">
<head> <head>
{% block title %}{% endblock %} {% block title %}{% endblock %}
<meta charset="utf-8"> <meta charset="utf-8">

View File

@@ -1,27 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% load custom_tags %}
{% block title %}<title>{% translate "Über uns" %}</title>{% endblock %}
{% block content %}
{% if about_us %}
<div class="block">
<h1 class="title is-1">{{ about_us.title }}</h1>
<div class="content">
{{ about_us.content | render_markdown }}
</div>
</div>
{% endif %}
{% if faq %}
<div class="card">
<div class="card-header">
<h2 class="card-header-title">{{ faq.title }}</h2>
</div>
<div class="card-content">
{{ faq.content | render_markdown }}
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -1,83 +0,0 @@
{% load static %}
{% load i18n %}
<footer class="footer">
<div class="columns">
<div class="column">
<div class="block">
<h3 class="bd-footer-title title is-3 has-text-left">
Notfellchen
</h3>
<!-- footer content -->
<p class="bd-footer-link
has-text-left">
Für Menschen die Ratten aus dem Tierschutz ein liebendes Zuhause geben wollen.
</p>
</div>
<div class="block">
<h3 class="bd-footer-title title is-5">
{% trans 'Sprache ändern' %}
</h3>
{% include "fellchensammlung/forms/change_language.html" %}
</div>
</div>
<div class="column">
<h4 class="bd-footer-title title is-4 has-text-justify">
{% translate 'Über uns' %}
</h4>
<a class="bd-footer-link" href="{% url "about-bulma" %}">
{% translate 'Das Notfellchen Projekt' %}
</a>
<br/>
<a class="bd-footer-link" href="{% url "terms-of-service" %}">
{% translate 'Nutzungsbedingungen' %}
</a>
<br/>
<a class="bd-footer-link" href="{% url "privacy" %}">
{% translate 'Datenschutz' %}
</a>
<br/>
<a class="bd-footer-link" href="{% url "imprint" %}">
{% translate 'Impressum' %}
</a>
<br/>
</div>
<div class="column">
<h4 class="bd-footer-title title is-4 has-text-justify">
Technisches
</h4>
<p class="bd-footer-link">
<a class="nav-link " href="{% url "rss" %}">
<i class="fa-solid fa-rss"></i> {% translate 'RSS' %}
</a>
<br/>
<a href="https://dokumentation.notfellchen.org/">
<span class="icon-text">
<span>{% translate 'Dokumentation' %}</span>
</span>
</a>
<br/>
<a href="mailto:info@notfellchen.org">
<span class="icon-text">
<span>{% translate 'Probleme melden' %}</span>
</span>
</a>
<br/>
<a href="https://codeberg.org/moanos/notfellchen">
<span class="icon-text">
<span>{% trans 'Code' %}</span>
</span>
</a>
</p>
</div>
</div>
</footer>

View File

@@ -1,43 +0,0 @@
{% load static %}
{% load i18n %}
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="{% url 'index-bulma' %}">
<img src="{% static 'fellchensammlung/img/logo_transparent.png' %}" alt="{% trans 'Notfellchen Logo' %}">
<h1 class="title is-4">notfellchen.org</h1>
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="{% url 'search-bulma' %}">
<i class="fas fa-search"></i> {% translate 'Suchen' %}
</a>
<a class="navbar-item" href="{% url "add-adoption-bulma" %}">
<i class="fas fa-feather"></i> {% translate 'Vermittlung hinzufügen' %}
</a>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<a class="button is-primary" href="{% url "django_registration_register" %}">
<strong>{% translate "Registrieren" %}</strong>
</a>
<a class="button is-light" href="{% url "login" %}">
<strong>{% translate "Login" %}</strong>
</a>
</div>
</div>
</div>
</div>
</div>
</nav>

View File

@@ -1,34 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% load custom_tags %}
{% block title %}<title>{% translate "Notfellchen - Farbratten aus dem Tierschutz adoptieren" %}</title>{% endblock %}
{% block content %}
{% for announcement in announcements %}
{% include "fellchensammlung/partials/bulma-partial-announcement.html" %}
{% endfor %}
{% if introduction %}
<h1>{{ introduction.title }}</h1>
{{ introduction.content | render_markdown }}
{% endif %}
<h2>{% translate "Aktuelle Vermittlungen" %}</h2>
<div class="block">
{% include "fellchensammlung/lists/bulma-list-adoption-notices.html" %}
<a class="button is-primary" href="{% url 'search' %}">{% translate "Mehr Vermittlungen" %}</a>
</div>
<div class="block" style="height: 50vh">
{% include "fellchensammlung/partials/bulma-partial-map.html" %}
</div>
{% if how_to %}
<div class="card">
<h1>{{ how_to.title }}</h1>
{{ how_to.content | render_markdown }}
</div>
{% endif %}
{% endblock %}

View File

@@ -1,9 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% block title %}<title>{% translate "Karte" %}</title>{% endblock %}
{% block content %}
<div style="height:70vh">
{% include "fellchensammlung/partials/bulma-partial-map.html" %}
</div>
{% endblock %}

View File

@@ -1,15 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% load custom_tags %}
{% block title %}<title>{{ text.title }}</title>{% endblock %}
{% block content %}
<div class="block">
<h1 class="title is-1">{{ text.title }}</h1>
<div class="content">
{{ text.content | render_markdown }}
</div>
</div>
{% endblock %}

View File

@@ -1,97 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% block title %}<title>{% translate "Suche" %}</title>{% endblock %}
{% block content %}
{% get_current_language as LANGUAGE_CODE_CURRENT %}
<div class="columns">
<div class="column is-two-thirds">
<div style="height: 50vh">
{% include "fellchensammlung/partials/bulma-partial-map.html" %}
</div>
</div>
<div class="column">
<form class="block" method="post">
{% csrf_token %}
<input type="hidden" name="longitude" maxlength="200" id="longitude">
<input type="hidden" name="latitude" maxlength="200" id="latitude">
<input type="hidden" id="place_id" name="place_id">
<!--- https://docs.djangoproject.com/en/5.2/topics/forms/#reusable-form-templates -->
{{ search_form }}
<ul id="results"></ul>
<button class="button is-primary" type="submit" value="search" name="search">
<i class="fas fa-search"></i> {% trans 'Suchen' %}
</button>
{% if searched %}
{% if subscribed_search %}
<button class="button" type="submit" value="{{ subscribed_search.pk }}"
name="unsubscribe_to_search">
<i class="fas fa-bell-slash"></i> {% trans 'Suche nicht mehr abonnieren' %}
</button>
{% else %}
<button class="button" type="submit" name="subscribe_to_search">
<i class="fas fa-bell"></i> {% trans 'Suche abonnieren' %}
</button>
{% endif %}
{% endif %}
</form>
<div class="block">
{% if place_not_found %}
<div class="block notification is-warning">
<p>
{% trans 'Ort nicht gefunden' %}
</p>
</div>
{% endif %}
</div>
</div>
</div>
<div class="">
{% include "fellchensammlung/lists/bulma-list-adoption-notices.html" %}
</div>
<script>
const locationInput = document.getElementById('id_location_string');
const resultsList = document.getElementById('results');
const placeIdInput = document.getElementById('place_id');
locationInput.addEventListener('input', async function () {
const query = locationInput.value.trim();
if (query.length < 3) {
resultsList.innerHTML = ''; // Don't search for or show results if input is less than 3 characters
return;
}
try {
const response = await fetch(`{{ geocoding_api_url }}/?q=${encodeURIComponent(query)}&limit=5&lang={{ LANGUAGE_CODE_CURRENT }}`);
const data = await response.json();
if (data && data.features) {
resultsList.innerHTML = ''; // Clear previous results
const locations = data.features.slice(0, 5); // Show only the first 5 results
locations.forEach(location => {
const listItem = document.createElement('li');
listItem.classList.add('result-item');
listItem.textContent = geojson_to_summary(location);
// Add event when user clicks on a result location
listItem.addEventListener('click', () => {
locationInput.value = geojson_to_searchable_string(location); // Set input field to selected location
resultsList.innerHTML = ''; // Clear the results after selecting a location
});
resultsList.appendChild(listItem);
});
}
} catch (error) {
console.error('Error fetching location data:', error);
resultsList.innerHTML = '<li class="result-item">Error fetching data. Please try again.</li>';
}
});
</script>
{% endblock %}

View File

@@ -1,120 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% load static %}
{% block title %}<title>{% translate "Styleguide für Bulma" %}</title>{% endblock %}
{% block content %}
<section class="section">
<div class="container">
<h1 class="title">
Hello World
</h1>
<p class="subtitle">
Notfellchen bald mit <strong>Bulma</strong>?
</p>
</div>
<div class="grid">
<div class="card">
<div class="card-image">
<figure class="image">
<img
src="{% static 'fellchensammlung/img/example_rat_single.png' %}"
alt="Placeholder image"
/>
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img
src="https://bulma.io/assets/images/placeholders/96x96.png"
alt="Placeholder image"
/>
</figure>
</div>
<div class="media-content">
<p class="title is-4">John Smith</p>
<p class="subtitle is-6">@johnsmith</p>
</div>
</div>
<div class="content">
Süße Ratte sucht Zuhause
<a href="#">#responsive</a>
<br/>
<time datetime="2016-1-1">11:09 PM - 1 Jan 2016</time>
</div>
</div>
</div>
<div class="card">
<div class="card-image">
<figure class="image">
<img
src="{% static 'fellchensammlung/img/example_rat_single.png' %}"
alt="Placeholder image"
/>
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img
src="https://bulma.io/assets/images/placeholders/96x96.png"
alt="Placeholder image"
/>
</figure>
</div>
<div class="media-content">
<p class="title is-4">John Smith</p>
<p class="subtitle is-6">@johnsmith</p>
</div>
</div>
<div class="content">
Süßeste Ratte sucht Zuhause
<a href="#">#responsive</a>
<br/>
<time datetime="2016-1-1">11:09 PM - 1 Jan 2016</time>
</div>
</div>
</div>
<div class="card">
<div class="card-image">
<figure class="image">
<img
src="{% static 'fellchensammlung/img/example_rat_single.png' %}"
alt="Placeholder image"
/>
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img
src="https://bulma.io/assets/images/placeholders/96x96.png"
alt="Placeholder image"
/>
</figure>
</div>
<div class="media-content">
<p class="title is-4">John Smith</p>
<p class="subtitle is-6">@johnsmith</p>
</div>
</div>
<div class="content">
Süßere Ratte sucht Zuhause
<a href="#">#responsive</a>
<br/>
<time datetime="2016-1-1">11:09 PM - 1 Jan 2016</time>
</div>
</div>
</div>
</div>
</section>
{% endblock %}

View File

@@ -1,17 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% load custom_tags %}
{% block title %}<title>{% translate "Über uns" %}</title>{% endblock %}
{% block content %}
<h2 class="title is-2">{% translate "Regeln" %}</h2>
{% include "fellchensammlung/lists/bulma-list-rules.html" %}
<div class="block">
<h1 class="title is-1">{{ text.title }}</h1>
<div class="content">
{{ text.content | render_markdown }}
</div>
</div>
{% endblock %}

View File

@@ -1,113 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load custom_tags %}
{% load i18n %}
{% load static %}
{% block title %}<title>{{ adoption_notice.name }}</title>{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h1 class="card-header-title title is-2">{{ adoption_notice.name }}</h1>
</div>
<div class="card-content">
<div class="grid">
<div class="cell">
<!--- General Information --->
<div class="grid">
<div class="cell">
<h2><strong>{% translate "Ort" %}</strong></h2>
<p>{% if adoption_notice.location %}
{{ adoption_notice.location }}
{% else %}
{{ adoption_notice.location_string }}
{% endif %}</p>
</div>
<div class="cell">
{% include "fellchensammlung/partials/bulma-sex-overview.html" %}
</div>
</div>
</div>
</div>
<div class="columns">
<!--- Images --->
<div class="column block">
<div class="card">
<div class="grid card-content">
<div class="cell" id="my-gallery">
{% for photo in adoption_notice.get_photos %}
<a href="{{ MEDIA_URL }}/{{ photo.image }}"
data-pswp-width="{{ photo.image.width }}"
data-pswp-height="{{ photo.image.height }}"
target="_blank">
<img style="height: 12rem" src="{{ MEDIA_URL }}/{{ photo.image }}"
alt="{ photo.alt_text }}"/>
</a>
{% endfor %}
</div>
</div>
</div>
</div>
<!--- Description --->
<div class="column block">
<div class="card">
<div class="card-header">
<h1 class="card-header-title title is-2">{% translate "Beschreibung" %}</h1>
</div>
<div class="card-content">
<p class="expandable">{% if adoption_notice.description %}
{{ adoption_notice.description | render_markdown }}
{% else %}
{% translate "Keine Beschreibung angegeben" %}
{% endif %}
</p>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer">
{% if has_edit_permission %}
<div class="card-footer-item">
<div class="column">
<a class="button is-primary is-light"
href="{% url 'adoption-notice-add-photo' adoption_notice_id=adoption_notice.pk %}">
{% translate 'Foto hinzufügen' %}
</a>
</div>
<div class="card-footer-item">
<a class="button is-primary"
href="{% url 'adoption-notice-edit' adoption_notice_id=adoption_notice.pk %}">
{% translate 'Bearbeiten' %}
</a>
</div>
</div>
{% endif %}
</div>
</div>
<div class="columns">
{% for animal in adoption_notice.animals %}
<div class="column">
{% include "fellchensammlung/partials/bulma-partial-animal-card.html" %}
</div>
{% endfor %}
</div>
<div class="block">
{% if adoption_notice.further_information %}
<form method="get" action="{% url 'external-site' %}">
<input type="hidden" name="url" value="{{ adoption_notice.further_information }}">
<button class="button is-primary is-fullwidth" type="submit" id="submit">
{{ adoption_notice.further_information | domain }} <i
class="fa-solid fa-arrow-up-right-from-square"></i>
</button>
</form>
{% endif %}
</div>
<div class="block">
{% include "fellchensammlung/partials/bulma-partial-comment-section.html" %}
</div>
{% endblock %}

View File

@@ -1,95 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load widget_tweaks %}
{% block title %}<title>{% translate "Vermittlung hinzufügen" %}</title>{% endblock %}
{% block content %}
<h1>{% translate "Vermitteln" %}</h1>
<div class="notification">
<button class="delete"></button>
<p>
{% url 'terms-of-service' as rules_url %}
{% trans "Regeln" as rules_text %}
{% blocktranslate with rules_link='<a href="'|add:rules_url|add:'">'|add:rules_text|add:'</a>'|safe %}
Bitte mach dich zunächst mit unseren {{ rules_link }} vertraut. Dann trage hier die ersten Informationen
ein.
Fotos kannst du im nächsten Schritt hinzufügen.
{% endblocktranslate %}
</p>
</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="field">
<label class="label" for="an-name">{{ form.name.label }}
{% if form.name.field.required %}<span class="special_class">*</span>{% endif %}</label>
{{ form.name|add_class:"input"|attr:"id:an-name" }}
</div>
<div class="field">
<label class="label" for="an-description">{% translate 'Beschreibung' %}</label>
{{ form.description|add_class:"input textarea"|attr:"rows:3"|attr:"id:an-description" }}
</div>
<div class="field">
<label class="label" for="an-location">{{ form.location_string.label }}</label>
{{ form.location_string|add_class:"input"|attr:"id:an-location" }}
</div>
<div class="field">
<label class="checkbox" for="an-group-only">{{ form.group_only.label }}</label>
{{ form.group_only|add_class:"checkbox"|attr:"id:an-group-only" }}
</div>
<div class="field">
<label class="label" for="an-searching-since">{{ form.searching_since.label }}</label>
{{ form.searching_since|add_class:"input"|attr:"id:an-searching-since"|attr:"type:date" }}
</div>
<div class="notification">
<button class="delete"></button>
<p>
{% blocktranslate %}
Gibt hier schonmal erste Details zu den Tieren an.
Wenn du Details und Fotos zu den Tieren hinzufügen willst oder ihr Geschlecht und Geburtsdatum
anpassen
willst,
kannst du das im nächsten Schritt tun.
{% endblocktranslate %}
</p>
</div>
<div class="field">
<label class="label" for="an-species">{% translate 'Tierart' %}</label>
<div class="select">
{{ form.species|attr:"id:an-species" }}
</div>
</div>
<div class="field">
<label class="label" for="an-num-animals">{{ form.num_animals.label }}</label>
{{ form.num_animals|add_class:"input"|attr:"id:an-num-animals" }}
</div>
<div class="field">
<label class="label" for="an-sex">{% translate 'Geschlecht' %}</label>
<div class="select">
{{ form.sex|attr:"id:an-sex" }}
</div>
</div>
<div class="field">
<label class="label" for="an-date-of-birth">{{ form.date_of_birth.label }}</label>
{{ form.date_of_birth|add_class:"input"|attr:"id:an-date-of-birth"|attr:"type:date" }}
</div>
<input class="button is-primary" type="submit" value="{% translate "Speichern" %}">
</form>
{% endblock %}

View File

@@ -1,15 +0,0 @@
{% load i18n %}
{% load crispy_forms_tags %}
<div class="card">
<div class="card-header">
<div class="card-header-title">
{% blocktrans %}
Als {{ user }} kommentieren
{% endblocktrans %}
</div>
</div>
<div class="card-content">
{% crispy comment_form %}
</div>
</div>

View File

@@ -1,5 +1,5 @@
{% load i18n %} {% load i18n %}
<form class="btn2 select" action="{% url 'change-language' %}" method="post" onchange='this.form.submit()'> <form class="btn2" action="{% url 'change-language' %}" method="post" onchange='this.form.submit()'>
{% csrf_token %} {% csrf_token %}
<select name="language" onchange='this.form.submit()'> <select name="language" onchange='this.form.submit()'>
{% get_current_language as LANGUAGE_CODE_CURRENT %} {% get_current_language as LANGUAGE_CODE_CURRENT %}

View File

@@ -1,26 +0,0 @@
<!--- See https://docs.djangoproject.com/en/5.2/topics/forms/#reusable-form-templates -->
{% load custom_tags %}
{% for field in form %}
<div class="field">
<label class="label">
{{ field.label }}
</label>
<div class="control">
{% if field|widget_type == 'TextInput' %}
{{ field|add_class:"input" }}
{% elif field|widget_type == 'Select' %}
<div class="select">
{{ field }}
</div>
{% else %}
{{ field|add_class:"input" }}
{% endif %}
</div>
<div class="help is-danger">
{{ field.errors }}
</div>
</div>
{% endfor %}

View File

@@ -3,9 +3,7 @@
<section class="header"> <section class="header">
<div> <div>
<a href="{% url "index" %}" class="logo"> <a href="{% url "index" %}" class="logo"><img src={% static 'fellchensammlung/img/logo_transparent.png' %}></a>
<img src="{% static 'fellchensammlung/img/logo_transparent.png' %}" alt="{% trans 'Notfellchen Logo' %}">
</a>
</div> </div>
<div class="profile-card"> <div class="profile-card">
@@ -29,7 +27,7 @@
</form> </form>
{% else %} {% else %}
<a class="btn2" href="{% url "django_registration_register" %}">{% translate "Registrieren" %}</a> <a class="btn2" href="{% url "django_registration_register" %}">{% translate "Registrieren" %}</a>
<a class="btn2" href="{% url "login" %}"><i class="fa fa-sign-in" aria-label="Login"></i></a> <a class="btn2" href="{% url "login" %}"><i class="fa fa-sign-in" aria-hidden="true"></i></a>
{% endif %} {% endif %}
<input id="menu-toggle" type="checkbox"/> <input id="menu-toggle" type="checkbox"/>
<label class='menu-button-container' for="menu-toggle"> <label class='menu-button-container' for="menu-toggle">

View File

@@ -1,12 +0,0 @@
{% load i18n %}
{% if adoption_notices %}
<div class="grid">
{% for adoption_notice in adoption_notices %}
<div class="cell">
{% include "fellchensammlung/partials/bulma-partial-adoption-notice-minimal.html" %}
</div>
{% endfor %}
</div>
{% else %}
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
{% endif %}

View File

@@ -1,5 +0,0 @@
<div class="container-cards">
{% for rule in rules %}
{% include "fellchensammlung/partials/bulma-partial-rule.html" %}
{% endfor %}
</div>

View File

@@ -1,6 +1,6 @@
{% extends "fellchensammlung/base_generic.html" %} {% extends "fellchensammlung/base_generic.html" %}
{% load i18n %} {% load i18n %}
{% block title %}<title>{% translate "Karte" %}</title>{% endblock %} {% block title %}<title>{% translate "Karte" %}</title> %}
{% block content %} {% block content %}
<div class="card"> <div class="card">

View File

@@ -1,40 +0,0 @@
{% load custom_tags %}
{% load i18n %}
<h2 class="heading-card-adoption-notice title is-4">
<a href="{{ adoption_notice.get_absolute_url_bulma }}"> {{ adoption_notice.name }}</a>
</h2>
<div class="grid mb-0">
<div class="cell">
<!--- General Information --->
<div class="grid">
<div class="cell">
<p>
<b><i class="fa-solid fa-location-dot"></i></b>
{% if adoption_notice.location %}
{{ adoption_notice.location }}
{% else %}
{{ adoption_notice.location_string }}
{% endif %}</p>
</div>
<div class="cell">
{% include "fellchensammlung/partials/bulma-sex-overview.html" %}
</div>
</div>
</div>
</div>
{% if adoption_notice.get_photo %}
<div class="adoption-notice-img img-small">
<img src="{{ MEDIA_URL }}/{{ adoption_notice.get_photo.image }}"
alt="{{ adoption_notice.get_photo.alt_text }}">
</div>
{% endif %}

View File

@@ -1,42 +0,0 @@
{% load custom_tags %}
{% load i18n %}
<div class="card">
<div class="header-card-adoption-notice">
<h2 class="heading-card-adoption-notice title is-4">
<a href="{{ adoption_notice.get_absolute_url_bulma }}"> {{ adoption_notice.name }}</a>
</h2>
</div>
<div class="grid">
<div class="cell">
<!--- General Information --->
<div class="grid">
<div class="cell">
<p>
<b><i class="fa-solid fa-location-dot"></i></b>
{% if adoption_notice.location %}
{{ adoption_notice.location }}
{% else %}
{{ adoption_notice.location_string }}
{% endif %}</p>
</div>
<div class="cell">
{% include "fellchensammlung/partials/bulma-sex-overview.html" %}
</div>
</div>
</div>
</div>
{% if adoption_notice.get_photo %}
<div class="adoption-notice-img img-small">
<img src="{{ MEDIA_URL }}/{{ adoption_notice.get_photo.image }}"
alt="{{ adoption_notice.get_photo.alt_text }}">
</div>
{% endif %}
</div>

View File

@@ -1,38 +0,0 @@
{% load i18n %}
{% load custom_tags %}
<div class="card">
<div class="card-header">
<h1 class="card-header-title">
<a href="{% url 'animal-detail' animal_id=animal.pk %}">{{ animal.name }}</a>
</h1>
<div class="tags">
<div class="tag species">{{ animal.species }}</div>
<div class="tag sex">{{ animal.get_sex_display }}</div>
</div>
</div>
<div class="card-content">
{% if animal.description %}
<p>{{ animal.description | render_markdown }}</p>
{% endif %}
<div class="cell" id="my-gallery">
{% for photo in animal.get_photos %}
<a href="{{ MEDIA_URL }}/{{ photo.image }}"
data-pswp-width="{{ photo.image.width }}"
data-pswp-height="{{ photo.image.height }}"
target="_blank">
<img src="{{ MEDIA_URL }}/{{ photo.image }}" alt="{{ photo.alt_text }}">
</a>
{% endfor %}
</div>
<!--- Assume a user does not have edit permissions on animal if they have no other edit permission --->
{% if has_edit_permission %}
<div class="card-footer">
<a class="card-footer-item button" href="{% url 'animal-edit' animal_id=animal.pk %}">{% translate 'Bearbeiten' %}</a>
<a class="card-footer-item button"
href="{% url 'animal-add-photo' animal_id=animal.pk %}">{% translate 'Foto hinzufügen' %}</a>
</div>
{% endif %}
</div>
</div>

View File

@@ -1,25 +0,0 @@
{% load i18n %}
<div class="card">
<div class="card-header">
<h2 class="card-header-title title is-2">{% translate 'Kommentare' %}</h2>
</div>
<div class="card-content">
{% if adoption_notice.comments %}
{% for comment in adoption_notice.comments %}
{% include "fellchensammlung/partials/bulma-partial-comment.html" %}
{% endfor %}
{% else %}
<p class="is-italic">{% translate 'Noch keine Kommentare' %}</p>
{% endif %}
</div>
<footer class="card-footer">
{% if user.is_authenticated %}
{% include "fellchensammlung/forms/bulma-form-comment.html" %}
{% else %}
<p class="card-footer-item">
{% translate 'Du musst dich einloggen um Kommentare zu hinterlassen' %}
</p>
{% endif %}
</footer>
</div>

View File

@@ -1,27 +0,0 @@
{% load i18n %}
{% load custom_tags %}
<div class="card">
<div class="card-header">
<div class="card-header-title content">
<b class="">{{ comment.user }}</b> <span class="tag"><time class="">{{ comment.created_at }}</time></span>
</div>
</div>
<div class="card-content">
<p class="content">
{{ comment.text | render_markdown }}
</p>
</div>
<div class="card-footer">
<a class="card-footer-item is-danger" href="{{ comment.get_report_url }}">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-flag"></i>
</span>
<span>{% trans 'Melden' %}</span>
</span>
</a>
</div>
</div>

View File

@@ -1,149 +0,0 @@
{% load static %}
{% load custom_tags %}
{% load i18n %}
<!-- add MapLibre JavaScript and CSS -->
<script src="{% settings_value "MAP_TILE_SERVER" %}/assets/lib/maplibre-gl/maplibre-gl.js"></script>
<link href="{% settings_value "MAP_TILE_SERVER" %}/assets/lib/maplibre-gl/maplibre-gl.css" rel="stylesheet"/>
<!-- add Turf see https://maplibre.org/maplibre-gl-js/docs/examples/draw-a-radius/ -->
<script src="{% static 'fellchensammlung/js/turf.min.js' %}"></script>
<!-- add container for the map -->
<div id="map" class="map"></div>
<!-- start map -->
<script>
{% if zoom_level %}
var zoom_level = {{ zoom_level }};
{% else %}
var zoom_level = 4;
{% endif %}
{% if map_center %}
var map_center = [{{ map_center.longitude | pointdecimal }}, {{ map_center.latitude | pointdecimal }}];
{% else %}
var map_center = [10.49, 50.68]; <!-- Point middle of Germany -->
zoom_level = 4; //Overwrite zoom level when no place is found
{% endif %}
let map = new maplibregl.Map({
container: 'map',
style: '{% static "fellchensammlung/map/styles/colorful/style.json" %}',
center: map_center,
zoom: zoom_level
});
map.addControl(new maplibregl.FullscreenControl());
map.addControl(new maplibregl.NavigationControl({showCompass: false}));
{% for adoption_notice in adoption_notices_map %}
{% if adoption_notice.location %}
// create the popup
const popup_{{ forloop.counter }} = new maplibregl.Popup({offset: 25}).setHTML(`{% include "fellchensammlung/partials/bulma-partial-adoption-notice-minimal-map.html" %}`);
// create DOM element for the marker
const el_{{ forloop.counter }} = document.createElement('div');
el_{{ forloop.counter }}.id = 'marker_{{ forloop.counter }}';
el_{{ forloop.counter }}.classList.add('marker');
const location_popup_{{ forloop.counter }} = [{{ adoption_notice.location.longitude | pointdecimal }}, {{ adoption_notice.location.latitude | pointdecimal }}];
// create the marker
new maplibregl.Marker({element: el_{{ forloop.counter }}})
.setLngLat(location_popup_{{ forloop.counter }})
.setPopup(popup_{{ forloop.counter }}) // sets a popup on this marker
.addTo(map);
{% endif %}
{% endfor %}
{% for rescue_organization in rescue_organizations %}
{% if rescue_organization.location %}
// create the popup
const popup_{{ forloop.counter }} = new maplibregl.Popup({offset: 25}).setHTML(`{% include "fellchensammlung/partials/partial-rescue-organization.html" %}`);
// create DOM element for the marker
const el_{{ forloop.counter }} = document.createElement('div');
el_{{ forloop.counter }}.id = 'marker_{{ forloop.counter }}';
el_{{ forloop.counter }}.classList.add('animal-shelter-marker', 'marker');
const location_popup_{{ forloop.counter }} = [{{ rescue_organization.location.longitude | pointdecimal }}, {{ rescue_organization.location.latitude | pointdecimal }}];
// create the marker
new maplibregl.Marker({element: el_{{ forloop.counter }}})
.setLngLat(location_popup_{{ forloop.counter }})
.setPopup(popup_{{ forloop.counter }}) // sets a popup on this marker
.addTo(map);
{% endif %}
{% endfor %}
map.on('load', async () => {
image = await map.loadImage('{% static "fellchensammlung/img/pin.png" %}');
map.addImage('pin', image.data);
{% for map_pin in map_pins %}
map.addSource('point_{{ forloop.counter }}', {
'type': 'geojson',
'data': {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [{{ map_pin.location.longitude | pointdecimal }}, {{ map_pin.location.latitude | pointdecimal }}]
}
}
]
}
});
map.addLayer({
'id': 'point_{{ forloop.counter }}',
'type': 'circle',
'source': 'point_{{ forloop.counter }}',
'paint': {
'circle-radius': 18,
'circle-color': '#ff878980'
}
});
{% endfor %}
});
{% if search_center %}
var search_center = [{{ search_center.longitude | pointdecimal }}, {{ search_center.latitude | pointdecimal }}];
map.on('load', () => {
const radius = {{ search_radius }}; // kilometer
const options = {
steps: 64,
units: 'kilometers'
};
const circle = turf.circle(search_center, radius, options);
// Add the circle as a GeoJSON source
map.addSource('location-radius', {
type: 'geojson',
data: circle
});
// Add a fill layer with some transparency
map.addLayer({
id: 'location-radius',
type: 'fill',
source: 'location-radius',
paint: {
'fill-color': 'rgba(140,207,255,0.3)',
'fill-opacity': 0.5
}
});
// Add a line layer to draw the circle outline
map.addLayer({
id: 'location-radius-outline',
type: 'line',
source: 'location-radius',
paint: {
'line-color': '#0094ff',
'line-width': 3
}
});
});
{% endif %}
</script>

View File

@@ -1,9 +0,0 @@
{% load custom_tags %}
<div class="card">
<div class="card-header">
<h2 class="card-header-title">{{ rule.title }}</h2>
</div>
<div class="card-content">
<p class="content">{{ rule.rule_text | render_markdown }}</p>
</div>
</div>

View File

@@ -1,44 +0,0 @@
{% load static %}
{% load i18n %}
<div class="grid">
{% if adoption_notice.num_per_sex.F > 0 %}
<span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.F }} x </span>
<span class="icon">
<img class="icon" src="{% static 'fellchensammlung/img/sexes/Female.png' %}"
alt="{% translate 'weibliche Tiere' %}">
</span>
</span>
{% endif %}
{% if adoption_notice.num_per_sex.I > 0 %}
<span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.I }}</span>
<span class="icon">
<img class="icon"
src="{% static 'fellchensammlung/img/sexes/Intersex.png' %}"
alt="{% translate 'intersexuelle Tiere' %}">
</span>
</span>
{% endif %}
{% if adoption_notice.num_per_sex.M > 0 %}
<span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.M }}</span>
<span class="icon">
<img class="icon" src="{% static 'fellchensammlung/img/sexes/Male.png' %}"
alt="{% translate 'männliche Tiere' %}">
</span>
</span>
{% endif %}
{% if adoption_notice.num_per_sex.M_N > 0 %}
<span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.M_N }}</span>
<span class="icon">
<img class="icon"
src="{% static 'fellchensammlung/img/sexes/Male Neutered.png' %}"
alt="{% translate 'männlich, kastrierte Tiere' %}">
</span>
</span>
{% endif %}
</div>

View File

@@ -1,21 +0,0 @@
{% load i18n %}
{% load custom_tags %}
<div class="card">
<h1>
<a href="{{ rescue_org.get_absolute_url }}">{{ rescue_org.name }}</a>
</h1>
<i>{% translate 'Zuletzt geprüft:' %} {{ rescue_org.last_checked_hr }}</i>
{% if rescue_org.website %}
<p>{% translate "Website" %}: <a href="{{ rescue_org.website }}">{{ rescue_org.website }}</a></p>
{% endif %}
<div class="container-edit-buttons">
<form method="post">
{% csrf_token %}
<input type="hidden"
name="rescue_organization_id"
value="{{ rescue_org.pk }}">
<input type="hidden" name="action" value="checked">
<button class="btn" type="submit">{% translate "Organisation geprüft" %}</button>
</form>
</div>
</div>

View File

@@ -3,8 +3,8 @@
{% load i18n %} {% load i18n %}
<!-- add MapLibre JavaScript and CSS --> <!-- add MapLibre JavaScript and CSS -->
<script src="{% settings_value "MAP_TILE_SERVER" %}/assets/lib/maplibre-gl/maplibre-gl.js"></script> <script src="{% settings_value "MAP_TILE_SERVER" %}/assets/maplibre-gl/maplibre-gl.js"></script>
<link href="{% settings_value "MAP_TILE_SERVER" %}/assets/lib/maplibre-gl/maplibre-gl.css" rel="stylesheet"/> <link href="{% settings_value "MAP_TILE_SERVER" %}/assets/maplibre-gl/maplibre-gl.css" rel="stylesheet"/>
<!-- add Turf see https://maplibre.org/maplibre-gl-js/docs/examples/draw-a-radius/ --> <!-- add Turf see https://maplibre.org/maplibre-gl-js/docs/examples/draw-a-radius/ -->
<script src="{% static 'fellchensammlung/js/turf.min.js' %}"></script> <script src="{% static 'fellchensammlung/js/turf.min.js' %}"></script>
@@ -29,7 +29,7 @@
let map = new maplibregl.Map({ let map = new maplibregl.Map({
container: 'map', container: 'map',
style: '{% static "fellchensammlung/map/styles/colorful/style.json" %}', style: '{% static "fellchensammlung/map/styles/colorful.json" %}',
center: map_center, center: map_center,
zoom: zoom_level zoom: zoom_level
}).addControl(new maplibregl.NavigationControl()); }).addControl(new maplibregl.NavigationControl());
@@ -76,7 +76,7 @@
image = await map.loadImage('{% static "fellchensammlung/img/pin.png" %}'); image = await map.loadImage('{% static "fellchensammlung/img/pin.png" %}');
map.addImage('pin', image.data); map.addImage('pin', image.data);
{% for map_pin in map_pins %} {% for map_pin in map_pins %}
map.addSource('point_{{ forloop.counter }}', { map.addSource('point', {
'type': 'geojson', 'type': 'geojson',
'data': { 'data': {
'type': 'FeatureCollection', 'type': 'FeatureCollection',
@@ -91,16 +91,16 @@
] ]
} }
}); });
map.addLayer({
'id': 'point_{{ forloop.counter }}',
'type': 'circle',
'source': 'point_{{ forloop.counter }}',
'paint': {
'circle-radius': 18,
'circle-color': '#ff878980'
}
});
{% endfor %} {% endfor %}
map.addLayer({
'id': 'pints',
'type': 'symbol',
'source': 'point',
'layout': {
'icon-image': 'pin',
'icon-size': 0.1
}
});
}); });
{% if search_center %} {% if search_center %}

View File

@@ -1,7 +1,9 @@
{% load i18n %} {% load i18n %}
<div class="report card"> <div class="report card">
<h2> <h2>
{% translate 'Meldung von ' %} <a href="{{ report.reported_content_url }}"><i>{{ report.reported_content }}</i></a> {% blocktranslate %}
Meldung von {{ report.reported_content }}
{% endblocktranslate %}
</h2> </h2>
{% if report.reported_broken_rules %} {% if report.reported_broken_rules %}
{% translate "Regeln gegen die Verstoßen wurde" %} {% translate "Regeln gegen die Verstoßen wurde" %}
@@ -11,25 +13,19 @@
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
<p> <p><b>{% translate "Kommentar zur Meldung" %}:</b>
{% if report.user_comment %} {{ report.user_comment }}
<b>{% translate "Kommentar zur Meldung" %}:</b> {{ report.user_comment }}
{% else %}
<i>{% translate 'Es wurde kein Kommentar zur Meldung hinzugefügt.' %}</i>
{% endif %}
</p> </p>
{% if is_mod_or_above %} <div class="container-edit-buttons">
<div class="container-edit-buttons"> <form action="allow" class="">
<form action="allow" class=""> {% csrf_token %}
{% csrf_token %} <input type="hidden" name="report_id" value="{{ report.pk }}">
<input type="hidden" name="report_id" value="{{ report.pk }}"> <button class="btn allow" type="submit">{% translate "Inhalt genehmigen" %}</button>
<button class="btn allow" type="submit">{% translate "Inhalt genehmigen" %}</button> </form>
</form> <form action="disallow" class="">
<form action="disallow" class=""> {% csrf_token %}
{% csrf_token %} <input type="hidden" name="report_id" value="{{ report.pk }}">
<input type="hidden" name="report_id" value="{{ report.pk }}"> <button class="btn allow" type="submit">{% translate "Inhalt als gesperrt kennzeichnen" %}</button>
<button class="btn allow" type="submit">{% translate "Inhalt als gesperrt kennzeichnen" %}</button> </form>
</form> </div>
</div>
{% endif %}
</div> </div>

View File

@@ -1,12 +0,0 @@
{% extends "fellchensammlung/base_generic.html" %}
{% load i18n %}
{% block content %}
<h1>{% translate "Aktualitätscheck" %}</h1>
<p>{% translate "Überprüfe ob im Tierheim neue Vermittlungen ein Zuhause suchen" %}</p>
<div class="container-cards spaced">
<h1>{% translate 'Organisation zur Überprüfung' %}</h1>
{% for rescue_org in rescue_orgs %}
{% include "fellchensammlung/partials/partial-check-rescue-org.html" %}
{% endfor %}
</div>
{% endblock %}

View File

@@ -11,7 +11,7 @@
<input type="hidden" name="longitude" maxlength="200" id="longitude"> <input type="hidden" name="longitude" maxlength="200" id="longitude">
<input type="hidden" name="latitude" maxlength="200" id="latitude"> <input type="hidden" name="latitude" maxlength="200" id="latitude">
<input type="hidden" id="place_id" name="place_id"> <input type="hidden" id="place_id" name="place_id">
{{ search_form }} {{ search_form.as_p }}
<ul id="results"></ul> <ul id="results"></ul>
<div class="container-edit-buttons"> <div class="container-edit-buttons">
<button class="btn" type="submit" value="search" name="search"> <button class="btn" type="submit" value="search" name="search">

View File

@@ -1,35 +0,0 @@
{% extends "fellchensammlung/base_generic.html" %}
{% load i18n %}
{% load static %}
{% block title %}<title>{% translate "Styleguide" %}</title>{% endblock %}
{% block content %}
<h1>This is a heading</h1>
<p>And this is a short paragraph below</p>
<div class="container-cards">
<h2>Card Containers</h2>
<div class="card">
<h3>I am a card</h3>
<p>Cards are responsive. Use them to display multiple items of the same category</p>
</div>
<div class="card">
<h3>Photos</h3>
<p>Cards are responsive. Use them to display multiple items of the same category</p>
<img src="{% static 'fellchensammlung/img/example_rat_single.png' %}" alt="A rat sitting on a wooden house">
</div>
</div>
<div class="container-cards">
<form class="form-search card half" method="post">
<label for="inputA">Input Alpha</label>
<input name="inputA" maxlength="200" id="inputA">
<label for="inputB">Beta</label>
<input name="inputB" maxlength="200" id="inputB">
<label for="id_location_string">Ort</label>
<input name="location_string" id="id_location_string">
</form>
<div class="card half">
{% include "fellchensammlung/partials/partial-map.html" %}
</div>
</div>
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
{% endblock %}

View File

@@ -49,7 +49,6 @@ def get_oxitraffic_script_if_enabled():
else: else:
return "" return ""
@register.filter @register.filter
@stringfilter @stringfilter
def pointdecimal(value): def pointdecimal(value):
@@ -58,7 +57,6 @@ def pointdecimal(value):
except ValueError: except ValueError:
return value return value
@register.filter @register.filter
@stringfilter @stringfilter
def domain(url): def domain(url):
@@ -70,17 +68,6 @@ def domain(url):
except ValueError: except ValueError:
return url return url
@register.simple_tag @register.simple_tag
def settings_value(name): def settings_value(name):
return getattr(settings, name) return getattr(settings, name)
@register.filter(name='add_class')
def add_class(field, css_class):
return field.as_widget(attrs={"class": css_class})
@register.filter
def widget_type(field):
return field.field.widget.__class__.__name__

View File

@@ -74,21 +74,13 @@ class GeoFeature:
geofeatures = [] geofeatures = []
for feature in result["features"]: for feature in result["features"]:
geojson = {} geojson = {}
# Necessary features
geojson['place_id'] = feature["properties"]["osm_id"]
geojson['lat'] = feature["geometry"]["coordinates"][1]
geojson['lon'] = feature["geometry"]["coordinates"][0]
try: try:
geojson['name'] = feature["properties"]["name"] geojson['name'] = feature["properties"]["name"]
except KeyError: except KeyError:
geojson['name'] = feature["properties"]["osm_id"] geojson['name'] = feature["properties"]["street"]
geojson['place_id'] = feature["properties"]["osm_id"]
optional_keys = ["housenumber", "street", "city", "postcode", "county", "countrycode"] geojson['lat'] = feature["geometry"]["coordinates"][1]
for key in optional_keys: geojson['lon'] = feature["geometry"]["coordinates"][0]
try:
geojson[key] = feature["properties"][key]
except KeyError:
pass
geofeatures.append(geojson) geofeatures.append(geojson)
return geofeatures return geofeatures
@@ -169,7 +161,6 @@ class LocationProxy:
""" """
self.geo_api = GeoAPI() self.geo_api = GeoAPI()
geofeatures = self.geo_api.get_geojson_for_query(location_string) geofeatures = self.geo_api.get_geojson_for_query(location_string)
if geofeatures is None: if geofeatures is None:
raise ValueError raise ValueError
result = geofeatures[0] result = geofeatures[0]
@@ -177,12 +168,6 @@ class LocationProxy:
self.place_id = result["place_id"] self.place_id = result["place_id"]
self.latitude = result["lat"] self.latitude = result["lat"]
self.longitude = result["lon"] self.longitude = result["lon"]
optional_keys = ["housenumber", "street", "city", "postcode", "county", "countrycode"]
for key in optional_keys:
try:
self.__setattr__(key, result[key])
except KeyError:
self.__setattr__(key, None)
def __eq__(self, other): def __eq__(self, other):
return self.place_id == other.place_id return self.place_id == other.place_id

View File

@@ -1,23 +0,0 @@
from django.utils import translation
from fellchensammlung.models import Language, Text
def get_text_by_language(text_code, lang=None):
if lang is None:
language_code = translation.get_language()
lang = Language.objects.get(languagecode=language_code)
return Text.objects.get(text_code=text_code, language=lang, )
def get_texts_by_language(text_codes):
language_code = translation.get_language()
lang = Language.objects.get(languagecode=language_code)
texts = {}
for text_code in text_codes:
try:
texts[text_code] = get_text_by_language(text_code, lang)
except Text.DoesNotExist:
texts[text_code] = None
return texts

View File

@@ -6,7 +6,7 @@ from ..forms import AdoptionNoticeSearchForm
from ..models import SearchSubscription, AdoptionNotice, AdoptionNoticeNotification, SexChoicesWithAll, Location from ..models import SearchSubscription, AdoptionNotice, AdoptionNoticeNotification, SexChoicesWithAll, Location
def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: bool = True): def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active : bool = True):
""" """
This functions checks for all search subscriptions if the new adoption notice fits the search. This functions checks for all search subscriptions if the new adoption notice fits the search.
If the new adoption notice fits the search subscription, it sends a notification to the user that created the search. If the new adoption notice fits the search subscription, it sends a notification to the user that created the search.
@@ -36,7 +36,7 @@ class Search:
self.sex = None self.sex = None
self.area_search = None self.area_search = None
self.max_distance = None self.max_distance = None
self.location = None # Can either be Location (DjangoModel) or LocationProxy self.location = None # Can either be Location (DjangoModel) or LocationProxy
self.place_not_found = False # Indicates that a location was given but could not be geocoded self.place_not_found = False # Indicates that a location was given but could not be geocoded
self.search_form = None self.search_form = None
# Either place_id or location string must be set for area search # Either place_id or location string must be set for area search
@@ -47,6 +47,7 @@ class Search:
elif search_subscription: elif search_subscription:
self.search_from_search_subscription(search_subscription) self.search_from_search_subscription(search_subscription)
def __str__(self): def __str__(self):
return f"Search: {self.sex=}, {self.location=}, {self.area_search=}, {self.max_distance=}" return f"Search: {self.sex=}, {self.location=}, {self.area_search=}, {self.max_distance=}"
@@ -92,6 +93,7 @@ class Search:
return False return False
return True return True
def get_adoption_notices(self): def get_adoption_notices(self):
adoptions = AdoptionNotice.objects.order_by("-created_at") adoptions = AdoptionNotice.objects.order_by("-created_at")
# Filter for active adoption notices # Filter for active adoption notices
@@ -116,21 +118,13 @@ class Search:
else: else:
self.search_form = AdoptionNoticeSearchForm() self.search_form = AdoptionNoticeSearchForm()
def search_from_predefined_i_location(self, i_location, max_distance=100):
self.sex = SexChoicesWithAll.ALL
self.location = i_location.location
self.area_search = True
self.search_form = AdoptionNoticeSearchForm(initial={"location_string": self.location.name,
"max_distance": max_distance,
"sex": SexChoicesWithAll.ALL})
self.max_distance = max_distance
def search_from_search_subscription(self, search_subscription: SearchSubscription): def search_from_search_subscription(self, search_subscription: SearchSubscription):
self.sex = search_subscription.sex self.sex = search_subscription.sex
self.location = search_subscription.location self.location = search_subscription.location
self.area_search = True self.area_search = True
self.max_distance = search_subscription.max_distance self.max_distance = search_subscription.max_distance
def subscribe(self, user): def subscribe(self, user):
logging.info(f"{user} subscribed to search") logging.info(f"{user} subscribed to search")
if isinstance(self.location, LocationProxy): if isinstance(self.location, LocationProxy):

View File

@@ -7,18 +7,8 @@ from .feeds import LatestAdoptionNoticesFeed
from . import views from . import views
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
from django.contrib.sitemaps.views import sitemap
from .sitemap import StaticViewSitemap, AdoptionNoticeSitemap, AnimalSitemap
sitemaps = {
"static": StaticViewSitemap,
"vermittlungen": AdoptionNoticeSitemap,
"tiere": AnimalSitemap,
}
urlpatterns = [ urlpatterns = [
path("", views.index, name="index"), path("", views.index, name="index"),
path("bulma/", views.index_bulma, name="index-bulma"),
path("rss/", LatestAdoptionNoticesFeed(), name="rss"), path("rss/", LatestAdoptionNoticesFeed(), name="rss"),
path("metrics/", views.metrics, name="metrics"), path("metrics/", views.metrics, name="metrics"),
# ex: /animal/5/ # ex: /animal/5/
@@ -29,15 +19,12 @@ urlpatterns = [
path("tier/<int:animal_id>/add-photo", views.add_photo_to_animal, name="animal-add-photo"), path("tier/<int:animal_id>/add-photo", views.add_photo_to_animal, name="animal-add-photo"),
# ex: /adoption_notice/7/ # ex: /adoption_notice/7/
path("vermittlung/<int:adoption_notice_id>/", views.adoption_notice_detail, name="adoption-notice-detail"), path("vermittlung/<int:adoption_notice_id>/", views.adoption_notice_detail, name="adoption-notice-detail"),
path("bulma/vermittlung/<int:adoption_notice_id>/", views.adoption_notice_detail_bulma, name="adoption-notice-detail-bulma"),
# ex: /adoption_notice/7/edit # ex: /adoption_notice/7/edit
path("vermittlung/<int:adoption_notice_id>/edit", views.adoption_notice_edit, name="adoption-notice-edit"), path("vermittlung/<int:adoption_notice_id>/edit", views.adoption_notice_edit, name="adoption-notice-edit"),
# ex: /vermittlung/5/add-photo # ex: /vermittlung/5/add-photo
path("vermittlung/<int:adoption_notice_id>/add-photo", views.add_photo_to_adoption_notice, path("vermittlung/<int:adoption_notice_id>/add-photo", views.add_photo_to_adoption_notice, name="adoption-notice-add-photo"),
name="adoption-notice-add-photo"),
# ex: /adoption_notice/2/add-animal # ex: /adoption_notice/2/add-animal
path("vermittlung/<int:adoption_notice_id>/add-animal", views.adoption_notice_add_animal, path("vermittlung/<int:adoption_notice_id>/add-animal", views.adoption_notice_add_animal, name="adoption-notice-add-animal"),
name="adoption-notice-add-animal"),
path("tierschutzorganisationen/", views.list_rescue_organizations, name="rescue-organizations"), path("tierschutzorganisationen/", views.list_rescue_organizations, name="rescue-organizations"),
path("organisation/<int:rescue_organization_id>/", views.detail_view_rescue_organization, path("organisation/<int:rescue_organization_id>/", views.detail_view_rescue_organization,
@@ -45,21 +32,12 @@ urlpatterns = [
# ex: /search/ # ex: /search/
path("suchen/", views.search, name="search"), path("suchen/", views.search, name="search"),
path("bulma/suchen/", views.search_bulma, name="search-bulma"),
path("suchen/<slug:important_location_slug>", views.search_important_locations, name="search-by-location"),
# ex: /map/ # ex: /map/
path("map/", views.map, name="map"), path("map/", views.map, name="map"),
# ex: /map/
path("bulma/map/", views.map_bulma, name="map-bulma"),
# ex: /vermitteln/ # ex: /vermitteln/
path("vermitteln/", views.add_adoption_notice, name="add-adoption"), path("vermitteln/", views.add_adoption_notice, name="add-adoption"),
path("bulma/vermitteln/", views.add_adoption_notice_bulma, name="add-adoption-bulma"),
path("ueber-uns/", views.about, name="about"), path("ueber-uns/", views.about, name="about"),
path("bulma/ueber-uns/", views.about_bulma, name="about-bulma"),
path("impressum/", views.imprint, name="imprint"),
path("terms-of-service/", views.terms_of_service, name="terms-of-service"),
path("datenschutz/", views.privacy, name="privacy"),
################ ################
## Moderation ## ## Moderation ##
@@ -70,11 +48,9 @@ urlpatterns = [
path("meldung/<uuid:report_id>/", views.report_detail, name="report-detail"), path("meldung/<uuid:report_id>/", views.report_detail, name="report-detail"),
path("meldung/<uuid:report_id>/sucess", views.report_detail_success, name="report-detail-success"), path("meldung/<uuid:report_id>/sucess", views.report_detail_success, name="report-detail-success"),
path("modqueue/", views.modqueue, name="modqueue"), path("modqueue/", views.modqueue, name="modqueue"),
path("updatequeue/", views.updatequeue, name="updatequeue"), path("updatequeue/", views.updatequeue, name="updatequeue"),
path("organization-check/", views.rescue_organization_check, name="organization-check"),
########### ###########
## USERS ## ## USERS ##
########### ###########
@@ -119,11 +95,4 @@ urlpatterns = [
################### ###################
path('external-site/', views.external_site_warning, name="external-site"), path('external-site/', views.external_site_warning, name="external-site"),
###############
## TECHNICAL ##
###############
path("sitemap.xml", sitemap, {"sitemaps": sitemaps}, name="django.contrib.sitemaps.views.sitemap"),
path("styleguide", views.styleguide, name="styleguide"),
path("styleguide-bulma", views.styleguide_bulma, name="styleguide-bulma"),
] ]

View File

@@ -2,8 +2,7 @@ import logging
from django.contrib.auth.views import redirect_to_login from django.contrib.auth.views import redirect_to_login
from django.http import HttpResponseRedirect, JsonResponse, HttpResponse from django.http import HttpResponseRedirect, JsonResponse, HttpResponse
from django.http.response import HttpResponseForbidden from django.shortcuts import render, redirect
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse from django.urls import reverse
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.utils import translation from django.utils import translation
@@ -18,14 +17,11 @@ from notfellchen import settings
from fellchensammlung import logger from fellchensammlung import logger
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \ from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
User, Location, AdoptionNoticeStatus, Subscriptions, CommentNotification, BaseNotification, RescueOrganization, \ User, Location, AdoptionNoticeStatus, Subscriptions, CommentNotification, BaseNotification, RescueOrganization, \
Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, AdoptionNoticeNotification, \ Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, AdoptionNoticeNotification
ImportantLocation
from .forms import AdoptionNoticeForm, AdoptionNoticeFormWithDateWidget, ImageForm, ReportAdoptionNoticeForm, \ from .forms import AdoptionNoticeForm, AdoptionNoticeFormWithDateWidget, ImageForm, ReportAdoptionNoticeForm, \
CommentForm, ReportCommentForm, AnimalForm, \ CommentForm, ReportCommentForm, AnimalForm, \
AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal, \ AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal
BulmaAdoptionNoticeForm
from .models import Language, Announcement from .models import Language, Announcement
from .tools import i18n
from .tools.geo import GeoAPI, zoom_level_for_radius from .tools.geo import GeoAPI, zoom_level_for_radius
from .tools.metrics import gather_metrics_data from .tools.metrics import gather_metrics_data
from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \ from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
@@ -66,22 +62,6 @@ def index(request):
return render(request, 'fellchensammlung/index.html', context=context) return render(request, 'fellchensammlung/index.html', context=context)
def index_bulma(request):
"""View function for home page of site."""
latest_adoption_list = AdoptionNotice.objects.filter(
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE).order_by("-created_at")
active_adoptions = [adoption for adoption in latest_adoption_list if adoption.is_active]
language_code = translation.get_language()
lang = Language.objects.get(languagecode=language_code)
active_announcements = Announcement.get_active_announcements(lang)
context = {"adoption_notices": active_adoptions[:5], "adoption_notices_map": active_adoptions,
"announcements": active_announcements}
Text.get_texts(["how_to", "introduction"], lang, context)
return render(request, 'fellchensammlung/bulma-index.html', context=context)
def change_language(request): def change_language(request):
if request.method == 'POST': if request.method == 'POST':
language_code = request.POST.get('language') language_code = request.POST.get('language')
@@ -99,11 +79,9 @@ def change_language(request):
return response return response
else: else:
return render(request, 'fellchensammlung/index.html') return render(request, 'fellchensammlung/index.html')
else:
return render(request, 'fellchensammlung/index.html')
def adoption_notice_detail(request, adoption_notice_id, template=None): def adoption_notice_detail(request, adoption_notice_id):
adoption_notice = AdoptionNotice.objects.get(id=adoption_notice_id) adoption_notice = AdoptionNotice.objects.get(id=adoption_notice_id)
if request.user.is_authenticated: if request.user.is_authenticated:
try: try:
@@ -155,20 +133,12 @@ def adoption_notice_detail(request, adoption_notice_id, template=None):
elif action == "subscribe": elif action == "subscribe":
return redirect_to_login(next=request.path) return redirect_to_login(next=request.path)
else: else:
return HttpResponseForbidden() raise PermissionDenied
else: else:
comment_form = CommentForm(instance=adoption_notice) comment_form = CommentForm(instance=adoption_notice)
context = {"adoption_notice": adoption_notice, "comment_form": comment_form, "user": request.user, context = {"adoption_notice": adoption_notice, "comment_form": comment_form, "user": request.user,
"has_edit_permission": has_edit_permission, "is_subscribed": is_subscribed} "has_edit_permission": has_edit_permission, "is_subscribed": is_subscribed}
if template is not None: return render(request, 'fellchensammlung/details/detail_adoption_notice.html', context=context)
return render(request, template, context=context)
else:
return render(request, 'fellchensammlung/details/detail_adoption_notice.html', context=context)
def adoption_notice_detail_bulma(request, adoption_notice_id):
return adoption_notice_detail(request, adoption_notice_id,
template='fellchensammlung/details/bulma-detail-adoption-notice.html')
@login_required() @login_required()
@@ -203,31 +173,7 @@ def animal_detail(request, animal_id):
return render(request, 'fellchensammlung/details/detail_animal.html', context=context) return render(request, 'fellchensammlung/details/detail_animal.html', context=context)
def search_important_locations(request, important_location_slug): def search(request):
i_location = get_object_or_404(ImportantLocation, slug=important_location_slug)
search = Search()
search.search_from_predefined_i_location(i_location)
context = {"adoption_notices": search.get_adoption_notices(),
"search_form": search.search_form,
"place_not_found": search.place_not_found,
"subscribed_search": None,
"searched": False,
"adoption_notices_map": AdoptionNotice.get_active_ANs(),
"map_center": search.position,
"search_center": search.position,
"map_pins": [search],
"location": search.location,
"search_radius": search.max_distance,
"zoom_level": zoom_level_for_radius(search.max_distance),
"geocoding_api_url": settings.GEOCODING_API_URL, }
return render(request, 'fellchensammlung/search.html', context=context)
def search_bulma(request):
return search(request, "fellchensammlung/bulma-search.html")
def search(request, templatename="fellchensammlung/search.html"):
# A user just visiting the search site did not search, only upon completing the search form a user has really # A user just visiting the search site did not search, only upon completing the search form a user has really
# searched. This will toggle the "subscribe" button # searched. This will toggle the "subscribe" button
searched = False searched = False
@@ -266,7 +212,7 @@ def search(request, templatename="fellchensammlung/search.html"):
"search_radius": search.max_distance, "search_radius": search.max_distance,
"zoom_level": zoom_level_for_radius(search.max_distance), "zoom_level": zoom_level_for_radius(search.max_distance),
"geocoding_api_url": settings.GEOCODING_API_URL, } "geocoding_api_url": settings.GEOCODING_API_URL, }
return render(request, templatename, context=context) return render(request, 'fellchensammlung/search.html', context=context)
@login_required @login_required
@@ -311,51 +257,6 @@ def add_adoption_notice(request):
return render(request, 'fellchensammlung/forms/form_add_adoption.html', {'form': form}) return render(request, 'fellchensammlung/forms/form_add_adoption.html', {'form': form})
@login_required
def add_adoption_notice_bulma(request):
if request.method == 'POST':
print("dada")
form = AdoptionNoticeFormWithDateWidgetAutoAnimal(request.POST)
if form.is_valid():
print("dodo")
an_instance = form.save(commit=False)
an_instance.owner = request.user
if request.user.trust_level >= TrustLevel.MODERATOR:
an_instance.set_active()
else:
an_instance.set_unchecked()
# Get the species and number of animals from the form
species = form.cleaned_data["species"]
sex = form.cleaned_data["sex"]
num_animals = form.cleaned_data["num_animals"]
date_of_birth = form.cleaned_data["date_of_birth"]
for i in range(0, num_animals):
Animal.objects.create(owner=request.user,
name=f"{species} {i + 1}", adoption_notice=an_instance, species=species, sex=sex,
date_of_birth=date_of_birth)
"""Log"""
Log.objects.create(user=request.user, action="add_adoption_notice",
text=f"{request.user} hat Vermittlung {an_instance.pk} hinzugefügt")
"""Spin up a task that adds the location and notifies search subscribers"""
post_adoption_notice_save.delay(an_instance.id)
"""Subscriptions"""
# Automatically subscribe user that created AN to AN
Subscriptions.objects.create(owner=request.user, adoption_notice=an_instance)
return redirect(reverse("adoption-notice-detail-bulma", args=[an_instance.pk]))
else:
print(form.errors)
else:
form = AdoptionNoticeFormWithDateWidgetAutoAnimal()
return render(request, 'fellchensammlung/forms/bulma-form-add-adoption.html', {'form': form})
@login_required @login_required
def adoption_notice_add_animal(request, adoption_notice_id): def adoption_notice_add_animal(request, adoption_notice_id):
# Only users that are mods or owners of the adoption notice are allowed to add to it # Only users that are mods or owners of the adoption notice are allowed to add to it
@@ -464,7 +365,15 @@ def animal_edit(request, animal_id):
def about(request): def about(request):
rules = Rule.objects.all() rules = Rule.objects.all()
legal = i18n.get_texts_by_language(["terms_of_service", "privacy_statement", "imprint", "about_us", "faq"]) language_code = translation.get_language()
lang = Language.objects.get(languagecode=language_code)
legal = {}
for text_code in ["terms_of_service", "privacy_statement", "imprint", "about_us", "faq"]:
try:
legal[text_code] = Text.objects.get(text_code=text_code, language=lang, )
except Text.DoesNotExist:
legal[text_code] = None
context = {"rules": rules, } context = {"rules": rules, }
context.update(legal) context.update(legal)
@@ -475,47 +384,6 @@ def about(request):
) )
def about_bulma(request):
context = i18n.get_texts_by_language(["about_us", "faq"])
return render(
request,
"fellchensammlung/bulma-about.html",
context=context
)
def render_text(request, text):
context = {"text": text}
return render(
request,
"fellchensammlung/bulma-one-text.html",
context=context
)
def imprint(request):
text = i18n.get_text_by_language("imprint")
return render_text(request, text)
def privacy(request):
text = i18n.get_text_by_language("privacy_statement")
return render_text(request, text)
def terms_of_service(request):
text = i18n.get_text_by_language("terms_of_service")
rules = Rule.objects.all()
context = {"rules": rules, "text": text}
return render(
request,
"fellchensammlung/bulma-terms-of-service.html",
context=context
)
def report_adoption(request, adoption_notice_id): def report_adoption(request, adoption_notice_id):
""" """
Form to report adoption notices Form to report adoption notices
@@ -560,13 +428,10 @@ def report_detail(request, report_id, form_complete=False):
""" """
Detailed view of a report, including moderation actions Detailed view of a report, including moderation actions
""" """
# Prefetching reduces the number of queries to the database that are needed (see reported_content) report = Report.objects.get(pk=report_id)
report = Report.objects.select_related("reportadoptionnotice", "reportcomment").get(pk=report_id)
moderation_actions = ModerationAction.objects.filter(report_id=report_id) moderation_actions = ModerationAction.objects.filter(report_id=report_id)
is_mod_or_above = user_is_trust_level_or_above(request.user, TrustLevel.MODERATOR)
context = {"report": report, "moderation_actions": moderation_actions, context = {"report": report, "moderation_actions": moderation_actions, "form_complete": form_complete}
"form_complete": form_complete, "is_mod_or_above": is_mod_or_above}
return render(request, 'fellchensammlung/details/detail-report.html', context) return render(request, 'fellchensammlung/details/detail-report.html', context)
@@ -637,7 +502,7 @@ def my_profile(request):
@user_passes_test(user_is_trust_level_or_above) @user_passes_test(user_is_trust_level_or_above)
def modqueue(request): def modqueue(request):
open_reports = Report.objects.select_related("reportadoptionnotice", "reportcomment").filter(status=Report.WAITING) open_reports = Report.objects.filter(status=Report.WAITING)
context = {"reports": open_reports} context = {"reports": open_reports}
return render(request, 'fellchensammlung/modqueue.html', context=context) return render(request, 'fellchensammlung/modqueue.html', context=context)
@@ -667,14 +532,10 @@ def updatequeue(request):
return render(request, 'fellchensammlung/updatequeue.html', context=context) return render(request, 'fellchensammlung/updatequeue.html', context=context)
def map(request, templatename='fellchensammlung/map.html'): def map(request):
adoption_notices = AdoptionNotice.get_active_ANs() adoption_notices = AdoptionNotice.get_active_ANs()
context = {"adoption_notices_map": adoption_notices} context = {"adoption_notices_map": adoption_notices}
return render(request, templatename, context=context) return render(request, 'fellchensammlung/map.html', context=context)
def map_bulma(request):
return map(request, templatename='fellchensammlung/bulma-map.html')
def metrics(request): def metrics(request):
@@ -771,28 +632,3 @@ def export_own_profile(request):
ANs_as_json = serialize('json', ANs) ANs_as_json = serialize('json', ANs)
full_json = f"{user_as_json}, {ANs_as_json}" full_json = f"{user_as_json}, {ANs_as_json}"
return HttpResponse(full_json, content_type="application/json") return HttpResponse(full_json, content_type="application/json")
def styleguide(request):
context = {"geocoding_api_url": settings.GEOCODING_API_URL, }
return render(request, 'fellchensammlung/styleguide.html', context=context)
def styleguide_bulma(request):
return render(request, 'fellchensammlung/bulma-styleguide.html')
@login_required
def rescue_organization_check(request):
if request.method == "POST":
rescue_org = RescueOrganization.objects.get(id=request.POST.get("rescue_organization_id"))
edit_permission = user_is_trust_level_or_above(request.user, TrustLevel.MODERATOR)
if not edit_permission:
return render(request, "fellchensammlung/errors/403.html", status=403)
action = request.POST.get("action")
if action == "checked":
rescue_org.set_checked()
last_checked_rescue_orgs = RescueOrganization.objects.order_by("last_checked")
context = {"rescue_orgs": last_checked_rescue_orgs, }
return render(request, 'fellchensammlung/rescue-organization-check.html', context=context)

View File

@@ -89,9 +89,9 @@ CELERY_RESULT_BACKEND = config.get("celery", "backend", fallback="redis://localh
HEALTHCHECKS_URL = config.get("monitoring", "healthchecks_url", fallback=None) HEALTHCHECKS_URL = config.get("monitoring", "healthchecks_url", fallback=None)
""" GEOCODING """ """ GEOCODING """
GEOCODING_API_URL = config.get("geocoding", "api_url", fallback="https://photon.hyteck.de/api") GEOCODING_API_URL = config.get("geocoding", "api_url", fallback="https://nominatim.hyteck.de/search")
# GEOCODING_API_FORMAT is allowed to be one of ['nominatim', 'photon'] # GEOCODING_API_FORMAT is allowed to be one of ['nominatim', 'photon']
GEOCODING_API_FORMAT = config.get("geocoding", "api_format", fallback="photon") GEOCODING_API_FORMAT = config.get("geocoding", "api_format", fallback="nominatim")
""" Tile Server """ """ Tile Server """
MAP_TILE_SERVER = config.get("map", "tile_server", fallback="https://tiles.hyteck.de") MAP_TILE_SERVER = config.get("map", "tile_server", fallback="https://tiles.hyteck.de")
@@ -168,7 +168,6 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
"django.contrib.sitemaps",
'fontawesomefree', 'fontawesomefree',
'crispy_forms', 'crispy_forms',
"crispy_bootstrap4", "crispy_bootstrap4",
@@ -176,7 +175,6 @@ INSTALLED_APPS = [
'rest_framework.authtoken', 'rest_framework.authtoken',
'drf_spectacular', 'drf_spectacular',
'drf_spectacular_sidecar', # required for Django collectstatic discovery 'drf_spectacular_sidecar', # required for Django collectstatic discovery
'widget_tweaks'
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View File

@@ -5,8 +5,7 @@ from django.urls import reverse
from model_bakery import baker from model_bakery import baker
from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus, TrustLevel, \ from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus, TrustLevel, \
Animal, Subscriptions, Comment, CommentNotification, SearchSubscription Animal, Subscriptions
from fellchensammlung.tools.geo import LocationProxy
from fellchensammlung.views import add_adoption_notice from fellchensammlung.views import add_adoption_notice
@@ -147,35 +146,6 @@ class SearchTest(TestCase):
self.assertContains(response, "TestAdoption3") self.assertContains(response, "TestAdoption3")
self.assertNotContains(response, "TestAdoption2") self.assertNotContains(response, "TestAdoption2")
def test_unauthenticated_subscribe(self):
response = self.client.post(reverse('search'), {"subscribe_to_search": ""})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/suchen/")
def test_unauthenticated_unsubscribe(self):
response = self.client.post(reverse('search'), {"unsubscribe_to_search": 1})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/suchen/")
def test_subscribe(self):
self.client.login(username='testuser0', password='12345')
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A",
"subscribe_to_search": ""})
self.assertEqual(response.status_code, 200)
self.assertTrue(SearchSubscription.objects.filter(owner=User.objects.get(username='testuser0'),
max_distance=50).exists())
def test_unsubscribe(self):
user0 = User.objects.get(username='testuser0')
self.client.login(username='testuser0', password='12345')
location = Location.get_location_from_string("München")
subscription = SearchSubscription.objects.create(owner=user0, max_distance=200, location=location, sex="A")
response = self.client.post(reverse('search'), {"max_distance": 200, "location_string": "München", "sex": "A",
"unsubscribe_to_search": subscription.pk})
self.assertEqual(response.status_code, 200)
self.assertFalse(SearchSubscription.objects.filter(owner=User.objects.get(username='testuser0'),
max_distance=200).exists())
def test_location_search(self): def test_location_search(self):
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A"}) response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A"})
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@@ -263,10 +233,10 @@ class AdoptionDetailTest(TestCase):
password='12345') password='12345')
test_user0.save() test_user0.save()
cls.test_user1 = User.objects.create_user(username='testuser1', test_user1 = User.objects.create_user(username='testuser1',
first_name="Max", first_name="Max",
last_name="Müller", last_name="Müller",
password='12345') password='12345')
test_user0.trust_level = TrustLevel.ADMIN test_user0.trust_level = TrustLevel.ADMIN
test_user0.save() test_user0.save()
@@ -286,16 +256,6 @@ class AdoptionDetailTest(TestCase):
adoption3.set_active() adoption3.set_active()
adoption2.set_unchecked() adoption2.set_unchecked()
def test_basic_view(self):
response = self.client.get(
reverse('adoption-notice-detail', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)), )
self.assertEqual(response.status_code, 200)
def test_basic_view_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(
reverse('adoption-notice-detail', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)), )
self.assertEqual(response.status_code, 200)
def test_subscribe(self): def test_subscribe(self):
self.client.login(username='testuser0', password='12345') self.client.login(username='testuser0', password='12345')
@@ -304,6 +264,7 @@ class AdoptionDetailTest(TestCase):
data={"action": "subscribe"}) data={"action": "subscribe"})
self.assertTrue(Subscriptions.objects.filter(owner__username="testuser0").exists()) self.assertTrue(Subscriptions.objects.filter(owner__username="testuser0").exists())
def test_unsubscribe(self): def test_unsubscribe(self):
# Make sure subscription exists # Make sure subscription exists
an = AdoptionNotice.objects.get(name="TestAdoption1") an = AdoptionNotice.objects.get(name="TestAdoption1")
@@ -323,73 +284,3 @@ class AdoptionDetailTest(TestCase):
data={"action": "subscribe"}) data={"action": "subscribe"})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/vermittlung/1/") self.assertEqual(response.url, "/accounts/login/?next=/vermittlung/1/")
def test_unauthenticated_comment(self):
response = self.client.post(
reverse('adoption-notice-detail', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)),
data={"action": "comment"})
self.assertEqual(response.status_code, 403)
def test_comment(self):
an1 = AdoptionNotice.objects.get(name="TestAdoption1")
# Set up subscription
Subscriptions.objects.create(owner=self.test_user1, adoption_notice=an1)
self.client.login(username='testuser0', password='12345')
response = self.client.post(
reverse('adoption-notice-detail', args=str(an1.pk)),
data={"action": "comment", "text": "Test"})
self.assertTrue(Comment.objects.filter(user__username="testuser0").exists())
self.assertFalse(CommentNotification.objects.filter(user__username="testuser0").exists())
self.assertTrue(CommentNotification.objects.filter(user__username="testuser1").exists())
class AdoptionEditTest(TestCase):
@classmethod
def setUpTestData(cls):
test_user0 = User.objects.create_user(username='testuser0',
first_name="Admin",
last_name="BOFH",
password='12345')
test_user0.save()
cls.test_user1 = User.objects.create_user(username='testuser1',
first_name="Max",
last_name="Müller",
password='12345')
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1", description="Test1", owner=test_user0)
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2", description="Test2")
def test_basic_view(self):
response = self.client.get(
reverse('adoption-notice-edit', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)), )
self.assertEqual(response.status_code, 302)
def test_basic_view_logged_in_unauthorized(self):
self.client.login(username='testuser1', password='12345')
response = self.client.get(
reverse('adoption-notice-edit', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)), )
self.assertEqual(response.status_code, 403)
def test_basic_view_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(
reverse('adoption-notice-edit', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)), )
self.assertEqual(response.status_code, 200)
def test_edit(self):
data = {"name": "Mia",
"searching_since": "01.01.2025",
"location_string": "Paderborn",
"organization": "",
"description": "Test3",
"further_information": ""}
an = AdoptionNotice.objects.get(name="TestAdoption1")
assert self.client.login(username='testuser0', password='12345')
response = self.client.post(reverse("adoption-notice-edit", args=str(an.pk)), data=data, follow=True)
self.assertEqual(response.redirect_chain[0][1],
302) # See https://docs.djangoproject.com/en/5.1/topics/testing/tools/
self.assertEqual(response.status_code, 200) # Redirects to AN page
self.assertContains(response, "Test3")
self.assertContains(response, "Mia")
self.assertNotContains(response, "Test1")

View File

@@ -1,8 +1,7 @@
from django.test import TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from docs.conf import language from fellchensammlung.models import User, TrustLevel, AdoptionNotice, Species
from fellchensammlung.models import User, TrustLevel, AdoptionNotice, Species, Rule, Language, Comment, ReportComment
from model_bakery import baker from model_bakery import baker
@@ -27,19 +26,6 @@ class BasicViewTest(TestCase):
for i in range(0, 4): for i in range(0, 4):
AdoptionNotice.objects.get(name=f"TestAdoption{i}").set_active() AdoptionNotice.objects.get(name=f"TestAdoption{i}").set_active()
rule1 = Rule.objects.create(title="Rule 1", rule_text="Description of r1", rule_identifier="rule1",
language=Language.objects.get(name="English"))
an1 = AdoptionNotice.objects.get(name="TestAdoption0")
comment1 = Comment.objects.create(adoption_notice=an1, text="Comment1", user=test_user1)
comment2 = Comment.objects.create(adoption_notice=an1, text="Comment2", user=test_user1)
comment3 = Comment.objects.create(adoption_notice=an1, text="Comment3", user=test_user1)
report_comment1 = ReportComment.objects.create(reported_comment=comment1,
user_comment="ReportComment1")
report_comment1.save()
report_comment1.reported_broken_rules.set({rule1,})
def test_index_logged_in(self): def test_index_logged_in(self):
self.client.login(username='testuser0', password='12345') self.client.login(username='testuser0', password='12345')
@@ -55,82 +41,3 @@ class BasicViewTest(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "TestAdoption1") self.assertContains(response, "TestAdoption1")
self.assertNotContains(response, "TestAdoption4") # Should not be active, therefore not shown self.assertNotContains(response, "TestAdoption4") # Should not be active, therefore not shown
def test_about_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('about'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Rule 1")
def test_about_anonymous(self):
response = self.client.get(reverse('about'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Rule 1")
def test_report_adoption_logged_in(self):
self.client.login(username='testuser0', password='12345')
an = AdoptionNotice.objects.get(name="TestAdoption0")
response = self.client.get(reverse('report-adoption-notice', args=str(an.pk)))
self.assertEqual(response.status_code, 200)
data = {"reported_broken_rules": 1, "user_comment": "animal cruelty"}
response = self.client.post(reverse('report-adoption-notice', args=str(an.pk)), data=data)
self.assertEqual(response.status_code, 302)
def test_report_adoption_anonymous(self):
an = AdoptionNotice.objects.get(name="TestAdoption0")
response = self.client.get(reverse('report-adoption-notice', args=str(an.pk)))
self.assertEqual(response.status_code, 200)
data = {"reported_broken_rules": 1, "user_comment": "animal cruelty"}
response = self.client.post(reverse('report-adoption-notice', args=str(an.pk)), data=data)
self.assertEqual(response.status_code, 302)
def test_report_comment_logged_in(self):
self.client.login(username='testuser0', password='12345')
c = Comment.objects.get(text="Comment1")
response = self.client.get(reverse('report-comment', args=str(c.pk)))
self.assertEqual(response.status_code, 200)
data = {"reported_broken_rules": 1, "user_comment": "animal cruelty"}
response = self.client.post(reverse('report-comment', args=str(c.pk)), data=data)
self.assertEqual(response.status_code, 302)
self.assertTrue(ReportComment.objects.filter(reported_comment=c.pk).exists())
def test_report_comment_anonymous(self):
c = Comment.objects.get(text="Comment2")
response = self.client.get(reverse('report-comment', args=str(c.pk)))
self.assertEqual(response.status_code, 200)
data = {"reported_broken_rules": 1, "user_comment": "animal cruelty"}
response = self.client.post(reverse('report-comment', args=str(c.pk)), data=data)
self.assertEqual(response.status_code, 302)
self.assertTrue(ReportComment.objects.filter(reported_comment=c.pk).exists())
def test_show_report_details_logged_in(self):
self.client.login(username='testuser1', password='12345')
report = ReportComment.objects.get(user_comment="ReportComment1")
response = self.client.get(reverse('report-detail', args=(report.pk,)))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Rule 1")
self.assertContains(response, "ReportComment1")
self.assertNotContains(response, '<form action="allow" class="">')
def test_show_report_details_anonymous(self):
report = ReportComment.objects.get(user_comment="ReportComment1")
response = self.client.get(reverse('report-detail', args=(report.pk,)))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Rule 1")
self.assertContains(response, "ReportComment1")
self.assertNotContains(response, '<form action="allow" class="">')
def test_show_report_details_admin(self):
self.client.login(username='testuser0', password='12345')
report = ReportComment.objects.get(user_comment="ReportComment1")
response = self.client.get(reverse('report-detail', args=(report.pk,)))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Rule 1")
self.assertContains(response, "ReportComment1")
self.assertContains(response, '<form action="allow" class="">')