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
|
notfellchen
|
||||||
|
|
||||||
# Media storage
|
# Media storage
|
||||||
static
|
/static
|
||||||
media
|
media
|
||||||
|
|
||||||
|
|
||||||
|
20
README.md
@ -77,6 +77,26 @@ docker push moanos/notfellchen:latest
|
|||||||
docker run -p8000:7345 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
|
||||||
|
|
||||||
Geocoding services (search map data by name, address or postcode) are provided via the
|
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
|
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
|
Get Adoption Notices
|
||||||
++++++++++++++++++++
|
++++++++++++++++++++
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ Report a bug
|
|||||||
^^^^^^^^^^^^
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
To report a bug, file an issue on `Github
|
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:
|
Try to include the following information:
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ To contribute simply clone the directory, make your changes and file a
|
|||||||
pull request.
|
pull request.
|
||||||
|
|
||||||
If you want to know what can be done, have a look at the current `Github
|
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!
|
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
|
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.
|
is made. Notfellchen follows `Semantic Versioning <https://semver.org/>`_.
|
||||||
Major releases are yet to be determined.
|
|
||||||
|
|
||||||
What should be done before a release?
|
What should be done before a release?
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
@ -14,7 +13,7 @@ What should be done before a release?
|
|||||||
Tested basic functions
|
Tested basic functions
|
||||||
######################
|
######################
|
||||||
|
|
||||||
Run :command:`pytest`
|
Run :command:`nf test src`
|
||||||
|
|
||||||
Test upgrade on a copy of a production database
|
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 tag -a v1.0.0 -m "Releasing version v1.0.0"
|
||||||
git push origin 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"
|
name = "notfellchen"
|
||||||
description = "A tool to help."
|
description = "A tool to help."
|
||||||
authors = [
|
authors = [
|
||||||
{name = "moanos", email = "julian-samuel@gebuehr.net"},
|
{ name = "moanos", email = "julian-samuel@gebuehr.net" },
|
||||||
]
|
]
|
||||||
maintainers = [
|
maintainers = [
|
||||||
{name = "moanos", email = "julian-samuel@gebuehr.net"},
|
{ name = "moanos", email = "julian-samuel@gebuehr.net" },
|
||||||
]
|
]
|
||||||
keywords = ["animal", "adoption", "django", "rescue", ]
|
keywords = ["animal", "adoption", "django", "rescue", ]
|
||||||
license = {text = "AGPL-3.0-or-later"}
|
license = { text = "AGPL-3.0-or-later" }
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Environment :: Web",
|
"Environment :: Web",
|
||||||
"License :: OSI Approved :: GNU Affero General Public License v3",
|
"License :: OSI Approved :: GNU Affero General Public License v3",
|
||||||
@ -24,14 +24,12 @@ classifiers = [
|
|||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"Django",
|
"Django",
|
||||||
"coverage",
|
|
||||||
"codecov",
|
"codecov",
|
||||||
"sphinx",
|
"sphinx",
|
||||||
"sphinx-rtd-theme",
|
"sphinx-rtd-theme",
|
||||||
"gunicorn",
|
"gunicorn",
|
||||||
"fontawesomefree",
|
"fontawesomefree",
|
||||||
"whitenoise",
|
"whitenoise",
|
||||||
"model_bakery",
|
|
||||||
"markdown",
|
"markdown",
|
||||||
"Pillow",
|
"Pillow",
|
||||||
"django-registration",
|
"django-registration",
|
||||||
@ -47,7 +45,9 @@ dynamic = ["version", "readme"]
|
|||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
develop = [
|
develop = [
|
||||||
"pytest",
|
"pytest",
|
||||||
|
"coverage",
|
||||||
|
"model_bakery",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
@ -62,6 +62,6 @@ nf = 'notfellchen.main:main'
|
|||||||
|
|
||||||
|
|
||||||
[tool.setuptools.dynamic]
|
[tool.setuptools.dynamic]
|
||||||
version = {attr = "notfellchen.__version__"}
|
version = { attr = "notfellchen.__version__" }
|
||||||
readme = {file = "README.md"}
|
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 django.utils.http import urlencode
|
||||||
|
|
||||||
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
|
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
|
||||||
SpeciesSpecificURL
|
SpeciesSpecificURL, ImportantLocation
|
||||||
|
|
||||||
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
|
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
|
||||||
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, BaseNotification
|
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, BaseNotification
|
||||||
@ -94,12 +94,14 @@ class ReportAdoptionNoticeAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
reported_content_link.short_description = "Reported Content"
|
reported_content_link.short_description = "Reported Content"
|
||||||
|
|
||||||
|
|
||||||
class SpeciesSpecificURLInline(admin.StackedInline):
|
class SpeciesSpecificURLInline(admin.StackedInline):
|
||||||
model = SpeciesSpecificURL
|
model = SpeciesSpecificURL
|
||||||
|
|
||||||
|
|
||||||
@admin.register(RescueOrganization)
|
@admin.register(RescueOrganization)
|
||||||
class RescueOrganizationAdmin(admin.ModelAdmin):
|
class RescueOrganizationAdmin(admin.ModelAdmin):
|
||||||
search_fields = ("name","description", "internal_comment", "location_string")
|
search_fields = ("name", "description", "internal_comment", "location_string")
|
||||||
list_display = ("name", "trusted", "allows_using_materials", "website")
|
list_display = ("name", "trusted", "allows_using_materials", "website")
|
||||||
list_filter = ("allows_using_materials", "trusted",)
|
list_filter = ("allows_using_materials", "trusted",)
|
||||||
|
|
||||||
@ -122,14 +124,46 @@ class CommentAdmin(admin.ModelAdmin):
|
|||||||
class BaseNotificationAdmin(admin.ModelAdmin):
|
class BaseNotificationAdmin(admin.ModelAdmin):
|
||||||
list_filter = ("user", "read")
|
list_filter = ("user", "read")
|
||||||
|
|
||||||
|
|
||||||
@admin.register(SearchSubscription)
|
@admin.register(SearchSubscription)
|
||||||
class SearchSubscriptionAdmin(admin.ModelAdmin):
|
class SearchSubscriptionAdmin(admin.ModelAdmin):
|
||||||
list_filter = ("owner",)
|
list_filter = ("owner",)
|
||||||
|
|
||||||
|
|
||||||
|
class ImportantLocationInline(admin.StackedInline):
|
||||||
|
model = ImportantLocation
|
||||||
|
|
||||||
|
|
||||||
|
class IsImportantListFilter(admin.SimpleListFilter):
|
||||||
|
# See https://docs.djangoproject.com/en/5.1/ref/contrib/admin/filters/#modeladmin-list-filters
|
||||||
|
title = _('Is Important Location?')
|
||||||
|
|
||||||
|
parameter_name = 'important'
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
return (
|
||||||
|
('is_important', _('Important Location')),
|
||||||
|
('is_normal', _('Normal Location')),
|
||||||
|
)
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value() == 'is_important':
|
||||||
|
return queryset.filter(importantlocation__isnull=False)
|
||||||
|
else:
|
||||||
|
return queryset.filter(importantlocation__isnull=True)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Location)
|
||||||
|
class LocationAdmin(admin.ModelAdmin):
|
||||||
|
search_fields = ("name__icontains", "city__icontains")
|
||||||
|
list_filter = [IsImportantListFilter]
|
||||||
|
inlines = [
|
||||||
|
ImportantLocationInline,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Animal)
|
admin.site.register(Animal)
|
||||||
admin.site.register(Species)
|
admin.site.register(Species)
|
||||||
admin.site.register(Location)
|
|
||||||
admin.site.register(Rule)
|
admin.site.register(Rule)
|
||||||
admin.site.register(Image)
|
admin.site.register(Image)
|
||||||
admin.site.register(ModerationAction)
|
admin.site.register(ModerationAction)
|
||||||
|
@ -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
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
|
class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
location = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
location_details = serializers.StringRelatedField(source='location', read_only=True)
|
||||||
|
organization = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=RescueOrganization.objects.all(),
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
organization = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=RescueOrganization.objects.all(),
|
||||||
|
required=False,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
photos = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=Image.objects.all(),
|
||||||
|
many=True,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AdoptionNotice
|
model = AdoptionNotice
|
||||||
fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information",
|
fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information",
|
||||||
"group_only"]
|
"group_only", "location", "location_details", "organization", "photos"]
|
||||||
|
|
||||||
|
|
||||||
class AnimalCreateSerializer(serializers.ModelSerializer):
|
class AnimalCreateSerializer(serializers.ModelSerializer):
|
||||||
@ -14,12 +37,14 @@ class AnimalCreateSerializer(serializers.ModelSerializer):
|
|||||||
model = Animal
|
model = Animal
|
||||||
fields = ["name", "date_of_birth", "description", "species", "sex", "adoption_notice"]
|
fields = ["name", "date_of_birth", "description", "species", "sex", "adoption_notice"]
|
||||||
|
|
||||||
|
|
||||||
class RescueOrgSerializer(serializers.ModelSerializer):
|
class RescueOrgSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RescueOrganization
|
model = RescueOrganization
|
||||||
fields = ["name", "location_string", "instagram", "facebook", "fediverse_profile", "email", "phone_number",
|
fields = ["name", "location_string", "instagram", "facebook", "fediverse_profile", "email", "phone_number",
|
||||||
"website", "description", "external_object_identifier", "external_source_identifier"]
|
"website", "description", "external_object_identifier", "external_source_identifier"]
|
||||||
|
|
||||||
|
|
||||||
class AnimalGetSerializer(serializers.ModelSerializer):
|
class AnimalGetSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Animal
|
model = Animal
|
||||||
@ -51,3 +76,9 @@ class SpeciesSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Species
|
model = Species
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class LocationSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Location
|
||||||
|
fields = "__all__"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from .views import (
|
from .views import (
|
||||||
AdoptionNoticeApiView,
|
AdoptionNoticeApiView,
|
||||||
AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView
|
AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView, LocationApiView
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -13,4 +13,5 @@ urlpatterns = [
|
|||||||
path("organizations/<int:id>/", RescueOrganizationApiView.as_view(), name="api-organization-detail"),
|
path("organizations/<int:id>/", RescueOrganizationApiView.as_view(), name="api-organization-detail"),
|
||||||
path("images/", AddImageApiView.as_view(), name="api-add-image"),
|
path("images/", AddImageApiView.as_view(), name="api-add-image"),
|
||||||
path("species/", SpeciesApiView.as_view(), name="api-species-list"),
|
path("species/", SpeciesApiView.as_view(), name="api-species-list"),
|
||||||
|
path("locations/", LocationApiView.as_view(), name="api-locations-list"),
|
||||||
]
|
]
|
||||||
|
@ -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.views import APIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel
|
from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel, Location
|
||||||
from fellchensammlung.tasks import post_adoption_notice_save
|
from fellchensammlung.tasks import post_adoption_notice_save, post_rescue_org_save
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
@ -16,6 +19,7 @@ from .serializers import (
|
|||||||
from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image
|
from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema
|
||||||
|
|
||||||
|
|
||||||
class AdoptionNoticeApiView(APIView):
|
class AdoptionNoticeApiView(APIView):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
@ -84,7 +88,6 @@ class AdoptionNoticeApiView(APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AnimalApiView(APIView):
|
class AnimalApiView(APIView):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
@ -118,6 +121,7 @@ class AnimalApiView(APIView):
|
|||||||
)
|
)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
class RescueOrganizationApiView(APIView):
|
class RescueOrganizationApiView(APIView):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
@ -129,14 +133,44 @@ class RescueOrganizationApiView(APIView):
|
|||||||
'description': 'ID of the rescue organization to retrieve.',
|
'description': 'ID of the rescue organization to retrieve.',
|
||||||
'type': int
|
'type': int
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'trusted',
|
||||||
|
'required': False,
|
||||||
|
'description': 'Filter by trusted status (true/false).',
|
||||||
|
'type': bool
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'external_object_identifier',
|
||||||
|
'required': False,
|
||||||
|
'description': 'Filter by external object identifier. Use "None" to filter for an empty field',
|
||||||
|
'type': str
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'external_source_identifier',
|
||||||
|
'required': False,
|
||||||
|
'description': 'Filter by external source identifier. Use "None" to filter for an empty field',
|
||||||
|
'type': str
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'search',
|
||||||
|
'required': False,
|
||||||
|
'description': 'Search by organization name or location name/city.',
|
||||||
|
'type': str
|
||||||
|
},
|
||||||
],
|
],
|
||||||
responses={200: RescueOrganizationSerializer(many=True)}
|
responses={200: RescueOrganizationSerializer(many=True)}
|
||||||
)
|
)
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Get list of rescue organizations or a specific organization by ID.
|
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:
|
if org_id:
|
||||||
try:
|
try:
|
||||||
organization = RescueOrganization.objects.get(pk=org_id)
|
organization = RescueOrganization.objects.get(pk=org_id)
|
||||||
@ -144,14 +178,33 @@ class RescueOrganizationApiView(APIView):
|
|||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
except RescueOrganization.DoesNotExist:
|
except RescueOrganization.DoesNotExist:
|
||||||
return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
|
return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
organizations = RescueOrganization.objects.all()
|
organizations = RescueOrganization.objects.all()
|
||||||
|
|
||||||
|
if external_object_identifier:
|
||||||
|
if external_object_identifier == "None":
|
||||||
|
external_object_identifier = None
|
||||||
|
organizations = organizations.filter(external_object_identifier=external_object_identifier)
|
||||||
|
|
||||||
|
if external_source_identifier:
|
||||||
|
if external_source_identifier == "None":
|
||||||
|
external_source_identifier = None
|
||||||
|
organizations = organizations.filter(external_source_identifier=external_source_identifier)
|
||||||
|
if search_query:
|
||||||
|
organizations = organizations.filter(
|
||||||
|
Q(name__icontains=search_query) |
|
||||||
|
Q(location_string__icontains=search_query) |
|
||||||
|
Q(location__name__icontains=search_query) |
|
||||||
|
Q(location__city__icontains=search_query)
|
||||||
|
)
|
||||||
|
|
||||||
serializer = RescueOrganizationSerializer(organizations, many=True, context={"request": request})
|
serializer = RescueOrganizationSerializer(organizations, many=True, context={"request": request})
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
request=RescueOrgSerializer, # Document the request body
|
request=RescueOrgSerializer,
|
||||||
responses={201: 'Rescue organization created/updated successfully!'}
|
responses={201: 'Rescue organization created successfully!'}
|
||||||
)
|
)
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -159,11 +212,39 @@ class RescueOrganizationApiView(APIView):
|
|||||||
"""
|
"""
|
||||||
serializer = RescueOrgSerializer(data=request.data, context={"request": request})
|
serializer = RescueOrgSerializer(data=request.data, context={"request": request})
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
rescue_org = serializer.save(owner=request.user)
|
rescue_org = serializer.save()
|
||||||
|
# Add the location
|
||||||
|
post_rescue_org_save.delay_on_commit(rescue_org.pk)
|
||||||
return Response(
|
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,
|
status=status.HTTP_201_CREATED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
@extend_schema(
|
||||||
|
request=RescueOrgSerializer,
|
||||||
|
responses={200: 'Rescue organization updated successfully!'}
|
||||||
|
)
|
||||||
|
def patch(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Partially update a rescue organization.
|
||||||
|
"""
|
||||||
|
org_id = kwargs.get("id")
|
||||||
|
if not org_id:
|
||||||
|
return Response({"error": "ID is required for updating."}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
try:
|
||||||
|
organization = RescueOrganization.objects.get(pk=org_id)
|
||||||
|
except RescueOrganization.DoesNotExist:
|
||||||
|
return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
serializer = RescueOrgSerializer(organization, data=request.data, partial=True, context={"request": request})
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response({"message": "Rescue organization updated successfully!"}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
class AddImageApiView(APIView):
|
class AddImageApiView(APIView):
|
||||||
@ -210,3 +291,63 @@ class SpeciesApiView(APIView):
|
|||||||
species = Species.objects.all()
|
species = Species.objects.all()
|
||||||
serializer = SpeciesSerializer(species, many=True, context={"request": request})
|
serializer = SpeciesSerializer(species, many=True, context={"request": request})
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
class LocationApiView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
parameters=[
|
||||||
|
{
|
||||||
|
'name': 'id',
|
||||||
|
'required': False,
|
||||||
|
'description': 'ID of the location to retrieve.',
|
||||||
|
'type': int
|
||||||
|
},
|
||||||
|
],
|
||||||
|
responses={200: LocationSerializer(many=True)}
|
||||||
|
)
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Retrieve a location
|
||||||
|
"""
|
||||||
|
location_id = kwargs.get("id")
|
||||||
|
if location_id:
|
||||||
|
try:
|
||||||
|
location = Location.objects.get(pk=location_id)
|
||||||
|
serializer = LocationSerializer(location, context={"request": request})
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
except Location.DoesNotExist:
|
||||||
|
return Response({"error": "Location not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
locations = Location.objects.all()
|
||||||
|
serializer = LocationSerializer(locations, many=True, context={"request": request})
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
@extend_schema(
|
||||||
|
request=LocationSerializer,
|
||||||
|
responses={201: 'Location created successfully!'}
|
||||||
|
)
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
API view to add a location
|
||||||
|
"""
|
||||||
|
serializer = LocationSerializer(data=request.data, context={'request': request})
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
location = serializer.save()
|
||||||
|
|
||||||
|
# Log the action
|
||||||
|
Log.objects.create(
|
||||||
|
user=request.user,
|
||||||
|
action="add_location",
|
||||||
|
text=f"{request.user} added adoption notice {location.pk} via API",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return success response with new adoption notice details
|
||||||
|
return Response(
|
||||||
|
{"message": "Location created successfully!", "id": location.pk},
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,6 +22,15 @@ class DateInput(forms.DateInput):
|
|||||||
input_type = 'date'
|
input_type = 'date'
|
||||||
|
|
||||||
|
|
||||||
|
class BulmaAdoptionNoticeForm(forms.ModelForm):
|
||||||
|
template_name = "fellchensammlung/forms/form_snippets.html"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = AdoptionNotice
|
||||||
|
fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string",
|
||||||
|
"organization"]
|
||||||
|
|
||||||
|
|
||||||
class AdoptionNoticeForm(forms.ModelForm):
|
class AdoptionNoticeForm(forms.ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
if 'in_adoption_notice_creation_flow' in kwargs:
|
if 'in_adoption_notice_creation_flow' in kwargs:
|
||||||
@ -127,8 +136,9 @@ class ImageForm(forms.ModelForm):
|
|||||||
self.helper.form_method = 'post'
|
self.helper.form_method = 'post'
|
||||||
|
|
||||||
if in_flow:
|
if in_flow:
|
||||||
submits= Div(Submit('submit', _('Speichern')),
|
submits = Div(Submit('submit', _('Speichern')),
|
||||||
Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')), css_class="container-edit-buttons")
|
Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')),
|
||||||
|
css_class="container-edit-buttons")
|
||||||
else:
|
else:
|
||||||
submits = Fieldset(Submit('submit', _('Speichern')), css_class="container-edit-buttons")
|
submits = Fieldset(Submit('submit', _('Speichern')), css_class="container-edit-buttons")
|
||||||
self.helper.layout = Layout(
|
self.helper.layout = Layout(
|
||||||
@ -140,7 +150,6 @@ class ImageForm(forms.ModelForm):
|
|||||||
submits
|
submits
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Image
|
model = Image
|
||||||
fields = ('image', 'alt_text')
|
fields = ('image', 'alt_text')
|
||||||
@ -164,7 +173,7 @@ class CommentForm(forms.ModelForm):
|
|||||||
self.helper = FormHelper()
|
self.helper = FormHelper()
|
||||||
self.helper.form_class = 'form-comments'
|
self.helper.form_class = 'form-comments'
|
||||||
self.helper.add_input(Hidden('action', 'comment'))
|
self.helper.add_input(Hidden('action', 'comment'))
|
||||||
self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="btn2"))
|
self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="button is-primary"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Comment
|
model = Comment
|
||||||
@ -181,7 +190,8 @@ class CustomRegistrationForm(RegistrationForm):
|
|||||||
class Meta(RegistrationForm.Meta):
|
class Meta(RegistrationForm.Meta):
|
||||||
model = User
|
model = User
|
||||||
|
|
||||||
captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_("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):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -193,7 +203,10 @@ class CustomRegistrationForm(RegistrationForm):
|
|||||||
|
|
||||||
|
|
||||||
class AdoptionNoticeSearchForm(forms.Form):
|
class AdoptionNoticeSearchForm(forms.Form):
|
||||||
|
template_name = "fellchensammlung/forms/form_snippets.html"
|
||||||
|
|
||||||
sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False,
|
sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False,
|
||||||
initial=SexChoicesWithAll.ALL)
|
initial=SexChoicesWithAll.ALL)
|
||||||
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED, 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)
|
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 tabnanny import verbose
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -39,15 +40,28 @@ class Language(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Location(models.Model):
|
class Location(models.Model):
|
||||||
place_id = models.IntegerField() # OSM id
|
place_id = models.CharField(max_length=200) # OSM id
|
||||||
latitude = models.FloatField()
|
latitude = models.FloatField()
|
||||||
longitude = models.FloatField()
|
longitude = models.FloatField()
|
||||||
name = models.CharField(max_length=2000)
|
name = models.CharField(max_length=2000)
|
||||||
|
city = models.CharField(max_length=200, blank=True, null=True)
|
||||||
|
housenumber = models.CharField(max_length=20, blank=True, null=True)
|
||||||
|
postcode = models.CharField(max_length=20, blank=True, null=True)
|
||||||
|
street = models.CharField(max_length=200, blank=True, null=True)
|
||||||
|
county = models.CharField(max_length=200, blank=True, null=True)
|
||||||
|
# Country code as per ISO 3166-1 alpha-2
|
||||||
|
# https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes
|
||||||
|
countrycode = models.CharField(max_length=2, verbose_name=_("Ländercode"),
|
||||||
|
help_text=_("Standardisierter Ländercode nach ISO 3166-1 ALPHA-2"),
|
||||||
|
blank=True, null=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
|
if self.city and self.postcode:
|
||||||
|
return f"{self.city} ({self.postcode})"
|
||||||
|
else:
|
||||||
|
return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def position(self):
|
def position(self):
|
||||||
@ -73,6 +87,11 @@ class Location(models.Model):
|
|||||||
latitude=proxy.latitude,
|
latitude=proxy.latitude,
|
||||||
longitude=proxy.longitude,
|
longitude=proxy.longitude,
|
||||||
name=proxy.name,
|
name=proxy.name,
|
||||||
|
postcode=proxy.postcode,
|
||||||
|
city=proxy.city,
|
||||||
|
street=proxy.street,
|
||||||
|
county=proxy.county,
|
||||||
|
countrycode=proxy.countrycode,
|
||||||
)
|
)
|
||||||
return location
|
return location
|
||||||
|
|
||||||
@ -84,33 +103,33 @@ class Location(models.Model):
|
|||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
|
|
||||||
|
class ImportantLocation(models.Model):
|
||||||
|
location = models.OneToOneField(Location, on_delete=models.CASCADE)
|
||||||
|
slug = models.SlugField(unique=True)
|
||||||
|
name = models.CharField(max_length=200)
|
||||||
|
|
||||||
|
|
||||||
class ExternalSourceChoices(models.TextChoices):
|
class ExternalSourceChoices(models.TextChoices):
|
||||||
OSM = "OSM", _("Open Street Map")
|
OSM = "OSM", _("Open Street Map")
|
||||||
|
|
||||||
|
|
||||||
|
class AllowUseOfMaterialsChices(models.TextChoices):
|
||||||
|
USE_MATERIALS_ALLOWED = "allowed", _("Usage allowed")
|
||||||
|
USE_MATERIALS_REQUESTED = "requested", _("Usage requested")
|
||||||
|
USE_MATERIALS_DENIED = "denied", _("Usage denied")
|
||||||
|
USE_MATERIALS_OTHER = "other", _("It's complicated")
|
||||||
|
USE_MATERIALS_NOT_ASKED = "not_asked", _("Not asked")
|
||||||
|
|
||||||
|
|
||||||
class RescueOrganization(models.Model):
|
class RescueOrganization(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}"
|
return f"{self.name}"
|
||||||
|
|
||||||
USE_MATERIALS_ALLOWED = "allowed"
|
|
||||||
USE_MATERIALS_REQUESTED = "requested"
|
|
||||||
USE_MATERIALS_DENIED = "denied"
|
|
||||||
USE_MATERIALS_OTHER = "other"
|
|
||||||
USE_MATERIALS_NOT_ASKED = "not_asked"
|
|
||||||
|
|
||||||
ALLOW_USE_MATERIALS_CHOICE = {
|
|
||||||
USE_MATERIALS_ALLOWED: "Usage allowed",
|
|
||||||
USE_MATERIALS_REQUESTED: "Usage requested",
|
|
||||||
USE_MATERIALS_DENIED: "Usage denied",
|
|
||||||
USE_MATERIALS_OTHER: "It's complicated",
|
|
||||||
USE_MATERIALS_NOT_ASKED: "Not asked"
|
|
||||||
}
|
|
||||||
|
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
|
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
|
||||||
allows_using_materials = models.CharField(max_length=200,
|
allows_using_materials = models.CharField(max_length=200,
|
||||||
default=ALLOW_USE_MATERIALS_CHOICE[USE_MATERIALS_NOT_ASKED],
|
default=AllowUseOfMaterialsChices.USE_MATERIALS_NOT_ASKED,
|
||||||
choices=ALLOW_USE_MATERIALS_CHOICE,
|
choices=AllowUseOfMaterialsChices.choices,
|
||||||
verbose_name=_('Erlaubt Nutzung von Inhalten'))
|
verbose_name=_('Erlaubt Nutzung von Inhalten'))
|
||||||
location_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation"))
|
location_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation"))
|
||||||
location = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True)
|
location = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True)
|
||||||
@ -122,6 +141,7 @@ class RescueOrganization(models.Model):
|
|||||||
website = models.URLField(null=True, blank=True, verbose_name=_('Website'))
|
website = models.URLField(null=True, blank=True, verbose_name=_('Website'))
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
last_checked = models.DateTimeField(auto_now_add=True, verbose_name=_('Datum der letzten Prüfung'))
|
||||||
internal_comment = models.TextField(verbose_name=_("Interner Kommentar"), null=True, blank=True, )
|
internal_comment = models.TextField(verbose_name=_("Interner Kommentar"), null=True, blank=True, )
|
||||||
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung')) # Markdown allowed
|
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung')) # Markdown allowed
|
||||||
external_object_identifier = models.CharField(max_length=200, null=True, blank=True,
|
external_object_identifier = models.CharField(max_length=200, null=True, blank=True,
|
||||||
@ -130,6 +150,9 @@ class RescueOrganization(models.Model):
|
|||||||
choices=ExternalSourceChoices.choices,
|
choices=ExternalSourceChoices.choices,
|
||||||
verbose_name=_('External Source Identifier'))
|
verbose_name=_('External Source Identifier'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('external_object_identifier', 'external_source_identifier',)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("rescue-organization-detail", args=[str(self.pk)])
|
return reverse("rescue-organization-detail", args=[str(self.pk)])
|
||||||
|
|
||||||
@ -149,7 +172,20 @@ class RescueOrganization(models.Model):
|
|||||||
if self.description is None:
|
if self.description is None:
|
||||||
return ""
|
return ""
|
||||||
if len(self.description) > 200:
|
if len(self.description) > 200:
|
||||||
return self.description[:200] + f" ... [weiterlesen]({self.get_absolute_url()})"
|
return self.description[:200] + _(f" ... [weiterlesen]({self.get_absolute_url()})")
|
||||||
|
|
||||||
|
def set_checked(self):
|
||||||
|
self.last_checked = timezone.now()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_checked_hr(self):
|
||||||
|
time_since_last_checked = timezone.now() - self.last_checked
|
||||||
|
return time_since_as_hr_string(time_since_last_checked)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def species_urls(self):
|
||||||
|
return SpeciesSpecificURL.objects.filter(organization=self)
|
||||||
|
|
||||||
|
|
||||||
# Admins can perform all actions and have the highest trust associated with them
|
# Admins can perform all actions and have the highest trust associated with them
|
||||||
@ -261,11 +297,13 @@ class AdoptionNotice(models.Model):
|
|||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=timezone.now)
|
last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=timezone.now)
|
||||||
searching_since = models.DateField(verbose_name=_('Sucht nach einem Zuhause seit'))
|
searching_since = models.DateField(verbose_name=_('Sucht nach einem Zuhause seit'))
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200, verbose_name=_('Titel der Vermittlung'))
|
||||||
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
|
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
|
||||||
organization = models.ForeignKey(RescueOrganization, blank=True, null=True, on_delete=models.SET_NULL,
|
organization = models.ForeignKey(RescueOrganization, blank=True, null=True, on_delete=models.SET_NULL,
|
||||||
verbose_name=_('Organisation'))
|
verbose_name=_('Organisation'))
|
||||||
further_information = models.URLField(null=True, blank=True, 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'))
|
group_only = models.BooleanField(default=False, verbose_name=_('Ausschließlich Gruppenadoption'))
|
||||||
photos = models.ManyToManyField(Image, blank=True)
|
photos = models.ManyToManyField(Image, blank=True)
|
||||||
location_string = models.CharField(max_length=200, verbose_name=_("Ortsangabe"))
|
location_string = models.CharField(max_length=200, verbose_name=_("Ortsangabe"))
|
||||||
@ -283,6 +321,13 @@ class AdoptionNotice(models.Model):
|
|||||||
sexes.add(animal.sex)
|
sexes.add(animal.sex)
|
||||||
return sexes
|
return sexes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def num_per_sex(self):
|
||||||
|
num_per_sex = dict()
|
||||||
|
for sex in SexChoices:
|
||||||
|
num_per_sex[sex] = self.animals.filter(sex=sex).count
|
||||||
|
return num_per_sex
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_checked_hr(self):
|
def last_checked_hr(self):
|
||||||
time_since_last_checked = timezone.now() - self.last_checked
|
time_since_last_checked = timezone.now() - self.last_checked
|
||||||
@ -323,6 +368,10 @@ class AdoptionNotice(models.Model):
|
|||||||
"""Returns the url to access a detailed page for the adoption notice."""
|
"""Returns the url to access a detailed page for the adoption notice."""
|
||||||
return reverse('adoption-notice-detail', args=[str(self.id)])
|
return reverse('adoption-notice-detail', args=[str(self.id)])
|
||||||
|
|
||||||
|
def get_absolute_url_bulma(self):
|
||||||
|
"""Returns the url to access a detailed page for the adoption notice."""
|
||||||
|
return reverse('adoption-notice-detail-bulma', args=[str(self.id)])
|
||||||
|
|
||||||
def get_report_url(self):
|
def get_report_url(self):
|
||||||
"""Returns the url to report an adoption notice."""
|
"""Returns the url to report an adoption notice."""
|
||||||
return reverse('report-adoption-notice', args=[str(self.id)])
|
return reverse('report-adoption-notice', args=[str(self.id)])
|
||||||
@ -666,6 +715,31 @@ class Report(models.Model):
|
|||||||
def get_moderation_actions(self):
|
def get_moderation_actions(self):
|
||||||
return ModerationAction.objects.filter(report=self)
|
return ModerationAction.objects.filter(report=self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reported_content(self):
|
||||||
|
"""
|
||||||
|
Dynamically fetch the reported content based on subclass.
|
||||||
|
The alternative would be to use the ContentType framework:
|
||||||
|
https://docs.djangoproject.com/en/5.1/ref/contrib/contenttypes/
|
||||||
|
"""
|
||||||
|
if hasattr(self, "reportadoptionnotice"):
|
||||||
|
return self.reportadoptionnotice.adoption_notice
|
||||||
|
elif hasattr(self, "reportcomment"):
|
||||||
|
return self.reportcomment.reported_comment
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def reported_content_url(self):
|
||||||
|
"""
|
||||||
|
Same as reported_content, just for url
|
||||||
|
"""
|
||||||
|
if hasattr(self, "reportadoptionnotice"):
|
||||||
|
print(self.reportadoptionnotice.adoption_notice.get_absolute_url)
|
||||||
|
return self.reportadoptionnotice.adoption_notice.get_absolute_url
|
||||||
|
elif hasattr(self, "reportcomment"):
|
||||||
|
return self.reportcomment.reported_comment.get_absolute_url
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ReportAdoptionNotice(Report):
|
class ReportAdoptionNotice(Report):
|
||||||
adoption_notice = models.ForeignKey("AdoptionNotice", on_delete=models.CASCADE)
|
adoption_notice = models.ForeignKey("AdoptionNotice", on_delete=models.CASCADE)
|
||||||
@ -674,6 +748,9 @@ class ReportAdoptionNotice(Report):
|
|||||||
def reported_content(self):
|
def reported_content(self):
|
||||||
return self.adoption_notice
|
return self.adoption_notice
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Report der Vermittlung {self.adoption_notice}"
|
||||||
|
|
||||||
|
|
||||||
class ReportComment(Report):
|
class ReportComment(Report):
|
||||||
reported_comment = models.ForeignKey("Comment", on_delete=models.CASCADE)
|
reported_comment = models.ForeignKey("Comment", on_delete=models.CASCADE)
|
||||||
@ -818,7 +895,8 @@ class Comment(models.Model):
|
|||||||
class BaseNotification(models.Model):
|
class BaseNotification(models.Model):
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=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")
|
text = models.TextField(verbose_name="Inhalt")
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||||
read = models.BooleanField(default=False)
|
read = models.BooleanField(default=False)
|
||||||
@ -829,6 +907,11 @@ class BaseNotification(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
self.user.get_notifications_url()
|
self.user.get_notifications_url()
|
||||||
|
|
||||||
|
def mark_read(self):
|
||||||
|
self.read = True
|
||||||
|
self.read_at = timezone.now()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
class CommentNotification(BaseNotification):
|
class CommentNotification(BaseNotification):
|
||||||
comment = models.ForeignKey(Comment, on_delete=models.CASCADE, verbose_name=_('Antwort'))
|
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.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
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 .tasks import task_send_notification_email
|
||||||
from notfellchen.settings import host
|
from notfellchen.settings import host
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -18,6 +18,13 @@ def base_notification_receiver(sender, instance: BaseNotification, created: bool
|
|||||||
else:
|
else:
|
||||||
task_send_notification_email.delay(instance.pk)
|
task_send_notification_email.delay(instance.pk)
|
||||||
|
|
||||||
|
@receiver(post_save, sender=RescueOrganization)
|
||||||
|
def rescue_org_receiver(sender, instance: RescueOrganization, created: bool, **kwargs):
|
||||||
|
if instance.location:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
task_send_notification_email.delay(instance.pk)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=User)
|
@receiver(post_save, sender=User)
|
||||||
def notification_new_user(sender, instance: User, created: bool, **kwargs):
|
def notification_new_user(sender, instance: User, created: bool, **kwargs):
|
||||||
|
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;
|
border: 1px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-small {
|
||||||
|
font-size: medium;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.checkmark {
|
.checkmark {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
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 .mail import send_notification_email
|
||||||
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices
|
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices
|
||||||
from .tools.misc import healthcheck_ok
|
from .tools.misc import healthcheck_ok
|
||||||
from .models import Location, AdoptionNotice, Timestamp
|
from .models import Location, AdoptionNotice, Timestamp, RescueOrganization
|
||||||
from .tools.notifications import notify_moderators_of_AN_to_be_checked
|
from .tools.notifications import notify_of_AN_to_be_checked
|
||||||
from .tools.search import notify_search_subscribers
|
from .tools.search import notify_search_subscribers
|
||||||
|
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ def post_adoption_notice_save(pk):
|
|||||||
logging.info(f"Location was added to Adoption notice {pk}")
|
logging.info(f"Location was added to Adoption notice {pk}")
|
||||||
|
|
||||||
notify_search_subscribers(instance, only_if_active=True)
|
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")
|
@celery_app.task(name="tools.healthcheck")
|
||||||
def task_healthcheck():
|
def task_healthcheck():
|
||||||
@ -57,3 +57,10 @@ def task_healthcheck():
|
|||||||
@shared_task
|
@shared_task
|
||||||
def task_send_notification_email(notification_pk):
|
def task_send_notification_email(notification_pk):
|
||||||
send_notification_email(notification_pk)
|
send_notification_email(notification_pk)
|
||||||
|
|
||||||
|
@celery_app.task(name="commit.post_rescue_org_save")
|
||||||
|
def post_rescue_org_save(pk):
|
||||||
|
instance = RescueOrganization.objects.get(pk=pk)
|
||||||
|
Location.add_location_to_object(instance)
|
||||||
|
set_timestamp("add_rescue_org_location")
|
||||||
|
logging.info(f"Location was added to Rescue Organization {pk}")
|
@ -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 custom_tags %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% get_current_language as LANGUAGE_CODE%}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="{{ LANGUAGE_CODE }}">
|
||||||
<head>
|
<head>
|
||||||
{% block title %}{% endblock %}
|
{% block title %}{% endblock %}
|
||||||
<meta charset="utf-8">
|
<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 %}
|
{% 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 %}
|
{% csrf_token %}
|
||||||
<select name="language" onchange='this.form.submit()'>
|
<select name="language" onchange='this.form.submit()'>
|
||||||
{% get_current_language as LANGUAGE_CODE_CURRENT %}
|
{% get_current_language as LANGUAGE_CODE_CURRENT %}
|
||||||
|
@ -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">
|
<section class="header">
|
||||||
<div>
|
<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>
|
||||||
|
|
||||||
<div class="profile-card">
|
<div class="profile-card">
|
||||||
@ -27,7 +29,7 @@
|
|||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="btn2" href="{% url "django_registration_register" %}">{% translate "Registrieren" %}</a>
|
<a class="btn2" href="{% url "django_registration_register" %}">{% translate "Registrieren" %}</a>
|
||||||
<a class="btn2" href="{% url "login" %}"><i class="fa fa-sign-in" aria-hidden="true"></i></a>
|
<a class="btn2" href="{% url "login" %}"><i class="fa fa-sign-in" aria-label="Login"></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input id="menu-toggle" type="checkbox"/>
|
<input id="menu-toggle" type="checkbox"/>
|
||||||
<label class='menu-button-container' for="menu-toggle">
|
<label class='menu-button-container' for="menu-toggle">
|
||||||
|
@ -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" %}
|
{% extends "fellchensammlung/base_generic.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% block title %}<title>{% translate "Karte" %}</title> %}
|
{% block title %}<title>{% translate "Karte" %}</title>{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="card">
|
<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 %}
|
{% load i18n %}
|
||||||
|
|
||||||
<!-- add MapLibre JavaScript and CSS -->
|
<!-- add MapLibre JavaScript and CSS -->
|
||||||
<script src="{% settings_value "MAP_TILE_SERVER" %}/assets/maplibre-gl/maplibre-gl.js"></script>
|
<script src="{% settings_value "MAP_TILE_SERVER" %}/assets/lib/maplibre-gl/maplibre-gl.js"></script>
|
||||||
<link href="{% settings_value "MAP_TILE_SERVER" %}/assets/maplibre-gl/maplibre-gl.css" rel="stylesheet"/>
|
<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/ -->
|
<!-- add Turf see https://maplibre.org/maplibre-gl-js/docs/examples/draw-a-radius/ -->
|
||||||
<script src="{% static 'fellchensammlung/js/turf.min.js' %}"></script>
|
<script src="{% static 'fellchensammlung/js/turf.min.js' %}"></script>
|
||||||
@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
let map = new maplibregl.Map({
|
let map = new maplibregl.Map({
|
||||||
container: 'map',
|
container: 'map',
|
||||||
style: '{% static "fellchensammlung/map/styles/colorful.json" %}',
|
style: '{% static "fellchensammlung/map/styles/colorful/style.json" %}',
|
||||||
center: map_center,
|
center: map_center,
|
||||||
zoom: zoom_level
|
zoom: zoom_level
|
||||||
}).addControl(new maplibregl.NavigationControl());
|
}).addControl(new maplibregl.NavigationControl());
|
||||||
@ -76,7 +76,7 @@
|
|||||||
image = await map.loadImage('{% static "fellchensammlung/img/pin.png" %}');
|
image = await map.loadImage('{% static "fellchensammlung/img/pin.png" %}');
|
||||||
map.addImage('pin', image.data);
|
map.addImage('pin', image.data);
|
||||||
{% for map_pin in map_pins %}
|
{% for map_pin in map_pins %}
|
||||||
map.addSource('point', {
|
map.addSource('point_{{ forloop.counter }}', {
|
||||||
'type': 'geojson',
|
'type': 'geojson',
|
||||||
'data': {
|
'data': {
|
||||||
'type': 'FeatureCollection',
|
'type': 'FeatureCollection',
|
||||||
@ -91,16 +91,16 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
map.addLayer({
|
||||||
|
'id': 'point_{{ forloop.counter }}',
|
||||||
|
'type': 'circle',
|
||||||
|
'source': 'point_{{ forloop.counter }}',
|
||||||
|
'paint': {
|
||||||
|
'circle-radius': 18,
|
||||||
|
'circle-color': '#ff878980'
|
||||||
|
}
|
||||||
|
});
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
map.addLayer({
|
|
||||||
'id': 'pints',
|
|
||||||
'type': 'symbol',
|
|
||||||
'source': 'point',
|
|
||||||
'layout': {
|
|
||||||
'icon-image': 'pin',
|
|
||||||
'icon-size': 0.1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
{% if search_center %}
|
{% if search_center %}
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<div class="report card">
|
<div class="report card">
|
||||||
<h2>
|
<h2>
|
||||||
{% blocktranslate %}
|
{% translate 'Meldung von ' %} <a href="{{ report.reported_content_url }}"><i>{{ report.reported_content }}</i></a>
|
||||||
Meldung von {{ report.reported_content }}
|
|
||||||
{% endblocktranslate %}
|
|
||||||
</h2>
|
</h2>
|
||||||
{% if report.reported_broken_rules %}
|
{% if report.reported_broken_rules %}
|
||||||
{% translate "Regeln gegen die Verstoßen wurde" %}
|
{% translate "Regeln gegen die Verstoßen wurde" %}
|
||||||
@ -13,19 +11,25 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p><b>{% translate "Kommentar zur Meldung" %}:</b>
|
<p>
|
||||||
{{ report.user_comment }}
|
{% 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>
|
</p>
|
||||||
<div class="container-edit-buttons">
|
{% if is_mod_or_above %}
|
||||||
<form action="allow" class="">
|
<div class="container-edit-buttons">
|
||||||
{% csrf_token %}
|
<form action="allow" class="">
|
||||||
<input type="hidden" name="report_id" value="{{ report.pk }}">
|
{% csrf_token %}
|
||||||
<button class="btn allow" type="submit">{% translate "Inhalt genehmigen" %}</button>
|
<input type="hidden" name="report_id" value="{{ report.pk }}">
|
||||||
</form>
|
<button class="btn allow" type="submit">{% translate "Inhalt genehmigen" %}</button>
|
||||||
<form action="disallow" class="">
|
</form>
|
||||||
{% csrf_token %}
|
<form action="disallow" class="">
|
||||||
<input type="hidden" name="report_id" value="{{ report.pk }}">
|
{% csrf_token %}
|
||||||
<button class="btn allow" type="submit">{% translate "Inhalt als gesperrt kennzeichnen" %}</button>
|
<input type="hidden" name="report_id" value="{{ report.pk }}">
|
||||||
</form>
|
<button class="btn allow" type="submit">{% translate "Inhalt als gesperrt kennzeichnen" %}</button>
|
||||||
</div>
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</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="longitude" maxlength="200" id="longitude">
|
||||||
<input type="hidden" name="latitude" maxlength="200" id="latitude">
|
<input type="hidden" name="latitude" maxlength="200" id="latitude">
|
||||||
<input type="hidden" id="place_id" name="place_id">
|
<input type="hidden" id="place_id" name="place_id">
|
||||||
{{ search_form.as_p }}
|
{{ search_form }}
|
||||||
<ul id="results"></ul>
|
<ul id="results"></ul>
|
||||||
<div class="container-edit-buttons">
|
<div class="container-edit-buttons">
|
||||||
<button class="btn" type="submit" value="search" name="search">
|
<button class="btn" type="submit" value="search" name="search">
|
||||||
|
@ -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:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
@stringfilter
|
@stringfilter
|
||||||
def pointdecimal(value):
|
def pointdecimal(value):
|
||||||
@ -57,6 +58,7 @@ def pointdecimal(value):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
@stringfilter
|
@stringfilter
|
||||||
def domain(url):
|
def domain(url):
|
||||||
@ -68,6 +70,17 @@ def domain(url):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def settings_value(name):
|
def settings_value(name):
|
||||||
return getattr(settings, name)
|
return getattr(settings, name)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='add_class')
|
||||||
|
def add_class(field, css_class):
|
||||||
|
return field.as_widget(attrs={"class": css_class})
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def widget_type(field):
|
||||||
|
return field.field.widget.__class__.__name__
|
||||||
|
@ -74,13 +74,21 @@ class GeoFeature:
|
|||||||
geofeatures = []
|
geofeatures = []
|
||||||
for feature in result["features"]:
|
for feature in result["features"]:
|
||||||
geojson = {}
|
geojson = {}
|
||||||
try:
|
# Necessary features
|
||||||
geojson['name'] = feature["properties"]["name"]
|
|
||||||
except KeyError:
|
|
||||||
geojson['name'] = feature["properties"]["street"]
|
|
||||||
geojson['place_id'] = feature["properties"]["osm_id"]
|
geojson['place_id'] = feature["properties"]["osm_id"]
|
||||||
geojson['lat'] = feature["geometry"]["coordinates"][1]
|
geojson['lat'] = feature["geometry"]["coordinates"][1]
|
||||||
geojson['lon'] = feature["geometry"]["coordinates"][0]
|
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)
|
geofeatures.append(geojson)
|
||||||
return geofeatures
|
return geofeatures
|
||||||
|
|
||||||
@ -137,7 +145,6 @@ class GeoAPI:
|
|||||||
result = self.requests.get(self.api_url,
|
result = self.requests.get(self.api_url,
|
||||||
{"q": location_string, "lang": language},
|
{"q": location_string, "lang": language},
|
||||||
headers=self.headers).json()
|
headers=self.headers).json()
|
||||||
logging.warning(result)
|
|
||||||
geofeatures = GeoFeature.geofeatures_from_photon_result(result)
|
geofeatures = GeoFeature.geofeatures_from_photon_result(result)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@ -162,6 +169,7 @@ class LocationProxy:
|
|||||||
"""
|
"""
|
||||||
self.geo_api = GeoAPI()
|
self.geo_api = GeoAPI()
|
||||||
geofeatures = self.geo_api.get_geojson_for_query(location_string)
|
geofeatures = self.geo_api.get_geojson_for_query(location_string)
|
||||||
|
|
||||||
if geofeatures is None:
|
if geofeatures is None:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
result = geofeatures[0]
|
result = geofeatures[0]
|
||||||
@ -169,6 +177,12 @@ class LocationProxy:
|
|||||||
self.place_id = result["place_id"]
|
self.place_id = result["place_id"]
|
||||||
self.latitude = result["lat"]
|
self.latitude = result["lat"]
|
||||||
self.longitude = result["lon"]
|
self.longitude = result["lon"]
|
||||||
|
optional_keys = ["housenumber", "street", "city", "postcode", "county", "countrycode"]
|
||||||
|
for key in optional_keys:
|
||||||
|
try:
|
||||||
|
self.__setattr__(key, result[key])
|
||||||
|
except KeyError:
|
||||||
|
self.__setattr__(key, None)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.place_id == other.place_id
|
return self.place_id == other.place_id
|
||||||
|
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
|
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:
|
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,
|
AdoptionNoticeNotification.objects.create(adoption_notice=adoption_notice,
|
||||||
user=moderator,
|
user=user,
|
||||||
title=f" Prüfe Vermittlung {adoption_notice}",
|
title=f" Prüfe Vermittlung {adoption_notice}",
|
||||||
text=f"{adoption_notice} muss geprüft werden bevor sie veröffentlicht wird.",
|
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
|
from ..models import SearchSubscription, AdoptionNotice, AdoptionNoticeNotification, SexChoicesWithAll, Location
|
||||||
|
|
||||||
|
|
||||||
def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active : bool = True):
|
def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: bool = True):
|
||||||
"""
|
"""
|
||||||
This functions checks for all search subscriptions if the new adoption notice fits the search.
|
This functions checks for all search subscriptions if the new adoption notice fits the search.
|
||||||
If the new adoption notice fits the search subscription, it sends a notification to the user that created the search.
|
If the new adoption notice fits the search subscription, it sends a notification to the user that created the search.
|
||||||
@ -36,7 +36,7 @@ class Search:
|
|||||||
self.sex = None
|
self.sex = None
|
||||||
self.area_search = None
|
self.area_search = None
|
||||||
self.max_distance = None
|
self.max_distance = None
|
||||||
self.location = None # Can either be Location (DjangoModel) or LocationProxy
|
self.location = None # Can either be Location (DjangoModel) or LocationProxy
|
||||||
self.place_not_found = False # Indicates that a location was given but could not be geocoded
|
self.place_not_found = False # Indicates that a location was given but could not be geocoded
|
||||||
self.search_form = None
|
self.search_form = None
|
||||||
# Either place_id or location string must be set for area search
|
# Either place_id or location string must be set for area search
|
||||||
@ -47,7 +47,6 @@ class Search:
|
|||||||
elif search_subscription:
|
elif search_subscription:
|
||||||
self.search_from_search_subscription(search_subscription)
|
self.search_from_search_subscription(search_subscription)
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Search: {self.sex=}, {self.location=}, {self.area_search=}, {self.max_distance=}"
|
return f"Search: {self.sex=}, {self.location=}, {self.area_search=}, {self.max_distance=}"
|
||||||
|
|
||||||
@ -93,7 +92,6 @@ class Search:
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_adoption_notices(self):
|
def get_adoption_notices(self):
|
||||||
adoptions = AdoptionNotice.objects.order_by("-created_at")
|
adoptions = AdoptionNotice.objects.order_by("-created_at")
|
||||||
# Filter for active adoption notices
|
# Filter for active adoption notices
|
||||||
@ -118,13 +116,21 @@ class Search:
|
|||||||
else:
|
else:
|
||||||
self.search_form = AdoptionNoticeSearchForm()
|
self.search_form = AdoptionNoticeSearchForm()
|
||||||
|
|
||||||
|
def search_from_predefined_i_location(self, i_location, max_distance=100):
|
||||||
|
self.sex = SexChoicesWithAll.ALL
|
||||||
|
self.location = i_location.location
|
||||||
|
self.area_search = True
|
||||||
|
self.search_form = AdoptionNoticeSearchForm(initial={"location_string": self.location.name,
|
||||||
|
"max_distance": max_distance,
|
||||||
|
"sex": SexChoicesWithAll.ALL})
|
||||||
|
self.max_distance = max_distance
|
||||||
|
|
||||||
def search_from_search_subscription(self, search_subscription: SearchSubscription):
|
def search_from_search_subscription(self, search_subscription: SearchSubscription):
|
||||||
self.sex = search_subscription.sex
|
self.sex = search_subscription.sex
|
||||||
self.location = search_subscription.location
|
self.location = search_subscription.location
|
||||||
self.area_search = True
|
self.area_search = True
|
||||||
self.max_distance = search_subscription.max_distance
|
self.max_distance = search_subscription.max_distance
|
||||||
|
|
||||||
|
|
||||||
def subscribe(self, user):
|
def subscribe(self, user):
|
||||||
logging.info(f"{user} subscribed to search")
|
logging.info(f"{user} subscribed to search")
|
||||||
if isinstance(self.location, LocationProxy):
|
if isinstance(self.location, LocationProxy):
|
||||||
|
@ -7,8 +7,18 @@ from .feeds import LatestAdoptionNoticesFeed
|
|||||||
from . import views
|
from . import views
|
||||||
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
||||||
|
|
||||||
|
from django.contrib.sitemaps.views import sitemap
|
||||||
|
from .sitemap import StaticViewSitemap, AdoptionNoticeSitemap, AnimalSitemap
|
||||||
|
|
||||||
|
sitemaps = {
|
||||||
|
"static": StaticViewSitemap,
|
||||||
|
"vermittlungen": AdoptionNoticeSitemap,
|
||||||
|
"tiere": AnimalSitemap,
|
||||||
|
}
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.index, name="index"),
|
path("", views.index, name="index"),
|
||||||
|
path("bulma/", views.index_bulma, name="index-bulma"),
|
||||||
path("rss/", LatestAdoptionNoticesFeed(), name="rss"),
|
path("rss/", LatestAdoptionNoticesFeed(), name="rss"),
|
||||||
path("metrics/", views.metrics, name="metrics"),
|
path("metrics/", views.metrics, name="metrics"),
|
||||||
# ex: /animal/5/
|
# ex: /animal/5/
|
||||||
@ -19,12 +29,15 @@ urlpatterns = [
|
|||||||
path("tier/<int:animal_id>/add-photo", views.add_photo_to_animal, name="animal-add-photo"),
|
path("tier/<int:animal_id>/add-photo", views.add_photo_to_animal, name="animal-add-photo"),
|
||||||
# ex: /adoption_notice/7/
|
# ex: /adoption_notice/7/
|
||||||
path("vermittlung/<int:adoption_notice_id>/", views.adoption_notice_detail, name="adoption-notice-detail"),
|
path("vermittlung/<int:adoption_notice_id>/", views.adoption_notice_detail, name="adoption-notice-detail"),
|
||||||
|
path("bulma/vermittlung/<int:adoption_notice_id>/", views.adoption_notice_detail_bulma, name="adoption-notice-detail-bulma"),
|
||||||
# ex: /adoption_notice/7/edit
|
# ex: /adoption_notice/7/edit
|
||||||
path("vermittlung/<int:adoption_notice_id>/edit", views.adoption_notice_edit, name="adoption-notice-edit"),
|
path("vermittlung/<int:adoption_notice_id>/edit", views.adoption_notice_edit, name="adoption-notice-edit"),
|
||||||
# ex: /vermittlung/5/add-photo
|
# ex: /vermittlung/5/add-photo
|
||||||
path("vermittlung/<int:adoption_notice_id>/add-photo", views.add_photo_to_adoption_notice, 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
|
# 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("tierschutzorganisationen/", views.list_rescue_organizations, name="rescue-organizations"),
|
||||||
path("organisation/<int:rescue_organization_id>/", views.detail_view_rescue_organization,
|
path("organisation/<int:rescue_organization_id>/", views.detail_view_rescue_organization,
|
||||||
@ -32,12 +45,21 @@ urlpatterns = [
|
|||||||
|
|
||||||
# ex: /search/
|
# ex: /search/
|
||||||
path("suchen/", views.search, name="search"),
|
path("suchen/", views.search, name="search"),
|
||||||
|
path("bulma/suchen/", views.search_bulma, name="search-bulma"),
|
||||||
|
path("suchen/<slug:important_location_slug>", views.search_important_locations, name="search-by-location"),
|
||||||
# ex: /map/
|
# ex: /map/
|
||||||
path("map/", views.map, name="map"),
|
path("map/", views.map, name="map"),
|
||||||
|
# ex: /map/
|
||||||
|
path("bulma/map/", views.map_bulma, name="map-bulma"),
|
||||||
# ex: /vermitteln/
|
# ex: /vermitteln/
|
||||||
path("vermitteln/", views.add_adoption_notice, name="add-adoption"),
|
path("vermitteln/", views.add_adoption_notice, name="add-adoption"),
|
||||||
|
path("bulma/vermitteln/", views.add_adoption_notice_bulma, name="add-adoption-bulma"),
|
||||||
|
|
||||||
path("ueber-uns/", views.about, name="about"),
|
path("ueber-uns/", views.about, name="about"),
|
||||||
|
path("bulma/ueber-uns/", views.about_bulma, name="about-bulma"),
|
||||||
|
path("impressum/", views.imprint, name="imprint"),
|
||||||
|
path("terms-of-service/", views.terms_of_service, name="terms-of-service"),
|
||||||
|
path("datenschutz/", views.privacy, name="privacy"),
|
||||||
|
|
||||||
################
|
################
|
||||||
## Moderation ##
|
## Moderation ##
|
||||||
@ -48,9 +70,11 @@ urlpatterns = [
|
|||||||
path("meldung/<uuid:report_id>/", views.report_detail, name="report-detail"),
|
path("meldung/<uuid:report_id>/", views.report_detail, name="report-detail"),
|
||||||
path("meldung/<uuid:report_id>/sucess", views.report_detail_success, name="report-detail-success"),
|
path("meldung/<uuid:report_id>/sucess", views.report_detail_success, name="report-detail-success"),
|
||||||
path("modqueue/", views.modqueue, name="modqueue"),
|
path("modqueue/", views.modqueue, name="modqueue"),
|
||||||
|
|
||||||
path("updatequeue/", views.updatequeue, name="updatequeue"),
|
path("updatequeue/", views.updatequeue, name="updatequeue"),
|
||||||
|
|
||||||
|
path("organization-check/", views.rescue_organization_check, name="organization-check"),
|
||||||
|
|
||||||
###########
|
###########
|
||||||
## USERS ##
|
## USERS ##
|
||||||
###########
|
###########
|
||||||
@ -95,4 +119,11 @@ urlpatterns = [
|
|||||||
###################
|
###################
|
||||||
path('external-site/', views.external_site_warning, name="external-site"),
|
path('external-site/', views.external_site_warning, name="external-site"),
|
||||||
|
|
||||||
|
###############
|
||||||
|
## TECHNICAL ##
|
||||||
|
###############
|
||||||
|
path("sitemap.xml", sitemap, {"sitemaps": sitemaps}, name="django.contrib.sitemaps.views.sitemap"),
|
||||||
|
path("styleguide", views.styleguide, name="styleguide"),
|
||||||
|
path("styleguide-bulma", views.styleguide_bulma, name="styleguide-bulma"),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -2,7 +2,8 @@ import logging
|
|||||||
|
|
||||||
from django.contrib.auth.views import redirect_to_login
|
from django.contrib.auth.views import redirect_to_login
|
||||||
from django.http import HttpResponseRedirect, JsonResponse, HttpResponse
|
from django.http import HttpResponseRedirect, JsonResponse, HttpResponse
|
||||||
from django.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.urls import reverse
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
@ -17,11 +18,14 @@ from notfellchen import settings
|
|||||||
from fellchensammlung import logger
|
from fellchensammlung import logger
|
||||||
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
|
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
|
||||||
User, Location, AdoptionNoticeStatus, Subscriptions, CommentNotification, BaseNotification, RescueOrganization, \
|
User, Location, AdoptionNoticeStatus, Subscriptions, CommentNotification, BaseNotification, RescueOrganization, \
|
||||||
Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, AdoptionNoticeNotification
|
Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, AdoptionNoticeNotification, \
|
||||||
|
ImportantLocation
|
||||||
from .forms import AdoptionNoticeForm, AdoptionNoticeFormWithDateWidget, ImageForm, ReportAdoptionNoticeForm, \
|
from .forms import AdoptionNoticeForm, AdoptionNoticeFormWithDateWidget, ImageForm, ReportAdoptionNoticeForm, \
|
||||||
CommentForm, ReportCommentForm, AnimalForm, \
|
CommentForm, ReportCommentForm, AnimalForm, \
|
||||||
AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal
|
AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal, \
|
||||||
|
BulmaAdoptionNoticeForm
|
||||||
from .models import Language, Announcement
|
from .models import Language, Announcement
|
||||||
|
from .tools import i18n
|
||||||
from .tools.geo import GeoAPI, zoom_level_for_radius
|
from .tools.geo import GeoAPI, zoom_level_for_radius
|
||||||
from .tools.metrics import gather_metrics_data
|
from .tools.metrics import gather_metrics_data
|
||||||
from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
|
from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
|
||||||
@ -62,6 +66,22 @@ def index(request):
|
|||||||
return render(request, 'fellchensammlung/index.html', context=context)
|
return render(request, 'fellchensammlung/index.html', context=context)
|
||||||
|
|
||||||
|
|
||||||
|
def index_bulma(request):
|
||||||
|
"""View function for home page of site."""
|
||||||
|
latest_adoption_list = AdoptionNotice.objects.filter(
|
||||||
|
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE).order_by("-created_at")
|
||||||
|
active_adoptions = [adoption for adoption in latest_adoption_list if adoption.is_active]
|
||||||
|
language_code = translation.get_language()
|
||||||
|
lang = Language.objects.get(languagecode=language_code)
|
||||||
|
active_announcements = Announcement.get_active_announcements(lang)
|
||||||
|
|
||||||
|
context = {"adoption_notices": active_adoptions[:5], "adoption_notices_map": active_adoptions,
|
||||||
|
"announcements": active_announcements}
|
||||||
|
Text.get_texts(["how_to", "introduction"], lang, context)
|
||||||
|
|
||||||
|
return render(request, 'fellchensammlung/bulma-index.html', context=context)
|
||||||
|
|
||||||
|
|
||||||
def change_language(request):
|
def change_language(request):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
language_code = request.POST.get('language')
|
language_code = request.POST.get('language')
|
||||||
@ -79,9 +99,11 @@ def change_language(request):
|
|||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
return render(request, 'fellchensammlung/index.html')
|
return render(request, 'fellchensammlung/index.html')
|
||||||
|
else:
|
||||||
|
return render(request, 'fellchensammlung/index.html')
|
||||||
|
|
||||||
|
|
||||||
def adoption_notice_detail(request, adoption_notice_id):
|
def adoption_notice_detail(request, adoption_notice_id, template=None):
|
||||||
adoption_notice = AdoptionNotice.objects.get(id=adoption_notice_id)
|
adoption_notice = AdoptionNotice.objects.get(id=adoption_notice_id)
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
try:
|
try:
|
||||||
@ -130,13 +152,23 @@ def adoption_notice_detail(request, adoption_notice_id):
|
|||||||
if action == "unsubscribe":
|
if action == "unsubscribe":
|
||||||
subscription.delete()
|
subscription.delete()
|
||||||
is_subscribed = False
|
is_subscribed = False
|
||||||
|
elif action == "subscribe":
|
||||||
|
return redirect_to_login(next=request.path)
|
||||||
else:
|
else:
|
||||||
raise PermissionDenied
|
return HttpResponseForbidden()
|
||||||
else:
|
else:
|
||||||
comment_form = CommentForm(instance=adoption_notice)
|
comment_form = CommentForm(instance=adoption_notice)
|
||||||
context = {"adoption_notice": adoption_notice, "comment_form": comment_form, "user": request.user,
|
context = {"adoption_notice": adoption_notice, "comment_form": comment_form, "user": request.user,
|
||||||
"has_edit_permission": has_edit_permission, "is_subscribed": is_subscribed}
|
"has_edit_permission": has_edit_permission, "is_subscribed": is_subscribed}
|
||||||
return render(request, 'fellchensammlung/details/detail_adoption_notice.html', context=context)
|
if template is not None:
|
||||||
|
return render(request, template, context=context)
|
||||||
|
else:
|
||||||
|
return render(request, 'fellchensammlung/details/detail_adoption_notice.html', context=context)
|
||||||
|
|
||||||
|
|
||||||
|
def adoption_notice_detail_bulma(request, adoption_notice_id):
|
||||||
|
return adoption_notice_detail(request, adoption_notice_id,
|
||||||
|
template='fellchensammlung/details/bulma-detail-adoption-notice.html')
|
||||||
|
|
||||||
|
|
||||||
@login_required()
|
@login_required()
|
||||||
@ -171,7 +203,31 @@ def animal_detail(request, animal_id):
|
|||||||
return render(request, 'fellchensammlung/details/detail_animal.html', context=context)
|
return render(request, 'fellchensammlung/details/detail_animal.html', context=context)
|
||||||
|
|
||||||
|
|
||||||
def search(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
|
# A user just visiting the search site did not search, only upon completing the search form a user has really
|
||||||
# searched. This will toggle the "subscribe" button
|
# searched. This will toggle the "subscribe" button
|
||||||
searched = False
|
searched = False
|
||||||
@ -210,7 +266,7 @@ def search(request):
|
|||||||
"search_radius": search.max_distance,
|
"search_radius": search.max_distance,
|
||||||
"zoom_level": zoom_level_for_radius(search.max_distance),
|
"zoom_level": zoom_level_for_radius(search.max_distance),
|
||||||
"geocoding_api_url": settings.GEOCODING_API_URL, }
|
"geocoding_api_url": settings.GEOCODING_API_URL, }
|
||||||
return render(request, 'fellchensammlung/search.html', context=context)
|
return render(request, templatename, context=context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -255,6 +311,51 @@ def add_adoption_notice(request):
|
|||||||
return render(request, 'fellchensammlung/forms/form_add_adoption.html', {'form': form})
|
return render(request, 'fellchensammlung/forms/form_add_adoption.html', {'form': form})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def add_adoption_notice_bulma(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
print("dada")
|
||||||
|
form = AdoptionNoticeFormWithDateWidgetAutoAnimal(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
print("dodo")
|
||||||
|
an_instance = form.save(commit=False)
|
||||||
|
an_instance.owner = request.user
|
||||||
|
|
||||||
|
if request.user.trust_level >= TrustLevel.MODERATOR:
|
||||||
|
an_instance.set_active()
|
||||||
|
else:
|
||||||
|
an_instance.set_unchecked()
|
||||||
|
|
||||||
|
# Get the species and number of animals from the form
|
||||||
|
species = form.cleaned_data["species"]
|
||||||
|
sex = form.cleaned_data["sex"]
|
||||||
|
num_animals = form.cleaned_data["num_animals"]
|
||||||
|
date_of_birth = form.cleaned_data["date_of_birth"]
|
||||||
|
for i in range(0, num_animals):
|
||||||
|
Animal.objects.create(owner=request.user,
|
||||||
|
name=f"{species} {i + 1}", adoption_notice=an_instance, species=species, sex=sex,
|
||||||
|
date_of_birth=date_of_birth)
|
||||||
|
|
||||||
|
"""Log"""
|
||||||
|
Log.objects.create(user=request.user, action="add_adoption_notice",
|
||||||
|
text=f"{request.user} hat Vermittlung {an_instance.pk} hinzugefügt")
|
||||||
|
|
||||||
|
"""Spin up a task that adds the location and notifies search subscribers"""
|
||||||
|
post_adoption_notice_save.delay(an_instance.id)
|
||||||
|
|
||||||
|
"""Subscriptions"""
|
||||||
|
# Automatically subscribe user that created AN to AN
|
||||||
|
Subscriptions.objects.create(owner=request.user, adoption_notice=an_instance)
|
||||||
|
|
||||||
|
return redirect(reverse("adoption-notice-detail-bulma", args=[an_instance.pk]))
|
||||||
|
else:
|
||||||
|
print(form.errors)
|
||||||
|
else:
|
||||||
|
form = AdoptionNoticeFormWithDateWidgetAutoAnimal()
|
||||||
|
return render(request, 'fellchensammlung/forms/bulma-form-add-adoption.html', {'form': form})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def adoption_notice_add_animal(request, adoption_notice_id):
|
def adoption_notice_add_animal(request, adoption_notice_id):
|
||||||
# Only users that are mods or owners of the adoption notice are allowed to add to it
|
# Only users that are mods or owners of the adoption notice are allowed to add to it
|
||||||
@ -363,15 +464,7 @@ def animal_edit(request, animal_id):
|
|||||||
def about(request):
|
def about(request):
|
||||||
rules = Rule.objects.all()
|
rules = Rule.objects.all()
|
||||||
|
|
||||||
language_code = translation.get_language()
|
legal = i18n.get_texts_by_language(["terms_of_service", "privacy_statement", "imprint", "about_us", "faq"])
|
||||||
lang = Language.objects.get(languagecode=language_code)
|
|
||||||
|
|
||||||
legal = {}
|
|
||||||
for text_code in ["terms_of_service", "privacy_statement", "imprint", "about_us", "faq"]:
|
|
||||||
try:
|
|
||||||
legal[text_code] = Text.objects.get(text_code=text_code, language=lang, )
|
|
||||||
except Text.DoesNotExist:
|
|
||||||
legal[text_code] = None
|
|
||||||
|
|
||||||
context = {"rules": rules, }
|
context = {"rules": rules, }
|
||||||
context.update(legal)
|
context.update(legal)
|
||||||
@ -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):
|
def report_adoption(request, adoption_notice_id):
|
||||||
"""
|
"""
|
||||||
Form to report adoption notices
|
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
|
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)
|
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)
|
return render(request, 'fellchensammlung/details/detail-report.html', context)
|
||||||
|
|
||||||
@ -481,13 +618,11 @@ def my_profile(request):
|
|||||||
notification = CommentNotification.objects.get(pk=notification_id)
|
notification = CommentNotification.objects.get(pk=notification_id)
|
||||||
except CommentNotification.DoesNotExist:
|
except CommentNotification.DoesNotExist:
|
||||||
notification = BaseNotification.objects.get(pk=notification_id)
|
notification = BaseNotification.objects.get(pk=notification_id)
|
||||||
notification.read = True
|
notification.mark_read()
|
||||||
notification.save()
|
|
||||||
elif action == "notification_mark_all_read":
|
elif action == "notification_mark_all_read":
|
||||||
notifications = CommentNotification.objects.filter(user=request.user, mark_read=False)
|
notifications = CommentNotification.objects.filter(user=request.user, mark_read=False)
|
||||||
for notification in notifications:
|
for notification in notifications:
|
||||||
notification.read = True
|
notification.mark_read()
|
||||||
notification.save()
|
|
||||||
elif action == "search_subscription_delete":
|
elif action == "search_subscription_delete":
|
||||||
search_subscription_id = request.POST.get("search_subscription_id")
|
search_subscription_id = request.POST.get("search_subscription_id")
|
||||||
SearchSubscription.objects.get(pk=search_subscription_id).delete()
|
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)
|
@user_passes_test(user_is_trust_level_or_above)
|
||||||
def modqueue(request):
|
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}
|
context = {"reports": open_reports}
|
||||||
return render(request, 'fellchensammlung/modqueue.html', context=context)
|
return render(request, 'fellchensammlung/modqueue.html', context=context)
|
||||||
|
|
||||||
@ -532,10 +667,14 @@ def updatequeue(request):
|
|||||||
return render(request, 'fellchensammlung/updatequeue.html', context=context)
|
return render(request, 'fellchensammlung/updatequeue.html', context=context)
|
||||||
|
|
||||||
|
|
||||||
def map(request):
|
def map(request, templatename='fellchensammlung/map.html'):
|
||||||
adoption_notices = AdoptionNotice.get_active_ANs()
|
adoption_notices = AdoptionNotice.get_active_ANs()
|
||||||
context = {"adoption_notices_map": adoption_notices}
|
context = {"adoption_notices_map": adoption_notices}
|
||||||
return render(request, '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):
|
def metrics(request):
|
||||||
@ -632,3 +771,28 @@ def export_own_profile(request):
|
|||||||
ANs_as_json = serialize('json', ANs)
|
ANs_as_json = serialize('json', ANs)
|
||||||
full_json = f"{user_as_json}, {ANs_as_json}"
|
full_json = f"{user_as_json}, {ANs_as_json}"
|
||||||
return HttpResponse(full_json, content_type="application/json")
|
return HttpResponse(full_json, content_type="application/json")
|
||||||
|
|
||||||
|
|
||||||
|
def styleguide(request):
|
||||||
|
context = {"geocoding_api_url": settings.GEOCODING_API_URL, }
|
||||||
|
return render(request, 'fellchensammlung/styleguide.html', context=context)
|
||||||
|
|
||||||
|
|
||||||
|
def styleguide_bulma(request):
|
||||||
|
return render(request, 'fellchensammlung/bulma-styleguide.html')
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def rescue_organization_check(request):
|
||||||
|
if request.method == "POST":
|
||||||
|
rescue_org = RescueOrganization.objects.get(id=request.POST.get("rescue_organization_id"))
|
||||||
|
edit_permission = user_is_trust_level_or_above(request.user, TrustLevel.MODERATOR)
|
||||||
|
if not edit_permission:
|
||||||
|
return render(request, "fellchensammlung/errors/403.html", status=403)
|
||||||
|
action = request.POST.get("action")
|
||||||
|
if action == "checked":
|
||||||
|
rescue_org.set_checked()
|
||||||
|
|
||||||
|
last_checked_rescue_orgs = RescueOrganization.objects.order_by("last_checked")
|
||||||
|
context = {"rescue_orgs": last_checked_rescue_orgs, }
|
||||||
|
return render(request, 'fellchensammlung/rescue-organization-check.html', context=context)
|
||||||
|
@ -89,9 +89,9 @@ CELERY_RESULT_BACKEND = config.get("celery", "backend", fallback="redis://localh
|
|||||||
HEALTHCHECKS_URL = config.get("monitoring", "healthchecks_url", fallback=None)
|
HEALTHCHECKS_URL = config.get("monitoring", "healthchecks_url", fallback=None)
|
||||||
|
|
||||||
""" GEOCODING """
|
""" GEOCODING """
|
||||||
GEOCODING_API_URL = config.get("geocoding", "api_url", fallback="https://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 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 """
|
""" Tile Server """
|
||||||
MAP_TILE_SERVER = config.get("map", "tile_server", fallback="https://tiles.hyteck.de")
|
MAP_TILE_SERVER = config.get("map", "tile_server", fallback="https://tiles.hyteck.de")
|
||||||
@ -168,6 +168,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
"django.contrib.sitemaps",
|
||||||
'fontawesomefree',
|
'fontawesomefree',
|
||||||
'crispy_forms',
|
'crispy_forms',
|
||||||
"crispy_bootstrap4",
|
"crispy_bootstrap4",
|
||||||
@ -175,6 +176,7 @@ INSTALLED_APPS = [
|
|||||||
'rest_framework.authtoken',
|
'rest_framework.authtoken',
|
||||||
'drf_spectacular',
|
'drf_spectacular',
|
||||||
'drf_spectacular_sidecar', # required for Django collectstatic discovery
|
'drf_spectacular_sidecar', # required for Django collectstatic discovery
|
||||||
|
'widget_tweaks'
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
@ -3,25 +3,32 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% if form.errors %}
|
{% if form.errors %}
|
||||||
<p>{% translate "Dein Username oder Passwort ist falsch." %}</p>
|
<p>{% translate "Dein Username oder Passwort ist falsch." %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<p>{% translate "Du bist bereits eingeloggt." %}</p>
|
<p>{% translate "Du bist bereits eingeloggt." %}</p>
|
||||||
{% else %} {% if next %}
|
{% 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 %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not user.is_authenticated %}
|
{% if not user.is_authenticated %}
|
||||||
<form class="card" method="post" action="{% url 'login' %}">
|
<div class="card">
|
||||||
{% csrf_token %}
|
<div class="container-edit-buttons">
|
||||||
{{ form.as_p }}
|
<form method="post" action="{% url 'login' %}">
|
||||||
<input class="btn" type="submit" value={% translate "Einloggen" %} />
|
{% csrf_token %}
|
||||||
<input type="hidden" name="next" value="{{ next }}" />
|
{{ form.as_p }}
|
||||||
</form>
|
<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>
|
<div class="container-edit-buttons">
|
||||||
{% endif %}
|
<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 %}
|
{% endblock %}
|
@ -27,7 +27,6 @@ class DistanceTest(TestCase):
|
|||||||
l_stuttgart = LocationProxy("Stuttgart")
|
l_stuttgart = LocationProxy("Stuttgart")
|
||||||
l_tue = LocationProxy("Tübingen")
|
l_tue = LocationProxy("Tübingen")
|
||||||
# Should be 30km
|
# Should be 30km
|
||||||
print(f"{l_stuttgart.position} -> {l_tue.position}")
|
|
||||||
distance_tue_stuttgart = calculate_distance_between_coordinates(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.assertLess(distance_tue_stuttgart, 50)
|
||||||
self.assertGreater(distance_tue_stuttgart, 20)
|
self.assertGreater(distance_tue_stuttgart, 20)
|
||||||
|
@ -4,7 +4,7 @@ from django.utils import timezone
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from model_bakery import baker
|
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):
|
class UserTest(TestCase):
|
||||||
@ -77,3 +77,21 @@ class AnnouncementTest(TestCase):
|
|||||||
self.assertTrue(self.announcement2 not in active_announcements)
|
self.assertTrue(self.announcement2 not in active_announcements)
|
||||||
self.assertTrue(self.announcement4 not in active_announcements)
|
self.assertTrue(self.announcement4 not in active_announcements)
|
||||||
self.assertTrue(self.announcement5 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="">')
|
||||||
|
|
||||||
|
|