Compare commits
81 Commits
37ecf28f2f
...
88987a973e
Author | SHA1 | Date | |
---|---|---|---|
88987a973e | |||
93ffbe09af | |||
e11848ea72 | |||
8bc9d12bfa | |||
1dbfdccb89 | |||
f085f5dcf5 | |||
33579e8446 | |||
a852da365f | |||
b53095ae17 | |||
3d7780e0ba | |||
478636bd98 | |||
d9ebee1e07 | |||
23e154bce6 | |||
5624f59258 | |||
56df942dd0 | |||
2dcb5fbf88 | |||
7a84b470f9 | |||
76232b7a0f | |||
349af16075 | |||
8641bead80 | |||
eb930b71d6 | |||
ae4ba06abf | |||
a2e237a81f | |||
f90c8c7e8c | |||
c316c74aff | |||
93dd0ae4f6 | |||
f79bb355cf | |||
45a534a042 | |||
2106a3423f | |||
d3f7274e92 | |||
5f576896b7 | |||
4a3cbfb8b0 | |||
3e93fe1a7a | |||
965e055ef1 | |||
13a0da6e46 | |||
1bb05dbf1c | |||
4c9c1e13a5 | |||
99cde15966 | |||
f2edc23e75 | |||
8aab4a13ae | |||
226102ccaf | |||
3d088c55d7 | |||
bb14a346cb | |||
f387930dee | |||
fe63e3b25c | |||
23adeb06e6 | |||
c1bd458c80 | |||
2a1d4178d7 | |||
f9a37b299d | |||
9950e87501 | |||
eff1ba6513 | |||
bb085aa9a8 | |||
b0dc0f9d78 | |||
d1a51b019c | |||
b7fade55fb | |||
79461518a3 | |||
8059d5d23f | |||
3098eacfb4 | |||
f3d1e1c203 | |||
e6a985ddfa | |||
388cc327be | |||
13adc695f6 | |||
f2c7943247 | |||
112fd52864 | |||
8279385966 | |||
1a9692949f | |||
e7af49b309 | |||
b822914db3 | |||
9ad33efe08 | |||
bd8f9fc1b7 | |||
4a2c18be4d | |||
479aba0195 | |||
1299fcac84 | |||
884a07f87b | |||
6557e9f9eb | |||
602cef1302 | |||
b400db603a | |||
0397311f6e | |||
abce89c829 | |||
bbad63a460 | |||
d940630086 |
2
.gitignore
vendored
@@ -4,7 +4,7 @@
|
||||
notfellchen
|
||||
|
||||
# Media storage
|
||||
static
|
||||
/static
|
||||
media
|
||||
|
||||
|
||||
|
@@ -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__":
|
||||
|
@@ -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)
|
||||
|
@@ -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__"
|
||||
|
@@ -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"),
|
||||
]
|
||||
|
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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'),
|
||||
),
|
||||
]
|
@@ -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')},
|
||||
),
|
||||
]
|
18
src/fellchensammlung/migrations/0042_location_county.py
Normal 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),
|
||||
),
|
||||
]
|
@@ -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',
|
||||
),
|
||||
]
|
@@ -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),
|
||||
),
|
||||
]
|
23
src/fellchensammlung/migrations/0045_importantlocation.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
@@ -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'),
|
||||
),
|
||||
]
|
@@ -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)
|
||||
|
||||
|
@@ -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):
|
||||
|
@@ -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%;
|
||||
}
|
3
src/fellchensammlung/static/fellchensammlung/css/bulma.min.css
vendored
Normal file
420
src/fellchensammlung/static/fellchensammlung/css/photoswipe.css
Normal 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;
|
||||
}
|
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 1.0 MiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 4.8 KiB |
BIN
src/fellchensammlung/static/fellchensammlung/img/sexes/Male.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
@@ -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();
|
||||
|
||||
|
32
src/fellchensammlung/static/fellchensammlung/js/toggles.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
@@ -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}")
|
@@ -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>
|
@@ -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">
|
||||
|
@@ -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 %}
|
@@ -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>
|
@@ -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>
|
@@ -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 %}
|
@@ -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 %}
|
@@ -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 %}
|
@@ -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 %}
|
@@ -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 %}
|
@@ -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 %}
|
@@ -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 %}
|
@@ -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 %}
|
@@ -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>
|
@@ -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 %}
|
||||
|
@@ -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 %}
|
@@ -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">
|
||||
|
@@ -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 %}
|
@@ -0,0 +1,5 @@
|
||||
<div class="container-cards">
|
||||
{% for rule in rules %}
|
||||
{% include "fellchensammlung/partials/bulma-partial-rule.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
@@ -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">
|
||||
|
@@ -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 %}
|
||||
|
||||
|
||||
|
||||
|
@@ -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>
|
||||
|
||||
|
||||
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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">
|
||||
|
@@ -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 %}
|
||||
|
@@ -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">
|
||||
|
@@ -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 %}
|
||||
|
@@ -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__
|
||||
|
@@ -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
|
||||
|
23
src/fellchensammlung/tools/i18n.py
Normal 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
|
@@ -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):
|
||||
|
@@ -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"),
|
||||
|
||||
]
|
||||
|
@@ -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)
|
||||
|
@@ -176,6 +176,7 @@ INSTALLED_APPS = [
|
||||
'rest_framework.authtoken',
|
||||
'drf_spectacular',
|
||||
'drf_spectacular_sidecar', # required for Django collectstatic discovery
|
||||
'widget_tweaks'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|