Compare commits

...

81 Commits

Author SHA1 Message Date
88987a973e feat: Manually craft the add adoption form to work with bulma
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-05-11 16:21:06 +02:00
93ffbe09af feat: Link to new layout 2025-05-11 13:48:25 +02:00
e11848ea72 feat: Add js to close notifications 2025-05-11 13:43:26 +02:00
8bc9d12bfa feat: Add basic bulma form to add adoptions 2025-05-11 13:43:10 +02:00
1dbfdccb89 feat: Add rules to TOS page 2025-05-11 13:42:51 +02:00
f085f5dcf5 feat: Remove leftover span 2025-05-11 09:11:07 +02:00
33579e8446 feat: Allow searching for rescue orgs 2025-05-11 08:55:29 +02:00
a852da365f feat: Remove about us from main menu 2025-05-10 14:13:00 +02:00
b53095ae17 feat: Add onpagers for imprint, privacy and terms of service 2025-05-10 13:35:53 +02:00
3d7780e0ba feat: style 2025-05-10 13:15:37 +02:00
478636bd98 feat: add bulma about 2025-05-10 13:12:58 +02:00
d9ebee1e07 feat: add bulma comment section 2025-05-10 12:02:11 +02:00
23e154bce6 feat: further restructure search 2025-05-10 09:26:49 +02:00
5624f59258 feat: Add image for animal selter 2025-05-10 08:56:00 +02:00
56df942dd0 feat: map 2025-05-10 08:55:23 +02:00
2dcb5fbf88 feat: Structure list a bit more 2025-05-09 21:54:06 +02:00
7a84b470f9 feat: Structure list a bit more 2025-05-09 21:53:59 +02:00
76232b7a0f feat: make sure maps extends over all available height, round corners 2025-05-09 21:16:54 +02:00
349af16075 feat: pad content 2025-05-09 21:16:17 +02:00
8641bead80 feat: make display of location nicer 2025-05-09 21:03:21 +02:00
eb930b71d6 fix: remove debug statements 2025-05-09 21:03:00 +02:00
ae4ba06abf fix: use normal partial 2025-05-09 21:01:33 +02:00
a2e237a81f feat: Make card heading more noticeable 2025-05-09 20:58:23 +02:00
f90c8c7e8c feat: Add name to header 2025-05-09 20:57:58 +02:00
c316c74aff feat: fix map title 2025-05-09 20:41:04 +02:00
93dd0ae4f6 feat: Add bulma map 2025-05-09 20:40:54 +02:00
f79bb355cf feat: Link to bulma url 2025-05-09 20:20:04 +02:00
45a534a042 feat: Add index page in bulma 2025-05-09 20:12:12 +02:00
2106a3423f feat: Remove compass and add fullscreen option 2025-05-09 18:34:09 +02:00
d3f7274e92 feat: Restructure search and add blocks 2025-05-09 18:27:52 +02:00
5f576896b7 feat: Add custom form rendering to support bulma 2025-05-09 18:15:17 +02:00
4a3cbfb8b0 feat: wrap blocks 2025-05-09 17:15:16 +02:00
3e93fe1a7a feat: Remove redundant heading "Pictures" 2025-05-09 17:14:55 +02:00
965e055ef1 feat: Add bulma search 2025-05-09 17:14:34 +02:00
13a0da6e46 feat: Move sex overview to partial 2025-05-09 17:13:31 +02:00
1bb05dbf1c feat: Add tags for sex 2025-05-01 18:41:35 +02:00
4c9c1e13a5 feat: further redesign 2025-05-01 18:15:25 +02:00
99cde15966 feat: Add filter for important locations 2025-04-28 22:46:18 +02:00
f2edc23e75 feat: Make cities visible at lower zoomlevels 2025-04-27 23:34:13 +02:00
8aab4a13ae feat: Exchange pin with circle
Allows to still see a cities label
2025-04-27 23:33:41 +02:00
226102ccaf feat: Display proper 404 when location is not found 2025-04-27 15:05:22 +02:00
3d088c55d7 fix: Adjust to use new versatiles structure
See https://docs.versatiles.org/compendium/specification_frontend.html
2025-04-27 14:31:53 +02:00
bb14a346cb feat: Add important locations to search around 2025-04-27 14:06:17 +02:00
f387930dee feat: Allow longer placids with no restrictions on int 2025-04-27 00:21:58 +02:00
fe63e3b25c feat: Link organization Website directly 2025-04-26 23:03:02 +02:00
23adeb06e6 feat: Allow getting and setting photos with ANs in API 2025-04-25 19:23:45 +02:00
c1bd458c80 feat: Allow adding locations and organizations to ANs in API 2025-04-25 19:18:06 +02:00
2a1d4178d7 feat: Allow creating locations via API 2025-04-24 22:35:38 +02:00
f9a37b299d feat: Extend location model to allow specifying address 2025-04-24 19:43:48 +02:00
9950e87501 feat: Make use of footer items 2025-04-24 18:50:48 +02:00
eff1ba6513 feat: Make use of footer items 2025-04-23 21:14:16 +02:00
bb085aa9a8 feat: Add basic bulma version of comment form 2025-04-23 21:12:22 +02:00
b0dc0f9d78 feat: Make photos to be in card 2025-04-23 20:56:19 +02:00
d1a51b019c feat: Make columns to stack vertically on mobile 2025-04-23 20:53:26 +02:00
b7fade55fb feat: Make header of description card header title 2025-04-23 20:20:24 +02:00
79461518a3 feat: add bulma animal cards 2025-04-10 15:12:39 +02:00
8059d5d23f feat: add photoswipe to adoption notice detail page 2025-04-10 15:12:21 +02:00
3098eacfb4 feat: only exclude the static folder in root from VSC 2025-04-10 15:11:26 +02:00
f3d1e1c203 feat: make headings strong 2025-04-07 21:32:46 +02:00
e6a985ddfa feat: Add initial bulma version of adoption notice detail page 2025-04-07 21:30:14 +02:00
388cc327be feat: Add cards 2025-04-06 10:48:03 +02:00
13adc695f6 feat: Style headings and add change langueg form 2025-04-06 10:25:37 +02:00
f2c7943247 fix: Add missing div 2025-04-06 10:03:52 +02:00
112fd52864 feat: Add footer 2025-04-06 10:03:35 +02:00
8279385966 feat: Rename bulma styleguide and add navigation 2025-04-06 09:05:50 +02:00
1a9692949f feat: add alt text to logo 2025-04-06 09:05:00 +02:00
e7af49b309 feat: add optional address data to location 2025-04-06 08:38:34 +02:00
b822914db3 feat: Allow updating existing rescue organizations 2025-03-21 16:11:58 +01:00
9ad33efe08 feat: Allow filtering for external object id and source 2025-03-21 15:53:58 +01:00
bd8f9fc1b7 feat: Ensure External object identifier and external source identifier are unique together 2025-03-21 15:26:01 +01:00
4a2c18be4d feat: replace upload script with version of g 2025-03-21 12:56:34 +01:00
479aba0195 fix: Adjust maplibre paths for versatiles 0.15.X
See https://github.com/versatiles-org/versatiles-docker/issues/16
2025-03-21 00:51:58 +01:00
1299fcac84 refactor: Rename 2025-03-21 00:29:23 +01:00
884a07f87b feat: Use choices and fix bug where default was not honored 2025-03-21 00:28:15 +01:00
6557e9f9eb feat: Auto-add location to rescue org 2025-03-20 23:26:16 +01:00
602cef1302 refactor: use bulma.min.css 2025-03-20 19:20:40 +01:00
b400db603a refactor: Remove js part -> model 2025-03-20 19:12:32 +01:00
0397311f6e feat: Add bulma and base for bulma styleguide 2025-03-20 19:12:32 +01:00
abce89c829 feat: Add bulma and base for bulma styleguide 2025-03-20 18:58:40 +01:00
bbad63a460 fix: Adjust site language based on selected language 2025-03-20 18:34:25 +01:00
d940630086 refactor: formatting 2025-03-17 21:08:41 +01:00
70 changed files with 5457 additions and 1079 deletions

