Compare commits
301 Commits
6702211c05
...
ci-test-co
Author | SHA1 | Date | |
---|---|---|---|
c9f46d7547 | |||
9f23f5768c | |||
19210f90cd | |||
462bb8f485 | |||
ea4d15b99a | |||
de30dfcb8b | |||
36a979954c | |||
71ef17dc97 | |||
206cd282e6 | |||
e399346c3e | |||
929c6dfff0 | |||
841b57fea2 | |||
9e5446ff1d | |||
3b79809b8c | |||
53e6db3655 | |||
424f91e919 | |||
84ce5f54b2 | |||
a7e85212c0 | |||
f1b3b660ff | |||
26cb60c1c8 | |||
69e58f1e0a | |||
5c33ac3833 | |||
fccfd59ea3 | |||
50897b6d35 | |||
8edfe8c401 | |||
0d82dba414 | |||
2dc038dfef | |||
c46a943c7f | |||
9f3592e64b | |||
bc1f4e7ab7 | |||
a2ef91e89a | |||
91d740511d | |||
c6af3e8d04 | |||
0c94049e21 | |||
29f1d2f0f2 | |||
2578e96b32 | |||
907ed583cd | |||
da51007b77 | |||
087f58c9ac | |||
860da7f06a | |||
457bee1ede | |||
3b37b5f588 | |||
6229f0f8a2 | |||
b2a3d910d9 | |||
33848cbe15 | |||
cc97fe32aa | |||
4576ac68e0 | |||
7c076e0bc3 | |||
74f54c7b31 | |||
87777cd5a4 | |||
eee4cdf86b | |||
b2d5265f7e | |||
d4af2d88b4 | |||
8b4f5713e3 | |||
4bff268537 | |||
57da42e4bd | |||
2864d27a7f | |||
0a73b5099e | |||
e3fb981542 | |||
5e80d75c91 | |||
e3833b4505 | |||
ab837ee80e | |||
f6c1224dde | |||
a78d671b6d | |||
fb9c78d96a | |||
4ef9da953c | |||
aefeffd63a | |||
81cc5cd53d | |||
002dded0d5 | |||
ad6e2f4e17 | |||
160e7166f8 | |||
867319fe9a | |||
13b67c1248 | |||
4c4cf4afea | |||
5f742c60db | |||
568874e6dd | |||
561a30b7ab | |||
a8c837e9f6 | |||
a75cacea66 | |||
b1e092769f | |||
5a93a1678c | |||
28772e1f74 | |||
1f3c3ecaef | |||
ab1e6a94d1 | |||
299653b53b | |||
fe9352e628 | |||
9fec95bd2e | |||
8e7cdafee0 | |||
6e2a2a1d5e | |||
5197875431 | |||
d05bd45cf4 | |||
0afb2bb0ce | |||
d17fcc1da2 | |||
c508bc2cd1 | |||
20872e547b | |||
25b748d2be | |||
1536bb302a | |||
d4ef706734 | |||
3bdce18e9e | |||
8b4488484d | |||
3881a4f3b4 | |||
2dbd908f4c | |||
9d0eed5915 | |||
ee12bb5286 | |||
5669c822b9 | |||
c1c4af6571 | |||
164ba7def2 | |||
7035b1642e | |||
b6fc5c634f | |||
0dfbd614ab | |||
2730ff3f51 | |||
fef211b2d0 | |||
f2e2599561 | |||
a9c0f628f7 | |||
e2adb20231 | |||
e8b3bf6516 | |||
3306f3e783 | |||
b993621773 | |||
3816290eb7 | |||
399ecf73ad | |||
8e2c0e857c | |||
3c7dcb4c51 | |||
9e1ec1711b | |||
bae4ee3d22 | |||
280eb83056 | |||
fca5879aeb | |||
373a44c9da | |||
674645c65c | |||
c2b3ff2395 | |||
d6740eb302 | |||
35a54474b4 | |||
6723dad4bd | |||
b51d04ffd1 | |||
a965f26d48 | |||
364a6f32f4 | |||
533142461a | |||
481635ac4e | |||
be6c30cb33 | |||
a617137fb0 | |||
8299162a77 | |||
085162d802 | |||
27b7e47f18 | |||
be97ac32fb | |||
9ea00655d4 | |||
9fffbffdb7 | |||
44cf2936d1 | |||
579f59580c | |||
241841bc9b | |||
78a6440f63 | |||
9d521b0129 | |||
39079c3c8e | |||
999c1a81b8 | |||
5a4720c41c | |||
858c6d4468 | |||
4b45b01e2a | |||
d0060ecf5e | |||
d1eeaafc42 | |||
9b824bc326 | |||
44f05cbb7d | |||
0e4e531414 | |||
6a7b3f19e9 | |||
ec9f5b305c | |||
e858f61b3f | |||
a04270718f | |||
a4f895de81 | |||
b2d0e783be | |||
4f5022e140 | |||
5771968981 | |||
b63b87872b | |||
1594b754cb | |||
8ec27191b6 | |||
c1332ee1f0 | |||
f6240a7189 | |||
7a02774a29 | |||
8945fdc0f4 | |||
9f0a18ad91 | |||
e7f26dd23a | |||
fc5b1391df | |||
70bf8e2053 | |||
caf98ba60b | |||
d7e466050a | |||
34b707ef20 | |||
064a9bf83a | |||
93070a3bcd | |||
23c35fe7dd | |||
d2542060a1 | |||
89f74cb709 | |||
ec38012ecb | |||
72d45a4f47 | |||
8de5f162eb | |||
dc3859d589 | |||
b4f52c7876 | |||
885622e581 | |||
a42a3fa177 | |||
27541c6fb6 | |||
14547ad621 | |||
8d2d80c30e | |||
e6f5a42d15 | |||
052e42f76a | |||
3eb7dbe984 | |||
202dfe46c2 | |||
01da0f1e29 | |||
8ccdf50bc5 | |||
d46ab8da6b | |||
1dd53a87e9 | |||
40bb2e54bd | |||
433ad9d4b9 | |||
231c27819d | |||
890309564f | |||
e1e1f822c8 | |||
7a788f4c90 | |||
7efa626b8b | |||
08e20e1875 | |||
f1c79a5f94 | |||
5dd1991af8 | |||
c0edef51bd | |||
cb703e79ae | |||
87066b0cea | |||
c4976c4b34 | |||
ee46ff9cda | |||
d4f27e8f2f | |||
4a6584370e | |||
82d3f95c99 | |||
dce3d89c7e | |||
5520590145 | |||
efabebfdbf | |||
6c52246bb7 | |||
2c11f7c385 | |||
9ee0bd8e30 | |||
1955476d24 | |||
05178da029 | |||
7a80cf8df1 | |||
db94ec41ed | |||
5582538a70 | |||
7aa364fc38 | |||
96ce5963fe | |||
bf54bc5d51 | |||
93ae172431 | |||
03d40a5092 | |||
993f8f9cd2 | |||
8efc0aad21 | |||
3a6e7f5344 | |||
dac9661d51 | |||
b9bfa8e359 | |||
d07589464c | |||
1880da5151 | |||
4e953c83ea | |||
2212df4729 | |||
98d67381c6 | |||
e02672c2bb | |||
c3dd9faa85 | |||
9f977e35c2 | |||
3269d5a39a | |||
d96a44bbdd | |||
2641b2e7bf | |||
50c1a4f2c6 | |||
573630f9ee | |||
1a09b7859f | |||
70b3ae4bbc | |||
5eaafe7646 | |||
5781b49c7c | |||
e2f516d409 | |||
ca8996fff6 | |||
eb734d2716 | |||
655e304c6c | |||
8e34ed440e | |||
0c7080f005 | |||
0b93b5eccb | |||
f1d9f7ad22 | |||
4e71ac7866 | |||
1d0a42a7e1 | |||
d384e75746 | |||
70154abd37 | |||
ab3437e61d | |||
0ccbb18411 | |||
e6f12ce5b1 | |||
6325de17d9 | |||
b9d6293546 | |||
dbe52e4884 | |||
3c286d84d8 | |||
227fa4d5a8 | |||
d47f181e1d | |||
272046142e | |||
5c18832961 | |||
d59cc0034a | |||
64024be833 | |||
5ef20bdce0 | |||
7ddd7b0c0c | |||
cbd8700917 | |||
6eb2f5000f | |||
1cd70228b9 | |||
23d8e85031 | |||
4fb92d8215 | |||
6dfc92bf15 | |||
2015f8b332 | |||
66a0b42718 | |||
efecfc910d | |||
96bc44c508 | |||
a2c8f469a7 | |||
a98b428614 | |||
dfede77e98 |
4
.coveragerc
Normal file
4
.coveragerc
Normal file
@@ -0,0 +1,4 @@
|
||||
[run]
|
||||
omit =
|
||||
*/migrations/*
|
||||
*/tests/*
|
@@ -6,6 +6,9 @@ steps:
|
||||
commands:
|
||||
- cd docs && make html
|
||||
|
||||
when:
|
||||
event: [ tag, push ]
|
||||
|
||||
deploy:
|
||||
image: appleboy/drone-scp
|
||||
settings:
|
||||
@@ -19,6 +22,8 @@ steps:
|
||||
source: docs/_build/html/
|
||||
key:
|
||||
from_secret: ssh_key
|
||||
when:
|
||||
event: [ tag, push ]
|
||||
|
||||
|
||||
|
14
.woodpecker/test.yml
Normal file
14
.woodpecker/test.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
|
||||
steps:
|
||||
test:
|
||||
image: python
|
||||
commands:
|
||||
- python -m pip install '.[develop]'
|
||||
- coverage run --source='.' src/manage.py test src && coverage html
|
||||
- coverage html
|
||||
- cat htmlcov/index.html
|
||||
when:
|
||||
event: [tag, push]
|
||||
|
||||
|
15
CHANGELOG.md
Normal file
15
CHANGELOG.md
Normal file
@@ -0,0 +1,15 @@
|
||||
## Version 0.4.0
|
||||
|
||||
Version 0.4.0 has added support for search-as-you-type when searching for animals to adopt. Furthermore, the display of
|
||||
maps in the search has been majorly improved.
|
||||
|
||||
Photon has been added as geocoding source option which allows to use this functionality.
|
||||
|
||||
Further improvements include the representation of rescue organizations and tooltips.
|
||||
|
||||
One of the biggest features is the addition of search subscriptions. These allow you to not only
|
||||
search for currently active adoption notices but to subscribe to that search so that you get notified if there are new
|
||||
rats in your search area in the future.
|
||||
|
||||
For developers the new API documentation might come in handy, it can be found at
|
||||
[/api/schema/swagger-ui/](https://notfellchen.org/api/schema/swagger-ui/)
|
65
README.md
65
README.md
@@ -44,14 +44,16 @@ nf query_location <query>
|
||||
There is a system for customizing texts in Notfellchen. Not every change of a tet should mean an update of the software. But this should also not become a CMS.
|
||||
Therefore, a solution is used where a number of predefined texts per site are supported. These markdown texts will then be included in the site, if defined.
|
||||
|
||||
| Textcode | Location |
|
||||
|---------------------|----------|
|
||||
| `how_to` | Index |
|
||||
| `introduction` | Index |
|
||||
| `privacy_statement` | About |
|
||||
| `terms_of_service` | About |
|
||||
| `imprint` | About |
|
||||
| Any rule | About |
|
||||
| Textcode | Location |
|
||||
|-------------------------|-----------------------|
|
||||
| `how_to` | Index |
|
||||
| `introduction` | Index |
|
||||
| `privacy_statement` | About |
|
||||
| `terms_of_service` | About |
|
||||
| `imprint` | About |
|
||||
| `about_us` | About |
|
||||
| `external_site_warning` | External Site Warning |
|
||||
| Any rule | About |
|
||||
|
||||
# Developer Notes
|
||||
|
||||
@@ -75,20 +77,36 @@ docker push moanos/notfellchen:latest
|
||||
docker run -p8000:7345 moanos/notfellchen:latest
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Tests can be run with
|
||||
|
||||
```zsh
|
||||
nf test src
|
||||
```
|
||||
|
||||
If you want to report on code coverage run
|
||||
|
||||
```zsh
|
||||
coverage run --source='.' src/manage.py test src
|
||||
```
|
||||
|
||||
and
|
||||
|
||||
```
|
||||
coverage report
|
||||
```
|
||||
|
||||
## Geocoding
|
||||
|
||||
Geocoding services (search map data by name, address or postcode) are provided via the
|
||||
[Nominatim](https://nominatim.org/) API, powered by [OpenStreetMap](https://openstreetmap.org) data. Notfellchen uses
|
||||
a selfhosted Nominatim instance to avoid overburdening the publicly hosted instance. Due to ressource constraints
|
||||
geocoding is only supported for Germany right now.
|
||||
|
||||
ToDos
|
||||
* [ ] Implement a report that shows the number of location strings that could not be converted into a location
|
||||
* [x] Add a management command to re-query location strings to fill location
|
||||
either [Nominatim](https://nominatim.org/) or [photon](https://github.com/komoot/photon) API, powered by [OpenStreetMap](https://openstreetmap.org) data.
|
||||
Notfellchen uses a selfhosted Photon instance to avoid overburdening the publicly hosted instance.
|
||||
|
||||
## Maps
|
||||
|
||||
The map on the main homepage is powered by [Versatiles](https://versatiles.org), and rendered using [Maplibre](https://maplibre.org/).
|
||||
The Versatiles server is self-hosted and does not send data to third parties.
|
||||
|
||||
## Translation
|
||||
|
||||
@@ -123,3 +141,20 @@ Start beat
|
||||
```zsh
|
||||
celery -A notfellchen.celery beat
|
||||
```
|
||||
|
||||
# Contributing
|
||||
|
||||
This project is currently solely developed by me, moanos. I'd like that to change and will be very happy for contributions
|
||||
and shared responsibilities. Some ideas where you can look for contributing first
|
||||
|
||||
* CSS structure: It's a hot mess right now, and I'm happy it somehow works. As you might see, there is much room for improvement. Refactoring this and streamlining the look across the app would be amazing.
|
||||
* Docker: If you know how to build a docker container that is a) smaller or b) utilizes staged builds this would be amazing. Any improvement welcome
|
||||
* Testing: Writing tests is always welcome, and it's likely you discover a few bugs
|
||||
|
||||
I'm also very happy for all other contributions. Before you do large refactoring efforts or features, best write a short
|
||||
issue for it before you spend a lot of work.
|
||||
|
||||
Send PRs either to [codeberg](https://codeberg.org/moanos/notfellchen) (preferred) or [Github](https://github.com/moan0s/notfellchen).
|
||||
CI (currently only for dcumentation) runs via [git.hyteck.de](https://git.hyteck.de), you can also ask moanos for an account there.
|
||||
|
||||
Also welcome are new issues with suggestions or bugs and additions to the documentation.
|
||||
|
@@ -2,10 +2,10 @@
|
||||
API Documentation
|
||||
*****************
|
||||
|
||||
The Notfellchen API serves the purpose of supporting 3rd-person applications and anything you can think of basically.
|
||||
The Notfellchen API serves the purpose of supporting 3rd-person applications, whether you want to display data in a custom format or add data from other sources.
|
||||
|
||||
.. warning::
|
||||
The current API is limited in it's functionality. I you miss a specific feature please contact the developer!
|
||||
The current API is limited in it's functionality. I you miss a specific feature please contact the developers!
|
||||
|
||||
API Access
|
||||
==========
|
||||
@@ -14,17 +14,88 @@ Via browser
|
||||
-----------
|
||||
|
||||
When a user is logged in, they can easily access the API in their browser, authenticated by their session.
|
||||
The API endpoint can be found at /library/api/
|
||||
http://notfellchen.org/
|
||||
The API endpoint can be found at http://notfellchen.org/api/adoption_notices
|
||||
|
||||
Via token
|
||||
---------
|
||||
|
||||
.. warning::
|
||||
This is currently not supported.
|
||||
|
||||
All users are able to generate a token that allows them to use the API. This can be done in the user's profile.
|
||||
An application can then send this token in the request header for authorization.
|
||||
|
||||
.. code-block::
|
||||
$ curl -X GET http://notfellchen.org/api/adoption_notice -H 'Authorization: Token 49b39856955dc6e5cc04365498d4ad30ea3aed78'
|
||||
|
||||
|
||||
.. warning::
|
||||
Usage or creation of content still has to follow the terms of Notfellchen.org
|
||||
Copyright of content is often held by rescue organizations, so you are not allowed to simply mirror content.
|
||||
Talk to the Notfellchen-Team if you want develop such things.
|
||||
|
||||
|
||||
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
|
||||
++++++++++++++++++++
|
||||
|
||||
.. code-block::
|
||||
curl --request GET \
|
||||
--url https://notfellchen.org/api/adoption_notice \
|
||||
--header 'Authorization: {{token}}'
|
||||
|
||||
Create Adoption Notice
|
||||
++++++++++++++++++++++
|
||||
|
||||
.. code-block::
|
||||
curl --request POST \
|
||||
--url https://notfellchen.org/api/adoption_notice \
|
||||
--header 'Authorization: {{token}}' \
|
||||
--header 'content-type: multipart/form-data' \
|
||||
--form name=TestAdoption1 \
|
||||
--form searching_since=2024-11-19 \
|
||||
--form 'description=Lorem ipsum **dolor sit** amet' \
|
||||
--form further_information=https://notfellchen.org \
|
||||
--form location_string=Berlin \
|
||||
--form group_only=true
|
||||
|
||||
Add Animal to Adoption Notice
|
||||
+++++++++++++++++++++++++++++
|
||||
|
||||
.. code-block::
|
||||
curl --request POST \
|
||||
--url https://notfellchen.org/api/animals/ \
|
||||
--header 'Authorization: {{token}}' \
|
||||
--header 'content-type: multipart/form-data' \
|
||||
--form name=TestAnimal1 \
|
||||
--form date_of_birth=2024-11-19 \
|
||||
--form 'description=Lorem animal **dolor sit**.' \
|
||||
--form sex=F \
|
||||
--form species=1 \
|
||||
--form adoption_notice=1
|
||||
|
||||
Add picture to Animal or Adoption Notice
|
||||
++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
.. code-block::
|
||||
curl -X POST https://notfellchen.org/api/images/ \
|
||||
-H "Authorization: Token {{token}}" \
|
||||
-F "image=@256-256-crop.jpg" \
|
||||
-F "alt_text=Puppy enjoying the sunshine" \
|
||||
-F "attach_to_type=animal" \
|
||||
-F "attach_to=48
|
||||
|
||||
Species
|
||||
+++++++
|
||||
|
||||
Getting available species is mainly important when creating animals
|
||||
|
||||
.. code-block::
|
||||
curl --request GET \
|
||||
--url https://notfellchen.org/api/species \
|
||||
--header 'Authorization: {{token}}'
|
||||
|
@@ -20,7 +20,7 @@
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'Notfellchen'
|
||||
copyright = 'Julian-Samuel Gebühr'
|
||||
copyright = 'CC-BY-SA Julian-Samuel Gebühr'
|
||||
author = 'Julian-Samuel Gebühr'
|
||||
|
||||
# The short X.Y version
|
||||
|
@@ -5,7 +5,7 @@ Report a bug
|
||||
^^^^^^^^^^^^
|
||||
|
||||
To report a bug, file an issue on `Github
|
||||
<https://codeberg.org/moanos/notfellchen/issues>`_
|
||||
<https://github.com/moan0s/notfellchen/issues>`_
|
||||
|
||||
Try to include the following information:
|
||||
|
||||
@@ -29,7 +29,7 @@ To contribute simply clone the directory, make your changes and file a
|
||||
pull request.
|
||||
|
||||
If you want to know what can be done, have a look at the current `Github
|
||||
<https://codeberg.org/moanos/notfellchen/issues>`_.
|
||||
<https://github.com/moan0s/notfellchen/issues>`_.
|
||||
|
||||
Get in touch!
|
||||
^^^^^^^^^^^^^
|
||||
|
@@ -8,5 +8,6 @@ Installation, customization and contributing
|
||||
|
||||
deployment.rst
|
||||
contributing.rst
|
||||
translation.rst
|
||||
release.rst
|
||||
backup.rst
|
||||
|
@@ -5,8 +5,7 @@ What qualifies as release?
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
A new release should be announced when a significant number functions, bugfixes or other improvements to the software
|
||||
is made. Usually this indicates a minor release.
|
||||
Major releases are yet to be determined.
|
||||
is made. Notfellchen follows `Semantic Versioning <https://semver.org/>`_.
|
||||
|
||||
What should be done before a release?
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -14,7 +13,7 @@ What should be done before a release?
|
||||
Tested basic functions
|
||||
######################
|
||||
|
||||
Run :command:`pytest`
|
||||
Run :command:`nf test src`
|
||||
|
||||
Test upgrade on a copy of a production database
|
||||
###############################################
|
||||
@@ -38,4 +37,4 @@ Do a final commit on this change, and tag the commit as release with appropriate
|
||||
git tag -a v1.0.0 -m "Releasing version v1.0.0"
|
||||
git push origin v1.0.0
|
||||
|
||||
Make sure the tag is visible on Codeberg and celebrate 🥳
|
||||
Make sure the tag is visible on GitHub/Codeberg and celebrate 🥳
|
||||
|
@@ -1,6 +1,7 @@
|
||||
Benachrichtigungen
|
||||
==================
|
||||
|
||||
Ersteller*innen von Vermittlungen werden über neue Kommentare per Mail benachrichtigt, ebenso alle die die Vermittlung abonniert haben.
|
||||
Jede Vermittlung kann abonniert werden. Dafür klickst du auf die Glocke neben dem Titel der Vermittlung.
|
||||
|
||||
.. image:: abonnieren.png
|
||||
|
@@ -1,10 +1,11 @@
|
||||
***********
|
||||
Users guide
|
||||
***********
|
||||
******************
|
||||
User Dokumentation
|
||||
******************
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
:caption: Inhalt:
|
||||
|
||||
registrierung.rst
|
||||
benachrichtigungen.rst
|
||||
vermittlungen.rst
|
||||
moderationskonzept.rst
|
||||
benachrichtigungen.rst
|
||||
|
29
docs/user/moderationskonzept.rst
Normal file
29
docs/user/moderationskonzept.rst
Normal file
@@ -0,0 +1,29 @@
|
||||
Moderationskonzept
|
||||
==================
|
||||
|
||||
Vertrauen in notfellchen.org ist uns wichtig. Unser Kernziel ist es Tierschutz und Tierwohl zu fördern. Dafür sind drei
|
||||
Grundkonzepte wichtig
|
||||
|
||||
* Aktualität: Informationen auf notfellchen.org müssen aktuell&richtig sein
|
||||
* Tierschutz: Ausschließlich Ratten aus dem Tierschutz werden vermittelt
|
||||
* Moderation: Vermittlungen und Kommentare können gemeldet werden und werden vom Team zügig moderiert.
|
||||
|
||||
Vermittlungen
|
||||
+++++++++++++
|
||||
|
||||
Vermittlungen können von allen Nutzer*innen mit Account erstellt werden. Vermittlungen normaler Nutzer*innen kommen dann in eine Warteschlange und werden vom Admin & Modertionsteam geprüft und sichtbar geschaltet.
|
||||
Tierheime und Pflegestellen können auf Anfrage einen Koordinations-Status bekommen, wodurch sie Vermittlungsanzeigen erstellen können die direkt öffentlich sichtbar sind.
|
||||
|
||||
Jede Vermittlung hat ein "Zuletzt-geprüft" Datum, das anzeigt, wann ein Mensch zuletzt überprüft hat, ob die Anzeige noch aktuell ist.
|
||||
Nach 3 Wochen ohne Prüfung werden Anzeigen automatisch von der Seite entfernt und nur dann wieder freigeschaltet, wenn eine manuelle Prüfung erfolgt.
|
||||
|
||||
Darüber hinaus werden einmal täglich die verlinkten Seiten automatisiert geprüft. Wenn eine Vermittlung auf der Website eines Tierheims oder einer Pflegestelle entfernt wird, wird die Anzeige sofort deaktiviert.
|
||||
|
||||
Vermittlungen können von allen Menschen, auch ohne Account gemeldet werden. Grund für eine Meldung kann sein, dass Informationen veraltet sind oder ein Verdacht von Tierwohlgefärdung. Gemeldete Vermittlungen werden vom Moderationsteam geprüft und ggf. entfernt.
|
||||
|
||||
Kommentare
|
||||
++++++++++
|
||||
|
||||
Die Kommentarfunktion von Vermittlungen ermöglicht es angemeldeten Nutzer*innen zusätzliche Informationen hinzuzufügen oder Fragen zu stellen.
|
||||
|
||||
Kommentare können, wie Vermittlungen, gemeldet werden wenn sie nicht den Regeln entsprechen.
|
@@ -24,4 +24,7 @@ console-only=true
|
||||
app_log_level=INFO
|
||||
django_log_level=INFO
|
||||
|
||||
[geocoding]
|
||||
api_url=https://photon.hyteck.de/api
|
||||
api_format=photon
|
||||
|
||||
|
@@ -8,13 +8,13 @@ build-backend = "setuptools.build_meta"
|
||||
name = "notfellchen"
|
||||
description = "A tool to help."
|
||||
authors = [
|
||||
{name = "moanos", email = "julian-samuel@gebuehr.net"},
|
||||
{ name = "moanos", email = "julian-samuel@gebuehr.net" },
|
||||
]
|
||||
maintainers = [
|
||||
{name = "moanos", email = "julian-samuel@gebuehr.net"},
|
||||
{ name = "moanos", email = "julian-samuel@gebuehr.net" },
|
||||
]
|
||||
keywords = ["animal", "adoption", "django", "rescue", ]
|
||||
license = {text = "AGPL-3.0-or-later"}
|
||||
license = { text = "AGPL-3.0-or-later" }
|
||||
classifiers = [
|
||||
"Environment :: Web",
|
||||
"License :: OSI Approved :: GNU Affero General Public License v3",
|
||||
@@ -24,25 +24,32 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"Django",
|
||||
"coverage",
|
||||
"codecov",
|
||||
"sphinx",
|
||||
"sphinx-rtd-theme",
|
||||
"gunicorn",
|
||||
"fontawesomefree",
|
||||
"whitenoise",
|
||||
"model_bakery",
|
||||
"markdown",
|
||||
"Pillow",
|
||||
"django-registration",
|
||||
"psycopg2",
|
||||
"psycopg2-binary",
|
||||
"django-crispy-forms",
|
||||
"crispy-bootstrap4",
|
||||
"djangorestframework",
|
||||
"celery[redis]"
|
||||
"celery[redis]",
|
||||
"drf-spectacular[sidecar]"
|
||||
]
|
||||
|
||||
dynamic = ["version", "readme"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
develop = [
|
||||
"pytest",
|
||||
"coverage",
|
||||
"model_bakery",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://notfellchen.org"
|
||||
repository = "https://codeberg.org/moanos/notfellchen/"
|
||||
@@ -55,6 +62,6 @@ nf = 'notfellchen.main:main'
|
||||
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "notfellchen.__version__"}
|
||||
readme = {file = "README.md"}
|
||||
version = { attr = "notfellchen.__version__" }
|
||||
readme = { file = "README.md" }
|
||||
|
||||
|
@@ -1,11 +1,17 @@
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
import csv
|
||||
|
||||
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log
|
||||
from django.contrib import admin
|
||||
from django.http import HttpResponse
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
|
||||
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
|
||||
SpeciesSpecificURL
|
||||
|
||||
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
|
||||
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions
|
||||
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, BaseNotification
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class StatusInline(admin.StackedInline):
|
||||
@@ -14,13 +20,52 @@ class StatusInline(admin.StackedInline):
|
||||
|
||||
@admin.register(AdoptionNotice)
|
||||
class AdoptionNoticeAdmin(admin.ModelAdmin):
|
||||
search_fields = ("name__icontains", "description__icontains")
|
||||
list_filter = ("owner",)
|
||||
inlines = [
|
||||
StatusInline,
|
||||
]
|
||||
actions = ("activate",)
|
||||
|
||||
def activate(self, request, queryset):
|
||||
for obj in queryset:
|
||||
obj.set_active()
|
||||
|
||||
activate.short_description = _("Ausgewählte Vermittlungen aktivieren")
|
||||
|
||||
|
||||
# Re-register UserAdmin
|
||||
admin.site.register(User)
|
||||
@admin.register(User)
|
||||
class UserAdmin(admin.ModelAdmin):
|
||||
search_fields = ("usernamname__icontains", "first_name__icontains", "last_name__icontains", "email__icontains")
|
||||
list_display = ("username", "email", "trust_level", "is_active", "view_adoption_notices")
|
||||
list_filter = ("is_active", "trust_level",)
|
||||
actions = ("export_as_csv",)
|
||||
|
||||
def view_adoption_notices(self, obj):
|
||||
count = obj.adoption_notices.count()
|
||||
url = (
|
||||
reverse("admin:fellchensammlung_adoptionnotice_changelist")
|
||||
+ "?"
|
||||
+ urlencode({"owner__id": f"{obj.id}"})
|
||||
)
|
||||
return format_html('<a href="{}">{} Adoption Notices</a>', url, count)
|
||||
|
||||
def export_as_csv(self, request, queryset):
|
||||
meta = self.model._meta
|
||||
field_names = [field.name for field in meta.fields]
|
||||
|
||||
response = HttpResponse(content_type='text/csv')
|
||||
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
|
||||
writer = csv.writer(response)
|
||||
|
||||
writer.writerow(field_names)
|
||||
for obj in queryset:
|
||||
row = writer.writerow([getattr(obj, field) for field in field_names])
|
||||
|
||||
return response
|
||||
|
||||
export_as_csv.short_description = _("Ausgewählte User exportieren")
|
||||
|
||||
|
||||
def _reported_content_link(obj):
|
||||
@@ -49,17 +94,48 @@ class ReportAdoptionNoticeAdmin(admin.ModelAdmin):
|
||||
|
||||
reported_content_link.short_description = "Reported Content"
|
||||
|
||||
class SpeciesSpecificURLInline(admin.StackedInline):
|
||||
model = SpeciesSpecificURL
|
||||
|
||||
@admin.register(RescueOrganization)
|
||||
class RescueOrganizationAdmin(admin.ModelAdmin):
|
||||
search_fields = ("name","description", "internal_comment", "location_string")
|
||||
list_display = ("name", "trusted", "allows_using_materials", "website")
|
||||
list_filter = ("allows_using_materials", "trusted",)
|
||||
|
||||
inlines = [
|
||||
SpeciesSpecificURLInline,
|
||||
]
|
||||
|
||||
|
||||
@admin.register(Text)
|
||||
class TextAdmin(admin.ModelAdmin):
|
||||
search_fields = ("title__icontains", "text_code__icontains",)
|
||||
|
||||
|
||||
@admin.register(Comment)
|
||||
class CommentAdmin(admin.ModelAdmin):
|
||||
list_filter = ("user",)
|
||||
|
||||
|
||||
@admin.register(BaseNotification)
|
||||
class BaseNotificationAdmin(admin.ModelAdmin):
|
||||
list_filter = ("user", "read")
|
||||
|
||||
@admin.register(SearchSubscription)
|
||||
class SearchSubscriptionAdmin(admin.ModelAdmin):
|
||||
list_filter = ("owner",)
|
||||
|
||||
|
||||
admin.site.register(Animal)
|
||||
admin.site.register(Species)
|
||||
admin.site.register(RescueOrganization)
|
||||
admin.site.register(Location)
|
||||
admin.site.register(Rule)
|
||||
admin.site.register(Image)
|
||||
admin.site.register(ModerationAction)
|
||||
admin.site.register(Language)
|
||||
admin.site.register(Text)
|
||||
admin.site.register(Announcement)
|
||||
admin.site.register(AdoptionNoticeStatus)
|
||||
admin.site.register(Subscriptions)
|
||||
admin.site.register(Log)
|
||||
admin.site.register(Timestamp)
|
||||
|
@@ -1,10 +1,53 @@
|
||||
from ..models import AdoptionNotice
|
||||
from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
|
||||
|
||||
class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
model = AdoptionNotice
|
||||
fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information", "group_only"]
|
||||
fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information",
|
||||
"group_only"]
|
||||
|
||||
|
||||
class AnimalCreateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Animal
|
||||
fields = ["name", "date_of_birth", "description", "species", "sex", "adoption_notice"]
|
||||
|
||||
class RescueOrgSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RescueOrganization
|
||||
fields = ["name", "location_string", "instagram", "facebook", "fediverse_profile", "email", "phone_number",
|
||||
"website", "description", "external_object_identifier", "external_source_identifier"]
|
||||
|
||||
class AnimalGetSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Animal
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class RescueOrganizationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RescueOrganization
|
||||
exclude = ["internal_comment", "allows_using_materials"]
|
||||
|
||||
|
||||
class ImageCreateSerializer(serializers.ModelSerializer):
|
||||
@staticmethod
|
||||
def _animal_or_an(value):
|
||||
if not value in ["animal", "adoption_notice"]:
|
||||
raise serializers.ValidationError(
|
||||
'Set either animal or adoption_notice, depending on what type of object the image should be attached to.')
|
||||
|
||||
attach_to_type = serializers.CharField(validators=[_animal_or_an])
|
||||
attach_to = serializers.IntegerField()
|
||||
|
||||
class Meta:
|
||||
model = Image
|
||||
exclude = ["owner"]
|
||||
|
||||
|
||||
class SpeciesSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Species
|
||||
fields = "__all__"
|
||||
|
@@ -1,8 +1,16 @@
|
||||
from django.urls import path
|
||||
from .views import (
|
||||
AdoptionNoticeApiView
|
||||
AdoptionNoticeApiView,
|
||||
AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('adoption_notice', AdoptionNoticeApiView.as_view()),
|
||||
path("adoption_notice", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-list"),
|
||||
path("adoption_notice/<int:id>/", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-detail"),
|
||||
path("animals/", AnimalApiView.as_view(), name="api-animal-list"),
|
||||
path("animals/<int:id>/", AnimalApiView.as_view(), name="api-animal-detail"),
|
||||
path("organizations/", RescueOrganizationApiView.as_view(), name="api-organization-list"),
|
||||
path("organizations/<int:id>/", RescueOrganizationApiView.as_view(), name="api-organization-detail"),
|
||||
path("images/", AddImageApiView.as_view(), name="api-add-image"),
|
||||
path("species/", SpeciesApiView.as_view(), name="api-species-list"),
|
||||
]
|
||||
|
@@ -1,37 +1,212 @@
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from django.db import transaction
|
||||
from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel
|
||||
from fellchensammlung.tasks import post_adoption_notice_save
|
||||
from rest_framework import status
|
||||
from rest_framework import permissions
|
||||
from ..models import AdoptionNotice
|
||||
from .serializers import AdoptionNoticeSerializer
|
||||
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from .serializers import (
|
||||
AnimalGetSerializer,
|
||||
AnimalCreateSerializer,
|
||||
RescueOrganizationSerializer,
|
||||
AdoptionNoticeSerializer,
|
||||
ImageCreateSerializer,
|
||||
SpeciesSerializer, RescueOrgSerializer,
|
||||
)
|
||||
from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image
|
||||
from drf_spectacular.utils import extend_schema
|
||||
|
||||
class AdoptionNoticeApiView(APIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
{
|
||||
'name': 'id',
|
||||
'required': False,
|
||||
'description': 'ID of the adoption notice to retrieve.',
|
||||
'type': int
|
||||
},
|
||||
],
|
||||
responses={200: AdoptionNoticeSerializer(many=True)}
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
serializer_context = {
|
||||
'request': request,
|
||||
}
|
||||
"""
|
||||
Retrieve adoption notices with their related animals and images.
|
||||
"""
|
||||
adoption_notice_id = kwargs.get("id")
|
||||
if adoption_notice_id:
|
||||
try:
|
||||
adoption_notice = AdoptionNotice.objects.get(pk=adoption_notice_id)
|
||||
serializer = AdoptionNoticeSerializer(adoption_notice, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except AdoptionNotice.DoesNotExist:
|
||||
return Response({"error": "Adoption notice not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
adoption_notices = AdoptionNotice.objects.all()
|
||||
serializer = AdoptionNoticeSerializer(adoption_notices, many=True, context=serializer_context)
|
||||
serializer = AdoptionNoticeSerializer(adoption_notices, many=True, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@transaction.atomic
|
||||
@extend_schema(
|
||||
request=AdoptionNoticeSerializer,
|
||||
responses={201: 'Adoption notice created successfully!'}
|
||||
)
|
||||
def post(self, request, *args, **kwargs):
|
||||
data = {
|
||||
'name': request.data.get('name'),
|
||||
"searching_since": request.data.get('searching_since'),
|
||||
"description": request.data.get('description'),
|
||||
"organization": request.data.get('organization'),
|
||||
"further_information": request.data.get('further_information'),
|
||||
"location_string": request.data.get('location_string'),
|
||||
"group_only": request.data.get('group_only'),
|
||||
"owner": request.data.get('owner')
|
||||
}
|
||||
serializer = AdoptionNoticeSerializer(data=data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
"""
|
||||
API view to add an adoption notice.
|
||||
"""
|
||||
serializer = AdoptionNoticeSerializer(data=request.data, context={'request': request})
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
adoption_notice = serializer.save(owner=request.user)
|
||||
|
||||
# Add the location
|
||||
post_adoption_notice_save.delay_on_commit(adoption_notice.pk)
|
||||
|
||||
# Only set active when user has trust level moderator or higher
|
||||
if request.user.trust_level >= TrustLevel.MODERATOR:
|
||||
adoption_notice.set_active()
|
||||
else:
|
||||
adoption_notice.set_unchecked()
|
||||
|
||||
# Log the action
|
||||
Log.objects.create(
|
||||
user=request.user,
|
||||
action="add_adoption_notice",
|
||||
text=f"{request.user} added adoption notice {adoption_notice.pk} via API",
|
||||
)
|
||||
|
||||
# Return success response with new adoption notice details
|
||||
return Response(
|
||||
{"message": "Adoption notice created successfully!", "id": adoption_notice.pk},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class AnimalApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Get list of animals or a specific animal by ID.
|
||||
"""
|
||||
animal_id = kwargs.get("id")
|
||||
if animal_id:
|
||||
try:
|
||||
animal = Animal.objects.get(pk=animal_id)
|
||||
serializer = AnimalGetSerializer(animal, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Animal.DoesNotExist:
|
||||
return Response({"error": "Animal not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
animals = Animal.objects.all()
|
||||
serializer = AnimalGetSerializer(animals, many=True, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Create a new animal.
|
||||
"""
|
||||
serializer = AnimalCreateSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
animal = serializer.save(owner=request.user)
|
||||
return Response(
|
||||
{"message": "Animal created successfully!", "id": animal.id},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
class RescueOrganizationApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
{
|
||||
'name': 'id',
|
||||
'required': False,
|
||||
'description': 'ID of the rescue organization to retrieve.',
|
||||
'type': int
|
||||
},
|
||||
],
|
||||
responses={200: RescueOrganizationSerializer(many=True)}
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Get list of rescue organizations or a specific organization by ID.
|
||||
"""
|
||||
org_id = kwargs.get("id")
|
||||
if org_id:
|
||||
try:
|
||||
organization = RescueOrganization.objects.get(pk=org_id)
|
||||
serializer = RescueOrganizationSerializer(organization, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except RescueOrganization.DoesNotExist:
|
||||
return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
organizations = RescueOrganization.objects.all()
|
||||
serializer = RescueOrganizationSerializer(organizations, many=True, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@transaction.atomic
|
||||
@extend_schema(
|
||||
request=RescueOrgSerializer, # Document the request body
|
||||
responses={201: 'Rescue organization created/updated successfully!'}
|
||||
)
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Create or update a rescue organization.
|
||||
"""
|
||||
serializer = RescueOrgSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
rescue_org = serializer.save(owner=request.user)
|
||||
return Response(
|
||||
{"message": "Rescue organization created/updated successfully!", "id": rescue_org.id},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
class AddImageApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@transaction.atomic
|
||||
@extend_schema(
|
||||
request=ImageCreateSerializer,
|
||||
responses={201: 'Image added successfully!'}
|
||||
)
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Add an image to an animal or adoption notice.
|
||||
"""
|
||||
serializer = ImageCreateSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
if serializer.validated_data["attach_to_type"] == "animal":
|
||||
object_to_attach_to = Animal.objects.get(id=serializer.validated_data["attach_to"])
|
||||
elif serializer.validated_data["attach_to_type"] == "adoption_notice":
|
||||
object_to_attach_to = AdoptionNotice.objects.get(id=serializer.validated_data["attach_to"])
|
||||
else:
|
||||
raise ValueError("Unknown attach_to_type given, should not happen. Check serializer")
|
||||
serializer.validated_data.pop('attach_to_type', None)
|
||||
serializer.validated_data.pop('attach_to', None)
|
||||
image = serializer.save(owner=request.user)
|
||||
object_to_attach_to.photos.add(image)
|
||||
return Response(
|
||||
{"message": "Image added successfully!", "id": image.id},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class SpeciesApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(
|
||||
responses={200: SpeciesSerializer(many=True)}
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Retrieve a list of species.
|
||||
"""
|
||||
species = Species.objects.all()
|
||||
serializer = SpeciesSerializer(species, many=True, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
@@ -15,3 +15,4 @@ class FellchensammlungConfig(AppConfig):
|
||||
except Permission.DoesNotExist:
|
||||
pass
|
||||
post_migrate.connect(ensure_languages, sender=self)
|
||||
import fellchensammlung.receivers
|
||||
|
@@ -1,14 +1,21 @@
|
||||
import datetime
|
||||
|
||||
from django import forms
|
||||
|
||||
from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportComment, ModerationAction, User, Species, \
|
||||
Comment
|
||||
Comment, SexChoicesWithAll, DistanceChoices
|
||||
from django_registration.forms import RegistrationForm
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Submit, Layout, Fieldset, HTML, Row, Column, Field, Hidden
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from notfellchen.settings import MEDIA_URL
|
||||
from crispy_forms.layout import Div
|
||||
|
||||
|
||||
def animal_validator(value: str):
|
||||
value = value.lower()
|
||||
animal_list = ["ratte", "farbratte", "katze", "hund", "kaninchen", "hase", "kuh", "fuchs", "cow", "rat", "cat",
|
||||
"dog", "rabbit", "fox", "fancy rat"]
|
||||
if value not in animal_list:
|
||||
raise forms.ValidationError(_("Dieses Tier kenne ich nicht. Probier ein anderes"))
|
||||
|
||||
|
||||
class DateInput(forms.DateInput):
|
||||
@@ -45,6 +52,7 @@ class AdoptionNoticeForm(forms.ModelForm):
|
||||
'group_only',
|
||||
'searching_since',
|
||||
'location_string',
|
||||
'organization',
|
||||
'description',
|
||||
'further_information',
|
||||
),
|
||||
@@ -52,19 +60,20 @@ class AdoptionNoticeForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = AdoptionNotice
|
||||
fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string"]
|
||||
fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string",
|
||||
"organization"]
|
||||
|
||||
|
||||
class AdoptionNoticeFormWithDateWidget(AdoptionNoticeForm):
|
||||
class Meta:
|
||||
model = AdoptionNotice
|
||||
fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string"]
|
||||
fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string",
|
||||
"organization"]
|
||||
widgets = {
|
||||
'searching_since': DateInput(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
class AnimalForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'in_adoption_notice_creation_flow' in kwargs:
|
||||
@@ -93,6 +102,7 @@ class AnimalFormWithDateWidget(AnimalForm):
|
||||
'date_of_birth': DateInput(),
|
||||
}
|
||||
|
||||
|
||||
class AdoptionNoticeFormWithDateWidgetAutoAnimal(AdoptionNoticeFormWithDateWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AdoptionNoticeFormWithDateWidgetAutoAnimal, self).__init__(*args, **kwargs)
|
||||
@@ -115,11 +125,21 @@ class ImageForm(forms.ModelForm):
|
||||
self.helper.form_id = 'form-animal-photo'
|
||||
self.helper.form_class = 'card'
|
||||
self.helper.form_method = 'post'
|
||||
|
||||
if in_flow:
|
||||
self.helper.add_input(Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')))
|
||||
self.helper.add_input(Submit('submit', _('Speichern')))
|
||||
submits= Div(Submit('submit', _('Speichern')),
|
||||
Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')), css_class="container-edit-buttons")
|
||||
else:
|
||||
self.helper.add_input(Submit('submit', _('Submit')))
|
||||
submits = Fieldset(Submit('submit', _('Speichern')), css_class="container-edit-buttons")
|
||||
self.helper.layout = Layout(
|
||||
Div(
|
||||
'image',
|
||||
'alt_text',
|
||||
css_class="spaced",
|
||||
),
|
||||
submits
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Image
|
||||
@@ -161,6 +181,8 @@ class CustomRegistrationForm(RegistrationForm):
|
||||
class Meta(RegistrationForm.Meta):
|
||||
model = User
|
||||
|
||||
captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_("Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um sicherzustellen, dass du kein Roboter bist."))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
@@ -170,10 +192,8 @@ class CustomRegistrationForm(RegistrationForm):
|
||||
self.helper.add_input(Submit('submit', _('Registrieren'), css_class="btn"))
|
||||
|
||||
|
||||
def _get_distances():
|
||||
return {i: i for i in [10, 20, 50, 100, 200, 500]}
|
||||
|
||||
|
||||
class AdoptionNoticeSearchForm(forms.Form):
|
||||
postcode = forms.CharField(max_length=20, label=_("Postleitzahl"))
|
||||
max_distance = forms.ChoiceField(choices=_get_distances, label=_("Max. Distanz"))
|
||||
sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False,
|
||||
initial=SexChoicesWithAll.ALL)
|
||||
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED, label=_("Suchradius"))
|
||||
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
|
||||
|
@@ -1,18 +1,18 @@
|
||||
import django.conf.global_settings
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import gettext
|
||||
from django.conf import settings
|
||||
from django.core import mail
|
||||
from django.db.models import Q, Min
|
||||
from fellchensammlung.models import User
|
||||
from fellchensammlung.models import User, CommentNotification, BaseNotification, TrustLevel
|
||||
from notfellchen.settings import host
|
||||
|
||||
NEWLINE = "\r\n"
|
||||
|
||||
|
||||
def mail_admins_new_report(report):
|
||||
subject = _("Neue Meldung")
|
||||
for moderator in User.objects.filter(trust_level__gt=User.TRUST_LEVEL[User.MODERATOR]):
|
||||
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
|
||||
greeting = _("Moin,") + "{NEWLINE}"
|
||||
new_report_text = _("es wurde ein Regelverstoß gemeldet.") + "{NEWLINE}"
|
||||
if len(report.reported_broken_rules.all()) > 0:
|
||||
@@ -29,5 +29,15 @@ def mail_admins_new_report(report):
|
||||
link_text = f"Um alle Details zu sehen, geh bitte auf: {report_url}"
|
||||
body_text = greeting + new_report_text + reported_rules_text + comment_text + link_text
|
||||
message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [moderator.email])
|
||||
print("Sending email to ", moderator.email)
|
||||
message.send()
|
||||
|
||||
|
||||
def send_notification_email(notification_pk):
|
||||
try:
|
||||
notification = CommentNotification.objects.get(pk=notification_pk)
|
||||
except CommentNotification.DoesNotExist:
|
||||
notification = BaseNotification.objects.get(pk=notification_pk)
|
||||
subject = f"🔔 {notification.title}"
|
||||
body_text = notification.text
|
||||
message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [notification.user.email])
|
||||
message.send()
|
||||
|
@@ -1,6 +1,6 @@
|
||||
from django.core.management import BaseCommand
|
||||
from fellchensammlung.models import AdoptionNotice, Location
|
||||
from fellchensammlung.tools.geo import clean_locations
|
||||
from fellchensammlung.tools.admin import clean_locations
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -15,4 +15,4 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
clean_locations(quiet=False)
|
||||
clean_locations(quiet=False)
|
||||
|
@@ -7,7 +7,7 @@ from fellchensammlung import baker_recipes
|
||||
from model_bakery import baker
|
||||
|
||||
from fellchensammlung.models import AdoptionNotice, Species, Animal, Image, ModerationAction, User, Rule, \
|
||||
Report, Comment, ReportAdoptionNotice
|
||||
Report, Comment, ReportAdoptionNotice, TrustLevel
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -101,10 +101,10 @@ class Command(BaseCommand):
|
||||
|
||||
User.objects.create_user('test', password='foobar')
|
||||
admin1 = User.objects.create_superuser(username="admin", password="admin", email="admin1@example.org",
|
||||
trust_level=User.TRUST_LEVEL[User.ADMIN])
|
||||
trust_level=TrustLevel.ADMIN)
|
||||
|
||||
mod1 = User.objects.create_user(username="mod1", password="mod", email="mod1@example.org",
|
||||
trust_level=User.TRUST_LEVEL[User.MODERATOR])
|
||||
trust_level=TrustLevel.MODERATOR)
|
||||
|
||||
comment1 = baker.make(Comment, user=admin1, text="This is a comment", adoption_notice=adoption1)
|
||||
comment2 = baker.make(Comment,
|
||||
|
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.1.1 on 2024-10-29 10:44
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0010_timestamp'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adoptionnotice',
|
||||
name='created_at',
|
||||
field=models.DateField(default=django.utils.timezone.now, verbose_name='Erstellt am'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adoptionnotice',
|
||||
name='last_checked',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Zuletzt überprüft am'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,136 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-03 20:07
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0011_alter_adoptionnotice_created_at_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='adoptionnotice',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='adoptionnoticestatus',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='adoptionnoticestatus',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='animal',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='animal',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='announcement',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='basenotification',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='comment',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='image',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='image',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='location',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='location',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='log',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moderationaction',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='report',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rule',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rule',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subscriptions',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
20
src/fellchensammlung/migrations/0013_alter_log_user.py
Normal file
20
src/fellchensammlung/migrations/0013_alter_log_user.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-06 07:02
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0012_adoptionnotice_updated_at_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='log',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-07 20:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0013_alter_log_user'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-Mail'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-09 09:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0014_rescueorganization_email'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='comment',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Kommentar'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-13 15:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0015_rescueorganization_comment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='phone_number',
|
||||
field=models.CharField(blank=True, max_length=15, null=True, verbose_name='Telefonnummer'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-14 06:42
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0016_rescueorganization_phone_number'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='organization_affiliation',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.rescueorganization', verbose_name='Organisation'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-14 17:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0017_user_organization_affiliation'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Beschreibung'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-14 18:30
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0018_rescueorganization_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='rescueorganization',
|
||||
old_name='comment',
|
||||
new_name='internal_comment',
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-14 18:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0019_rename_comment_rescueorganization_internal_comment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='rescueorganization',
|
||||
name='internal_comment',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Interner Kommentar'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-14 20:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0020_alter_rescueorganization_internal_comment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='reason_for_signup',
|
||||
field=models.TextField(default='-', verbose_name='Grund für die Registrierung'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-20 18:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0021_user_reason_for_signup'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='reason_for_signup',
|
||||
field=models.TextField(help_text="Wir würden gerne wissen warum du dich registriertst, ob du dich z.B. Tiere eines bestimmten Tierheim einstellen willst 'nur mal gucken' willst. Beides ist toll! Wenn du für ein Tierheim/eine Pflegestelle arbeitest kontaktieren wir dich ggf. um dir erweiterte Rechte zu geben.", verbose_name='Grund für die Registrierung'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='trust_level',
|
||||
field=models.IntegerField(choices=[(1, 'Member'), (2, 'Coordinator'), (3, 'Moderator'), (4, 'Admin')], default=1),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-20 19:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0022_alter_user_reason_for_signup_alter_user_trust_level'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='email_notifications',
|
||||
field=models.BooleanField(default=True, verbose_name='Benachrichtigung per E-Mail'),
|
||||
),
|
||||
]
|
18
src/fellchensammlung/migrations/0024_alter_animal_sex.py
Normal file
18
src/fellchensammlung/migrations/0024_alter_animal_sex.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-21 19:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0023_user_email_notifications'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='animal',
|
||||
name='sex',
|
||||
field=models.CharField(choices=[('M_N', 'neutered male'), ('M', 'male'), ('F_N', 'neutered female'), ('F', 'female'), ('I', 'intersex')], max_length=20),
|
||||
),
|
||||
]
|
18
src/fellchensammlung/migrations/0025_alter_animal_sex.py
Normal file
18
src/fellchensammlung/migrations/0025_alter_animal_sex.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-21 19:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0024_alter_animal_sex'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='animal',
|
||||
name='sex',
|
||||
field=models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich Kastriert'), ('I', 'Intersex')], max_length=20),
|
||||
),
|
||||
]
|
18
src/fellchensammlung/migrations/0026_alter_animal_sex.py
Normal file
18
src/fellchensammlung/migrations/0026_alter_animal_sex.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-21 21:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0025_alter_animal_sex'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='animal',
|
||||
name='sex',
|
||||
field=models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich, kastriert'), ('I', 'Intergeschlechtlich')], max_length=20),
|
||||
),
|
||||
]
|
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.1.1 on 2024-12-14 07:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0026_alter_animal_sex'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='animal',
|
||||
name='species',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.species', verbose_name='Tierart'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AndoptionNoticeNotification',
|
||||
fields=[
|
||||
('basenotification_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='fellchensammlung.basenotification')),
|
||||
('adoption_notice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')),
|
||||
],
|
||||
bases=('fellchensammlung.basenotification',),
|
||||
),
|
||||
]
|
25
src/fellchensammlung/migrations/0028_searchsubscription.py
Normal file
25
src/fellchensammlung/migrations/0028_searchsubscription.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.1.1 on 2024-12-26 15:56
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0027_alter_animal_species_andoptionnoticenotification'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SearchSubscription',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('sex', models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich, kastriert'), ('I', 'Intergeschlechtlich')], max_length=20)),
|
||||
('radius', models.IntegerField(choices=[(20, '20 km'), (50, '50 km'), (100, '100 km'), (200, '200 km'), (500, '500 km')])),
|
||||
('location', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.location')),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-31 10:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0028_searchsubscription'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='AndoptionNoticeNotification',
|
||||
new_name='AdoptionNoticeNotification',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='searchsubscription',
|
||||
name='sex',
|
||||
field=models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich Kastriert'), ('I', 'Intergeschlechtlich'), ('A', 'Alle')], max_length=20),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-31 12:23
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0029_rename_andoptionnoticenotification_adoptionnoticenotification_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='searchsubscription',
|
||||
old_name='radius',
|
||||
new_name='max_distance',
|
||||
),
|
||||
]
|
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-31 12:42
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0030_rename_radius_searchsubscription_max_distance'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='searchsubscription',
|
||||
name='location',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.location'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='searchsubscription',
|
||||
name='max_distance',
|
||||
field=models.IntegerField(choices=[(20, '20 km'), (50, '50 km'), (100, '100 km'), (200, '200 km'), (500, '500 km')], null=True),
|
||||
),
|
||||
]
|
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-01 22:04
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0031_alter_searchsubscription_location_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='searchsubscription',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='searchsubscription',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-05 18:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0032_searchsubscription_created_at_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='external_object_identifier',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='External Object Identifier'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='external_source_identifier',
|
||||
field=models.CharField(blank=True, choices=[('OSM', 'Open Street Map')], max_length=200, null=True, verbose_name='External Source Identifier'),
|
||||
),
|
||||
]
|
23
src/fellchensammlung/migrations/0034_speciesspecificurl.py
Normal file
23
src/fellchensammlung/migrations/0034_speciesspecificurl.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-05 19:36
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0033_rescueorganization_external_object_identifier_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SpeciesSpecificURL',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('url', models.URLField(verbose_name='Tierartspezifische URL')),
|
||||
('rescues_organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.rescueorganization', verbose_name='Tierschutzorganisation')),
|
||||
('species', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.species', verbose_name='Tierart')),
|
||||
],
|
||||
),
|
||||
]
|
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-11 12:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0034_speciesspecificurl'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='image',
|
||||
name='alt_text',
|
||||
field=models.TextField(max_length=2000, verbose_name='Alternativtext'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='report',
|
||||
name='reported_broken_rules',
|
||||
field=models.ManyToManyField(to='fellchensammlung.rule', verbose_name='Regeln gegen die verstoßen wurde'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='report',
|
||||
name='user_comment',
|
||||
field=models.TextField(blank=True, verbose_name='Kommentar/Zusätzliche Information'),
|
||||
),
|
||||
]
|
@@ -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'),
|
||||
),
|
||||
]
|
@@ -1,9 +1,10 @@
|
||||
import uuid
|
||||
from random import choices
|
||||
from tabnanny import verbose
|
||||
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from datetime import datetime
|
||||
from django.utils import timezone
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import post_save
|
||||
@@ -12,6 +13,8 @@ from django.contrib.auth.models import AbstractUser
|
||||
|
||||
from .tools import misc, geo
|
||||
from notfellchen.settings import MEDIA_URL
|
||||
from .tools.geo import LocationProxy, Position
|
||||
from .tools.misc import age_as_hr_string, time_since_as_hr_string
|
||||
|
||||
|
||||
class Language(models.Model):
|
||||
@@ -35,102 +38,41 @@ class Language(models.Model):
|
||||
verbose_name_plural = _('Sprachen')
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
"""
|
||||
Model that holds a user's profile, including the django user model
|
||||
|
||||
The trust levels act as permission system and can be displayed as a badge for the user
|
||||
"""
|
||||
|
||||
# Admins can perform all actions and have the highest trust associated with them
|
||||
# Moderators can make moderation decisions regarding the deletion of content
|
||||
# Coordinators can create adoption notices without them being checked
|
||||
# Members can create adoption notices that must be activated
|
||||
ADMIN = "admin"
|
||||
MODERATOR = "Moderator"
|
||||
COORDINATOR = "Koordinator*in"
|
||||
MEMBER = "Mitglied"
|
||||
TRUST_LEVEL = {
|
||||
ADMIN: 4,
|
||||
MODERATOR: 3,
|
||||
COORDINATOR: 2,
|
||||
MEMBER: 1,
|
||||
}
|
||||
|
||||
preferred_language = models.ForeignKey(Language, on_delete=models.PROTECT, null=True, blank=True,
|
||||
verbose_name=_('Bevorzugte Sprache'))
|
||||
trust_level = models.IntegerField(choices=TRUST_LEVEL, default=TRUST_LEVEL[MEMBER])
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Nutzer*in')
|
||||
verbose_name_plural = _('Nutzer*innen')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("user-detail", args=[str(self.pk)])
|
||||
|
||||
def get_notifications_url(self):
|
||||
return self.get_absolute_url()
|
||||
|
||||
def get_num_unread_notifications(self):
|
||||
return BaseNotification.objects.filter(user=self, read=False).count()
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
return self
|
||||
|
||||
|
||||
class Image(models.Model):
|
||||
image = models.ImageField(upload_to='images')
|
||||
alt_text = models.TextField(max_length=2000)
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return self.alt_text
|
||||
|
||||
@property
|
||||
def as_html(self):
|
||||
return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">'
|
||||
|
||||
|
||||
class Species(models.Model):
|
||||
"""Model representing a species of animal."""
|
||||
name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
|
||||
verbose_name=_('Name'))
|
||||
|
||||
def __str__(self):
|
||||
"""String for representing the Model object."""
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Tierart')
|
||||
verbose_name_plural = _('Tierarten')
|
||||
|
||||
|
||||
class Location(models.Model):
|
||||
place_id = models.IntegerField()
|
||||
place_id = models.IntegerField() # OSM id
|
||||
latitude = models.FloatField()
|
||||
longitude = models.FloatField()
|
||||
name = models.CharField(max_length=2000)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
return (self.latitude, self.longitude)
|
||||
|
||||
@property
|
||||
def str_hr(self):
|
||||
return f"{self.name.split(',')[0]}"
|
||||
|
||||
@staticmethod
|
||||
def get_location_from_string(location_string):
|
||||
geo_api = geo.GeoAPI()
|
||||
geojson = geo_api.get_geojson_for_query(location_string)
|
||||
if geojson is None:
|
||||
try:
|
||||
proxy = LocationProxy(location_string)
|
||||
except ValueError:
|
||||
return None
|
||||
result = geojson[0]
|
||||
if "name" in result:
|
||||
name = result["name"]
|
||||
else:
|
||||
name = result["display_name"]
|
||||
location = Location.get_location_from_proxy(proxy)
|
||||
return location
|
||||
|
||||
@staticmethod
|
||||
def get_location_from_proxy(proxy):
|
||||
location = Location.objects.create(
|
||||
place_id=result["place_id"],
|
||||
latitude=result["lat"],
|
||||
longitude=result["lon"],
|
||||
name=name,
|
||||
place_id=proxy.place_id,
|
||||
latitude=proxy.latitude,
|
||||
longitude=proxy.longitude,
|
||||
name=proxy.name,
|
||||
)
|
||||
return location
|
||||
|
||||
@@ -142,6 +84,10 @@ class Location(models.Model):
|
||||
instance.save()
|
||||
|
||||
|
||||
class ExternalSourceChoices(models.TextChoices):
|
||||
OSM = "OSM", _("Open Street Map")
|
||||
|
||||
|
||||
class RescueOrganization(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.name}"
|
||||
@@ -171,7 +117,133 @@ class RescueOrganization(models.Model):
|
||||
instagram = models.URLField(null=True, blank=True, verbose_name=_('Instagram Profil'))
|
||||
facebook = models.URLField(null=True, blank=True, verbose_name=_('Facebook Profil'))
|
||||
fediverse_profile = models.URLField(null=True, blank=True, verbose_name=_('Fediverse Profil'))
|
||||
email = models.EmailField(null=True, blank=True, verbose_name=_('E-Mail'))
|
||||
phone_number = models.CharField(max_length=15, null=True, blank=True, verbose_name=_('Telefonnummer'))
|
||||
website = models.URLField(null=True, blank=True, verbose_name=_('Website'))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
internal_comment = models.TextField(verbose_name=_("Interner Kommentar"), null=True, blank=True, )
|
||||
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung')) # Markdown allowed
|
||||
external_object_identifier = models.CharField(max_length=200, null=True, blank=True,
|
||||
verbose_name=_('External Object Identifier'))
|
||||
external_source_identifier = models.CharField(max_length=200, null=True, blank=True,
|
||||
choices=ExternalSourceChoices.choices,
|
||||
verbose_name=_('External Source Identifier'))
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("rescue-organization-detail", args=[str(self.pk)])
|
||||
|
||||
@property
|
||||
def adoption_notices(self):
|
||||
return AdoptionNotice.objects.filter(organization=self)
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
if self.location:
|
||||
return Position(latitude=self.location.latitude, longitude=self.location.longitude)
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def description_short(self):
|
||||
if self.description is None:
|
||||
return ""
|
||||
if len(self.description) > 200:
|
||||
return self.description[:200] + f" ... [weiterlesen]({self.get_absolute_url()})"
|
||||
|
||||
|
||||
# Admins can perform all actions and have the highest trust associated with them
|
||||
# Moderators can make moderation decisions regarding the deletion of content
|
||||
# Coordinators can create adoption notices without them being checked
|
||||
# Members can create adoption notices that must be activated
|
||||
class TrustLevel(models.IntegerChoices):
|
||||
MEMBER = 1, 'Member'
|
||||
COORDINATOR = 2, 'Coordinator'
|
||||
MODERATOR = 3, 'Moderator'
|
||||
ADMIN = 4, 'Admin'
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
"""
|
||||
Model that holds a user's profile, including the django user model
|
||||
|
||||
The trust levels act as permission system and can be displayed as a badge for the user
|
||||
"""
|
||||
|
||||
trust_level = models.IntegerField(
|
||||
choices=TrustLevel.choices,
|
||||
default=TrustLevel.MEMBER, # Default to the lowest trust level
|
||||
)
|
||||
preferred_language = models.ForeignKey(Language, on_delete=models.PROTECT, null=True, blank=True,
|
||||
verbose_name=_('Bevorzugte Sprache'))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
organization_affiliation = models.ForeignKey(RescueOrganization, on_delete=models.PROTECT, null=True, blank=True,
|
||||
verbose_name=_('Organisation'))
|
||||
reason_for_signup = models.TextField(verbose_name=_("Grund für die Registrierung"), help_text=_(
|
||||
"Wir würden gerne wissen warum du dich registriertst, ob du dich z.B. Tiere eines bestimmten Tierheim einstellen willst 'nur mal gucken' willst. Beides ist toll! Wenn du für ein Tierheim/eine Pflegestelle arbeitest kontaktieren wir dich ggf. um dir erweiterte Rechte zu geben."))
|
||||
email_notifications = models.BooleanField(verbose_name=_("Benachrichtigung per E-Mail"), default=True)
|
||||
REQUIRED_FIELDS = ["reason_for_signup", "email"]
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Nutzer*in')
|
||||
verbose_name_plural = _('Nutzer*innen')
|
||||
|
||||
def get_full_name(self):
|
||||
if self.first_name and self.last_name:
|
||||
return self.first_name + self.last_name
|
||||
else:
|
||||
return self.username
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("user-detail", args=[str(self.pk)])
|
||||
|
||||
def get_notifications_url(self):
|
||||
return self.get_absolute_url()
|
||||
|
||||
def get_unread_notifications(self):
|
||||
return BaseNotification.objects.filter(user=self, read=False)
|
||||
|
||||
def get_num_unread_notifications(self):
|
||||
return BaseNotification.objects.filter(user=self, read=False).count()
|
||||
|
||||
@property
|
||||
def adoption_notices(self):
|
||||
return AdoptionNotice.objects.filter(owner=self)
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
return self
|
||||
|
||||
|
||||
class Image(models.Model):
|
||||
image = models.ImageField(upload_to='images')
|
||||
alt_text = models.TextField(max_length=2000, verbose_name=_('Alternativtext'))
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.alt_text
|
||||
|
||||
@property
|
||||
def as_html(self):
|
||||
return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">'
|
||||
|
||||
|
||||
class Species(models.Model):
|
||||
"""Model representing a species of animal."""
|
||||
name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
|
||||
verbose_name=_('Name'))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
"""String for representing the Model object."""
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Tierart')
|
||||
verbose_name_plural = _('Tierarten')
|
||||
|
||||
|
||||
class AdoptionNotice(models.Model):
|
||||
@@ -181,10 +253,13 @@ class AdoptionNotice(models.Model):
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}"
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
return self.name
|
||||
return f"[{self.adoptionnoticestatus.as_string()}] {self.name}"
|
||||
|
||||
created_at = models.DateField(verbose_name=_('Erstellt am'), default=datetime.now)
|
||||
last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=datetime.now)
|
||||
created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=timezone.now)
|
||||
searching_since = models.DateField(verbose_name=_('Sucht nach einem Zuhause seit'))
|
||||
name = models.CharField(max_length=200)
|
||||
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
|
||||
@@ -201,6 +276,31 @@ class AdoptionNotice(models.Model):
|
||||
def animals(self):
|
||||
return Animal.objects.filter(adoption_notice=self)
|
||||
|
||||
@property
|
||||
def sexes(self):
|
||||
sexes = set()
|
||||
for animal in self.animals:
|
||||
sexes.add(animal.sex)
|
||||
return sexes
|
||||
|
||||
@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)
|
||||
|
||||
def sex_code(self):
|
||||
# Treat Intersex as mixed in order to increase their visibility
|
||||
if len(self.sexes) > 1:
|
||||
return "mixed"
|
||||
|
||||
sex = self.sexes.pop()
|
||||
if sex == SexChoices.MALE:
|
||||
return "male"
|
||||
elif sex == SexChoices.FEMALE:
|
||||
return "female"
|
||||
else:
|
||||
return "mixed"
|
||||
|
||||
@property
|
||||
def comments(self):
|
||||
return Comment.objects.filter(adoption_notice=self)
|
||||
@@ -231,6 +331,11 @@ class AdoptionNotice(models.Model):
|
||||
# returns all subscriptions to that adoption notice
|
||||
return Subscriptions.objects.filter(adoption_notice=self)
|
||||
|
||||
@staticmethod
|
||||
def get_active_ANs():
|
||||
active_ans = [an for an in AdoptionNotice.objects.all() if an.is_active]
|
||||
return active_ans
|
||||
|
||||
def get_photos(self):
|
||||
"""
|
||||
First trys to get group photos that are attached to the adoption notice if there is none it trys to fetch
|
||||
@@ -288,19 +393,35 @@ class AdoptionNotice(models.Model):
|
||||
return self.adoptionnoticestatus.is_active
|
||||
|
||||
@property
|
||||
def is_to_be_checked(self, include_active=False):
|
||||
def is_disabled_unchecked(self):
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
return False
|
||||
return self.adoptionnoticestatus.is_to_be_checked or (include_active and self.adoptionnoticestatus.is_active)
|
||||
|
||||
def set_checked(self):
|
||||
self.last_checked = datetime.now()
|
||||
self.save()
|
||||
return self.adoptionnoticestatus.is_disabled_unchecked
|
||||
|
||||
def set_closed(self):
|
||||
self.last_checked = datetime.now()
|
||||
self.last_checked = timezone.now()
|
||||
self.save()
|
||||
self.adoptionnoticestatus.set_closed()
|
||||
|
||||
def set_active(self):
|
||||
self.last_checked = timezone.now()
|
||||
self.save()
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
AdoptionNoticeStatus.create_other(self)
|
||||
self.adoptionnoticestatus.set_active()
|
||||
|
||||
def set_unchecked(self):
|
||||
self.last_checked = timezone.now()
|
||||
self.save()
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
AdoptionNoticeStatus.create_other(self)
|
||||
self.adoptionnoticestatus.set_unchecked()
|
||||
|
||||
for subscription in self.get_subscriptions():
|
||||
notification_title = _("Vermittlung deaktiviert:") + f" {self.name}"
|
||||
text = _("Die folgende Vermittlung wurde deaktiviert: ") + f"[{self.name}]({self.get_absolute_url()})"
|
||||
BaseNotification.objects.create(user=subscription.owner, text=text, title=notification_title)
|
||||
|
||||
|
||||
class AdoptionNoticeStatus(models.Model):
|
||||
"""
|
||||
@@ -351,60 +472,90 @@ class AdoptionNoticeStatus(models.Model):
|
||||
minor_choices.update(MINOR_STATUS_CHOICES[key])
|
||||
minor_status = models.CharField(choices=minor_choices, max_length=200)
|
||||
adoption_notice = models.OneToOneField(AdoptionNotice, on_delete=models.CASCADE)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.adoption_notice}: {self.major_status}, {self.minor_status}"
|
||||
|
||||
def as_string(self):
|
||||
return f"{self.major_status}, {self.minor_status}"
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return self.major_status == self.ACTIVE
|
||||
|
||||
@property
|
||||
def is_to_be_checked(self):
|
||||
def is_disabled_unchecked(self):
|
||||
return self.major_status == self.DISABLED and self.minor_status == "unchecked"
|
||||
|
||||
@staticmethod
|
||||
def get_minor_choices(major_status):
|
||||
return AdoptionNoticeStatus.MINOR_STATUS_CHOICES[major_status]
|
||||
|
||||
@staticmethod
|
||||
def create_other(an_instance):
|
||||
# Used as empty status to be changed immediately
|
||||
major_status = AdoptionNoticeStatus.DISABLED
|
||||
minor_status = AdoptionNoticeStatus.MINOR_STATUS_CHOICES[AdoptionNoticeStatus.DISABLED]["other"]
|
||||
AdoptionNoticeStatus.objects.create(major_status=major_status,
|
||||
minor_status=minor_status,
|
||||
adoption_notice=an_instance)
|
||||
|
||||
def set_closed(self):
|
||||
self.major_status = self.MAJOR_STATUS_CHOICES[self.CLOSED]
|
||||
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"]
|
||||
self.save()
|
||||
|
||||
def deactivate_unchecked(self):
|
||||
def set_unchecked(self):
|
||||
self.major_status = self.MAJOR_STATUS_CHOICES[self.DISABLED]
|
||||
self.minor_status = self.MINOR_STATUS_CHOICES[self.DISABLED]["unchecked"]
|
||||
self.save()
|
||||
|
||||
def set_active(self):
|
||||
self.major_status = self.MAJOR_STATUS_CHOICES[self.ACTIVE]
|
||||
self.minor_status = self.MINOR_STATUS_CHOICES[self.ACTIVE]["searching"]
|
||||
self.save()
|
||||
|
||||
|
||||
class SexChoices(models.TextChoices):
|
||||
FEMALE = "F", _("Weiblich")
|
||||
MALE = "M", _("Männlich")
|
||||
MALE_NEUTERED = "M_N", _("Männlich, kastriert")
|
||||
FEMALE_NEUTERED = "F_N", _("Weiblich, kastriert")
|
||||
INTER = "I", _("Intergeschlechtlich")
|
||||
|
||||
|
||||
class SexChoicesWithAll(models.TextChoices):
|
||||
FEMALE = "F", _("Weiblich")
|
||||
MALE = "M", _("Männlich")
|
||||
MALE_NEUTERED = "M_N", _("Männlich, kastriert")
|
||||
FEMALE_NEUTERED = "F_N", _("Weiblich Kastriert")
|
||||
INTER = "I", _("Intergeschlechtlich")
|
||||
ALL = "A", _("Alle")
|
||||
|
||||
|
||||
class Animal(models.Model):
|
||||
MALE_NEUTERED = "M_N"
|
||||
MALE = "M"
|
||||
FEMALE_NEUTERED = "F_N"
|
||||
FEMALE = "F"
|
||||
SEX_CHOICES = {
|
||||
MALE_NEUTERED: "neutered male",
|
||||
MALE: "male",
|
||||
FEMALE_NEUTERED: "neutered female",
|
||||
FEMALE: "female",
|
||||
}
|
||||
|
||||
date_of_birth = models.DateField(verbose_name=_('Geburtsdatum'))
|
||||
name = models.CharField(max_length=200)
|
||||
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
|
||||
species = models.ForeignKey(Species, on_delete=models.PROTECT)
|
||||
species = models.ForeignKey(Species, on_delete=models.PROTECT, verbose_name=_("Tierart"))
|
||||
photos = models.ManyToManyField(Image, blank=True)
|
||||
sex = models.CharField(max_length=20, choices=SEX_CHOICES, )
|
||||
sex = models.CharField(
|
||||
max_length=20,
|
||||
choices=SexChoices.choices,
|
||||
)
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE)
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}"
|
||||
|
||||
@property
|
||||
def age(self):
|
||||
return datetime.today().date() - self.date_of_birth
|
||||
return timezone.now().today().date() - self.date_of_birth
|
||||
|
||||
@property
|
||||
def hr_age(self):
|
||||
@@ -430,6 +581,40 @@ class Animal(models.Model):
|
||||
return reverse('animal-detail', args=[str(self.id)])
|
||||
|
||||
|
||||
class DistanceChoices(models.IntegerChoices):
|
||||
TWENTY = 20, '20 km'
|
||||
FIFTY = 50, '50 km'
|
||||
ONE_HUNDRED = 100, '100 km'
|
||||
TWO_HUNDRED = 200, '200 km'
|
||||
FIVE_HUNDRED = 500, '500 km'
|
||||
|
||||
|
||||
class SearchSubscription(models.Model):
|
||||
"""
|
||||
SearchSubscriptions allow a user to get a notification when a new AdoptionNotice is added that matches their Search
|
||||
criteria. Search criteria are location, SexChoicesWithAll and distance
|
||||
|
||||
Process:
|
||||
- User performs a normal search
|
||||
- User clicks Button "Subscribe to this Search"
|
||||
- SearchSubscription is added to database
|
||||
- On new AdoptionNotice: Check all existing SearchSubscriptions for matches
|
||||
- For matches: Send notification to user of the SearchSubscription
|
||||
"""
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
location = models.ForeignKey(Location, on_delete=models.PROTECT, null=True)
|
||||
sex = models.CharField(max_length=20, choices=SexChoicesWithAll.choices)
|
||||
max_distance = models.IntegerField(choices=DistanceChoices.choices, null=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
if self.location and self.max_distance:
|
||||
return f"{self.owner}: [{SexChoicesWithAll(self.sex).label}] {self.max_distance}km - {self.location}"
|
||||
else:
|
||||
return f"{self.owner}: [{SexChoicesWithAll(self.sex).label}]"
|
||||
|
||||
|
||||
class Rule(models.Model):
|
||||
"""
|
||||
Class to store rules
|
||||
@@ -441,6 +626,8 @@ class Rule(models.Model):
|
||||
language = models.ForeignKey(Language, on_delete=models.PROTECT)
|
||||
# Rule identifier allows to translate rules with the same identifier
|
||||
rule_identifier = models.CharField(max_length=24)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
@@ -461,9 +648,10 @@ class Report(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, help_text=_('ID dieses reports'),
|
||||
verbose_name=_('ID'))
|
||||
status = models.CharField(max_length=30, choices=STATES)
|
||||
reported_broken_rules = models.ManyToManyField(Rule)
|
||||
user_comment = models.TextField(blank=True)
|
||||
reported_broken_rules = models.ManyToManyField(Rule, verbose_name=_("Regeln gegen die verstoßen wurde"))
|
||||
user_comment = models.TextField(blank=True, verbose_name=_("Kommentar/Zusätzliche Information"))
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.status}]: {self.user_comment:.20}"
|
||||
@@ -510,20 +698,22 @@ class ModerationAction(models.Model):
|
||||
}
|
||||
action = models.CharField(max_length=30, choices=ACTIONS.items())
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
public_comment = models.TextField(blank=True)
|
||||
# Only visible to moderator
|
||||
private_comment = models.TextField(blank=True)
|
||||
report = models.ForeignKey(Report, on_delete=models.CASCADE)
|
||||
|
||||
# TODO: Needs field for moderator that performed the action
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.action}]: {self.public_comment}"
|
||||
|
||||
|
||||
"""
|
||||
Membership
|
||||
"""
|
||||
class TextTypeChoices(models.TextChoices):
|
||||
DEDICATED = "dedicated", _("Fest zugeordnet")
|
||||
MALE = "M", _("Männlich")
|
||||
MALE_NEUTERED = "M_N", _("Männlich, kastriert")
|
||||
FEMALE_NEUTERED = "F_N", _("Weiblich, kastriert")
|
||||
INTER = "I", _("Intergeschlechtlich")
|
||||
|
||||
|
||||
class Text(models.Model):
|
||||
@@ -560,6 +750,7 @@ class Announcement(Text):
|
||||
"""
|
||||
logged_in_only = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
publish_start_time = models.DateTimeField(verbose_name="Veröffentlichungszeitpunkt")
|
||||
publish_end_time = models.DateTimeField(verbose_name="Veröffentlichungsende")
|
||||
IMPORTANT = "important"
|
||||
@@ -608,6 +799,7 @@ class Comment(models.Model):
|
||||
"""
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
|
||||
text = models.TextField(verbose_name="Inhalt")
|
||||
reply_to = models.ForeignKey("self", verbose_name="Antwort auf", blank=True, null=True, on_delete=models.CASCADE)
|
||||
@@ -625,7 +817,9 @@ class Comment(models.Model):
|
||||
|
||||
class BaseNotification(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
title = models.CharField(max_length=100)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
read_at = models.DateTimeField(blank=True, null=True, verbose_name=_("Gelesen am"))
|
||||
title = models.CharField(max_length=100, verbose_name=_("Titel"))
|
||||
text = models.TextField(verbose_name="Inhalt")
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||
read = models.BooleanField(default=False)
|
||||
@@ -636,20 +830,33 @@ class BaseNotification(models.Model):
|
||||
def get_absolute_url(self):
|
||||
self.user.get_notifications_url()
|
||||
|
||||
def mark_read(self):
|
||||
self.read = True
|
||||
self.read_at = timezone.now()
|
||||
self.save()
|
||||
|
||||
|
||||
class CommentNotification(BaseNotification):
|
||||
comment = models.ForeignKey(Comment, on_delete=models.CASCADE, verbose_name=_('Antwort'))
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
print(f"URL: self.comment.get_absolute_url()")
|
||||
return self.comment.get_absolute_url
|
||||
|
||||
|
||||
class AdoptionNoticeNotification(BaseNotification):
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self.adoption_notice.get_absolute_url
|
||||
|
||||
|
||||
class Subscriptions(models.Model):
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.owner} - {self.adoption_notice}"
|
||||
@@ -659,10 +866,11 @@ class Log(models.Model):
|
||||
"""
|
||||
Basic class that allows logging random entries for later inspection
|
||||
"""
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_("Nutzer*in"))
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_("Nutzer*in"), blank=True, null=True)
|
||||
action = models.CharField(max_length=255, verbose_name=_("Aktion"))
|
||||
text = models.CharField(max_length=1000, verbose_name=_("Log text"))
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.action}] - {self.user} - {self.created_at.strftime('%H:%M:%S %d-%m-%Y ')}"
|
||||
@@ -678,3 +886,13 @@ class Timestamp(models.Model):
|
||||
|
||||
def ___str__(self):
|
||||
return f"[{self.key}] - {self.timestamp.strftime('%H:%M:%S %d-%m-%Y ')} - {self.data}"
|
||||
|
||||
|
||||
class SpeciesSpecificURL(models.Model):
|
||||
"""
|
||||
Model that allows to specify a URL for a rescue organization where a certain species can be found
|
||||
"""
|
||||
species = models.ForeignKey(Species, on_delete=models.CASCADE, verbose_name=_("Tierart"))
|
||||
rescues_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE,
|
||||
verbose_name=_("Tierschutzorganisation"))
|
||||
url = models.URLField(verbose_name=_("Tierartspezifische URL"))
|
||||
|
37
src/fellchensammlung/receivers.py
Normal file
37
src/fellchensammlung/receivers.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from fellchensammlung.models import BaseNotification, CommentNotification, User, TrustLevel
|
||||
from .tasks import task_send_notification_email
|
||||
from notfellchen.settings import host
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
@receiver(post_save, sender=CommentNotification)
|
||||
def comment_notification_receiver(sender, instance: BaseNotification, created: bool, **kwargs):
|
||||
base_notification_receiver(sender, instance, created, **kwargs)
|
||||
|
||||
|
||||
@receiver(post_save, sender=BaseNotification)
|
||||
def base_notification_receiver(sender, instance: BaseNotification, created: bool, **kwargs):
|
||||
if not created or not instance.user.email_notifications:
|
||||
return
|
||||
else:
|
||||
task_send_notification_email.delay(instance.pk)
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def notification_new_user(sender, instance: User, created: bool, **kwargs):
|
||||
NEWLINE = "\r\n"
|
||||
if not created:
|
||||
return
|
||||
# Create Notification text
|
||||
subject = _("Neuer User") + f": {instance.username}"
|
||||
new_user_text = _("Es hat sich eine neue Person registriert.") + f"{NEWLINE}"
|
||||
user_detail_text = _("Username") + f": {instance.username}{NEWLINE}" + _(
|
||||
"E-Mail") + f": {instance.email}{NEWLINE}"
|
||||
user_url = "https://" + host + instance.get_absolute_url()
|
||||
link_text = f"Um alle Details zu sehen, geh bitte auf: {user_url}"
|
||||
body_text = new_user_text + user_detail_text + link_text
|
||||
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
|
||||
notification = BaseNotification.objects.create(title=subject, text=body_text, user=moderator)
|
||||
notification.save()
|
@@ -22,6 +22,7 @@
|
||||
--text-three: var(--primary-light-one);
|
||||
--shadow-three: var(--primary-dark-one);
|
||||
}
|
||||
|
||||
/**************************/
|
||||
/* TAG SETTINGS (GENERAL) */
|
||||
/**************************/
|
||||
@@ -36,13 +37,49 @@ body {
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
margin-bottom: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
@media screen and (max-width: 600px) {
|
||||
.responsive thead {
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.responsive tr {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.responsive td {
|
||||
border: 1px solid;
|
||||
border-bottom: none;
|
||||
display: block;
|
||||
font-size: .8em;
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.responsive td::before {
|
||||
content: attr(data-label);
|
||||
float: left;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.responsive td:last-child {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
@@ -64,7 +101,7 @@ td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
th {
|
||||
thead td {
|
||||
border: 3px solid black;
|
||||
border-collapse: collapse;
|
||||
padding: 8px;
|
||||
@@ -95,13 +132,19 @@ textarea {
|
||||
.container-cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container-cards h1,
|
||||
.container-cards h2 {
|
||||
width: 100%; /* Make sure heading fills complete line */
|
||||
}
|
||||
|
||||
.card {
|
||||
flex: 1 25%;
|
||||
margin: 10px;
|
||||
border-radius: 8px;
|
||||
padding: 5px;
|
||||
padding: 8px;
|
||||
background: var(--background-three);
|
||||
color: var(--text-two);
|
||||
}
|
||||
@@ -115,6 +158,10 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
.spaced > * {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
/*******************************/
|
||||
/* PARTIAL SPECIFIC CONTAINERS */
|
||||
/*******************************/
|
||||
@@ -135,9 +182,8 @@ textarea {
|
||||
|
||||
.profile-card {
|
||||
display: flex;
|
||||
border-radius: 0px 0px 8px 8px;
|
||||
background-color: var(--highlight-two);
|
||||
color: var(--highlight-one-text);
|
||||
align-items: center;
|
||||
|
||||
.btn2 {
|
||||
height: 40px;
|
||||
@@ -163,6 +209,16 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
/*************/
|
||||
/* Modifiers */
|
||||
/*************/
|
||||
|
||||
/* Used to enlargen cards */
|
||||
.full-width {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/***********/
|
||||
/* BUTTONS */
|
||||
/***********/
|
||||
@@ -183,9 +239,14 @@ select, .button {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.btn2 {
|
||||
a.btn, a.btn2, a.nav-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn2, .btn3 {
|
||||
background-color: var(--secondary-light-one);
|
||||
color: var(--primary-dark-one);
|
||||
padding: 8px;
|
||||
@@ -194,6 +255,164 @@ select, .button {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.btn3 {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
font-size: medium;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
left: 0.2rem;
|
||||
bottom: 0.075rem;
|
||||
background-color: var(--primary-light-one);
|
||||
color: var(--secondary-light-one);
|
||||
border-radius: 0.5rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.switch {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
display: inline-block;
|
||||
background: #ccc;
|
||||
border-radius: 16px;
|
||||
width: 58px;
|
||||
height: 32px;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
transition: background 0.25s;
|
||||
}
|
||||
|
||||
.toggle-switch:before, .toggle-switch:after {
|
||||
content: "";
|
||||
}
|
||||
|
||||
.toggle-switch:before {
|
||||
display: block;
|
||||
background: linear-gradient(to bottom, #fff 0%, #eee 100%);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
transition: left 0.25s;
|
||||
}
|
||||
|
||||
.toggle:hover .toggle-switch:before {
|
||||
background: linear-gradient(to bottom, #fff 0%, #fff 100%);
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.checked + .toggle-switch {
|
||||
background: #56c080;
|
||||
}
|
||||
|
||||
.checked + .toggle-switch:before {
|
||||
left: 30px;
|
||||
}
|
||||
|
||||
.toggle-checkbox {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.slider-label {
|
||||
margin-left: 5px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
/* Refactor tooltip based on https://luigicavalieri.com/blog/css-tooltip-appearing-from-any-direction/ to allow different directions */
|
||||
.tooltip {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip:hover .tooltiptext {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.tooltip .tooltiptext {
|
||||
border-radius: 4px;
|
||||
bottom: calc(100% + 0.6em + 2px);
|
||||
box-shadow: 0px 2px 4px #07172258;
|
||||
background-color: var(--primary-dark-one);
|
||||
color: var(--secondary-light-one);
|
||||
font-size: 0.68rem;
|
||||
justify-content: center;
|
||||
line-height: 1.35em;
|
||||
padding: 0.5em 0.7em;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: 7rem;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease-in;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.tooltip .tooltiptext::before {
|
||||
border-width: 0.6em 0.8em 0;
|
||||
border-color: transparent;
|
||||
border-top-color: var(--primary-dark-one);
|
||||
content: "";
|
||||
display: block;
|
||||
border-style: solid;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
/* Makes the tooltip fly from above */
|
||||
.tooltip.top .tooltiptext {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tooltip.top:hover .tooltiptext {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Make adjustments for bottom */
|
||||
.tooltip.bottom .tooltiptext {
|
||||
top: calc(100% + 0.6em + 2px);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.tooltip.bottom:hover .tooltiptext {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.tooltip.bottom .tooltiptext::before {
|
||||
transform: rotate(180deg);
|
||||
/* 100% of the height of .tooltip */
|
||||
bottom: 100%;
|
||||
}
|
||||
|
||||
.tooltip:not(.top) .tooltiptext {
|
||||
bottom: auto;
|
||||
|
||||
}
|
||||
|
||||
.tooltip:not(.top) .tooltiptext::before {
|
||||
top: auto;
|
||||
}
|
||||
|
||||
|
||||
/*********************/
|
||||
/* UNIQUE COMPONENTS */
|
||||
/*********************/
|
||||
@@ -207,63 +426,143 @@ select, .button {
|
||||
background-color: var(--background-two);
|
||||
border-bottom-left-radius: 15px;
|
||||
border-bottom-right-radius: 15px;
|
||||
}
|
||||
|
||||
|
||||
.nav-link {
|
||||
color: var(--text-one);
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header a, .header form {
|
||||
float: left;
|
||||
padding: 5px 12px 5px 12px;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.header a:hover {
|
||||
background-color: var(--highlight-one);
|
||||
color: var(--highlight-one-text);
|
||||
}
|
||||
|
||||
.header a.active {
|
||||
background-color: dodgerblue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.header-right select.option {
|
||||
color: #000;
|
||||
background-color: var(--highlight-one);
|
||||
border: 1px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
float: right;
|
||||
display: flex;
|
||||
border-radius: 0px 0px 15px 15px;
|
||||
background-color: var(--highlight-two);
|
||||
color: var(--highlight-one-text);
|
||||
padding: 5px 5px 0px 5px;
|
||||
height: 67px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
/* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
|
||||
color: #FFF;
|
||||
height: 50px;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
.header a {
|
||||
float: none;
|
||||
display: block;
|
||||
text-align: left;
|
||||
#main-menu {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.menu > li {
|
||||
margin: 0 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-button-container {
|
||||
display: none;
|
||||
height: 100%;
|
||||
width: 30px;
|
||||
cursor: pointer;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #4ab457;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#menu-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu-button,
|
||||
.menu-button::before,
|
||||
.menu-button::after {
|
||||
display: block;
|
||||
background-color: #fff;
|
||||
position: absolute;
|
||||
height: 4px;
|
||||
width: 30px;
|
||||
transition: transform 400ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.menu-button::before {
|
||||
content: '';
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
.menu-button::after {
|
||||
content: '';
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
#menu-toggle:checked + .menu-button-container .menu-button::before {
|
||||
margin-top: 0px;
|
||||
transform: rotate(405deg);
|
||||
}
|
||||
|
||||
#menu-toggle:checked + .menu-button-container .menu-button {
|
||||
background: rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
#menu-toggle:checked + .menu-button-container .menu-button::after {
|
||||
margin-top: 0px;
|
||||
transform: rotate(-405deg);
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.menu-button-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
float: none;
|
||||
.menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
margin-top: 50px;
|
||||
left: 0;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#menu-toggle ~ nav .menu li {
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
transition: height 400ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
#menu-toggle:checked ~ nav .menu li {
|
||||
height: 3em;
|
||||
padding: 1em;
|
||||
transition: height 400ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
.header {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.menu > li {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0.5em 0;
|
||||
width: 100%;
|
||||
color: white;
|
||||
background-color: var(--background-two);
|
||||
}
|
||||
|
||||
.menu > li:not(:last-child) {
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
#header-sign-out, #header-change-language {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.logo img {
|
||||
height: 40px;
|
||||
}
|
||||
@@ -412,9 +711,6 @@ select, .button {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.photos {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -449,12 +745,22 @@ select, .button {
|
||||
|
||||
.header-card-adoption-notice {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
}
|
||||
|
||||
.search-subscription-header {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
flex-wrap: wrap;
|
||||
|
||||
h3 {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.table-adoption-notice-info {
|
||||
margin-top: 10px;
|
||||
}
|
||||
@@ -480,7 +786,6 @@ select, .button {
|
||||
.btn-notification {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Make the badge float in the top right corner of the button */
|
||||
@@ -500,15 +805,30 @@ select, .button {
|
||||
.adoption-card-report-link, .notification-card-mark-read {
|
||||
margin-left: auto;
|
||||
font-size: 2rem;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.adoption-card-report-link {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
|
||||
.notification-card-mark-read {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.heading-card-adoption-notice {
|
||||
.inline-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.inline-container > * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
h2.heading-card-adoption-notice {
|
||||
font-size: 2rem;
|
||||
line-height: 2rem;
|
||||
word-wrap: anywhere;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.tags {
|
||||
@@ -523,17 +843,15 @@ select, .button {
|
||||
}
|
||||
|
||||
.detail-adoption-notice-header h1 {
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.detail-adoption-notice-header a {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
.detail-adoption-notice-header h1 {
|
||||
.detail-adoption-notice-header .inline-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -551,7 +869,7 @@ select, .button {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.comment, .notification {
|
||||
.comment, .notification, .search-subscription {
|
||||
flex: 1 100%;
|
||||
margin: 10px;
|
||||
border-radius: 8px;
|
||||
@@ -561,14 +879,22 @@ select, .button {
|
||||
}
|
||||
|
||||
|
||||
|
||||
.form-comments {
|
||||
.btn {
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.announcement {
|
||||
.announcement-header {
|
||||
font-size: 1.2rem;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
color: var(--text-two);
|
||||
text-shadow: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.announcement {
|
||||
flex: 1 100%;
|
||||
margin: 10px;
|
||||
border-radius: 8px;
|
||||
@@ -576,13 +902,6 @@ select, .button {
|
||||
background: var(--background-three);
|
||||
color: var(--text-two);
|
||||
|
||||
h1 {
|
||||
font-size: 1.2rem;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
color: var(--text-two);
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -596,6 +915,43 @@ select, .button {
|
||||
|
||||
}
|
||||
|
||||
.half {
|
||||
width: 49%;
|
||||
}
|
||||
|
||||
|
||||
#results {
|
||||
margin-top: 10px;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
padding: 8px;
|
||||
margin: 4px 0;
|
||||
background-color: #ddd1a5;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.result-item:hover {
|
||||
background-color: #ede1b5;
|
||||
}
|
||||
|
||||
.label {
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.active-adoption {
|
||||
background-color: #4a9455;
|
||||
}
|
||||
|
||||
.inactive-adoption {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
/************************/
|
||||
/* GENERAL HIGHLIGHTING */
|
||||
/************************/
|
||||
@@ -612,6 +968,14 @@ select, .button {
|
||||
border: rgba(17, 58, 224, 0.51) 4px solid;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #370707;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error::before {
|
||||
content: "⚠️";
|
||||
}
|
||||
|
||||
/*******/
|
||||
/* MAP */
|
||||
@@ -625,6 +989,11 @@ select, .button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.animal-shelter-marker {
|
||||
background-image: url('../img/animal_shelter.png');
|
||||
!important;
|
||||
}
|
||||
|
||||
.maplibregl-popup {
|
||||
max-width: 600px !important;
|
||||
}
|
||||
|
BIN
src/fellchensammlung/static/fellchensammlung/img/pin.png
Normal file
BIN
src/fellchensammlung/static/fellchensammlung/img/pin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
21
src/fellchensammlung/static/fellchensammlung/js/custom.js
Normal file
21
src/fellchensammlung/static/fellchensammlung/js/custom.js
Normal file
@@ -0,0 +1,21 @@
|
||||
function ifdef(variable, prefix = "", suffix = "") {
|
||||
if (variable !== undefined) {
|
||||
return prefix + variable + suffix;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function geojson_to_summary(location) {
|
||||
if (ifdef(location.properties.name) !== "") {
|
||||
return location.properties.name + ifdef(location.properties.city, " (", ")");
|
||||
} else {
|
||||
return ifdef(location.properties.street, "", ifdef(location.properties.housenumber, " ","")) + ifdef(location.properties.city, ", ", "") + ifdef(location.properties.countrycode, ", ", "")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function geojson_to_searchable_string(location) {
|
||||
return ifdef(location.properties.name, "", ", ") + ifdef(location.properties.street, "", ifdef(location.properties.housenumber, " ",", ")) + ifdef(location.properties.city, "", ", ") + ifdef(location.properties.country, "", "")
|
||||
}
|
||||
|
37
src/fellchensammlung/static/fellchensammlung/js/turf.min.js
vendored
Normal file
37
src/fellchensammlung/static/fellchensammlung/js/turf.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,16 +1,23 @@
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
from celery.app import shared_task
|
||||
from django.utils import timezone
|
||||
from notfellchen.celery import app as celery_app
|
||||
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices
|
||||
from .mail import send_notification_email
|
||||
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices
|
||||
from .tools.misc import healthcheck_ok
|
||||
from .models import Location, AdoptionNotice, Timestamp
|
||||
from .tools.notifications import notify_of_AN_to_be_checked
|
||||
from .tools.search import notify_search_subscribers
|
||||
|
||||
|
||||
def set_timestamp(key: str):
|
||||
try:
|
||||
ts = Timestamp.objects.get(key=key)
|
||||
ts.timestamp = datetime.now()
|
||||
ts.timestamp = timezone.now()
|
||||
ts.save()
|
||||
except Timestamp.DoesNotExist:
|
||||
Timestamp.objects.create(key=key, timestamp=datetime.now())
|
||||
Timestamp.objects.create(key=key, timestamp=timezone.now())
|
||||
|
||||
|
||||
@celery_app.task(name="admin.clean_locations")
|
||||
@@ -19,20 +26,34 @@ def task_clean_locations():
|
||||
set_timestamp("task_clean_locations")
|
||||
|
||||
|
||||
@celery_app.task(name="admin.deactivate_unchecked")
|
||||
@celery_app.task(name="admin.daily_unchecked_deactivation")
|
||||
def task_deactivate_unchecked():
|
||||
deactivate_unchecked_adoption_notices()
|
||||
set_timestamp("task_deactivate_unchecked")
|
||||
set_timestamp("task_daily_unchecked_deactivation")
|
||||
|
||||
|
||||
@celery_app.task(name="commit.add_location")
|
||||
def add_adoption_notice_location(pk):
|
||||
@celery_app.task(name="admin.deactivate_404_adoption_notices")
|
||||
def task_deactivate_unchecked():
|
||||
deactivate_404_adoption_notices()
|
||||
set_timestamp("task_deactivate_404_adoption_notices")
|
||||
|
||||
|
||||
@celery_app.task(name="commit.post_an_save")
|
||||
def post_adoption_notice_save(pk):
|
||||
instance = AdoptionNotice.objects.get(pk=pk)
|
||||
Location.add_location_to_object(instance)
|
||||
set_timestamp("add_adoption_notice_location")
|
||||
logging.info(f"Location was added to Adoption notice {pk}")
|
||||
|
||||
notify_search_subscribers(instance, only_if_active=True)
|
||||
notify_of_AN_to_be_checked(instance)
|
||||
|
||||
@celery_app.task(name="tools.healthcheck")
|
||||
def task_healthcheck():
|
||||
healthcheck_ok()
|
||||
set_timestamp("task_healthcheck")
|
||||
|
||||
|
||||
@shared_task
|
||||
def task_send_notification_email(notification_pk):
|
||||
send_notification_email(notification_pk)
|
||||
|
@@ -2,24 +2,54 @@
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
|
||||
{% block title %}<title>{% translate "Über uns und Regeln" %}</title> %}{% endblock %}
|
||||
{% block title %}<title>{% translate "Über uns und Regeln" %}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% translate "Regeln" %}</h1>
|
||||
{% if about_us %}
|
||||
<div class="card">
|
||||
<h1>{{ about_us.title }}</h1>
|
||||
<p>
|
||||
{{ about_us.content | render_markdown }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h2>{% translate "Regeln" %}</h2>
|
||||
{% include "fellchensammlung/lists/list-rules.html" %}
|
||||
|
||||
{% if faq %}
|
||||
<div class="card">
|
||||
<h2>{{ faq.title }}</h2>
|
||||
<p>
|
||||
{{ faq.content | render_markdown }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if privacy_statement %}
|
||||
<h1>{{ privacy_statement.title }}</h1>
|
||||
{{ privacy_statement.content | render_markdown }}
|
||||
<div class="card">
|
||||
<h2>{{ privacy_statement.title }}</h2>
|
||||
<p>
|
||||
{{ privacy_statement.content | render_markdown }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if terms_of_service %}
|
||||
<h1>{{ terms_of_service.title }}</h1>
|
||||
{{ terms_of_service.content | render_markdown }}
|
||||
<div class="card">
|
||||
<h2>{{ terms_of_service.title }}</h2>
|
||||
<p>
|
||||
{{ terms_of_service.content | render_markdown }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if imprint %}
|
||||
<h1>{{ imprint.title }}</h1>
|
||||
{{ imprint.content | render_markdown }}
|
||||
<div class="card">
|
||||
<h2>{{ imprint.title }}</h2>
|
||||
<p>
|
||||
{{ imprint.content | render_markdown }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@@ -0,0 +1,13 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}<title>{% translate "Tierschutzorganisationen" %}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-cards">
|
||||
<div class="card">
|
||||
{% include "fellchensammlung/partials/partial-map.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "fellchensammlung/lists/list-animal-shelters.html" %}
|
||||
{% endblock %}
|
@@ -14,6 +14,8 @@
|
||||
<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>
|
||||
|
||||
<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' %}">
|
||||
|
@@ -0,0 +1,69 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% load custom_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}<title>{{ org.name }}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-cards">
|
||||
<div class="card half">
|
||||
<h1>{{ org.name }}</h1>
|
||||
|
||||
<b><i class="fa-solid fa-location-dot"></i></b>
|
||||
{% if org.location %}
|
||||
{{ org.location.str_hr }}
|
||||
{% else %}
|
||||
{{ org.location_string }}
|
||||
{% endif %}
|
||||
<p>{{ org.description | render_markdown }}</p>
|
||||
<table class="responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
{% if org.website %}
|
||||
<td>{% translate "Website" %}</td>
|
||||
{% endif %}
|
||||
{% if org.phone_number %}
|
||||
<td>{% translate "Telefonnummer" %}</td>
|
||||
{% endif %}
|
||||
{% if org.email %}
|
||||
<td>{% translate "E-Mail" %}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tr>
|
||||
{% if org.website %}
|
||||
<td data-label="{% trans 'Website' %} ">
|
||||
{{ org.website }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if org.phone_number %}
|
||||
<td data-label="{% trans 'Telefonnummer' %}">
|
||||
{{ org.phone_number }}
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
{% if org.email %}
|
||||
<td data-label="{% trans 'E-Mail' %}">
|
||||
{{ org.email }}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card half">
|
||||
{% include "fellchensammlung/partials/partial-map.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<h2>{% translate 'Vermittlungen der Organisation' %}</h2>
|
||||
<div class="container-cards">
|
||||
{% if org.adoption_notices %}
|
||||
{% for adoption_notice in org.adoption_notices %}
|
||||
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
@@ -1,24 +1,83 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}<title>{{ user.get_full_name }}</title>{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ user.get_full_name }}</h1>
|
||||
<div class="spaced">
|
||||
|
||||
<p><strong>{% translate "Username" %}:</strong> {{ user.username }}</p>
|
||||
<p><strong>{% translate "E-Mail" %}:</strong> {{ user.email }}</p>
|
||||
<div class="container-cards">
|
||||
<h2>{% trans 'Daten' %}</h2>
|
||||
<div class="card">
|
||||
<p><strong>{% translate "Username" %}:</strong> {{ user.username }}</p>
|
||||
<p><strong>{% translate "E-Mail" %}:</strong> {{ user.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if user.preferred_language %}
|
||||
<p><strong>{% translate "Sprache" %}:</strong> {{ user.preferred_language }}</p>
|
||||
{% else %}
|
||||
<p>{% translate "Keine bevorzugte Sprache gesetzt." %}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if user.id is request.user.id %}
|
||||
<div class="container-cards">
|
||||
<h2>{% trans 'Profil verwalten' %}</h2>
|
||||
<div class="container-comment-form">
|
||||
<p>
|
||||
<a class="btn2" href="{% url 'password_change' %}">{% translate "Change password" %}</a>
|
||||
<a class="btn2" href="{% url 'user-me-export' %}">{% translate "Daten exportieren" %}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>{% translate 'Benachrichtigungen' %}</h2>
|
||||
{% include "fellchensammlung/lists/list-notifications.html" %}
|
||||
<h2>{% translate 'Meine Vermittlungen' %}</h2>
|
||||
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
|
||||
{% if user.id is request.user.id %}
|
||||
<div class="detail-animal-header"><h2>{% trans 'Einstellungen' %}</h2></div>
|
||||
<div class="container-cards">
|
||||
<form class="card" action="" method="POST">
|
||||
{% csrf_token %}
|
||||
{% if user.email_notifications %}
|
||||
<label class="toggle">
|
||||
<input type="submit" class="toggle-checkbox checked" name="toggle_email_notifications">
|
||||
<div class="toggle-switch round "></div>
|
||||
<span class="slider-label">
|
||||
{% translate 'E-Mail Benachrichtigungen' %}
|
||||
</span>
|
||||
</label>
|
||||
{% else %}
|
||||
<label class="toggle">
|
||||
<input type="submit" class="toggle-checkbox" name="toggle_email_notifications">
|
||||
<div class="toggle-switch round"></div>
|
||||
<span class="slider-label">
|
||||
{% translate 'E-Mail Benachrichtigungen' %}
|
||||
</span>
|
||||
</label>
|
||||
{% endif %}
|
||||
</form>
|
||||
<div class="card">
|
||||
{% if token %}
|
||||
<form action="" method="POST">
|
||||
{% csrf_token %}
|
||||
<p class="text-muted"><strong>{% translate "API token:" %}</strong> {{ token }}</p>
|
||||
<input class="btn" type="submit" name="delete_token"
|
||||
value={% translate "Delete API token" %}>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>{% translate "Kein API-Token vorhanden." %}</p>
|
||||
<form action="" method="POST">
|
||||
{% csrf_token %}
|
||||
<input class="btn" type="submit" name="create_token"
|
||||
value={% translate "Create API token" %}>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
<h2>{% translate 'Benachrichtigungen' %}</h2>
|
||||
{% include "fellchensammlung/lists/list-notifications.html" %}
|
||||
|
||||
<h2>{% translate 'Abonnierte Suchen' %}</h2>
|
||||
{% include "fellchensammlung/lists/list-search-subscriptions.html" %}
|
||||
|
||||
<h2>{% translate 'Meine Vermittlungen' %}</h2>
|
||||
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
@@ -2,27 +2,43 @@
|
||||
{% load custom_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}<title>{{adoption_notice.name }}</title>{% endblock %}
|
||||
{% block title %}<title>{{ adoption_notice.name }}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="detail-adoption-notice-header">
|
||||
<h1 class="detail-adoption-notice-header">{{ adoption_notice.name }}
|
||||
<div class="inline-container">
|
||||
<h1>{{ adoption_notice.name }}</h1>
|
||||
{% if not is_subscribed %}
|
||||
<form class="notification-card-mark-read" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="subscribe">
|
||||
<input type="hidden" name="adoption_notice_id" value="{{ adoption_notice.pk }}">
|
||||
<button class="btn2" type="submit" id="submit"><i class="fa-solid fa-bell"></i></button>
|
||||
</form>
|
||||
<div class="tooltip bottom">
|
||||
<form class="notification-card-mark-read" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="subscribe">
|
||||
<input type="hidden" name="adoption_notice_id" value="{{ adoption_notice.pk }}">
|
||||
<button class="btn2" type="submit" id="submit"><i class="fa-solid fa-bell"></i></button>
|
||||
</form>
|
||||
<span class="tooltiptext">
|
||||
{% translate 'Abonniere diese Vermittlung um bei Kommentaren oder Statusänderungen benachrichtigt zu werden' %}
|
||||
</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<form class="notification-card-mark-read" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="unsubscribe">
|
||||
<input type="hidden" name="adoption_notice_id" value="{{ adoption_notice.pk }}">
|
||||
<button class="btn2" type="submit" id="submit"><i class="fa-solid fa-bell-slash"></i></button>
|
||||
</form>
|
||||
<div class="tooltip bottom">
|
||||
<form class="notification-card-mark-read" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="unsubscribe">
|
||||
<input type="hidden" name="adoption_notice_id" value="{{ adoption_notice.pk }}">
|
||||
<button class="btn2" type="submit" id="submit"><i class="fa-solid fa-bell-slash"></i></button>
|
||||
</form>
|
||||
<span class="tooltiptext">
|
||||
{% translate 'Deabonnieren. Du bekommst keine Benachrichtigungen zu dieser Vermittlung mehr' %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% if adoption_notice.is_active %}
|
||||
<span id="submit" class="label active-adoption" style=>{% trans 'Aktive Vermittlung' %}</span>
|
||||
{% else %}
|
||||
<span id="submit" class="label inactive-adoption" style=>{% trans 'Vermittlung inaktiv' %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if has_edit_permission %}
|
||||
<a class="btn2"
|
||||
href="{% url 'adoption-notice-add-photo' adoption_notice_id=adoption_notice.pk %}">{% translate 'Foto hinzufügen' %}</a>
|
||||
@@ -31,36 +47,61 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="table-adoption-notice-info">
|
||||
<table>
|
||||
<table class="responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% translate "Ort" %}</th>
|
||||
<th>{% translate "Suchen seit" %}</th>
|
||||
<th>{% translate "Zuletzt aktualisiert" %}</th>
|
||||
<th>{% translate "Weitere Informationen" %}</th>
|
||||
<td>{% translate "Ort" %}</td>
|
||||
{% if adoption_notice.organization %}
|
||||
<td>{% translate "Organisation" %}</td>
|
||||
{% endif %}
|
||||
<td>{% translate "Suchen seit" %}</td>
|
||||
<td>{% translate "Zuletzt aktualisiert" %}</td>
|
||||
<td>{% translate "Weitere Informationen" %}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr>
|
||||
<td>
|
||||
<td data-label="{% trans 'Ort' %} ">
|
||||
{% if adoption_notice.location %}
|
||||
{{ adoption_notice.location }}
|
||||
{% else %}
|
||||
{{ adoption_notice.location_string }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if adoption_notice.organization %}
|
||||
<td data-label="{% trans 'Organisation' %}">
|
||||
<div>
|
||||
<a href="{{ adoption_notice.organization.get_absolute_url }}">{{ adoption_notice.organization }}</a>
|
||||
{% if adoption_notice.organization.trusted %}
|
||||
<div class="tooltip top">
|
||||
<div class="checkmark"><i class="fa-solid fa-check"></i></div>
|
||||
<span class="tooltiptext">
|
||||
{% translate 'Diese Organisation kennt sich mit Ratten aus und achtet auf gute Abgabebedingungen' %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<td>{{ adoption_notice.searching_since }}</td>
|
||||
<td>{{ adoption_notice.last_checked | date:'d. F Y' }}</td>
|
||||
{% if adoption_notice.further_information %}
|
||||
<td>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<td data-label="{% trans 'Suchen seit' %}">{{ adoption_notice.searching_since }}</td>
|
||||
<td data-label="{% trans 'Zuletzt aktualisiert' %}">
|
||||
{{ adoption_notice.last_checked_hr }}
|
||||
</td>
|
||||
<td data-label="{% trans 'Weitere Informationen' %}">
|
||||
{% if adoption_notice.further_information %}
|
||||
<form method="get" action="{% url 'external-site' %}">
|
||||
<input type="hidden" name="url" value="{{ adoption_notice.further_information }}">
|
||||
<button class="btn" type="submit" id="submit">
|
||||
{{ adoption_notice.further_information | domain }} <i class="fa-solid fa-arrow-up-right-from-square"></i>
|
||||
{{ adoption_notice.further_information | domain }} <i
|
||||
class="fa-solid fa-arrow-up-right-from-square"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>-</td>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
@@ -0,0 +1,15 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
|
||||
{% block title %}<title>{% translate "403 Forbidden" %}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>403 Forbidden</h1>
|
||||
<p>
|
||||
{% blocktranslate %}
|
||||
Diese Aktion ist dir nicht erlaubt. Logge dich ein oder nutze einen anderen Account. Wenn du denkst, dass hier
|
||||
ein Fehler vorliegt, kontaktiere das Team!
|
||||
{% endblocktranslate %}
|
||||
</p>
|
||||
{% endblock %}
|
@@ -1,10 +1,15 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
{% blocktranslate %}
|
||||
<p>Achtung du verlässt notfellchen.org</p>
|
||||
{% endblocktranslate %}
|
||||
<a href="{{ url }}" class="btn" >{% translate "Weiter" %}</a>
|
||||
{% if external_site_warning %}
|
||||
{{ external_site_warning.content | render_markdown }}
|
||||
{% else %}
|
||||
{% blocktranslate %}
|
||||
<p>Achtung du verlässt notfellchen.org</p>
|
||||
{% endblocktranslate %}
|
||||
{% endif %}
|
||||
<a href="{{ url }}" class="btn button">{% translate "Weiter" %}</a>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
@@ -8,9 +8,6 @@
|
||||
<option value="{{ language.0 }}" {% if language.0 == LANGUAGE_CODE_CURRENT %} selected{% endif %}>
|
||||
{{ language.0|language_name_local }}
|
||||
</option>
|
||||
<!--<option value="{{ language.0 }}" {% if language.0 == LANGUAGE_CODE %} selected{% endif %}>
|
||||
{{ language.0|language_name_local }} ({{ language.0 }})
|
||||
</option>-->
|
||||
{% endfor %}
|
||||
</select>
|
||||
<!--<input type="submit" value={% translate "change" %}>-->
|
||||
|
@@ -9,7 +9,7 @@
|
||||
Lade hier ein Foto hoch - wähle den Titel wie du willst und mach bitte eine Bildbeschreibung,
|
||||
damit die Fotos auch für blinde und sehbehinderte Personen zugänglich sind.
|
||||
{% endblocktranslate %}
|
||||
<p><a class="btn" href="https://www.dbsv.org/bildbeschreibung-4-regeln.html">{% translate 'Anleitung für Bildbeschreibungen' %}</a></p>
|
||||
<p><a class="btn2" href="https://www.dbsv.org/bildbeschreibung-4-regeln.html">{% translate 'Anleitung für Bildbeschreibungen' %}</a></p>
|
||||
</p>
|
||||
<div class="container-form">
|
||||
{% crispy form %}
|
||||
|
@@ -7,6 +7,6 @@
|
||||
<form method = "post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button class="button-report" type="submit">{% translate "Melden" %}</button>
|
||||
<button class="btn2" type="submit">{% translate "Melden" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
@@ -1,41 +1,57 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="header">
|
||||
<a href="{% url "index" %}" class="logo"><img src={% static 'fellchensammlung/img/logo_transparent.png' %}></a>
|
||||
<nav id="nav" class="nav justify-content-center">
|
||||
<a class="nav-link " href="{% url "search" %}"><i class="fas fa-search"></i> {% translate 'Suchen' %}</a>
|
||||
<a class="nav-link " href="{% url "add-adoption" %}"><i
|
||||
class="fas fa-feather"></i> {% translate 'Vermittlung hinzufügen' %}</a>
|
||||
<a class="nav-link " href="{% url "about" %}"><i class="fas fa-info"></i> {% translate 'Über uns' %}</a>
|
||||
<a class="nav-link " href="{% url "rss" %}"><i class="fa-solid fa-rss"></i> {% translate 'RSS' %}</a>
|
||||
|
||||
</nav>
|
||||
<div class="header-right">
|
||||
<div class="profile-card">
|
||||
{% include "fellchensammlung/forms/change_language.html" %}
|
||||
{% if user.is_authenticated %}
|
||||
|
||||
<div class="btn2 button_darken btn-notification">
|
||||
<a href="{{ user.get_notifications_url }}">
|
||||
<i class="fa fa-bell" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% if user.get_num_unread_notifications > 0 %}
|
||||
<span class="button__badge">{{ user.get_num_unread_notifications }}</span>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<a class="btn2" href="{{ user.get_absolute_url }}"><i aria-hidden="true" class="fas fa-user"></i></a>
|
||||
<form class="btn2 button_darken" action="{% url 'logout' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="button" type="submit"><i aria-hidden="true" class="fas fa-sign-out"></i></button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a class="btn2" href="{% url "django_registration_register" %}">{% translate "Registrieren" %}</a>
|
||||
<a class="btn2" href="{% url "login" %}"><i class="fa fa-sign-in" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<section class="header">
|
||||
<div>
|
||||
<a href="{% url "index" %}" class="logo"><img src={% static 'fellchensammlung/img/logo_transparent.png' %}></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-card">
|
||||
<div id="header-change-language">
|
||||
{% include "fellchensammlung/forms/change_language.html" %}
|
||||
</div>
|
||||
{% if user.is_authenticated %}
|
||||
<div class="btn2 button_darken btn-notification">
|
||||
<a href="{{ user.get_notifications_url }}">
|
||||
<i class="fa fa-bell" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% if user.get_num_unread_notifications > 0 %}
|
||||
<span class="button__badge">{{ user.get_num_unread_notifications }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<a class="btn2" href="{% url 'user-me' %}"><i aria-hidden="true" class="fas fa-user"></i></a>
|
||||
<form class="btn2 button_darken" id="header-sign-out" action="{% url 'logout' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="button" type="submit"><i aria-hidden="true" class="fas fa-sign-out"></i></button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a class="btn2" href="{% url "django_registration_register" %}">{% translate "Registrieren" %}</a>
|
||||
<a class="btn2" href="{% url "login" %}"><i class="fa fa-sign-in" aria-hidden="true"></i></a>
|
||||
{% endif %}
|
||||
<input id="menu-toggle" type="checkbox"/>
|
||||
<label class='menu-button-container' for="menu-toggle">
|
||||
<div class='menu-button'></div>
|
||||
</label>
|
||||
|
||||
<nav id="main-menu">
|
||||
<ul class="menu">
|
||||
<li>
|
||||
<a class="nav-link " href="{% url "search" %}">
|
||||
<i class="fas fa-search"></i> {% translate 'Suchen' %}
|
||||
</a>
|
||||
</li>
|
||||
<li><a class="nav-link " href="{% url "add-adoption" %}"><i
|
||||
class="fas fa-feather"></i> {% translate 'Vermittlung hinzufügen' %}</a></li>
|
||||
<li><a class="nav-link " href="{% url "about" %}"><i
|
||||
class="fas fa-info"></i> {% translate 'Über uns' %}
|
||||
</a>
|
||||
</li>
|
||||
<li><a class="nav-link " href="{% url "rss" %}"><i
|
||||
class="fa-solid fa-rss"></i> {% translate 'RSS' %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
@@ -106,7 +106,15 @@
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="deactivate_unchecked_adoption_notices">
|
||||
<button class="btn" type="submit" id="submit">
|
||||
<i class="fa-solid fa-broom"></i> {% translate "Deaktivire ungeprüfte Vermittlungen" %}
|
||||
<i class="fa-solid fa-broom"></i> {% translate "Deaktiviere ungeprüfte Vermittlungen" %}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form class="notification-card-mark-read" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="deactivate_404">
|
||||
<button class="btn" type="submit" id="submit">
|
||||
<i class="fa-solid fa-broom"></i> {% translate "Deaktiviere 404 Vermittlungen" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div class="container-cards">
|
||||
{% if adoption_notices %}
|
||||
{% for adoption_notice in adoption_notices %}
|
||||
{% include "fellchensammlung/partials/partial-adoption-notice.html" %}
|
||||
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
|
||||
|
@@ -0,0 +1,10 @@
|
||||
{% load i18n %}
|
||||
<div class="container-cards spaced">
|
||||
{% if rescue_organizations %}
|
||||
{% for rescue_organization in rescue_organizations %}
|
||||
{% include "fellchensammlung/partials/partial-rescue-organization.html" %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>{% translate "Keine Tierschutzorganisationen gefunden." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
@@ -1,5 +1,10 @@
|
||||
{% load i18n %}
|
||||
<div class="container-cards">
|
||||
{% for notification in notifications %}
|
||||
{% include "fellchensammlung/partials/partial-notification.html" %}
|
||||
{% endfor %}
|
||||
{% if notifications %}
|
||||
{% for notification in notifications %}
|
||||
{% include "fellchensammlung/partials/partial-notification.html" %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>{% translate 'Keine ungelesenen Benachrichtigungen' %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@@ -0,0 +1,10 @@
|
||||
{% load i18n %}
|
||||
<div class="container-cards">
|
||||
{% if search_subscriptions %}
|
||||
{% for search_subscription in search_subscriptions %}
|
||||
{% include "fellchensammlung/partials/partial-search-subscription.html" %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>{% translate 'Keine abonnierten Suchen' %}</p>
|
||||
{% endif %}
|
||||
</div>
|
@@ -2,31 +2,34 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div class="card">
|
||||
<div>
|
||||
<div class="header-card-adoption-notice">
|
||||
<h1><a class="heading-card-adoption-notice"
|
||||
href="{{ adoption_notice.get_absolute_url }}"> {{ adoption_notice.name }}</a></h1>
|
||||
<div class="header-card-adoption-notice">
|
||||
<h2 class="heading-card-adoption-notice"><a
|
||||
href="{{ adoption_notice.get_absolute_url }}"> {{ adoption_notice.name }}</a></h2>
|
||||
<div class="tooltip bottom">
|
||||
<a class="adoption-card-report-link" href="{{ adoption_notice.get_report_url }}"><i
|
||||
class="fa-solid fa-flag"></i></a>
|
||||
<span class="tooltiptext">
|
||||
{% translate 'Melde diese Vermittlung an Moderator*innen' %}
|
||||
</span>
|
||||
</div>
|
||||
<p>
|
||||
<b>Ort</b>
|
||||
{% if adoption_notice.location %}
|
||||
{{ adoption_notice.location }}
|
||||
{% else %}
|
||||
{{ adoption_notice.location_string }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
{% if adoption_notice.description_short %}
|
||||
{{ adoption_notice.description_short | render_markdown }}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% 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>
|
||||
</div>
|
||||
<p>
|
||||
<b><i class="fa-solid fa-location-dot"></i></b>
|
||||
{% if adoption_notice.location %}
|
||||
{{ adoption_notice.location.str_hr }}
|
||||
{% else %}
|
||||
{{ adoption_notice.location_string }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
{% if adoption_notice.description_short %}
|
||||
{{ adoption_notice.description_short | render_markdown }}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% 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>
|
||||
|
@@ -14,7 +14,7 @@
|
||||
<p>
|
||||
<b>Ort</b>
|
||||
{% if adoption_notice.location %}
|
||||
{{ adoption_notice.location }}
|
||||
{{ adoption_notice.location.str_hr }}
|
||||
{% else %}
|
||||
{{ adoption_notice.location_string }}
|
||||
{% endif %}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
<div class="card">
|
||||
<div class="detail-animal-header">
|
||||
<h1><a href="{% url 'animal-detail' animal_id=animal.pk %}">{{ animal.name }}</a></h1>
|
||||
@@ -19,7 +20,7 @@
|
||||
</div>
|
||||
|
||||
{% if animal.description %}
|
||||
<p>{{ animal.description }}</p>
|
||||
<p>{{ animal.description | render_markdown }}</p>
|
||||
{% endif %}
|
||||
{% for photo in animal.get_photos %}
|
||||
<img src="{{ MEDIA_URL }}/{{ photo.image }}" alt="{{ photo.alt_text }}">
|
||||
|
@@ -1,10 +1,11 @@
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
<div class="announcement {{ announcement.type }}">
|
||||
<div class="announcement-header">
|
||||
<h1 class="announcement">{{ announcement.title }}</h1>
|
||||
</div>
|
||||
<p>
|
||||
{{ announcement.content | render_markdown }}
|
||||
</p>
|
||||
<details class="announcement" open>
|
||||
<summary class="announcement-header">{{ announcement.title }}</summary>
|
||||
<p>
|
||||
{{ announcement.content | render_markdown }}
|
||||
</p>
|
||||
</details>
|
||||
|
||||
</div>
|
||||
|
@@ -0,0 +1,29 @@
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
<div class="card">
|
||||
<h1>
|
||||
<a href="{{ adoption_notice.get_absolute_url }}">{{ adoption_notice.name }}</a>
|
||||
</h1>
|
||||
<i>{% translate 'Zuletzt geprüft:' %} {{ adoption_notice.last_checked_hr }}</i>
|
||||
{% if adoption_notice.further_information %}
|
||||
<p>{% translate "Externe Quelle" %}: {{ adoption_notice.link_to_more_information | safe }}</p>
|
||||
{% endif %}
|
||||
<div class="container-edit-buttons">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden"
|
||||
name="adoption_notice_id"
|
||||
value="{{ adoption_notice.pk }}">
|
||||
<input type="hidden" name="action" value="checked_active">
|
||||
<button class="btn" type="submit">{% translate "Vermittlung noch aktuell" %}</button>
|
||||
</form>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden"
|
||||
name="adoption_notice_id"
|
||||
value="{{ adoption_notice.pk }}">
|
||||
<input type="hidden" name="action" value="checked_inactive">
|
||||
<button class="btn" type="submit">{% translate "Vermittlung inaktiv" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@@ -1,7 +1,7 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div class="container-comments">
|
||||
<h2>{% translate 'Comments' %}</h2>
|
||||
<h2>{% translate 'Kommentare' %}</h2>
|
||||
{% if adoption_notice.comments %}
|
||||
{% for comment in adoption_notice.comments %}
|
||||
{% include "fellchensammlung/partials/partial-comment.html" %}
|
||||
|
@@ -6,35 +6,141 @@
|
||||
<script src="{% settings_value "MAP_TILE_SERVER" %}/assets/maplibre-gl/maplibre-gl.js"></script>
|
||||
<link href="{% settings_value "MAP_TILE_SERVER" %}/assets/maplibre-gl/maplibre-gl.css" rel="stylesheet"/>
|
||||
|
||||
<!-- 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" style="width:100%;aspect-ratio:16/9"></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.json" %}',
|
||||
center: [10.49, 50.68],
|
||||
zoom: 5
|
||||
center: map_center,
|
||||
zoom: zoom_level
|
||||
}).addControl(new maplibregl.NavigationControl());
|
||||
|
||||
{% 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/partial-adoption-notice-minimal.html" %}`);
|
||||
// create the popup
|
||||
const popup_{{ forloop.counter }} = new maplibregl.Popup({offset: 25}).setHTML(`{% include "fellchensammlung/partials/partial-adoption-notice-minimal.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');
|
||||
// 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);
|
||||
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', {
|
||||
'type': 'geojson',
|
||||
'data': {
|
||||
'type': 'FeatureCollection',
|
||||
'features': [
|
||||
{
|
||||
'type': 'Feature',
|
||||
'geometry': {
|
||||
'type': 'Point',
|
||||
'coordinates': [{{ map_pin.location.longitude | pointdecimal }}, {{ map_pin.location.latitude | pointdecimal }}]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
{% endfor %}
|
||||
map.addLayer({
|
||||
'id': 'pints',
|
||||
'type': 'symbol',
|
||||
'source': 'point',
|
||||
'layout': {
|
||||
'icon-image': 'pin',
|
||||
'icon-size': 0.1
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
{% 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>
|
||||
|
@@ -16,7 +16,7 @@
|
||||
<p><b>{% translate "Kommentar zur Meldung" %}:</b>
|
||||
{{ report.user_comment }}
|
||||
</p>
|
||||
<div>
|
||||
<div class="container-edit-buttons">
|
||||
<form action="allow" class="">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="report_id" value="{{ report.pk }}">
|
||||
|
@@ -0,0 +1,22 @@
|
||||
{% load custom_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="card">
|
||||
<div>
|
||||
<h2 class="heading-card-adoption-notice"><a
|
||||
href="{{ rescue_organization.get_absolute_url }}"> {{ rescue_organization.name }}</a></h2>
|
||||
<p>
|
||||
<b><i class="fa-solid fa-location-dot"></i></b>
|
||||
{% if rescue_organization.location %}
|
||||
{{ rescue_organization.location.str_hr }}
|
||||
{% else %}
|
||||
{{ rescue_organization.location_string }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
{% if rescue_organization.description_short %}
|
||||
{{ rescue_organization.description_short | render_markdown }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,25 @@
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
<div class="search-subscription">
|
||||
<div class="search-subscription-header">
|
||||
<h3>{{ search_subscription }}</h3>
|
||||
<form class="" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="search_subscription_delete">
|
||||
<input type="hidden" name="search_subscription_id" value="{{ search_subscription.pk }}">
|
||||
<button class="btn3" type="submit" id="submit"><i class="fa-solid fa-close"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{% trans 'Geschlecht' %}</th>
|
||||
<th>{% trans 'Suchort' %}</th>
|
||||
<th>{% trans 'Suchradius' %}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ search_subscription.sex }}</td>
|
||||
<td>{{ search_subscription.location }}</td>
|
||||
<td>{{ search_subscription.max_distance }}km</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
@@ -4,15 +4,85 @@
|
||||
{% block title %}<title>{% translate "Suche" %}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form class="form-search card" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="longitude" maxlength="200" id="longitude">
|
||||
<input type="hidden" name="latitude" maxlength="200" id="latitude">
|
||||
{{ search_form.as_p }}
|
||||
<input class="btn" type="submit" value="Search" name="search">
|
||||
</form>
|
||||
{% if place_not_found %}
|
||||
<p class="error">{% translate "Ort nicht gefunden" %}</p>
|
||||
{% endif %}
|
||||
{% get_current_language as LANGUAGE_CODE_CURRENT %}
|
||||
<div class="container-cards">
|
||||
<form class="form-search card half" 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">
|
||||
{{ search_form.as_p }}
|
||||
<ul id="results"></ul>
|
||||
<div class="container-edit-buttons">
|
||||
<button class="btn" type="submit" value="search" name="search">
|
||||
<i class="fas fa-search"></i> {% trans 'Suchen' %}
|
||||
</button>
|
||||
{% if searched %}
|
||||
{% if subscribed_search %}
|
||||
<button class="btn" type="submit" value="{{ subscribed_search.pk }}"
|
||||
name="unsubscribe_to_search">
|
||||
<i class="fas fa-bell-slash"></i> {% trans 'Suche nicht mehr abonnieren' %}
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn" type="submit" name="subscribe_to_search">
|
||||
<i class="fas fa-bell"></i> {% trans 'Suche abonnieren' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if place_not_found %}
|
||||
<p class="error">
|
||||
{% trans 'Ort nicht gefunden' %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</form>
|
||||
<div class="card half">
|
||||
{% include "fellchensammlung/partials/partial-map.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
|
||||
|
||||
<script>
|
||||
const locationInput = document.getElementById('id_location_string');
|
||||
const resultsList = document.getElementById('results');
|
||||
const placeIdInput = document.getElementById('place_id');
|
||||
|
||||
locationInput.addEventListener('input', async function () {
|
||||
const query = locationInput.value.trim();
|
||||
|
||||
if (query.length < 3) {
|
||||
resultsList.innerHTML = ''; // Don't search for or show results if input is less than 3 characters
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`{{ geocoding_api_url }}/?q=${encodeURIComponent(query)}&limit=5&lang={{ 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 %}
|
||||
|
@@ -3,32 +3,16 @@
|
||||
{% block content %}
|
||||
<h1>{% translate "Aktualitätscheck" %}</h1>
|
||||
<p>{% translate "Überprüfe ob Vermittlungen noch aktuell sind" %}</p>
|
||||
{% for adoption_notice in adoption_notices %}
|
||||
<div class="card">
|
||||
<h1>
|
||||
<a href="{{ adoption_notice.get_absolute_url }}">{{ adoption_notice.name }}</a>
|
||||
</h1>
|
||||
{% if adoption_notice.further_information %}
|
||||
<p>{% translate "Externe Quelle" %}: {{ adoption_notice.link_to_more_information | safe }}</p>
|
||||
{% endif %}
|
||||
<div>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden"
|
||||
name="adoption_notice_id"
|
||||
value="{{ adoption_notice.pk }}">
|
||||
<input type="hidden" name="action" value="checked_active">
|
||||
<button class="btn" type="submit">{% translate "Vermittlung noch aktuell" %}</button>
|
||||
</form>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden"
|
||||
name="adoption_notice_id"
|
||||
value="{{ adoption_notice.pk }}">
|
||||
<input type="hidden" name="action" value="checked_inactive">
|
||||
<button class="btn" type="submit">{% translate "Vermittlung inaktiv" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="container-cards spaced">
|
||||
<h1>{% translate 'Deaktivierte Vermittlungen zur Überprüfung' %}</h1>
|
||||
{% for adoption_notice in adoption_notices_disabled %}
|
||||
{% include "fellchensammlung/partials/partial-check-adoption-notice.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="container-cards spaced">
|
||||
<h1>{% translate 'Aktive Vermittlungen zur Überprüfung' %}</h1>
|
||||
{% for adoption_notice in adoption_notices_active %}
|
||||
{% include "fellchensammlung/partials/partial-check-adoption-notice.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
@@ -1,7 +1,11 @@
|
||||
import logging
|
||||
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
from fellchensammlung.models import AdoptionNotice, Location, RescueOrganization, AdoptionNoticeStatus
|
||||
from fellchensammlung.models import AdoptionNotice, Location, RescueOrganization, AdoptionNoticeStatus, Log, \
|
||||
AdoptionNoticeNotification
|
||||
from fellchensammlung.tools.misc import is_404
|
||||
|
||||
|
||||
def clean_locations(quiet=True):
|
||||
@@ -50,16 +54,39 @@ def clean_locations(quiet=True):
|
||||
|
||||
def get_unchecked_adoption_notices(weeks=3):
|
||||
now = timezone.now()
|
||||
three_weeks_ago = now - timedelta(weeks=weeks)
|
||||
n_weeks_ago = now - timedelta(weeks=weeks)
|
||||
|
||||
# Query for active adoption notices that were checked in the last three weeks
|
||||
# Query for active adoption notices that were not checked in the last n weeks
|
||||
unchecked_adoptions = AdoptionNotice.objects.filter(
|
||||
last_checked__gte=three_weeks_ago
|
||||
last_checked__lte=n_weeks_ago
|
||||
)
|
||||
active_unchecked_adoptions = [adoption for adoption in unchecked_adoptions if adoption.is_active]
|
||||
return active_unchecked_adoptions
|
||||
|
||||
|
||||
def get_active_adoption_notices():
|
||||
ans = AdoptionNotice.objects.all()
|
||||
active_adoptions = [adoption for adoption in ans if adoption.is_active]
|
||||
return active_adoptions
|
||||
|
||||
|
||||
def deactivate_unchecked_adoption_notices():
|
||||
for adoption_notice in get_unchecked_adoption_notices(weeks=3):
|
||||
AdoptionNoticeStatus.objects.get(adoption_notice=adoption_notice).deactivate_unchecked()
|
||||
adoption_notice.set_unchecked()
|
||||
|
||||
|
||||
def deactivate_404_adoption_notices():
|
||||
for adoption_notice in get_active_adoption_notices():
|
||||
if adoption_notice.further_information and adoption_notice.further_information != "":
|
||||
if is_404(adoption_notice.further_information):
|
||||
adoption_notice.set_closed()
|
||||
logging_msg = f"Automatically set Adoption Notice {adoption_notice.id} closed as link to more information returened 404"
|
||||
logging.info(logging_msg)
|
||||
Log.objects.create(action="automated", text=logging_msg)
|
||||
|
||||
deactivation_message = f'Die Vermittlung [{adoption_notice.name}]({adoption_notice.get_absolute_url()}) wurde automatisch deaktiviert, da die Website unter "Mehr Informationen" nicht mehr online ist.'
|
||||
for subscription in adoption_notice.get_subscriptions():
|
||||
AdoptionNoticeNotification.objects.create(user=subscription.owner,
|
||||
title="Vermittlung deaktiviert",
|
||||
adoption_notice=adoption_notice,
|
||||
text=deactivation_message)
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
|
||||
import requests
|
||||
import json
|
||||
from math import radians, sqrt, sin, cos, atan2
|
||||
@@ -6,6 +8,23 @@ from math import radians, sqrt, sin, cos, atan2
|
||||
from notfellchen import __version__ as nf_version
|
||||
from notfellchen import settings
|
||||
|
||||
Position = namedtuple('Position', ['latitude', 'longitude'])
|
||||
|
||||
|
||||
def zoom_level_for_radius(radius) -> int:
|
||||
if radius is None:
|
||||
return 4
|
||||
if radius <= 20:
|
||||
return 8
|
||||
if radius <= 50:
|
||||
return 7
|
||||
if radius <= 150:
|
||||
return 6
|
||||
if radius <= 300:
|
||||
return 5
|
||||
else:
|
||||
return 4
|
||||
|
||||
|
||||
def calculate_distance_between_coordinates(position1, position2):
|
||||
"""
|
||||
@@ -29,6 +48,8 @@ def calculate_distance_between_coordinates(position1, position2):
|
||||
|
||||
distance_in_km = earth_radius_km * c
|
||||
|
||||
logging.debug(f"Calculated Distance: {distance_in_km:.5}km")
|
||||
|
||||
return distance_in_km
|
||||
|
||||
|
||||
@@ -46,8 +67,44 @@ class RequestMock:
|
||||
return ResponseMock()
|
||||
|
||||
|
||||
class GeoFeature:
|
||||
|
||||
@staticmethod
|
||||
def geofeatures_from_photon_result(result):
|
||||
geofeatures = []
|
||||
for feature in result["features"]:
|
||||
geojson = {}
|
||||
try:
|
||||
geojson['name'] = feature["properties"]["name"]
|
||||
except KeyError:
|
||||
geojson['name'] = feature["properties"]["street"]
|
||||
geojson['place_id'] = feature["properties"]["osm_id"]
|
||||
geojson['lat'] = feature["geometry"]["coordinates"][1]
|
||||
geojson['lon'] = feature["geometry"]["coordinates"][0]
|
||||
geofeatures.append(geojson)
|
||||
return geofeatures
|
||||
|
||||
@staticmethod
|
||||
def geofeatures_from_nominatim_result(result):
|
||||
geofeatures = []
|
||||
for feature in result:
|
||||
geojson = {}
|
||||
if "name" in feature:
|
||||
geojson['name'] = feature["name"]
|
||||
else:
|
||||
geojson['name'] = feature["display_name"]
|
||||
geojson['place_id'] = feature["place_id"]
|
||||
geojson['lat'] = feature["lat"]
|
||||
geojson['lon'] = feature["lon"]
|
||||
geofeatures.append(geojson)
|
||||
return geofeatures
|
||||
|
||||
|
||||
class GeoAPI:
|
||||
api_url = settings.GEOCODING_API_URL
|
||||
api_format = settings.GEOCODING_API_FORMAT
|
||||
assert api_format in ['nominatim', 'photon']
|
||||
|
||||
# Set User-Agent headers as required by most usage policies (and it's the nice thing to do)
|
||||
headers = {
|
||||
'User-Agent': f"Notfellchen {nf_version}",
|
||||
@@ -62,34 +119,67 @@ class GeoAPI:
|
||||
else:
|
||||
self.requests = requests
|
||||
|
||||
def get_coordinates_from_query(self, location_string):
|
||||
try:
|
||||
result = \
|
||||
self.requests.get(self.api_url, {"q": location_string, "format": "jsonv2"}, headers=self.headers).json()[0]
|
||||
except IndexError:
|
||||
return None
|
||||
return result["lat"], result["lon"]
|
||||
|
||||
def _get_raw_response(self, location_string):
|
||||
result = self.requests.get(self.api_url, {"q": location_string, "format": "jsonv2"}, headers=self.headers)
|
||||
return result.content
|
||||
|
||||
def get_geojson_for_query(self, location_string):
|
||||
def get_geojson_for_query(self, location_string, language="de"):
|
||||
try:
|
||||
result = self.requests.get(self.api_url,
|
||||
{"q": location_string,
|
||||
"format": "jsonv2"},
|
||||
headers=self.headers).json()
|
||||
if self.api_format == 'nominatim':
|
||||
logging.info(f"Querying nominatim instance for: {location_string} ({self.api_url})")
|
||||
result = self.requests.get(self.api_url,
|
||||
{"q": location_string,
|
||||
"format": "jsonv2"},
|
||||
headers=self.headers).json()
|
||||
geofeatures = GeoFeature.geofeatures_from_nominatim_result(result)
|
||||
elif self.api_format == 'photon':
|
||||
logging.info(f"Querying photon instance for: {location_string} ({self.api_url})")
|
||||
result = self.requests.get(self.api_url,
|
||||
{"q": location_string, "lang": language},
|
||||
headers=self.headers).json()
|
||||
geofeatures = GeoFeature.geofeatures_from_photon_result(result)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
except Exception as e:
|
||||
logging.warning(f"Exception {e} when querying Nominatim")
|
||||
logging.warning(f"Exception {e} when querying geocoding server")
|
||||
return None
|
||||
if len(result) == 0:
|
||||
logging.warning(f"Couldn't find a result for {location_string} when querying Nominatim")
|
||||
if len(geofeatures) == 0:
|
||||
logging.warning(f"Couldn't find a result for {location_string} when querying geocoding server")
|
||||
return None
|
||||
return result
|
||||
return geofeatures
|
||||
|
||||
|
||||
class LocationProxy:
|
||||
"""
|
||||
Location proxy is used as a precursor to the location model without the need to create unnecessary database objects
|
||||
"""
|
||||
|
||||
def __init__(self, location_string):
|
||||
"""
|
||||
Creates the location proxy from the location string
|
||||
"""
|
||||
self.geo_api = GeoAPI()
|
||||
geofeatures = self.geo_api.get_geojson_for_query(location_string)
|
||||
if geofeatures is None:
|
||||
raise ValueError
|
||||
result = geofeatures[0]
|
||||
self.name = result["name"]
|
||||
self.place_id = result["place_id"]
|
||||
self.latitude = result["lat"]
|
||||
self.longitude = result["lon"]
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.place_id == other.place_id
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
return (self.latitude, self.longitude)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
geo = GeoAPI(debug=False)
|
||||
print(geo.get_coordinates_from_query("12101"))
|
||||
print(calculate_distance_between_coordinates(('48.4949904', '9.040330235970146'), ("48.648333", "9.451111")))
|
||||
|
@@ -1,6 +1,9 @@
|
||||
import datetime as datetime
|
||||
import logging
|
||||
|
||||
from django.utils.translation import ngettext
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from notfellchen import settings
|
||||
import requests
|
||||
|
||||
@@ -29,8 +32,41 @@ def age_as_hr_string(age: datetime.timedelta) -> str:
|
||||
return f'{days:.0f} Tag{pluralize(days)}'
|
||||
|
||||
|
||||
def time_since_as_hr_string(age: datetime.timedelta) -> str:
|
||||
days = age.days
|
||||
weeks = age.days / 7
|
||||
months = age.days / 30
|
||||
years = age.days / 365
|
||||
if years >= 1:
|
||||
text = ngettext(
|
||||
"vor einem Jahr",
|
||||
"vor %(years)d Tagen",
|
||||
years,
|
||||
) % {
|
||||
"years": years,
|
||||
}
|
||||
elif months >= 3:
|
||||
text = _("vor %(month)d Monaten") % {"month": months}
|
||||
elif weeks >= 3:
|
||||
text = _("vor %(weeks)d Wochen") % {"weeks": weeks}
|
||||
else:
|
||||
if days == 0:
|
||||
text = _("Heute")
|
||||
else:
|
||||
text = ngettext("vor einem Tag","vor %(count)d Tagen", days,) % {"count": days,}
|
||||
return text
|
||||
|
||||
|
||||
def healthcheck_ok():
|
||||
try:
|
||||
requests.get(settings.HEALTHCHECKS_URL, timeout=10)
|
||||
except requests.RequestException as e:
|
||||
logging.error("Ping to healthcheck-server failed: %s" % e)
|
||||
|
||||
|
||||
def is_404(url):
|
||||
try:
|
||||
result = requests.get(url, timeout=10)
|
||||
return result.status_code == 404
|
||||
except requests.RequestException as e:
|
||||
logging.warning(f"Request to {url} failed: {e}")
|
||||
|
13
src/fellchensammlung/tools/notifications.py
Normal file
13
src/fellchensammlung/tools/notifications.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from fellchensammlung.models import User, AdoptionNoticeNotification, TrustLevel
|
||||
|
||||
|
||||
def notify_of_AN_to_be_checked(adoption_notice):
|
||||
if adoption_notice.is_disabled_unchecked:
|
||||
users_to_notify = set(User.objects.filter(trust_level__gt=TrustLevel.MODERATOR))
|
||||
users_to_notify.add(adoption_notice.owner)
|
||||
for user in users_to_notify:
|
||||
AdoptionNoticeNotification.objects.create(adoption_notice=adoption_notice,
|
||||
user=user,
|
||||
title=f" Prüfe Vermittlung {adoption_notice}",
|
||||
text=f"{adoption_notice} muss geprüft werden bevor sie veröffentlicht wird.",
|
||||
)
|
151
src/fellchensammlung/tools/search.py
Normal file
151
src/fellchensammlung/tools/search.py
Normal file
@@ -0,0 +1,151 @@
|
||||
import logging
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .geo import LocationProxy, Position
|
||||
from ..forms import AdoptionNoticeSearchForm
|
||||
from ..models import SearchSubscription, AdoptionNotice, AdoptionNoticeNotification, SexChoicesWithAll, Location
|
||||
|
||||
|
||||
def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active : bool = True):
|
||||
"""
|
||||
This functions checks for all search subscriptions if the new adoption notice fits the search.
|
||||
If the new adoption notice fits the search subscription, it sends a notification to the user that created the search.
|
||||
"""
|
||||
logging.debug(f"Notifying {adoption_notice}.")
|
||||
if only_if_active and not adoption_notice.is_active:
|
||||
logging.debug(f"No notifications triggered for adoption notice {adoption_notice} because it's not active.")
|
||||
return
|
||||
for search_subscription in SearchSubscription.objects.all():
|
||||
logging.debug(f"Search subscription {search_subscription} found.")
|
||||
search = Search(search_subscription=search_subscription)
|
||||
if search.adoption_notice_fits_search(adoption_notice):
|
||||
notification_text = f"{_('Zu deiner Suche')} {search_subscription} wurde eine neue Vermittlung gefunden"
|
||||
AdoptionNoticeNotification.objects.create(user=search_subscription.owner,
|
||||
title=f"{_('Neue Vermittlung')}: {adoption_notice}",
|
||||
adoption_notice=adoption_notice,
|
||||
text=notification_text)
|
||||
logging.debug(f"Notification for search subscription {search_subscription} was sent.")
|
||||
else:
|
||||
logging.debug(f"Adoption notice {adoption_notice} was not fitting the search subscription.")
|
||||
|
||||
logging.info(f"Subscribers for AN {adoption_notice.pk} have been notified\n")
|
||||
|
||||
|
||||
class Search:
|
||||
def __init__(self, request=None, search_subscription=None):
|
||||
self.sex = None
|
||||
self.area_search = None
|
||||
self.max_distance = None
|
||||
self.location = None # Can either be Location (DjangoModel) or LocationProxy
|
||||
self.place_not_found = False # Indicates that a location was given but could not be geocoded
|
||||
self.search_form = None
|
||||
# Either place_id or location string must be set for area search
|
||||
self.location_string = None
|
||||
|
||||
if request:
|
||||
self.search_from_request(request)
|
||||
elif search_subscription:
|
||||
self.search_from_search_subscription(search_subscription)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"Search: {self.sex=}, {self.location=}, {self.area_search=}, {self.max_distance=}"
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Custom equals that also supports SearchSubscriptions
|
||||
|
||||
Only allowed to be called for located subscriptions
|
||||
"""
|
||||
# If both locations are empty check only for sex
|
||||
if self.location is None and other.location is None:
|
||||
return self.sex == other.sex
|
||||
# If one location is empty and the other is not, they are not equal
|
||||
elif self.location is not None and other.location is None or self.location is None and other.location is not None:
|
||||
return False
|
||||
return self.location == other.location and self.sex == other.sex and self.max_distance == other.max_distance
|
||||
|
||||
def _locate(self):
|
||||
try:
|
||||
self.location = LocationProxy(self.location_string)
|
||||
except ValueError:
|
||||
self.place_not_found = True
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
if self.area_search and not self.place_not_found:
|
||||
return Position(latitude=self.location.latitude, longitude=self.location.longitude)
|
||||
else:
|
||||
return None
|
||||
|
||||
def adoption_notice_fits_search(self, adoption_notice: AdoptionNotice):
|
||||
# Make sure sex is set and sex is not set to all (then it can be disregarded)
|
||||
if self.sex is not None and self.sex != SexChoicesWithAll.ALL:
|
||||
# AN does not fit search if search sex is not in available sexes of this AN
|
||||
if not self.sex in adoption_notice.sexes:
|
||||
logging.debug("Sex mismatch")
|
||||
return False
|
||||
# make sure it's an area search and the place is found to check location
|
||||
if self.area_search and not self.place_not_found:
|
||||
# If adoption notice is in not in search distance, return false
|
||||
if not adoption_notice.in_distance(self.location.position, self.max_distance):
|
||||
logging.debug("Area mismatch")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_adoption_notices(self):
|
||||
adoptions = AdoptionNotice.objects.order_by("-created_at")
|
||||
# Filter for active adoption notices
|
||||
adoptions = [adoption for adoption in adoptions if adoption.is_active]
|
||||
# Check if adoption notice fits search.
|
||||
adoptions = [adoption for adoption in adoptions if self.adoption_notice_fits_search(adoption)]
|
||||
|
||||
return adoptions
|
||||
|
||||
def search_from_request(self, request):
|
||||
if request.method == 'POST':
|
||||
self.search_form = AdoptionNoticeSearchForm(request.POST)
|
||||
self.search_form.is_valid()
|
||||
self.sex = self.search_form.cleaned_data["sex"]
|
||||
|
||||
if self.search_form.cleaned_data["location_string"] != "" and self.search_form.cleaned_data[
|
||||
"max_distance"] != "":
|
||||
self.area_search = True
|
||||
self.location_string = self.search_form.cleaned_data["location_string"]
|
||||
self.max_distance = int(self.search_form.cleaned_data["max_distance"])
|
||||
self._locate()
|
||||
else:
|
||||
self.search_form = AdoptionNoticeSearchForm()
|
||||
|
||||
def search_from_search_subscription(self, search_subscription: SearchSubscription):
|
||||
self.sex = search_subscription.sex
|
||||
self.location = search_subscription.location
|
||||
self.area_search = True
|
||||
self.max_distance = search_subscription.max_distance
|
||||
|
||||
|
||||
def subscribe(self, user):
|
||||
logging.info(f"{user} subscribed to search")
|
||||
if isinstance(self.location, LocationProxy):
|
||||
self.location = Location.get_location_from_proxy(self.location)
|
||||
SearchSubscription.objects.create(owner=user,
|
||||
location=self.location,
|
||||
sex=self.sex,
|
||||
max_distance=self.max_distance)
|
||||
|
||||
def get_subscription_or_none(self, user):
|
||||
user_subscriptions = SearchSubscription.objects.filter(owner=user)
|
||||
for subscription in user_subscriptions:
|
||||
if self == subscription:
|
||||
return subscription
|
||||
|
||||
def is_subscribed(self, user):
|
||||
"""
|
||||
Returns true if a user is already subscribed to a search with these parameters
|
||||
"""
|
||||
subscription = self.get_subscription_or_none()
|
||||
if subscription is None:
|
||||
return False
|
||||
else:
|
||||
return True
|
@@ -5,6 +5,7 @@ from .forms import CustomRegistrationForm
|
||||
from .feeds import LatestAdoptionNoticesFeed
|
||||
|
||||
from . import views
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.index, name="index"),
|
||||
@@ -25,6 +26,10 @@ urlpatterns = [
|
||||
# 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("tierschutzorganisationen/", views.list_rescue_organizations, name="rescue-organizations"),
|
||||
path("organisation/<int:rescue_organization_id>/", views.detail_view_rescue_organization,
|
||||
name="rescue-organization-detail"),
|
||||
|
||||
# ex: /search/
|
||||
path("suchen/", views.search, name="search"),
|
||||
# ex: /map/
|
||||
@@ -50,7 +55,9 @@ urlpatterns = [
|
||||
## USERS ##
|
||||
###########
|
||||
# ex: user/1
|
||||
path("user/<int:user_id>/", views.user_detail, name="user-detail"),
|
||||
path("user/<int:user_id>/", views.user_by_id, name="user-detail"),
|
||||
path("user/me/", views.my_profile, name="user-me"),
|
||||
path('user/me/export/', views.export_own_profile, name='user-me-export'),
|
||||
|
||||
path('accounts/register/',
|
||||
RegistrationView.as_view(
|
||||
@@ -78,6 +85,10 @@ urlpatterns = [
|
||||
## API ##
|
||||
#########
|
||||
path('api/', include('fellchensammlung.api.urls')),
|
||||
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||
# Optional UI:
|
||||
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||
path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
|
||||
|
||||
###################
|
||||
## External Site ##
|
||||
|
@@ -1,12 +1,15 @@
|
||||
import logging
|
||||
|
||||
from django.http import HttpResponseRedirect, JsonResponse
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
from django.http import HttpResponseRedirect, JsonResponse, HttpResponse
|
||||
from django.shortcuts import render, redirect
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils import translation
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
from django.core.serializers import serialize
|
||||
import json
|
||||
|
||||
from .mail import mail_admins_new_report
|
||||
from notfellchen import settings
|
||||
@@ -14,27 +17,31 @@ from notfellchen import settings
|
||||
from fellchensammlung import logger
|
||||
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
|
||||
User, Location, AdoptionNoticeStatus, Subscriptions, CommentNotification, BaseNotification, RescueOrganization, \
|
||||
Species, Log, Timestamp
|
||||
Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, AdoptionNoticeNotification
|
||||
from .forms import AdoptionNoticeForm, AdoptionNoticeFormWithDateWidget, ImageForm, ReportAdoptionNoticeForm, \
|
||||
CommentForm, ReportCommentForm, AnimalForm, \
|
||||
AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal
|
||||
from .models import Language, Announcement
|
||||
from .tools.geo import GeoAPI
|
||||
from .tools.geo import GeoAPI, zoom_level_for_radius
|
||||
from .tools.metrics import gather_metrics_data
|
||||
from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices
|
||||
from .tasks import add_adoption_notice_location
|
||||
from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
|
||||
deactivate_404_adoption_notices
|
||||
from .tasks import post_adoption_notice_save
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
from .tools.search import Search
|
||||
|
||||
|
||||
def user_is_trust_level_or_above(user, trust_level=User.MODERATOR):
|
||||
return user.is_authenticated and user.trust_level >= User.TRUST_LEVEL[trust_level]
|
||||
def user_is_trust_level_or_above(user, trust_level=TrustLevel.MODERATOR):
|
||||
return user.is_authenticated and user.trust_level >= trust_level
|
||||
|
||||
|
||||
def user_is_owner_or_trust_level(user, django_object, trust_level=User.MODERATOR):
|
||||
def user_is_owner_or_trust_level(user, django_object, trust_level=TrustLevel.MODERATOR):
|
||||
return user.is_authenticated and (
|
||||
user.trust_level == User.TRUST_LEVEL[trust_level] or django_object.owner == user)
|
||||
user.trust_level == trust_level or django_object.owner == user)
|
||||
|
||||
|
||||
def fail_if_user_not_owner_or_trust_level(user, django_object, trust_level=User.MODERATOR):
|
||||
def fail_if_user_not_owner_or_trust_level(user, django_object, trust_level=TrustLevel.MODERATOR):
|
||||
if not user_is_owner_or_trust_level(user, django_object, trust_level):
|
||||
raise PermissionDenied
|
||||
|
||||
@@ -69,7 +76,9 @@ def change_language(request):
|
||||
translation.activate(language_code)
|
||||
response = HttpResponseRedirect(redirect_path)
|
||||
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language_code)
|
||||
return response
|
||||
return response
|
||||
else:
|
||||
return render(request, 'fellchensammlung/index.html')
|
||||
|
||||
|
||||
def adoption_notice_detail(request, adoption_notice_id):
|
||||
@@ -121,6 +130,8 @@ def adoption_notice_detail(request, adoption_notice_id):
|
||||
if action == "unsubscribe":
|
||||
subscription.delete()
|
||||
is_subscribed = False
|
||||
elif action == "subscribe":
|
||||
return redirect_to_login(next=request.path)
|
||||
else:
|
||||
raise PermissionDenied
|
||||
else:
|
||||
@@ -148,7 +159,8 @@ def adoption_notice_edit(request, adoption_notice_id):
|
||||
adoption_notice_instance.save()
|
||||
|
||||
"""Log"""
|
||||
Log.objects.create(user=request.user, action="adoption_notice_edit", text=f"{request.user} hat Vermittlung {adoption_notice.pk} geändert")
|
||||
Log.objects.create(user=request.user, action="adoption_notice_edit",
|
||||
text=f"{request.user} hat Vermittlung {adoption_notice.pk} geändert")
|
||||
return redirect(reverse("adoption-notice-detail", args=[adoption_notice_instance.pk], ))
|
||||
else:
|
||||
form = AdoptionNoticeForm(instance=adoption_notice)
|
||||
@@ -162,30 +174,44 @@ def animal_detail(request, animal_id):
|
||||
|
||||
|
||||
def search(request):
|
||||
place_not_found = None
|
||||
# A user just visiting the search site did not search, only upon completing the search form a user has really
|
||||
# searched. This will toggle the "subscribe" button
|
||||
searched = False
|
||||
search = Search()
|
||||
search.search_from_request(request)
|
||||
if request.method == 'POST':
|
||||
latest_adoption_list = AdoptionNotice.objects.order_by("-created_at")
|
||||
active_adoptions = [adoption for adoption in latest_adoption_list if adoption.is_active]
|
||||
|
||||
search_form = AdoptionNoticeSearchForm(request.POST)
|
||||
max_distance = int(request.POST.get('max_distance'))
|
||||
if max_distance == "":
|
||||
max_distance = None
|
||||
geo_api = GeoAPI()
|
||||
search_position = geo_api.get_coordinates_from_query(request.POST['postcode'])
|
||||
if search_position is None:
|
||||
place_not_found = True
|
||||
adoption_notices_in_distance = active_adoptions
|
||||
else:
|
||||
adoption_notices_in_distance = [a for a in active_adoptions if a.in_distance(search_position, max_distance)]
|
||||
|
||||
context = {"adoption_notices": adoption_notices_in_distance, "search_form": search_form,
|
||||
"place_not_found": place_not_found}
|
||||
searched = True
|
||||
if "subscribe_to_search" in request.POST:
|
||||
# Make sure user is logged in
|
||||
if not request.user.is_authenticated:
|
||||
return redirect_to_login(next=request.path)
|
||||
search.subscribe(request.user)
|
||||
if "unsubscribe_to_search" in request.POST:
|
||||
if not request.user.is_authenticated:
|
||||
return redirect_to_login(next=request.path)
|
||||
search_subscription = SearchSubscription.objects.get(pk=request.POST["unsubscribe_to_search"])
|
||||
if search_subscription.owner == request.user:
|
||||
search_subscription.delete()
|
||||
else:
|
||||
raise PermissionDenied
|
||||
if request.user.is_authenticated:
|
||||
subscribed_search = search.get_subscription_or_none(request.user)
|
||||
else:
|
||||
latest_adoption_list = AdoptionNotice.objects.order_by("-created_at")
|
||||
active_adoptions = [adoption for adoption in latest_adoption_list if adoption.is_active]
|
||||
search_form = AdoptionNoticeSearchForm()
|
||||
context = {"adoption_notices": active_adoptions, "search_form": search_form}
|
||||
subscribed_search = None
|
||||
|
||||
context = {"adoption_notices": search.get_adoption_notices(),
|
||||
"search_form": search.search_form,
|
||||
"place_not_found": search.place_not_found,
|
||||
"subscribed_search": subscribed_search,
|
||||
"searched": searched,
|
||||
"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)
|
||||
|
||||
|
||||
@@ -196,25 +222,13 @@ def add_adoption_notice(request):
|
||||
in_adoption_notice_creation_flow=True)
|
||||
|
||||
if form.is_valid():
|
||||
instance = form.save(commit=False)
|
||||
instance.owner = request.user
|
||||
instance.save()
|
||||
an_instance = form.save(commit=False)
|
||||
an_instance.owner = request.user
|
||||
|
||||
"""Spin up a task that adds the location"""
|
||||
add_adoption_notice_location.delay_on_commit(instance.pk)
|
||||
|
||||
# Set correct status
|
||||
if request.user.trust_level >= User.TRUST_LEVEL[User.COORDINATOR]:
|
||||
major_status = AdoptionNoticeStatus.ACTIVE
|
||||
minor_status = AdoptionNoticeStatus.MINOR_STATUS_CHOICES[AdoptionNoticeStatus.ACTIVE]["searching"]
|
||||
if request.user.trust_level >= TrustLevel.MODERATOR:
|
||||
an_instance.set_active()
|
||||
else:
|
||||
major_status = AdoptionNoticeStatus.AWAITING_ACTION
|
||||
minor_status = AdoptionNoticeStatus.MINOR_STATUS_CHOICES[AdoptionNoticeStatus.AWAITING_ACTION][
|
||||
"waiting_for_review"]
|
||||
status = AdoptionNoticeStatus.objects.create(major_status=major_status,
|
||||
minor_status=minor_status,
|
||||
adoption_notice=instance)
|
||||
status.save()
|
||||
an_instance.set_unchecked()
|
||||
|
||||
# Get the species and number of animals from the form
|
||||
species = form.cleaned_data["species"]
|
||||
@@ -223,13 +237,21 @@ def add_adoption_notice(request):
|
||||
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=instance, species=species, sex=sex,
|
||||
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 {instance.pk} hinzugefügt")
|
||||
return redirect(reverse("adoption-notice-detail", args=[instance.pk]))
|
||||
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", args=[an_instance.pk]))
|
||||
else:
|
||||
form = AdoptionNoticeFormWithDateWidgetAutoAnimal(in_adoption_notice_creation_flow=True)
|
||||
return render(request, 'fellchensammlung/forms/form_add_adoption.html', {'form': form})
|
||||
@@ -282,7 +304,7 @@ def add_photo_to_animal(request, animal_id):
|
||||
form = ImageForm(in_flow=True)
|
||||
return render(request, 'fellchensammlung/forms/form-image.html', {'form': form})
|
||||
else:
|
||||
return redirect(reverse("animal-detail", args=[animal_id]))
|
||||
return redirect(reverse("adoption-notice-detail", args=[animal.adoption_notice.pk], ))
|
||||
else:
|
||||
form = ImageForm(in_flow=True)
|
||||
return render(request, 'fellchensammlung/forms/form-image.html', {'form': form})
|
||||
@@ -334,7 +356,7 @@ def animal_edit(request, animal_id):
|
||||
"""Log"""
|
||||
Log.objects.create(user=request.user, action="add_photo_to_animal",
|
||||
text=f"{request.user} hat Tier {animal.pk} zum Tier geändert")
|
||||
return redirect(reverse("animal-detail", args=[animal.pk], ))
|
||||
return redirect(reverse("adoption-notice-detail", args=[animal.adoption_notice.pk], ))
|
||||
else:
|
||||
form = AnimalForm(instance=animal)
|
||||
return render(request, 'fellchensammlung/forms/form-adoption-notice.html', context={"form": form})
|
||||
@@ -347,7 +369,7 @@ def about(request):
|
||||
lang = Language.objects.get(languagecode=language_code)
|
||||
|
||||
legal = {}
|
||||
for text_code in ["terms_of_service", "privacy_statement", "imprint"]:
|
||||
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:
|
||||
@@ -421,28 +443,61 @@ def report_detail_success(request, report_id):
|
||||
return report_detail(request, report_id, form_complete=True)
|
||||
|
||||
|
||||
def user_detail(request, user, token=None):
|
||||
context = {"user": user,
|
||||
"adoption_notices": AdoptionNotice.objects.filter(owner=user),
|
||||
"notifications": BaseNotification.objects.filter(user=user, read=False),
|
||||
"search_subscriptions": SearchSubscription.objects.filter(owner=user), }
|
||||
if token is not None:
|
||||
context["token"] = token
|
||||
return render(request, 'fellchensammlung/details/detail-user.html', context=context)
|
||||
|
||||
|
||||
@login_required
|
||||
def user_detail(request, user_id):
|
||||
def user_by_id(request, user_id):
|
||||
user = User.objects.get(id=user_id)
|
||||
# Only users that are mods or owners of the user are allowed to view
|
||||
fail_if_user_not_owner_or_trust_level(request.user, user)
|
||||
if request.method == "POST":
|
||||
if user == request.user:
|
||||
return my_profile(request)
|
||||
else:
|
||||
return user_detail(request, user)
|
||||
|
||||
|
||||
@login_required()
|
||||
def my_profile(request):
|
||||
if request.method == 'POST':
|
||||
if "create_token" in request.POST:
|
||||
Token.objects.create(user=request.user)
|
||||
elif "delete_token" in request.POST:
|
||||
Token.objects.get(user=request.user).delete()
|
||||
elif "toggle_email_notifications" in request.POST:
|
||||
user = request.user
|
||||
user.email_notifications = not user.email_notifications
|
||||
user.save()
|
||||
|
||||
action = request.POST.get("action")
|
||||
if action == "notification_mark_read":
|
||||
notification_id = request.POST.get("notification_id")
|
||||
notification = CommentNotification.objects.get(pk=notification_id)
|
||||
notification.read = True
|
||||
notification.save()
|
||||
try:
|
||||
notification = CommentNotification.objects.get(pk=notification_id)
|
||||
except CommentNotification.DoesNotExist:
|
||||
notification = BaseNotification.objects.get(pk=notification_id)
|
||||
notification.mark_read()
|
||||
elif action == "notification_mark_all_read":
|
||||
notifications = CommentNotification.objects.filter(user=request.user, mark_read=False)
|
||||
for notification in notifications:
|
||||
notification.read = True
|
||||
notification.save()
|
||||
notification.mark_read()
|
||||
elif action == "search_subscription_delete":
|
||||
search_subscription_id = request.POST.get("search_subscription_id")
|
||||
SearchSubscription.objects.get(pk=search_subscription_id).delete()
|
||||
logging.info(f"Deleted subscription {search_subscription_id}")
|
||||
|
||||
context = {"user": user,
|
||||
"adoption_notices": AdoptionNotice.objects.filter(owner=user),
|
||||
"notifications": CommentNotification.objects.filter(user=user, read=False)}
|
||||
return render(request, 'fellchensammlung/details/detail-user.html', context=context)
|
||||
try:
|
||||
token = Token.objects.get(user=request.user)
|
||||
except Token.DoesNotExist:
|
||||
token = None
|
||||
return user_detail(request, request.user, token)
|
||||
|
||||
|
||||
@user_passes_test(user_is_trust_level_or_above)
|
||||
@@ -454,30 +509,31 @@ def modqueue(request):
|
||||
|
||||
@login_required
|
||||
def updatequeue(request):
|
||||
#TODO: Make sure update can only be done for instances with permission
|
||||
if request.method == "POST":
|
||||
print(request.POST.get("adoption_notice_id"))
|
||||
adoption_notice = AdoptionNotice.objects.get(id=request.POST.get("adoption_notice_id"))
|
||||
edit_permission = request.user == adoption_notice.owner or 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")
|
||||
print(f"Action: {action}")
|
||||
if action == "checked_inactive":
|
||||
adoption_notice.set_closed()
|
||||
elif action == "checked_active":
|
||||
print("set checked")
|
||||
adoption_notice.set_checked()
|
||||
if action == "checked_active":
|
||||
adoption_notice.set_active()
|
||||
|
||||
if user_is_trust_level_or_above(request.user, User.MODERATOR):
|
||||
if user_is_trust_level_or_above(request.user, TrustLevel.MODERATOR):
|
||||
last_checked_adoption_list = AdoptionNotice.objects.order_by("last_checked")
|
||||
else:
|
||||
last_checked_adoption_list = AdoptionNotice.objects.filter(owner=request.user).order_by("last_checked")
|
||||
adoption_notices = [adoption for adoption in last_checked_adoption_list if adoption.is_active or adoption.is_to_be_checked]
|
||||
|
||||
context = {"adoption_notices": adoption_notices}
|
||||
adoption_notices_active = [adoption for adoption in last_checked_adoption_list if adoption.is_active]
|
||||
adoption_notices_disabled = [adoption for adoption in last_checked_adoption_list if adoption.is_disabled_unchecked]
|
||||
context = {"adoption_notices_disabled": adoption_notices_disabled,
|
||||
"adoption_notices_active": adoption_notices_active}
|
||||
return render(request, 'fellchensammlung/updatequeue.html', context=context)
|
||||
|
||||
|
||||
def map(request):
|
||||
adoption_notices = AdoptionNotice.objects.all() #TODO: Filter to active
|
||||
adoption_notices = AdoptionNotice.get_active_ANs()
|
||||
context = {"adoption_notices_map": adoption_notices}
|
||||
return render(request, 'fellchensammlung/map.html', context=context)
|
||||
|
||||
@@ -498,6 +554,8 @@ def instance_health_check(request):
|
||||
clean_locations(quiet=False)
|
||||
elif action == "deactivate_unchecked_adoption_notices":
|
||||
deactivate_unchecked_adoption_notices()
|
||||
elif action == "deactivate_404":
|
||||
deactivate_404_adoption_notices()
|
||||
|
||||
number_of_adoption_notices = AdoptionNotice.objects.all().count()
|
||||
none_geocoded_adoption_notices = AdoptionNotice.objects.filter(location__isnull=True)
|
||||
@@ -546,6 +604,31 @@ def external_site_warning(request):
|
||||
context = {"url": url}
|
||||
language_code = translation.get_language()
|
||||
lang = Language.objects.get(languagecode=language_code)
|
||||
Text.get_texts(["external_site_warning", "good_adoption_practices"], language=lang)
|
||||
texts = Text.get_texts(["external_site_warning", "good_adoption_practices"], language=lang)
|
||||
context.update(texts)
|
||||
|
||||
return render(request, 'fellchensammlung/external_site_warning.html', context=context)
|
||||
|
||||
|
||||
def list_rescue_organizations(request):
|
||||
rescue_organizations = RescueOrganization.objects.all()
|
||||
context = {"rescue_organizations": rescue_organizations}
|
||||
return render(request, 'fellchensammlung/animal-shelters.html', context=context)
|
||||
|
||||
|
||||
def detail_view_rescue_organization(request, rescue_organization_id):
|
||||
org = RescueOrganization.objects.get(pk=rescue_organization_id)
|
||||
return render(request, 'fellchensammlung/details/detail-rescue-organization.html',
|
||||
context={"org": org, "map_center": org.position, "zoom_level": 6, "rescue_organizations": [org]})
|
||||
|
||||
|
||||
def export_own_profile(request):
|
||||
user = request.user
|
||||
ANs = AdoptionNotice.objects.filter(owner=user)
|
||||
user_as_json = serialize('json', [user])
|
||||
user_editable = json.loads(user_as_json)
|
||||
user_editable[0]["fields"]["password"] = "Password hash redacted for security reasons"
|
||||
user_as_json = json.dumps(user_editable)
|
||||
ANs_as_json = serialize('json', ANs)
|
||||
full_json = f"{user_as_json}, {ANs_as_json}"
|
||||
return HttpResponse(full_json, content_type="application/json")
|
||||
|
@@ -1,4 +1,4 @@
|
||||
__version__ = "0.3.0"
|
||||
__version__ = "0.4.0"
|
||||
|
||||
# This will make sure the app is always imported when
|
||||
# Django starts so that shared_task will use this app.
|
||||
|
@@ -16,10 +16,14 @@ app.conf.beat_schedule = {
|
||||
'task': 'admin.clean_locations',
|
||||
'schedule': crontab(hour=2),
|
||||
},
|
||||
'daily-deactivation': {
|
||||
'task': 'admin.deactivate_unchecked',
|
||||
'daily-unchecked-deactivation': {
|
||||
'task': 'admin.daily_unchecked_deactivation',
|
||||
'schedule': crontab(hour=1),
|
||||
},
|
||||
'daily-404-deactivation': {
|
||||
'task': 'admin.deactivate_404_adoption_notices',
|
||||
'schedule': crontab(hour=3),
|
||||
},
|
||||
}
|
||||
|
||||
if settings.HEALTHCHECKS_URL is not None and settings.HEALTHCHECKS_URL != "":
|
||||
|
@@ -76,6 +76,7 @@ DB_NAME = config.get("database", "name", fallback="notfellchen.sqlite3")
|
||||
DB_USER = config.get("database", "user", fallback='')
|
||||
DB_PASSWORD = config.get("database", "password", fallback='')
|
||||
DB_HOST = config.get("database", "host", fallback='')
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale')]
|
||||
@@ -89,10 +90,12 @@ HEALTHCHECKS_URL = config.get("monitoring", "healthchecks_url", fallback=None)
|
||||
|
||||
""" GEOCODING """
|
||||
GEOCODING_API_URL = config.get("geocoding", "api_url", fallback="https://nominatim.hyteck.de/search")
|
||||
# GEOCODING_API_FORMAT is allowed to be one of ['nominatim', 'photon']
|
||||
GEOCODING_API_FORMAT = config.get("geocoding", "api_format", fallback="nominatim")
|
||||
|
||||
""" Tile Server """
|
||||
MAP_TILE_SERVER = config.get("map", "tile_server", fallback="https://tiles.hyteck.de")
|
||||
|
||||
|
||||
""" OxiTraffic"""
|
||||
OXITRAFFIC_ENABLED = config.get("tracking", "oxitraffic_enabled", fallback=False)
|
||||
OXITRAFFIC_BASE_URL = config.get("tracking", "oxitraffic_base_url", fallback="")
|
||||
@@ -169,6 +172,9 @@ INSTALLED_APPS = [
|
||||
'crispy_forms',
|
||||
"crispy_bootstrap4",
|
||||
"rest_framework",
|
||||
'rest_framework.authtoken',
|
||||
'drf_spectacular',
|
||||
'drf_spectacular_sidecar', # required for Django collectstatic discovery
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@@ -187,6 +193,8 @@ MIDDLEWARE = [
|
||||
|
||||
ROOT_URLCONF = 'notfellchen.urls'
|
||||
|
||||
SETTINGS_PATH = os.path.dirname(os.path.dirname(__file__))
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
@@ -280,5 +288,16 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
]
|
||||
],
|
||||
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||
}
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
'SWAGGER_UI_DIST': 'SIDECAR', # shorthand to use the sidecar instead
|
||||
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
|
||||
'REDOC_DIST': 'SIDECAR',
|
||||
'TITLE': 'Notfellchen API',
|
||||
'DESCRIPTION': 'Adopt a animal in need',
|
||||
'VERSION': '1.0.0',
|
||||
'SERVE_INCLUDE_SCHEMA': False,
|
||||
}
|
||||
|
@@ -25,9 +25,9 @@ urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
]
|
||||
|
||||
urlpatterns += i18n_patterns (
|
||||
urlpatterns += i18n_patterns(
|
||||
path("", include("fellchensammlung.urls")),
|
||||
prefix_default_language = False
|
||||
prefix_default_language=False
|
||||
)
|
||||
|
||||
if settings.DEBUG:
|
||||
|
@@ -2,5 +2,7 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<p>{% translate "Dein Account ist nun aktiviert. Viel Spaß!" %}</p>
|
||||
<div class="card">
|
||||
<p>{% translate "Dein Account ist nun aktiviert. Viel Spaß!" %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user