Compare commits
562 Commits
ci-test-co
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 151ce0d88e | |||
| e07e633651 | |||
| dd3b1fde9d | |||
| 2ffc9b4ba1 | |||
| 22eebd4586 | |||
| e589a048d3 | |||
| 392eb5a7a8 | |||
| 44fa4d4880 | |||
| 9b97cc4cb1 | |||
| 656a24ef02 | |||
| 74643db087 | |||
| 3a6fd3cee1 | |||
| 29e9d1bd8c | |||
| 3c5ca9ae00 | |||
| 3d1ad6112d | |||
| b843e67e9b | |||
| 4cab71e8fb | |||
| 969339a95f | |||
| e06efa1539 | |||
| 2fb6d2782f | |||
| f69eccd0e4 | |||
| e20e6d4b1d | |||
| 0352a60e28 | |||
| abeb14601a | |||
| f52225495d | |||
| 797b2c15f7 | |||
| e81618500b | |||
| f7a5da306c | |||
| 92a9b5c6c9 | |||
| 964aeb97a7 | |||
| 474e9eb0f8 | |||
| 7acc2c6eec | |||
| 5a02837d7f | |||
| 7ff0a9b489 | |||
| 9af4b58a4f | |||
| 7a20890f17 | |||
| f6c9e532f8 | |||
| f1698c4fd3 | |||
| cb82aeffde | |||
| e9c1ef2604 | |||
| d8f0f2b3be | |||
| 65f065f5ce | |||
| 5cba64e500 | |||
| 064784a222 | |||
| b890ef3563 | |||
| 962f2ae86c | |||
| c71a1940dd | |||
| 8b2913a8be | |||
| 111ffc2b2e | |||
| 600aa918ef | |||
| 68e13ed176 | |||
| 0fa4330f2c | |||
| c9289b1e8c | |||
| bb3136bfc7 | |||
| b708e9ecaf | |||
| f3619b2881 | |||
| 7572c92da5 | |||
| 5a2b11b44e | |||
| df15ea100b | |||
| 3da6e90f73 | |||
| f784ab0c78 | |||
| ebaa477cff | |||
| b4be21bf45 | |||
| 0df36df9d8 | |||
| a5754b2633 | |||
| 7c6e01a436 | |||
| ad90429ec7 | |||
| 0e36237890 | |||
| 3261f5a90a | |||
| 1551c1bdf2 | |||
| 996bd7af67 | |||
| 21bd34c94d | |||
| fb581c940b | |||
| b428f46213 | |||
| 38fe55dd86 | |||
| 0da6c425fd | |||
| 81962ab9e7 | |||
| 48dd0a6a19 | |||
| 661827a957 | |||
| 242de5f749 | |||
| bd7f940987 | |||
| 0634671c84 | |||
| 1fb5be0cf8 | |||
| 3f9e4265e5 | |||
| de21b8b5e5 | |||
| fd481fef2e | |||
| 70f077e393 | |||
| 1c7d943a21 | |||
| 41873ebfe5 | |||
| fc2dbde064 | |||
| a372be4af2 | |||
| 5d333b28ab | |||
| 84ad047c01 | |||
| c93b2631cb | |||
| 15dd06a91f | |||
| 30ff26c7ef | |||
| 1434e7502a | |||
| 93b21fb7d0 | |||
| e5c82f392c | |||
| 0626964461 | |||
| 23a724e390 | |||
| 2a9c7cf854 | |||
| 335630e16d | |||
| 6051f7c294 | |||
| c1ea6cd211 | |||
| 6c43b46007 | |||
| dc9e68c4b9 | |||
| 4b03f99971 | |||
| 426f4b3d8b | |||
| 3604233507 | |||
| 8c5099f14a | |||
| d5bc348453 | |||
| bce98cb439 | |||
| 1ed3d27533 | |||
| 39a098af8e | |||
| 62491b84c1 | |||
| 81f7f5bb5d | |||
| 8ce4122160 | |||
| 370ad2ce66 | |||
| f25c425d85 | |||
| d921623f31 | |||
| 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 |
10
.gitignore
vendored
@@ -2,11 +2,18 @@
|
||||
|
||||
# Database
|
||||
notfellchen
|
||||
*.sq3
|
||||
|
||||
# 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 +168,4 @@ dmypy.json
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
/node_modules/
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
51
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,6 +117,7 @@ docker push moanos/notfellchen:latest
|
||||
docker run -p8000:7345 moanos/notfellchen:latest
|
||||
```
|
||||
|
||||
|
||||
## Testing
|
||||
|
||||
Tests can be run with
|
||||
@@ -144,17 +185,17 @@ Start beat
|
||||
|
||||
# Contributing
|
||||
|
||||
This project is currently solely developed by me, moanos. I'd like that to change and will be very happy for contributions
|
||||
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
|
||||
|
||||
* CSS structure: It's a hot mess right now, and I'm happy it somehow works. As you might see, there is much room for improvement. Refactoring this and streamlining the look across the app would be amazing.
|
||||
* 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 dcumentation) runs via [git.hyteck.de](https://git.hyteck.de), you can also ask moanos for an account there.
|
||||
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.
|
||||
|
||||
@@ -14,7 +14,8 @@ 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 http://notfellchen.org/api/adoption_notices
|
||||
|
||||
For example: You can check all current adoption notices here: https://notfellchen.org/api/adoption_notice
|
||||
|
||||
Via token
|
||||
---------
|
||||
@@ -28,9 +29,9 @@ An application can then send this token in the request header for authorization.
|
||||
|
||||
|
||||
.. warning::
|
||||
Usage or creation of content still has to follow the terms of Notfellchen.org
|
||||
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.
|
||||
Talk to the notfellchen team if you want develop such things.
|
||||
|
||||
|
||||
Endpoints
|
||||
@@ -45,7 +46,8 @@ Get Adoption Notices
|
||||
++++++++++++++++++++
|
||||
|
||||
.. code-block::
|
||||
curl --request GET \
|
||||
|
||||
curl --request GET \
|
||||
--url https://notfellchen.org/api/adoption_notice \
|
||||
--header 'Authorization: {{token}}'
|
||||
|
||||
@@ -53,7 +55,8 @@ Create Adoption Notice
|
||||
++++++++++++++++++++++
|
||||
|
||||
.. code-block::
|
||||
curl --request POST \
|
||||
|
||||
curl --request POST \
|
||||
--url https://notfellchen.org/api/adoption_notice \
|
||||
--header 'Authorization: {{token}}' \
|
||||
--header 'content-type: multipart/form-data' \
|
||||
@@ -68,6 +71,7 @@ Add Animal to Adoption Notice
|
||||
+++++++++++++++++++++++++++++
|
||||
|
||||
.. code-block::
|
||||
|
||||
curl --request POST \
|
||||
--url https://notfellchen.org/api/animals/ \
|
||||
--header 'Authorization: {{token}}' \
|
||||
@@ -83,6 +87,7 @@ 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" \
|
||||
@@ -96,6 +101,7 @@ Species
|
||||
Getting available species is mainly important when creating animals
|
||||
|
||||
.. code-block::
|
||||
|
||||
curl --request GET \
|
||||
--url https://notfellchen.org/api/species \
|
||||
--header 'Authorization: {{token}}'
|
||||
|
||||
74
docs/_ext/drawio.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from docutils import nodes
|
||||
from sphinx.application import Sphinx
|
||||
from sphinx.util.docutils import SphinxDirective
|
||||
from sphinx.util.typing import ExtensionMetadata
|
||||
|
||||
|
||||
class DrawioDirective(SphinxDirective):
|
||||
"""A directive to show a drawio diagram!
|
||||
|
||||
Usage:
|
||||
.. drawio::
|
||||
example-diagram.drawio.html
|
||||
example-diagram.drawio.png
|
||||
:alt: Example of a Draw.io diagram
|
||||
"""
|
||||
|
||||
has_content = False
|
||||
required_arguments = 2 # html and png
|
||||
optional_arguments = 1
|
||||
final_argument_whitespace = True # indicating if the final argument may contain whitespace
|
||||
option_spec = {
|
||||
"alt": str,
|
||||
}
|
||||
|
||||
def run(self) -> list[nodes.Node]:
|
||||
env = self.state.document.settings.env
|
||||
builder = env.app.builder
|
||||
|
||||
# Resolve paths relative to the document
|
||||
docdir = Path(env.doc2path(env.docname)).parent
|
||||
html_rel = Path(self.arguments[0])
|
||||
png_rel = Path(self.arguments[1])
|
||||
html_path = (docdir / html_rel).resolve()
|
||||
png_path = (docdir / png_rel).resolve()
|
||||
|
||||
alt_text = self.options.get("alt", "")
|
||||
|
||||
container = nodes.container()
|
||||
|
||||
# HTML output -> raw HTML node
|
||||
if builder.format == "html":
|
||||
# Embed the HTML file contents directly
|
||||
try:
|
||||
html_content = html_path.read_text(encoding="utf-8")
|
||||
except OSError as e:
|
||||
msg = self.state_machine.reporter.error(f"Cannot read HTML file: {e}")
|
||||
return [msg]
|
||||
aria_attribute = f' aria-label="{alt_text}"' if alt_text else ""
|
||||
raw_html_node = nodes.raw(
|
||||
"",
|
||||
f'<div class="drawio-diagram"{aria_attribute}>{html_content}</div>',
|
||||
format="html",
|
||||
)
|
||||
container += raw_html_node
|
||||
else:
|
||||
# Other outputs -> PNG image node
|
||||
image_node = nodes.image(uri=png_path)
|
||||
container += image_node
|
||||
|
||||
return [container]
|
||||
|
||||
|
||||
def setup(app: Sphinx) -> ExtensionMetadata:
|
||||
app.add_directive("drawio", DrawioDirective)
|
||||
|
||||
return {
|
||||
"version": "0.2",
|
||||
"parallel_read_safe": True,
|
||||
"parallel_write_safe": True,
|
||||
}
|
||||
@@ -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
|
||||
|
||||
12
docs/conf.py
@@ -16,6 +16,10 @@
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.append(str(Path('_ext').resolve()))
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
@@ -28,7 +32,6 @@ version = ''
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = '0.2.0'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
@@ -40,6 +43,7 @@ release = '0.2.0'
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.ifconfig',
|
||||
'drawio'
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
@@ -69,7 +73,6 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = None
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
@@ -104,7 +107,6 @@ html_static_path = ['_static']
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'notfellchen'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
@@ -133,7 +135,6 @@ latex_documents = [
|
||||
'Julian-Samuel Gebühr', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
@@ -143,7 +144,6 @@ man_pages = [
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output ----------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
@@ -155,7 +155,6 @@ texinfo_documents = [
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Epub output -------------------------------------------------
|
||||
|
||||
# Bibliographic Dublin Core info.
|
||||
@@ -173,5 +172,4 @@ epub_title = project
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ['search.html']
|
||||
|
||||
|
||||
# -- Extension configuration -------------------------------------------------
|
||||
|
||||
BIN
docs/user/Screenshot-Moderationstools.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/user/Screenshot-hilfreiche-Links.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
11
docs/user/Tiere-in-Vermittlung-entdecken.drawio.html
Normal file
BIN
docs/user/Tiere-in-Vermittlung-entdecken.drawio.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
docs/user/Vermittlung-Lifecycle.drawio.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
11
docs/user/Vermittlung_Lifecycle.drawio.html
Normal file
@@ -6,14 +6,27 @@ Jede Vermittlung kann abonniert werden. Dafür klickst du auf die Glocke neben d
|
||||
|
||||
.. image:: abonnieren.png
|
||||
|
||||
|
||||
Einstellungen
|
||||
-------------
|
||||
|
||||
Du kannst E-Mail Benachrichtigungen in den Einstellungen deaktivieren.
|
||||
|
||||
.. image::
|
||||
einstellungen-benachrichtigungen.png
|
||||
:alt: Screenshot der Profileinstellungen in Notfellchen. Ein roter Pfeil zeigt auf einen Schalter "E-Mail Benachrichtigungen"
|
||||
|
||||
Auf der Website
|
||||
+++++++++++++++
|
||||
|
||||
.. image::
|
||||
screenshot-benachrichtigungen.png
|
||||
:alt: Screenshot der Menüleiste von Notfellchen.org. Neben dem Symbol einer Glocke steht die Zahl 27.
|
||||
|
||||
|
||||
|
||||
E-Mail
|
||||
++++++
|
||||
|
||||
Mit während deiner :doc:`registrierung` gibst du eine E-Mail Addresse an.
|
||||
|
||||
Benachrichtigungen senden wir per Mail - du kannst das jederzeit in den Einstellungen deaktivieren.
|
||||
Mit während deiner :doc:`registrierung` gibst du eine E-Mail Adresse an. An diese senden wir Benachrichtigungen, außer
|
||||
du deaktiviert dies wie oben beschrieben.
|
||||
BIN
docs/user/einstellungen-benachrichtigungen.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
58
docs/user/erste-schritte.rst
Normal file
@@ -0,0 +1,58 @@
|
||||
Erste Schritte
|
||||
==============
|
||||
|
||||
Tiere zum Adoptieren suchen
|
||||
---------------------------
|
||||
|
||||
Wenn du Tiere zum adoptieren suchst, brauchst du keinen Account. Du kannst bequem die `Suche <https://notfellchen.org/suchen/>`_ nutzen, um Tiere zur Adoption in deiner Nähe zu finden.
|
||||
Wenn dich eine Vermittlung interessiert, kannst du folgendes tun
|
||||
|
||||
* die Vermittlung aufrufen um Details zu sehen
|
||||
* den Link :guilabel:`Weitere Informationen` anklicken um auf der Tierheimwebsite mehr zu erfahren
|
||||
* per Kommentar weitere Informationen erfragen oder hinzufügen
|
||||
|
||||
Wenn du die Tiere tatsächlich informieren willst, folge der Anleitung unter :guilabel:`Adoptionsprozess`.
|
||||
Dieser kann sich je nach Tierschutzorganisation unterscheiden.
|
||||
|
||||
.. image::
|
||||
screenshot-adoptionsprozess.png
|
||||
:alt: Screenshot der Sektion "Adoptionsprozess" einer Vermittlungsanzeige. Der Prozess ist folgendermaßen: 1. Link zu "Weiteren Informationen" prüfen, 2. Organization kontaktieren, 3. Bei erfolgreicher Vermittlung: Vermittlung als geschlossen melden
|
||||
|
||||
Suchen abonnieren
|
||||
+++++++++++++++++
|
||||
|
||||
Es kann sein, dass es in deiner Umgebung keine passenden Tiere für deine Suche gibt. Damit du nicht ständig wieder Suchen musst, gibt es die Funktion "Suche abonnieren".
|
||||
Wenn du eine Suche abonnierst, wirst du für neue Vermittlungen, die den Kriterien der Suche entsprechen, benachrichtigt.
|
||||
|
||||
.. image::
|
||||
screenshot-suche-abonnieren.png
|
||||
:alt: Screenshot der Suchmaske auf Notfellchen.org . Ein roter Pfeil zeigt auf den Button "Suche abonnieren"
|
||||
|
||||
.. important::
|
||||
|
||||
Um Suchen zu abonnieren brauchst du einen Account. Wie du einen Account erstellst erfährst du hier: :doc:`registrierung`.
|
||||
|
||||
.. hint::
|
||||
|
||||
Mehr über Benachrichtigungen findest du hier: :doc:`benachrichtigungen`.
|
||||
|
||||
Vermittlungen hinzufügen
|
||||
------------------------
|
||||
|
||||
Gehe zu `Vermittlung hinzufügen <https://notfellchen.org/vermitteln/>`_ um eine neue Vermittlung einzustellen.
|
||||
Füge alle Informationen die du hast hinzu.
|
||||
|
||||
.. important::
|
||||
|
||||
Um Vermittlungen hinzuzufügen brauchst du einen Account.
|
||||
Wie du einen Account erstellst erfährst du hier: :doc:`registrierung`.
|
||||
|
||||
|
||||
.. important::
|
||||
|
||||
Vermittlungen die du einstellst müssen erst durch Moderator\*innen freigeschaltet werden. Das passiert normalerweise
|
||||
innerhalb von 24 Stunden. Wenn deine Vermittlung dann noch nicht freigeschaltet ist, prüfe bitte dein E-Mail Postfach,
|
||||
es könnte sein, dass die Moderator\*innen Rückfragen haben. Melde dich gerne unter info@notfellchen.org, wenn deine
|
||||
Vermittlung nach 24 Stunden nicht freigeschaltet ist.
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
******************
|
||||
User Dokumentation
|
||||
******************
|
||||
****************
|
||||
Benutzerhandbuch
|
||||
****************
|
||||
|
||||
Im Benutzerhandbuch findest du Informationen zur Benutzung von `notfellchen.org <https://notfellchen.org>`_.
|
||||
Solltest du darüber hinaus Fragen haben, komm gerne auf uns zu: info@notfellchen.org
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Inhalt:
|
||||
|
||||
|
||||
erste-schritte.rst
|
||||
registrierung.rst
|
||||
vermittlungen.rst
|
||||
moderationskonzept.rst
|
||||
benachrichtigungen.rst
|
||||
organisationen-pruefen.rst
|
||||
|
||||
55
docs/user/organisationen-pruefen.rst
Normal file
@@ -0,0 +1,55 @@
|
||||
Tiere in Vermittlung systematisch entdecken & eintragen
|
||||
=======================================================
|
||||
|
||||
Notfellchen hat eine Liste der meisten deutschen Tierheime und anderer Tierschutzorganisationen.
|
||||
Die meisten dieser Organisationen nehmen Tiere auf die bei Notfellchen eingetragen werden können.
|
||||
Es ist daher das Ziel, diese Organisationen alle zwei Wochen auf neue Tiere zu prüfen.
|
||||
|
||||
|
||||
+-------------------------------------------------+---------+----------------------+
|
||||
| Gruppe | Anzahl | Zuletzt aktualisiert |
|
||||
+=================================================+=========+======================+
|
||||
| Tierschutzorganisationen im Verzeichnis | 550 | Oktober 2025 |
|
||||
+-------------------------------------------------+---------+----------------------+
|
||||
| Tierschutzorganisationen in regelmäßigerPrüfung | 412 | Oktober 2025 |
|
||||
+-------------------------------------------------+---------+----------------------+
|
||||
|
||||
.. warning::
|
||||
|
||||
Organisationen auf neue Tiere zu prüfen ist eine Funktion für Moderator\*innen. Falls du Lust hast mitzuhelfen,
|
||||
meld dich unter info@notfellchen.org
|
||||
|
||||
Als Moderator\*in kannst du direkt auf den `Moderations-Check <https://notfellchen.org/organization-check/>`_ zugreifen
|
||||
oder findest ihn in unter :menuselection:`Hilfreiche Links --> Moderationstools`:
|
||||
|
||||
.. image::
|
||||
Screenshot-hilfreiche-Links.png
|
||||
:alt: Screenshot der Hilfreichen Links. Zur Auswahl stehen "Tierheime in der Nähe","Moderationstools" und "Admin-Bereich"
|
||||
|
||||
.. image::
|
||||
Screenshot-Moderationstools.png
|
||||
:alt: Screenshot der Moderationstools. Zur Auswahl stehen "Moderationswarteschlange", "Up-to-Date Check", "Organisations-Check" und "Vermittlung ins Fediverse posten".
|
||||
|
||||
|
||||
Arbeitsmodus
|
||||
------------
|
||||
|
||||
.. drawio::
|
||||
Tiere-in-Vermittlung-entdecken.drawio.html
|
||||
Tiere-in-Vermittlung-entdecken.drawio.png
|
||||
|
||||
Shortcuts
|
||||
---------
|
||||
|
||||
Um die Prüfung schneller zu gestalten, gibt es eine Reihe von Shortcuts die du nutzen kannst. Aus Gründen der
|
||||
Übersichtlichkeit sind im Folgenden auch Shortcuts im Browser aufgeführt.
|
||||
|
||||
+------------------------------------------------------+---------------+
|
||||
| Aktion | Shortcut |
|
||||
+======================================================+===============+
|
||||
| Website der ersten Tierschutzorganisation öffnen | :kbd:`O` |
|
||||
+------------------------------------------------------+---------------+
|
||||
| Tab schließen (Firefox/Chrome) | :kbd:`STRG+W` |
|
||||
+------------------------------------------------------+---------------+
|
||||
| Erste Tierschutzorganisationa als geprüft markieren | :kbd:`C` |
|
||||
+------------------------------------------------------+---------------+
|
||||
BIN
docs/user/screenshot-adoptionsprozess.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
docs/user/screenshot-benachrichtigungen.png
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
docs/user/screenshot-suche-abonnieren.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/user/screenshot-suche.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
@@ -1,7 +1,7 @@
|
||||
Vermittlungen
|
||||
=============
|
||||
|
||||
Vermittlungen können von allen Nutzer*innen mit Account erstellt werden. Vermittlungen normaler Nutzer*innen kommen dann in eine Warteschlange und werden vom Admin & Modertionsteam geprüft und sichtbar geschaltet.
|
||||
Vermittlungen können von allen Nutzer\*innen mit Account erstellt werden. Vermittlungen normaler Nutzer*innen kommen dann in eine Warteschlange und werden vom Admin & Modertionsteam geprüft und sichtbar geschaltet.
|
||||
Tierheime und Pflegestellen können auf Anfrage einen Koordinations-Status bekommen, wodurch sie Vermittlungsanzeigen erstellen können die direkt öffentlich sichtbar sind.
|
||||
|
||||
Jede Vermittlung hat ein "Zuletzt-geprüft" Datum, das anzeigt, wann ein Mensch zuletzt überprüft hat, ob die Anzeige noch aktuell ist.
|
||||
@@ -15,3 +15,114 @@ Die Kommentarfunktion von Vermittlungen ermöglicht es angemeldeten Nutzer*innen
|
||||
Ersteller*innen von Vermittlungen werden über neue Kommentare per Mail benachrichtigt, ebenso alle die die Vermittlung abonniert haben.
|
||||
|
||||
Kommentare können, wie Vermittlungen, gemeldet werden.
|
||||
|
||||
.. drawio::
|
||||
Vermittlung_Lifecycle.drawio.html
|
||||
Vermittlung-Lifecycle.drawio.png
|
||||
:alt: Diagramm das den Prozess der Vermittlungen zeigt.
|
||||
|
||||
|
||||
Adoption Notice Status Choices
|
||||
++++++++++++++++++++++++++++++
|
||||
|
||||
Aktiv
|
||||
-----
|
||||
|
||||
Aktive Vermittlungen die über die Suche auffindbar sind.
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:width: 100%
|
||||
:widths: 1 1 2
|
||||
|
||||
* - Value
|
||||
- Label
|
||||
- Description
|
||||
|
||||
* - ``active_searching``
|
||||
- Searching
|
||||
-
|
||||
|
||||
* - ``active_interested``
|
||||
- Interested
|
||||
- Jemand hat bereits Interesse an den Tieren.
|
||||
|
||||
Warte auf Aktion
|
||||
----------------
|
||||
|
||||
Vermittlungen in diesem Status warten darauf, dass ein Mensch sie überprüft. Sie können nicht über die Suche gefunden werden.
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:width: 100%
|
||||
:widths: 1 1 2
|
||||
|
||||
* - ``awaiting_action_waiting_for_review``
|
||||
- Waiting for review
|
||||
- Neue Vermittlung die deaktiviert ist bis Moderator*innen sie überprüfen.
|
||||
|
||||
* - ``awaiting_action_needs_additional_info``
|
||||
- Needs additional info
|
||||
- Deaktiviert bis Informationen nachgetragen werden.
|
||||
|
||||
* - ``disabled_unchecked``
|
||||
- Unchecked
|
||||
- Vermittlung deaktiviert bis sie vom Team auf Aktualität geprüft wurde.
|
||||
|
||||
Geschlossen
|
||||
-----------
|
||||
|
||||
Geschlossene Vermittlungen tauchen in keiner Suche auf. Sie werden aber weiterhin angezeigt, wenn der Link zu ihnen direkt aufgerufen wird.
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:width: 100%
|
||||
:widths: 1 1 2
|
||||
|
||||
* - ``closed_successful_with_notfellchen``
|
||||
- Successful (with Notfellchen)
|
||||
- Vermittlung erfolgreich abgeschlossen.
|
||||
|
||||
* - ``closed_successful_without_notfellchen``
|
||||
- Successful (without Notfellchen)
|
||||
- Vermittlung erfolgreich abgeschlossen.
|
||||
|
||||
* - ``closed_animal_died``
|
||||
- Animal died
|
||||
- Die zu vermittelnden Tiere sind über die Regenbrücke gegangen.
|
||||
|
||||
* - ``closed_for_other_adoption_notice``
|
||||
- Closed for other adoption notice
|
||||
- Vermittlung wurde zugunsten einer anderen geschlossen.
|
||||
|
||||
* - ``closed_not_open_for_adoption_anymore``
|
||||
- Not open for adoption anymore
|
||||
- Tier(e) stehen nicht mehr zur Vermittlung bereit.
|
||||
|
||||
* - ``closed_link_to_more_info_not_reachable``
|
||||
- Der Link zu weiteren Informationen ist nicht mehr erreichbar.
|
||||
- Der Link zu weiteren Informationen ist nicht mehr erreichbar, die Vermittlung wurde daher automatisch deaktiviert.
|
||||
|
||||
* - ``closed_other``
|
||||
- Other (closed)
|
||||
- Vermittlung geschlossen.
|
||||
|
||||
Deaktiviert
|
||||
-----------
|
||||
|
||||
Deaktivierte Vermittlungen werden nur noch Moderator\*innen und Administrator\*innen angezeigt.
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:width: 100%
|
||||
:widths: 1 1 2
|
||||
|
||||
* - ``disabled_against_the_rules``
|
||||
- Against the rules
|
||||
- Vermittlung deaktiviert da sie gegen die Regeln verstößt.
|
||||
|
||||
* - ``disabled_other``
|
||||
- Other (disabled)
|
||||
- Vermittlung deaktiviert.
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ host=localhost
|
||||
[django]
|
||||
secret=CHANGE-ME
|
||||
debug=True
|
||||
internal_ips=["127.0.0.1"]
|
||||
|
||||
[database]
|
||||
backend=sqlite3
|
||||
@@ -18,7 +19,7 @@ media=./media
|
||||
static=./static
|
||||
|
||||
[mail]
|
||||
console-only=true
|
||||
console_only=true
|
||||
|
||||
[logging]
|
||||
app_log_level=INFO
|
||||
@@ -28,3 +29,6 @@ django_log_level=INFO
|
||||
api_url=https://photon.hyteck.de/api
|
||||
api_format=photon
|
||||
|
||||
[security]
|
||||
totp_issuer="NF Localhost"
|
||||
webauth_allow_insecure_origin=True
|
||||
|
||||
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,14 +6,14 @@ 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" },
|
||||
]
|
||||
maintainers = [
|
||||
{ name = "moanos", email = "julian-samuel@gebuehr.net" },
|
||||
]
|
||||
keywords = ["animal", "adoption", "django", "rescue", ]
|
||||
keywords = ["animal", "adoption", "django", "rescue", "rats" ]
|
||||
license = { text = "AGPL-3.0-or-later" }
|
||||
classifiers = [
|
||||
"Environment :: Web",
|
||||
@@ -25,8 +25,6 @@ classifiers = [
|
||||
dependencies = [
|
||||
"Django",
|
||||
"codecov",
|
||||
"sphinx",
|
||||
"sphinx-rtd-theme",
|
||||
"gunicorn",
|
||||
"fontawesomefree",
|
||||
"whitenoise",
|
||||
@@ -38,7 +36,10 @@ dependencies = [
|
||||
"crispy-bootstrap4",
|
||||
"djangorestframework",
|
||||
"celery[redis]",
|
||||
"drf-spectacular[sidecar]"
|
||||
"drf-spectacular[sidecar]",
|
||||
"django-widget-tweaks",
|
||||
"django-super-deduper",
|
||||
"django-allauth[mfa]",
|
||||
]
|
||||
|
||||
dynamic = ["version", "readme"]
|
||||
@@ -48,6 +49,12 @@ develop = [
|
||||
"pytest",
|
||||
"coverage",
|
||||
"model_bakery",
|
||||
"debug_toolbar",
|
||||
]
|
||||
docs = [
|
||||
"sphinx",
|
||||
"sphinx-rtd-theme",
|
||||
"sphinx-autobuild"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
275
scripts/upload_animal_shelters.py
Normal file
@@ -0,0 +1,275 @@
|
||||
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"]
|
||||
stats = {"num_updated_orgs": 0,
|
||||
"num_inserted_orgs": 0}
|
||||
|
||||
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 exists
|
||||
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)
|
||||
# Rescue organization exits
|
||||
if search_result.status_code == 200:
|
||||
stats["num_updated_orgs"] += 1
|
||||
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
|
||||
# Rescue organization does not exist
|
||||
else:
|
||||
stats["num_inserted_orgs"] += 1
|
||||
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()}")
|
||||
print(f"Upload finished. Inserted {stats['num_inserted_orgs']} new orgs and updated {stats['num_updated_orgs']} orgs.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,35 +1,32 @@
|
||||
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, SearchSubscription, \
|
||||
SpeciesSpecificURL
|
||||
from .models import 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, BaseNotification
|
||||
Comment, Announcement, User, Subscriptions, Notification
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class StatusInline(admin.StackedInline):
|
||||
model = AdoptionNoticeStatus
|
||||
from .tools.model_helpers import AdoptionNoticeStatusChoices
|
||||
|
||||
|
||||
@admin.register(AdoptionNotice)
|
||||
class AdoptionNoticeAdmin(admin.ModelAdmin):
|
||||
search_fields = ("name__icontains", "description__icontains")
|
||||
list_filter = ("owner",)
|
||||
inlines = [
|
||||
StatusInline,
|
||||
]
|
||||
actions = ("activate",)
|
||||
|
||||
def activate(self, request, queryset):
|
||||
for obj in queryset:
|
||||
obj.set_active()
|
||||
obj.adoption_notice_status = AdoptionNoticeStatusChoices.Active.SEARCHING
|
||||
obj.save()
|
||||
|
||||
activate.short_description = _("Ausgewählte Vermittlungen aktivieren")
|
||||
|
||||
@@ -94,14 +91,16 @@ class ReportAdoptionNoticeAdmin(admin.ModelAdmin):
|
||||
|
||||
reported_content_link.short_description = "Reported Content"
|
||||
|
||||
|
||||
class SpeciesSpecificURLInline(admin.StackedInline):
|
||||
model = SpeciesSpecificURL
|
||||
|
||||
|
||||
@admin.register(RescueOrganization)
|
||||
class RescueOrganizationAdmin(admin.ModelAdmin):
|
||||
search_fields = ("name","description", "internal_comment", "location_string")
|
||||
search_fields = ("name", "description", "internal_comment", "location_string", "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,
|
||||
@@ -118,24 +117,66 @@ class CommentAdmin(admin.ModelAdmin):
|
||||
list_filter = ("user",)
|
||||
|
||||
|
||||
@admin.register(BaseNotification)
|
||||
@admin.register(Notification)
|
||||
class BaseNotificationAdmin(admin.ModelAdmin):
|
||||
list_filter = ("user", "read")
|
||||
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.register(Log)
|
||||
class LogAdmin(admin.ModelAdmin):
|
||||
ordering = ["-created_at"]
|
||||
list_filter = ("action",)
|
||||
list_display = ("action", "user", "created_at")
|
||||
|
||||
|
||||
admin.site.register(Animal)
|
||||
admin.site.register(Species)
|
||||
admin.site.register(Location)
|
||||
admin.site.register(Rule)
|
||||
admin.site.register(Image)
|
||||
admin.site.register(ModerationAction)
|
||||
admin.site.register(Language)
|
||||
admin.site.register(Announcement)
|
||||
admin.site.register(AdoptionNoticeStatus)
|
||||
admin.site.register(Subscriptions)
|
||||
admin.site.register(Log)
|
||||
admin.site.register(Timestamp)
|
||||
|
||||
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,12 +1,135 @@
|
||||
from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image
|
||||
from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image, Location
|
||||
from rest_framework import serializers
|
||||
import math
|
||||
|
||||
|
||||
class ImageSerializer(serializers.ModelSerializer):
|
||||
width = serializers.SerializerMethodField()
|
||||
height = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Image
|
||||
fields = ['id', 'image', 'alt_text', 'width', 'height']
|
||||
|
||||
def get_width(self, obj):
|
||||
return obj.image.width
|
||||
|
||||
def get_height(self, obj):
|
||||
return obj.image.height
|
||||
|
||||
|
||||
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
|
||||
)
|
||||
url = serializers.SerializerMethodField()
|
||||
|
||||
photos = ImageSerializer(many=True, read_only=True)
|
||||
|
||||
def get_url(self, obj):
|
||||
return obj.get_full_url()
|
||||
|
||||
class Meta:
|
||||
model = AdoptionNotice
|
||||
fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information",
|
||||
"group_only"]
|
||||
"group_only", "location", "location_details", "organization", "photos", "adoption_notice_status",
|
||||
"url"]
|
||||
|
||||
|
||||
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):
|
||||
@@ -14,11 +137,6 @@ class AnimalCreateSerializer(serializers.ModelSerializer):
|
||||
model = Animal
|
||||
fields = ["name", "date_of_birth", "description", "species", "sex", "adoption_notice"]
|
||||
|
||||
class RescueOrgSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RescueOrganization
|
||||
fields = ["name", "location_string", "instagram", "facebook", "fediverse_profile", "email", "phone_number",
|
||||
"website", "description", "external_object_identifier", "external_source_identifier"]
|
||||
|
||||
class AnimalGetSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
@@ -51,3 +169,9 @@ class SpeciesSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Species
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class LocationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = "__all__"
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
from django.urls import path
|
||||
from .views import (
|
||||
AdoptionNoticeApiView,
|
||||
AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView
|
||||
AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView, LocationApiView,
|
||||
AdoptionNoticeGeoJSONView, RescueOrgGeoJSONView, AdoptionNoticePerOrgApiView
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
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("organizations/<int:id>/adoption-notices", AdoptionNoticePerOrgApiView.as_view(), name="api-organization-adoption-notices"),
|
||||
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,20 +1,28 @@
|
||||
from django.db.models import Q
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
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 django.db import transaction
|
||||
from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel
|
||||
from fellchensammlung.tasks import post_adoption_notice_save
|
||||
from rest_framework import status
|
||||
from fellchensammlung.models import Log, TrustLevel, Location, AdoptionNoticeStatusChoices
|
||||
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,
|
||||
RescueOrganizationSerializer,
|
||||
RescueOrgeGeoJSONSerializer,
|
||||
AdoptionNoticeSerializer,
|
||||
ImageCreateSerializer,
|
||||
SpeciesSerializer, RescueOrgSerializer,
|
||||
SpeciesSerializer, RescueOrganizationSerializer,
|
||||
)
|
||||
from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from drf_spectacular.utils import extend_schema, inline_serializer, OpenApiParameter
|
||||
|
||||
|
||||
class AdoptionNoticeApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
@@ -59,22 +67,22 @@ class AdoptionNoticeApiView(APIView):
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
adoption_notice = serializer.save(owner=request.user)
|
||||
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.trust_level >= TrustLevel.MODERATOR:
|
||||
adoption_notice.set_active()
|
||||
if request.user_to_notify.trust_level >= TrustLevel.MODERATOR:
|
||||
adoption_notice.adoption_notice_status = AdoptionNoticeStatusChoices.Active.SEARCHING
|
||||
else:
|
||||
adoption_notice.set_unchecked()
|
||||
adoption_notice.adoption_notice_status = AdoptionNoticeStatusChoices.AwaitingAction.WAITING_FOR_REVIEW
|
||||
|
||||
# Log the action
|
||||
Log.objects.create(
|
||||
user=request.user,
|
||||
user=request.user_to_notify,
|
||||
action="add_adoption_notice",
|
||||
text=f"{request.user} added adoption notice {adoption_notice.pk} via API",
|
||||
text=f"{request.user_to_notify} added adoption notice {adoption_notice.pk} via API",
|
||||
)
|
||||
|
||||
# Return success response with new adoption notice details
|
||||
@@ -84,10 +92,12 @@ class AdoptionNoticeApiView(APIView):
|
||||
)
|
||||
|
||||
|
||||
|
||||
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.
|
||||
@@ -104,6 +114,16 @@ class AnimalApiView(APIView):
|
||||
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):
|
||||
"""
|
||||
@@ -111,13 +131,14 @@ class AnimalApiView(APIView):
|
||||
"""
|
||||
serializer = AnimalCreateSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
animal = serializer.save(owner=request.user)
|
||||
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]
|
||||
|
||||
@@ -129,14 +150,44 @@ class RescueOrganizationApiView(APIView):
|
||||
'description': 'ID of the rescue organization to retrieve.',
|
||||
'type': int
|
||||
},
|
||||
{
|
||||
'name': 'trusted',
|
||||
'required': False,
|
||||
'description': 'Filter by trusted status (true/false).',
|
||||
'type': bool
|
||||
},
|
||||
{
|
||||
'name': 'external_object_identifier',
|
||||
'required': False,
|
||||
'description': 'Filter by external object identifier. Use "None" to filter for an empty field',
|
||||
'type': str
|
||||
},
|
||||
{
|
||||
'name': 'external_source_identifier',
|
||||
'required': False,
|
||||
'description': 'Filter by external source identifier. Use "None" to filter for an empty field',
|
||||
'type': str
|
||||
},
|
||||
{
|
||||
'name': 'search',
|
||||
'required': False,
|
||||
'description': 'Search by organization name or location name/city.',
|
||||
'type': str
|
||||
},
|
||||
],
|
||||
responses={200: RescueOrganizationSerializer(many=True)}
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Get list of rescue organizations or a specific organization by ID.
|
||||
Get list of rescue organizations or a specific organization by ID or get a list with available filters for
|
||||
- external_object_identifier
|
||||
- external_source_identifier
|
||||
"""
|
||||
org_id = kwargs.get("id")
|
||||
org_id = request.query_params.get("id")
|
||||
external_object_identifier = request.query_params.get("external_object_identifier")
|
||||
external_source_identifier = request.query_params.get("external_source_identifier")
|
||||
search_query = request.query_params.get("search")
|
||||
|
||||
if org_id:
|
||||
try:
|
||||
organization = RescueOrganization.objects.get(pk=org_id)
|
||||
@@ -144,28 +195,79 @@ class RescueOrganizationApiView(APIView):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except RescueOrganization.DoesNotExist:
|
||||
return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
organizations = RescueOrganization.objects.all()
|
||||
|
||||
if external_object_identifier:
|
||||
if external_object_identifier == "None":
|
||||
external_object_identifier = None
|
||||
organizations = organizations.filter(external_object_identifier=external_object_identifier)
|
||||
|
||||
if external_source_identifier:
|
||||
if external_source_identifier == "None":
|
||||
external_source_identifier = None
|
||||
organizations = organizations.filter(external_source_identifier=external_source_identifier)
|
||||
if search_query:
|
||||
organizations = organizations.filter(
|
||||
Q(name__icontains=search_query) |
|
||||
Q(location_string__icontains=search_query) |
|
||||
Q(location__name__icontains=search_query) |
|
||||
Q(location__city__icontains=search_query)
|
||||
)
|
||||
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=RescueOrgSerializer, # Document the request body
|
||||
responses={201: 'Rescue organization created/updated successfully!'}
|
||||
request=RescueOrganizationSerializer,
|
||||
responses={201: 'Rescue organization created successfully!'}
|
||||
)
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Create or update a rescue organization.
|
||||
"""
|
||||
serializer = RescueOrgSerializer(data=request.data, context={"request": request})
|
||||
serializer = RescueOrganizationSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
rescue_org = serializer.save(owner=request.user)
|
||||
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/updated successfully!", "id": rescue_org.id},
|
||||
{"message": "Rescue organization created successfully!", "id": rescue_org.id},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@transaction.atomic
|
||||
@extend_schema(
|
||||
request=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]
|
||||
|
||||
@@ -188,7 +290,7 @@ class AddImageApiView(APIView):
|
||||
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)
|
||||
image = serializer.save(owner=request.user_to_notify)
|
||||
object_to_attach_to.photos.add(image)
|
||||
return Response(
|
||||
{"message": "Image added successfully!", "id": image.id},
|
||||
@@ -210,3 +312,141 @@ class SpeciesApiView(APIView):
|
||||
species = Species.objects.all()
|
||||
serializer = SpeciesSerializer(species, many=True, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class LocationApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
{
|
||||
'name': 'id',
|
||||
'required': False,
|
||||
'description': 'ID of the location to retrieve.',
|
||||
'type': int
|
||||
},
|
||||
],
|
||||
responses={200: LocationSerializer(many=True)}
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Retrieve a location
|
||||
"""
|
||||
location_id = kwargs.get("id")
|
||||
if location_id:
|
||||
try:
|
||||
location = Location.objects.get(pk=location_id)
|
||||
serializer = LocationSerializer(location, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Location.DoesNotExist:
|
||||
return Response({"error": "Location not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
locations = Location.objects.all()
|
||||
serializer = LocationSerializer(locations, many=True, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@transaction.atomic
|
||||
@extend_schema(
|
||||
request=LocationSerializer,
|
||||
responses={201: 'Location created successfully!'}
|
||||
)
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
API view to add a location
|
||||
"""
|
||||
serializer = LocationSerializer(data=request.data, context={'request': request})
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
location = serializer.save()
|
||||
|
||||
# Log the action
|
||||
Log.objects.create(
|
||||
user=request.user,
|
||||
action="add_location",
|
||||
text=f"{request.user} added adoption notice {location.pk} via API",
|
||||
)
|
||||
|
||||
# Return success response with new adoption notice details
|
||||
return Response(
|
||||
{"message": "Location created successfully!", "id": location.pk},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
|
||||
class AdoptionNoticeGeoJSONView(ListAPIView):
|
||||
queryset = AdoptionNotice.objects.select_related('location').filter(location__isnull=False).filter(
|
||||
adoption_notice_status__in=AdoptionNoticeStatusChoices.Active.values)
|
||||
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]
|
||||
|
||||
|
||||
class AdoptionNoticePerOrgApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name='id',
|
||||
required=False,
|
||||
description='ID of the rescue organization from which to retrieve adoption notices.',
|
||||
type=OpenApiTypes.INT
|
||||
),
|
||||
OpenApiParameter(
|
||||
name='in_hierarchy',
|
||||
type=OpenApiTypes.BOOL,
|
||||
required=False,
|
||||
description='Show all Adoption Notices in hierarchy.',
|
||||
),
|
||||
OpenApiParameter(
|
||||
name='status',
|
||||
type=OpenApiTypes.STR,
|
||||
required=False,
|
||||
description='Show all Adoption Notices in a certain status. Comma separated list of values e.g. '
|
||||
'"active,closed"',
|
||||
),
|
||||
],
|
||||
responses={200: AdoptionNoticeSerializer(many=True)}
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Retrieve adoption notices with their related animals and images.
|
||||
"""
|
||||
org_id = kwargs.get("id")
|
||||
in_hierarchy = request.query_params.get("in_hierarchy")
|
||||
an_status = request.query_params.get("status")
|
||||
try:
|
||||
org = RescueOrganization.objects.get(id=org_id)
|
||||
except RescueOrganization.DoesNotExist:
|
||||
return Response({"error": "Rescue Organization notice not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
if in_hierarchy:
|
||||
adoption_notices = org.adoption_notices_in_hierarchy
|
||||
else:
|
||||
adoption_notices = AdoptionNotice.objects.filter(organization=org)
|
||||
if an_status:
|
||||
status_list = an_status.lower().strip().split(",")
|
||||
temporary_an_storage = []
|
||||
if "active" in status_list:
|
||||
active_ans = [adoption_notice for adoption_notice in adoption_notices if
|
||||
adoption_notice.adoption_notice_status in AdoptionNoticeStatusChoices.Active.values]
|
||||
temporary_an_storage.extend(active_ans)
|
||||
if "closed" in status_list:
|
||||
closed_ans = [adoption_notice for adoption_notice in adoption_notices if
|
||||
adoption_notice.adoption_notice_status in AdoptionNoticeStatusChoices.Closed.values]
|
||||
temporary_an_storage.extend(closed_ans)
|
||||
if "disabled" in status_list:
|
||||
disabled_ans = [adoption_notice for adoption_notice in adoption_notices if
|
||||
adoption_notice.adoption_notice_status in AdoptionNoticeStatusChoices.Disabled.values]
|
||||
temporary_an_storage.extend(disabled_ans)
|
||||
if "awaiting_action" in status_list:
|
||||
awaiting_action_ans = [adoption_notice for adoption_notice in adoption_notices if
|
||||
adoption_notice.adoption_notice_status in AdoptionNoticeStatusChoices.AwaitingAction.values]
|
||||
temporary_an_storage.extend(awaiting_action_ans)
|
||||
adoption_notices = temporary_an_storage
|
||||
serializer = AdoptionNoticeSerializer(adoption_notices, many=True, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
0
src/fellchensammlung/aviews/__init__.py
Normal file
37
src/fellchensammlung/aviews/embeddables.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
|
||||
from fellchensammlung.aviews.helpers import headers
|
||||
from fellchensammlung.models import RescueOrganization, AdoptionNotice, Species
|
||||
|
||||
|
||||
@xframe_options_exempt
|
||||
@headers({"X-Robots-Tag": "noindex"})
|
||||
def list_ans_per_rescue_organization(request, rescue_organization_id, species_slug=None, active=True):
|
||||
expand = request.GET.get("expand")
|
||||
background_color = request.GET.get("background_color")
|
||||
if expand is not None:
|
||||
expand = True
|
||||
else:
|
||||
expand = False
|
||||
org = get_object_or_404(RescueOrganization, pk=rescue_organization_id)
|
||||
|
||||
# Get only active adoption notices or all
|
||||
if active:
|
||||
adoption_notices_of_org = org.adoption_notices_in_hierarchy_divided_by_status[0]
|
||||
else:
|
||||
adoption_notices_of_org = org.adoption_notices
|
||||
|
||||
# Filter for Species if necessary
|
||||
if species_slug is None:
|
||||
adoption_notices = adoption_notices_of_org
|
||||
else:
|
||||
species = get_object_or_404(Species, slug=species_slug)
|
||||
adoption_notices = [adoption_notice for adoption_notice in adoption_notices_of_org if
|
||||
species in adoption_notice.species]
|
||||
|
||||
template = 'fellchensammlung/embeddables/list-adoption-notices.html'
|
||||
return render(request, template,
|
||||
context={"adoption_notices": adoption_notices,
|
||||
"expand": expand,
|
||||
"background_color": background_color})
|
||||
23
src/fellchensammlung/aviews/helpers.py
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
def headers(headers):
|
||||
"""Decorator adding arbitrary HTTP headers to the response.
|
||||
|
||||
This decorator adds HTTP headers specified in the argument (map), to the
|
||||
HTTPResponse returned by the function being decorated.
|
||||
|
||||
Example:
|
||||
|
||||
@headers({'Refresh': '10', 'X-Bender': 'Bite my shiny, metal ass!'})
|
||||
def index(request):
|
||||
....
|
||||
Source: https://djangosnippets.org/snippets/275/
|
||||
"""
|
||||
def headers_wrapper(fun):
|
||||
def wrapped_function(*args, **kwargs):
|
||||
response = fun(*args, **kwargs)
|
||||
for key in headers:
|
||||
response[key] = headers[key]
|
||||
return response
|
||||
return wrapped_function
|
||||
return headers_wrapper
|
||||
|
||||
12
src/fellchensammlung/aviews/urls.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import embeddables
|
||||
|
||||
urlpatterns = [
|
||||
path("tierschutzorganisationen/<int:rescue_organization_id>/vermittlungen/",
|
||||
embeddables.list_ans_per_rescue_organization,
|
||||
name="list-adoption-notices-for-rescue-organization"),
|
||||
path("tierschutzorganisationen/<int:rescue_organization_id>/vermittlungen/<slug:species_slug>/",
|
||||
embeddables.list_ans_per_rescue_organization,
|
||||
name="list-adoption-notices-for-rescue-organization-species"),
|
||||
]
|
||||
@@ -1,7 +1,8 @@
|
||||
from django import forms
|
||||
from django.forms.widgets import Textarea
|
||||
|
||||
from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportComment, ModerationAction, User, Species, \
|
||||
Comment, SexChoicesWithAll, DistanceChoices
|
||||
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
|
||||
@@ -9,6 +10,8 @@ from django.utils.translation import gettext_lazy as _
|
||||
from notfellchen.settings import MEDIA_URL
|
||||
from crispy_forms.layout import Div
|
||||
|
||||
from .tools.model_helpers import reason_for_signup_label, reason_for_signup_help_text
|
||||
|
||||
|
||||
def animal_validator(value: str):
|
||||
value = value.lower()
|
||||
@@ -23,95 +26,46 @@ 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 UpdateRescueOrgRegularCheckStatus(forms.ModelForm):
|
||||
template_name = "fellchensammlung/forms/form_snippets.html"
|
||||
|
||||
class Meta:
|
||||
model = RescueOrganization
|
||||
fields = ["regular_check_status"]
|
||||
|
||||
|
||||
class ImageForm(forms.ModelForm):
|
||||
@@ -127,8 +81,9 @@ class ImageForm(forms.ModelForm):
|
||||
self.helper.form_method = 'post'
|
||||
|
||||
if in_flow:
|
||||
submits= Div(Submit('submit', _('Speichern')),
|
||||
Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')), css_class="container-edit-buttons")
|
||||
submits = Div(Submit('submit', _('Speichern')),
|
||||
Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')),
|
||||
css_class="container-edit-buttons")
|
||||
else:
|
||||
submits = Fieldset(Submit('submit', _('Speichern')), css_class="container-edit-buttons")
|
||||
self.helper.layout = Layout(
|
||||
@@ -140,60 +95,86 @@ class ImageForm(forms.ModelForm):
|
||||
submits
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Image
|
||||
fields = ('image', 'alt_text')
|
||||
|
||||
|
||||
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
|
||||
fields = ('action', 'public_comment', 'private_comment')
|
||||
|
||||
|
||||
class AddedRegistrationForm(forms.Form):
|
||||
reason_for_signup = forms.CharField(label=reason_for_signup_label,
|
||||
help_text=reason_for_signup_help_text,
|
||||
widget=Textarea)
|
||||
captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_(
|
||||
"Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um "
|
||||
"sicherzustellen, dass du kein Roboter bist."))
|
||||
|
||||
def signup(self, request, user):
|
||||
pass
|
||||
|
||||
|
||||
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"))
|
||||
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):
|
||||
template_name = "fellchensammlung/forms/form_snippets.html"
|
||||
|
||||
sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False,
|
||||
initial=SexChoicesWithAll.ALL)
|
||||
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED, label=_("Suchradius"))
|
||||
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED,
|
||||
label=_("Suchradius"))
|
||||
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
|
||||
|
||||
|
||||
class RescueOrgSearchForm(forms.Form):
|
||||
template_name = "fellchensammlung/forms/form_snippets.html"
|
||||
|
||||
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
|
||||
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.TWENTY,
|
||||
label=_("Suchradius"))
|
||||
|
||||
@@ -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_REPORT_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,13 @@
|
||||
from django.core.management import BaseCommand
|
||||
from fellchensammlung.tools.admin import mask_organization_contact_data
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Mask e-mail addresses and phone numbers of organizations for testing purposes.'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("domain", type=str)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
domain = options["domain"]
|
||||
mask_organization_contact_data(domain)
|
||||
19
src/fellchensammlung/management/commands/sync_to_twenty.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from django.core.management import BaseCommand
|
||||
from tqdm import tqdm
|
||||
|
||||
from fellchensammlung.models import RescueOrganization
|
||||
from fellchensammlung.tools.twenty import sync_rescue_org_to_twenty
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Send rescue organizations as companies to twenty'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("base_url", type=str)
|
||||
parser.add_argument("token", type=str)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
base_url = options["base_url"]
|
||||
token = options["token"]
|
||||
for rescue_org in tqdm(RescueOrganization.objects.all()):
|
||||
sync_rescue_org_to_twenty(rescue_org, base_url, token)
|
||||
@@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.1 on 2025-08-02 09:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0058_socialmediapost'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='twenty_id',
|
||||
field=models.UUIDField(blank=True, help_text='ID der der Organisation in Twenty', null=True, verbose_name='Twenty-ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rescueorganization',
|
||||
name='specializations',
|
||||
field=models.ManyToManyField(blank=True, to='fellchensammlung.species'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,87 @@
|
||||
# Generated by Django 5.2.1 on 2025-08-30 21:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0059_rescueorganization_twenty_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='adoptionnotice',
|
||||
options={'permissions': [('create_active_adoption_notice', 'Can create an active adoption notice')], 'verbose_name': 'Vermittlung', 'verbose_name_plural': 'Vermittlungen'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='adoptionnoticestatus',
|
||||
options={'verbose_name': 'Vermittlungsstatus', 'verbose_name_plural': 'Vermittlungsstati'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='animal',
|
||||
options={'verbose_name': 'Tier', 'verbose_name_plural': 'Tiere'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='announcement',
|
||||
options={'verbose_name': 'Banner', 'verbose_name_plural': 'Banner'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='comment',
|
||||
options={'verbose_name': 'Kommentar', 'verbose_name_plural': 'Kommentare'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='image',
|
||||
options={'verbose_name': 'Bild', 'verbose_name_plural': 'Bilder'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='importantlocation',
|
||||
options={'verbose_name': 'Wichtiger Standort', 'verbose_name_plural': 'Wichtige Standorte'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='location',
|
||||
options={'verbose_name': 'Standort', 'verbose_name_plural': 'Standorte'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='moderationaction',
|
||||
options={'verbose_name': 'Moderationsaktion', 'verbose_name_plural': 'Moderationsaktionen'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='notification',
|
||||
options={'verbose_name': 'Benachrichtigung', 'verbose_name_plural': 'Benachrichtigungen'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='report',
|
||||
options={'verbose_name': 'Meldung', 'verbose_name_plural': 'Meldungen'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='rescueorganization',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Tierschutzorganisation', 'verbose_name_plural': 'Tierschutzorganisationen'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='rule',
|
||||
options={'verbose_name': 'Regel', 'verbose_name_plural': 'Regeln'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='searchsubscription',
|
||||
options={'verbose_name': 'Abonnierte Suche', 'verbose_name_plural': 'Abonnierte Suchen'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='speciesspecificurl',
|
||||
options={'verbose_name': 'Tierartspezifische URL', 'verbose_name_plural': 'Tierartspezifische URLs'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='subscriptions',
|
||||
options={'verbose_name': 'Abonnement', 'verbose_name_plural': 'Abonnements'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='timestamp',
|
||||
options={'verbose_name': 'Zeitstempel', 'verbose_name_plural': 'Zeitstempel'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='adoptionnotice',
|
||||
name='adoption_notice_status',
|
||||
field=models.TextField(choices=[('active_searching', 'Searching'), ('active_interested', 'Interested'), ('awaiting_action_waiting_for_review', 'Waiting for review'), ('awaiting_action_needs_additional_info', 'Needs additional info'), ('closed_successful_with_notfellchen', 'Successful (with Notfellchen)'), ('closed_successful_without_notfellchen', 'Successful (without Notfellchen)'), ('closed_animal_died', 'Animal died'), ('closed_for_other_adoption_notice', 'Closed for other adoption notice'), ('closed_not_open_for_adoption_anymore', 'Not open for adoption anymore'), ('closed_other', 'Other (closed)'), ('disabled_against_the_rules', 'Against the rules'), ('disabled_unchecked', 'Unchecked'), ('disabled_other', 'Other (disabled)')], default='disabled_other', max_length=64, verbose_name='Status'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,63 @@
|
||||
# Generated by Django 5.2.1 on 2025-08-30 21:51
|
||||
import logging
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def map_status(adoption_notice_status):
|
||||
minor = adoption_notice_status.minor_status
|
||||
|
||||
if minor == "searching":
|
||||
return "active_searching"
|
||||
if minor == "interested":
|
||||
return "active_interested"
|
||||
|
||||
if minor == "waiting_for_review":
|
||||
return "awaiting_action_waiting_for_review"
|
||||
if minor == "needs_additional_info":
|
||||
return "awaiting_action_needs_additional_info"
|
||||
|
||||
if minor == "successful_with_notfellchen":
|
||||
return "closed_successful_with_notfellchen"
|
||||
if minor == "successful_without_notfellchen":
|
||||
return "closed_successful_without_notfellchen"
|
||||
if minor == "animal_died":
|
||||
return "closed_animal_died"
|
||||
if minor == "closed_for_other_adoption_notice":
|
||||
return "closed_for_other_adoption_notice"
|
||||
if minor == "not_open_for_adoption_anymore":
|
||||
return "closed_not_open_for_adoption_anymore"
|
||||
if minor == "other":
|
||||
return "closed_other"
|
||||
|
||||
if minor == "against_the_rules":
|
||||
return "disabled_against_the_rules"
|
||||
if minor == "unchecked":
|
||||
return "disabled_unchecked"
|
||||
if minor in ["missing_information", "technical_error"]:
|
||||
return "disabled_other"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def migrate_status(apps, schema_editor):
|
||||
# We can't import the model directly as it may be a newer
|
||||
# version than this migration expects. We use the historical version.
|
||||
AdoptionNoticeStatus = apps.get_model("fellchensammlung", "AdoptionNoticeStatus")
|
||||
AdoptionNotice = apps.get_model("fellchensammlung", "AdoptionNotice")
|
||||
for ans in AdoptionNoticeStatus.objects.all():
|
||||
adoption_notice = AdoptionNotice.objects.get(id=ans.adoption_notice.id)
|
||||
new_status = map_status(ans)
|
||||
logging.debug(f"{ans.minor_status} -> {new_status}")
|
||||
adoption_notice.adoption_notice_status = map_status(ans)
|
||||
adoption_notice.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('fellchensammlung', '0060_alter_adoptionnotice_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_status),
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.2.1 on 2025-08-30 22:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0061_datamigration_status_model_to_field'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adoptionnotice',
|
||||
name='adoption_notice_status',
|
||||
field=models.TextField(choices=[('active_searching', 'Searching'), ('active_interested', 'Interested'), ('awaiting_action_waiting_for_review', 'Waiting for review'), ('awaiting_action_needs_additional_info', 'Needs additional info'), ('closed_successful_with_notfellchen', 'Successful (with Notfellchen)'), ('closed_successful_without_notfellchen', 'Successful (without Notfellchen)'), ('closed_animal_died', 'Animal died'), ('closed_for_other_adoption_notice', 'Closed for other adoption notice'), ('closed_not_open_for_adoption_anymore', 'Not open for adoption anymore'), ('closed_link_to_more_info_not_reachable', 'Der Link zu weiteren Informationen ist nicht mehr erreichbar.'), ('closed_other', 'Other (closed)'), ('disabled_against_the_rules', 'Against the rules'), ('disabled_unchecked', 'Unchecked'), ('disabled_other', 'Other (disabled)')], max_length=64, verbose_name='Status'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='AdoptionNoticeStatus',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.1 on 2025-09-05 14:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0062_alter_adoptionnotice_adoption_notice_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='adoptionnotice',
|
||||
name='adoption_process',
|
||||
field=models.TextField(blank=True, choices=[('contact_person_in_an', 'Kontaktiere die Person im Vermittlungstext')], max_length=64, null=True, verbose_name='Adoptionsprozess'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,140 @@
|
||||
# Generated by Django 5.2.1 on 2025-09-06 11:11
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0063_adoptionnotice_adoption_process'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='animal',
|
||||
name='name',
|
||||
field=models.CharField(max_length=200, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='animal',
|
||||
name='photos',
|
||||
field=models.ManyToManyField(blank=True, to='fellchensammlung.image', verbose_name='Fotos'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='animal',
|
||||
name='sex',
|
||||
field=models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich, kastriert'), ('I', 'Intergeschlechtlich')], max_length=20, verbose_name='Geschlecht'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='adoption_notice',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='image',
|
||||
name='alt_text',
|
||||
field=models.TextField(help_text='Beschreibe das Bild für blinde und sehbehinderte Menschen', max_length=2000, verbose_name='Alternativtext'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='location',
|
||||
name='city',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Stadt'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='location',
|
||||
name='county',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Landkreis'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='location',
|
||||
name='housenumber',
|
||||
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Hausnummer'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='location',
|
||||
name='latitude',
|
||||
field=models.FloatField(verbose_name='Breitengrad'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='location',
|
||||
name='longitude',
|
||||
field=models.FloatField(verbose_name='Längengrad'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='location',
|
||||
name='postcode',
|
||||
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Postleitzahl'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='location',
|
||||
name='street',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Straße'),
|
||||
),
|
||||
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'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='report',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='report',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert am'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rule',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rule',
|
||||
name='language',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.language', verbose_name='Sprache'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rule',
|
||||
name='rule_identifier',
|
||||
field=models.CharField(help_text='Ein eindeutiger Identifikator der Regel. Ein Regelobjekt derselben Regel in einer anderen Sprache muss den gleichen Identifikator haben', max_length=24, verbose_name='Regel-ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rule',
|
||||
name='rule_text',
|
||||
field=models.TextField(verbose_name='Regeltext'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rule',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert am'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='searchsubscription',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am'),
|
||||
),
|
||||
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, verbose_name='Geschlecht'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='searchsubscription',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert am'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subscriptions',
|
||||
name='adoption_notice',
|
||||
field=models.ForeignKey(help_text='Vermittlung die abonniert wurde', on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='text',
|
||||
name='title',
|
||||
field=models.CharField(max_length=100, verbose_name='Titel'),
|
||||
),
|
||||
]
|
||||
18
src/fellchensammlung/migrations/0065_species_slug.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.1 on 2025-09-06 13:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0064_alter_animal_name_alter_animal_photos_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='slug',
|
||||
field=models.SlugField(null=True, unique=True, verbose_name='Slug'),
|
||||
),
|
||||
]
|
||||
20
src/fellchensammlung/migrations/0066_add_slug_to_species.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.2.1 on 2025-09-06 13:05
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_slug(apps, schema_editor):
|
||||
Species = apps.get_model("fellchensammlung", "Species")
|
||||
for species in Species.objects.all():
|
||||
species.slug = f"species-{species.id}"
|
||||
species.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('fellchensammlung', '0065_species_slug'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_slug),
|
||||
]
|
||||
18
src/fellchensammlung/migrations/0067_alter_species_slug.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.1 on 2025-09-06 13:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0066_add_slug_to_species'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='species',
|
||||
name='slug',
|
||||
field=models.SlugField(unique=True, verbose_name='Slug'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.1 on 2025-09-29 15:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0067_alter_species_slug'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adoptionnotice',
|
||||
name='adoption_notice_status',
|
||||
field=models.TextField(choices=[('active_searching', 'Searching'), ('active_interested', 'Interested'), ('awaiting_action_waiting_for_review', 'Waiting for review'), ('awaiting_action_needs_additional_info', 'Needs additional info'), ('awaiting_action_unchecked', 'Unchecked'), ('closed_successful_with_notfellchen', 'Successful (with Notfellchen)'), ('closed_successful_without_notfellchen', 'Successful (without Notfellchen)'), ('closed_animal_died', 'Animal died'), ('closed_for_other_adoption_notice', 'Closed for other adoption notice'), ('closed_not_open_for_adoption_anymore', 'Not open for adoption anymore'), ('closed_link_to_more_info_not_reachable', 'Der Link zu weiteren Informationen ist nicht mehr erreichbar.'), ('closed_other', 'Other (closed)'), ('disabled_against_the_rules', 'Against the rules'), ('disabled_other', 'Other (disabled)')], max_length=64, verbose_name='Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='image',
|
||||
name='image',
|
||||
field=models.ImageField(help_text='Wähle ein Bild aus', upload_to='images', verbose_name='Bild'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.1 on 2025-10-20 08:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0068_alter_adoptionnotice_adoption_notice_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='regular_check_status',
|
||||
field=models.CharField(choices=[('regular_check', 'Wird regelmäßig geprüft'), ('excluded_no_online_listing', 'Exkludiert: Tiere werden nicht online gelistet'), ('excluded_other_org', 'Exkludiert: Andere Organisation wird geprüft'), ('excluded_scope', 'Exkludiert: Organisation hat nie Notfellchen-relevanten Vermittlungen'), ('excluded_other', 'Exkludiert: Anderer Grund')], default='regular_check', help_text='Organisationen können, durch ändern dieser Einstellung, von der regelmäßigen Prüfung ausgeschlossen werden.', max_length=30, verbose_name='Status der regelmäßigen Prüfung'),
|
||||
),
|
||||
]
|
||||
@@ -1,20 +1,21 @@
|
||||
import uuid
|
||||
from random import choices
|
||||
from tabnanny import verbose
|
||||
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils import timezone
|
||||
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
|
||||
import base64
|
||||
|
||||
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.misc import time_since_as_hr_string
|
||||
from .tools.model_helpers import NotificationTypeChoices, AdoptionNoticeStatusChoices, AdoptionProcess, \
|
||||
AdoptionNoticeStatusChoicesDescriptions, RegularCheckStatusChoices, reason_for_signup_label, \
|
||||
reason_for_signup_help_text
|
||||
from .tools.model_helpers import ndm as NotificationDisplayMapping
|
||||
|
||||
|
||||
class Language(models.Model):
|
||||
@@ -39,24 +40,37 @@ class Language(models.Model):
|
||||
|
||||
|
||||
class Location(models.Model):
|
||||
place_id = models.IntegerField() # OSM id
|
||||
latitude = models.FloatField()
|
||||
longitude = models.FloatField()
|
||||
place_id = models.CharField(max_length=200) # OSM id
|
||||
latitude = models.FloatField(verbose_name=_("Breitengrad"))
|
||||
longitude = models.FloatField(verbose_name=_("Längengrad"))
|
||||
name = models.CharField(max_length=2000)
|
||||
city = models.CharField(max_length=200, blank=True, null=True, verbose_name=_('Stadt'))
|
||||
housenumber = models.CharField(max_length=20, blank=True, null=True, verbose_name=_("Hausnummer"))
|
||||
postcode = models.CharField(max_length=20, blank=True, null=True, verbose_name=_("Postleitzahl"))
|
||||
street = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Straße"))
|
||||
county = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Landkreis"))
|
||||
# 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)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Standort")
|
||||
verbose_name_plural = _("Standorte")
|
||||
|
||||
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 position(self):
|
||||
return (self.latitude, self.longitude)
|
||||
|
||||
@property
|
||||
def str_hr(self):
|
||||
return f"{self.name.split(',')[0]}"
|
||||
|
||||
@staticmethod
|
||||
def get_location_from_string(location_string):
|
||||
try:
|
||||
@@ -73,6 +87,11 @@ class Location(models.Model):
|
||||
latitude=proxy.latitude,
|
||||
longitude=proxy.longitude,
|
||||
name=proxy.name,
|
||||
postcode=proxy.postcode,
|
||||
city=proxy.city,
|
||||
street=proxy.street,
|
||||
county=proxy.county,
|
||||
countrycode=proxy.countrycode,
|
||||
)
|
||||
return location
|
||||
|
||||
@@ -84,35 +103,56 @@ class Location(models.Model):
|
||||
instance.save()
|
||||
|
||||
|
||||
class ImportantLocation(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("Wichtiger Standort")
|
||||
verbose_name_plural = _("Wichtige Standorte")
|
||||
|
||||
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 RescueOrganization(models.Model):
|
||||
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'))
|
||||
slug = models.SlugField(unique=True, verbose_name=_('Slug'))
|
||||
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'))
|
||||
@@ -122,6 +162,7 @@ class RescueOrganization(models.Model):
|
||||
website = models.URLField(null=True, blank=True, verbose_name=_('Website'))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
last_checked = models.DateTimeField(auto_now_add=True, verbose_name=_('Datum der letzten Prüfung'))
|
||||
internal_comment = models.TextField(verbose_name=_("Interner Kommentar"), null=True, blank=True, )
|
||||
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung')) # Markdown allowed
|
||||
external_object_identifier = models.CharField(max_length=200, null=True, blank=True,
|
||||
@@ -129,6 +170,37 @@ class RescueOrganization(models.Model):
|
||||
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"))
|
||||
regular_check_status = models.CharField(max_length=30, choices=RegularCheckStatusChoices.choices,
|
||||
default=RegularCheckStatusChoices.REGULAR_CHECK,
|
||||
verbose_name=_('Status der regelmäßigen Prüfung'),
|
||||
help_text=_(
|
||||
"Organisationen können, durch ändern dieser Einstellung, von der "
|
||||
"regelmäßigen Prüfung ausgeschlossen 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)
|
||||
twenty_id = models.UUIDField(verbose_name=_("Twenty-ID"), null=True, blank=True,
|
||||
help_text=_("ID der der Organisation in Twenty"))
|
||||
|
||||
class Meta:
|
||||
unique_together = ('external_object_identifier', 'external_source_identifier',)
|
||||
ordering = ['name']
|
||||
verbose_name = _("Tierschutzorganisation")
|
||||
verbose_name_plural = _("Tierschutzorganisationen")
|
||||
|
||||
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)])
|
||||
@@ -137,6 +209,29 @@ 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 adoption_notices_in_hierarchy_divided_by_status(self):
|
||||
"""Returns two lists of adoption notices, the first active, the other inactive."""
|
||||
active_adoption_notices = []
|
||||
inactive_adoption_notices = []
|
||||
for an in self.adoption_notices_in_hierarchy:
|
||||
if an.is_active:
|
||||
active_adoption_notices.append(an)
|
||||
else:
|
||||
inactive_adoption_notices.append(an)
|
||||
return active_adoption_notices, inactive_adoption_notices
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
if self.location:
|
||||
@@ -149,7 +244,41 @@ class RescueOrganization(models.Model):
|
||||
if self.description is None:
|
||||
return ""
|
||||
if len(self.description) > 200:
|
||||
return self.description[:200] + f" ... [weiterlesen]({self.get_absolute_url()})"
|
||||
return self.description[:200] + _(f" ... [weiterlesen]({self.get_absolute_url()})")
|
||||
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
|
||||
|
||||
@property
|
||||
def child_organizations(self):
|
||||
return RescueOrganization.objects.filter(parent_org=self)
|
||||
|
||||
def in_distance(self, position, max_distance, unknown_true=True):
|
||||
"""
|
||||
Returns a boolean indicating if the Location of the adoption notice is within a given distance to the position
|
||||
|
||||
If the location is none, we by default return that the location is within the given distance
|
||||
"""
|
||||
return geo.object_in_distance(self, position, max_distance, unknown_true)
|
||||
|
||||
|
||||
# Admins can perform all actions and have the highest trust associated with them
|
||||
@@ -179,8 +308,7 @@ class User(AbstractUser):
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
organization_affiliation = models.ForeignKey(RescueOrganization, on_delete=models.PROTECT, null=True, blank=True,
|
||||
verbose_name=_('Organisation'))
|
||||
reason_for_signup = models.TextField(verbose_name=_("Grund für die Registrierung"), help_text=_(
|
||||
"Wir würden gerne wissen warum du dich registriertst, ob du dich z.B. Tiere eines bestimmten Tierheim einstellen willst 'nur mal gucken' willst. Beides ist toll! Wenn du für ein Tierheim/eine Pflegestelle arbeitest kontaktieren wir dich ggf. um dir erweiterte Rechte zu geben."))
|
||||
reason_for_signup = models.TextField(verbose_name=reason_for_signup_label, help_text=reason_for_signup_help_text)
|
||||
email_notifications = models.BooleanField(verbose_name=_("Benachrichtigung per E-Mail"), default=True)
|
||||
REQUIRED_FIELDS = ["reason_for_signup", "email"]
|
||||
|
||||
@@ -197,14 +325,17 @@ class User(AbstractUser):
|
||||
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 BaseNotification.objects.filter(user=self, read=False)
|
||||
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):
|
||||
@@ -216,8 +347,9 @@ class User(AbstractUser):
|
||||
|
||||
|
||||
class Image(models.Model):
|
||||
image = models.ImageField(upload_to='images')
|
||||
alt_text = models.TextField(max_length=2000, verbose_name=_('Alternativtext'))
|
||||
image = models.ImageField(upload_to='images', verbose_name=_("Bild"), help_text=_("Wähle ein Bild aus"))
|
||||
alt_text = models.TextField(max_length=2000, verbose_name=_('Alternativtext'),
|
||||
help_text=_("Beschreibe das Bild für blinde und sehbehinderte Menschen"))
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
@@ -225,25 +357,18 @@ class Image(models.Model):
|
||||
def __str__(self):
|
||||
return self.alt_text
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Bild")
|
||||
verbose_name_plural = _("Bilder")
|
||||
|
||||
@property
|
||||
def as_html(self):
|
||||
return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">'
|
||||
|
||||
|
||||
class Species(models.Model):
|
||||
"""Model representing a species of animal."""
|
||||
name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
|
||||
verbose_name=_('Name'))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
"""String for representing the Model object."""
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Tierart')
|
||||
verbose_name_plural = _('Tierarten')
|
||||
@property
|
||||
def as_base64(self):
|
||||
encoded_string = base64.b64encode(self.image.file.read())
|
||||
return encoded_string.decode("utf-8")
|
||||
|
||||
|
||||
class AdoptionNotice(models.Model):
|
||||
@@ -251,26 +376,35 @@ class AdoptionNotice(models.Model):
|
||||
permissions = [
|
||||
("create_active_adoption_notice", "Can create an active adoption notice"),
|
||||
]
|
||||
verbose_name = _("Vermittlung")
|
||||
verbose_name_plural = _("Vermittlungen")
|
||||
|
||||
def __str__(self):
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
return self.name
|
||||
return f"[{self.adoptionnoticestatus.as_string()}] {self.name}"
|
||||
return self.name
|
||||
|
||||
created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=timezone.now)
|
||||
searching_since = models.DateField(verbose_name=_('Sucht nach einem Zuhause seit'))
|
||||
name = models.CharField(max_length=200)
|
||||
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"))
|
||||
location = models.ForeignKey(Location, blank=True, null=True, on_delete=models.SET_NULL, )
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Creator'))
|
||||
adoption_notice_status = models.TextField(max_length=64, verbose_name=_('Status'),
|
||||
choices=AdoptionNoticeStatusChoices.all_choices())
|
||||
adoption_process = models.TextField(null=True, blank=True,
|
||||
max_length=64, verbose_name=_('Adoptionsprozess'),
|
||||
choices=AdoptionProcess)
|
||||
|
||||
@property
|
||||
def animals(self):
|
||||
@@ -283,6 +417,21 @@ class AdoptionNotice(models.Model):
|
||||
sexes.add(animal.sex)
|
||||
return sexes
|
||||
|
||||
@property
|
||||
def num_per_sex(self):
|
||||
print(f"{self.pk} x")
|
||||
num_per_sex = dict()
|
||||
for sex in SexChoices:
|
||||
num_per_sex[sex] = len([animal for animal in self.animals if animal.sex == sex])
|
||||
return num_per_sex
|
||||
|
||||
@property
|
||||
def species(self):
|
||||
species = set()
|
||||
for animal in self.animals:
|
||||
species.add(animal.species)
|
||||
return species
|
||||
|
||||
@property
|
||||
def last_checked_hr(self):
|
||||
time_since_last_checked = timezone.now() - self.last_checked
|
||||
@@ -312,17 +461,30 @@ class AdoptionNotice(models.Model):
|
||||
else:
|
||||
return self.location.latitude, self.location.longitude
|
||||
|
||||
@property
|
||||
def description_short(self):
|
||||
def _get_short_description(self, length: int) -> str:
|
||||
if self.description is None:
|
||||
return ""
|
||||
if len(self.description) > 200:
|
||||
return self.description[:200] + f" ... [weiterlesen]({self.get_absolute_url()})"
|
||||
elif len(self.description) > length:
|
||||
return self.description[:length] + f" ... [weiterlesen]({self.get_absolute_url()})"
|
||||
else:
|
||||
return self.description
|
||||
|
||||
@property
|
||||
def description_short(self):
|
||||
return self._get_short_description(200)
|
||||
|
||||
@property
|
||||
def description_100_short(self):
|
||||
return self._get_short_description(90)
|
||||
|
||||
def get_absolute_url(self):
|
||||
"""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)])
|
||||
@@ -350,6 +512,7 @@ class AdoptionNotice(models.Model):
|
||||
photos.extend(animal.photos.all())
|
||||
if len(photos) > 0:
|
||||
return photos
|
||||
return None
|
||||
|
||||
def get_photo(self):
|
||||
"""
|
||||
@@ -373,149 +536,53 @@ class AdoptionNotice(models.Model):
|
||||
|
||||
If the location is none, we by default return that the location is within the given distance
|
||||
"""
|
||||
if unknown_true and self.position is None:
|
||||
return True
|
||||
return geo.object_in_distance(self, position, max_distance, unknown_true)
|
||||
|
||||
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>"
|
||||
@staticmethod
|
||||
def _values_of(list_of_enums):
|
||||
return list(map(lambda x: x[0], list_of_enums))
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
return False
|
||||
return self.adoptionnoticestatus.is_active
|
||||
return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.Active.choices)
|
||||
|
||||
@property
|
||||
def is_disabled_unchecked(self):
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
return False
|
||||
return self.adoptionnoticestatus.is_disabled_unchecked
|
||||
def is_disabled(self):
|
||||
return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.Disabled.choices)
|
||||
|
||||
def set_closed(self):
|
||||
self.last_checked = timezone.now()
|
||||
self.save()
|
||||
self.adoptionnoticestatus.set_closed()
|
||||
@property
|
||||
def is_closed(self):
|
||||
return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.Closed.choices)
|
||||
|
||||
def set_active(self):
|
||||
self.last_checked = timezone.now()
|
||||
self.save()
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
AdoptionNoticeStatus.create_other(self)
|
||||
self.adoptionnoticestatus.set_active()
|
||||
@property
|
||||
def is_awaiting_action(self):
|
||||
return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.AwaitingAction.choices)
|
||||
|
||||
@property
|
||||
def status_description(self):
|
||||
return AdoptionNoticeStatusChoicesDescriptions.mapping[self.adoption_notice_status]
|
||||
|
||||
def set_unchecked(self):
|
||||
self.last_checked = timezone.now()
|
||||
self.adoption_notice_status = AdoptionNoticeStatusChoices.AwaitingAction.UNCHECKED
|
||||
self.save()
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
AdoptionNoticeStatus.create_other(self)
|
||||
self.adoptionnoticestatus.set_unchecked()
|
||||
|
||||
for subscription in self.get_subscriptions():
|
||||
notification_title = _("Vermittlung deaktiviert:") + f" {self.name}"
|
||||
text = _("Die folgende Vermittlung wurde deaktiviert: ") + f"[{self.name}]({self.get_absolute_url()})"
|
||||
BaseNotification.objects.create(user=subscription.owner, text=text, title=notification_title)
|
||||
Notification.objects.create(user_to_notify=subscription.owner,
|
||||
notification_type=NotificationTypeChoices.AN_WAS_DEACTIVATED,
|
||||
adoption_notice=self,
|
||||
text=text,
|
||||
title=notification_title)
|
||||
|
||||
|
||||
class AdoptionNoticeStatus(models.Model):
|
||||
"""
|
||||
The major status indicates a general state of an adoption notice
|
||||
whereas the minor status is used for reporting
|
||||
"""
|
||||
|
||||
ACTIVE = "active"
|
||||
AWAITING_ACTION = "awaiting_action"
|
||||
CLOSED = "closed"
|
||||
DISABLED = "disabled"
|
||||
MAJOR_STATUS_CHOICES = {
|
||||
ACTIVE: "active",
|
||||
AWAITING_ACTION: "in review",
|
||||
CLOSED: "closed",
|
||||
DISABLED: "disabled",
|
||||
}
|
||||
|
||||
MINOR_STATUS_CHOICES = {
|
||||
ACTIVE: {
|
||||
"searching": "searching",
|
||||
"interested": "interested",
|
||||
},
|
||||
AWAITING_ACTION: {
|
||||
"waiting_for_review": "waiting_for_review",
|
||||
"needs_additional_info": "needs_additional_info",
|
||||
},
|
||||
CLOSED: {
|
||||
"successful_with_notfellchen": "successful_with_notfellchen",
|
||||
"successful_without_notfellchen": "successful_without_notfellchen",
|
||||
"animal_died": "animal_died",
|
||||
"closed_for_other_adoption_notice": "closed_for_other_adoption_notice",
|
||||
"not_open_for_adoption_anymore": "not_open_for_adoption_anymore",
|
||||
"other": "other"
|
||||
},
|
||||
DISABLED: {
|
||||
"against_the_rules": "against_the_rules",
|
||||
"missing_information": "missing_information",
|
||||
"technical_error": "technical_error",
|
||||
"unchecked": "unchecked",
|
||||
"other": "other"
|
||||
}
|
||||
}
|
||||
|
||||
major_status = models.CharField(choices=MAJOR_STATUS_CHOICES, max_length=200)
|
||||
minor_choices = {}
|
||||
for key in MINOR_STATUS_CHOICES:
|
||||
minor_choices.update(MINOR_STATUS_CHOICES[key])
|
||||
minor_status = models.CharField(choices=minor_choices, max_length=200)
|
||||
adoption_notice = models.OneToOneField(AdoptionNotice, on_delete=models.CASCADE)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.adoption_notice}: {self.major_status}, {self.minor_status}"
|
||||
|
||||
def as_string(self):
|
||||
return f"{self.major_status}, {self.minor_status}"
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return self.major_status == self.ACTIVE
|
||||
|
||||
@property
|
||||
def is_disabled_unchecked(self):
|
||||
return self.major_status == self.DISABLED and self.minor_status == "unchecked"
|
||||
|
||||
@staticmethod
|
||||
def get_minor_choices(major_status):
|
||||
return AdoptionNoticeStatus.MINOR_STATUS_CHOICES[major_status]
|
||||
|
||||
@staticmethod
|
||||
def create_other(an_instance):
|
||||
# Used as empty status to be changed immediately
|
||||
major_status = AdoptionNoticeStatus.DISABLED
|
||||
minor_status = AdoptionNoticeStatus.MINOR_STATUS_CHOICES[AdoptionNoticeStatus.DISABLED]["other"]
|
||||
AdoptionNoticeStatus.objects.create(major_status=major_status,
|
||||
minor_status=minor_status,
|
||||
adoption_notice=an_instance)
|
||||
|
||||
def set_closed(self):
|
||||
self.major_status = self.MAJOR_STATUS_CHOICES[self.CLOSED]
|
||||
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"]
|
||||
self.save()
|
||||
|
||||
def set_unchecked(self):
|
||||
self.major_status = self.MAJOR_STATUS_CHOICES[self.DISABLED]
|
||||
self.minor_status = self.MINOR_STATUS_CHOICES[self.DISABLED]["unchecked"]
|
||||
self.save()
|
||||
|
||||
def set_active(self):
|
||||
self.major_status = self.MAJOR_STATUS_CHOICES[self.ACTIVE]
|
||||
self.minor_status = self.MINOR_STATUS_CHOICES[self.ACTIVE]["searching"]
|
||||
self.save()
|
||||
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 SexChoices(models.TextChoices):
|
||||
@@ -536,14 +603,19 @@ class SexChoicesWithAll(models.TextChoices):
|
||||
|
||||
|
||||
class Animal(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _('Tier')
|
||||
verbose_name_plural = _('Tiere')
|
||||
|
||||
date_of_birth = models.DateField(verbose_name=_('Geburtsdatum'))
|
||||
name = models.CharField(max_length=200)
|
||||
name = models.CharField(max_length=200, verbose_name=_('Name'))
|
||||
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
|
||||
species = models.ForeignKey(Species, on_delete=models.PROTECT, verbose_name=_("Tierart"))
|
||||
photos = models.ManyToManyField(Image, blank=True)
|
||||
photos = models.ManyToManyField(Image, blank=True, verbose_name=_("Fotos"))
|
||||
sex = models.CharField(
|
||||
max_length=20,
|
||||
choices=SexChoices.choices,
|
||||
verbose_name=_("Geschlecht")
|
||||
)
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE)
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
@@ -601,12 +673,17 @@ class SearchSubscription(models.Model):
|
||||
- On new AdoptionNotice: Check all existing SearchSubscriptions for matches
|
||||
- For matches: Send notification to user of the SearchSubscription
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Abonnierte Suche")
|
||||
verbose_name_plural = _("Abonnierte Suchen")
|
||||
|
||||
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)
|
||||
sex = models.CharField(max_length=20, choices=SexChoicesWithAll.choices, verbose_name=_("Geschlecht"))
|
||||
max_distance = models.IntegerField(choices=DistanceChoices.choices, null=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Zuletzt geändert am"))
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am"))
|
||||
|
||||
def __str__(self):
|
||||
if self.location and self.max_distance:
|
||||
@@ -619,15 +696,24 @@ class Rule(models.Model):
|
||||
"""
|
||||
Class to store rules
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Regel")
|
||||
verbose_name_plural = _("Regeln")
|
||||
|
||||
title = models.CharField(max_length=200)
|
||||
|
||||
# Markdown is allowed in rule text
|
||||
rule_text = models.TextField()
|
||||
language = models.ForeignKey(Language, on_delete=models.PROTECT)
|
||||
rule_text = models.TextField(verbose_name=_("Regeltext"))
|
||||
language = models.ForeignKey(Language, on_delete=models.PROTECT, verbose_name=_("Sprache"))
|
||||
# Rule identifier allows to translate rules with the same identifier
|
||||
rule_identifier = models.CharField(max_length=24)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
rule_identifier = models.CharField(max_length=24,
|
||||
verbose_name=_("Regel-ID"),
|
||||
help_text=_("Ein eindeutiger Identifikator der Regel. Ein Regelobjekt "
|
||||
"derselben Regel in einer anderen Sprache muss den gleichen "
|
||||
"Identifikator haben"))
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Zuletzt geändert am"))
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am"))
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
@@ -635,7 +721,8 @@ class Rule(models.Model):
|
||||
|
||||
class Report(models.Model):
|
||||
class Meta:
|
||||
permissions = []
|
||||
verbose_name = _("Meldung")
|
||||
verbose_name_plural = _("Meldungen")
|
||||
|
||||
ACTION_TAKEN = "action taken"
|
||||
NO_ACTION_TAKEN = "no action taken"
|
||||
@@ -650,8 +737,8 @@ class Report(models.Model):
|
||||
status = models.CharField(max_length=30, choices=STATES)
|
||||
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)
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Zuletzt geändert am"))
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am"))
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.status}]: {self.user_comment:.20}"
|
||||
@@ -660,12 +747,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)
|
||||
@@ -674,6 +789,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)
|
||||
@@ -684,6 +802,10 @@ class ReportComment(Report):
|
||||
|
||||
|
||||
class ModerationAction(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("Moderationsaktion")
|
||||
verbose_name_plural = _("Moderationsaktionen")
|
||||
|
||||
BAN = "user_banned"
|
||||
DELETE = "content_deleted"
|
||||
COMMENT = "comment"
|
||||
@@ -720,7 +842,7 @@ class Text(models.Model):
|
||||
"""
|
||||
Base class to store markdown content
|
||||
"""
|
||||
title = models.CharField(max_length=100)
|
||||
title = models.CharField(max_length=100, verbose_name=_("Titel"))
|
||||
content = models.TextField(verbose_name="Inhalt")
|
||||
language = models.ForeignKey(Language, verbose_name="Sprache", on_delete=models.PROTECT)
|
||||
text_code = models.CharField(max_length=24, verbose_name="Text code", blank=True)
|
||||
@@ -748,6 +870,11 @@ class Announcement(Text):
|
||||
"""
|
||||
Class to store announcements that should be displayed for all users
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Banner")
|
||||
verbose_name_plural = _("Banner")
|
||||
|
||||
logged_in_only = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
@@ -797,10 +924,15 @@ class Comment(models.Model):
|
||||
"""
|
||||
Class to store comments in markdown content
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Kommentar")
|
||||
verbose_name_plural = _("Kommentare")
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
|
||||
text = models.TextField(verbose_name="Inhalt")
|
||||
reply_to = models.ForeignKey("self", verbose_name="Antwort auf", blank=True, null=True, on_delete=models.CASCADE)
|
||||
|
||||
@@ -815,46 +947,63 @@ class Comment(models.Model):
|
||||
return self.adoption_notice.get_absolute_url()
|
||||
|
||||
|
||||
class BaseNotification(models.Model):
|
||||
class Notification(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("Benachrichtigung")
|
||||
verbose_name_plural = _("Benachrichtigungen")
|
||||
|
||||
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(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
|
||||
|
||||
|
||||
class AdoptionNoticeNotification(BaseNotification):
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self.adoption_notice.get_absolute_url
|
||||
def get_body_part(self):
|
||||
return NotificationDisplayMapping[self.notification_type].web_partial
|
||||
|
||||
|
||||
class Subscriptions(models.Model):
|
||||
"""Subscription to a AdoptionNotice"""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Abonnement")
|
||||
verbose_name_plural = _("Abonnements")
|
||||
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'),
|
||||
help_text=_("Vermittlung die abonniert wurde"))
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
@@ -880,11 +1029,16 @@ class Timestamp(models.Model):
|
||||
"""
|
||||
Class to store timestamps based on keys
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Zeitstempel")
|
||||
verbose_name_plural = _("Zeitstempel")
|
||||
|
||||
key = models.CharField(max_length=255, verbose_name=_("Schlüssel"), primary_key=True)
|
||||
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}"
|
||||
|
||||
|
||||
@@ -892,7 +1046,33 @@ class SpeciesSpecificURL(models.Model):
|
||||
"""
|
||||
Model that allows to specify a URL for a rescue organization where a certain species can be found
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Tierartspezifische URL")
|
||||
verbose_name_plural = _("Tierartspezifische URLs")
|
||||
|
||||
species = models.ForeignKey(Species, on_delete=models.CASCADE, verbose_name=_("Tierart"))
|
||||
rescues_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE,
|
||||
verbose_name=_("Tierschutzorganisation"))
|
||||
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,
|
||||
adoption_notice_status__in=AdoptionNoticeStatusChoices.Active.values)
|
||||
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()
|
||||
363
src/fellchensammlung/static/fellchensammlung/css/main.scss
Normal file
@@ -0,0 +1,363 @@
|
||||
$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;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
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: 70px;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/* Embedding Specifics */
|
||||
.embed-main-content {
|
||||
padding: 20px 10px 20px 10px;
|
||||
}
|
||||
|
||||
// FLOATING BUTTON
|
||||
|
||||
.floating {
|
||||
position: fixed;
|
||||
border-radius: 0.3rem;
|
||||
bottom: 4.5rem;
|
||||
right: 1rem;
|
||||
}
|
||||
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;
|
||||
|
||||
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 |
|
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 |
@@ -19,3 +19,6 @@ 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
11
src/fellchensammlung/static/fellchensammlung/js/mousetrap.min.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/* mousetrap v1.6.5 craig.is/killing/mice */
|
||||
(function(q,u,c){function v(a,b,g){a.addEventListener?a.addEventListener(b,g,!1):a.attachEvent("on"+b,g)}function z(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return n[a.which]?n[a.which]:r[a.which]?r[a.which]:String.fromCharCode(a.which).toLowerCase()}function F(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function w(a){return"shift"==a||"ctrl"==a||"alt"==a||
|
||||
"meta"==a}function A(a,b){var g,d=[];var e=a;"+"===e?e=["+"]:(e=e.replace(/\+{2}/g,"+plus"),e=e.split("+"));for(g=0;g<e.length;++g){var m=e[g];B[m]&&(m=B[m]);b&&"keypress"!=b&&C[m]&&(m=C[m],d.push("shift"));w(m)&&d.push(m)}e=m;g=b;if(!g){if(!p){p={};for(var c in n)95<c&&112>c||n.hasOwnProperty(c)&&(p[n[c]]=c)}g=p[e]?"keydown":"keypress"}"keypress"==g&&d.length&&(g="keydown");return{key:m,modifiers:d,action:g}}function D(a,b){return null===a||a===u?!1:a===b?!0:D(a.parentNode,b)}function d(a){function b(a){a=
|
||||
a||{};var b=!1,l;for(l in p)a[l]?b=!0:p[l]=0;b||(x=!1)}function g(a,b,t,f,g,d){var l,E=[],h=t.type;if(!k._callbacks[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(l=0;l<k._callbacks[a].length;++l){var c=k._callbacks[a][l];if((f||!c.seq||p[c.seq]==c.level)&&h==c.action){var e;(e="keypress"==h&&!t.metaKey&&!t.ctrlKey)||(e=c.modifiers,e=b.sort().join(",")===e.sort().join(","));e&&(e=f&&c.seq==f&&c.level==d,(!f&&c.combo==g||e)&&k._callbacks[a].splice(l,1),E.push(c))}}return E}function c(a,b,c,f){k.stopCallback(b,
|
||||
b.target||b.srcElement,c,f)||!1!==a(b,c)||(b.preventDefault?b.preventDefault():b.returnValue=!1,b.stopPropagation?b.stopPropagation():b.cancelBubble=!0)}function e(a){"number"!==typeof a.which&&(a.which=a.keyCode);var b=z(a);b&&("keyup"==a.type&&y===b?y=!1:k.handleKey(b,F(a),a))}function m(a,g,t,f){function h(c){return function(){x=c;++p[a];clearTimeout(q);q=setTimeout(b,1E3)}}function l(g){c(t,g,a);"keyup"!==f&&(y=z(g));setTimeout(b,10)}for(var d=p[a]=0;d<g.length;++d){var e=d+1===g.length?l:h(f||
|
||||
A(g[d+1]).action);n(g[d],e,f,a,d)}}function n(a,b,c,f,d){k._directMap[a+":"+c]=b;a=a.replace(/\s+/g," ");var e=a.split(" ");1<e.length?m(a,e,b,c):(c=A(a,c),k._callbacks[c.key]=k._callbacks[c.key]||[],g(c.key,c.modifiers,{type:c.action},f,a,d),k._callbacks[c.key][f?"unshift":"push"]({callback:b,modifiers:c.modifiers,action:c.action,seq:f,level:d,combo:a}))}var k=this;a=a||u;if(!(k instanceof d))return new d(a);k.target=a;k._callbacks={};k._directMap={};var p={},q,y=!1,r=!1,x=!1;k._handleKey=function(a,
|
||||
d,e){var f=g(a,d,e),h;d={};var k=0,l=!1;for(h=0;h<f.length;++h)f[h].seq&&(k=Math.max(k,f[h].level));for(h=0;h<f.length;++h)f[h].seq?f[h].level==k&&(l=!0,d[f[h].seq]=1,c(f[h].callback,e,f[h].combo,f[h].seq)):l||c(f[h].callback,e,f[h].combo);f="keypress"==e.type&&r;e.type!=x||w(a)||f||b(d);r=l&&"keydown"==e.type};k._bindMultiple=function(a,b,c){for(var d=0;d<a.length;++d)n(a[d],b,c)};v(a,"keypress",e);v(a,"keydown",e);v(a,"keyup",e)}if(q){var n={8:"backspace",9:"tab",13:"enter",16:"shift",17:"ctrl",
|
||||
18:"alt",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"ins",46:"del",91:"meta",93:"meta",224:"meta"},r={106:"*",107:"+",109:"-",110:".",111:"/",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},C={"~":"`","!":"1","@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0",_:"-","+":"=",":":";",'"':"'","<":",",">":".","?":"/","|":"\\"},B={option:"alt",command:"meta","return":"enter",
|
||||
escape:"esc",plus:"+",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p;for(c=1;20>c;++c)n[111+c]="f"+c;for(c=0;9>=c;++c)n[c+96]=c.toString();d.prototype.bind=function(a,b,c){a=a instanceof Array?a:[a];this._bindMultiple.call(this,a,b,c);return this};d.prototype.unbind=function(a,b){return this.bind.call(this,a,function(){},b)};d.prototype.trigger=function(a,b){if(this._directMap[a+":"+b])this._directMap[a+":"+b]({},a);return this};d.prototype.reset=function(){this._callbacks={};
|
||||
this._directMap={};return this};d.prototype.stopCallback=function(a,b){if(-1<(" "+b.className+" ").indexOf(" mousetrap ")||D(b,this.target))return!1;if("composedPath"in a&&"function"===typeof a.composedPath){var c=a.composedPath()[0];c!==a.target&&(b=c)}return"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable};d.prototype.handleKey=function(){return this._handleKey.apply(this,arguments)};d.addKeycodes=function(a){for(var b in a)a.hasOwnProperty(b)&&(n[b]=a[b]);p=null};
|
||||
d.init=function(){var a=d(u),b;for(b in a)"_"!==b.charAt(0)&&(d[b]=function(b){return function(){return a[b].apply(a,arguments)}}(b))};d.init();q.Mousetrap=d;"undefined"!==typeof module&&module.exports&&(module.exports=d);"function"===typeof define&&define.amd&&define(function(){return d})}})("undefined"!==typeof window?window:null,"undefined"!==typeof window?document:null);
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
function mark_checked(index) {
|
||||
document.getElementById('mark_checked_'+index).submit();
|
||||
}
|
||||
|
||||
function open_information(index) {
|
||||
let link = document.getElementById('species_url_'+index+'_1');
|
||||
if (!link) {
|
||||
link = document.getElementById('rescue_org_website_'+index);
|
||||
}
|
||||
window.open(link.href);
|
||||
}
|
||||
|
||||
Mousetrap.bind('c', function() { mark_checked(1); });
|
||||
|
||||
Mousetrap.bind('o', function() { open_information(1); });
|
||||
117
src/fellchensammlung/static/fellchensammlung/js/toggles.js
Normal file
@@ -0,0 +1,117 @@
|
||||
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:not(.js-delete-excluded)') || []).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");
|
||||
});
|
||||
}
|
||||
|
||||
// MODALS //
|
||||
|
||||
function openModal($el) {
|
||||
$el.classList.add('is-active');
|
||||
send("Modal.open", {
|
||||
modal: $el.id
|
||||
});
|
||||
}
|
||||
|
||||
function closeModal($el) {
|
||||
$el.classList.remove('is-active');
|
||||
}
|
||||
|
||||
function closeAllModals() {
|
||||
(document.querySelectorAll('.modal') || []).forEach(($modal) => {
|
||||
closeModal($modal);
|
||||
});
|
||||
}
|
||||
|
||||
// Add a click event on buttons to open a specific modal
|
||||
(document.querySelectorAll('.js-modal-trigger') || []).forEach(($trigger) => {
|
||||
const modal = $trigger.dataset.target;
|
||||
const $target = document.getElementById(modal);
|
||||
|
||||
$trigger.addEventListener('click', () => {
|
||||
openModal($target);
|
||||
});
|
||||
});
|
||||
|
||||
// Add a click event on various child elements to close the parent modal
|
||||
(document.querySelectorAll('.modal-background, .modal-close, .delete, .nf-modal-close') || []).forEach(($close) => {
|
||||
const $target = $close.closest('.modal');
|
||||
|
||||
$close.addEventListener('click', () => {
|
||||
closeModal($target);
|
||||
});
|
||||
});
|
||||
|
||||
// Add a keyboard event to close all modals
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === "Escape") {
|
||||
closeAllModals();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||