2
.gitignore vendored
View File

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

View File

@@ -1,10 +1,10 @@
import argparse
import json
import os
import requests
from tqdm import tqdm
DEFAULT_OSM_DATA_FILE = "osm_data.geojson"
DEFAULT_OSM_DATA_FILE = "export.geojson"
def parse_args():
@@ -30,67 +30,75 @@ def get_config():
return api_token, instance, data_file
def load_osm_data(file_path):
"""Load OSM data from a GeoJSON file."""
with open(file_path, "r", encoding="utf-8") as file:
data = json.load(file)
return data
def load_osm_data(file_path):
#Load OSM data from a GeoJSON file.
with open(file_path, "r", encoding="utf-8") as file:
data = json.load(file)
return data
def transform_osm_data(feature):
#Transform a single OSM feature into the API payload format
prop = feature.get("properties", {})
geometry = feature.get("geometry", {})
return {
"name": prop.get("name", "Unnamed Shelter"),
"phone": prop.get("phone"),
"website": prop.get("website"),
"opening_hours": prop.get("opening_hours"),
"email": prop.get("email"),
"location_string": f'{prop.get("addr:street", "")} {prop.get("addr:housenumber", "")} {prop.get("addr:postcode", "")} {prop.get("addr:city", "")}',
"external_object_id": prop.get("@id"),
"external_source_id": "OSM"
}
def send_to_api(data, endpoint, headers):
# Send transformed data to the Notfellchen API.
response = requests.post(endpoint, headers=headers, json=data)
if response.status_code == 201:
print(f"Success: Shelter '{data['name']}' uploaded.")
elif response.status_code == 400:
print(f"Error: Shelter '{data['name']}' already exists or invalid data. {response.text}")
def get_or_none(data, key):
if key in data["properties"].keys():
return data["properties"][key]
else:
print(f"Unexpected Error: {response.status_code} - {response.text}")
raise ConnectionError
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():
# Get configuration
api_token, instance, data_file = get_config()
# Set headers and endpoint
endpoint = f"{instance}/api/organizations/"
headers = {
"Authorization": f"Token {api_token}",
"Content-Type": "application/json"
}
h = {'Authorization': f'Token {api_token}', "content-type": "application/json"}
# Step 1: Load OSM data
osm_data = load_osm_data(data_file)
with open(data_file, encoding="utf8") as f:
d = json.load(f)
# Step 2: Process each shelter and send it to the API
for feature in osm_data.get("features", []):
shelter_data = transform_osm_data(feature)
send_to_api(shelter_data, endpoint, headers)
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__":

