Compare commits
627 Commits
8de5f162eb
...
develop
Author | SHA1 | Date | |
---|---|---|---|
2589f1c703 | |||
0edb9094c4 | |||
bc8feba701 | |||
f37d74a7d1 | |||
fa8612ad1a | |||
1d8a054b06 | |||
5898fbf86d | |||
cd1cdd2e0b | |||
c0f920544b | |||
36c90531a8 | |||
7f7c5a3b04 | |||
c084e56ad8 | |||
84acc3c76e | |||
e1f0014898 | |||
05b3a470f3 | |||
ebe060646a | |||
bb412be8d3 | |||
e3c48eac24 | |||
da89cdceda | |||
5a6c2c99e5 | |||
9f53836ce8 | |||
5d53d1a1dc | |||
e00dda1dc2 | |||
a93e0c819f | |||
c87733b37a | |||
9aa964bf05 | |||
dcb1d3ec15 | |||
5d9b8f3213 | |||
d12989d195 | |||
a9f384b50e | |||
afedf2d0bd | |||
a4b8486bd4 | |||
d8bcb8ece6 | |||
b01ac219a3 | |||
42320866c4 | |||
e2e6c14d57 | |||
4761c38cd2 | |||
e2bef3efe2 | |||
bbfd4c3800 | |||
b671d8fbb4 | |||
1ea04e98e8 | |||
c1a7d6790b | |||
f519f78922 | |||
551b5ed6be | |||
20cbb0397a | |||
26f999c4cf | |||
9858dfc1bd | |||
2d1df879dc | |||
2f12dc6a5e | |||
4b3286f12d | |||
d53c5707b8 | |||
ba64661217 | |||
554de17e9e | |||
5bc4b538e6 | |||
8a691d59e7 | |||
e99798ba5c | |||
caa962700b | |||
648466aa70 | |||
25f84bf2ad | |||
ac2147095a | |||
4856c720b1 | |||
ba8ff743f2 | |||
1e827af2dd | |||
7d107abc6a | |||
3bb1cd29cd | |||
0388367c7a | |||
a8843dfc8f | |||
9d1264b6e6 | |||
8df04519b9 | |||
112a731cd6 | |||
8b4ff83921 | |||
2249b615f4 | |||
fbfc800453 | |||
752aaf9b89 | |||
f1a0d5f475 | |||
fb5f38b3e6 | |||
ded5299387 | |||
090548905f | |||
12a89b6927 | |||
fc4c348ff9 | |||
1cd133e335 | |||
b5dc6ca97d | |||
f7b98c9dfe | |||
8e9f4e2b2e | |||
b3faa06c4c | |||
165aeb6dcc | |||
24bae28cec | |||
65182d4c2f | |||
1d879993c9 | |||
f3fbf3ba1d | |||
5a24f32327 | |||
d72bd22f6d | |||
0eb94038f6 | |||
3a69397c0a | |||
d49cb5a783 | |||
c13805dd75 | |||
bcf8a0e6e4 | |||
81f9398da4 | |||
32c90aecd3 | |||
01d6c1e0f6 | |||
607a442e22 | |||
b2a79b3547 | |||
63c692d46b | |||
3d2ef9e735 | |||
aee8b0c1e8 | |||
d5e28ba3d9 | |||
1edaf4df14 | |||
b6d31e3c3b | |||
ed7b55c090 | |||
e66e9ad888 | |||
4ad8b30e04 | |||
ae2ac5c462 | |||
2330542a85 | |||
7363e1ab30 | |||
d2131b2c91 | |||
78866c86cd | |||
f5dbccb9c4 | |||
8ab38cc71b | |||
7dfcbfe38f | |||
cd8471036c | |||
c3ec477a6e | |||
5a6294adf6 | |||
1ba44cdd67 | |||
dfeb88f980 | |||
44a724809f | |||
1acd4be953 | |||
930a5383c4 | |||
eda6da7f12 | |||
448fc395d8 | |||
7e8b665c7c | |||
7be61bc9b8 | |||
6828af76dc | |||
fe92d762be | |||
b91a17e950 | |||
aabc549bcf | |||
4bb4d0386b | |||
a74af5e4d3 | |||
a939d53286 | |||
35c6aae552 | |||
83bd6cf7e6 | |||
17a0cfbde0 | |||
dbcad42da0 | |||
1b48022b63 | |||
7b8e3061d5 | |||
3f27564075 | |||
e7c2746eab | |||
1e243496fb | |||
53bc433aaa | |||
6f5e75a1b3 | |||
f83851b694 | |||
3f5a5dceb5 | |||
f9c7dd8c39 | |||
f3e437dbd1 | |||
5ee1e61eac | |||
abd34ec7cb | |||
b73f6db7b6 | |||
3b9f10dad7 | |||
![]() |
53c0e8b3b8 | ||
![]() |
7d264fe131 | ||
![]() |
c968b39657 | ||
![]() |
ebf116f347 | ||
8227866e7a | |||
6f5e73b533 | |||
a302a36fd4 | |||
1307b2ff7b | |||
d9730e765e | |||
8420c698d4 | |||
14be917d43 | |||
20da09fb96 | |||
19de7d3e4c | |||
41d821b86e | |||
b758b54233 | |||
c650266c6b | |||
dae9bb0916 | |||
2f2371d8df | |||
baaf4b70ac | |||
3b1cd800f6 | |||
0f5f7216ac | |||
c40872379e | |||
897ac5ceef | |||
eb3dbb3e45 | |||
9eb6042ba7 | |||
075833aa25 | |||
f84d800bff | |||
20f814b0ef | |||
4376a63e93 | |||
ee3d316175 | |||
452113b4bf | |||
e9b28ea1c1 | |||
ba07533667 | |||
1ab5c4885e | |||
8c977cf255 | |||
d4c6014e17 | |||
078e5e28cc | |||
7010b4f3d2 | |||
43f38b88ce | |||
7a12a1a4d6 | |||
d784f14c4c | |||
339cdf3ea9 | |||
060be3b486 | |||
9f93a19d51 | |||
c131c07afe | |||
42dbf5c6f7 | |||
2e9039a569 | |||
64b48efafb | |||
37e8dc4bdc | |||
61deb96961 | |||
3a6ce1d38b | |||
82fb73ae59 | |||
4e71c8704f | |||
0c1edf647b | |||
9b38898a8a | |||
25348e45e0 | |||
631c2360e6 | |||
6798cf3477 | |||
cc873d6029 | |||
5d147a4fc9 | |||
640862d8ee | |||
99bb53a7a3 | |||
4f05dc18b9 | |||
2a2df3bf52 | |||
c2b15c2175 | |||
edc27b899e | |||
59d96e36a4 | |||
2c976f926c | |||
671c6ec6f5 | |||
ef9ac58c0f | |||
60e6fdf4e4 | |||
06e6455ba0 | |||
007eb3b5a9 | |||
f3333f2da4 | |||
96b40c5169 | |||
d81408b79c | |||
5ae5e90461 | |||
2534ef3319 | |||
0c2e774891 | |||
895bb3c901 | |||
fca5445aa7 | |||
31d2b85b2f | |||
a8b3214c49 | |||
ccdfd388c4 | |||
cad6acd125 | |||
8dc9c1b9e7 | |||
cd4de2528f | |||
3ef4b98c1c | |||
349917e887 | |||
6c200ba076 | |||
f16aa845d2 | |||
3bccb1e690 | |||
10ae697e33 | |||
baf0d2db72 | |||
b30123a890 | |||
44c34d2daa | |||
e010fa413b | |||
e79aca4efa | |||
037f6529fd | |||
14752d9746 | |||
a8b2bd4e90 | |||
60ae971f14 | |||
1920d72821 | |||
01aa8baadd | |||
f2f526c9de | |||
4d490690e2 | |||
d9c7aa8c49 | |||
040299b90c | |||
0bd321e5ec | |||
c038370602 | |||
c08f7fc792 | |||
4dd35c3866 | |||
8bd041d7ea | |||
d450ad42c0 | |||
d34dcada09 | |||
d8448de419 | |||
35ef6676a2 | |||
e132b1c9f6 | |||
5511d8275c | |||
accf877375 | |||
9ac362fa58 | |||
9253fde2e5 | |||
975c962025 | |||
ba72b4e59f | |||
89e001bd17 | |||
623ca8bc0a | |||
0b483ce630 | |||
16998b85d5 | |||
b55952ac67 | |||
30967dac33 | |||
3166faa7eb | |||
9bba81be22 | |||
18a2d16bf6 | |||
9265cdaea9 | |||
fcb9b60656 | |||
599702f50a | |||
c8453db69d | |||
6ad93abe3b | |||
3c60782ae7 | |||
736f645bf0 | |||
b0887ab731 | |||
ada194122d | |||
b3d1ec142b | |||
e7a8a163f1 | |||
c3ef54a267 | |||
da3b43a713 | |||
8cfddd7882 | |||
80eafbb014 | |||
cc2a659767 | |||
cacfeff3fe | |||
9379728b71 | |||
d30d15c0d4 | |||
5343f53661 | |||
3126b2b962 | |||
43c671018b | |||
7a37377a09 | |||
19d9dea8b1 | |||
c50e0b18b5 | |||
4c07c0feb2 | |||
cf15b60bef | |||
328f64aa51 | |||
fdf4e79a69 | |||
bbc8732112 | |||
17dbe85219 | |||
3dc011a22c | |||
f5b89456ab | |||
2e4f63b250 | |||
3b261ff240 | |||
f06b00fb9d | |||
88987a973e | |||
93ffbe09af | |||
e11848ea72 | |||
8bc9d12bfa | |||
1dbfdccb89 | |||
f085f5dcf5 | |||
33579e8446 | |||
a852da365f | |||
b53095ae17 | |||
3d7780e0ba | |||
478636bd98 | |||
d9ebee1e07 | |||
23e154bce6 | |||
5624f59258 | |||
56df942dd0 | |||
2dcb5fbf88 | |||
7a84b470f9 | |||
76232b7a0f | |||
349af16075 | |||
8641bead80 | |||
eb930b71d6 | |||
ae4ba06abf | |||
a2e237a81f | |||
f90c8c7e8c | |||
c316c74aff | |||
93dd0ae4f6 | |||
f79bb355cf | |||
45a534a042 | |||
2106a3423f | |||
d3f7274e92 | |||
5f576896b7 | |||
4a3cbfb8b0 | |||
3e93fe1a7a | |||
965e055ef1 | |||
13a0da6e46 | |||
1bb05dbf1c | |||
4c9c1e13a5 | |||
99cde15966 | |||
f2edc23e75 | |||
8aab4a13ae | |||
226102ccaf | |||
3d088c55d7 | |||
bb14a346cb | |||
f387930dee | |||
fe63e3b25c | |||
23adeb06e6 | |||
c1bd458c80 | |||
2a1d4178d7 | |||
f9a37b299d | |||
9950e87501 | |||
eff1ba6513 | |||
bb085aa9a8 | |||
b0dc0f9d78 | |||
d1a51b019c | |||
b7fade55fb | |||
79461518a3 | |||
8059d5d23f | |||
3098eacfb4 | |||
f3d1e1c203 | |||
e6a985ddfa | |||
388cc327be | |||
13adc695f6 | |||
f2c7943247 | |||
112fd52864 | |||
8279385966 | |||
1a9692949f | |||
e7af49b309 | |||
b822914db3 | |||
9ad33efe08 | |||
bd8f9fc1b7 | |||
4a2c18be4d | |||
479aba0195 | |||
1299fcac84 | |||
884a07f87b | |||
6557e9f9eb | |||
602cef1302 | |||
b400db603a | |||
0397311f6e | |||
abce89c829 | |||
bbad63a460 | |||
d940630086 | |||
37ecf28f2f | |||
12d5a976cc | |||
9086e2e75b | |||
3607eb0e4e | |||
3daf83d725 | |||
5ad0cb74cc | |||
9ae64e8cb1 | |||
1b5a0c71e0 | |||
4d4f11c479 | |||
835c89d1d4 | |||
46bf07dd8d | |||
f557672586 | |||
4e27e1be7f | |||
6d390ad21e | |||
2f2543160e | |||
64a9db133e | |||
712c3d32f3 | |||
8998bbdf6d | |||
ff31caa139 | |||
ad06829c31 | |||
03a48da355 | |||
885bed888d | |||
0051cb07c9 | |||
8858cff9cf | |||
70e2af6172 | |||
461abd2e46 | |||
![]() |
d7269106db | ||
77fb99a527 | |||
38a56daa24 | |||
![]() |
ac0749797f | ||
f193f7d7ca | |||
43657e0862 | |||
68ad366f74 | |||
350d2c5da9 | |||
462bb8f485 | |||
ea4d15b99a | |||
de30dfcb8b | |||
36a979954c | |||
71ef17dc97 | |||
206cd282e6 | |||
e399346c3e | |||
929c6dfff0 | |||
841b57fea2 | |||
9e5446ff1d | |||
3b79809b8c | |||
53e6db3655 | |||
424f91e919 | |||
84ce5f54b2 | |||
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 |
4
.coveragerc
Normal file
@@ -0,0 +1,4 @@
|
||||
[run]
|
||||
omit =
|
||||
*/migrations/*
|
||||
*/tests/*
|
9
.gitignore
vendored
@@ -3,10 +3,16 @@
|
||||
# Database
|
||||
notfellchen
|
||||
|
||||
# Geojson from imports
|
||||
*.geojson
|
||||
|
||||
# Media storage
|
||||
static
|
||||
/static
|
||||
media
|
||||
|
||||
# Compiled CSS
|
||||
/src/fellchensammlung/static/fellchensammlung/css/main.css
|
||||
/src/fellchensammlung/static/fellchensammlung/css/main.css.map
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
@@ -161,3 +167,4 @@ dmypy.json
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
/node_modules/
|
||||
|
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/)
|
@@ -1,6 +1,6 @@
|
||||
FROM python:3.11-slim
|
||||
# Use 3.11 to avoid django.core.exceptions.ImproperlyConfigured: Error loading psycopg2 or psycopg module
|
||||
MAINTAINER Julian-Samuel Gebühr
|
||||
LABEL org.opencontainers.image.authors="Julian-Samuel Gebühr"
|
||||
|
||||
ENV DOCKER_BUILD=true
|
||||
|
||||
|
90
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
[notfellchen.org](https://notfellchen.org) ist eine Sammelstelle für Tier-Vermittlungen. Die Idee entstand, da in der
|
||||
deutschsprachigen Rattencommunity ein wilder Mix aus Websites, Foren und Facebookgruppen besteht die Ratten vermitteln.
|
||||
Diese Website soll die bestehende Communities NICHT ersetzten, jedoch ermöglichen, dass Menschen die Ratten aufnehmen
|
||||
Diese Website soll die bestehende Communitys NICHT ersetzten, jedoch ermöglichen, dass Menschen die Ratten aufnehmen
|
||||
wollen Informationen einfach finden und nicht bereits in jeder Gruppe sein müssen.
|
||||
|
||||
Wir nehmen Angebote auf die
|
||||
@@ -57,10 +57,50 @@ Therefore, a solution is used where a number of predefined texts per site are su
|
||||
|
||||
# Developer Notes
|
||||
|
||||
## Getting started
|
||||
|
||||
### Clone the project
|
||||
|
||||
```
|
||||
git clone https://codeberg.org/moanos/notfellchen.git
|
||||
```
|
||||
|
||||
### Install dependencies
|
||||
```
|
||||
pip install -e '.[all]'
|
||||
```
|
||||
|
||||
### Create the database
|
||||
|
||||
```
|
||||
nf migrate
|
||||
```
|
||||
|
||||
Because of a wired bug the initial migrations must run two times as the first time the permissions
|
||||
for `create_active_adoption_notice` are created but can not yet be accessed and on the second time this permission will
|
||||
be added to groups.
|
||||
|
||||
### Start the server
|
||||
|
||||
```
|
||||
nf runserver
|
||||
```
|
||||
|
||||
### Build the docs
|
||||
|
||||
```
|
||||
sphinx-autobuild ./docs ./docs/_build/html
|
||||
```
|
||||
|
||||
|
||||
## Styling
|
||||
|
||||
Bulma is used for styling, including related SCSS. All styles should eventually be migrated to SCSS.
|
||||
|
||||
Use `npm run build-bulma` to generate the css file from SCSS.
|
||||
You can use `npm start` during development so that the file is re-generated upon change.
|
||||
|
||||
|
||||
## Docker
|
||||
|
||||
Build latest image
|
||||
@@ -77,20 +117,37 @@ 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
|
||||
|
||||
@@ -125,3 +182,20 @@ Start beat
|
||||
```zsh
|
||||
celery -A notfellchen.celery beat
|
||||
```
|
||||
|
||||
# Contributing
|
||||
|
||||
This project is currently mainly 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
|
||||
|
||||
* UI improvements: Since a major redesign I'm much happier but the UI could use many, many little tweaks
|
||||
* 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 documentation) 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,94 @@ 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/
|
||||
|
||||
For example: You can check all current adoption notices here: https://notfellchen.org/api/adoption_notice
|
||||
|
||||
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}}'
|
||||
|
@@ -67,5 +67,6 @@ Healthchecks
|
||||
You can configure notfellchen to give a hourly ping to a healthchecks server. If this ping is not received, you will get notified and cna check why the celery jobs are no running.
|
||||
Add the following to your `notfellchen.cfg` and adjust the URL to match your check.
|
||||
.. code::
|
||||
|
||||
[monitoring]
|
||||
healthchecks_url=https://health.example.org/ping/5fa7c9b2-753a-4cb3-bcc9-f982f5bc68e8
|
||||
|
@@ -5,7 +5,7 @@ Report a bug
|
||||
^^^^^^^^^^^^
|
||||
|
||||
To report a bug, file an issue on `Github
|
||||
<https://codeberg.org/moanos/notfellchen/issues>`_
|
||||
<https://github.com/moan0s/notfellchen/issues>`_
|
||||
|
||||
Try to include the following information:
|
||||
|
||||
@@ -29,7 +29,7 @@ To contribute simply clone the directory, make your changes and file a
|
||||
pull request.
|
||||
|
||||
If you want to know what can be done, have a look at the current `Github
|
||||
<https://codeberg.org/moanos/notfellchen/issues>`_.
|
||||
<https://github.com/moan0s/notfellchen/issues>`_.
|
||||
|
||||
Get in touch!
|
||||
^^^^^^^^^^^^^
|
||||
|
@@ -5,8 +5,7 @@ What qualifies as release?
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
A new release should be announced when a significant number functions, bugfixes or other improvements to the software
|
||||
is made. Usually this indicates a minor release.
|
||||
Major releases are yet to be determined.
|
||||
is made. Notfellchen follows `Semantic Versioning <https://semver.org/>`_.
|
||||
|
||||
What should be done before a release?
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -14,7 +13,7 @@ What should be done before a release?
|
||||
Tested basic functions
|
||||
######################
|
||||
|
||||
Run :command:`pytest`
|
||||
Run :command:`nf test src`
|
||||
|
||||
Test upgrade on a copy of a production database
|
||||
###############################################
|
||||
@@ -38,4 +37,4 @@ Do a final commit on this change, and tag the commit as release with appropriate
|
||||
git tag -a v1.0.0 -m "Releasing version v1.0.0"
|
||||
git push origin v1.0.0
|
||||
|
||||
Make sure the tag is visible on Codeberg and celebrate 🥳
|
||||
Make sure the tag is visible on GitHub/Codeberg and celebrate 🥳
|
||||
|
@@ -18,10 +18,13 @@ media=./media
|
||||
static=./static
|
||||
|
||||
[mail]
|
||||
console-only=true
|
||||
console_only=true
|
||||
|
||||
[logging]
|
||||
app_log_level=INFO
|
||||
django_log_level=INFO
|
||||
|
||||
[geocoding]
|
||||
api_url=https://photon.hyteck.de/api
|
||||
api_format=photon
|
||||
|
||||
|
497
package-lock.json
generated
Normal file
@@ -0,0 +1,497 @@
|
||||
{
|
||||
"name": "notfellchen",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"bulma": "^1.0.4",
|
||||
"sass": "^1.89.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^1.0.3",
|
||||
"is-glob": "^4.0.3",
|
||||
"micromatch": "^4.0.5",
|
||||
"node-addon-api": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher-android-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||
"@parcel/watcher-freebsd-x64": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||
"@parcel/watcher-win32-ia32": "2.5.1",
|
||||
"@parcel/watcher-win32-x64": "2.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-android-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-darwin-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-freebsd-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
||||
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-linux-x64-musl": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
||||
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-arm64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
|
||||
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-ia32": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
|
||||
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@parcel/watcher-win32-x64": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
|
||||
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/bulma": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.4.tgz",
|
||||
"integrity": "sha512-Ffb6YGXDiZYX3cqvSbHWqQ8+LkX6tVoTcZuVB3lm93sbAVXlO0D6QlOTMnV6g18gILpAXqkG2z9hf9z4hCjz2g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.16.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"detect-libc": "bin/detect-libc.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
|
||||
"integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.18.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.89.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz",
|
||||
"integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@parcel/watcher": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"bulma": "^1.0.4",
|
||||
"sass": "^1.89.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build-bulma": "sass --load-path=node_modules src/fellchensammlung/static/fellchensammlung/css/main.scss src/fellchensammlung/static/fellchensammlung/css/main.css --style compressed",
|
||||
"start": "npm run build-bulma -- --watch"
|
||||
}
|
||||
}
|
@@ -6,15 +6,15 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "notfellchen"
|
||||
description = "A tool to help."
|
||||
description = "A website to help animals to find a loving home. It features organized input of adoption notices and related animals including automated lifecycle, location-based search, roles, and support for easy checking of rescue organizations."
|
||||
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"}
|
||||
keywords = ["animal", "adoption", "django", "rescue", "rats" ]
|
||||
license = { text = "AGPL-3.0-or-later" }
|
||||
classifiers = [
|
||||
"Environment :: Web",
|
||||
"License :: OSI Approved :: GNU Affero General Public License v3",
|
||||
@@ -24,14 +24,10 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"Django",
|
||||
"coverage",
|
||||
"codecov",
|
||||
"sphinx",
|
||||
"sphinx-rtd-theme",
|
||||
"gunicorn",
|
||||
"fontawesomefree",
|
||||
"whitenoise",
|
||||
"model_bakery",
|
||||
"markdown",
|
||||
"Pillow",
|
||||
"django-registration",
|
||||
@@ -39,14 +35,24 @@ dependencies = [
|
||||
"django-crispy-forms",
|
||||
"crispy-bootstrap4",
|
||||
"djangorestframework",
|
||||
"celery[redis]"
|
||||
"celery[redis]",
|
||||
"drf-spectacular[sidecar]",
|
||||
"django-widget-tweaks",
|
||||
"django-super-deduper"
|
||||
]
|
||||
|
||||
dynamic = ["version", "readme"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
develop = [
|
||||
"pytest",
|
||||
"pytest",
|
||||
"coverage",
|
||||
"model_bakery",
|
||||
]
|
||||
docs = [
|
||||
"sphinx",
|
||||
"sphinx-rtd-theme",
|
||||
"sphinx-autobuild"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -61,6 +67,6 @@ nf = 'notfellchen.main:main'
|
||||
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "notfellchen.__version__"}
|
||||
readme = {file = "README.md"}
|
||||
version = { attr = "notfellchen.__version__" }
|
||||
readme = { file = "README.md" }
|
||||
|
||||
|
268
scripts/upload_animal_shelters.py
Normal file
@@ -0,0 +1,268 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from types import SimpleNamespace
|
||||
|
||||
import requests
|
||||
# TODO: consider using OSMPythonTools instead of requests or overpass library
|
||||
from osmtogeojson import osmtogeojson
|
||||
from tqdm import tqdm
|
||||
|
||||
DEFAULT_OSM_DATA_FILE = "export.geojson"
|
||||
# Search area must be the official name, e.g. "Germany" is not a valid area name in Overpass API
|
||||
# Consider instead finding & using the code within the query itself, e.g. "ISO3166-1"="DE"
|
||||
DEFAULT_OVERPASS_SEARCH_AREA = "Deutschland"
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse command-line arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Download animal shelter data from the Overpass API to the Notfellchen API.")
|
||||
parser.add_argument("--api-token", type=str, help="API token for authentication.")
|
||||
parser.add_argument("--area", type=str, help="Area to search for animal shelters (default: Deutschland).")
|
||||
parser.add_argument("--instance", type=str, help="API instance URL.")
|
||||
parser.add_argument("--data-file", type=str, help="Path to the GeoJSON file containing (only) animal shelters.")
|
||||
parser.add_argument("--use-cached", action='store_true', help="Use the stored GeoJSON file")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def get_config():
|
||||
"""Get configuration from environment variables or command-line arguments."""
|
||||
args = parse_args()
|
||||
|
||||
api_token = args.api_token or os.getenv("NOTFELLCHEN_API_TOKEN")
|
||||
# TODO: document new environment variable NOTFELLCHEN_AREA
|
||||
area = args.area or os.getenv("NOTFELLCHEN_AREA", DEFAULT_OVERPASS_SEARCH_AREA)
|
||||
instance = args.instance or os.getenv("NOTFELLCHEN_INSTANCE")
|
||||
data_file = args.data_file or os.getenv("NOTFELLCHEN_DATA_FILE", DEFAULT_OSM_DATA_FILE)
|
||||
use_cached = args.use_cached or os.getenv("NOTFELLCHEN_USE_CACHED", False)
|
||||
|
||||
if not api_token or not instance:
|
||||
raise ValueError("API token and instance URL must be provided via environment variables or CLI arguments.")
|
||||
|
||||
return api_token, area, instance, data_file, use_cached
|
||||
|
||||
|
||||
def get_or_none(data, key):
|
||||
if key in data["properties"].keys():
|
||||
return data["properties"][key]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_or_empty(data, key):
|
||||
if key in data["properties"].keys():
|
||||
return data["properties"][key]
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def choose(keys, data, replace=False):
|
||||
for key in keys:
|
||||
if key in data.keys():
|
||||
if replace:
|
||||
return data[key].replace(" ", "").replace("-", "").replace("(", "").replace(")", "")
|
||||
else:
|
||||
return data[key]
|
||||
return None
|
||||
|
||||
|
||||
def add(value, platform):
|
||||
if value != "":
|
||||
if value.find(platform) == -1:
|
||||
return f"https://www.{platform}.com/{value}"
|
||||
else:
|
||||
return value
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def https(value):
|
||||
if value is not None and value != "":
|
||||
value = value.replace("http://", "")
|
||||
if value.find("https") == -1:
|
||||
return f"https://{value}"
|
||||
else:
|
||||
return value
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def calc_coordinate_center(coordinates):
|
||||
"""
|
||||
Calculates the center as the arithmetic mean of the list of coordinates.
|
||||
|
||||
Not perfect because earth is a sphere (citation needed) but good enough.
|
||||
"""
|
||||
if not coordinates:
|
||||
return None, None
|
||||
|
||||
lon_sum = 0.0
|
||||
lat_sum = 0.0
|
||||
count = 0
|
||||
|
||||
for lon, lat in coordinates:
|
||||
lon_sum += lon
|
||||
lat_sum += lat
|
||||
count += 1
|
||||
|
||||
return lon_sum / count, lat_sum / count
|
||||
|
||||
|
||||
def get_center_coordinates(geometry):
|
||||
"""
|
||||
Given a GeoJSON geometry dict, return (longitude, latitude)
|
||||
|
||||
If a shape, calculate the center, else reurn the point
|
||||
"""
|
||||
geom_type = geometry["type"]
|
||||
coordinates = geometry["coordinates"]
|
||||
|
||||
if geom_type == "Point":
|
||||
return coordinates[0], coordinates[1]
|
||||
|
||||
elif geom_type == "LineString":
|
||||
return calc_coordinate_center(coordinates)
|
||||
|
||||
elif geom_type == "Polygon":
|
||||
outer_ring = coordinates[0]
|
||||
return calc_coordinate_center(outer_ring)
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unsupported geometry type: {geom_type}")
|
||||
|
||||
|
||||
# TODO: take note of new get_overpass_result function which does the bulk of the new overpass query work
|
||||
def get_overpass_result(area, data_file):
|
||||
"""Build the Overpass query for fetching animal shelters in the specified area."""
|
||||
overpass_endpoint = "https://overpass-api.de/api/interpreter"
|
||||
overpass_query = f"""
|
||||
[out:json][timeout:25];
|
||||
area[name="{area}"]->.searchArea;
|
||||
nwr["amenity"="animal_shelter"](area.searchArea);
|
||||
out body;
|
||||
>;
|
||||
out skel qt;
|
||||
"""
|
||||
r = requests.get(overpass_endpoint, params={'data': overpass_query})
|
||||
if r.status_code == 200:
|
||||
rjson = r.json()
|
||||
result = osmtogeojson.process_osm_json(rjson)
|
||||
with open(data_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(result, f, ensure_ascii=False)
|
||||
return result
|
||||
|
||||
|
||||
def add_if_available(base_data, keys, result):
|
||||
# Loads the data into the org if available
|
||||
for key in keys:
|
||||
if getattr(base_data, key) is not None:
|
||||
result[key] = getattr(base_data, key)
|
||||
return result
|
||||
|
||||
|
||||
def create_location(tierheim, instance, headers):
|
||||
location_data = {
|
||||
"place_id": tierheim["id"],
|
||||
"longitude": get_center_coordinates(tierheim["geometry"])[0],
|
||||
"latitude": get_center_coordinates(tierheim["geometry"])[1],
|
||||
"name": tierheim["properties"]["name"],
|
||||
"city": tierheim["properties"]["addr:city"],
|
||||
"housenumber": get_or_empty(tierheim, "addr:housenumber"),
|
||||
"postcode": get_or_empty(tierheim, "addr:postcode"),
|
||||
"street": get_or_empty(tierheim, "addr:street"),
|
||||
"countrycode": get_or_empty(tierheim, "addr:country"),
|
||||
}
|
||||
|
||||
location_result = requests.post(f"{instance}/api/locations/", json=location_data, headers=headers)
|
||||
|
||||
if location_result.status_code != 201:
|
||||
try:
|
||||
print(
|
||||
f"Location for {tierheim["properties"]["name"]}:{location_result.status_code} {location_result.json()} not created")
|
||||
except requests.exceptions.JSONDecodeError:
|
||||
print(f"Location for {tierheim["properties"]["name"]} could not be created")
|
||||
exit()
|
||||
|
||||
return location_result.json()
|
||||
|
||||
|
||||
def main():
|
||||
api_token, area, instance, data_file, use_cached = get_config()
|
||||
if not use_cached:
|
||||
# Query shelters
|
||||
overpass_result = get_overpass_result(area, data_file)
|
||||
if overpass_result is None:
|
||||
print("Error: get_overpass_result returned None")
|
||||
return
|
||||
else:
|
||||
with open(data_file, 'r', encoding='utf-8') as f:
|
||||
overpass_result = json.load(f)
|
||||
|
||||
# Set headers and endpoint
|
||||
endpoint = f"{instance}/api/organizations/"
|
||||
h = {'Authorization': f'Token {api_token}', "content-type": "application/json"}
|
||||
|
||||
tierheime = overpass_result["features"]
|
||||
|
||||
for idx, tierheim in enumerate(tqdm(tierheime)):
|
||||
# Check if data is low quality
|
||||
if "name" not in tierheim["properties"].keys() or "addr:city" not in tierheim["properties"].keys():
|
||||
continue
|
||||
|
||||
# Load TH data in for easier accessing
|
||||
th_data = SimpleNamespace(
|
||||
name=tierheim["properties"]["name"],
|
||||
email=choose(("contact:email", "email"), tierheim["properties"]),
|
||||
phone_number=choose(("contact:phone", "phone"), tierheim["properties"], replace=True),
|
||||
fediverse_profile=get_or_none(tierheim, "contact:mastodon"),
|
||||
facebook=https(add(get_or_empty(tierheim, "contact:facebook"), "facebook")),
|
||||
instagram=https(add(get_or_empty(tierheim, "contact:instagram"), "instagram")),
|
||||
website=https(choose(("contact:website", "website"), tierheim["properties"])),
|
||||
description=get_or_none(tierheim, "opening_hours"),
|
||||
external_object_identifier=tierheim["id"],
|
||||
EXTERNAL_SOURCE_IDENTIFIER="OSM",
|
||||
)
|
||||
|
||||
# Define here for later
|
||||
optional_data = ["email", "phone_number", "website", "description", "fediverse_profile", "facebook",
|
||||
"instagram"]
|
||||
|
||||
# Check if rescue organization exits
|
||||
search_data = {"external_source_identifier": "OSM",
|
||||
"external_object_identifier": f"{tierheim["id"]}"}
|
||||
search_result = requests.get(f"{instance}/api/organizations", params=search_data, headers=h)
|
||||
if search_result.status_code == 200:
|
||||
org_id = search_result.json()[0]["id"]
|
||||
logging.debug(f"{th_data.name} already exists as ID {org_id}.")
|
||||
org_patch_data = {"id": org_id,
|
||||
"name": th_data.name}
|
||||
if search_result.json()[0]["location"] is None:
|
||||
location = create_location(tierheim, instance, h)
|
||||
org_patch_data["location"] = location["id"]
|
||||
|
||||
org_patch_data = add_if_available(th_data, optional_data, org_patch_data)
|
||||
|
||||
result = requests.patch(endpoint, json=org_patch_data, headers=h)
|
||||
if result.status_code != 200:
|
||||
logging.warning(f"Updating {tierheim['properties']['name']} failed:{result.status_code} {result.json()}")
|
||||
continue
|
||||
else:
|
||||
location = create_location(tierheim, instance, h)
|
||||
org_data = {"name": tierheim["properties"]["name"],
|
||||
"external_object_identifier": f"{tierheim["id"]}",
|
||||
"external_source_identifier": "OSM",
|
||||
"location": location["id"]
|
||||
}
|
||||
|
||||
org_data = add_if_available(th_data, optional_data, org_data)
|
||||
|
||||
result = requests.post(endpoint, json=org_data, headers=h)
|
||||
|
||||
if result.status_code != 201:
|
||||
print(f"{idx} {tierheim["properties"]["name"]}:{result.status_code} {result.json()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@@ -1,15 +1,17 @@
|
||||
import csv
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin import EmptyFieldListFilter
|
||||
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
|
||||
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
|
||||
SpeciesSpecificURL, ImportantLocation, SocialMediaPost
|
||||
|
||||
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
|
||||
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions
|
||||
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, Notification
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
@@ -24,6 +26,13 @@ class AdoptionNoticeAdmin(admin.ModelAdmin):
|
||||
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
|
||||
@@ -59,6 +68,7 @@ class UserAdmin(admin.ModelAdmin):
|
||||
|
||||
export_as_csv.short_description = _("Ausgewählte User exportieren")
|
||||
|
||||
|
||||
def _reported_content_link(obj):
|
||||
reported_content = obj.reported_content
|
||||
return format_html(f'<a href="{reported_content.get_absolute_url}">{reported_content}</a>')
|
||||
@@ -86,24 +96,78 @@ 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__icontains",)
|
||||
search_fields = ("name", "description", "internal_comment", "location_string", "location__city")
|
||||
list_display = ("name", "trusted", "allows_using_materials", "website")
|
||||
list_filter = ("allows_using_materials", "trusted",)
|
||||
list_filter = ("allows_using_materials", "trusted", ("external_source_identifier", EmptyFieldListFilter))
|
||||
|
||||
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(Notification)
|
||||
class BaseNotificationAdmin(admin.ModelAdmin):
|
||||
list_filter = ("user_to_notify", "read")
|
||||
|
||||
|
||||
@admin.register(SearchSubscription)
|
||||
class SearchSubscriptionAdmin(admin.ModelAdmin):
|
||||
list_filter = ("owner",)
|
||||
|
||||
|
||||
class ImportantLocationInline(admin.StackedInline):
|
||||
model = ImportantLocation
|
||||
|
||||
|
||||
class IsImportantListFilter(admin.SimpleListFilter):
|
||||
# See https://docs.djangoproject.com/en/5.1/ref/contrib/admin/filters/#modeladmin-list-filters
|
||||
title = _('Is Important Location?')
|
||||
|
||||
parameter_name = 'important'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
('is_important', _('Important Location')),
|
||||
('is_normal', _('Normal Location')),
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() == 'is_important':
|
||||
return queryset.filter(importantlocation__isnull=False)
|
||||
else:
|
||||
return queryset.filter(importantlocation__isnull=True)
|
||||
|
||||
|
||||
@admin.register(Location)
|
||||
class LocationAdmin(admin.ModelAdmin):
|
||||
search_fields = ("name__icontains", "city__icontains")
|
||||
list_filter = [IsImportantListFilter]
|
||||
inlines = [
|
||||
ImportantLocationInline,
|
||||
]
|
||||
|
||||
@admin.register(SocialMediaPost)
|
||||
class SocialMediaPostAdmin(admin.ModelAdmin):
|
||||
list_filter = ("platform",)
|
||||
|
||||
admin.site.register(Animal)
|
||||
admin.site.register(Species)
|
||||
admin.site.register(Location)
|
||||
admin.site.register(Rule)
|
||||
admin.site.register(Image)
|
||||
admin.site.register(ModerationAction)
|
||||
|
33
src/fellchensammlung/api/renderers.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from rest_framework.renderers import BaseRenderer
|
||||
import json
|
||||
|
||||
|
||||
class GeoJSONRenderer(BaseRenderer):
|
||||
media_type = 'application/json'
|
||||
format = 'geojson'
|
||||
charset = 'utf-8'
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
features = []
|
||||
for item in data:
|
||||
coords = item["coordinates"]
|
||||
if coords:
|
||||
feature = {
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": coords
|
||||
},
|
||||
"properties": {
|
||||
k: v for k, v in item.items()
|
||||
},
|
||||
"id": f"{item['id']}"
|
||||
}
|
||||
features.append(feature)
|
||||
|
||||
geojson = {
|
||||
"type": "FeatureCollection",
|
||||
"generator": "notfellchen",
|
||||
"features": features
|
||||
}
|
||||
return json.dumps(geojson)
|
@@ -1,10 +1,161 @@
|
||||
from ..models import AdoptionNotice
|
||||
from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image, Location
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
import math
|
||||
|
||||
|
||||
class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
|
||||
location = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
location_details = serializers.StringRelatedField(source='location', read_only=True)
|
||||
organization = serializers.PrimaryKeyRelatedField(
|
||||
queryset=RescueOrganization.objects.all(),
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
organization = serializers.PrimaryKeyRelatedField(
|
||||
queryset=RescueOrganization.objects.all(),
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
photos = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Image.objects.all(),
|
||||
many=True,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = AdoptionNotice
|
||||
fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information", "group_only"]
|
||||
fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information",
|
||||
"group_only", "location", "location_details", "organization", "photos"]
|
||||
|
||||
|
||||
class AdoptionNoticeGeoJSONSerializer(serializers.ModelSerializer):
|
||||
species = serializers.SerializerMethodField()
|
||||
title = serializers.CharField(source='name')
|
||||
url = serializers.SerializerMethodField()
|
||||
location_hr = serializers.SerializerMethodField()
|
||||
coordinates = serializers.SerializerMethodField()
|
||||
image_url = serializers.SerializerMethodField()
|
||||
image_alt = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = AdoptionNotice
|
||||
fields = ('id', 'species', 'title', 'description', 'url', 'location_hr', 'coordinates', 'image_url',
|
||||
'image_alt')
|
||||
|
||||
def get_species(self, obj):
|
||||
return "rat"
|
||||
|
||||
def get_url(self, obj):
|
||||
return obj.get_absolute_url()
|
||||
|
||||
def get_image_url(self, obj):
|
||||
photo = obj.get_photo()
|
||||
if photo is not None:
|
||||
return obj.get_photo().image.url
|
||||
return None
|
||||
|
||||
def get_image_alt(self, obj):
|
||||
photo = obj.get_photo()
|
||||
if photo is not None:
|
||||
return obj.get_photo().alt_text
|
||||
return None
|
||||
|
||||
def get_coordinates(self, obj):
|
||||
"""
|
||||
Coordinates are randomly moved around real location, roughly in a circle. The object id is used as angle so that
|
||||
points are always displayed at the same location (as if they were a seed for a random function).
|
||||
|
||||
It's not exactly a circle, because the earth is round.
|
||||
"""
|
||||
if obj.location:
|
||||
longitude_addition = math.sin(obj.id) / 2000
|
||||
latitude_addition = math.cos(obj.id) / 2000
|
||||
return [obj.location.longitude + longitude_addition, obj.location.latitude + latitude_addition]
|
||||
return None
|
||||
|
||||
def get_location_hr(self, obj):
|
||||
if obj.location:
|
||||
return f"{obj.location}"
|
||||
return None
|
||||
|
||||
|
||||
class RescueOrgeGeoJSONSerializer(serializers.ModelSerializer):
|
||||
name = serializers.CharField()
|
||||
url = serializers.SerializerMethodField()
|
||||
location_hr = serializers.SerializerMethodField()
|
||||
coordinates = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = AdoptionNotice
|
||||
fields = ('id', 'name', 'description', 'url', 'location_hr', 'coordinates')
|
||||
|
||||
def get_url(self, obj):
|
||||
return obj.get_absolute_url()
|
||||
|
||||
def get_coordinates(self, obj):
|
||||
"""
|
||||
Coordinates are randomly moved around real location, roughly in a circle. The object id is used as angle so that
|
||||
points are always displayed at the same location (as if they were a seed for a random function).
|
||||
|
||||
It's not exactly a circle, because the earth is round.
|
||||
"""
|
||||
if obj.location:
|
||||
return [obj.location.longitude, obj.location.latitude]
|
||||
return None
|
||||
|
||||
def get_location_hr(self, obj):
|
||||
if obj.location.city:
|
||||
return f"{obj.location.city}"
|
||||
elif obj.location:
|
||||
return f"{obj.location}"
|
||||
return None
|
||||
|
||||
|
||||
class AnimalCreateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Animal
|
||||
fields = ["name", "date_of_birth", "description", "species", "sex", "adoption_notice"]
|
||||
|
||||
|
||||
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__"
|
||||
|
||||
|
||||
class LocationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = "__all__"
|
||||
|
@@ -1,8 +1,20 @@
|
||||
from django.urls import path
|
||||
from .views import (
|
||||
AdoptionNoticeApiView
|
||||
AdoptionNoticeApiView,
|
||||
AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView, LocationApiView,
|
||||
AdoptionNoticeGeoJSONView, RescueOrgGeoJSONView
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('adoption_notice', AdoptionNoticeApiView.as_view()),
|
||||
path("adoption_notice", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-list"),
|
||||
path("adoption_notice.geojson", AdoptionNoticeGeoJSONView.as_view(), name="api-adoption-notice-list-geojson"),
|
||||
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.geojson", RescueOrgGeoJSONView.as_view(), name="api-organization-list-geojson"),
|
||||
path("organizations/<int:id>/", RescueOrganizationApiView.as_view(), name="api-organization-detail"),
|
||||
path("images/", AddImageApiView.as_view(), name="api-add-image"),
|
||||
path("species/", SpeciesApiView.as_view(), name="api-species-list"),
|
||||
path("locations/", LocationApiView.as_view(), name="api-locations-list"),
|
||||
]
|
||||
|
@@ -1,37 +1,385 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Q
|
||||
from rest_framework.generics import ListAPIView
|
||||
|
||||
from fellchensammlung.api.serializers import LocationSerializer, AdoptionNoticeGeoJSONSerializer
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework import permissions
|
||||
from ..models import AdoptionNotice
|
||||
from .serializers import AdoptionNoticeSerializer
|
||||
from django.db import transaction
|
||||
from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel, Location, AdoptionNoticeStatus
|
||||
from fellchensammlung.tasks import post_adoption_notice_save, post_rescue_org_save
|
||||
from rest_framework import status, serializers
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from .renderers import GeoJSONRenderer
|
||||
from .serializers import (
|
||||
AnimalGetSerializer,
|
||||
AnimalCreateSerializer,
|
||||
RescueOrgeGeoJSONSerializer,
|
||||
AdoptionNoticeSerializer,
|
||||
ImageCreateSerializer,
|
||||
SpeciesSerializer, RescueOrganizationSerializer,
|
||||
)
|
||||
from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image
|
||||
from drf_spectacular.utils import extend_schema, inline_serializer
|
||||
|
||||
|
||||
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)
|
||||
"""
|
||||
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_to_notify)
|
||||
|
||||
# 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_to_notify.trust_level >= TrustLevel.MODERATOR:
|
||||
adoption_notice.set_active()
|
||||
else:
|
||||
adoption_notice.set_unchecked()
|
||||
|
||||
# Log the action
|
||||
Log.objects.create(
|
||||
user=request.user_to_notify,
|
||||
action="add_adoption_notice",
|
||||
text=f"{request.user_to_notify} 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]
|
||||
|
||||
@extend_schema(
|
||||
responses=AnimalGetSerializer
|
||||
)
|
||||
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
|
||||
@extend_schema(
|
||||
request=AnimalCreateSerializer,
|
||||
responses={201: inline_serializer(
|
||||
name='Animal',
|
||||
fields={
|
||||
'id': serializers.IntegerField(),
|
||||
"message": serializers.Field()}),
|
||||
400: "json"}
|
||||
)
|
||||
@transaction.atomic
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Create a new animal.
|
||||
"""
|
||||
serializer = AnimalCreateSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
animal = serializer.save(owner=request.user_to_notify)
|
||||
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
|
||||
},
|
||||
{
|
||||
'name': 'trusted',
|
||||
'required': False,
|
||||
'description': 'Filter by trusted status (true/false).',
|
||||
'type': bool
|
||||
},
|
||||
{
|
||||
'name': 'external_object_identifier',
|
||||
'required': False,
|
||||
'description': 'Filter by external object identifier. Use "None" to filter for an empty field',
|
||||
'type': str
|
||||
},
|
||||
{
|
||||
'name': 'external_source_identifier',
|
||||
'required': False,
|
||||
'description': 'Filter by external source identifier. Use "None" to filter for an empty field',
|
||||
'type': str
|
||||
},
|
||||
{
|
||||
'name': 'search',
|
||||
'required': False,
|
||||
'description': 'Search by organization name or location name/city.',
|
||||
'type': str
|
||||
},
|
||||
],
|
||||
responses={200: RescueOrganizationSerializer(many=True)}
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Get list of rescue organizations or a specific organization by ID or get a list with available filters for
|
||||
- external_object_identifier
|
||||
- external_source_identifier
|
||||
"""
|
||||
org_id = request.query_params.get("id")
|
||||
external_object_identifier = request.query_params.get("external_object_identifier")
|
||||
external_source_identifier = request.query_params.get("external_source_identifier")
|
||||
search_query = request.query_params.get("search")
|
||||
|
||||
if org_id:
|
||||
try:
|
||||
organization = RescueOrganization.objects.get(pk=org_id)
|
||||
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()
|
||||
|
||||
if external_object_identifier:
|
||||
if external_object_identifier == "None":
|
||||
external_object_identifier = None
|
||||
organizations = organizations.filter(external_object_identifier=external_object_identifier)
|
||||
|
||||
if external_source_identifier:
|
||||
if external_source_identifier == "None":
|
||||
external_source_identifier = None
|
||||
organizations = organizations.filter(external_source_identifier=external_source_identifier)
|
||||
if search_query:
|
||||
organizations = organizations.filter(
|
||||
Q(name__icontains=search_query) |
|
||||
Q(location_string__icontains=search_query) |
|
||||
Q(location__name__icontains=search_query) |
|
||||
Q(location__city__icontains=search_query)
|
||||
)
|
||||
if organizations.count() == 0:
|
||||
return Response({"error": "No organizations found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
serializer = RescueOrganizationSerializer(organizations, many=True, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@transaction.atomic
|
||||
@extend_schema(
|
||||
request=RescueOrganizationSerializer,
|
||||
responses={201: 'Rescue organization created successfully!'}
|
||||
)
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Create or update a rescue organization.
|
||||
"""
|
||||
serializer = RescueOrganizationSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
rescue_org = serializer.save()
|
||||
if rescue_org.location is None:
|
||||
# Add the location
|
||||
post_rescue_org_save.delay_on_commit(rescue_org.pk)
|
||||
return Response(
|
||||
{"message": "Rescue organization created successfully!", "id": rescue_org.id},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@transaction.atomic
|
||||
@extend_schema(
|
||||
request=RescueOrganizationSerializer,
|
||||
responses={200: 'Rescue organization updated successfully!'}
|
||||
)
|
||||
def patch(self, request, *args, **kwargs):
|
||||
"""
|
||||
Partially update a rescue organization.
|
||||
"""
|
||||
org_id = request.data.get("id")
|
||||
if not org_id:
|
||||
return Response({"error": "ID is required for updating."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
organization = RescueOrganization.objects.get(pk=org_id)
|
||||
except RescueOrganization.DoesNotExist:
|
||||
return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
serializer = RescueOrganizationSerializer(organization, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response({"message": "Rescue organization updated successfully!"}, status=status.HTTP_200_OK)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class AddImageApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@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_to_notify)
|
||||
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)
|
||||
|
||||
|
||||
class LocationApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
{
|
||||
'name': 'id',
|
||||
'required': False,
|
||||
'description': 'ID of the location to retrieve.',
|
||||
'type': int
|
||||
},
|
||||
],
|
||||
responses={200: LocationSerializer(many=True)}
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Retrieve a location
|
||||
"""
|
||||
location_id = kwargs.get("id")
|
||||
if location_id:
|
||||
try:
|
||||
location = Location.objects.get(pk=location_id)
|
||||
serializer = LocationSerializer(location, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Location.DoesNotExist:
|
||||
return Response({"error": "Location not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
locations = Location.objects.all()
|
||||
serializer = LocationSerializer(locations, many=True, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@transaction.atomic
|
||||
@extend_schema(
|
||||
request=LocationSerializer,
|
||||
responses={201: 'Location created successfully!'}
|
||||
)
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
API view to add a location
|
||||
"""
|
||||
serializer = LocationSerializer(data=request.data, context={'request': request})
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
location = serializer.save()
|
||||
|
||||
# Log the action
|
||||
Log.objects.create(
|
||||
user=request.user,
|
||||
action="add_location",
|
||||
text=f"{request.user} added adoption notice {location.pk} via API",
|
||||
)
|
||||
|
||||
# Return success response with new adoption notice details
|
||||
return Response(
|
||||
{"message": "Location created successfully!", "id": location.pk},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
|
||||
class AdoptionNoticeGeoJSONView(ListAPIView):
|
||||
queryset = AdoptionNotice.objects.select_related('location').filter(location__isnull=False).filter(
|
||||
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE)
|
||||
serializer_class = AdoptionNoticeGeoJSONSerializer
|
||||
renderer_classes = [GeoJSONRenderer]
|
||||
|
||||
|
||||
class RescueOrgGeoJSONView(ListAPIView):
|
||||
queryset = RescueOrganization.objects.select_related('location').filter(location__isnull=False)
|
||||
serializer_class = RescueOrgeGeoJSONSerializer
|
||||
renderer_classes = [GeoJSONRenderer]
|
||||
|
@@ -1,12 +1,13 @@
|
||||
from django import forms
|
||||
|
||||
from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportComment, ModerationAction, User, Species, \
|
||||
Comment, SexChoicesWithAll
|
||||
Comment, SexChoicesWithAll, DistanceChoices, SpeciesSpecificURL, RescueOrganization
|
||||
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):
|
||||
@@ -22,95 +23,38 @@ class DateInput(forms.DateInput):
|
||||
|
||||
|
||||
class AdoptionNoticeForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'in_adoption_notice_creation_flow' in kwargs:
|
||||
in_flow = kwargs.pop('in_adoption_notice_creation_flow')
|
||||
else:
|
||||
in_flow = False
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
|
||||
self.helper.form_id = 'form-adoption-notice'
|
||||
self.helper.form_class = 'card'
|
||||
self.helper.form_method = 'post'
|
||||
|
||||
if in_flow:
|
||||
submit = Submit('save-and-add-another-animal', _('Speichern'))
|
||||
|
||||
else:
|
||||
submit = Submit('submit', _('Speichern'))
|
||||
|
||||
self.helper.layout = Layout(
|
||||
Fieldset(
|
||||
_('Vermittlungsdetails'),
|
||||
'name',
|
||||
'species',
|
||||
'num_animals',
|
||||
'date_of_birth',
|
||||
'sex',
|
||||
'group_only',
|
||||
'searching_since',
|
||||
'location_string',
|
||||
'organization',
|
||||
'description',
|
||||
'further_information',
|
||||
),
|
||||
submit)
|
||||
template_name = "fellchensammlung/forms/form_snippets.html"
|
||||
|
||||
class Meta:
|
||||
model = AdoptionNotice
|
||||
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",
|
||||
"organization"]
|
||||
widgets = {
|
||||
'searching_since': DateInput(),
|
||||
'searching_since': DateInput(format=('%Y-%m-%d')),
|
||||
}
|
||||
|
||||
|
||||
class AnimalForm(forms.ModelForm):
|
||||
class AdoptionNoticeFormAutoAnimal(AdoptionNoticeForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'in_adoption_notice_creation_flow' in kwargs:
|
||||
adding = kwargs.pop('in_adoption_notice_creation_flow')
|
||||
else:
|
||||
adding = False
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_class = 'form-animal card'
|
||||
if adding:
|
||||
self.helper.add_input(Submit('save-and-add-another-animal', _('Speichern und weiteres Tier hinzufügen')))
|
||||
self.helper.add_input(Submit('save-and-finish', _('Speichern und beenden')))
|
||||
else:
|
||||
self.helper.add_input(Submit('submit', _('Speichern'), css_class="btn"))
|
||||
|
||||
class Meta:
|
||||
model = Animal
|
||||
fields = ["name", "date_of_birth", "species", "sex", "description"]
|
||||
|
||||
|
||||
class AnimalFormWithDateWidget(AnimalForm):
|
||||
class Meta:
|
||||
model = Animal
|
||||
fields = ["name", "date_of_birth", "species", "sex", "description"]
|
||||
widgets = {
|
||||
'date_of_birth': DateInput(),
|
||||
}
|
||||
|
||||
|
||||
class AdoptionNoticeFormWithDateWidgetAutoAnimal(AdoptionNoticeFormWithDateWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AdoptionNoticeFormWithDateWidgetAutoAnimal, self).__init__(*args, **kwargs)
|
||||
super(AdoptionNoticeFormAutoAnimal, self).__init__(*args, **kwargs)
|
||||
self.fields["num_animals"] = forms.fields.IntegerField(min_value=1, max_value=30, label=_("Zahl der Tiere"))
|
||||
animal_form = AnimalForm()
|
||||
self.fields["species"] = animal_form.fields["species"]
|
||||
self.fields["sex"] = animal_form.fields["sex"]
|
||||
self.fields["date_of_birth"] = animal_form.fields["date_of_birth"]
|
||||
self.fields["date_of_birth"].widget = DateInput()
|
||||
self.fields["date_of_birth"].widget = DateInput(format=('%Y-%m-%d'))
|
||||
|
||||
|
||||
class AnimalForm(forms.ModelForm):
|
||||
template_name = "fellchensammlung/forms/form_snippets.html"
|
||||
|
||||
class Meta:
|
||||
model = Animal
|
||||
fields = ["name", "date_of_birth", "species", "sex", "description"]
|
||||
|
||||
widgets = {
|
||||
'date_of_birth': DateInput(format=('%Y-%m-%d'))
|
||||
}
|
||||
|
||||
|
||||
class ImageForm(forms.ModelForm):
|
||||
@@ -124,11 +68,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
|
||||
@@ -136,30 +90,39 @@ class ImageForm(forms.ModelForm):
|
||||
|
||||
|
||||
class ReportAdoptionNoticeForm(forms.ModelForm):
|
||||
template_name = "fellchensammlung/forms/form_snippets.html"
|
||||
|
||||
class Meta:
|
||||
model = ReportAdoptionNotice
|
||||
fields = ('reported_broken_rules', 'user_comment')
|
||||
|
||||
|
||||
class ReportCommentForm(forms.ModelForm):
|
||||
template_name = "fellchensammlung/forms/form_snippets.html"
|
||||
|
||||
class Meta:
|
||||
model = ReportComment
|
||||
fields = ('reported_broken_rules', 'user_comment')
|
||||
|
||||
|
||||
class CommentForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_class = 'form-comments'
|
||||
self.helper.add_input(Hidden('action', 'comment'))
|
||||
self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="btn2"))
|
||||
|
||||
class Meta:
|
||||
model = Comment
|
||||
fields = ('text',)
|
||||
|
||||
|
||||
class SpeciesURLForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = SpeciesSpecificURL
|
||||
fields = ('species', 'url')
|
||||
|
||||
|
||||
class RescueOrgInternalComment(forms.ModelForm):
|
||||
class Meta:
|
||||
model = RescueOrganization
|
||||
fields = ('internal_comment',)
|
||||
|
||||
|
||||
class ModerationActionForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ModerationAction
|
||||
@@ -170,23 +133,17 @@ 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."))
|
||||
template_name = "fellchensammlung/forms/form_snippets.html"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_id = 'form-registration'
|
||||
self.helper.form_class = 'card'
|
||||
|
||||
self.helper.add_input(Submit('submit', _('Registrieren'), css_class="btn"))
|
||||
|
||||
|
||||
def _get_distances():
|
||||
return {i: i for i in [20, 50, 100, 200, 500]}
|
||||
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."))
|
||||
|
||||
|
||||
class AdoptionNoticeSearchForm(forms.Form):
|
||||
location = forms.CharField(max_length=20, label=_("Stadt"), required=False)
|
||||
max_distance = forms.ChoiceField(choices=_get_distances, label=_("Max. Distanz"))
|
||||
template_name = "fellchensammlung/forms/form_snippets.html"
|
||||
|
||||
sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False,
|
||||
initial=SexChoicesWithAll.ALL)
|
||||
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED,
|
||||
label=_("Suchradius"))
|
||||
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
|
||||
|
@@ -1,43 +1,43 @@
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.conf import settings
|
||||
from django.core import mail
|
||||
from fellchensammlung.models import User, CommentNotification, BaseNotification, TrustLevel
|
||||
from notfellchen.settings import host
|
||||
|
||||
NEWLINE = "\r\n"
|
||||
from fellchensammlung.models import User, Notification, TrustLevel, NotificationTypeChoices
|
||||
from fellchensammlung.tools.model_helpers import ndm
|
||||
|
||||
|
||||
def mail_admins_new_report(report):
|
||||
subject = _("Neue Meldung")
|
||||
def notify_mods_new_report(report, notification_type):
|
||||
"""
|
||||
Sends an e-mail to all users that should handle the report.
|
||||
"""
|
||||
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:
|
||||
reported_rules_text = (f"Ein Verstoß gegen die folgenden Regeln wurde gemeldet:{NEWLINE}"
|
||||
f"- {f'{NEWLINE} - '.join([str(r) for r in report.reported_broken_rules.all()])}{NEWLINE}")
|
||||
if notification_type == NotificationTypeChoices.NEW_REPORT_AN:
|
||||
title = _("Vermittlung gemeldet")
|
||||
elif notification_type == NotificationTypeChoices.NEW_COMMENT:
|
||||
title = _("Kommentar gemeldet")
|
||||
else:
|
||||
reported_rules_text = f"Es wurden keine Regeln angegeben gegen die Verstoßen wurde.{NEWLINE}"
|
||||
if report.user_comment:
|
||||
comment_text = f'Kommentar zum Report: "{report.user_comment}"{NEWLINE}'
|
||||
else:
|
||||
comment_text = f"Es wurde kein Kommentar hinzugefügt.{NEWLINE}"
|
||||
|
||||
report_url = "https://" + host + report.get_absolute_url()
|
||||
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])
|
||||
message.send()
|
||||
raise NotImplementedError
|
||||
notification = Notification.objects.create(
|
||||
notification_type=notification_type,
|
||||
user_to_notify=moderator,
|
||||
report=report,
|
||||
title=title,
|
||||
)
|
||||
notification.save()
|
||||
|
||||
|
||||
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()
|
||||
notification = Notification.objects.get(pk=notification_pk)
|
||||
|
||||
subject = f"{notification.title}"
|
||||
context = {"notification": notification, }
|
||||
html_message = render_to_string(ndm[notification.notification_type].email_html_template, context)
|
||||
plain_message = render_to_string(ndm[notification.notification_type].email_plain_template, context)
|
||||
|
||||
mail.send_mail(subject, plain_message, settings.DEFAULT_FROM_EMAIL,
|
||||
[notification.user_to_notify.email],
|
||||
html_message=html_message)
|
||||
|
10
src/fellchensammlung/management/commands/dedup_locations.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.core.management import BaseCommand
|
||||
from fellchensammlung.tools.admin import dedup_locations
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Deduplicate locations based on place_id'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dedup_locations()
|
||||
|
11
src/fellchensammlung/management/commands/export_contacts.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.core.management import BaseCommand
|
||||
|
||||
from fellchensammlung.tools.admin import export_orgs_as_vcf
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Export organizations with phone number as contacts in vcf format'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
export_orgs_as_vcf()
|
||||
|
@@ -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
@@ -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
@@ -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'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.1.4 on 2025-03-09 08:31
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0037_alter_basenotification_title'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='last_checked',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Datum der letzten Prüfung'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.4 on 2025-03-09 16:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0038_rescueorganization_last_checked'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='rescueorganization',
|
||||
name='last_checked',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='Datum der letzten Prüfung'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.4 on 2025-03-20 23:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0039_alter_rescueorganization_last_checked'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='rescueorganization',
|
||||
name='allows_using_materials',
|
||||
field=models.CharField(choices=[('allowed', 'Usage allowed'), ('requested', 'Usage requested'), ('denied', 'Usage denied'), ('other', "It's complicated"), ('not_asked', 'Not asked')], default='not_asked', max_length=200, verbose_name='Erlaubt Nutzung von Inhalten'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,42 @@
|
||||
# Generated by Django 5.1.4 on 2025-04-06 06:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0040_alter_rescueorganization_allows_using_materials'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='location',
|
||||
name='city',
|
||||
field=models.CharField(blank=True, max_length=200, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='location',
|
||||
name='country',
|
||||
field=models.CharField(blank=True, help_text='Standardisierter Ländercode nach ISO 3166-1 ALPHA-2', max_length=2, null=True, verbose_name='Ländercode'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='location',
|
||||
name='housenumber',
|
||||
field=models.CharField(blank=True, max_length=20, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='location',
|
||||
name='postcode',
|
||||
field=models.CharField(blank=True, max_length=20, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='location',
|
||||
name='street',
|
||||
field=models.CharField(blank=True, max_length=200, null=True),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='rescueorganization',
|
||||
unique_together={('external_object_identifier', 'external_source_identifier')},
|
||||
),
|
||||
]
|
18
src/fellchensammlung/migrations/0042_location_county.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.4 on 2025-04-24 17:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0041_location_city_location_country_location_housenumber_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='location',
|
||||
name='county',
|
||||
field=models.CharField(blank=True, max_length=200, null=True),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.4 on 2025-04-24 17:41
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0042_location_county'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='location',
|
||||
old_name='country',
|
||||
new_name='countrycode',
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.4 on 2025-04-26 22:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0043_rename_country_location_countrycode'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='location',
|
||||
name='place_id',
|
||||
field=models.CharField(max_length=200),
|
||||
),
|
||||
]
|
23
src/fellchensammlung/migrations/0045_importantlocation.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.4 on 2025-04-27 11:31
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0044_alter_location_place_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ImportantLocation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.location')),
|
||||
],
|
||||
),
|
||||
]
|
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.4 on 2025-04-27 11:51
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0045_importantlocation'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='importantlocation',
|
||||
name='location',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.location'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.1 on 2025-05-23 16:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0046_alter_importantlocation_location'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adoptionnotice',
|
||||
name='further_information',
|
||||
field=models.URLField(blank=True, help_text='Verlinke hier die Quelle der Vermittlung (z.B. die Website des Tierheims', null=True, verbose_name='Link zu mehr Informationen'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adoptionnotice',
|
||||
name='name',
|
||||
field=models.CharField(max_length=200, verbose_name='Titel der Vermittlung'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rescueorganization',
|
||||
name='location_string',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Ort der Organisation'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-19 15:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0047_alter_adoptionnotice_further_information_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adoptionnotice',
|
||||
name='further_information',
|
||||
field=models.URLField(blank=True, help_text='Verlinke hier die Quelle der Vermittlung (z.B. die Website des Tierheims)', null=True, verbose_name='Link zu mehr Informationen'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-19 21:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0048_alter_adoptionnotice_further_information'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='exclude_from_check',
|
||||
field=models.BooleanField(default=False, help_text='Organisation von der manuellen Überprüfung ausschließen, z.B. weil Tiere nicht online geführt werden', verbose_name='Von Prüfung ausschließen'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.1 on 2025-06-20 16:52
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0049_rescueorganization_exclude_from_check'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='speciesspecificurl',
|
||||
old_name='rescues_organization',
|
||||
new_name='rescue_organization',
|
||||
),
|
||||
]
|
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.2.1 on 2025-07-03 09:28
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0050_rename_rescues_organization_speciesspecificurl_rescue_organization'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='parent_org',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.rescueorganization'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SpeciesSpecialization',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('rescue_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,54 @@
|
||||
# Generated by Django 5.2.1 on 2025-07-11 09:01
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0051_rescueorganization_parent_org_speciesspecialization'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='basenotification',
|
||||
name='user',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='commentnotification',
|
||||
name='basenotification_ptr',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='commentnotification',
|
||||
name='comment',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('read_at', models.DateTimeField(blank=True, null=True, verbose_name='Gelesen am')),
|
||||
('notification_type', models.CharField(choices=[('new_user', 'Useraccount wurde erstellt'), ('new_report_an', 'Vermittlung wurde gemeldet'), ('new_report_comment', 'Kommentar wurde gemeldet'), ('an_is_to_be_checked', 'Vermittlung muss überprüft werden'), ('an_was_deactivated', 'Vermittlung wurde deaktiviert'), ('an_for_search_found', 'Vermittlung für Suche gefunden')], max_length=200, verbose_name='Benachrichtigungsgrund')),
|
||||
('title', models.CharField(max_length=100, verbose_name='Titel')),
|
||||
('text', models.TextField(verbose_name='Inhalt')),
|
||||
('read', models.BooleanField(default=False)),
|
||||
('adoption_notice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')),
|
||||
('comment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.comment', verbose_name='Antwort')),
|
||||
('report', models.ForeignKey(help_text='Report auf den sich die Benachrichtigung bezieht.', on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.report', verbose_name='Report')),
|
||||
('user_related', models.ForeignKey(help_text='Useraccount auf den sich die Benachrichtigung bezieht.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Verwandter Useraccount')),
|
||||
('user_to_notify', models.ForeignKey(help_text='Useraccount der Benachrichtigt wird', on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in')),
|
||||
],
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='AdoptionNoticeNotification',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='BaseNotification',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='CommentNotification',
|
||||
),
|
||||
]
|
@@ -0,0 +1,40 @@
|
||||
# Generated by Django 5.2.1 on 2025-07-11 09:10
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0052_remove_basenotification_user_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='adoption_notice',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='notification_type',
|
||||
field=models.CharField(choices=[('new_user', 'Useraccount wurde erstellt'), ('new_report_an', 'Vermittlung wurde gemeldet'), ('new_report_comment', 'Kommentar wurde gemeldet'), ('an_is_to_be_checked', 'Vermittlung muss überprüft werden'), ('an_was_deactivated', 'Vermittlung wurde deaktiviert'), ('an_for_search_found', 'Vermittlung für Suche gefunden'), ('new_comment', 'Neuer Kommentar')], max_length=200, verbose_name='Benachrichtigungsgrund'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='report',
|
||||
field=models.ForeignKey(blank=True, help_text='Report auf den sich die Benachrichtigung bezieht.', null=True, on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.report', verbose_name='Report'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='user_related',
|
||||
field=models.ForeignKey(blank=True, help_text='Useraccount auf den sich die Benachrichtigung bezieht.', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Verwandter Useraccount'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='user_to_notify',
|
||||
field=models.ForeignKey(help_text='Useraccount der Benachrichtigt wird', on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.1 on 2025-07-11 11:47
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0053_alter_notification_adoption_notice_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='comment',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.comment', verbose_name='Antwort'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.2.1 on 2025-07-13 10:54
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0054_alter_notification_comment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='ongoing_communication',
|
||||
field=models.BooleanField(default=False, help_text='Es findet gerade Kommunikation zwischen Notfellchen und der Organisation statt.', verbose_name='In aktiver Kommunikation'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='user_to_notify',
|
||||
field=models.ForeignKey(help_text='Useraccount der Benachrichtigt wird', on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL, verbose_name='Empfänger*in'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.2.1 on 2025-07-14 05:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0055_rescueorganization_ongoing_communication_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='rescueorganization',
|
||||
options={'ordering': ['name']},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='specializations',
|
||||
field=models.ManyToManyField(to='fellchensammlung.species'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,16 @@
|
||||
# Generated by Django 5.2.1 on 2025-07-14 05:15
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0056_alter_rescueorganization_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='SpeciesSpecialization',
|
||||
),
|
||||
]
|
25
src/fellchensammlung/migrations/0058_socialmediapost.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.2.1 on 2025-07-19 17:48
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0057_delete_speciesspecialization'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SocialMediaPost',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateField(default=django.utils.timezone.now, verbose_name='Erstellt am')),
|
||||
('platform', models.CharField(choices=[('fediverse', 'Fediverse')], max_length=255, verbose_name='Social Media Platform')),
|
||||
('url', models.URLField(verbose_name='URL')),
|
||||
('adoption_notice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')),
|
||||
],
|
||||
),
|
||||
]
|
@@ -1,6 +1,9 @@
|
||||
import uuid
|
||||
from random import choices
|
||||
from tabnanny import verbose
|
||||
|
||||
from django.db import models
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils import timezone
|
||||
@@ -8,9 +11,14 @@ from django.dispatch import receiver
|
||||
from django.db.models.signals import post_save
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from .tools import misc, geo
|
||||
from notfellchen.settings import MEDIA_URL
|
||||
from notfellchen.settings import MEDIA_URL, base_url
|
||||
from .tools.geo import LocationProxy, Position
|
||||
from .tools.misc import age_as_hr_string, time_since_as_hr_string
|
||||
from .tools.model_helpers import NotificationTypeChoices
|
||||
from .tools.model_helpers import ndm as NotificationDisplayMapping
|
||||
|
||||
|
||||
class Language(models.Model):
|
||||
@@ -35,36 +43,54 @@ class Language(models.Model):
|
||||
|
||||
|
||||
class Location(models.Model):
|
||||
place_id = models.IntegerField()
|
||||
place_id = models.CharField(max_length=200) # OSM id
|
||||
latitude = models.FloatField()
|
||||
longitude = models.FloatField()
|
||||
name = models.CharField(max_length=2000)
|
||||
city = models.CharField(max_length=200, blank=True, null=True)
|
||||
housenumber = models.CharField(max_length=20, blank=True, null=True)
|
||||
postcode = models.CharField(max_length=20, blank=True, null=True)
|
||||
street = models.CharField(max_length=200, blank=True, null=True)
|
||||
county = models.CharField(max_length=200, blank=True, null=True)
|
||||
# Country code as per ISO 3166-1 alpha-2
|
||||
# https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes
|
||||
countrycode = models.CharField(max_length=2, verbose_name=_("Ländercode"),
|
||||
help_text=_("Standardisierter Ländercode nach ISO 3166-1 ALPHA-2"),
|
||||
blank=True, null=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
|
||||
if self.city and self.postcode:
|
||||
return f"{self.city} ({self.postcode})"
|
||||
else:
|
||||
return f"{self.name}"
|
||||
|
||||
@property
|
||||
def str_hr(self):
|
||||
return f"{self.name.split(',')[0]}"
|
||||
def position(self):
|
||||
return (self.latitude, self.longitude)
|
||||
|
||||
@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,
|
||||
postcode=proxy.postcode,
|
||||
city=proxy.city,
|
||||
street=proxy.street,
|
||||
county=proxy.county,
|
||||
countrycode=proxy.countrycode,
|
||||
)
|
||||
return location
|
||||
|
||||
@@ -76,31 +102,51 @@ class Location(models.Model):
|
||||
instance.save()
|
||||
|
||||
|
||||
class RescueOrganization(models.Model):
|
||||
class ImportantLocation(models.Model):
|
||||
location = models.OneToOneField(Location, on_delete=models.CASCADE)
|
||||
slug = models.SlugField(unique=True)
|
||||
name = models.CharField(max_length=200)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('search-by-location', kwargs={'important_location_slug': self.slug})
|
||||
|
||||
|
||||
class ExternalSourceChoices(models.TextChoices):
|
||||
OSM = "OSM", _("Open Street Map")
|
||||
|
||||
|
||||
class AllowUseOfMaterialsChices(models.TextChoices):
|
||||
USE_MATERIALS_ALLOWED = "allowed", _("Usage allowed")
|
||||
USE_MATERIALS_REQUESTED = "requested", _("Usage requested")
|
||||
USE_MATERIALS_DENIED = "denied", _("Usage denied")
|
||||
USE_MATERIALS_OTHER = "other", _("It's complicated")
|
||||
USE_MATERIALS_NOT_ASKED = "not_asked", _("Not asked")
|
||||
|
||||
|
||||
class 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):
|
||||
return f"{self.name}"
|
||||
"""String for representing the Model object."""
|
||||
return self.name
|
||||
|
||||
USE_MATERIALS_ALLOWED = "allowed"
|
||||
USE_MATERIALS_REQUESTED = "requested"
|
||||
USE_MATERIALS_DENIED = "denied"
|
||||
USE_MATERIALS_OTHER = "other"
|
||||
USE_MATERIALS_NOT_ASKED = "not_asked"
|
||||
class Meta:
|
||||
verbose_name = _('Tierart')
|
||||
verbose_name_plural = _('Tierarten')
|
||||
|
||||
ALLOW_USE_MATERIALS_CHOICE = {
|
||||
USE_MATERIALS_ALLOWED: "Usage allowed",
|
||||
USE_MATERIALS_REQUESTED: "Usage requested",
|
||||
USE_MATERIALS_DENIED: "Usage denied",
|
||||
USE_MATERIALS_OTHER: "It's complicated",
|
||||
USE_MATERIALS_NOT_ASKED: "Not asked"
|
||||
}
|
||||
|
||||
class RescueOrganization(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
|
||||
allows_using_materials = models.CharField(max_length=200,
|
||||
default=ALLOW_USE_MATERIALS_CHOICE[USE_MATERIALS_NOT_ASKED],
|
||||
choices=ALLOW_USE_MATERIALS_CHOICE,
|
||||
default=AllowUseOfMaterialsChices.USE_MATERIALS_NOT_ASKED,
|
||||
choices=AllowUseOfMaterialsChices.choices,
|
||||
verbose_name=_('Erlaubt Nutzung von Inhalten'))
|
||||
location_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation"))
|
||||
location_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation"), null=True, blank=True, )
|
||||
location = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True)
|
||||
instagram = models.URLField(null=True, blank=True, verbose_name=_('Instagram Profil'))
|
||||
facebook = models.URLField(null=True, blank=True, verbose_name=_('Facebook Profil'))
|
||||
@@ -110,8 +156,35 @@ class RescueOrganization(models.Model):
|
||||
website = models.URLField(null=True, blank=True, verbose_name=_('Website'))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
last_checked = models.DateTimeField(auto_now_add=True, verbose_name=_('Datum der letzten Prüfung'))
|
||||
internal_comment = models.TextField(verbose_name=_("Interner Kommentar"), null=True, blank=True, )
|
||||
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung')) # Markdown allowed
|
||||
external_object_identifier = models.CharField(max_length=200, null=True, blank=True,
|
||||
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'))
|
||||
exclude_from_check = models.BooleanField(default=False, verbose_name=_('Von Prüfung ausschließen'),
|
||||
help_text=_("Organisation von der manuellen Überprüfung ausschließen, "
|
||||
"z.B. weil Tiere nicht online geführt werden"))
|
||||
ongoing_communication = models.BooleanField(default=False, verbose_name=_('In aktiver Kommunikation'),
|
||||
help_text=_(
|
||||
"Es findet gerade Kommunikation zwischen Notfellchen und der Organisation statt."))
|
||||
parent_org = models.ForeignKey("RescueOrganization", on_delete=models.PROTECT, blank=True, null=True)
|
||||
# allows to specify if a rescue organization has a specialization for dedicated species
|
||||
specializations = models.ManyToManyField(Species, blank=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('external_object_identifier', 'external_source_identifier',)
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}"
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.location is None and self.location_string is None:
|
||||
raise ValidationError(_('Location or Location String must be set'))
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("rescue-organization-detail", args=[str(self.pk)])
|
||||
@@ -120,6 +193,61 @@ class RescueOrganization(models.Model):
|
||||
def adoption_notices(self):
|
||||
return AdoptionNotice.objects.filter(organization=self)
|
||||
|
||||
@property
|
||||
def adoption_notices_in_hierarchy(self):
|
||||
"""
|
||||
Shows all adoption notices of this rescue organization and all child organizations.
|
||||
"""
|
||||
adoption_notices_discovered = list(self.adoption_notices)
|
||||
if self.child_organizations:
|
||||
for child in self.child_organizations:
|
||||
adoption_notices_discovered.extend(child.adoption_notices_in_hierarchy)
|
||||
return adoption_notices_discovered
|
||||
|
||||
@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()})")
|
||||
else:
|
||||
return self.description
|
||||
|
||||
def set_checked(self):
|
||||
self.last_checked = timezone.now()
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def last_checked_hr(self):
|
||||
time_since_last_checked = timezone.now() - self.last_checked
|
||||
return time_since_as_hr_string(time_since_last_checked)
|
||||
|
||||
@property
|
||||
def species_urls(self):
|
||||
return SpeciesSpecificURL.objects.filter(rescue_organization=self)
|
||||
|
||||
@property
|
||||
def has_contact_data(self):
|
||||
"""
|
||||
Returns true if at least one type of contact data is available.
|
||||
"""
|
||||
return self.instagram or self.facebook or self.website or self.phone_number or self.email or self.fediverse_profile
|
||||
|
||||
def set_exclusion_from_checks(self):
|
||||
self.exclude_from_check = True
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def child_organizations(self):
|
||||
return RescueOrganization.objects.filter(parent_org=self)
|
||||
|
||||
|
||||
# Admins can perform all actions and have the highest trust associated with them
|
||||
# Moderators can make moderation decisions regarding the deletion of content
|
||||
@@ -157,14 +285,26 @@ class User(AbstractUser):
|
||||
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_full_url(self):
|
||||
return f"{base_url}{self.get_absolute_url()}"
|
||||
|
||||
def get_notifications_url(self):
|
||||
return self.get_absolute_url()
|
||||
|
||||
def get_unread_notifications(self):
|
||||
return Notification.objects.filter(user_to_notify=self, read=False)
|
||||
|
||||
def get_num_unread_notifications(self):
|
||||
return BaseNotification.objects.filter(user=self, read=False).count()
|
||||
return Notification.objects.filter(user_to_notify=self, read=False).count()
|
||||
|
||||
@property
|
||||
def adoption_notices(self):
|
||||
@@ -177,7 +317,7 @@ class User(AbstractUser):
|
||||
|
||||
class Image(models.Model):
|
||||
image = models.ImageField(upload_to='images')
|
||||
alt_text = models.TextField(max_length=2000)
|
||||
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)
|
||||
@@ -190,22 +330,6 @@ class Image(models.Model):
|
||||
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):
|
||||
class Meta:
|
||||
permissions = [
|
||||
@@ -221,11 +345,15 @@ class AdoptionNotice(models.Model):
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=timezone.now)
|
||||
searching_since = models.DateField(verbose_name=_('Sucht nach einem Zuhause seit'))
|
||||
name = models.CharField(max_length=200)
|
||||
name = models.CharField(max_length=200, verbose_name=_('Titel der Vermittlung'))
|
||||
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
|
||||
organization = models.ForeignKey(RescueOrganization, blank=True, null=True, on_delete=models.SET_NULL,
|
||||
verbose_name=_('Organisation'))
|
||||
further_information = models.URLField(null=True, blank=True, verbose_name=_('Link zu mehr Informationen'))
|
||||
further_information = models.URLField(null=True, blank=True,
|
||||
verbose_name=_('Link zu mehr Informationen'),
|
||||
help_text=_(
|
||||
"Verlinke hier die Quelle der Vermittlung (z.B. die Website des "
|
||||
"Tierheims)"))
|
||||
group_only = models.BooleanField(default=False, verbose_name=_('Ausschließlich Gruppenadoption'))
|
||||
photos = models.ManyToManyField(Image, blank=True)
|
||||
location_string = models.CharField(max_length=200, verbose_name=_("Ortsangabe"))
|
||||
@@ -243,6 +371,18 @@ class AdoptionNotice(models.Model):
|
||||
sexes.add(animal.sex)
|
||||
return sexes
|
||||
|
||||
@property
|
||||
def num_per_sex(self):
|
||||
num_per_sex = dict()
|
||||
for sex in SexChoices:
|
||||
num_per_sex[sex] = self.animals.filter(sex=sex).count()
|
||||
return num_per_sex
|
||||
|
||||
@property
|
||||
def last_checked_hr(self):
|
||||
time_since_last_checked = timezone.now() - self.last_checked
|
||||
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:
|
||||
@@ -278,6 +418,10 @@ class AdoptionNotice(models.Model):
|
||||
"""Returns the url to access a detailed page for the adoption notice."""
|
||||
return reverse('adoption-notice-detail', args=[str(self.id)])
|
||||
|
||||
def get_full_url(self):
|
||||
"""Returns the url including protocol and domain"""
|
||||
return f"{base_url}{self.get_absolute_url()}"
|
||||
|
||||
def get_report_url(self):
|
||||
"""Returns the url to report an adoption notice."""
|
||||
return reverse('report-adoption-notice', args=[str(self.id)])
|
||||
@@ -286,6 +430,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
|
||||
@@ -329,48 +478,65 @@ class AdoptionNotice(models.Model):
|
||||
distance = geo.calculate_distance_between_coordinates(self.position, position)
|
||||
return distance < max_distance
|
||||
|
||||
@property
|
||||
def link_to_more_information(self):
|
||||
from urllib.parse import urlparse
|
||||
|
||||
domain = urlparse(self.further_information).netloc
|
||||
return f"<a href='{self.further_information}'>{domain}</a>"
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
return False
|
||||
return self.adoptionnoticestatus.is_active
|
||||
|
||||
@property
|
||||
def is_disabled(self):
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
return False
|
||||
return self.adoptionnoticestatus.is_disabled
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
return False
|
||||
return self.adoptionnoticestatus.is_closed
|
||||
|
||||
@property
|
||||
def is_disabled_unchecked(self):
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
return False
|
||||
return self.adoptionnoticestatus.is_disabled_unchecked
|
||||
|
||||
def set_closed(self):
|
||||
def set_closed(self, minor_status=None):
|
||||
self.last_checked = timezone.now()
|
||||
self.adoptionnoticestatus.set_closed()
|
||||
self.save()
|
||||
self.adoptionnoticestatus.set_closed(minor_status)
|
||||
|
||||
def set_active(self):
|
||||
self.last_checked = timezone.now()
|
||||
self.save()
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
AdoptionNoticeStatus.create_other(self)
|
||||
self.adoptionnoticestatus.set_active()
|
||||
self.save()
|
||||
|
||||
def set_unchecked(self):
|
||||
self.last_checked = timezone.now()
|
||||
self.save()
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
AdoptionNoticeStatus.create_other(self)
|
||||
self.adoptionnoticestatus.set_unchecked()
|
||||
self.save()
|
||||
|
||||
for subscription in self.get_subscriptions():
|
||||
notification_title = _("Vermittlung deaktiviert:") + f" {self}"
|
||||
text = _("Die folgende Vermittlung wurde deaktiviert: ") + f"{self.name}, {self.get_absolute_url()}"
|
||||
BaseNotification.objects.create(user=subscription.owner, text=text, title=notification_title)
|
||||
notification_title = _("Vermittlung deaktiviert:") + f" {self.name}"
|
||||
text = _("Die folgende Vermittlung wurde deaktiviert: ") + f"[{self.name}]({self.get_absolute_url()})"
|
||||
Notification.objects.create(user_to_notify=subscription.owner,
|
||||
notification_type=NotificationTypeChoices.AN_WAS_DEACTIVATED,
|
||||
adoption_notice=self,
|
||||
text=text,
|
||||
title=notification_title)
|
||||
|
||||
def last_posted(self, platform=None):
|
||||
if platform is None:
|
||||
last_post = SocialMediaPost.objects.filter(adoption_notice=self).order_by('-created_at').first()
|
||||
else:
|
||||
last_post = SocialMediaPost.objects.filter(adoption_notice=self, platform=platform).order_by(
|
||||
'-created_at').first()
|
||||
return last_post.created_at
|
||||
|
||||
|
||||
class AdoptionNoticeStatus(models.Model):
|
||||
@@ -435,6 +601,14 @@ class AdoptionNoticeStatus(models.Model):
|
||||
def is_active(self):
|
||||
return self.major_status == self.ACTIVE
|
||||
|
||||
@property
|
||||
def is_disabled(self):
|
||||
return self.major_status == self.DISABLED
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
return self.major_status == self.CLOSED
|
||||
|
||||
@property
|
||||
def is_disabled_unchecked(self):
|
||||
return self.major_status == self.DISABLED and self.minor_status == "unchecked"
|
||||
@@ -452,9 +626,12 @@ class AdoptionNoticeStatus(models.Model):
|
||||
minor_status=minor_status,
|
||||
adoption_notice=an_instance)
|
||||
|
||||
def set_closed(self):
|
||||
def set_closed(self, minor_status=None):
|
||||
self.major_status = self.MAJOR_STATUS_CHOICES[self.CLOSED]
|
||||
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"]
|
||||
if minor_status is None:
|
||||
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"]
|
||||
else:
|
||||
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED][minor_status]
|
||||
self.save()
|
||||
|
||||
def set_unchecked(self):
|
||||
@@ -489,7 +666,7 @@ class Animal(models.Model):
|
||||
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,
|
||||
@@ -531,6 +708,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
|
||||
@@ -564,8 +775,8 @@ 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)
|
||||
|
||||
@@ -576,12 +787,40 @@ class Report(models.Model):
|
||||
"""Returns the url to access a detailed page for the report."""
|
||||
return reverse('report-detail', args=[str(self.id)])
|
||||
|
||||
def get_full_url(self):
|
||||
return f"{base_url}{self.get_absolute_url()}"
|
||||
|
||||
def get_reported_rules(self):
|
||||
return self.reported_broken_rules.all()
|
||||
|
||||
def get_moderation_actions(self):
|
||||
return ModerationAction.objects.filter(report=self)
|
||||
|
||||
@property
|
||||
def reported_content(self):
|
||||
"""
|
||||
Dynamically fetch the reported content based on subclass.
|
||||
The alternative would be to use the ContentType framework:
|
||||
https://docs.djangoproject.com/en/5.1/ref/contrib/contenttypes/
|
||||
"""
|
||||
if hasattr(self, "reportadoptionnotice"):
|
||||
return self.reportadoptionnotice.adoption_notice
|
||||
elif hasattr(self, "reportcomment"):
|
||||
return self.reportcomment.reported_comment
|
||||
return None
|
||||
|
||||
@property
|
||||
def reported_content_url(self):
|
||||
"""
|
||||
Same as reported_content, just for url
|
||||
"""
|
||||
if hasattr(self, "reportadoptionnotice"):
|
||||
print(self.reportadoptionnotice.adoption_notice.get_absolute_url)
|
||||
return self.reportadoptionnotice.adoption_notice.get_absolute_url
|
||||
elif hasattr(self, "reportcomment"):
|
||||
return self.reportcomment.reported_comment.get_absolute_url
|
||||
return None
|
||||
|
||||
|
||||
class ReportAdoptionNotice(Report):
|
||||
adoption_notice = models.ForeignKey("AdoptionNotice", on_delete=models.CASCADE)
|
||||
@@ -590,6 +829,9 @@ class ReportAdoptionNotice(Report):
|
||||
def reported_content(self):
|
||||
return self.adoption_notice
|
||||
|
||||
def __str__(self):
|
||||
return f"Report der Vermittlung {self.adoption_notice}"
|
||||
|
||||
|
||||
class ReportComment(Report):
|
||||
reported_comment = models.ForeignKey("Comment", on_delete=models.CASCADE)
|
||||
@@ -624,9 +866,12 @@ class ModerationAction(models.Model):
|
||||
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):
|
||||
@@ -728,27 +973,47 @@ class Comment(models.Model):
|
||||
return self.adoption_notice.get_absolute_url()
|
||||
|
||||
|
||||
class BaseNotification(models.Model):
|
||||
class Notification(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
title = models.CharField(max_length=100)
|
||||
notification_type = models.CharField(max_length=200,
|
||||
choices=NotificationTypeChoices.choices,
|
||||
verbose_name=_('Benachrichtigungsgrund'))
|
||||
user_to_notify = models.ForeignKey(User,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_('Empfänger*in'),
|
||||
help_text=_("Useraccount der Benachrichtigt wird"),
|
||||
related_name='user')
|
||||
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)
|
||||
read_at = models.DateTimeField(blank=True, null=True, verbose_name=_("Gelesen am"))
|
||||
comment = models.ForeignKey(Comment, blank=True, null=True, on_delete=models.CASCADE, verbose_name=_('Antwort'))
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, blank=True, null=True, on_delete=models.CASCADE,
|
||||
verbose_name=_('Vermittlung'))
|
||||
user_related = models.ForeignKey(User,
|
||||
blank=True, null=True,
|
||||
on_delete=models.CASCADE, verbose_name=_('Verwandter Useraccount'),
|
||||
help_text=_("Useraccount auf den sich die Benachrichtigung bezieht."))
|
||||
report = models.ForeignKey(Report,
|
||||
blank=True, null=True,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_('Report'),
|
||||
help_text=_("Report auf den sich die Benachrichtigung bezieht."))
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.user}] {self.title} ({self.created_at})"
|
||||
return f"[{self.user_to_notify}] {self.title} ({self.created_at})"
|
||||
|
||||
def get_absolute_url(self):
|
||||
self.user.get_notifications_url()
|
||||
self.user_to_notify.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):
|
||||
return self.comment.get_absolute_url
|
||||
def get_body_part(self):
|
||||
return NotificationDisplayMapping[self.notification_type].web_partial
|
||||
|
||||
|
||||
class Subscriptions(models.Model):
|
||||
@@ -783,5 +1048,36 @@ class Timestamp(models.Model):
|
||||
timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("Zeitstempel"))
|
||||
data = models.CharField(max_length=2000, blank=True, null=True)
|
||||
|
||||
def ___str__(self):
|
||||
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"))
|
||||
rescue_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE,
|
||||
verbose_name=_("Tierschutzorganisation"))
|
||||
url = models.URLField(verbose_name=_("Tierartspezifische URL"))
|
||||
|
||||
|
||||
class PlatformChoices(models.TextChoices):
|
||||
FEDIVERSE = "fediverse", _("Fediverse")
|
||||
|
||||
|
||||
class SocialMediaPost(models.Model):
|
||||
created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
|
||||
platform = models.CharField(max_length=255, verbose_name=_("Social Media Platform"),
|
||||
choices=PlatformChoices.choices)
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
|
||||
url = models.URLField(verbose_name=_("URL"))
|
||||
|
||||
@staticmethod
|
||||
def get_an_to_post():
|
||||
adoption_notices_without_post = AdoptionNotice.objects.filter(socialmediapost__isnull=True,
|
||||
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE)
|
||||
return adoption_notices_without_post.first()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.platform} - {self.adoption_notice}"
|
||||
|
@@ -1,19 +1,23 @@
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from fellchensammlung.models import BaseNotification, CommentNotification, User, TrustLevel
|
||||
from fellchensammlung.models import Notification, User, TrustLevel, RescueOrganization, \
|
||||
NotificationTypeChoices
|
||||
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=Notification)
|
||||
def base_notification_receiver(sender, instance: Notification, created: bool, **kwargs):
|
||||
if not created or not instance.user_to_notify.email_notifications:
|
||||
return
|
||||
else:
|
||||
task_send_notification_email.delay(instance.pk)
|
||||
|
||||
|
||||
@receiver(post_save, sender=BaseNotification)
|
||||
def base_notification_receiver(sender, instance: BaseNotification, created: bool, **kwargs):
|
||||
if not created or not instance.user.email_notifications:
|
||||
@receiver(post_save, sender=RescueOrganization)
|
||||
def rescue_org_receiver(sender, instance: RescueOrganization, created: bool, **kwargs):
|
||||
if instance.location:
|
||||
return
|
||||
else:
|
||||
task_send_notification_email.delay(instance.pk)
|
||||
@@ -33,5 +37,9 @@ def notification_new_user(sender, instance: User, created: bool, **kwargs):
|
||||
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 = Notification.objects.create(title=subject,
|
||||
text=body_text,
|
||||
notification_type=NotificationTypeChoices.NEW_USER,
|
||||
user_to_notify=moderator,
|
||||
user_related=instance)
|
||||
notification.save()
|
||||
|
29
src/fellchensammlung/registration_views.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from django.utils.html import strip_tags
|
||||
from django_registration.backends.activation.views import RegistrationView
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class HTMLMailRegistrationView(RegistrationView):
|
||||
def send_activation_email(self, user):
|
||||
"""
|
||||
overwrites the function in django registration
|
||||
"""
|
||||
activation_key = self.get_activation_key(user)
|
||||
context = self.get_email_context(activation_key)
|
||||
context["user"] = user
|
||||
subject = render_to_string(
|
||||
template_name=self.email_subject_template,
|
||||
context=context,
|
||||
request=self.request,
|
||||
)
|
||||
# Force subject to a single line to avoid header-injection issues.
|
||||
subject = "".join(subject.splitlines())
|
||||
message = render_to_string(
|
||||
template_name=self.email_body_template,
|
||||
context=context,
|
||||
request=self.request,
|
||||
)
|
||||
plain_message = strip_tags(message)
|
||||
user.email_user(subject, plain_message, settings.DEFAULT_FROM_EMAIL, html_message=message)
|
45
src/fellchensammlung/sitemap.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from django.contrib.sitemaps import Sitemap
|
||||
from django.urls import reverse
|
||||
from .models import AdoptionNotice, RescueOrganization, ImportantLocation, Animal
|
||||
|
||||
|
||||
class StaticViewSitemap(Sitemap):
|
||||
priority = 0.8
|
||||
changefreq = "weekly"
|
||||
|
||||
def items(self):
|
||||
return ["index", "search", "map", "about", "rescue-organizations", "buying", "imprint", "terms-of-service",
|
||||
"privacy"]
|
||||
|
||||
def location(self, item):
|
||||
return reverse(item)
|
||||
|
||||
|
||||
class AdoptionNoticeSitemap(Sitemap):
|
||||
priority = 0.5
|
||||
changefreq = "daily"
|
||||
|
||||
def items(self):
|
||||
return AdoptionNotice.get_active_ANs()
|
||||
|
||||
def lastmod(self, obj):
|
||||
return obj.updated_at
|
||||
|
||||
|
||||
class RescueOrganizationSitemap(Sitemap):
|
||||
priority = 0.3
|
||||
changefreq = "weekly"
|
||||
|
||||
def items(self):
|
||||
return RescueOrganization.objects.all()
|
||||
|
||||
def lastmod(self, obj):
|
||||
return obj.updated_at
|
||||
|
||||
|
||||
class SearchSitemap(Sitemap):
|
||||
priority = 0.5
|
||||
chanfreq = "daily"
|
||||
|
||||
def items(self):
|
||||
return ImportantLocation.objects.all()
|
348
src/fellchensammlung/static/fellchensammlung/css/main.scss
Normal file
@@ -0,0 +1,348 @@
|
||||
$primary: #6CD4FF;
|
||||
$link: #292a2c;
|
||||
$grey-light: #c4c6ce;
|
||||
$grey-dark: #262728;
|
||||
$confirm: hsl(133deg, 100%, calc(41% + 0%));
|
||||
|
||||
// Path to Bulma's sass folder
|
||||
@use "bulma/sass" with (
|
||||
$family-primary: '"Nunito", sans-serif',
|
||||
$grey-dark: $grey-dark,
|
||||
$grey-light: $grey-light,
|
||||
$primary: $primary,
|
||||
$link: $link,
|
||||
$control-border-width: 2px,
|
||||
$input-shadow: none
|
||||
);
|
||||
|
||||
@use "bulma/sass/utilities/css-variables" as cv;
|
||||
|
||||
@include cv.system-theme($name: "dark") {
|
||||
.navbar-item > img {
|
||||
background-color: $grey-light !important;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.card-header {
|
||||
background-color: $grey-dark;
|
||||
}
|
||||
a.card-footer-item.is-danger {
|
||||
color: black;
|
||||
}
|
||||
.tag {
|
||||
color: $grey-dark;
|
||||
background-color: $grey-light;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// General Styles
|
||||
|
||||
.main-content {
|
||||
margin: auto;
|
||||
max-width: 80em;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
p > a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
p > a.button {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// Cards
|
||||
|
||||
.card-header {
|
||||
background-color: $primary;
|
||||
}
|
||||
|
||||
|
||||
// Search form suggestion dropdown
|
||||
|
||||
#location-result-list {
|
||||
display: inline; //ensures that the dropdown is not restricted in width WTF
|
||||
}
|
||||
|
||||
.result-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.result-item:hover {
|
||||
background-color: #b2aaaa;
|
||||
}
|
||||
|
||||
|
||||
// Toggle switch
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
// Button in card footer
|
||||
.card-footer {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-footer .card-footer-item.is-confirm {
|
||||
background-color: $confirm;
|
||||
}
|
||||
|
||||
.card-footer .card-footer-item.is-confirm:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
.card-footer .card-footer-item.is-danger {
|
||||
background-color: sass.$danger;
|
||||
}
|
||||
|
||||
.card-footer .card-footer-item.is-danger:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
|
||||
.card-footer .card-footer-item.is-warning {
|
||||
background-color: sass.$warning;
|
||||
}
|
||||
|
||||
.card-footer .card-footer-item.is-warning:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
/*******/
|
||||
/* MAP */
|
||||
/*******/
|
||||
|
||||
.map {
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.marker {
|
||||
background-image: url('../img/logo_transparent.png');
|
||||
background-size: cover;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.animal-shelter-marker {
|
||||
background-image: url('../img/animal_shelter.png');
|
||||
}
|
||||
|
||||
.map-in-content #map {
|
||||
max-height: 500px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 768px) {
|
||||
.maplibregl-popup {
|
||||
max-width: 280px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.maplibregl-popup {
|
||||
max-width: 150px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.maplibregl-popup-close-button {
|
||||
all: unset; /* Remove all inherited styles */
|
||||
font-size: 1.2rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
right: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
|
||||
/*****
|
||||
IMAGES
|
||||
*****/
|
||||
|
||||
.gallery .main-photo img {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
object-fit: cover; /* Crops the images */
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.thumbnail-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.thumbnail img {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
object-fit: cover; /* Crops the images */
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Ensure each thumbnail takes equal width */
|
||||
.thumbnail {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
AN Cards
|
||||
*/
|
||||
|
||||
.an-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
// Fonts
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(../fonts/nunito.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
|
||||
.new-animal-ad fieldset {
|
||||
border-top: 4px solid var(--bulma-text-weak);
|
||||
margin-top: 2em;
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.new-animal-ad * {
|
||||
transition: all ease 0.5s;
|
||||
}
|
||||
|
||||
.new-animal-ad .open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.new-animal-ad .closed {
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.new-animal-ad legend {
|
||||
font-weight: bold;
|
||||
padding-right: 0.2em;
|
||||
color: var(--bulma-label-color);
|
||||
font-size: 130%;
|
||||
}
|
||||
|
||||
.feedback-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.feedback-add-new {
|
||||
width: 40ch;
|
||||
min-height: 40ch;
|
||||
padding: 1.5em;
|
||||
background-color: var(--bulma-info-on-scheme);
|
||||
color: black;
|
||||
}
|
||||
|
||||
.feedback-add-new.error {
|
||||
background-color: var(--bulma-danger-on-scheme);
|
||||
}
|
||||
|
||||
.feedback-add-new.success {
|
||||
background-color: var(--bulma-success-on-scheme);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.notification-container {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.notification-label {
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
/* Make the badge float in the top right corner of the button */
|
||||
.notification-badge {
|
||||
background-color: #fa3e3e;
|
||||
border-radius: 2px;
|
||||
color: white;
|
||||
|
||||
padding: 1px 3px;
|
||||
font-size: 8px;
|
||||
|
||||
position: absolute; /* Position the badge within the relatively positioned button */
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
420
src/fellchensammlung/static/fellchensammlung/css/photoswipe.css
Normal file
@@ -0,0 +1,420 @@
|
||||
/*! PhotoSwipe main CSS by Dmytro Semenov | photoswipe.com */
|
||||
|
||||
.pswp {
|
||||
--pswp-bg: #000;
|
||||
--pswp-placeholder-bg: #222;
|
||||
|
||||
|
||||
--pswp-root-z-index: 100000;
|
||||
|
||||
--pswp-preloader-color: rgba(79, 79, 79, 0.4);
|
||||
--pswp-preloader-color-secondary: rgba(255, 255, 255, 0.9);
|
||||
|
||||
/* defined via js:
|
||||
--pswp-transition-duration: 333ms; */
|
||||
|
||||
--pswp-icon-color: #fff;
|
||||
--pswp-icon-color-secondary: #4f4f4f;
|
||||
--pswp-icon-stroke-color: #4f4f4f;
|
||||
--pswp-icon-stroke-width: 2px;
|
||||
|
||||
--pswp-error-text-color: var(--pswp-icon-color);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Styles for basic PhotoSwipe (pswp) functionality (sliding area, open/close transitions)
|
||||
*/
|
||||
|
||||
.pswp {
|
||||
position: fixed;
|
||||
z-index: var(--pswp-root-z-index);
|
||||
display: none;
|
||||
touch-action: none;
|
||||
outline: 0;
|
||||
opacity: 0.003;
|
||||
contain: layout style size;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Prevents focus outline on the root element,
|
||||
(it may be focused initially) */
|
||||
.pswp:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.pswp * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pswp img {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.pswp--open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pswp,
|
||||
.pswp__bg {
|
||||
transform: translateZ(0);
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
.pswp__bg {
|
||||
opacity: 0.005;
|
||||
background: var(--pswp-bg);
|
||||
}
|
||||
|
||||
.pswp,
|
||||
.pswp__scroll-wrap {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pswp,
|
||||
.pswp__scroll-wrap,
|
||||
.pswp__bg,
|
||||
.pswp__container,
|
||||
.pswp__item,
|
||||
.pswp__content,
|
||||
.pswp__img,
|
||||
.pswp__zoom-wrap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pswp {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.pswp__img,
|
||||
.pswp__zoom-wrap {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.pswp--click-to-zoom.pswp--zoom-allowed .pswp__img {
|
||||
cursor: -webkit-zoom-in;
|
||||
cursor: -moz-zoom-in;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.pswp--click-to-zoom.pswp--zoomed-in .pswp__img {
|
||||
cursor: move;
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.pswp--click-to-zoom.pswp--zoomed-in .pswp__img:active {
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* :active to override grabbing cursor */
|
||||
.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img,
|
||||
.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img:active,
|
||||
.pswp__img {
|
||||
cursor: -webkit-zoom-out;
|
||||
cursor: -moz-zoom-out;
|
||||
cursor: zoom-out;
|
||||
}
|
||||
|
||||
|
||||
/* Prevent selection and tap highlights */
|
||||
.pswp__container,
|
||||
.pswp__img,
|
||||
.pswp__button,
|
||||
.pswp__counter {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.pswp__item {
|
||||
/* z-index for fade transition */
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pswp__hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Allow to click through pswp__content element, but not its children */
|
||||
.pswp__content {
|
||||
pointer-events: none;
|
||||
}
|
||||
.pswp__content > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
|
||||
PhotoSwipe UI
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
Error message appears when image is not loaded
|
||||
(JS option errorMsg controls markup)
|
||||
*/
|
||||
.pswp__error-msg-container {
|
||||
display: grid;
|
||||
}
|
||||
.pswp__error-msg {
|
||||
margin: auto;
|
||||
font-size: 1em;
|
||||
line-height: 1;
|
||||
color: var(--pswp-error-text-color);
|
||||
}
|
||||
|
||||
/*
|
||||
class pswp__hide-on-close is applied to elements that
|
||||
should hide (for example fade out) when PhotoSwipe is closed
|
||||
and show (for example fade in) when PhotoSwipe is opened
|
||||
*/
|
||||
.pswp .pswp__hide-on-close {
|
||||
opacity: 0.005;
|
||||
will-change: opacity;
|
||||
transition: opacity var(--pswp-transition-duration) cubic-bezier(0.4, 0, 0.22, 1);
|
||||
z-index: 10; /* always overlap slide content */
|
||||
pointer-events: none; /* hidden elements should not be clickable */
|
||||
}
|
||||
|
||||
/* class pswp--ui-visible is added when opening or closing transition starts */
|
||||
.pswp--ui-visible .pswp__hide-on-close {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* <button> styles, including css reset */
|
||||
.pswp__button {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 50px;
|
||||
height: 60px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
opacity: 0.85;
|
||||
-webkit-appearance: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.pswp__button:hover,
|
||||
.pswp__button:active,
|
||||
.pswp__button:focus {
|
||||
transition: none;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.pswp__button:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.pswp__icn {
|
||||
fill: var(--pswp-icon-color);
|
||||
color: var(--pswp-icon-color-secondary);
|
||||
}
|
||||
|
||||
.pswp__icn {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 9px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pswp__icn-shadow {
|
||||
stroke: var(--pswp-icon-stroke-color);
|
||||
stroke-width: var(--pswp-icon-stroke-width);
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.pswp__icn:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
div element that matches size of large image,
|
||||
large image loads on top of it,
|
||||
used when msrc is not provided
|
||||
*/
|
||||
div.pswp__img--placeholder,
|
||||
.pswp__img--with-bg {
|
||||
background: var(--pswp-placeholder-bg);
|
||||
}
|
||||
|
||||
.pswp__top-bar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
z-index: 10;
|
||||
|
||||
/* allow events to pass through top bar itself */
|
||||
pointer-events: none !important;
|
||||
}
|
||||
.pswp__top-bar > * {
|
||||
pointer-events: auto;
|
||||
/* this makes transition significantly more smooth,
|
||||
even though inner elements are not animated */
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
|
||||
Close button
|
||||
|
||||
*/
|
||||
.pswp__button--close {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
|
||||
Arrow buttons
|
||||
|
||||
*/
|
||||
.pswp__button--arrow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 75px;
|
||||
height: 100px;
|
||||
top: 50%;
|
||||
margin-top: -50px;
|
||||
}
|
||||
|
||||
.pswp__button--arrow:disabled {
|
||||
display: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.pswp__button--arrow .pswp__icn {
|
||||
top: 50%;
|
||||
margin-top: -30px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.pswp--one-slide .pswp__button--arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* hide arrows on touch screens */
|
||||
.pswp--touch .pswp__button--arrow {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* show arrows only after mouse was used */
|
||||
.pswp--has_mouse .pswp__button--arrow {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.pswp__button--arrow--prev {
|
||||
right: auto;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.pswp__button--arrow--next {
|
||||
right: 0px;
|
||||
}
|
||||
.pswp__button--arrow--next .pswp__icn {
|
||||
left: auto;
|
||||
right: 14px;
|
||||
/* flip horizontally */
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Zoom button
|
||||
|
||||
*/
|
||||
.pswp__button--zoom {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pswp--zoom-allowed .pswp__button--zoom {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* "+" => "-" */
|
||||
.pswp--zoomed-in .pswp__zoom-icn-bar-v {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
|
||||
Loading indicator
|
||||
|
||||
*/
|
||||
.pswp__preloader {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 50px;
|
||||
height: 60px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.pswp__preloader .pswp__icn {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s linear;
|
||||
animation: pswp-clockwise 600ms linear infinite;
|
||||
}
|
||||
|
||||
.pswp__preloader--active .pswp__icn {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
@keyframes pswp-clockwise {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
|
||||
"1 of 10" counter
|
||||
|
||||
*/
|
||||
.pswp__counter {
|
||||
height: 30px;
|
||||
margin: 15px 0 0 20px;
|
||||
font-size: 14px;
|
||||
line-height: 30px;
|
||||
color: var(--pswp-icon-color);
|
||||
text-shadow: 1px 1px 3px var(--pswp-icon-color-secondary);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.pswp--one-slide .pswp__counter {
|
||||
display: none;
|
||||
}
|
@@ -1,27 +1,15 @@
|
||||
:root {
|
||||
--primary-light-one: #5daa68;
|
||||
--primary-light-two: #4a9455;
|
||||
--primary-dark-one: #17311b;
|
||||
--secondary-light-one: #faf1cf;
|
||||
--secondary-light-two: #e1d7b5;
|
||||
--background-one: var(--primary-light-one);
|
||||
--background-two: var(--primary-light-two);
|
||||
--background-three: var(--secondary-light-one);
|
||||
--background-four: var(--primary-dark-one);
|
||||
--highlight-one: var(--primary-dark-one);
|
||||
--highlight-one-text: var(--secondary-light-one);
|
||||
--text-one: var(--secondary-light-one);
|
||||
--shadow-one: var(--primary-dark-one);
|
||||
--text-two: var(--primary-dark-one);
|
||||
--text-three: var(--primary-light-one);
|
||||
--shadow-three: var(--primary-dark-one);
|
||||
--primary: #6CD4FF;
|
||||
--link: #292a2c;
|
||||
--grey-light: #c4c6ce;
|
||||
--grey-dark: #262728;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--background-one);
|
||||
color: var(--text-one);
|
||||
background-color: hsl(221, 14%, 100%)r;
|
||||
color: #000000;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -32,24 +20,22 @@ body {
|
||||
|
||||
|
||||
alert-box {
|
||||
color: var(--highlight-one);
|
||||
display: block;
|
||||
margin: 3rem 0;
|
||||
padding: 2rem 3rem;
|
||||
border: 1px solid var(--highlight-one);
|
||||
border-left-width: .5rem;
|
||||
border-radius: .4rem;
|
||||
background-color: var(--background-three);
|
||||
background-color: var(--primary);
|
||||
|
||||
a {
|
||||
color: var(--text-three);
|
||||
text-decoration: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +51,7 @@ a {
|
||||
margin: 1rem;
|
||||
padding: 5px;
|
||||
border-radius: .4rem;
|
||||
background-color: var(--background-one);
|
||||
border: 3px solid var(--primary);
|
||||
}
|
||||
|
||||
.post-summary h1 {
|
||||
@@ -79,8 +65,7 @@ a {
|
||||
}
|
||||
|
||||
.navigation-sticky {
|
||||
background-color: var(--secondary-light-one);
|
||||
color: var(--primary-light-one);
|
||||
background-color: var(--primary);
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
border-bottom-right-radius: 8px;
|
||||
|
@@ -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;
|
||||
@@ -98,11 +135,16 @@ textarea {
|
||||
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);
|
||||
}
|
||||
@@ -116,8 +158,8 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
.spaced {
|
||||
margin-bottom: 30px;
|
||||
.spaced > * {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
/*******************************/
|
||||
@@ -140,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;
|
||||
@@ -201,7 +242,11 @@ select, .button {
|
||||
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;
|
||||
@@ -210,7 +255,162 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
/*********************/
|
||||
@@ -226,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;
|
||||
}
|
||||
@@ -431,9 +711,6 @@ select, .button {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.photos {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -468,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;
|
||||
}
|
||||
@@ -499,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 */
|
||||
@@ -519,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 {
|
||||
@@ -542,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%;
|
||||
}
|
||||
|
||||
@@ -570,7 +869,7 @@ select, .button {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.comment, .notification {
|
||||
.comment, .notification, .search-subscription {
|
||||
flex: 1 100%;
|
||||
margin: 10px;
|
||||
border-radius: 8px;
|
||||
@@ -580,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;
|
||||
@@ -595,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -615,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 */
|
||||
/************************/
|
||||
@@ -631,6 +968,14 @@ select, .button {
|
||||
border: rgba(17, 58, 224, 0.51) 4px solid;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #370707;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error::before {
|
||||
content: "⚠️";
|
||||
}
|
||||
|
||||
/*******/
|
||||
/* MAP */
|
||||
@@ -644,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/fonts/nunito.woff2
Normal file
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 1.0 MiB |
After Width: | Height: | Size: 103 KiB |
After Width: | Height: | Size: 546 B |
BIN
src/fellchensammlung/static/fellchensammlung/img/pin.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 4.8 KiB |
BIN
src/fellchensammlung/static/fellchensammlung/img/sexes/Male.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
@@ -0,0 +1,423 @@
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// ------------------------------------------------ functions
|
||||
var show = function (elem) {
|
||||
// Get the natural height of the element
|
||||
var getHeight = function () {
|
||||
elem.style.display = 'block'; // Make it visible
|
||||
var height = elem.scrollHeight + 'px'; // Get its height
|
||||
elem.style.display = ''; // Hide it again
|
||||
return height;
|
||||
};
|
||||
var height = getHeight(); // Get the natural height
|
||||
elem.classList.remove('closed');
|
||||
elem.classList.add('open'); // Make the element visible
|
||||
elem.setAttribute('aria-hidden', 'false');
|
||||
elem.style.height = height; // Update the max-height
|
||||
// Once the transition is complete, remove the inline max-height so the content can scale responsively
|
||||
window.setTimeout(function () {
|
||||
elem.style.height = '';
|
||||
}, 500);
|
||||
};
|
||||
|
||||
var hide = function (elem) {
|
||||
// Give the element a height to change from
|
||||
elem.style.height = elem.scrollHeight + 'px';
|
||||
// Set the height back to 0
|
||||
window.setTimeout(function () {
|
||||
elem.style.height = '0';
|
||||
}, 1);
|
||||
// When the transition is complete, hide it
|
||||
window.setTimeout(function () {
|
||||
elem.classList.remove('open');
|
||||
elem.classList.add('closed');
|
||||
elem.setAttribute('aria-hidden', 'true');
|
||||
}, 500);
|
||||
};
|
||||
|
||||
var toggle = function (elem, timing) {
|
||||
// If the element is visible, hide it
|
||||
if (elem.classList.contains('open')) {
|
||||
hide(elem);
|
||||
return;
|
||||
}
|
||||
// Otherwise, show it
|
||||
show(elem);
|
||||
};
|
||||
|
||||
|
||||
// ------------------------------------------------ build form
|
||||
let orig_form = document.querySelector('form');
|
||||
orig_form.style.display = 'none';
|
||||
|
||||
let an_max = 6;
|
||||
|
||||
let an_fieldset = document.createElement('fieldset');
|
||||
an_fieldset.classList.add('cell');
|
||||
let an_fieldset_legend = document.createElement('legend');
|
||||
an_fieldset_legend.innerHTML = "Allgemeines";
|
||||
an_fieldset.appendChild(an_fieldset_legend);
|
||||
|
||||
let an_name = document.createElement('input');
|
||||
an_name.setAttribute('type', 'text');
|
||||
an_name.setAttribute('name', 'name');
|
||||
an_name.setAttribute('class', 'input');
|
||||
an_name.setAttribute('maxlength', 200);
|
||||
an_name.setAttribute('required', 'required');
|
||||
let an_name_label = document.createElement('label');
|
||||
an_name_label.setAttribute('class', 'label');
|
||||
an_name_label.innerHTML = 'Titel der Vermittlung';
|
||||
an_name_label.appendChild(an_name);
|
||||
|
||||
let an_location_string = document.createElement('input');
|
||||
an_location_string.setAttribute('type', 'text');
|
||||
an_location_string.setAttribute('name', 'location_string');
|
||||
an_location_string.setAttribute('class', 'input');
|
||||
an_location_string.setAttribute('maxlength', 200);
|
||||
an_location_string.setAttribute('required', 'required');
|
||||
let an_location_string_label = document.createElement('label');
|
||||
an_location_string_label.setAttribute('class', 'label');
|
||||
an_location_string_label.innerHTML = 'Ortsangabe';
|
||||
an_location_string_label.appendChild(an_location_string);
|
||||
|
||||
let an_further_information = document.createElement('input');
|
||||
an_further_information.setAttribute('type', 'url');
|
||||
an_further_information.setAttribute('name', 'further_information');
|
||||
an_further_information.setAttribute('class', 'input');
|
||||
an_further_information.setAttribute('maxlength', 200);
|
||||
let an_further_information_label = document.createElement('label');
|
||||
an_further_information_label.setAttribute('class', 'label');
|
||||
an_further_information_label.innerHTML = 'Link zu mehr Informationen';
|
||||
an_further_information_label.appendChild(an_further_information);
|
||||
|
||||
let an_species = document.createElement('select');
|
||||
let an_species_rat = document.createElement('option');
|
||||
an_species_rat.value = 1;
|
||||
an_species_rat.innerHTML = "Farbratte";
|
||||
an_species.appendChild(an_species_rat);
|
||||
an_species.setAttribute('name', 'species');
|
||||
an_species.setAttribute('class', 'input');
|
||||
an_species.setAttribute('required', 'required');
|
||||
let an_species_label = document.createElement('label');
|
||||
an_species_label.setAttribute('class', 'label');
|
||||
an_species_label.innerHTML = 'Tierart';
|
||||
an_species_label.appendChild(an_species);
|
||||
|
||||
let an_number = document.createElement('input');
|
||||
an_number.setAttribute('type', 'number');
|
||||
an_number.setAttribute('name', 'number');
|
||||
an_number.setAttribute('class', 'input');
|
||||
an_number.setAttribute('min', 1);
|
||||
an_number.setAttribute('max', an_max);
|
||||
an_number.setAttribute('required', 'required');
|
||||
let an_number_label = document.createElement('label');
|
||||
an_number_label.setAttribute('class', 'label');
|
||||
an_number_label.innerHTML = 'Anzahl Tiere';
|
||||
an_number_label.appendChild(an_number);
|
||||
|
||||
let an_dateofbirth = document.createElement('input');
|
||||
an_dateofbirth.setAttribute('type', 'date');
|
||||
an_dateofbirth.setAttribute('name', 'dateofbirth');
|
||||
an_dateofbirth.setAttribute('class', 'input');
|
||||
an_dateofbirth.setAttribute('maxlength', 200);
|
||||
an_dateofbirth.setAttribute('required', 'required');
|
||||
let an_dateofbirth_label = document.createElement('label');
|
||||
an_dateofbirth_label.setAttribute('class', 'label');
|
||||
an_dateofbirth_label.innerHTML = 'Geburtsdatum';
|
||||
an_dateofbirth_label.appendChild(an_dateofbirth);
|
||||
|
||||
let an_sex = document.createElement('select');
|
||||
let an_sex_F = document.createElement('option');
|
||||
an_sex_F.value = 'F';
|
||||
an_sex_F.innerHTML = "Weiblich";
|
||||
an_sex.appendChild(an_sex_F);
|
||||
let an_sex_M = document.createElement('option');
|
||||
an_sex_M.value = 'M';
|
||||
an_sex_M.innerHTML = "Männlich";
|
||||
an_sex.appendChild(an_sex_M);
|
||||
let an_sex_F_N = document.createElement('option');
|
||||
an_sex_F_N.value = 'F_N';
|
||||
an_sex_F_N.innerHTML = "Weiblich, kastriert";
|
||||
an_sex.appendChild(an_sex_F_N);
|
||||
let an_sex_M_N = document.createElement('option');
|
||||
an_sex_M_N.value = 'M_N';
|
||||
an_sex_M_N.innerHTML = "Männlich, kastriert";
|
||||
an_sex.appendChild(an_sex_M_N);
|
||||
let an_sex_I = document.createElement('option');
|
||||
an_sex_I.value = 'I';
|
||||
an_sex_I.innerHTML = "Intergeschlechtlich";
|
||||
an_sex.appendChild(an_sex_I);
|
||||
an_sex.setAttribute('name', 'sex');
|
||||
an_sex.setAttribute('class', 'input');
|
||||
an_sex.setAttribute('required', 'required');
|
||||
let an_sex_label = document.createElement('label');
|
||||
an_sex_label.setAttribute('class', 'label');
|
||||
an_sex_label.innerHTML = 'Geschlecht';
|
||||
an_sex_label.appendChild(an_sex);
|
||||
|
||||
let an_searching_since = document.createElement('input');
|
||||
an_searching_since.setAttribute('type', 'date');
|
||||
an_searching_since.setAttribute('name', 'searching_since');
|
||||
an_searching_since.setAttribute('class', 'input');
|
||||
an_searching_since.setAttribute('maxlength', 200);
|
||||
an_searching_since.setAttribute('required', 'required');
|
||||
let an_searching_since_label = document.createElement('label');
|
||||
an_searching_since_label.setAttribute('class', 'label');
|
||||
an_searching_since_label.innerHTML = 'neues Zuhause gesucht seit';
|
||||
an_searching_since_label.appendChild(an_searching_since);
|
||||
|
||||
let an_group_only = document.createElement('select');
|
||||
let an_group_only_yes = document.createElement('option');
|
||||
an_group_only_yes.value = 1;
|
||||
an_group_only_yes.innerHTML = "nur zusammen";
|
||||
let an_group_only_no = document.createElement('option');
|
||||
an_group_only_no.value = 0;
|
||||
an_group_only_no.innerHTML = "auch einzeln";
|
||||
an_group_only.appendChild(an_group_only_yes);
|
||||
an_group_only.appendChild(an_group_only_no);
|
||||
an_group_only.setAttribute('name', 'group_only');
|
||||
an_group_only.setAttribute('class', 'input');
|
||||
an_group_only.setAttribute('required', 'required');
|
||||
let an_group_only_label = document.createElement('label');
|
||||
an_group_only_label.setAttribute('class', 'label');
|
||||
an_group_only_label.innerHTML = 'Gruppenvermittlung';
|
||||
an_group_only_label.appendChild(an_group_only);
|
||||
|
||||
|
||||
let animals = document.createElement('fieldset');
|
||||
animals.classList.add('cell', 'is-col-span-2');
|
||||
let animals_legend = document.createElement('legend');
|
||||
animals_legend.innerHTML = 'Angaben zu den Tieren';
|
||||
animals.appendChild(animals_legend);
|
||||
let noteNumber = document.createElement('p');
|
||||
noteNumber.setAttribute('id', 'noteNumber');
|
||||
noteNumber.innerHTML = 'Bitte Anzahl Tiere angeben';
|
||||
animals.appendChild(noteNumber);
|
||||
|
||||
let an_description = document.createElement('textarea');
|
||||
an_description.setAttribute('name', 'an_description');
|
||||
an_description.classList.add('input', 'textarea');
|
||||
let an_description_label = document.createElement('label');
|
||||
an_description_label.innerHTML = 'Beschreibung der Gruppe';
|
||||
an_description_label.classList.add('label');
|
||||
an_description_label.appendChild(an_description);
|
||||
animals.appendChild(an_group_only_label);
|
||||
animals.appendChild(an_description_label);
|
||||
|
||||
for (let i = 0; i < an_max; i++) {
|
||||
let an_fieldset_$i = document.createElement('fieldset');
|
||||
an_fieldset_$i.classList.add('animal-' + i, 'animal');
|
||||
an_fieldset_$i.appendChild(document.createElement('legend'));
|
||||
an_fieldset_$i.querySelector('legend').innerHTML = 'Tier ' + parseInt(i + 1);
|
||||
let an_name_$i = document.createElement('input');
|
||||
an_name_$i.setAttribute('type', 'text');
|
||||
an_name_$i.setAttribute('name', 'name-' + i);
|
||||
an_name_$i.setAttribute('class', 'input');
|
||||
an_name_$i.setAttribute('maxlength', 200);
|
||||
an_name_$i.setAttribute('required', 'required');
|
||||
let an_name_$i_label = document.createElement('label');
|
||||
an_name_$i_label.setAttribute('class', 'label');
|
||||
an_name_$i_label.innerHTML = 'Name';
|
||||
an_name_$i_label.appendChild(an_name_$i);
|
||||
|
||||
let an_dateofbirth_$i = document.createElement('input');
|
||||
an_dateofbirth_$i.setAttribute('type', 'date');
|
||||
an_dateofbirth_$i.setAttribute('name', 'dateofbirth');
|
||||
an_dateofbirth_$i.setAttribute('class', 'input');
|
||||
an_dateofbirth_$i.setAttribute('maxlength', 200);
|
||||
an_dateofbirth_$i.setAttribute('required', 'required');
|
||||
let an_dateofbirth_$i_label = document.createElement('label');
|
||||
an_dateofbirth_$i_label.setAttribute('class', 'label');
|
||||
an_dateofbirth_$i_label.innerHTML = 'Geburtsdatum';
|
||||
an_dateofbirth_$i_label.appendChild(an_dateofbirth_$i);
|
||||
|
||||
let an_sex_$i = document.createElement('select');
|
||||
let an_sex_F = document.createElement('option');
|
||||
an_sex_F.value = 'F';
|
||||
an_sex_F.innerHTML = "Weiblich";
|
||||
an_sex_$i.appendChild(an_sex_F);
|
||||
let an_sex_M = document.createElement('option');
|
||||
an_sex_M.value = 'M';
|
||||
an_sex_M.innerHTML = "Männlich";
|
||||
an_sex_$i.appendChild(an_sex_M);
|
||||
let an_sex_F_N = document.createElement('option');
|
||||
an_sex_F_N.value = 'F_N';
|
||||
an_sex_F_N.innerHTML = "Weiblich, kastriert";
|
||||
an_sex_$i.appendChild(an_sex_F_N);
|
||||
let an_sex_M_N = document.createElement('option');
|
||||
an_sex_M_N.value = 'M_N';
|
||||
an_sex_M_N.innerHTML = "Männlich, kastriert";
|
||||
an_sex_$i.appendChild(an_sex_M_N);
|
||||
let an_sex_I = document.createElement('option');
|
||||
an_sex_I.value = 'I';
|
||||
an_sex_I.innerHTML = "Intergeschlechtlich";
|
||||
an_sex_$i.appendChild(an_sex_I);
|
||||
an_sex_$i.setAttribute('name', 'sex');
|
||||
an_sex_$i.setAttribute('class', 'input');
|
||||
an_sex_$i.setAttribute('required', 'required');
|
||||
let an_sex_$i_label = document.createElement('label');
|
||||
an_sex_$i_label.setAttribute('class', 'label');
|
||||
an_sex_$i_label.innerHTML = 'Geschlecht';
|
||||
an_sex_$i_label.appendChild(an_sex_$i);
|
||||
|
||||
let an_description_$i = document.createElement('textarea');
|
||||
an_description_$i.setAttribute('name', 'an_description');
|
||||
an_description_$i.classList.add('input', 'textarea');
|
||||
let an_description_$i_label = document.createElement('label');
|
||||
an_description_$i_label.innerHTML = 'Beschreibung';
|
||||
an_description_$i_label.classList.add('label');
|
||||
an_description_$i_label.appendChild(an_description_$i);
|
||||
|
||||
an_fieldset_$i.appendChild(an_description_$i_label);
|
||||
an_fieldset_$i.appendChild(an_name_$i_label);
|
||||
an_fieldset_$i.appendChild(an_dateofbirth_$i_label);
|
||||
an_fieldset_$i.appendChild(an_sex_$i_label);
|
||||
an_fieldset_$i.appendChild(an_description_$i_label);
|
||||
animals.appendChild(an_fieldset_$i);
|
||||
}
|
||||
|
||||
|
||||
an_fieldset.appendChild(an_name_label);
|
||||
an_fieldset.appendChild(an_location_string_label);
|
||||
an_fieldset.appendChild(an_further_information_label);
|
||||
an_fieldset.appendChild(an_species_label);
|
||||
an_fieldset.appendChild(an_number_label);
|
||||
an_fieldset.appendChild(an_dateofbirth_label);
|
||||
an_fieldset.appendChild(an_sex_label);
|
||||
an_fieldset.appendChild(an_searching_since_label);
|
||||
|
||||
let new_form = document.createElement('form');
|
||||
new_form.classList.add('new-animal-ad', 'fixed-grid', 'has-3-cols', 'has-1-cols-mobile');
|
||||
let div = document.createElement('div');
|
||||
div.classList.add('grid');
|
||||
let sButton = document.createElement('button');
|
||||
sButton.classList.add('button');
|
||||
sButton.innerHTML = "Abschicken";
|
||||
|
||||
div.appendChild(an_fieldset);
|
||||
div.appendChild(animals);
|
||||
div.appendChild(sButton);
|
||||
new_form.appendChild(div);
|
||||
document.querySelector('.main-content').appendChild(new_form);
|
||||
|
||||
// ------------------------------------------------ listeners
|
||||
// number of animals
|
||||
let tmpAnimal;
|
||||
an_number.addEventListener('change', function () {
|
||||
if (an_number.value > 0) {
|
||||
hide(noteNumber);
|
||||
} else {
|
||||
show(noteNumber);
|
||||
}
|
||||
if (an_number.value < 2) {
|
||||
hide(an_description_label);
|
||||
hide(an_group_only_label);
|
||||
an_group_only.selectedIndex = 1;
|
||||
} else {
|
||||
show(an_description_label);
|
||||
show(an_group_only_label);
|
||||
an_group_only.selectedIndex = 0;
|
||||
}
|
||||
for (let i = 0; i < an_max; i++) {
|
||||
tmpAnimal = document.querySelector('.animal-' + i);
|
||||
if (i < an_number.value) {
|
||||
tmpAnimal.removeAttribute('disabled');
|
||||
show(tmpAnimal);
|
||||
} else {
|
||||
tmpAnimal.setAttribute('disabled', 'true');
|
||||
hide(tmpAnimal);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// sex
|
||||
an_sex.addEventListener('change', function () {
|
||||
for (let i = 0; i < an_max; i++) {
|
||||
let selList = document.querySelector('.animal-' + i).querySelector('[name="sex"]');
|
||||
for (let j = 0; j < selList.options.length; j++) {
|
||||
if (selList.options[j].value == an_sex.value) {
|
||||
selList.selectedIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// date of birth
|
||||
an_dateofbirth.addEventListener('change', function () {
|
||||
for (let i = 0; i < an_max; i++) {
|
||||
document.querySelector('.animal-' + i).querySelector('[name="dateofbirth"]').value = an_dateofbirth.value;
|
||||
}
|
||||
});
|
||||
|
||||
// ------------------------------------------------ initialise
|
||||
show(noteNumber);
|
||||
hide(an_description_label);
|
||||
hide(an_group_only_label);
|
||||
for (let i = 0; i < an_max; i++) {
|
||||
hide(document.querySelector('.animal-' + i));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------- submit
|
||||
new_form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
let date = new Date();
|
||||
let postDate = date.toISOString().slice(0, 10);
|
||||
const path = '';
|
||||
|
||||
let elResultsBd = document.createElement('div');
|
||||
elResultsBd.classList.add('feedback-backdrop');
|
||||
let elResults = document.createElement('div');
|
||||
elResults.classList.add('feedback-add-new');
|
||||
elResultsBd.appendChild(elResults);
|
||||
document.querySelector('body').appendChild(elResultsBd);
|
||||
|
||||
let data = JSON.stringify({
|
||||
"created_at": postDate,
|
||||
"searching_since": an_searching_since.value,
|
||||
"name": an_name.value,
|
||||
"description": an_description.value,
|
||||
"further_information": an_further_information.value,
|
||||
"group_only": an_group_only.value,
|
||||
"location_string": an_location_string.value,
|
||||
});
|
||||
|
||||
async function submitAN() {
|
||||
const csrftoken = getCookie('csrftoken');
|
||||
let response = await fetch('http://localhost:8000/api/adoption_notice', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
'X-CSRFToken': csrftoken,
|
||||
},
|
||||
body: data,
|
||||
});
|
||||
console.log(response.status);
|
||||
if (response.status === 201) {
|
||||
let result = await response.json();
|
||||
elResults.textContent = result.message + '<br>neue Id: ' + result.id;
|
||||
elResults.classList.add('success');
|
||||
} else {
|
||||
elResults.textContent = 'Fehler! Status Code: ' + response.status;
|
||||
elResults.classList.add('error');
|
||||
}
|
||||
}
|
||||
|
||||
submitAN();
|
||||
});
|
||||
});
|
24
src/fellchensammlung/static/fellchensammlung/js/custom.js
Normal file
@@ -0,0 +1,24 @@
|
||||
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, "", "")
|
||||
}
|
||||
|
||||
function truncate(str, n, url){
|
||||
return (str.length > n) ? str.slice(0, n-1) + '<a href="' + url + '">…</a>' : str;
|
||||
};
|
2
src/fellchensammlung/static/fellchensammlung/js/jquery.min.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import PhotoSwipeLightbox from './photoswipe-lightbox.esm.js';
|
||||
|
||||
const lightbox = new PhotoSwipeLightbox({
|
||||
gallery: '.gallery',
|
||||
children: 'a',
|
||||
pswpModule: () => import('https://unpkg.com/photoswipe'),
|
||||
});
|
||||
|
||||
lightbox.init();
|
||||
|
||||
|
72
src/fellchensammlung/static/fellchensammlung/js/toggles.js
Normal file
@@ -0,0 +1,72 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Get all "navbar-burger" elements
|
||||
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
|
||||
|
||||
// Add a click event on each of them
|
||||
$navbarBurgers.forEach( el => {
|
||||
el.addEventListener('click', () => {
|
||||
|
||||
// Get the target from the "data-target" attribute
|
||||
const target = el.dataset.target;
|
||||
const $target = document.getElementById(target);
|
||||
|
||||
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
|
||||
el.classList.toggle('is-active');
|
||||
$target.classList.toggle('is-active');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Looks for all notifications with a delete and allows closing them when pressing delete
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
|
||||
const $notification = $delete.parentNode;
|
||||
|
||||
$delete.addEventListener('click', () => {
|
||||
$notification.parentNode.removeChild($notification);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
(document.querySelectorAll('.message .delete') || []).forEach(($delete) => {
|
||||
$delete.addEventListener('click', () => {
|
||||
const message = $delete.closest('.message');
|
||||
if (message) {
|
||||
message.remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
// DROPDOWNS
|
||||
const $clickableDropdowns = document.querySelectorAll(
|
||||
".dropdown:not(.is-hoverable)",
|
||||
);
|
||||
|
||||
if ($clickableDropdowns.length > 0) {
|
||||
$clickableDropdowns.forEach(($dropdown) => {
|
||||
if (!$dropdown.querySelector("button")) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dropdown.querySelector("button").addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
$dropdown.classList.toggle("is-active");
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("click", () => {
|
||||
closeDropdowns();
|
||||
});
|
||||
}
|
||||
|
||||
function closeDropdowns() {
|
||||
$clickableDropdowns.forEach(($el) => {
|
||||
$el.classList.remove("is-active");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
37
src/fellchensammlung/static/fellchensammlung/js/turf.min.js
vendored
Normal file
7
src/fellchensammlung/static/robots.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
User-agent: *
|
||||
Disallow: /admin/
|
||||
|
||||
User-agent: OpenAI
|
||||
Disallow: /
|
||||
|
||||
Sitemap: https://notfellchen.org/sitemap.xml
|
@@ -1,10 +1,15 @@
|
||||
import logging
|
||||
|
||||
from celery.app import shared_task
|
||||
from django.utils import timezone
|
||||
from notfellchen.celery import app as celery_app
|
||||
from .mail import send_notification_email
|
||||
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices
|
||||
from .tools.fedi import post_an_to_fedi
|
||||
from .tools.misc import healthcheck_ok
|
||||
from .models import Location, AdoptionNotice, Timestamp
|
||||
from .models import Location, AdoptionNotice, Timestamp, RescueOrganization, SocialMediaPost
|
||||
from .tools.notifications import notify_of_AN_to_be_checked
|
||||
from .tools.search import notify_search_subscribers
|
||||
|
||||
|
||||
def set_timestamp(key: str):
|
||||
@@ -34,11 +39,22 @@ def task_deactivate_unchecked():
|
||||
set_timestamp("task_deactivate_404_adoption_notices")
|
||||
|
||||
|
||||
@celery_app.task(name="commit.add_location")
|
||||
def add_adoption_notice_location(pk):
|
||||
@celery_app.task(name="social_media.post_fedi")
|
||||
def task_post_to_fedi():
|
||||
adoption_notice = SocialMediaPost.get_an_to_post()
|
||||
post_an_to_fedi(adoption_notice)
|
||||
set_timestamp("task_social_media.post_fedi")
|
||||
|
||||
|
||||
@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")
|
||||
@@ -50,3 +66,11 @@ def task_healthcheck():
|
||||
@shared_task
|
||||
def task_send_notification_email(notification_pk):
|
||||
send_notification_email(notification_pk)
|
||||
|
||||
|
||||
@celery_app.task(name="commit.post_rescue_org_save")
|
||||
def post_rescue_org_save(pk):
|
||||
instance = RescueOrganization.objects.get(pk=pk)
|
||||
Location.add_location_to_object(instance)
|
||||
set_timestamp("add_rescue_org_location")
|
||||
logging.info(f"Location was added to Rescue Organization {pk}")
|
||||
|
@@ -1,30 +1,38 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% extends "fellchensammlung/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
|
||||
{% block title %}<title>{% translate "Über uns und Regeln" %}</title>{% endblock %}
|
||||
{% block title %}<title>{% translate "Über uns" %}</title>{% endblock %}
|
||||
{% block og_title %}
|
||||
<meta property="og:title" content="{% translate "Über uns" %} - Notfellchen"/>
|
||||
{% endblock %}
|
||||
{% block description %}
|
||||
<meta name="description" content="{% translate 'Erfahre mehr über das Notfellchen-Projekt. Im FAQ werden häufige Fragen beantwortet.' %}">
|
||||
{% endblock %}
|
||||
{% block og_description %}
|
||||
<meta name="og:description" content="{% translate 'Erfahre mehr über das Notfellchen-Projekt. Im FAQ werden häufige Fragen beantwortet.' %}">
|
||||
{% endblock %}
|
||||
{% block canonical_url %}{% host %}{% url 'about' %}{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% if about_us %}
|
||||
<h1>{{ about_us.title }}</h1>
|
||||
{{ about_us.content | render_markdown }}
|
||||
<div class="block">
|
||||
<h1 class="title is-1">{{ about_us.title }}</h1>
|
||||
<div class="content">
|
||||
{{ about_us.content | render_markdown }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h1>{% translate "Regeln" %}</h1>
|
||||
{% include "fellchensammlung/lists/list-rules.html" %}
|
||||
|
||||
{% if privacy_statement %}
|
||||
<h1>{{ privacy_statement.title }}</h1>
|
||||
{{ privacy_statement.content | render_markdown }}
|
||||
{% endif %}
|
||||
|
||||
{% if terms_of_service %}
|
||||
<h1>{{ terms_of_service.title }}</h1>
|
||||
{{ terms_of_service.content | render_markdown }}
|
||||
{% endif %}
|
||||
|
||||
{% if imprint %}
|
||||
<h1>{{ imprint.title }}</h1>
|
||||
{{ imprint.content | render_markdown }}
|
||||
{% if faq %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-header-title">{{ faq.title }}</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
{{ faq.content | render_markdown }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@@ -0,0 +1,55 @@
|
||||
{% extends "fellchensammlung/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
|
||||
{% block title %}<title>{% translate "Tierschutzorganisationen" %}</title>{% endblock %}
|
||||
|
||||
{% block og_title %}
|
||||
<meta property="og:title" content="{% translate "Tierschutzorganisationen" %} - Notfellchen"/>
|
||||
{% endblock %}
|
||||
{% block description %}
|
||||
<meta name="description" content="{% translate 'Finde Tierschutzorganisationen in deiner Gegend.' %}">
|
||||
{% endblock %}
|
||||
{% block og_description %}
|
||||
<meta name="og:description" content="{% translate 'Finde Tierschutzorganisationen in deiner Gegend.' %}">
|
||||
{% endblock %}
|
||||
{% block canonical_url %}{% host %}{% url 'rescue-organizations' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block">
|
||||
<div style="height: 70vh">
|
||||
{% include "fellchensammlung/partials/partial-map.html" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
{% with rescue_organizations=rescue_organizations_to_list %}
|
||||
{% include "fellchensammlung/lists/list-animal-shelters.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<nav class="pagination" role="navigation" aria-label="{% trans 'Paginierung' %}">
|
||||
{% if rescue_organizations_to_list.has_previous %}
|
||||
<a class="pagination-previous"
|
||||
href="?page={{ rescue_organizations_to_list.previous_page_number }}">{% trans 'Vorherige' %}</a>
|
||||
{% endif %}
|
||||
{% if rescue_organizations_to_list.has_next %}
|
||||
<a class="pagination-next" href="?page={{ rescue_organizations_to_list.next_page_number }}">{% trans 'Nächste' %}</a>
|
||||
{% endif %}
|
||||
<ul class="pagination-list">
|
||||
{% for page in elided_page_range %}
|
||||
{% if page != "…" %}
|
||||
<li>
|
||||
<a href="?page={{ page }}"
|
||||
class="pagination-link {% if page == rescue_organizations_to_list.number %} is-current{% endif %}"
|
||||
aria-label="{% blocktranslate %}Gehe zu Seite {{ page }}.{% endblocktranslate %}">
|
||||
{{ page }}
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<span aria-hidden="true" class="pagination-ellipsis">…</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endblock %}
|
@@ -1,19 +1,33 @@
|
||||
{% load custom_tags %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="{{ LANGUAGE_CODE }}">
|
||||
<head>
|
||||
{% block title %}{% endblock %}
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="{% translate "Farbratten aus dem Tierschutz finden und adoptieren" %}">
|
||||
{% block title %}{% endblock %}
|
||||
{% block og_title %}{% endblock %}
|
||||
{% block description %}{% endblock %}
|
||||
{% block og_description %}{% endblock %}
|
||||
{% block og_image %}{% endblock %}
|
||||
<link rel="canonical" href="{% block canonical_url %}{% endblock %}">
|
||||
|
||||
<!-- Add additional CSS in static file -->
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'fellchensammlung/css/styles.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'fellchensammlung/css/main.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'fellchensammlung/css/photoswipe.css' %}">
|
||||
<link href="{% static 'fontawesomefree/css/fontawesome.css' %}" rel="stylesheet" type="text/css">
|
||||
<link href="{% static 'fontawesomefree/css/brands.css' %}" rel="stylesheet" type="text/css">
|
||||
<link href="{% static 'fontawesomefree/css/solid.css' %}" rel="stylesheet" type="text/css">
|
||||
|
||||
<script src="{% static 'fellchensammlung/js/custom.js' %}"></script>
|
||||
<script src="{% static 'fellchensammlung/js/toggles.js' %}"></script>
|
||||
<script src="{% static 'fellchensammlung/js/jquery.min.js' %}"></script>
|
||||
<script type="module" src="{% static 'fellchensammlung/js/photoswipe.js' %}"></script>
|
||||
{% block additional_scrips %}{% endblock %}
|
||||
|
||||
|
||||
<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' %}">
|
||||
@@ -23,12 +37,13 @@
|
||||
{% block header %}
|
||||
{% include "fellchensammlung/header.html" %}
|
||||
{% endblock %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-sm-10 content-box">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="main-content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
|
||||
{% block footer %}
|
||||
{% include "fellchensammlung/footer.html" %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
13
src/fellchensammlung/templates/fellchensammlung/contacts.vcf
Normal file
@@ -0,0 +1,13 @@
|
||||
{% for contact in contacts %}
|
||||
BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
N:{{contact.name|safe}};;;;
|
||||
FN:{{contact.name|safe}}
|
||||
{% if contact.location %}GEO:geo:{{contact.location.latitude}},{{contact.location.longitude}}
|
||||
{% endif %}TEL;TYPE=work:{{ contact.phone_number }}
|
||||
{% if contact.email %}EMAIL:{{ contact.email }}
|
||||
{% endif %}REV:{{ current_time|date:'Ymd' }}T{{ current_time|date:'His' }}Z
|
||||
CATEGORIES:{{ categories }}
|
||||
{% if contact.website %}URL:{{ contact.website }}
|
||||
{% endif %}END:VCARD
|
||||
{% endfor %}
|
@@ -0,0 +1,260 @@
|
||||
{% extends "fellchensammlung/base.html" %}
|
||||
{% load custom_tags %}
|
||||
{% load admin_urls %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}<title>{{ adoption_notice.name }}</title>{% endblock %}
|
||||
{% block og_title %}
|
||||
<meta property="og:title" content="{{ adoption_notice.name }} - Notfellchen"/>
|
||||
{% endblock %}
|
||||
{% block description %}
|
||||
<meta name="description" content="{{ adoption_notice.description }}">
|
||||
{% endblock %}
|
||||
{% block og_description %}
|
||||
<meta name="og:description" content="{{ adoption_notice.description }}">
|
||||
{% endblock %}
|
||||
{% block canonical_url %}{% host %}
|
||||
{% url 'adoption-notice-detail' adoption_notice_id=adoption_notice.id %}{% endblock %}
|
||||
|
||||
{% block og_image %}
|
||||
{% if adoption_notice.get_photos %}
|
||||
<meta property="og:image" content="{{ MEDIA_URL }}{{ adoption_notice.get_photos.0.image }}"/>
|
||||
{% else %}
|
||||
<meta property="og:image" content="{% static 'fellchensammlung/img/link_preview.png' %}"/>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if adoption_notice.is_closed %}
|
||||
<article class="message is-warning">
|
||||
<div class="message-header">
|
||||
<p>{% translate 'Vermittlung deaktiviert' %}</p>
|
||||
</div>
|
||||
<div class="message-body content">
|
||||
{% blocktranslate %}
|
||||
Diese Vermittlung wurde deaktiviert. Typischerweise passiert das, wenn die Tiere erfolgreich
|
||||
vermittelt wurden.
|
||||
In den Kommentaren findest du ggf. mehr Informationen.
|
||||
{% endblocktranslate %}
|
||||
</div>
|
||||
</article>
|
||||
{% endif %}
|
||||
<div class="columns">
|
||||
<div class="column is-two-thirds">
|
||||
<!--- Title level (including action dropdown) -->
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<p class="title is-3 is-size-4-mobile">{{ adoption_notice.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<div class="dropdown is-right">
|
||||
<div class="dropdown-trigger">
|
||||
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu4">
|
||||
<span><i class="fas fa-gear" aria-label="{% trans 'Aktionen' %}"></i></span>
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-angle-down" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<!--- Action menu (dropdown) --->
|
||||
<div class="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content">
|
||||
{% if is_subscribed %}
|
||||
<form class="dropdown-item" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="unsubscribe">
|
||||
|
||||
<button type="submit" id="submit">
|
||||
<i class="fas fa-bell-slash fa-fw"
|
||||
aria-hidden="true"></i> {% trans 'Deabonnieren' %}
|
||||
</button>
|
||||
|
||||
</form>
|
||||
{% else %}
|
||||
<form class="dropdown-item" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="subscribe">
|
||||
|
||||
<button type="submit" id="submit">
|
||||
<i class="fas fa-bell fa-fw"
|
||||
aria-hidden="true"></i> {% trans 'Abonnieren' %}
|
||||
</button>
|
||||
|
||||
</form>
|
||||
{% endif %}
|
||||
<hr class="dropdown-divider">
|
||||
|
||||
{% if has_edit_permission %}
|
||||
|
||||
<form class="dropdown-item" method="POST">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="checked_active">
|
||||
<button type="submit" id="submit">
|
||||
<i class="fas fa-check fa-fw"
|
||||
aria-hidden="true"></i> {% trans 'Als aktiv bestätigen' %}
|
||||
</button>
|
||||
</form>
|
||||
<a class="dropdown-item"
|
||||
href="{% url 'adoption-notice-edit' adoption_notice_id=adoption_notice.pk %}">
|
||||
<i class="fas fa-pencil fa-fw"
|
||||
aria-hidden="true"></i> {% translate 'Bearbeiten' %}
|
||||
</a>
|
||||
<a class="dropdown-item"
|
||||
href="{% url 'adoption-notice-add-photo' adoption_notice.pk %}">
|
||||
<i class="fas fa-image fa-fw"
|
||||
aria-hidden="true"></i> {% trans 'Bilder hinzufügen' %}
|
||||
</a>
|
||||
<a class="dropdown-item"
|
||||
href="{% url 'adoption-notice-add-animal' adoption_notice.pk %}">
|
||||
<i class="fas fa-plus fa-fw"
|
||||
aria-hidden="true"></i> {% trans 'Tier hinzufügen' %}
|
||||
</a>
|
||||
<a class="dropdown-item"
|
||||
href="{% url 'adoption-notice-close' adoption_notice_id=adoption_notice.pk %}">
|
||||
<i class="fas fa-circle-xmark fa-fw"
|
||||
aria-hidden="true"></i> {% trans 'Deaktivieren' %}
|
||||
</a>
|
||||
<hr class="dropdown-divider">
|
||||
{% endif %}
|
||||
<a class="dropdown-item" href="{{ adoption_notice.get_report_url }}">
|
||||
<i class="fas fa-flag"
|
||||
aria-hidden="true"></i> {% trans 'Melden' %}
|
||||
</a>
|
||||
{% if request.user.is_superuser %}
|
||||
<hr class="dropdown-divider">
|
||||
<a class="dropdown-item is-warning"
|
||||
href="{% url adoption_notice_meta|admin_urlname:'change' adoption_notice.pk %}">
|
||||
<i class="fa-solid fa-tools fa-fw"></i> Admin interface
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--- General Information --->
|
||||
<div class="grid">
|
||||
<div class="cell">
|
||||
<div class="grid">
|
||||
{% if adoption_notice.organization %}
|
||||
<div class="cell">
|
||||
<span>
|
||||
<i class="fa-solid fa-building fa-fw" aria-label="{% trans 'Tierschutzorganisation' %}"></i>
|
||||
<a href="{{ adoption_notice.organization.get_absolute_url }}"> {{ adoption_notice.organization }}</a>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="cell">
|
||||
<span>
|
||||
<i class="fa-solid fa-location-dot fa-fw" aria-label="{% trans 'Ort' %}"></i>
|
||||
{% if adoption_notice.location %}
|
||||
{{ adoption_notice.location }}
|
||||
{% else %}
|
||||
{{ adoption_notice.location_string }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="cell">
|
||||
{% include "fellchensammlung/partials/sex-overview.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--- Images and Description --->
|
||||
<div class="columns">
|
||||
<!--- Images --->
|
||||
{% if adoption_notice.get_photos %}
|
||||
<div class="column block">
|
||||
<div class="card">
|
||||
<div class="grid card-content">
|
||||
<div class="gallery">
|
||||
{% with photo=adoption_notice.get_photos.0 %}
|
||||
<div class="main-photo">
|
||||
<a href="{{ MEDIA_URL }}{{ photo.image }}"
|
||||
data-pswp-width="{{ photo.image.width }}"
|
||||
data-pswp-height="{{ photo.image.height }}"
|
||||
target="_blank">
|
||||
<img src="{{ MEDIA_URL }}{{ photo.image }}"
|
||||
alt="{{ photo.alt_text }}">
|
||||
</a>
|
||||
</div>
|
||||
{% endwith %}
|
||||
|
||||
<div class="thumbnail-row">
|
||||
{% for photo in adoption_notice.get_photos|slice:"1:4" %}
|
||||
<div class="thumbnail">
|
||||
<a href="{{ MEDIA_URL }}{{ photo.image }}"
|
||||
data-pswp-width="{{ photo.image.width }}"
|
||||
data-pswp-height="{{ photo.image.height }}"
|
||||
target="_blank">
|
||||
<img src="{{ MEDIA_URL }}{{ photo.image }}"
|
||||
alt="{{ photo.alt_text }}">
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!--- Description --->
|
||||
<div class="column block">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1 class="card-header-title title is-4">{% translate "Beschreibung" %}</h1>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<p class="expandable">{% if adoption_notice.description %}
|
||||
{{ adoption_notice.description | render_markdown }}
|
||||
{% else %}
|
||||
{% translate "Keine Beschreibung angegeben" %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<hr>
|
||||
<p>
|
||||
<strong>
|
||||
{% translate 'Zuletzt auf Aktualität überprüft:' %}
|
||||
</strong>
|
||||
{{ adoption_notice.last_checked|time_since_hr }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
{% if adoption_notice.further_information %}
|
||||
<form method="get" action="{% url 'external-site' %}">
|
||||
<input type="hidden" name="url" value="{{ adoption_notice.further_information }}">
|
||||
<button class="button is-primary is-fullwidth" type="submit" id="submit">
|
||||
{{ adoption_notice.further_information | domain }} <i
|
||||
class="fa-solid fa-arrow-up-right-from-square fa-fw"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
{% for animal in adoption_notice.animals %}
|
||||
<div class="block">
|
||||
{% include "fellchensammlung/partials/partial-animal-card.html" %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="block">
|
||||
{% include "fellchensammlung/partials/partial-comment-section.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
@@ -2,7 +2,7 @@
|
||||
{% load i18n %}
|
||||
<div class="detail-animal"></div>
|
||||
<div class="detail-animal-header">
|
||||
<h1>{{ animal.name }}</h1>
|
||||
<h1 class="title is-1">{{ animal.name }}</h1>
|
||||
<div class="tag">{{ animal.species }}</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -18,7 +18,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<p>{{ animal.description }}</p>
|
||||
<h2>{% translate "Bilder" %}</h2>
|
||||
<h2 class="title is-2">{% translate "Bilder" %}</h2>
|
||||
<div class="photos">
|
||||
{% for image in animal.get_photos %}
|
||||
<div class="card-photo">
|
||||
@@ -28,5 +28,4 @@
|
||||
{% if not animal.get_photos %}
|
||||
{% translate "Keine Bilder" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
@@ -1,9 +1,9 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% extends "fellchensammlung/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% if form_complete %}
|
||||
<h1>{% translate "Erfolgreich gemeldet" %}</h1>
|
||||
<h1 class="title is-2">{% translate "Erfolgreich gemeldet" %}</h1>
|
||||
{% blocktranslate %}
|
||||
Wenn du sehen willst welche Moderationsentscheidungen getroffen werden, schau zu einem späteren Zeitpunkt
|
||||
wieder auf dieser Seite vorbei.
|
||||
@@ -11,8 +11,10 @@
|
||||
<a href="mailto:info@notfellchen.org">info@notfellchen.org</a> Einspruch einlegen.
|
||||
{% endblocktranslate %}
|
||||
{% endif %}
|
||||
<hr>
|
||||
{% include "fellchensammlung/partials/partial-report.html" %}
|
||||
<h2>{% translate "Moderationsverlauf" %}</h2>
|
||||
<hr>
|
||||
<h1 class="title is-1">{% translate "Moderationsverlauf" %}</h1>
|
||||
{% if report.get_moderation_actions %}
|
||||
{% include "fellchensammlung/lists/list-moderation-action.html" %}
|
||||
{% else %}
|
||||
|
@@ -1,25 +1,94 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% extends "fellchensammlung/base.html" %}
|
||||
{% load custom_tags %}
|
||||
{% load i18n %}
|
||||
{% load admin_urls %}
|
||||
|
||||
{% block title %}<title>{{ org.name }}</title>{% endblock %}
|
||||
{% block og_title %}
|
||||
<meta property="og:title" content="{{ org.name }} - Notfellchen"/>
|
||||
{% endblock %}
|
||||
{% block description %}
|
||||
<meta name="description" content="{{ org.name }}: {% translate 'Kontaktdaten und Tiere zur Adoption' %}">
|
||||
{% endblock %}
|
||||
{% block og_description %}
|
||||
<meta property="og:description" content="{{ org.name }}: {% translate 'Kontaktdaten und Tiere zur Adoption' %}">
|
||||
{% endblock %}
|
||||
{% block canonical_url %}{% host %}{% url 'rescue-organization-detail' rescue_organization_id=org.id %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<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>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="block">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h1 class="card-header-title">{{ org.name }}</h1>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="block">
|
||||
<b><i class="fa-solid fa-location-dot"></i></b>
|
||||
{% if org.location %}
|
||||
{{ org.location }}
|
||||
{% else %}
|
||||
{{ org.location_string }}
|
||||
{% endif %}
|
||||
{% if org.description %}
|
||||
<p>{{ org.description | render_markdown }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if org.specializations %}
|
||||
<div class="block">
|
||||
<h3 class="title is-5">{% translate 'Spezialisierung' %}</h3>
|
||||
<div class="content">
|
||||
<ul>
|
||||
{% for specialization in org.specializations.all %}
|
||||
<li>{{ specialization }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if org.parent_org %}
|
||||
<div class="block">
|
||||
<h3 class="title is-5">{% translate 'Übergeordnete Organisation' %}</h3>
|
||||
<p>
|
||||
<span>
|
||||
<i class="fa-solid fa-building fa-fw" aria-label="{% trans 'Tierschutzorganisation' %}"></i>
|
||||
<a href="{{ org.parent_org.get_absolute_url }}"> {{ org.parent_org }}</a>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
{% include "fellchensammlung/partials/partial-rescue-organization-contact.html" %}
|
||||
</div>
|
||||
<div class="block">
|
||||
<a class="button is-warning is-fullwidth" href="{% url org_meta|admin_urlname:'change' org.pk %}">
|
||||
<i class="fa-solid fa-tools fa-fw"></i> Admin interface
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
{% include "fellchensammlung/partials/partial-map.html" %}
|
||||
</div>
|
||||
</div>
|
||||
<h2>{% translate 'Vermittlungen der Organisation' %}</h2>
|
||||
|
||||
{% if org.child_organizations %}
|
||||
<div class="block">
|
||||
<h2 class="title is-2">{% translate 'Unterorganisationen' %}</h2>
|
||||
{% with rescue_organizations=org.child_organizations %}
|
||||
{% include "fellchensammlung/lists/list-animal-shelters.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<h2 class="title is-2">{% translate 'Vermittlungen der Organisation' %}</h2>
|
||||
<div class="container-cards">
|
||||
{% if org.adoption_notices %}
|
||||
{% for adoption_notice in org.adoption_notices %}
|
||||
{% if org.adoption_notices_in_hierarchy %}
|
||||
{% for adoption_notice in org.adoption_notices_in_hierarchy %}
|
||||
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
|
@@ -1,50 +1,93 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% extends "fellchensammlung/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}<title>{{ user.get_full_name }}</title>{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ user.get_full_name }}</h1>
|
||||
|
||||
<p><strong>{% translate "Username" %}:</strong> {{ user.username }}</p>
|
||||
<p><strong>{% translate "E-Mail" %}:</strong> {{ user.email }}</p>
|
||||
|
||||
{% if user.preferred_language %}
|
||||
<p><strong>{% translate "Sprache" %}:</strong> {{ user.preferred_language }}</p>
|
||||
{% else %}
|
||||
<p>{% translate "Keine bevorzugte Sprache gesetzt." %}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="container-cards">
|
||||
{% if user.id is request.user.id %}
|
||||
<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 class="level is-mobile">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<h1 class="title is-1"><i class="fas fa-user"></i> {{ user.get_full_name }}</h1>
|
||||
</div>
|
||||
</div><p>
|
||||
<div class="container-comment-form">
|
||||
<h2>{% trans 'Profil verwalten' %}</h2>
|
||||
<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 class="level-right">
|
||||
<div class="level-item">
|
||||
<form class="" action="{% url 'logout' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="button" type="submit">
|
||||
<i aria-hidden="true" class="fas fa-sign-out fa-fw"></i> Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<h2 class="title is-2">{% trans 'Profil verwalten' %}</h2>
|
||||
<p><strong>{% translate "E-Mail" %}:</strong> {{ user.email }}</p>
|
||||
<div class="">
|
||||
<p>
|
||||
<a class="button is-warning" href="{% url 'password_change' %}">{% translate "Change password" %}</a>
|
||||
<a class="button is-info" href="{% url 'user-me-export' %}">{% translate "Daten exportieren" %}</a>
|
||||
</p>
|
||||
<h2>{% translate 'Benachrichtigungen' %}</h2>
|
||||
{% include "fellchensammlung/lists/list-notifications.html" %}
|
||||
<h2>{% translate 'Meine Vermittlungen' %}</h2>
|
||||
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% if user.id is request.user.id %}
|
||||
<div class="block">
|
||||
<h2 class="title is-2">{% trans 'Einstellungen' %}</h2>
|
||||
<form class="block" 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>
|
||||
<details>
|
||||
<summary><strong>{% trans 'Erweiterte Einstellungen' %}</strong></summary>
|
||||
<div class="block">
|
||||
{% if token %}
|
||||
<form action="" method="POST">
|
||||
{% csrf_token %}
|
||||
<p class="text-muted"><strong>{% translate "API token:" %}</strong> {{ token }}</p>
|
||||
<input class="button is-danger" 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="button is-primary" type="submit" name="create_token"
|
||||
value={% translate "Create API token" %}>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
</div>
|
||||
|
||||
<h2 class="title is-2">{% translate 'Benachrichtigungen' %}</h2>
|
||||
{% include "fellchensammlung/lists/list-notifications.html" %}
|
||||
|
||||
<h2 class="title is-2">{% translate 'Abonnierte Suchen' %}</h2>
|
||||
{% include "fellchensammlung/lists/list-search-subscriptions.html" %}
|
||||
|
||||
<h2 class="title is-2">{% translate 'Meine Vermittlungen' %}</h2>
|
||||
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
@@ -1,99 +0,0 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% load custom_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
{% 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 }}
|
||||
{% 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>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% if has_edit_permission %}
|
||||
<a class="btn2"
|
||||
href="{% url 'adoption-notice-add-photo' adoption_notice_id=adoption_notice.pk %}">{% translate 'Foto hinzufügen' %}</a>
|
||||
<a class="btn2 detail-adoption-notice-header"
|
||||
href="{% url 'adoption-notice-edit' adoption_notice_id=adoption_notice.pk %}">{% translate 'Bearbeiten' %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="table-adoption-notice-info">
|
||||
<table>
|
||||
<tr>
|
||||
<th>{% translate "Ort" %}</th>
|
||||
{% if adoption_notice.organization %}
|
||||
<th>{% translate "Organisation" %}</th>
|
||||
{% endif %}
|
||||
<th>{% translate "Suchen seit" %}</th>
|
||||
<th>{% translate "Zuletzt aktualisiert" %}</th>
|
||||
<th>{% translate "Weitere Informationen" %}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{% if adoption_notice.location %}
|
||||
{{ adoption_notice.location }}
|
||||
{% else %}
|
||||
{{ adoption_notice.location_string }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if adoption_notice.organization %}
|
||||
<td><a href="{{adoption_notice.organization.get_absolute_url }}">{{ adoption_notice.organization }}</a></td>
|
||||
{% endif %}
|
||||
|
||||
<td>{{ adoption_notice.searching_since }}</td>
|
||||
<td>{{ adoption_notice.last_checked | date:'d. F Y' }}</td>
|
||||
{% if adoption_notice.further_information %}
|
||||
<td>
|
||||
<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>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>-</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h1>{% translate "Bilder" %}</h1>
|
||||
{% for photo in adoption_notice.get_photos %}
|
||||
<img src="{{ MEDIA_URL }}/{{ photo.image }}" alt="{{ photo.alt_text }}">
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h1>{% translate "Beschreibung" %}</h1>
|
||||
<p>{% if adoption_notice.description %}
|
||||
{{ adoption_notice.description | render_markdown }}
|
||||
{% else %}
|
||||
{% translate "Keine Beschreibung angegeben" %}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{% for animal in adoption_notice.animals %}
|
||||
{% include "fellchensammlung/partials/partial-animal-card.html" %}
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% include "fellchensammlung/partials/partial-comment-section.html" %}
|
||||
|
||||
{% endblock %}
|
@@ -1,9 +0,0 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% load custom_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}<title>{{ animal.name }}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "fellchensammlung/details/detail-animal-partial.html" %}
|
||||
{% endblock %}
|
@@ -1,11 +1,11 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% extends "fellchensammlung/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
|
||||
{% block title %}<title>{% translate "403 Forbidden" %}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>403 Forbidden</h1>
|
||||
<h1 class="title is-1">403 Forbidden</h1>
|
||||
<p>
|
||||
{% blocktranslate %}
|
||||
Diese Aktion ist dir nicht erlaubt. Logge dich ein oder nutze einen anderen Account. Wenn du denkst, dass hier
|
||||
|
@@ -0,0 +1,14 @@
|
||||
{% extends "fellchensammlung/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
|
||||
{% block title %}<title>{% translate "403 Forbidden" %}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title is-1">404 Not Found</h1>
|
||||
<p>
|
||||
{% blocktranslate %}
|
||||
Diese Seite existiert nicht.
|
||||
{% endblocktranslate %}
|
||||
</p>
|
||||
{% endblock %}
|
@@ -0,0 +1,28 @@
|
||||
{% extends "fellchensammlung/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
{% block content %}
|
||||
<div class="block">
|
||||
<div class="message is-warning">
|
||||
{% if external_site_warning %}
|
||||
<h1 class="message-header">
|
||||
{{ external_site_warning.title }}
|
||||
</h1>
|
||||
<div class="message-body">
|
||||
{{ external_site_warning.content | render_markdown }}
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<h1 class="message-header">
|
||||
{% trans 'Achtung du verlässt notfellchen.org' %}
|
||||
</h1>
|
||||
<div class="message-body">
|
||||
{% trans 'Sichere Abgabebedingungen können von uns, trotz vieler Bemühungen, nicht garantiert werden. Nimm Kontakt zu einer Rattenhilfe oder dem VdRD e.V. auf, die dich beraten können.' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="block">
|
||||
<a href="{{ url }}" class="button is-primary is-fullwidth">{% translate "Weiter" %}<i class="fa fa-arrow-right fa-fw" aria-hidden="true"></i> </a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
@@ -1,15 +0,0 @@
|
||||
{% extends "fellchensammlung/base_generic.html" %}
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
{% 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 %}
|
109
src/fellchensammlung/templates/fellchensammlung/footer.html
Normal file
@@ -0,0 +1,109 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
|
||||
<footer class="footer">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="block">
|
||||
<h3 class="title is-3 has-text-left">
|
||||
Notfellchen
|
||||
</h3>
|
||||
|
||||
<!-- footer content -->
|
||||
<p class="has-text-left">
|
||||
Für Menschen die Ratten aus dem Tierschutz ein liebendes Zuhause geben wollen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<h3 class="title is-5">
|
||||
{% trans 'Sprache ändern' %}
|
||||
</h3>
|
||||
{% include "fellchensammlung/forms/form_change_language.html" %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<h4 class="title is-4 has-text-justify">
|
||||
{% translate 'Über uns' %}
|
||||
</h4>
|
||||
|
||||
<a href="{% url "about" %}">
|
||||
{% translate 'Das Notfellchen Projekt' %}
|
||||
</a>
|
||||
<br/>
|
||||
<a href="{% url "buying" %}">
|
||||
{% translate 'Ratten kaufen' %}
|
||||
</a>
|
||||
<br/>
|
||||
<a href="{% url "terms-of-service" %}">
|
||||
{% translate 'Nutzungsbedingungen' %}
|
||||
</a>
|
||||
<br/>
|
||||
|
||||
<a href="{% url "privacy" %}">
|
||||
{% translate 'Datenschutz' %}
|
||||
</a>
|
||||
<br/>
|
||||
<a href="{% url "imprint" %}">
|
||||
{% translate 'Impressum' %}
|
||||
</a>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<h4 class="title is-4 has-text-justify">
|
||||
Technisches
|
||||
</h4>
|
||||
|
||||
<div>
|
||||
<a class="nav-link " href="{% url "rss" %}">
|
||||
<i class="fa-solid fa-rss fa-fw"></i> {% translate 'RSS' %}
|
||||
</a>
|
||||
<br/>
|
||||
|
||||
<a href="https://dokumentation.notfellchen.org/">
|
||||
<span class="icon-text">
|
||||
<span>{% translate 'Dokumentation' %}</span>
|
||||
</span>
|
||||
</a>
|
||||
<br/>
|
||||
|
||||
<a href="mailto:info@notfellchen.org">
|
||||
<span class="icon-text">
|
||||
<span>{% translate 'Probleme melden' %}</span>
|
||||
</span>
|
||||
</a>
|
||||
<br/>
|
||||
<a href="https://codeberg.org/moanos/notfellchen">
|
||||
<span class="icon-text">
|
||||
<span>{% trans 'Code' %}</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h4 class="title is-4 has-text-justify">
|
||||
{% trans 'Hilfreiche Links' %}
|
||||
</h4>
|
||||
<a class="nav-link " href="{% url "rescue-organizations" %}">
|
||||
{% translate 'Tierheime in der Nähe' %}
|
||||
</a>
|
||||
<br/>
|
||||
{% trust_level "MODERATOR" as coordinator_trust_level %}
|
||||
{% if request.user.trust_level >= coordinator_trust_level %}
|
||||
<a class="nav-link " href="{% url "modtools" %}">
|
||||
{% translate 'Moderationstools' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<br/>
|
||||
{% if request.user.is_superuser %}
|
||||
<a class="nav-link " href="{% url "admin:index" %}">
|
||||
<i class="fa-solid fa-screwdriver-wrench fa-fw"></i>{% translate 'Admin-Bereich' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|