Compare commits
129 Commits
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 | |||
37ecf28f2f | |||
12d5a976cc | |||
9086e2e75b | |||
3607eb0e4e | |||
3daf83d725 | |||
5ad0cb74cc | |||
9ae64e8cb1 | |||
1b5a0c71e0 | |||
4d4f11c479 | |||
835c89d1d4 | |||
46bf07dd8d | |||
f557672586 | |||
4e27e1be7f | |||
6d390ad21e | |||
2f2543160e | |||
64a9db133e | |||
712c3d32f3 | |||
8998bbdf6d | |||
ff31caa139 | |||
ad06829c31 | |||
03a48da355 | |||
885bed888d | |||
0051cb07c9 | |||
8858cff9cf | |||
70e2af6172 | |||
461abd2e46 | |||
![]() |
d7269106db | ||
77fb99a527 | |||
38a56daa24 | |||
![]() |
ac0749797f | ||
f193f7d7ca | |||
43657e0862 | |||
68ad366f74 | |||
350d2c5da9 | |||
462bb8f485 | |||
ea4d15b99a | |||
de30dfcb8b | |||
36a979954c | |||
71ef17dc97 | |||
206cd282e6 | |||
e399346c3e | |||
929c6dfff0 | |||
841b57fea2 | |||
9e5446ff1d | |||
3b79809b8c | |||
53e6db3655 | |||
424f91e919 | |||
84ce5f54b2 |
4
.coveragerc
Normal file
@ -0,0 +1,4 @@
|
||||
[run]
|
||||
omit =
|
||||
*/migrations/*
|
||||
*/tests/*
|
2
.gitignore
vendored
@ -4,7 +4,7 @@
|
||||
notfellchen
|
||||
|
||||
# Media storage
|
||||
static
|
||||
/static
|
||||
media
|
||||
|
||||
|
||||
|
20
README.md
@ -77,6 +77,26 @@ docker push moanos/notfellchen:latest
|
||||
docker run -p8000:7345 moanos/notfellchen:latest
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Tests can be run with
|
||||
|
||||
```zsh
|
||||
nf test src
|
||||
```
|
||||
|
||||
If you want to report on code coverage run
|
||||
|
||||
```zsh
|
||||
coverage run --source='.' src/manage.py test src
|
||||
```
|
||||
|
||||
and
|
||||
|
||||
```
|
||||
coverage report
|
||||
```
|
||||
|
||||
## Geocoding
|
||||
|
||||
Geocoding services (search map data by name, address or postcode) are provided via the
|
||||
|
@ -36,6 +36,11 @@ An application can then send this token in the request header for authorization.
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
All Endpoints are documented at https://notfellchen.org/api/schema/swagger-ui/ or at https://notfellchen.org/api/schema/redoc/ if you prefer redoc.
|
||||
The OpenAI schema can be downloaded at https://notfellchen.org/api/schema/
|
||||
|
||||
Examples are documented here.
|
||||
|
||||
Get Adoption Notices
|
||||
++++++++++++++++++++
|
||||
|
||||
|
@ -5,7 +5,7 @@ Report a bug
|
||||
^^^^^^^^^^^^
|
||||
|
||||
To report a bug, file an issue on `Github
|
||||
<https://codeberg.org/moanos/notfellchen/issues>`_
|
||||
<https://github.com/moan0s/notfellchen/issues>`_
|
||||
|
||||
Try to include the following information:
|
||||
|
||||
@ -29,7 +29,7 @@ To contribute simply clone the directory, make your changes and file a
|
||||
pull request.
|
||||
|
||||
If you want to know what can be done, have a look at the current `Github
|
||||
<https://codeberg.org/moanos/notfellchen/issues>`_.
|
||||
<https://github.com/moan0s/notfellchen/issues>`_.
|
||||
|
||||
Get in touch!
|
||||
^^^^^^^^^^^^^
|
||||
|
@ -5,8 +5,7 @@ What qualifies as release?
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
A new release should be announced when a significant number functions, bugfixes or other improvements to the software
|
||||
is made. Usually this indicates a minor release.
|
||||
Major releases are yet to be determined.
|
||||
is made. Notfellchen follows `Semantic Versioning <https://semver.org/>`_.
|
||||
|
||||
What should be done before a release?
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@ -14,7 +13,7 @@ What should be done before a release?
|
||||
Tested basic functions
|
||||
######################
|
||||
|
||||
Run :command:`pytest`
|
||||
Run :command:`nf test src`
|
||||
|
||||
Test upgrade on a copy of a production database
|
||||
###############################################
|
||||
@ -38,4 +37,4 @@ Do a final commit on this change, and tag the commit as release with appropriate
|
||||
git tag -a v1.0.0 -m "Releasing version v1.0.0"
|
||||
git push origin v1.0.0
|
||||
|
||||
Make sure the tag is visible on Codeberg and celebrate 🥳
|
||||
Make sure the tag is visible on GitHub/Codeberg and celebrate 🥳
|
||||
|
@ -8,13 +8,13 @@ build-backend = "setuptools.build_meta"
|
||||
name = "notfellchen"
|
||||
description = "A tool to help."
|
||||
authors = [
|
||||
{name = "moanos", email = "julian-samuel@gebuehr.net"},
|
||||
{ name = "moanos", email = "julian-samuel@gebuehr.net" },
|
||||
]
|
||||
maintainers = [
|
||||
{name = "moanos", email = "julian-samuel@gebuehr.net"},
|
||||
{ name = "moanos", email = "julian-samuel@gebuehr.net" },
|
||||
]
|
||||
keywords = ["animal", "adoption", "django", "rescue", ]
|
||||
license = {text = "AGPL-3.0-or-later"}
|
||||
license = { text = "AGPL-3.0-or-later" }
|
||||
classifiers = [
|
||||
"Environment :: Web",
|
||||
"License :: OSI Approved :: GNU Affero General Public License v3",
|
||||
@ -24,14 +24,12 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"Django",
|
||||
"coverage",
|
||||
"codecov",
|
||||
"sphinx",
|
||||
"sphinx-rtd-theme",
|
||||
"gunicorn",
|
||||
"fontawesomefree",
|
||||
"whitenoise",
|
||||
"model_bakery",
|
||||
"markdown",
|
||||
"Pillow",
|
||||
"django-registration",
|
||||
@ -48,6 +46,8 @@ dynamic = ["version", "readme"]
|
||||
[project.optional-dependencies]
|
||||
develop = [
|
||||
"pytest",
|
||||
"coverage",
|
||||
"model_bakery",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@ -62,6 +62,6 @@ nf = 'notfellchen.main:main'
|
||||
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "notfellchen.__version__"}
|
||||
readme = {file = "README.md"}
|
||||
version = { attr = "notfellchen.__version__" }
|
||||
readme = { file = "README.md" }
|
||||
|
||||
|
105
scripts/upload_animal_shelters.py
Normal file
@ -0,0 +1,105 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
from tqdm import tqdm
|
||||
|
||||
DEFAULT_OSM_DATA_FILE = "export.geojson"
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse command-line arguments."""
|
||||
parser = argparse.ArgumentParser(description="Upload animal shelter data to the Notfellchen API.")
|
||||
parser.add_argument("--api-token", type=str, help="API token for authentication.")
|
||||
parser.add_argument("--instance", type=str, help="API instance URL.")
|
||||
parser.add_argument("--data-file", type=str, help="Path to the GeoJSON file containing (only) animal shelters.")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def get_config():
|
||||
"""Get configuration from environment variables or command-line arguments."""
|
||||
args = parse_args()
|
||||
|
||||
api_token = args.api_token or os.getenv("NOTFELLCHEN_API_TOKEN")
|
||||
instance = args.instance or os.getenv("NOTFELLCHEN_INSTANCE")
|
||||
data_file = args.data_file or os.getenv("NOTFELLCHEN_DATA_FILE", DEFAULT_OSM_DATA_FILE)
|
||||
|
||||
if not api_token or not instance:
|
||||
raise ValueError("API token and instance URL must be provided via environment variables or CLI arguments.")
|
||||
|
||||
return api_token, instance, data_file
|
||||
|
||||
|
||||
def get_or_none(data, key):
|
||||
if key in data["properties"].keys():
|
||||
return data["properties"][key]
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def choose(keys, data, replace=False):
|
||||
for key in keys:
|
||||
if key in data.keys():
|
||||
if replace:
|
||||
return data[key].replace(" ", "").replace("-", "").replace("(", "").replace(")", "")
|
||||
else:
|
||||
return data[key]
|
||||
return None
|
||||
|
||||
|
||||
def add(value, platform):
|
||||
if value != "":
|
||||
if value.find(platform) == -1:
|
||||
return f"https://www.{platform}.com/{value}"
|
||||
else:
|
||||
return value
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def https(value):
|
||||
if value is not None and value != "":
|
||||
value = value.replace("http://", "")
|
||||
if value.find("https") == -1:
|
||||
return f"https://{value}"
|
||||
else:
|
||||
return value
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
api_token, instance, data_file = get_config()
|
||||
# Set headers and endpoint
|
||||
endpoint = f"{instance}/api/organizations/"
|
||||
h = {'Authorization': f'Token {api_token}', "content-type": "application/json"}
|
||||
|
||||
with open(data_file, encoding="utf8") as f:
|
||||
d = json.load(f)
|
||||
|
||||
for idx, tierheim in tqdm(enumerate(d["features"])):
|
||||
|
||||
if "name" not in tierheim["properties"].keys() or "addr:city" not in tierheim["properties"].keys():
|
||||
continue
|
||||
|
||||
data = {"name": tierheim["properties"]["name"],
|
||||
"location_string": f"{get_or_none(tierheim, "addr:street")} {get_or_none(tierheim, "addr:housenumber")}, {get_or_none(tierheim, "addr:postcode")} {tierheim["properties"]["addr:city"]}",
|
||||
"phone_number": choose(("contact:phone", "phone"), tierheim["properties"], replace=True),
|
||||
"fediverse_profile": get_or_none(tierheim, "contact:mastodon"),
|
||||
"facebook": https(add(get_or_none(tierheim, "contact:facebook"), "facebook")),
|
||||
"instagram": https(add(get_or_none(tierheim, "contact:instagram"), "instagram")),
|
||||
"website": https(choose(("contact:website", "website"), tierheim["properties"])),
|
||||
"email": choose(("contact:email", "email"), tierheim["properties"]),
|
||||
"description": get_or_none(tierheim, "opening_hours"),
|
||||
"external_object_identifier": f"{tierheim["id"]}",
|
||||
"external_source_identifier": "OSM"
|
||||
}
|
||||
|
||||
result = requests.post(endpoint, json=data, headers=h)
|
||||
|
||||
if result.status_code != 201:
|
||||
print(f"{idx} {tierheim["properties"]["name"]}:{result.status_code} {result.json()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -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 (
|
||||
@ -16,6 +19,7 @@ from .serializers import (
|
||||
from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image
|
||||
from drf_spectacular.utils import extend_schema
|
||||
|
||||
|
||||
class AdoptionNoticeApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@ -84,7 +88,6 @@ class AdoptionNoticeApiView(APIView):
|
||||
)
|
||||
|
||||
|
||||
|
||||
class AnimalApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@ -118,6 +121,7 @@ class AnimalApiView(APIView):
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class RescueOrganizationApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@ -129,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)
|
||||
@ -144,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):
|
||||
"""
|
||||
@ -159,11 +212,39 @@ class RescueOrganizationApiView(APIView):
|
||||
"""
|
||||
serializer = RescueOrgSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
rescue_org = serializer.save(owner=request.user)
|
||||
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):
|
||||
@ -210,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-01-14 06:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0035_alter_image_alt_text_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='basenotification',
|
||||
name='read_at',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Gelesen am'),
|
||||
),
|
||||
]
|
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-14 06:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0036_basenotification_read_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='basenotification',
|
||||
name='title',
|
||||
field=models.CharField(max_length=100, verbose_name='Titel'),
|
||||
),
|
||||
]
|
@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.1.4 on 2025-03-09 08:31
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0037_alter_basenotification_title'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='last_checked',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Datum der letzten Prüfung'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.4 on 2025-03-09 16:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0038_rescueorganization_last_checked'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='rescueorganization',
|
||||
name='last_checked',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='Datum der letzten Prüfung'),
|
||||
),
|
||||
]
|
@ -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,14 +40,27 @@ 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):
|
||||
if self.city and self.postcode:
|
||||
return f"{self.city} ({self.postcode})"
|
||||
else:
|
||||
return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
|
||||
|
||||
@property
|
||||
@ -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)
|
||||
@ -122,6 +141,7 @@ class RescueOrganization(models.Model):
|
||||
website = models.URLField(null=True, blank=True, verbose_name=_('Website'))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
last_checked = models.DateTimeField(auto_now_add=True, verbose_name=_('Datum der letzten Prüfung'))
|
||||
internal_comment = models.TextField(verbose_name=_("Interner Kommentar"), null=True, blank=True, )
|
||||
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung')) # Markdown allowed
|
||||
external_object_identifier = models.CharField(max_length=200, null=True, blank=True,
|
||||
@ -130,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)])
|
||||
|
||||
@ -149,7 +172,20 @@ class RescueOrganization(models.Model):
|
||||
if self.description is None:
|
||||
return ""
|
||||
if len(self.description) > 200:
|
||||
return self.description[:200] + f" ... [weiterlesen]({self.get_absolute_url()})"
|
||||
return self.description[:200] + _(f" ... [weiterlesen]({self.get_absolute_url()})")
|
||||
|
||||
def set_checked(self):
|
||||
self.last_checked = timezone.now()
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def last_checked_hr(self):
|
||||
time_since_last_checked = timezone.now() - self.last_checked
|
||||
return time_since_as_hr_string(time_since_last_checked)
|
||||
|
||||
@property
|
||||
def species_urls(self):
|
||||
return SpeciesSpecificURL.objects.filter(organization=self)
|
||||
|
||||
|
||||
# Admins can perform all actions and have the highest trust associated with them
|
||||
@ -261,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"))
|
||||
@ -283,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
|
||||
@ -323,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)])
|
||||
@ -666,6 +715,31 @@ class Report(models.Model):
|
||||
def get_moderation_actions(self):
|
||||
return ModerationAction.objects.filter(report=self)
|
||||
|
||||
@property
|
||||
def reported_content(self):
|
||||
"""
|
||||
Dynamically fetch the reported content based on subclass.
|
||||
The alternative would be to use the ContentType framework:
|
||||
https://docs.djangoproject.com/en/5.1/ref/contrib/contenttypes/
|
||||
"""
|
||||
if hasattr(self, "reportadoptionnotice"):
|
||||
return self.reportadoptionnotice.adoption_notice
|
||||
elif hasattr(self, "reportcomment"):
|
||||
return self.reportcomment.reported_comment
|
||||
return None
|
||||
|
||||
@property
|
||||
def reported_content_url(self):
|
||||
"""
|
||||
Same as reported_content, just for url
|
||||
"""
|
||||
if hasattr(self, "reportadoptionnotice"):
|
||||
print(self.reportadoptionnotice.adoption_notice.get_absolute_url)
|
||||
return self.reportadoptionnotice.adoption_notice.get_absolute_url
|
||||
elif hasattr(self, "reportcomment"):
|
||||
return self.reportcomment.reported_comment.get_absolute_url
|
||||
return None
|
||||
|
||||
|
||||
class ReportAdoptionNotice(Report):
|
||||
adoption_notice = models.ForeignKey("AdoptionNotice", on_delete=models.CASCADE)
|
||||
@ -674,6 +748,9 @@ class ReportAdoptionNotice(Report):
|
||||
def reported_content(self):
|
||||
return self.adoption_notice
|
||||
|
||||
def __str__(self):
|
||||
return f"Report der Vermittlung {self.adoption_notice}"
|
||||
|
||||
|
||||
class ReportComment(Report):
|
||||
reported_comment = models.ForeignKey("Comment", on_delete=models.CASCADE)
|
||||
@ -818,7 +895,8 @@ class Comment(models.Model):
|
||||
class BaseNotification(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
title = models.CharField(max_length=100)
|
||||
read_at = models.DateTimeField(blank=True, null=True, verbose_name=_("Gelesen am"))
|
||||
title = models.CharField(max_length=100, verbose_name=_("Titel"))
|
||||
text = models.TextField(verbose_name="Inhalt")
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||
read = models.BooleanField(default=False)
|
||||
@ -829,6 +907,11 @@ class BaseNotification(models.Model):
|
||||
def get_absolute_url(self):
|
||||
self.user.get_notifications_url()
|
||||
|
||||
def mark_read(self):
|
||||
self.read = True
|
||||
self.read_at = timezone.now()
|
||||
self.save()
|
||||
|
||||
|
||||
class CommentNotification(BaseNotification):
|
||||
comment = models.ForeignKey(Comment, on_delete=models.CASCADE, verbose_name=_('Antwort'))
|
||||
|
@ -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):
|
||||
|
47
src/fellchensammlung/sitemap.py
Normal file
@ -0,0 +1,47 @@
|
||||
from django.contrib.sitemaps import Sitemap
|
||||
from django.urls import reverse
|
||||
from .models import AdoptionNotice, RescueOrganization
|
||||
|
||||
|
||||
class StaticViewSitemap(Sitemap):
|
||||
priority = 0.8
|
||||
changefreq = "weekly"
|
||||
|
||||
def items(self):
|
||||
return ["index", "search", "map", "about", "rescue-organizations"]
|
||||
|
||||
def location(self, item):
|
||||
return reverse(item)
|
||||
|
||||
|
||||
class AdoptionNoticeSitemap(Sitemap):
|
||||
priority = 0.5
|
||||
changefreq = "daily"
|
||||
|
||||
def items(self):
|
||||
return AdoptionNotice.get_active_ANs()
|
||||
|
||||
def lastmod(self, obj):
|
||||
return obj.updated_at
|
||||
|
||||
|
||||
class AnimalSitemap(Sitemap):
|
||||
priority = 0.2
|
||||
changefreq = "daily"
|
||||
|
||||
def items(self):
|
||||
return AdoptionNotice.objects.all()
|
||||
|
||||
def lastmod(self, obj):
|
||||
return obj.updated_at
|
||||
|
||||
|
||||
class RescueOrganizationSitemap(Sitemap):
|
||||
priority = 0.3
|
||||
changefreq = "weekly"
|
||||
|
||||
def items(self):
|
||||
return RescueOrganization.objects.all()
|
||||
|
||||
def lastmod(self, obj):
|
||||
return obj.updated_at
|
@ -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;
|
||||
}
|
@ -259,6 +259,11 @@ a.btn, a.btn2, a.nav-link {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
font-size: medium;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
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);
|
||||
});
|
||||
});
|
||||
});
|
7
src/fellchensammlung/static/robots.txt
Normal file
@ -0,0 +1,7 @@
|
||||
User-agent: *
|
||||
Disallow: /admin/
|
||||
|
||||
User-agent: OpenAI
|
||||
Disallow: /
|
||||
|
||||
Sitemap: https://notfellchen.org/sitemap.xml
|
@ -6,8 +6,8 @@ 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 .tools.notifications import notify_moderators_of_AN_to_be_checked
|
||||
from .models import Location, AdoptionNotice, Timestamp, RescueOrganization
|
||||
from .tools.notifications import notify_of_AN_to_be_checked
|
||||
from .tools.search import notify_search_subscribers
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ def post_adoption_notice_save(pk):
|
||||
logging.info(f"Location was added to Adoption notice {pk}")
|
||||
|
||||
notify_search_subscribers(instance, only_if_active=True)
|
||||
notify_moderators_of_AN_to_be_checked(instance)
|
||||
notify_of_AN_to_be_checked(instance)
|
||||
|
||||
@celery_app.task(name="tools.healthcheck")
|
||||
def task_healthcheck():
|
||||
@ -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>
|
@ -0,0 +1,21 @@
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
<div class="card">
|
||||
<h1>
|
||||
<a href="{{ rescue_org.get_absolute_url }}">{{ rescue_org.name }}</a>
|
||||
</h1>
|
||||
<i>{% translate 'Zuletzt geprüft:' %} {{ rescue_org.last_checked_hr }}</i>
|
||||
{% if rescue_org.website %}
|
||||
<p>{% translate "Website" %}: <a href="{{ rescue_org.website }}">{{ rescue_org.website }}</a></p>
|
||||
{% endif %}
|
||||
<div class="container-edit-buttons">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden"
|
||||
name="rescue_organization_id"
|
||||
value="{{ rescue_org.pk }}">
|
||||
<input type="hidden" name="action" value="checked">
|
||||
<button class="btn" type="submit">{% translate "Organisation geprüft" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -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 @@
|
||||
]
|
||||
}
|
||||
});
|
||||
{% endfor %}
|
||||
map.addLayer({
|
||||
'id': 'pints',
|
||||
'type': 'symbol',
|
||||
'source': 'point',
|
||||
'layout': {
|
||||
'icon-image': 'pin',
|
||||
'icon-size': 0.1
|
||||
'id': 'point_{{ forloop.counter }}',
|
||||
'type': 'circle',
|
||||
'source': 'point_{{ forloop.counter }}',
|
||||
'paint': {
|
||||
'circle-radius': 18,
|
||||
'circle-color': '#ff878980'
|
||||
}
|
||||
});
|
||||
{% endfor %}
|
||||
});
|
||||
|
||||
{% if search_center %}
|
||||
|
@ -1,9 +1,7 @@
|
||||
{% load i18n %}
|
||||
<div class="report card">
|
||||
<h2>
|
||||
{% blocktranslate %}
|
||||
Meldung von {{ report.reported_content }}
|
||||
{% endblocktranslate %}
|
||||
{% translate 'Meldung von ' %} <a href="{{ report.reported_content_url }}"><i>{{ report.reported_content }}</i></a>
|
||||
</h2>
|
||||
{% if report.reported_broken_rules %}
|
||||
{% translate "Regeln gegen die Verstoßen wurde" %}
|
||||
@ -13,9 +11,14 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<p><b>{% translate "Kommentar zur Meldung" %}:</b>
|
||||
{{ report.user_comment }}
|
||||
<p>
|
||||
{% if report.user_comment %}
|
||||
<b>{% translate "Kommentar zur Meldung" %}:</b> {{ report.user_comment }}
|
||||
{% else %}
|
||||
<i>{% translate 'Es wurde kein Kommentar zur Meldung hinzugefügt.' %}</i>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if is_mod_or_above %}
|
||||
<div class="container-edit-buttons">
|
||||
<form action="allow" class="">
|
||||
{% csrf_token %}
|
||||
@ -28,4 +31,5 @@
|
||||
<button class="btn allow" type="submit">{% translate "Inhalt als gesperrt kennzeichnen" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
@ -0,0 +1,12 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<h1>{% translate "Aktualitätscheck" %}</h1>
|
||||
<p>{% translate "Überprüfe ob im Tierheim neue Vermittlungen ein Zuhause suchen" %}</p>
|
||||
<div class="container-cards spaced">
|
||||
<h1>{% translate 'Organisation zur Überprüfung' %}</h1>
|
||||
{% for rescue_org in rescue_orgs %}
|
||||
{% include "fellchensammlung/partials/partial-check-rescue-org.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -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">
|
||||
|
@ -0,0 +1,35 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% block title %}<title>{% translate "Styleguide" %}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>This is a heading</h1>
|
||||
<p>And this is a short paragraph below</p>
|
||||
<div class="container-cards">
|
||||
<h2>Card Containers</h2>
|
||||
<div class="card">
|
||||
<h3>I am a card</h3>
|
||||
<p>Cards are responsive. Use them to display multiple items of the same category</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Photos</h3>
|
||||
<p>Cards are responsive. Use them to display multiple items of the same category</p>
|
||||
<img src="{% static 'fellchensammlung/img/example_rat_single.png' %}" alt="A rat sitting on a wooden house">
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-cards">
|
||||
<form class="form-search card half" method="post">
|
||||
<label for="inputA">Input Alpha</label>
|
||||
<input name="inputA" maxlength="200" id="inputA">
|
||||
<label for="inputB">Beta</label>
|
||||
<input name="inputB" maxlength="200" id="inputB">
|
||||
<label for="id_location_string">Ort</label>
|
||||
<input name="location_string" id="id_location_string">
|
||||
</form>
|
||||
<div class="card half">
|
||||
{% include "fellchensammlung/partials/partial-map.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
|
||||
{% endblock %}
|
@ -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
|
||||
|
||||
@ -137,7 +145,6 @@ class GeoAPI:
|
||||
result = self.requests.get(self.api_url,
|
||||
{"q": location_string, "lang": language},
|
||||
headers=self.headers).json()
|
||||
logging.warning(result)
|
||||
geofeatures = GeoFeature.geofeatures_from_photon_result(result)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
@ -162,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]
|
||||
@ -169,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
|
@ -1,11 +1,13 @@
|
||||
from fellchensammlung.models import User, AdoptionNoticeNotification, TrustLevel
|
||||
|
||||
|
||||
def notify_moderators_of_AN_to_be_checked(adoption_notice):
|
||||
def notify_of_AN_to_be_checked(adoption_notice):
|
||||
if adoption_notice.is_disabled_unchecked:
|
||||
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
|
||||
users_to_notify = set(User.objects.filter(trust_level__gt=TrustLevel.MODERATOR))
|
||||
users_to_notify.add(adoption_notice.owner)
|
||||
for user in users_to_notify:
|
||||
AdoptionNoticeNotification.objects.create(adoption_notice=adoption_notice,
|
||||
user=moderator,
|
||||
user=user,
|
||||
title=f" Prüfe Vermittlung {adoption_notice}",
|
||||
text=f"{adoption_notice} muss geprüft werden bevor sie veröffentlicht wird.",
|
||||
)
|
@ -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.
|
||||
@ -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):
|
||||
|
@ -7,8 +7,18 @@ from .feeds import LatestAdoptionNoticesFeed
|
||||
from . import views
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
||||
|
||||
from django.contrib.sitemaps.views import sitemap
|
||||
from .sitemap import StaticViewSitemap, AdoptionNoticeSitemap, AnimalSitemap
|
||||
|
||||
sitemaps = {
|
||||
"static": StaticViewSitemap,
|
||||
"vermittlungen": AdoptionNoticeSitemap,
|
||||
"tiere": AnimalSitemap,
|
||||
}
|
||||
|
||||
urlpatterns = [
|
||||
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/
|
||||
@ -19,12 +29,15 @@ urlpatterns = [
|
||||
path("tier/<int:animal_id>/add-photo", views.add_photo_to_animal, name="animal-add-photo"),
|
||||
# ex: /adoption_notice/7/
|
||||
path("vermittlung/<int:adoption_notice_id>/", views.adoption_notice_detail, name="adoption-notice-detail"),
|
||||
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
|
||||
path("vermittlung/<int:adoption_notice_id>/add-photo", views.add_photo_to_adoption_notice, name="adoption-notice-add-photo"),
|
||||
path("vermittlung/<int:adoption_notice_id>/add-photo", views.add_photo_to_adoption_notice,
|
||||
name="adoption-notice-add-photo"),
|
||||
# ex: /adoption_notice/2/add-animal
|
||||
path("vermittlung/<int:adoption_notice_id>/add-animal", views.adoption_notice_add_animal, name="adoption-notice-add-animal"),
|
||||
path("vermittlung/<int:adoption_notice_id>/add-animal", views.adoption_notice_add_animal,
|
||||
name="adoption-notice-add-animal"),
|
||||
|
||||
path("tierschutzorganisationen/", views.list_rescue_organizations, name="rescue-organizations"),
|
||||
path("organisation/<int:rescue_organization_id>/", views.detail_view_rescue_organization,
|
||||
@ -32,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 ##
|
||||
@ -51,6 +73,8 @@ urlpatterns = [
|
||||
|
||||
path("updatequeue/", views.updatequeue, name="updatequeue"),
|
||||
|
||||
path("organization-check/", views.rescue_organization_check, name="organization-check"),
|
||||
|
||||
###########
|
||||
## USERS ##
|
||||
###########
|
||||
@ -95,4 +119,11 @@ urlpatterns = [
|
||||
###################
|
||||
path('external-site/', views.external_site_warning, name="external-site"),
|
||||
|
||||
###############
|
||||
## TECHNICAL ##
|
||||
###############
|
||||
path("sitemap.xml", sitemap, {"sitemaps": sitemaps}, name="django.contrib.sitemaps.views.sitemap"),
|
||||
path("styleguide", views.styleguide, name="styleguide"),
|
||||
path("styleguide-bulma", views.styleguide_bulma, name="styleguide-bulma"),
|
||||
|
||||
]
|
||||
|
@ -2,7 +2,8 @@ import logging
|
||||
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
from django.http import HttpResponseRedirect, JsonResponse, HttpResponse
|
||||
from django.shortcuts import render, redirect
|
||||
from django.http.response import HttpResponseForbidden
|
||||
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
|
||||
@ -17,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, \
|
||||
@ -62,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')
|
||||
@ -79,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:
|
||||
@ -130,15 +152,25 @@ def adoption_notice_detail(request, adoption_notice_id):
|
||||
if action == "unsubscribe":
|
||||
subscription.delete()
|
||||
is_subscribed = False
|
||||
elif action == "subscribe":
|
||||
return redirect_to_login(next=request.path)
|
||||
else:
|
||||
raise PermissionDenied
|
||||
return HttpResponseForbidden()
|
||||
else:
|
||||
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}
|
||||
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()
|
||||
def adoption_notice_edit(request, adoption_notice_id):
|
||||
"""
|
||||
@ -171,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
|
||||
@ -210,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
|
||||
@ -255,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
|
||||
@ -363,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)
|
||||
@ -382,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
|
||||
@ -426,10 +560,13 @@ def report_detail(request, report_id, form_complete=False):
|
||||
"""
|
||||
Detailed view of a report, including moderation actions
|
||||
"""
|
||||
report = Report.objects.get(pk=report_id)
|
||||
# Prefetching reduces the number of queries to the database that are needed (see reported_content)
|
||||
report = Report.objects.select_related("reportadoptionnotice", "reportcomment").get(pk=report_id)
|
||||
moderation_actions = ModerationAction.objects.filter(report_id=report_id)
|
||||
is_mod_or_above = user_is_trust_level_or_above(request.user, TrustLevel.MODERATOR)
|
||||
|
||||
context = {"report": report, "moderation_actions": moderation_actions, "form_complete": form_complete}
|
||||
context = {"report": report, "moderation_actions": moderation_actions,
|
||||
"form_complete": form_complete, "is_mod_or_above": is_mod_or_above}
|
||||
|
||||
return render(request, 'fellchensammlung/details/detail-report.html', context)
|
||||
|
||||
@ -481,13 +618,11 @@ def my_profile(request):
|
||||
notification = CommentNotification.objects.get(pk=notification_id)
|
||||
except CommentNotification.DoesNotExist:
|
||||
notification = BaseNotification.objects.get(pk=notification_id)
|
||||
notification.read = True
|
||||
notification.save()
|
||||
notification.mark_read()
|
||||
elif action == "notification_mark_all_read":
|
||||
notifications = CommentNotification.objects.filter(user=request.user, mark_read=False)
|
||||
for notification in notifications:
|
||||
notification.read = True
|
||||
notification.save()
|
||||
notification.mark_read()
|
||||
elif action == "search_subscription_delete":
|
||||
search_subscription_id = request.POST.get("search_subscription_id")
|
||||
SearchSubscription.objects.get(pk=search_subscription_id).delete()
|
||||
@ -502,7 +637,7 @@ def my_profile(request):
|
||||
|
||||
@user_passes_test(user_is_trust_level_or_above)
|
||||
def modqueue(request):
|
||||
open_reports = Report.objects.filter(status=Report.WAITING)
|
||||
open_reports = Report.objects.select_related("reportadoptionnotice", "reportcomment").filter(status=Report.WAITING)
|
||||
context = {"reports": open_reports}
|
||||
return render(request, 'fellchensammlung/modqueue.html', context=context)
|
||||
|
||||
@ -532,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):
|
||||
@ -632,3 +771,28 @@ def export_own_profile(request):
|
||||
ANs_as_json = serialize('json', ANs)
|
||||
full_json = f"{user_as_json}, {ANs_as_json}"
|
||||
return HttpResponse(full_json, content_type="application/json")
|
||||
|
||||
|
||||
def styleguide(request):
|
||||
context = {"geocoding_api_url": settings.GEOCODING_API_URL, }
|
||||
return render(request, 'fellchensammlung/styleguide.html', context=context)
|
||||
|
||||
|
||||
def styleguide_bulma(request):
|
||||
return render(request, 'fellchensammlung/bulma-styleguide.html')
|
||||
|
||||
|
||||
@login_required
|
||||
def rescue_organization_check(request):
|
||||
if request.method == "POST":
|
||||
rescue_org = RescueOrganization.objects.get(id=request.POST.get("rescue_organization_id"))
|
||||
edit_permission = user_is_trust_level_or_above(request.user, TrustLevel.MODERATOR)
|
||||
if not edit_permission:
|
||||
return render(request, "fellchensammlung/errors/403.html", status=403)
|
||||
action = request.POST.get("action")
|
||||
if action == "checked":
|
||||
rescue_org.set_checked()
|
||||
|
||||
last_checked_rescue_orgs = RescueOrganization.objects.order_by("last_checked")
|
||||
context = {"rescue_orgs": last_checked_rescue_orgs, }
|
||||
return render(request, 'fellchensammlung/rescue-organization-check.html', context=context)
|
||||
|
@ -89,9 +89,9 @@ CELERY_RESULT_BACKEND = config.get("celery", "backend", fallback="redis://localh
|
||||
HEALTHCHECKS_URL = config.get("monitoring", "healthchecks_url", fallback=None)
|
||||
|
||||
""" GEOCODING """
|
||||
GEOCODING_API_URL = config.get("geocoding", "api_url", fallback="https://nominatim.hyteck.de/search")
|
||||
GEOCODING_API_URL = config.get("geocoding", "api_url", fallback="https://photon.hyteck.de/api")
|
||||
# GEOCODING_API_FORMAT is allowed to be one of ['nominatim', 'photon']
|
||||
GEOCODING_API_FORMAT = config.get("geocoding", "api_format", fallback="nominatim")
|
||||
GEOCODING_API_FORMAT = config.get("geocoding", "api_format", fallback="photon")
|
||||
|
||||
""" Tile Server """
|
||||
MAP_TILE_SERVER = config.get("map", "tile_server", fallback="https://tiles.hyteck.de")
|
||||
@ -168,6 +168,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
"django.contrib.sitemaps",
|
||||
'fontawesomefree',
|
||||
'crispy_forms',
|
||||
"crispy_bootstrap4",
|
||||
@ -175,6 +176,7 @@ INSTALLED_APPS = [
|
||||
'rest_framework.authtoken',
|
||||
'drf_spectacular',
|
||||
'drf_spectacular_sidecar', # required for Django collectstatic discovery
|
||||
'widget_tweaks'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
@ -7,21 +7,28 @@
|
||||
<p>{% translate "Dein Username oder Passwort ist falsch." %}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
{% if user.is_authenticated %}
|
||||
<p>{% translate "Du bist bereits eingeloggt." %}</p>
|
||||
{% else %} {% if next %}
|
||||
<p>{% translate "Bitte log dich ein um diese Seite sehen zu können." %}</p>
|
||||
<p class="card">{% translate "Bitte log dich ein um diese Seite sehen zu können." %}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if not user.is_authenticated %}
|
||||
<form class="card" method="post" action="{% url 'login' %}">
|
||||
{% if not user.is_authenticated %}
|
||||
<div class="card">
|
||||
<div class="container-edit-buttons">
|
||||
<form method="post" action="{% url 'login' %}">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input class="btn" type="submit" value={% translate "Einloggen" %} />
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
<input class="btn" type="submit" value="{% translate 'Einloggen' %}"/>
|
||||
<input type="hidden" name="next" value="{{ next }}"/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p><a class="btn2" href="{% url 'password_reset' %}">{% translate "Passwort vergessen?" %}</a></p>
|
||||
{% endif %}
|
||||
<div class="container-edit-buttons">
|
||||
<a class="btn btn-small" href="{% url 'password_reset' %}">{% translate "Passwort vergessen?" %}</a>
|
||||
<a class="btn btn-small" href="{% url 'django_registration_register' %}">{% translate "Registrieren" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@ -27,7 +27,6 @@ class DistanceTest(TestCase):
|
||||
l_stuttgart = LocationProxy("Stuttgart")
|
||||
l_tue = LocationProxy("Tübingen")
|
||||
# Should be 30km
|
||||
print(f"{l_stuttgart.position} -> {l_tue.position}")
|
||||
distance_tue_stuttgart = calculate_distance_between_coordinates(l_stuttgart.position, l_tue.position)
|
||||
self.assertLess(distance_tue_stuttgart, 50)
|
||||
self.assertGreater(distance_tue_stuttgart, 20)
|
||||
|
@ -4,7 +4,7 @@ from django.utils import timezone
|
||||
from django.test import TestCase
|
||||
from model_bakery import baker
|
||||
|
||||
from fellchensammlung.models import Announcement, Language, User, TrustLevel
|
||||
from fellchensammlung.models import Announcement, Language, User, TrustLevel, BaseNotification
|
||||
|
||||
|
||||
class UserTest(TestCase):
|
||||
@ -77,3 +77,21 @@ class AnnouncementTest(TestCase):
|
||||
self.assertTrue(self.announcement2 not in active_announcements)
|
||||
self.assertTrue(self.announcement4 not in active_announcements)
|
||||
self.assertTrue(self.announcement5 in active_announcements)
|
||||
|
||||
|
||||
class TestNotifications(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.test_user_1 = User.objects.create(username="Testuser1", password="SUPERSECRET", email="test@example.org")
|
||||
|
||||
def test_mark_read(self):
|
||||
not1 = BaseNotification.objects.create(user=self.test_user_1, text="New rats to adopt", title="🔔 New Rat alert")
|
||||
not2 = BaseNotification.objects.create(user=self.test_user_1,
|
||||
text="New wombat to adopt", title="🔔 New Wombat alert")
|
||||
not1.mark_read()
|
||||
|
||||
self.assertTrue(not1.read)
|
||||
self.assertFalse(not2.read)
|
||||
self.assertTrue((timezone.now() - timedelta(hours=1)) < not1.read_at < timezone.now())
|
||||
self.assertIsNone(not2.read_at)
|
||||
|
||||
|
34
src/tests/test_tools_notifications.py
Normal file
@ -0,0 +1,34 @@
|
||||
from django.test import TestCase
|
||||
from model_bakery import baker
|
||||
|
||||
from fellchensammlung.models import User, TrustLevel, Species, Location, AdoptionNotice, AdoptionNoticeNotification
|
||||
from fellchensammlung.tools.notifications import notify_of_AN_to_be_checked
|
||||
|
||||
|
||||
class TestNotifications(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.test_user0 = User.objects.create_user(username='testuser0',
|
||||
first_name="Admin",
|
||||
last_name="BOFH",
|
||||
password='12345')
|
||||
|
||||
cls.test_user1 = User.objects.create_user(username='testuser1',
|
||||
first_name="Max",
|
||||
last_name="Müller",
|
||||
password='12345')
|
||||
cls.test_user2 = User.objects.create_user(username='testuser2',
|
||||
first_name="Miriam",
|
||||
last_name="Müller",
|
||||
password='12345')
|
||||
cls.test_user0.trust_level = TrustLevel.ADMIN
|
||||
cls.test_user0.save()
|
||||
|
||||
cls.adoption1 = baker.make(AdoptionNotice, name="TestAdoption1", owner=cls.test_user1,)
|
||||
cls.adoption1.set_unchecked() # Could also emit notification
|
||||
|
||||
def test_notify_of_AN_to_be_checked(self):
|
||||
notify_of_AN_to_be_checked(self.adoption1)
|
||||
self.assertTrue(AdoptionNoticeNotification.objects.filter(user=self.test_user0).exists())
|
||||
self.assertTrue(AdoptionNoticeNotification.objects.filter(user=self.test_user1).exists())
|
||||
self.assertFalse(AdoptionNoticeNotification.objects.filter(user=self.test_user2).exists())
|
@ -1,198 +0,0 @@
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.urls import reverse
|
||||
|
||||
from model_bakery import baker
|
||||
|
||||
from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus, TrustLevel
|
||||
from fellchensammlung.views import add_adoption_notice
|
||||
|
||||
|
||||
class AnimalAndAdoptionTest(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
test_user0 = User.objects.create_user(username='testuser0',
|
||||
first_name="Admin",
|
||||
last_name="BOFH",
|
||||
password='12345')
|
||||
|
||||
test_user1 = User.objects.create_user(username='testuser1',
|
||||
first_name="Max",
|
||||
last_name="Müller",
|
||||
password='12345')
|
||||
test_user0.trust_level = TrustLevel.ADMIN
|
||||
test_user0.save()
|
||||
|
||||
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1", owner=test_user0)
|
||||
rat = baker.make(Species, name="Farbratte")
|
||||
|
||||
rat1 = baker.make(Animal,
|
||||
name="Rat1",
|
||||
adoption_notice=adoption1,
|
||||
species=rat,
|
||||
description="Eine unglaublich süße Ratte")
|
||||
|
||||
def test_detail_animal(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
|
||||
response = self.client.post(reverse('animal-detail', args="1"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Check our user is logged in
|
||||
self.assertEqual(str(response.context['user']), 'testuser0')
|
||||
self.assertContains(response, "Rat1")
|
||||
|
||||
def test_detail_animal_notice(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
|
||||
response = self.client.post(reverse('adoption-notice-detail', args="1"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Check our user is logged in
|
||||
self.assertEqual(str(response.context['user']), 'testuser0')
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertContains(response, "Rat1")
|
||||
|
||||
def test_creating_AN_as_admin(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
|
||||
form_data = {"name": "TestAdoption4",
|
||||
"species": Species.objects.first().pk,
|
||||
"num_animals": "2",
|
||||
"date_of_birth": "2024-11-04",
|
||||
"sex": "M",
|
||||
"group_only": "on",
|
||||
"searching_since": "2024-11-10",
|
||||
"location_string": "Mannheim",
|
||||
"description": "Blaaaa",
|
||||
"further_information": "https://notfellchen.org",
|
||||
"save-and-add-another-animal": "Speichern"}
|
||||
|
||||
response = self.client.post(reverse('add-adoption'), data=form_data)
|
||||
|
||||
self.assertTrue(response.status_code < 400)
|
||||
self.assertTrue(AdoptionNotice.objects.get(name="TestAdoption4").is_active)
|
||||
|
||||
|
||||
|
||||
class SearchTest(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
test_user0 = User.objects.create_user(username='testuser0',
|
||||
first_name="Admin",
|
||||
last_name="BOFH",
|
||||
password='12345')
|
||||
test_user0.save()
|
||||
|
||||
# Location of Berlin: lat 52.5170365 lon 13.3888599 PLZ 10115 (Mitte)
|
||||
|
||||
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
|
||||
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2")
|
||||
adoption3 = baker.make(AdoptionNotice, name="TestAdoption3")
|
||||
|
||||
berlin = Location.get_location_from_string("Berlin")
|
||||
adoption1.location = berlin
|
||||
adoption1.save()
|
||||
|
||||
stuttgart = Location.get_location_from_string("Stuttgart")
|
||||
adoption3.location = stuttgart
|
||||
adoption3.save()
|
||||
|
||||
adoption1.set_active()
|
||||
adoption3.set_active()
|
||||
adoption2.set_unchecked()
|
||||
|
||||
def test_basic_view(self):
|
||||
response = self.client.get(reverse('search'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertNotContains(response, "TestAdoption2")
|
||||
self.assertContains(response, "TestAdoption3")
|
||||
|
||||
def test_basic_view_logged_in(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
response = self.client.get(reverse('search'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Check our user is logged in
|
||||
self.assertEqual(str(response.context['user']), 'testuser0')
|
||||
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertContains(response, "TestAdoption3")
|
||||
self.assertNotContains(response, "TestAdoption2")
|
||||
|
||||
def test_location_search(self):
|
||||
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# We can't use assertContains because TestAdoption3 will always be in response at is included in map
|
||||
# In order to test properly, we need to only care for the context that influences the list display
|
||||
an_names = [a.name for a in response.context["adoption_notices"]]
|
||||
self.assertTrue("TestAdoption1" in an_names) # Adoption in Berlin
|
||||
self.assertFalse("TestAdoption3" in an_names) # Adoption in Stuttgart
|
||||
|
||||
|
||||
class UpdateQueueTest(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
test_user0 = User.objects.create_user(username='testuser0',
|
||||
first_name="Admin",
|
||||
last_name="BOFH",
|
||||
password='12345',
|
||||
trust_level=TrustLevel.MODERATOR)
|
||||
test_user0.is_superuser = True
|
||||
test_user0.save()
|
||||
|
||||
# Location of Berlin: lat 52.5170365 lon 13.3888599 PLZ 10115 (Mitte)
|
||||
|
||||
cls.adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
|
||||
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2")
|
||||
cls.adoption3 = baker.make(AdoptionNotice, name="TestAdoption3")
|
||||
|
||||
cls.adoption1.set_unchecked()
|
||||
cls.adoption3.set_unchecked()
|
||||
|
||||
def test_login_required(self):
|
||||
response = self.client.get(reverse('updatequeue'))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, "/accounts/login/?next=/updatequeue/")
|
||||
|
||||
def test_set_updated(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
|
||||
# First get the list
|
||||
response = self.client.get(reverse('updatequeue'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Make sure Adoption1 is in response
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertNotContains(response, "TestAdoption2")
|
||||
|
||||
self.assertFalse(self.adoption1.is_active)
|
||||
|
||||
# Mark as checked
|
||||
response = self.client.post(reverse('updatequeue'), {"adoption_notice_id": self.adoption1.pk,
|
||||
"action": "checked_active"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.adoption1.refresh_from_db()
|
||||
self.assertTrue(self.adoption1.is_active)
|
||||
|
||||
def test_set_checked_inactive(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
# First get the list
|
||||
response = self.client.get(reverse('updatequeue'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Make sure Adoption3 is in response
|
||||
self.assertContains(response, "TestAdoption3")
|
||||
self.assertNotContains(response, "TestAdoption2")
|
||||
|
||||
self.assertFalse(self.adoption3.is_active)
|
||||
|
||||
# Mark as checked
|
||||
response = self.client.post(reverse('updatequeue'),
|
||||
{"adoption_notice_id": self.adoption3.id, "action": "checked_inactive"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.adoption3.refresh_from_db()
|
||||
|
||||
# Make sure correct status is set and AN is not shown anymore
|
||||
self.assertNotContains(response, "TestAdoption3")
|
||||
self.assertFalse(self.adoption3.is_active)
|
||||
self.assertEqual(self.adoption3.adoptionnoticestatus.major_status, AdoptionNoticeStatus.CLOSED)
|
||||
|
0
src/tests/test_views/__init__.py
Normal file
395
src/tests/test_views/test_advanced_views.py
Normal file
@ -0,0 +1,395 @@
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.urls import reverse
|
||||
|
||||
from model_bakery import baker
|
||||
|
||||
from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus, TrustLevel, \
|
||||
Animal, Subscriptions, Comment, CommentNotification, SearchSubscription
|
||||
from fellchensammlung.tools.geo import LocationProxy
|
||||
from fellchensammlung.views import add_adoption_notice
|
||||
|
||||
|
||||
class AnimalAndAdoptionTest(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
test_user0 = User.objects.create_user(username='testuser0',
|
||||
first_name="Admin",
|
||||
last_name="BOFH",
|
||||
password='12345')
|
||||
|
||||
test_user1 = User.objects.create_user(username='testuser1',
|
||||
first_name="Max",
|
||||
last_name="Müller",
|
||||
password='12345')
|
||||
test_user0.trust_level = TrustLevel.ADMIN
|
||||
test_user0.save()
|
||||
|
||||
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1", owner=test_user0)
|
||||
rat = baker.make(Species, name="Farbratte")
|
||||
|
||||
rat1 = baker.make(Animal,
|
||||
name="Rat1",
|
||||
adoption_notice=adoption1,
|
||||
species=rat,
|
||||
description="Eine unglaublich süße Ratte")
|
||||
|
||||
def test_detail_animal(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
|
||||
response = self.client.post(reverse('animal-detail', args="1"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Check our user is logged in
|
||||
self.assertEqual(str(response.context['user']), 'testuser0')
|
||||
self.assertContains(response, "Rat1")
|
||||
|
||||
def test_detail_animal_notice(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
|
||||
response = self.client.post(reverse('adoption-notice-detail', args="1"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Check our user is logged in
|
||||
self.assertEqual(str(response.context['user']), 'testuser0')
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertContains(response, "Rat1")
|
||||
|
||||
def test_creating_AN_as_admin(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
|
||||
form_data = {"name": "TestAdoption4",
|
||||
"species": Species.objects.first().pk,
|
||||
"num_animals": "2",
|
||||
"date_of_birth": "2024-11-04",
|
||||
"sex": "M",
|
||||
"group_only": "on",
|
||||
"searching_since": "2024-11-10",
|
||||
"location_string": "Mannheim",
|
||||
"description": "Blaaaa",
|
||||
"further_information": "https://notfellchen.org",
|
||||
"save-and-add-another-animal": "Speichern"}
|
||||
|
||||
response = self.client.post(reverse('add-adoption'), data=form_data)
|
||||
|
||||
self.assertTrue(response.status_code < 400)
|
||||
self.assertTrue(AdoptionNotice.objects.get(name="TestAdoption4").is_active)
|
||||
an = AdoptionNotice.objects.get(name="TestAdoption4")
|
||||
animals = Animal.objects.filter(adoption_notice=an)
|
||||
self.assertTrue(len(animals) == 2)
|
||||
|
||||
def test_creating_AN_as_user(self):
|
||||
self.client.login(username='testuser1', password='12345')
|
||||
|
||||
form_data = {"name": "TestAdoption5",
|
||||
"species": Species.objects.first().pk,
|
||||
"num_animals": "3",
|
||||
"date_of_birth": "2024-12-04",
|
||||
"sex": "M",
|
||||
"group_only": "on",
|
||||
"searching_since": "2024-11-10",
|
||||
"location_string": "München",
|
||||
"description": "Blaaaa",
|
||||
"further_information": "https://notfellchen.org/",
|
||||
"save-and-add-another-animal": "Speichern"}
|
||||
|
||||
response = self.client.post(reverse('add-adoption'), data=form_data)
|
||||
|
||||
self.assertTrue(response.status_code < 400)
|
||||
self.assertFalse(AdoptionNotice.objects.get(name="TestAdoption5").is_active)
|
||||
an = AdoptionNotice.objects.get(name="TestAdoption5")
|
||||
animals = Animal.objects.filter(adoption_notice=an)
|
||||
self.assertTrue(len(animals) == 3)
|
||||
self.assertTrue(an.sexes == set("M", ))
|
||||
|
||||
|
||||
class SearchTest(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
test_user0 = User.objects.create_user(username='testuser0',
|
||||
first_name="Admin",
|
||||
last_name="BOFH",
|
||||
password='12345')
|
||||
test_user0.save()
|
||||
|
||||
# Location of Berlin: lat 52.5170365 lon 13.3888599 PLZ 10115 (Mitte)
|
||||
|
||||
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
|
||||
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2")
|
||||
adoption3 = baker.make(AdoptionNotice, name="TestAdoption3")
|
||||
|
||||
berlin = Location.get_location_from_string("Berlin")
|
||||
adoption1.location = berlin
|
||||
adoption1.save()
|
||||
|
||||
stuttgart = Location.get_location_from_string("Stuttgart")
|
||||
adoption3.location = stuttgart
|
||||
adoption3.save()
|
||||
|
||||
adoption1.set_active()
|
||||
adoption3.set_active()
|
||||
adoption2.set_unchecked()
|
||||
|
||||
def test_basic_view(self):
|
||||
response = self.client.get(reverse('search'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertNotContains(response, "TestAdoption2")
|
||||
self.assertContains(response, "TestAdoption3")
|
||||
|
||||
def test_basic_view_logged_in(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
response = self.client.get(reverse('search'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Check our user is logged in
|
||||
self.assertEqual(str(response.context['user']), 'testuser0')
|
||||
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertContains(response, "TestAdoption3")
|
||||
self.assertNotContains(response, "TestAdoption2")
|
||||
|
||||
def test_unauthenticated_subscribe(self):
|
||||
response = self.client.post(reverse('search'), {"subscribe_to_search": ""})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, "/accounts/login/?next=/suchen/")
|
||||
|
||||
def test_unauthenticated_unsubscribe(self):
|
||||
response = self.client.post(reverse('search'), {"unsubscribe_to_search": 1})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, "/accounts/login/?next=/suchen/")
|
||||
|
||||
def test_subscribe(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A",
|
||||
"subscribe_to_search": ""})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(SearchSubscription.objects.filter(owner=User.objects.get(username='testuser0'),
|
||||
max_distance=50).exists())
|
||||
|
||||
def test_unsubscribe(self):
|
||||
user0 = User.objects.get(username='testuser0')
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
location = Location.get_location_from_string("München")
|
||||
subscription = SearchSubscription.objects.create(owner=user0, max_distance=200, location=location, sex="A")
|
||||
response = self.client.post(reverse('search'), {"max_distance": 200, "location_string": "München", "sex": "A",
|
||||
"unsubscribe_to_search": subscription.pk})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(SearchSubscription.objects.filter(owner=User.objects.get(username='testuser0'),
|
||||
max_distance=200).exists())
|
||||
|
||||
def test_location_search(self):
|
||||
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# We can't use assertContains because TestAdoption3 will always be in response at is included in map
|
||||
# In order to test properly, we need to only care for the context that influences the list display
|
||||
an_names = [a.name for a in response.context["adoption_notices"]]
|
||||
self.assertTrue("TestAdoption1" in an_names) # Adoption in Berlin
|
||||
self.assertFalse("TestAdoption3" in an_names) # Adoption in Stuttgart
|
||||
|
||||
|
||||
class UpdateQueueTest(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
test_user0 = User.objects.create_user(username='testuser0',
|
||||
first_name="Admin",
|
||||
last_name="BOFH",
|
||||
password='12345',
|
||||
trust_level=TrustLevel.MODERATOR)
|
||||
test_user0.is_superuser = True
|
||||
test_user0.save()
|
||||
|
||||
# Location of Berlin: lat 52.5170365 lon 13.3888599 PLZ 10115 (Mitte)
|
||||
|
||||
cls.adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
|
||||
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2")
|
||||
cls.adoption3 = baker.make(AdoptionNotice, name="TestAdoption3")
|
||||
|
||||
cls.adoption1.set_unchecked()
|
||||
cls.adoption3.set_unchecked()
|
||||
|
||||
def test_login_required(self):
|
||||
response = self.client.get(reverse('updatequeue'))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, "/accounts/login/?next=/updatequeue/")
|
||||
|
||||
def test_set_updated(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
|
||||
# First get the list
|
||||
response = self.client.get(reverse('updatequeue'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Make sure Adoption1 is in response
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertNotContains(response, "TestAdoption2")
|
||||
|
||||
self.assertFalse(self.adoption1.is_active)
|
||||
|
||||
# Mark as checked
|
||||
response = self.client.post(reverse('updatequeue'), {"adoption_notice_id": self.adoption1.pk,
|
||||
"action": "checked_active"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.adoption1.refresh_from_db()
|
||||
self.assertTrue(self.adoption1.is_active)
|
||||
|
||||
def test_set_checked_inactive(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
# First get the list
|
||||
response = self.client.get(reverse('updatequeue'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Make sure Adoption3 is in response
|
||||
self.assertContains(response, "TestAdoption3")
|
||||
self.assertNotContains(response, "TestAdoption2")
|
||||
|
||||
self.assertFalse(self.adoption3.is_active)
|
||||
|
||||
# Mark as checked
|
||||
response = self.client.post(reverse('updatequeue'),
|
||||
{"adoption_notice_id": self.adoption3.id, "action": "checked_inactive"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.adoption3.refresh_from_db()
|
||||
|
||||
# Make sure correct status is set and AN is not shown anymore
|
||||
self.assertNotContains(response, "TestAdoption3")
|
||||
self.assertFalse(self.adoption3.is_active)
|
||||
self.assertEqual(self.adoption3.adoptionnoticestatus.major_status, AdoptionNoticeStatus.CLOSED)
|
||||
|
||||
|
||||
class AdoptionDetailTest(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
test_user0 = User.objects.create_user(username='testuser0',
|
||||
first_name="Admin",
|
||||
last_name="BOFH",
|
||||
password='12345')
|
||||
test_user0.save()
|
||||
|
||||
cls.test_user1 = User.objects.create_user(username='testuser1',
|
||||
first_name="Max",
|
||||
last_name="Müller",
|
||||
password='12345')
|
||||
test_user0.trust_level = TrustLevel.ADMIN
|
||||
test_user0.save()
|
||||
|
||||
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
|
||||
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2")
|
||||
adoption3 = baker.make(AdoptionNotice, name="TestAdoption3")
|
||||
|
||||
berlin = Location.get_location_from_string("Berlin")
|
||||
adoption1.location = berlin
|
||||
adoption1.save()
|
||||
|
||||
stuttgart = Location.get_location_from_string("Stuttgart")
|
||||
adoption3.location = stuttgart
|
||||
adoption3.save()
|
||||
|
||||
adoption1.set_active()
|
||||
adoption3.set_active()
|
||||
adoption2.set_unchecked()
|
||||
|
||||
def test_basic_view(self):
|
||||
response = self.client.get(
|
||||
reverse('adoption-notice-detail', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)), )
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_basic_view_logged_in(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
response = self.client.get(
|
||||
reverse('adoption-notice-detail', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)), )
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_subscribe(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
response = self.client.post(
|
||||
reverse('adoption-notice-detail', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)),
|
||||
data={"action": "subscribe"})
|
||||
self.assertTrue(Subscriptions.objects.filter(owner__username="testuser0").exists())
|
||||
|
||||
def test_unsubscribe(self):
|
||||
# Make sure subscription exists
|
||||
an = AdoptionNotice.objects.get(name="TestAdoption1")
|
||||
user = User.objects.get(username="testuser0")
|
||||
subscription = Subscriptions.objects.get_or_create(owner=user, adoption_notice=an)
|
||||
|
||||
# Unsubscribe
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
response = self.client.post(
|
||||
reverse('adoption-notice-detail', args=str(an.pk)),
|
||||
data={"action": "unsubscribe"})
|
||||
self.assertFalse(Subscriptions.objects.filter(owner__username="testuser0").exists())
|
||||
|
||||
def test_login_required(self):
|
||||
response = self.client.post(
|
||||
reverse('adoption-notice-detail', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)),
|
||||
data={"action": "subscribe"})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, "/accounts/login/?next=/vermittlung/1/")
|
||||
|
||||
def test_unauthenticated_comment(self):
|
||||
response = self.client.post(
|
||||
reverse('adoption-notice-detail', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)),
|
||||
data={"action": "comment"})
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_comment(self):
|
||||
an1 = AdoptionNotice.objects.get(name="TestAdoption1")
|
||||
# Set up subscription
|
||||
Subscriptions.objects.create(owner=self.test_user1, adoption_notice=an1)
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
response = self.client.post(
|
||||
reverse('adoption-notice-detail', args=str(an1.pk)),
|
||||
data={"action": "comment", "text": "Test"})
|
||||
self.assertTrue(Comment.objects.filter(user__username="testuser0").exists())
|
||||
self.assertFalse(CommentNotification.objects.filter(user__username="testuser0").exists())
|
||||
self.assertTrue(CommentNotification.objects.filter(user__username="testuser1").exists())
|
||||
|
||||
|
||||
class AdoptionEditTest(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
test_user0 = User.objects.create_user(username='testuser0',
|
||||
first_name="Admin",
|
||||
last_name="BOFH",
|
||||
password='12345')
|
||||
test_user0.save()
|
||||
|
||||
cls.test_user1 = User.objects.create_user(username='testuser1',
|
||||
first_name="Max",
|
||||
last_name="Müller",
|
||||
password='12345')
|
||||
|
||||
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1", description="Test1", owner=test_user0)
|
||||
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2", description="Test2")
|
||||
|
||||
def test_basic_view(self):
|
||||
response = self.client.get(
|
||||
reverse('adoption-notice-edit', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)), )
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_basic_view_logged_in_unauthorized(self):
|
||||
self.client.login(username='testuser1', password='12345')
|
||||
response = self.client.get(
|
||||
reverse('adoption-notice-edit', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)), )
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_basic_view_logged_in(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
response = self.client.get(
|
||||
reverse('adoption-notice-edit', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)), )
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_edit(self):
|
||||
data = {"name": "Mia",
|
||||
"searching_since": "01.01.2025",
|
||||
"location_string": "Paderborn",
|
||||
"organization": "",
|
||||
"description": "Test3",
|
||||
"further_information": ""}
|
||||
an = AdoptionNotice.objects.get(name="TestAdoption1")
|
||||
assert self.client.login(username='testuser0', password='12345')
|
||||
response = self.client.post(reverse("adoption-notice-edit", args=str(an.pk)), data=data, follow=True)
|
||||
self.assertEqual(response.redirect_chain[0][1],
|
||||
302) # See https://docs.djangoproject.com/en/5.1/topics/testing/tools/
|
||||
self.assertEqual(response.status_code, 200) # Redirects to AN page
|
||||
self.assertContains(response, "Test3")
|
||||
self.assertContains(response, "Mia")
|
||||
self.assertNotContains(response, "Test1")
|
136
src/tests/test_views/test_basic_views.py
Normal file
@ -0,0 +1,136 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from docs.conf import language
|
||||
from fellchensammlung.models import User, TrustLevel, AdoptionNotice, Species, Rule, Language, Comment, ReportComment
|
||||
from model_bakery import baker
|
||||
|
||||
|
||||
class BasicViewTest(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
test_user0 = User.objects.create_user(username='testuser0',
|
||||
first_name="Admin",
|
||||
last_name="BOFH",
|
||||
password='12345')
|
||||
|
||||
test_user1 = User.objects.create_user(username='testuser1',
|
||||
first_name="Max",
|
||||
last_name="Müller",
|
||||
password='12345')
|
||||
test_user0.trust_level = TrustLevel.ADMIN
|
||||
test_user0.save()
|
||||
|
||||
ans = []
|
||||
for i in range(0, 8):
|
||||
ans.append(baker.make(AdoptionNotice, name=f"TestAdoption{i}"))
|
||||
for i in range(0, 4):
|
||||
AdoptionNotice.objects.get(name=f"TestAdoption{i}").set_active()
|
||||
|
||||
rule1 = Rule.objects.create(title="Rule 1", rule_text="Description of r1", rule_identifier="rule1",
|
||||
language=Language.objects.get(name="English"))
|
||||
|
||||
an1 = AdoptionNotice.objects.get(name="TestAdoption0")
|
||||
comment1 = Comment.objects.create(adoption_notice=an1, text="Comment1", user=test_user1)
|
||||
comment2 = Comment.objects.create(adoption_notice=an1, text="Comment2", user=test_user1)
|
||||
comment3 = Comment.objects.create(adoption_notice=an1, text="Comment3", user=test_user1)
|
||||
|
||||
report_comment1 = ReportComment.objects.create(reported_comment=comment1,
|
||||
user_comment="ReportComment1")
|
||||
report_comment1.save()
|
||||
report_comment1.reported_broken_rules.set({rule1,})
|
||||
|
||||
def test_index_logged_in(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
|
||||
response = self.client.get(reverse('index'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Check our user is logged in
|
||||
self.assertEqual(str(response.context['user']), 'testuser0')
|
||||
self.assertContains(response, "TestAdoption0")
|
||||
self.assertNotContains(response, "TestAdoption5") # Should not be active, therefore not shown
|
||||
|
||||
def test_index_anonymous(self):
|
||||
response = self.client.get(reverse('index'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertNotContains(response, "TestAdoption4") # Should not be active, therefore not shown
|
||||
|
||||
def test_about_logged_in(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
response = self.client.get(reverse('about'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Rule 1")
|
||||
|
||||
def test_about_anonymous(self):
|
||||
response = self.client.get(reverse('about'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Rule 1")
|
||||
|
||||
def test_report_adoption_logged_in(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
an = AdoptionNotice.objects.get(name="TestAdoption0")
|
||||
response = self.client.get(reverse('report-adoption-notice', args=str(an.pk)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = {"reported_broken_rules": 1, "user_comment": "animal cruelty"}
|
||||
response = self.client.post(reverse('report-adoption-notice', args=str(an.pk)), data=data)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_report_adoption_anonymous(self):
|
||||
an = AdoptionNotice.objects.get(name="TestAdoption0")
|
||||
response = self.client.get(reverse('report-adoption-notice', args=str(an.pk)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = {"reported_broken_rules": 1, "user_comment": "animal cruelty"}
|
||||
response = self.client.post(reverse('report-adoption-notice', args=str(an.pk)), data=data)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_report_comment_logged_in(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
c = Comment.objects.get(text="Comment1")
|
||||
response = self.client.get(reverse('report-comment', args=str(c.pk)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = {"reported_broken_rules": 1, "user_comment": "animal cruelty"}
|
||||
response = self.client.post(reverse('report-comment', args=str(c.pk)), data=data)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(ReportComment.objects.filter(reported_comment=c.pk).exists())
|
||||
|
||||
def test_report_comment_anonymous(self):
|
||||
c = Comment.objects.get(text="Comment2")
|
||||
response = self.client.get(reverse('report-comment', args=str(c.pk)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = {"reported_broken_rules": 1, "user_comment": "animal cruelty"}
|
||||
response = self.client.post(reverse('report-comment', args=str(c.pk)), data=data)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(ReportComment.objects.filter(reported_comment=c.pk).exists())
|
||||
|
||||
def test_show_report_details_logged_in(self):
|
||||
self.client.login(username='testuser1', password='12345')
|
||||
report = ReportComment.objects.get(user_comment="ReportComment1")
|
||||
response = self.client.get(reverse('report-detail', args=(report.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Rule 1")
|
||||
self.assertContains(response, "ReportComment1")
|
||||
self.assertNotContains(response, '<form action="allow" class="">')
|
||||
|
||||
def test_show_report_details_anonymous(self):
|
||||
report = ReportComment.objects.get(user_comment="ReportComment1")
|
||||
response = self.client.get(reverse('report-detail', args=(report.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Rule 1")
|
||||
self.assertContains(response, "ReportComment1")
|
||||
self.assertNotContains(response, '<form action="allow" class="">')
|
||||
|
||||
def test_show_report_details_admin(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
report = ReportComment.objects.get(user_comment="ReportComment1")
|
||||
response = self.client.get(reverse('report-detail', args=(report.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Rule 1")
|
||||
self.assertContains(response, "ReportComment1")
|
||||
self.assertContains(response, '<form action="allow" class="">')
|
||||
|
||||
|