View File

@@ -7,7 +7,7 @@ from django.urls import reverse
from django.utils.http import urlencode
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
SpeciesSpecificURL
SpeciesSpecificURL, ImportantLocation
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, BaseNotification
@@ -94,12 +94,14 @@ class ReportAdoptionNoticeAdmin(admin.ModelAdmin):
reported_content_link.short_description = "Reported Content"
class SpeciesSpecificURLInline(admin.StackedInline):
model = SpeciesSpecificURL
@admin.register(RescueOrganization)
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_filter = ("allows_using_materials", "trusted",)
@@ -122,14 +124,46 @@ class CommentAdmin(admin.ModelAdmin):
class BaseNotificationAdmin(admin.ModelAdmin):
list_filter = ("user", "read")
@admin.register(SearchSubscription)
class SearchSubscriptionAdmin(admin.ModelAdmin):
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(Species)
admin.site.register(Location)
admin.site.register(Rule)
admin.site.register(Image)
admin.site.register(ModerationAction)

View File

@@ -1,12 +1,35 @@
from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image
from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image, Location
from rest_framework import serializers
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:
model = AdoptionNotice
fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information",
"group_only"]
"group_only", "location", "location_details", "organization", "photos"]
class AnimalCreateSerializer(serializers.ModelSerializer):
@@ -14,12 +37,14 @@ class AnimalCreateSerializer(serializers.ModelSerializer):
model = Animal
fields = ["name", "date_of_birth", "description", "species", "sex", "adoption_notice"]
class RescueOrgSerializer(serializers.ModelSerializer):
class Meta:
model = RescueOrganization
fields = ["name", "location_string", "instagram", "facebook", "fediverse_profile", "email", "phone_number",
"website", "description", "external_object_identifier", "external_source_identifier"]
class AnimalGetSerializer(serializers.ModelSerializer):
class Meta:
model = Animal
@@ -51,3 +76,9 @@ class SpeciesSerializer(serializers.ModelSerializer):
class Meta:
model = Species
fields = "__all__"
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Location
fields = "__all__"

View File

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

View File

@@ -1,8 +1,11 @@
from django.db.models import Q
from fellchensammlung.api.serializers import LocationSerializer
from rest_framework.views import APIView
from rest_framework.response import Response
from django.db import transaction
from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel
from fellchensammlung.tasks import post_adoption_notice_save
from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel, Location
from fellchensammlung.tasks import post_adoption_notice_save, post_rescue_org_save
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from .serializers import (
@@ -130,14 +133,44 @@ class RescueOrganizationApiView(APIView):
'description': 'ID of the rescue organization to retrieve.',
'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)}
)
def get(self, request, *args, **kwargs):
"""
Get list of rescue organizations or a specific organization by ID.
Get list of rescue organizations or a specific organization by ID or get a list with available filters for
- external_object_identifier
- external_source_identifier
"""
org_id = kwargs.get("id")
org_id = request.query_params.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:
try:
organization = RescueOrganization.objects.get(pk=org_id)
@@ -145,14 +178,33 @@ class RescueOrganizationApiView(APIView):
return Response(serializer.data, status=status.HTTP_200_OK)
except RescueOrganization.DoesNotExist:
return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
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})
return Response(serializer.data, status=status.HTTP_200_OK)
@transaction.atomic
@extend_schema(
request=RescueOrgSerializer, # Document the request body
responses={201: 'Rescue organization created/updated successfully!'}
request=RescueOrgSerializer,
responses={201: 'Rescue organization created successfully!'}
)
def post(self, request, *args, **kwargs):
"""
@@ -161,12 +213,39 @@ class RescueOrganizationApiView(APIView):
serializer = RescueOrgSerializer(data=request.data, context={"request": request})
if serializer.is_valid():
rescue_org = serializer.save()
# Add the location
post_rescue_org_save.delay_on_commit(rescue_org.pk)
return Response(
{"message": "Rescue organization created/updated successfully!", "id": rescue_org.id},
{"message": "Rescue organization created successfully!", "id": rescue_org.id},
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)
class AddImageApiView(APIView):
permission_classes = [IsAuthenticated]
@@ -212,3 +291,63 @@ class SpeciesApiView(APIView):
species = Species.objects.all()
serializer = SpeciesSerializer(species, many=True, context={"request": request})
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,6 +22,15 @@ class DateInput(forms.DateInput):
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):
def __init__(self, *args, **kwargs):
if 'in_adoption_notice_creation_flow' in kwargs:
@@ -127,8 +136,9 @@ class ImageForm(forms.ModelForm):
self.helper.form_method = 'post'
if in_flow:
submits= Div(Submit('submit', _('Speichern')),
Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')), css_class="container-edit-buttons")
submits = Div(Submit('submit', _('Speichern')),
Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')),
css_class="container-edit-buttons")
else:
submits = Fieldset(Submit('submit', _('Speichern')), css_class="container-edit-buttons")
self.helper.layout = Layout(
@@ -140,7 +150,6 @@ class ImageForm(forms.ModelForm):
submits
)
class Meta:
model = Image
fields = ('image', 'alt_text')
@@ -164,7 +173,7 @@ class CommentForm(forms.ModelForm):
self.helper = FormHelper()
self.helper.form_class = 'form-comments'
self.helper.add_input(Hidden('action', 'comment'))
self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="btn2"))
self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="button is-primary"))
class Meta:
model = Comment
@@ -181,7 +190,8 @@ class CustomRegistrationForm(RegistrationForm):
class Meta(RegistrationForm.Meta):
model = User
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."))
captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_(
"Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um sicherzustellen, dass du kein Roboter bist."))
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -193,7 +203,10 @@ class CustomRegistrationForm(RegistrationForm):
class AdoptionNoticeSearchForm(forms.Form):
template_name = "fellchensammlung/forms/form_snippets.html"
sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False,
initial=SexChoicesWithAll.ALL)
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED, label=_("Suchradius"))
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED,
label=_("Suchradius"))
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)

View File

@@ -0,0 +1,18 @@
# 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

@@ -0,0 +1,42 @@
# 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

@@ -0,0 +1,18 @@
# 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

@@ -0,0 +1,18 @@
# 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

@@ -0,0 +1,18 @@
# 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

@@ -0,0 +1,23 @@
# 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

@@ -0,0 +1,19 @@
# 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,6 +3,7 @@ from random import choices
from tabnanny import verbose
from django.db import models
from django.template.defaultfilters import slugify
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
@@ -39,15 +40,28 @@ class Language(models.Model):
class Location(models.Model):
place_id = models.IntegerField() # OSM id
place_id = models.CharField(max_length=200) # OSM id
latitude = models.FloatField()
longitude = models.FloatField()
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)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
if self.city and self.postcode:
return f"{self.city} ({self.postcode})"
else:
return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
@property
def position(self):
@@ -73,6 +87,11 @@ class Location(models.Model):
latitude=proxy.latitude,
longitude=proxy.longitude,
name=proxy.name,
postcode=proxy.postcode,
city=proxy.city,
street=proxy.street,
county=proxy.county,
countrycode=proxy.countrycode,
)
return location
@@ -84,33 +103,33 @@ class Location(models.Model):
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):
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):
def __str__(self):
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)
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
allows_using_materials = models.CharField(max_length=200,
default=ALLOW_USE_MATERIALS_CHOICE[USE_MATERIALS_NOT_ASKED],
choices=ALLOW_USE_MATERIALS_CHOICE,
default=AllowUseOfMaterialsChices.USE_MATERIALS_NOT_ASKED,
choices=AllowUseOfMaterialsChices.choices,
verbose_name=_('Erlaubt Nutzung von Inhalten'))
location_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation"))
location = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True)
@@ -131,6 +150,9 @@ class RescueOrganization(models.Model):
choices=ExternalSourceChoices.choices,
verbose_name=_('External Source Identifier'))
class Meta:
unique_together = ('external_object_identifier', 'external_source_identifier',)
def get_absolute_url(self):
return reverse("rescue-organization-detail", args=[str(self.pk)])
@@ -275,11 +297,13 @@ class AdoptionNotice(models.Model):
updated_at = models.DateTimeField(auto_now=True)
last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=timezone.now)
searching_since = models.DateField(verbose_name=_('Sucht nach einem Zuhause seit'))
name = models.CharField(max_length=200)
name = models.CharField(max_length=200, verbose_name=_('Titel der Vermittlung'))
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
organization = models.ForeignKey(RescueOrganization, blank=True, null=True, on_delete=models.SET_NULL,
verbose_name=_('Organisation'))
further_information = models.URLField(null=True, blank=True, verbose_name=_('Link zu mehr Informationen'))
further_information = models.URLField(null=True, blank=True,
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'))
photos = models.ManyToManyField(Image, blank=True)
location_string = models.CharField(max_length=200, verbose_name=_("Ortsangabe"))
@@ -297,6 +321,13 @@ class AdoptionNotice(models.Model):
sexes.add(animal.sex)
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
def last_checked_hr(self):
time_since_last_checked = timezone.now() - self.last_checked
@@ -337,6 +368,10 @@ class AdoptionNotice(models.Model):
"""Returns the url to access a detailed page for the adoption notice."""
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):
"""Returns the url to report an adoption notice."""
return reverse('report-adoption-notice', args=[str(self.id)])
@@ -705,6 +740,7 @@ class Report(models.Model):
return self.reportcomment.reported_comment.get_absolute_url
return None
class ReportAdoptionNotice(Report):
adoption_notice = models.ForeignKey("AdoptionNotice", on_delete=models.CASCADE)

View File

@@ -1,6 +1,6 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from fellchensammlung.models import BaseNotification, CommentNotification, User, TrustLevel
from fellchensammlung.models import BaseNotification, CommentNotification, User, TrustLevel, RescueOrganization
from .tasks import task_send_notification_email
from notfellchen.settings import host
from django.utils.translation import gettext_lazy as _
@@ -18,6 +18,13 @@ def base_notification_receiver(sender, instance: BaseNotification, created: bool
else:
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)
def notification_new_user(sender, instance: User, created: bool, **kwargs):

View File

@@ -0,0 +1,60 @@
/***************/
/* 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

@@ -0,0 +1,420 @@
/*! 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.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
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

@@ -0,0 +1,32 @@
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

@@ -6,7 +6,7 @@ from notfellchen.celery import app as celery_app
from .mail import send_notification_email
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices
from .tools.misc import healthcheck_ok
from .models import Location, AdoptionNotice, Timestamp
from .models import Location, AdoptionNotice, Timestamp, RescueOrganization
from .tools.notifications import notify_of_AN_to_be_checked
from .tools.search import notify_search_subscribers
@@ -57,3 +57,10 @@ def task_healthcheck():
@shared_task
def task_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

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

View File

@@ -0,0 +1,27 @@
{% 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

@@ -0,0 +1,83 @@
{% 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

@@ -0,0 +1,43 @@
{% 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

@@ -0,0 +1,34 @@
{% 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

@@ -0,0 +1,9 @@
{% 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

@@ -0,0 +1,15 @@
{% 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

@@ -0,0 +1,97 @@
{% 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

@@ -0,0 +1,120 @@
{% 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

@@ -0,0 +1,17 @@
{% 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

@@ -0,0 +1,113 @@
{% 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

@@ -0,0 +1,95 @@
{% 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

@@ -0,0 +1,15 @@
{% 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 %}
<form class="btn2" action="{% url 'change-language' %}" method="post" onchange='this.form.submit()'>
<form class="btn2 select" action="{% url 'change-language' %}" method="post" onchange='this.form.submit()'>
{% csrf_token %}
<select name="language" onchange='this.form.submit()'>
{% get_current_language as LANGUAGE_CODE_CURRENT %}

View File

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

View File

@@ -0,0 +1,12 @@
{% 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

@@ -0,0 +1,5 @@
<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" %}
{% load i18n %}
{% block title %}<title>{% translate "Karte" %}</title> %}
{% block title %}<title>{% translate "Karte" %}</title>{% endblock %}
{% block content %}
<div class="card">

View File

@@ -0,0 +1,40 @@
{% 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

@@ -0,0 +1,42 @@
{% 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

@@ -0,0 +1,38 @@
{% 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

@@ -0,0 +1,25 @@
{% 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

@@ -0,0 +1,27 @@
{% 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

@@ -0,0 +1,149 @@
{% 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

@@ -0,0 +1,9 @@
{% 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

@@ -0,0 +1,44 @@
{% 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

@@ -6,7 +6,7 @@
</h1>
<i>{% translate 'Zuletzt geprüft:' %} {{ rescue_org.last_checked_hr }}</i>
{% if rescue_org.website %}
<p>{% translate "Website" %}: {{ rescue_org.website | safe }}</p>
<p>{% translate "Website" %}: <a href="{{ rescue_org.website }}">{{ rescue_org.website }}</a></p>
{% endif %}
<div class="container-edit-buttons">
<form method="post">

View File

@@ -3,8 +3,8 @@
{% load i18n %}
<!-- add MapLibre JavaScript and CSS -->
<script src="{% settings_value "MAP_TILE_SERVER" %}/assets/maplibre-gl/maplibre-gl.js"></script>
<link href="{% settings_value "MAP_TILE_SERVER" %}/assets/maplibre-gl/maplibre-gl.css" rel="stylesheet"/>
<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>
@@ -29,7 +29,7 @@
let map = new maplibregl.Map({
container: 'map',
style: '{% static "fellchensammlung/map/styles/colorful.json" %}',
style: '{% static "fellchensammlung/map/styles/colorful/style.json" %}',
center: map_center,
zoom: zoom_level
}).addControl(new maplibregl.NavigationControl());
@@ -76,7 +76,7 @@
image = await map.loadImage('{% static "fellchensammlung/img/pin.png" %}');
map.addImage('pin', image.data);
{% for map_pin in map_pins %}
map.addSource('point', {
map.addSource('point_{{ forloop.counter }}', {
'type': 'geojson',
'data': {
'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 %}
map.addLayer({
'id': 'pints',
'type': 'symbol',
'source': 'point',
'layout': {
'icon-image': 'pin',
'icon-size': 0.1
}
});
});
{% if search_center %}

View File

@@ -11,7 +11,7 @@
<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">
{{ search_form.as_p }}
{{ search_form }}
<ul id="results"></ul>
<div class="container-edit-buttons">
<button class="btn" type="submit" value="search" name="search">

View File

@@ -26,77 +26,10 @@
<input name="inputB" maxlength="200" id="inputB">
<label for="id_location_string">Ort</label>
<input name="location_string" id="id_location_string">
<ul id="results"></ul>
<div class="container-edit-buttons">
<button class="btn" type="submit" value="search" name="search">
<i class="fas fa-search"></i> {% trans 'Suchen' %}
</button>
{% if searched %}
{% if subscribed_search %}
<button class="btn" 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="btn" type="submit" name="subscribe_to_search">
<i class="fas fa-bell"></i> {% trans 'Suche abonnieren' %}
</button>
{% endif %}
{% endif %}
</div>
{% if place_not_found %}
<p class="error">
{% trans 'Ort nicht gefunden' %}
</p>
{% endif %}
</form>
<div class="card half">
{% include "fellchensammlung/partials/partial-map.html" %}
</div>
</div>
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
<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=de`);
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

@@ -49,6 +49,7 @@ def get_oxitraffic_script_if_enabled():
else:
return ""
@register.filter
@stringfilter
def pointdecimal(value):
@@ -57,6 +58,7 @@ def pointdecimal(value):
except ValueError:
return value
@register.filter
@stringfilter
def domain(url):
@@ -68,6 +70,17 @@ def domain(url):
except ValueError:
return url
@register.simple_tag
def settings_value(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,13 +74,21 @@ class GeoFeature:
geofeatures = []
for feature in result["features"]:
geojson = {}
try:
geojson['name'] = feature["properties"]["name"]
except KeyError:
geojson['name'] = feature["properties"]["street"]
# Necessary features
geojson['place_id'] = feature["properties"]["osm_id"]
geojson['lat'] = feature["geometry"]["coordinates"][1]
geojson['lon'] = feature["geometry"]["coordinates"][0]
try:
geojson['name'] = feature["properties"]["name"]
except KeyError:
geojson['name'] = feature["properties"]["osm_id"]
optional_keys = ["housenumber", "street", "city", "postcode", "county", "countrycode"]
for key in optional_keys:
try:
geojson[key] = feature["properties"][key]
except KeyError:
pass
geofeatures.append(geojson)
return geofeatures
@@ -161,6 +169,7 @@ class LocationProxy:
"""
self.geo_api = GeoAPI()
geofeatures = self.geo_api.get_geojson_for_query(location_string)
if geofeatures is None:
raise ValueError
result = geofeatures[0]
@@ -168,6 +177,12 @@ class LocationProxy:
self.place_id = result["place_id"]
self.latitude = result["lat"]
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):
return self.place_id == other.place_id

View File

@@ -0,0 +1,23 @@
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
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.
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.area_search = 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.search_form = None
# Either place_id or location string must be set for area search
@@ -47,7 +47,6 @@ class Search:
elif search_subscription:
self.search_from_search_subscription(search_subscription)
def __str__(self):
return f"Search: {self.sex=}, {self.location=}, {self.area_search=}, {self.max_distance=}"
@@ -93,7 +92,6 @@ class Search:
return False
return True
def get_adoption_notices(self):
adoptions = AdoptionNotice.objects.order_by("-created_at")
# Filter for active adoption notices
@@ -118,13 +116,21 @@ class Search:
else:
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):
self.sex = search_subscription.sex
self.location = search_subscription.location
self.area_search = True
self.max_distance = search_subscription.max_distance
def subscribe(self, user):
logging.info(f"{user} subscribed to search")
if isinstance(self.location, LocationProxy):

View File

@@ -18,6 +18,7 @@ sitemaps = {
urlpatterns = [
path("", views.index, name="index"),
path("bulma/", views.index_bulma, name="index-bulma"),
path("rss/", LatestAdoptionNoticesFeed(), name="rss"),
path("metrics/", views.metrics, name="metrics"),
# ex: /animal/5/
@@ -28,6 +29,7 @@ urlpatterns = [
path("tier/<int:animal_id>/add-photo", views.add_photo_to_animal, name="animal-add-photo"),
# ex: /adoption_notice/7/
path("vermittlung/<int:adoption_notice_id>/", views.adoption_notice_detail, name="adoption-notice-detail"),
path("bulma/vermittlung/<int:adoption_notice_id>/", views.adoption_notice_detail_bulma, name="adoption-notice-detail-bulma"),
# ex: /adoption_notice/7/edit
path("vermittlung/<int:adoption_notice_id>/edit", views.adoption_notice_edit, name="adoption-notice-edit"),
# ex: /vermittlung/5/add-photo
@@ -43,12 +45,21 @@ urlpatterns = [
# ex: /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/
path("map/", views.map, name="map"),
# ex: /map/
path("bulma/map/", views.map_bulma, name="map-bulma"),
# ex: /vermitteln/
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("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 ##
@@ -113,5 +124,6 @@ urlpatterns = [
###############
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

@@ -3,7 +3,7 @@ import logging
from django.contrib.auth.views import redirect_to_login
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.contrib.auth.decorators import login_required
from django.utils import translation
@@ -18,11 +18,14 @@ from notfellchen import settings
from fellchensammlung import logger
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
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, \
CommentForm, ReportCommentForm, AnimalForm, \
AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal
AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal, \
BulmaAdoptionNoticeForm
from .models import Language, Announcement
from .tools import i18n
from .tools.geo import GeoAPI, zoom_level_for_radius
from .tools.metrics import gather_metrics_data
from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
@@ -63,6 +66,22 @@ def index(request):
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):
if request.method == 'POST':
language_code = request.POST.get('language')
@@ -80,9 +99,11 @@ def change_language(request):
return response
else:
return render(request, 'fellchensammlung/index.html')
else:
return render(request, 'fellchensammlung/index.html')
def adoption_notice_detail(request, adoption_notice_id):
def adoption_notice_detail(request, adoption_notice_id, template=None):
adoption_notice = AdoptionNotice.objects.get(id=adoption_notice_id)
if request.user.is_authenticated:
try:
@@ -139,7 +160,15 @@ def adoption_notice_detail(request, adoption_notice_id):
comment_form = CommentForm(instance=adoption_notice)
context = {"adoption_notice": adoption_notice, "comment_form": comment_form, "user": request.user,
"has_edit_permission": has_edit_permission, "is_subscribed": is_subscribed}
return render(request, 'fellchensammlung/details/detail_adoption_notice.html', context=context)
if template is not None:
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()
@@ -174,7 +203,31 @@ def animal_detail(request, animal_id):
return render(request, 'fellchensammlung/details/detail_animal.html', context=context)
def search(request):
def search_important_locations(request, important_location_slug):
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
# searched. This will toggle the "subscribe" button
searched = False
@@ -213,7 +266,7 @@ def search(request):
"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)
return render(request, templatename, context=context)
@login_required
@@ -258,6 +311,51 @@ def add_adoption_notice(request):
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
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
@@ -366,15 +464,7 @@ def animal_edit(request, animal_id):
def about(request):
rules = Rule.objects.all()
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
legal = i18n.get_texts_by_language(["terms_of_service", "privacy_statement", "imprint", "about_us", "faq"])
context = {"rules": rules, }
context.update(legal)
@@ -385,6 +475,47 @@ 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):
"""
Form to report adoption notices
@@ -536,10 +667,14 @@ def updatequeue(request):
return render(request, 'fellchensammlung/updatequeue.html', context=context)
def map(request):
def map(request, templatename='fellchensammlung/map.html'):
adoption_notices = AdoptionNotice.get_active_ANs()
context = {"adoption_notices_map": adoption_notices}
return render(request, 'fellchensammlung/map.html', context=context)
return render(request, templatename, context=context)
def map_bulma(request):
return map(request, templatename='fellchensammlung/bulma-map.html')
def metrics(request):
@@ -639,10 +774,14 @@ def export_own_profile(request):
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":
@@ -655,5 +794,5 @@ def rescue_organization_check(request):
rescue_org.set_checked()
last_checked_rescue_orgs = RescueOrganization.objects.order_by("last_checked")
context = {"rescue_orgs": last_checked_rescue_orgs,}
context = {"rescue_orgs": last_checked_rescue_orgs, }
return render(request, 'fellchensammlung/rescue-organization-check.html', context=context)

View File

@@ -176,6 +176,7 @@ INSTALLED_APPS = [
'rest_framework.authtoken',
'drf_spectacular',
'drf_spectacular_sidecar', # required for Django collectstatic discovery
'widget_tweaks'
]
MIDDLEWARE = [