Compare commits
791 Commits
a28b6ce4b3
...
develop
Author | SHA1 | Date | |
---|---|---|---|
2589f1c703 | |||
0edb9094c4 | |||
bc8feba701 | |||
f37d74a7d1 | |||
fa8612ad1a | |||
1d8a054b06 | |||
5898fbf86d | |||
cd1cdd2e0b | |||
c0f920544b | |||
36c90531a8 | |||
7f7c5a3b04 | |||
c084e56ad8 | |||
84acc3c76e | |||
e1f0014898 | |||
05b3a470f3 | |||
ebe060646a | |||
bb412be8d3 | |||
e3c48eac24 | |||
da89cdceda | |||
5a6c2c99e5 | |||
9f53836ce8 | |||
5d53d1a1dc | |||
e00dda1dc2 | |||
a93e0c819f | |||
c87733b37a | |||
9aa964bf05 | |||
dcb1d3ec15 | |||
5d9b8f3213 | |||
d12989d195 | |||
a9f384b50e | |||
afedf2d0bd | |||
a4b8486bd4 | |||
d8bcb8ece6 | |||
b01ac219a3 | |||
42320866c4 | |||
e2e6c14d57 | |||
4761c38cd2 | |||
e2bef3efe2 | |||
bbfd4c3800 | |||
b671d8fbb4 | |||
1ea04e98e8 | |||
c1a7d6790b | |||
f519f78922 | |||
551b5ed6be | |||
20cbb0397a | |||
26f999c4cf | |||
9858dfc1bd | |||
2d1df879dc | |||
2f12dc6a5e | |||
4b3286f12d | |||
d53c5707b8 | |||
ba64661217 | |||
554de17e9e | |||
5bc4b538e6 | |||
8a691d59e7 | |||
e99798ba5c | |||
caa962700b | |||
648466aa70 | |||
25f84bf2ad | |||
ac2147095a | |||
4856c720b1 | |||
ba8ff743f2 | |||
1e827af2dd | |||
7d107abc6a | |||
3bb1cd29cd | |||
0388367c7a | |||
a8843dfc8f | |||
9d1264b6e6 | |||
8df04519b9 | |||
112a731cd6 | |||
8b4ff83921 | |||
2249b615f4 | |||
fbfc800453 | |||
752aaf9b89 | |||
f1a0d5f475 | |||
fb5f38b3e6 | |||
ded5299387 | |||
090548905f | |||
12a89b6927 | |||
fc4c348ff9 | |||
1cd133e335 | |||
b5dc6ca97d | |||
f7b98c9dfe | |||
8e9f4e2b2e | |||
b3faa06c4c | |||
165aeb6dcc | |||
24bae28cec | |||
65182d4c2f | |||
1d879993c9 | |||
f3fbf3ba1d | |||
5a24f32327 | |||
d72bd22f6d | |||
0eb94038f6 | |||
3a69397c0a | |||
d49cb5a783 | |||
c13805dd75 | |||
bcf8a0e6e4 | |||
81f9398da4 | |||
32c90aecd3 | |||
01d6c1e0f6 | |||
607a442e22 | |||
b2a79b3547 | |||
63c692d46b | |||
3d2ef9e735 | |||
aee8b0c1e8 | |||
d5e28ba3d9 | |||
1edaf4df14 | |||
b6d31e3c3b | |||
ed7b55c090 | |||
e66e9ad888 | |||
4ad8b30e04 | |||
ae2ac5c462 | |||
2330542a85 | |||
7363e1ab30 | |||
d2131b2c91 | |||
78866c86cd | |||
f5dbccb9c4 | |||
8ab38cc71b | |||
7dfcbfe38f | |||
cd8471036c | |||
c3ec477a6e | |||
5a6294adf6 | |||
1ba44cdd67 | |||
dfeb88f980 | |||
44a724809f | |||
1acd4be953 | |||
930a5383c4 | |||
eda6da7f12 | |||
448fc395d8 | |||
7e8b665c7c | |||
7be61bc9b8 | |||
6828af76dc | |||
fe92d762be | |||
b91a17e950 | |||
aabc549bcf | |||
4bb4d0386b | |||
a74af5e4d3 | |||
a939d53286 | |||
35c6aae552 | |||
83bd6cf7e6 | |||
17a0cfbde0 | |||
dbcad42da0 | |||
1b48022b63 | |||
7b8e3061d5 | |||
3f27564075 | |||
e7c2746eab | |||
1e243496fb | |||
53bc433aaa | |||
6f5e75a1b3 | |||
f83851b694 | |||
3f5a5dceb5 | |||
f9c7dd8c39 | |||
f3e437dbd1 | |||
5ee1e61eac | |||
abd34ec7cb | |||
b73f6db7b6 | |||
3b9f10dad7 | |||
![]() |
53c0e8b3b8 | ||
![]() |
7d264fe131 | ||
![]() |
c968b39657 | ||
![]() |
ebf116f347 | ||
8227866e7a | |||
6f5e73b533 | |||
a302a36fd4 | |||
1307b2ff7b | |||
d9730e765e | |||
8420c698d4 | |||
14be917d43 | |||
20da09fb96 | |||
19de7d3e4c | |||
41d821b86e | |||
b758b54233 | |||
c650266c6b | |||
dae9bb0916 | |||
2f2371d8df | |||
baaf4b70ac | |||
3b1cd800f6 | |||
0f5f7216ac | |||
c40872379e | |||
897ac5ceef | |||
eb3dbb3e45 | |||
9eb6042ba7 | |||
075833aa25 | |||
f84d800bff | |||
20f814b0ef | |||
4376a63e93 | |||
ee3d316175 | |||
452113b4bf | |||
e9b28ea1c1 | |||
ba07533667 | |||
1ab5c4885e | |||
8c977cf255 | |||
d4c6014e17 | |||
078e5e28cc | |||
7010b4f3d2 | |||
43f38b88ce | |||
7a12a1a4d6 | |||
d784f14c4c | |||
339cdf3ea9 | |||
060be3b486 | |||
9f93a19d51 | |||
c131c07afe | |||
42dbf5c6f7 | |||
2e9039a569 | |||
64b48efafb | |||
37e8dc4bdc | |||
61deb96961 | |||
3a6ce1d38b | |||
82fb73ae59 | |||
4e71c8704f | |||
0c1edf647b | |||
9b38898a8a | |||
25348e45e0 | |||
631c2360e6 | |||
6798cf3477 | |||
cc873d6029 | |||
5d147a4fc9 | |||
640862d8ee | |||
99bb53a7a3 | |||
4f05dc18b9 | |||
2a2df3bf52 | |||
c2b15c2175 | |||
edc27b899e | |||
59d96e36a4 | |||
2c976f926c | |||
671c6ec6f5 | |||
ef9ac58c0f | |||
60e6fdf4e4 | |||
06e6455ba0 | |||
007eb3b5a9 | |||
f3333f2da4 | |||
96b40c5169 | |||
d81408b79c | |||
5ae5e90461 | |||
2534ef3319 | |||
0c2e774891 | |||
895bb3c901 | |||
fca5445aa7 | |||
31d2b85b2f | |||
a8b3214c49 | |||
ccdfd388c4 | |||
cad6acd125 | |||
8dc9c1b9e7 | |||
cd4de2528f | |||
3ef4b98c1c | |||
349917e887 | |||
6c200ba076 | |||
f16aa845d2 | |||
3bccb1e690 | |||
10ae697e33 | |||
baf0d2db72 | |||
b30123a890 | |||
44c34d2daa | |||
e010fa413b | |||
e79aca4efa | |||
037f6529fd | |||
14752d9746 | |||
a8b2bd4e90 | |||
60ae971f14 | |||
1920d72821 | |||
01aa8baadd | |||
f2f526c9de | |||
4d490690e2 | |||
d9c7aa8c49 | |||
040299b90c | |||
0bd321e5ec | |||
c038370602 | |||
c08f7fc792 | |||
4dd35c3866 | |||
8bd041d7ea | |||
d450ad42c0 | |||
d34dcada09 | |||
d8448de419 | |||
35ef6676a2 | |||
e132b1c9f6 | |||
5511d8275c | |||
accf877375 | |||
9ac362fa58 | |||
9253fde2e5 | |||
975c962025 | |||
ba72b4e59f | |||
89e001bd17 | |||
623ca8bc0a | |||
0b483ce630 | |||
16998b85d5 | |||
b55952ac67 | |||
30967dac33 | |||
3166faa7eb | |||
9bba81be22 | |||
18a2d16bf6 | |||
9265cdaea9 | |||
fcb9b60656 | |||
599702f50a | |||
c8453db69d | |||
6ad93abe3b | |||
3c60782ae7 | |||
736f645bf0 | |||
b0887ab731 | |||
ada194122d | |||
b3d1ec142b | |||
e7a8a163f1 | |||
c3ef54a267 | |||
da3b43a713 | |||
8cfddd7882 | |||
80eafbb014 | |||
cc2a659767 | |||
cacfeff3fe | |||
9379728b71 | |||
d30d15c0d4 | |||
5343f53661 | |||
3126b2b962 | |||
43c671018b | |||
7a37377a09 | |||
19d9dea8b1 | |||
c50e0b18b5 | |||
4c07c0feb2 | |||
cf15b60bef | |||
328f64aa51 | |||
fdf4e79a69 | |||
bbc8732112 | |||
17dbe85219 | |||
3dc011a22c | |||
f5b89456ab | |||
2e4f63b250 | |||
3b261ff240 | |||
f06b00fb9d | |||
88987a973e | |||
93ffbe09af | |||
e11848ea72 | |||
8bc9d12bfa | |||
1dbfdccb89 | |||
f085f5dcf5 | |||
33579e8446 | |||
a852da365f | |||
b53095ae17 | |||
3d7780e0ba | |||
478636bd98 | |||
d9ebee1e07 | |||
23e154bce6 | |||
5624f59258 | |||
56df942dd0 | |||
2dcb5fbf88 | |||
7a84b470f9 | |||
76232b7a0f | |||
349af16075 | |||
8641bead80 | |||
eb930b71d6 | |||
ae4ba06abf | |||
a2e237a81f | |||
f90c8c7e8c | |||
c316c74aff | |||
93dd0ae4f6 | |||
f79bb355cf | |||
45a534a042 | |||
2106a3423f | |||
d3f7274e92 | |||
5f576896b7 | |||
4a3cbfb8b0 | |||
3e93fe1a7a | |||
965e055ef1 | |||
13a0da6e46 | |||
1bb05dbf1c | |||
4c9c1e13a5 | |||
99cde15966 | |||
f2edc23e75 | |||
8aab4a13ae | |||
226102ccaf | |||
3d088c55d7 | |||
bb14a346cb | |||
f387930dee | |||
fe63e3b25c | |||
23adeb06e6 | |||
c1bd458c80 | |||
2a1d4178d7 | |||
f9a37b299d | |||
9950e87501 | |||
eff1ba6513 | |||
bb085aa9a8 | |||
b0dc0f9d78 | |||
d1a51b019c | |||
b7fade55fb | |||
79461518a3 | |||
8059d5d23f | |||
3098eacfb4 | |||
f3d1e1c203 | |||
e6a985ddfa | |||
388cc327be | |||
13adc695f6 | |||
f2c7943247 | |||
112fd52864 | |||
8279385966 | |||
1a9692949f | |||
e7af49b309 | |||
b822914db3 | |||
9ad33efe08 | |||
bd8f9fc1b7 | |||
4a2c18be4d | |||
479aba0195 | |||
1299fcac84 | |||
884a07f87b | |||
6557e9f9eb | |||
602cef1302 | |||
b400db603a | |||
0397311f6e | |||
abce89c829 | |||
bbad63a460 | |||
d940630086 | |||
37ecf28f2f | |||
12d5a976cc | |||
9086e2e75b | |||
3607eb0e4e | |||
3daf83d725 | |||
5ad0cb74cc | |||
9ae64e8cb1 | |||
1b5a0c71e0 | |||
4d4f11c479 | |||
835c89d1d4 | |||
46bf07dd8d | |||
f557672586 | |||
4e27e1be7f | |||
6d390ad21e | |||
2f2543160e | |||
64a9db133e | |||
712c3d32f3 | |||
8998bbdf6d | |||
ff31caa139 | |||
ad06829c31 | |||
03a48da355 | |||
885bed888d | |||
0051cb07c9 | |||
8858cff9cf | |||
70e2af6172 | |||
461abd2e46 | |||
![]() |
d7269106db | ||
77fb99a527 | |||
38a56daa24 | |||
![]() |
ac0749797f | ||
f193f7d7ca | |||
43657e0862 | |||
68ad366f74 | |||
350d2c5da9 | |||
462bb8f485 | |||
ea4d15b99a | |||
de30dfcb8b | |||
36a979954c | |||
71ef17dc97 | |||
206cd282e6 | |||
e399346c3e | |||
929c6dfff0 | |||
841b57fea2 | |||
9e5446ff1d | |||
3b79809b8c | |||
53e6db3655 | |||
424f91e919 | |||
84ce5f54b2 | |||
a7e85212c0 | |||
f1b3b660ff | |||
26cb60c1c8 | |||
69e58f1e0a | |||
5c33ac3833 | |||
fccfd59ea3 | |||
50897b6d35 | |||
8edfe8c401 | |||
0d82dba414 | |||
2dc038dfef | |||
c46a943c7f | |||
9f3592e64b | |||
bc1f4e7ab7 | |||
a2ef91e89a | |||
91d740511d | |||
c6af3e8d04 | |||
0c94049e21 | |||
29f1d2f0f2 | |||
2578e96b32 | |||
907ed583cd | |||
da51007b77 | |||
087f58c9ac | |||
860da7f06a | |||
457bee1ede | |||
3b37b5f588 | |||
6229f0f8a2 | |||
b2a3d910d9 | |||
33848cbe15 | |||
cc97fe32aa | |||
4576ac68e0 | |||
7c076e0bc3 | |||
74f54c7b31 | |||
87777cd5a4 | |||
eee4cdf86b | |||
b2d5265f7e | |||
d4af2d88b4 | |||
8b4f5713e3 | |||
4bff268537 | |||
57da42e4bd | |||
2864d27a7f | |||
0a73b5099e | |||
e3fb981542 | |||
5e80d75c91 | |||
e3833b4505 | |||
ab837ee80e | |||
f6c1224dde | |||
a78d671b6d | |||
fb9c78d96a | |||
4ef9da953c | |||
aefeffd63a | |||
81cc5cd53d | |||
002dded0d5 | |||
ad6e2f4e17 | |||
160e7166f8 | |||
867319fe9a | |||
13b67c1248 | |||
4c4cf4afea | |||
5f742c60db | |||
568874e6dd | |||
561a30b7ab | |||
a8c837e9f6 | |||
a75cacea66 | |||
b1e092769f | |||
5a93a1678c | |||
28772e1f74 | |||
1f3c3ecaef | |||
ab1e6a94d1 | |||
299653b53b | |||
fe9352e628 | |||
9fec95bd2e | |||
8e7cdafee0 | |||
6e2a2a1d5e | |||
5197875431 | |||
d05bd45cf4 | |||
0afb2bb0ce | |||
d17fcc1da2 | |||
c508bc2cd1 | |||
20872e547b | |||
25b748d2be | |||
1536bb302a | |||
d4ef706734 | |||
3bdce18e9e | |||
8b4488484d | |||
3881a4f3b4 | |||
2dbd908f4c | |||
9d0eed5915 | |||
ee12bb5286 | |||
5669c822b9 | |||
c1c4af6571 | |||
164ba7def2 | |||
7035b1642e | |||
b6fc5c634f | |||
0dfbd614ab | |||
2730ff3f51 | |||
fef211b2d0 | |||
f2e2599561 | |||
a9c0f628f7 | |||
e2adb20231 | |||
e8b3bf6516 | |||
3306f3e783 | |||
b993621773 | |||
3816290eb7 | |||
399ecf73ad | |||
8e2c0e857c | |||
3c7dcb4c51 | |||
9e1ec1711b | |||
bae4ee3d22 | |||
280eb83056 | |||
fca5879aeb | |||
373a44c9da | |||
674645c65c | |||
c2b3ff2395 | |||
d6740eb302 | |||
35a54474b4 | |||
6723dad4bd | |||
b51d04ffd1 | |||
a965f26d48 | |||
364a6f32f4 | |||
533142461a | |||
481635ac4e | |||
be6c30cb33 | |||
a617137fb0 | |||
8299162a77 | |||
085162d802 | |||
27b7e47f18 | |||
be97ac32fb | |||
9ea00655d4 | |||
9fffbffdb7 | |||
44cf2936d1 | |||
579f59580c | |||
241841bc9b | |||
78a6440f63 | |||
9d521b0129 | |||
39079c3c8e | |||
999c1a81b8 | |||
5a4720c41c | |||
858c6d4468 | |||
4b45b01e2a | |||
d0060ecf5e | |||
d1eeaafc42 | |||
9b824bc326 | |||
44f05cbb7d | |||
0e4e531414 | |||
6a7b3f19e9 | |||
ec9f5b305c | |||
e858f61b3f | |||
a04270718f | |||
a4f895de81 | |||
b2d0e783be | |||
4f5022e140 | |||
5771968981 | |||
b63b87872b | |||
1594b754cb | |||
8ec27191b6 | |||
c1332ee1f0 | |||
f6240a7189 | |||
7a02774a29 | |||
8945fdc0f4 | |||
9f0a18ad91 | |||
e7f26dd23a | |||
fc5b1391df | |||
70bf8e2053 | |||
caf98ba60b | |||
d7e466050a | |||
34b707ef20 | |||
064a9bf83a | |||
93070a3bcd | |||
23c35fe7dd | |||
d2542060a1 | |||
89f74cb709 | |||
ec38012ecb | |||
72d45a4f47 | |||
8de5f162eb | |||
dc3859d589 | |||
b4f52c7876 | |||
885622e581 | |||
a42a3fa177 | |||
27541c6fb6 | |||
14547ad621 | |||
8d2d80c30e | |||
e6f5a42d15 | |||
052e42f76a | |||
3eb7dbe984 | |||
202dfe46c2 | |||
01da0f1e29 | |||
8ccdf50bc5 | |||
d46ab8da6b | |||
1dd53a87e9 | |||
40bb2e54bd | |||
433ad9d4b9 | |||
231c27819d | |||
890309564f | |||
e1e1f822c8 | |||
7a788f4c90 | |||
7efa626b8b | |||
08e20e1875 | |||
f1c79a5f94 | |||
5dd1991af8 | |||
c0edef51bd | |||
cb703e79ae | |||
87066b0cea | |||
c4976c4b34 | |||
ee46ff9cda | |||
d4f27e8f2f | |||
4a6584370e | |||
82d3f95c99 | |||
dce3d89c7e | |||
5520590145 | |||
efabebfdbf | |||
6c52246bb7 | |||
2c11f7c385 | |||
9ee0bd8e30 | |||
1955476d24 | |||
05178da029 | |||
7a80cf8df1 | |||
db94ec41ed | |||
5582538a70 | |||
7aa364fc38 | |||
96ce5963fe | |||
bf54bc5d51 | |||
93ae172431 | |||
03d40a5092 | |||
993f8f9cd2 | |||
8efc0aad21 | |||
3a6e7f5344 | |||
dac9661d51 | |||
b9bfa8e359 | |||
d07589464c | |||
1880da5151 | |||
4e953c83ea | |||
2212df4729 | |||
98d67381c6 | |||
e02672c2bb | |||
c3dd9faa85 | |||
9f977e35c2 | |||
3269d5a39a | |||
d96a44bbdd | |||
2641b2e7bf | |||
50c1a4f2c6 | |||
573630f9ee | |||
1a09b7859f | |||
70b3ae4bbc | |||
5eaafe7646 | |||
5781b49c7c | |||
e2f516d409 | |||
ca8996fff6 | |||
eb734d2716 | |||
655e304c6c | |||
8e34ed440e | |||
0c7080f005 | |||
0b93b5eccb | |||
f1d9f7ad22 | |||
4e71ac7866 | |||
1d0a42a7e1 | |||
d384e75746 | |||
70154abd37 | |||
ab3437e61d | |||
0ccbb18411 | |||
e6f12ce5b1 | |||
6325de17d9 | |||
b9d6293546 | |||
dbe52e4884 | |||
3c286d84d8 | |||
227fa4d5a8 | |||
d47f181e1d | |||
272046142e | |||
5c18832961 | |||
d59cc0034a | |||
64024be833 | |||
5ef20bdce0 | |||
7ddd7b0c0c | |||
cbd8700917 | |||
6eb2f5000f | |||
1cd70228b9 | |||
23d8e85031 | |||
4fb92d8215 | |||
6dfc92bf15 | |||
2015f8b332 | |||
66a0b42718 | |||
efecfc910d | |||
96bc44c508 | |||
a2c8f469a7 | |||
a98b428614 | |||
dfede77e98 | |||
6702211c05 | |||
f97e682640 | |||
c1e3248cc8 | |||
0e67b777b5 | |||
0435c427b3 | |||
be2df6970a | |||
1f5e7856b1 | |||
793de1ec64 | |||
6844e771b5 | |||
1282b6b201 | |||
975de1a230 | |||
fd3478600f | |||
4d2991ba2f | |||
5aaaf57dd4 | |||
766b19e7c2 | |||
f660a6b49a | |||
ab0c1a5c46 | |||
74a6b5f2aa | |||
e38234b736 | |||
ce38002676 | |||
314cdfdd7c | |||
4504a18f60 | |||
df41028e99 | |||
f404cfa0a3 | |||
72dedb6b0c | |||
17468097ec | |||
28331f105a | |||
39893c2185 | |||
ab2b91735e | |||
1b9574cca9 | |||
0d52101f22 | |||
96c0c1218f | |||
864c76bc21 | |||
c3646e6334 | |||
83a219df0c | |||
e6428965c4 | |||
7f31c58abf | |||
1c3d2c7cf5 | |||
90489e01b0 | |||
ed8ccd96f4 | |||
2d690d4536 | |||
4ca5ed733c | |||
61cee697e4 | |||
8798f38f2a | |||
cf061e02d2 | |||
258d9827fd | |||
cc95a2832d | |||
73a6abef18 | |||
0fcc0e5d11 | |||
f5e7fb1a12 | |||
d9232a1095 | |||
022bf577d4 |
4
.coveragerc
Normal file
4
.coveragerc
Normal file
@@ -0,0 +1,4 @@
|
||||
[run]
|
||||
omit =
|
||||
*/migrations/*
|
||||
*/tests/*
|
9
.gitignore
vendored
9
.gitignore
vendored
@@ -3,10 +3,16 @@
|
||||
# Database
|
||||
notfellchen
|
||||
|
||||
# Geojson from imports
|
||||
*.geojson
|
||||
|
||||
# Media storage
|
||||
static
|
||||
/static
|
||||
media
|
||||
|
||||
# Compiled CSS
|
||||
/src/fellchensammlung/static/fellchensammlung/css/main.css
|
||||
/src/fellchensammlung/static/fellchensammlung/css/main.css.map
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
@@ -161,3 +167,4 @@ dmypy.json
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
/node_modules/
|
||||
|
24
.woodpecker.yml
Normal file
24
.woodpecker.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
|
||||
steps:
|
||||
build:
|
||||
image: moanos/sphinx-rtd
|
||||
commands:
|
||||
- cd docs && make html
|
||||
|
||||
deploy:
|
||||
image: appleboy/drone-scp
|
||||
settings:
|
||||
strip_components: 3
|
||||
host:
|
||||
from_secret: host
|
||||
username:
|
||||
from_secret: ssh_user
|
||||
target:
|
||||
from_secret: path
|
||||
source: docs/_build/html/
|
||||
key:
|
||||
from_secret: ssh_key
|
||||
|
||||
|
||||
|
15
CHANGELOG.md
Normal file
15
CHANGELOG.md
Normal file
@@ -0,0 +1,15 @@
|
||||
## Version 0.4.0
|
||||
|
||||
Version 0.4.0 has added support for search-as-you-type when searching for animals to adopt. Furthermore, the display of
|
||||
maps in the search has been majorly improved.
|
||||
|
||||
Photon has been added as geocoding source option which allows to use this functionality.
|
||||
|
||||
Further improvements include the representation of rescue organizations and tooltips.
|
||||
|
||||
One of the biggest features is the addition of search subscriptions. These allow you to not only
|
||||
search for currently active adoption notices but to subscribe to that search so that you get notified if there are new
|
||||
rats in your search area in the future.
|
||||
|
||||
For developers the new API documentation might come in handy, it can be found at
|
||||
[/api/schema/swagger-ui/](https://notfellchen.org/api/schema/swagger-ui/)
|
@@ -1,10 +1,12 @@
|
||||
FROM python:3-slim
|
||||
MAINTAINER Julian-Samuel Gebühr
|
||||
FROM python:3.11-slim
|
||||
# Use 3.11 to avoid django.core.exceptions.ImproperlyConfigured: Error loading psycopg2 or psycopg module
|
||||
LABEL org.opencontainers.image.authors="Julian-Samuel Gebühr"
|
||||
|
||||
ENV DOCKER_BUILD=true
|
||||
|
||||
RUN apt update
|
||||
RUN apt install gettext -y
|
||||
RUN apt install libpq-dev gcc -y
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN mkdir /app/data
|
||||
|
125
README.md
125
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
|
||||
@@ -44,21 +44,63 @@ nf query_location <query>
|
||||
There is a system for customizing texts in Notfellchen. Not every change of a tet should mean an update of the software. But this should also not become a CMS.
|
||||
Therefore, a solution is used where a number of predefined texts per site are supported. These markdown texts will then be included in the site, if defined.
|
||||
|
||||
| Textcode | Location |
|
||||
|---------------------|----------|
|
||||
| `how_to` | Index |
|
||||
| `introduction` | Index |
|
||||
| `privacy_statement` | About |
|
||||
| `terms_of_service` | About |
|
||||
| `imprint` | About |
|
||||
| Any rule | About |
|
||||
| Textcode | Location |
|
||||
|-------------------------|-----------------------|
|
||||
| `how_to` | Index |
|
||||
| `introduction` | Index |
|
||||
| `privacy_statement` | About |
|
||||
| `terms_of_service` | About |
|
||||
| `imprint` | About |
|
||||
| `about_us` | About |
|
||||
| `external_site_warning` | External Site Warning |
|
||||
| Any rule | About |
|
||||
|
||||
# Developer Notes
|
||||
|
||||
## 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
|
||||
@@ -75,20 +117,37 @@ docker push moanos/notfellchen:latest
|
||||
docker run -p8000:7345 moanos/notfellchen:latest
|
||||
```
|
||||
|
||||
|
||||
## Testing
|
||||
|
||||
Tests can be run with
|
||||
|
||||
```zsh
|
||||
nf test src
|
||||
```
|
||||
|
||||
If you want to report on code coverage run
|
||||
|
||||
```zsh
|
||||
coverage run --source='.' src/manage.py test src
|
||||
```
|
||||
|
||||
and
|
||||
|
||||
```
|
||||
coverage report
|
||||
```
|
||||
|
||||
## Geocoding
|
||||
|
||||
Geocoding services (search map data by name, address or postcode) are provided via the
|
||||
[Nominatim](https://nominatim.org/) API, powered by [OpenStreetMap](https://openstreetmap.org) data. Notfellchen uses
|
||||
a selfhosted Nominatim instance to avoid overburdening the publicly hosted instance. Due to ressource constraints
|
||||
geocoding is only supported for Germany right now.
|
||||
|
||||
ToDos
|
||||
* [ ] Implement a report that shows the number of location strings that could not be converted into a location
|
||||
* [x] Add a management command to re-query location strings to fill location
|
||||
either [Nominatim](https://nominatim.org/) or [photon](https://github.com/komoot/photon) API, powered by [OpenStreetMap](https://openstreetmap.org) data.
|
||||
Notfellchen uses a selfhosted Photon instance to avoid overburdening the publicly hosted instance.
|
||||
|
||||
## Maps
|
||||
|
||||
The map on the main homepage is powered by [Versatiles](https://versatiles.org), and rendered using [Maplibre](https://maplibre.org/).
|
||||
The Versatiles server is self-hosted and does not send data to third parties.
|
||||
|
||||
## Translation
|
||||
|
||||
@@ -106,3 +165,37 @@ Use a program like `gtranslator` or `poedit` to start translations
|
||||
| Edit adoption notice | User that created, Moderator, Admin |
|
||||
| Edit animal | User that created, Moderator, Admin |
|
||||
| Add animal/photo to adoption notice | User that created, Moderator, Admin |
|
||||
|
||||
# Celery and KeyDB
|
||||
|
||||
Start KeyDB docker container
|
||||
```zsh
|
||||
docker run -d --name keydb -p 6379:6379 eqalpha/keydb
|
||||
```
|
||||
|
||||
Start worker
|
||||
```zsh
|
||||
celery -A notfellchen.celery worker
|
||||
```
|
||||
|
||||
Start beat
|
||||
```zsh
|
||||
celery -A notfellchen.celery beat
|
||||
```
|
||||
|
||||
# Contributing
|
||||
|
||||
This project is currently mainly developed by me, moanos. I'd like that to change and will be very happy for contributions
|
||||
and shared responsibilities. Some ideas where you can look for contributing first
|
||||
|
||||
* UI improvements: Since a major redesign I'm much happier but the UI could use many, many little tweaks
|
||||
* Docker: If you know how to build a docker container that is a) smaller or b) utilizes staged builds this would be amazing. Any improvement welcome
|
||||
* Testing: Writing tests is always welcome, and it's likely you discover a few bugs
|
||||
|
||||
I'm also very happy for all other contributions. Before you do large refactoring efforts or features, best write a short
|
||||
issue for it before you spend a lot of work.
|
||||
|
||||
Send PRs either to [codeberg](https://codeberg.org/moanos/notfellchen) (preferred) or [GitHub](https://github.com/moan0s/notfellchen).
|
||||
CI (currently only for documentation) runs via [git.hyteck.de](https://git.hyteck.de), you can also ask moanos for an account there.
|
||||
|
||||
Also welcome are new issues with suggestions or bugs and additions to the documentation.
|
||||
|
1
docs/.gitignore
vendored
Normal file
1
docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
_build/
|
107
docs/API/index.rst
Normal file
107
docs/API/index.rst
Normal file
@@ -0,0 +1,107 @@
|
||||
*****************
|
||||
API Documentation
|
||||
*****************
|
||||
|
||||
The Notfellchen API serves the purpose of supporting 3rd-person applications, whether you want to display data in a custom format or add data from other sources.
|
||||
|
||||
.. warning::
|
||||
The current API is limited in it's functionality. I you miss a specific feature please contact the developers!
|
||||
|
||||
API Access
|
||||
==========
|
||||
|
||||
Via browser
|
||||
-----------
|
||||
|
||||
When a user is logged in, they can easily access the API in their browser, authenticated by their session.
|
||||
|
||||
For example: You can check all current adoption notices here: https://notfellchen.org/api/adoption_notice
|
||||
|
||||
Via token
|
||||
---------
|
||||
|
||||
|
||||
All users are able to generate a token that allows them to use the API. This can be done in the user's profile.
|
||||
An application can then send this token in the request header for authorization.
|
||||
|
||||
.. code-block::
|
||||
$ curl -X GET http://notfellchen.org/api/adoption_notice -H 'Authorization: Token 49b39856955dc6e5cc04365498d4ad30ea3aed78'
|
||||
|
||||
|
||||
.. warning::
|
||||
Usage or creation of content still has to follow the terms of notfellchen.org.
|
||||
Copyright of content is often held by rescue organizations, so you are not allowed to simply mirror content.
|
||||
Talk to the notfellchen team if you want develop such things.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
All Endpoints are documented at https://notfellchen.org/api/schema/swagger-ui/ or at https://notfellchen.org/api/schema/redoc/ if you prefer redoc.
|
||||
The OpenAI schema can be downloaded at https://notfellchen.org/api/schema/
|
||||
|
||||
Examples are documented here.
|
||||
|
||||
Get Adoption Notices
|
||||
++++++++++++++++++++
|
||||
|
||||
.. code-block::
|
||||
|
||||
curl --request GET \
|
||||
--url https://notfellchen.org/api/adoption_notice \
|
||||
--header 'Authorization: {{token}}'
|
||||
|
||||
Create Adoption Notice
|
||||
++++++++++++++++++++++
|
||||
|
||||
.. code-block::
|
||||
|
||||
curl --request POST \
|
||||
--url https://notfellchen.org/api/adoption_notice \
|
||||
--header 'Authorization: {{token}}' \
|
||||
--header 'content-type: multipart/form-data' \
|
||||
--form name=TestAdoption1 \
|
||||
--form searching_since=2024-11-19 \
|
||||
--form 'description=Lorem ipsum **dolor sit** amet' \
|
||||
--form further_information=https://notfellchen.org \
|
||||
--form location_string=Berlin \
|
||||
--form group_only=true
|
||||
|
||||
Add Animal to Adoption Notice
|
||||
+++++++++++++++++++++++++++++
|
||||
|
||||
.. code-block::
|
||||
|
||||
curl --request POST \
|
||||
--url https://notfellchen.org/api/animals/ \
|
||||
--header 'Authorization: {{token}}' \
|
||||
--header 'content-type: multipart/form-data' \
|
||||
--form name=TestAnimal1 \
|
||||
--form date_of_birth=2024-11-19 \
|
||||
--form 'description=Lorem animal **dolor sit**.' \
|
||||
--form sex=F \
|
||||
--form species=1 \
|
||||
--form adoption_notice=1
|
||||
|
||||
Add picture to Animal or Adoption Notice
|
||||
++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
.. code-block::
|
||||
|
||||
curl -X POST https://notfellchen.org/api/images/ \
|
||||
-H "Authorization: Token {{token}}" \
|
||||
-F "image=@256-256-crop.jpg" \
|
||||
-F "alt_text=Puppy enjoying the sunshine" \
|
||||
-F "attach_to_type=animal" \
|
||||
-F "attach_to=48
|
||||
|
||||
Species
|
||||
+++++++
|
||||
|
||||
Getting available species is mainly important when creating animals
|
||||
|
||||
.. code-block::
|
||||
|
||||
curl --request GET \
|
||||
--url https://notfellchen.org/api/species \
|
||||
--header 'Authorization: {{token}}'
|
19
docs/Makefile
Normal file
19
docs/Makefile
Normal file
@@ -0,0 +1,19 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
99
docs/README.md
Normal file
99
docs/README.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# QZT Dokumentation
|
||||
|
||||

|
||||
|
||||
# Quickstart
|
||||
|
||||
Create & activate a virtual environment to avoid cluttering your system
|
||||
|
||||
```zsh
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
Install dependencies
|
||||
|
||||
```zsh
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
And serve a local development version
|
||||
|
||||
```zsh
|
||||
cd docs
|
||||
sphinx-autobuild ./ ./_build/html
|
||||
```
|
||||
|
||||
You can now access the documentation on [http://127.0.0.1:8000](http://127.0.0.1:8000). It will be rebuilt automatically upon file changes.
|
||||
|
||||
If you only want to build the static files once you can do `make html`.
|
||||
|
||||
## Docker
|
||||
|
||||
Build the docker image with
|
||||
|
||||
```bash
|
||||
docker build . -t sphinx-qzt
|
||||
```
|
||||
|
||||
and use it to build the documentation like this
|
||||
|
||||
```bash
|
||||
docker run --rm -v ./docs:/docs sphinx-qzt make html
|
||||
```
|
||||
|
||||
# QZT Dokumentation
|
||||
|
||||
# Quickstart
|
||||
|
||||
Create & activate a virtual environment to avoid cluttering your system
|
||||
|
||||
```zsh
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
Install dependencies
|
||||
|
||||
```zsh
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
And serve a local development version
|
||||
|
||||
```zsh
|
||||
cd docs
|
||||
sphinx-autobuild ./ ./_build/html
|
||||
```
|
||||
|
||||
You can now access the documentation on <http://127.0.0.1:8000>. It will be rebuilt automatically upon file changes.
|
||||
|
||||
If you only want to build the static files once you can do `make html`.
|
||||
|
||||
## Docker
|
||||
|
||||
Build the docker image with
|
||||
|
||||
```bash
|
||||
docker build . -t sphinx-rtd
|
||||
```
|
||||
|
||||
and use it to build the documentation like this
|
||||
|
||||
```bash
|
||||
docker run --rm -v ./docs:/docs sphinx-rtd make html
|
||||
```
|
||||
|
||||
# CI
|
||||
|
||||
Woodpecker can be used to deploy the documentation to a server. Fo that purpose it builds a docker container that contains
|
||||
sphinx and the read-the-docs theme, builds the documentation with that and pushes it to a server which will serve the static files.
|
||||
|
||||
| Key | Example | Description |
|
||||
|-------------------|-------------------------------------------------------------|-------------------------------------------------------------|
|
||||
| `host` | `dokumentation.notfellchen.org` | Hostename of the server where you want to deploy |
|
||||
| `ssh_user` | `username` | User on the server |
|
||||
| `ssh_key` | `-----BEGIN OPENSSH PRIVATE KEY-----` | The private SSH key of the user |
|
||||
| `path` | `/static_sites/static-hyteck/dokumentation.notfellchen.org` | Path where to deploy the static files. |
|
||||
| `docker_username` | `moanos` | Username authenticate to dockerhub to push the docker image |
|
||||
| `docker_password` | `SUPERSECRET` | Password authenticate to dockerhub to push the docker image |
|
38
docs/admin/GDPR.rst
Normal file
38
docs/admin/GDPR.rst
Normal file
@@ -0,0 +1,38 @@
|
||||
GDPR
|
||||
====
|
||||
|
||||
The GDPR provides the user with different rights regarding their data and Notfellchen tries to help you fulfill these requirements.
|
||||
For this application there are different scenarios that are applicable.
|
||||
|
||||
Transparency and Modality
|
||||
-------------------------
|
||||
|
||||
The user must be informed in a "in a concise, transparent, intelligible and easily accessible form,
|
||||
using clear and plain language". This is currently up to you, to provide an imprint with such information.
|
||||
|
||||
Information and Access
|
||||
----------------------
|
||||
|
||||
Article 15 gives the user the right to access their personal data and information about how this personal data is being
|
||||
processed, specifically the purpose of the processing (Article 15(1)(a)), with whom the data is shared
|
||||
(Article 15(1)(c)), and how it acquired the data (Article 15(1)(g)).
|
||||
|
||||
For Notfellchen this could be a short description::
|
||||
|
||||
Notfellchen processes your data to provide you with the functionality of the plattform, like creation of adoption notices.
|
||||
Your data is not given to third parties. Your data was provided by you or added by staff/automatically if you consented to this.
|
||||
It is also possible that there is data of you accessing resources of this program to prevent malicious activity and to improve the software in it's functionality.
|
||||
|
||||
The right to access the data can easily fulfilled with Notfellchen, a user can always request a copy of their data in their profile.
|
||||
|
||||
Rectification and erasure
|
||||
-------------------------
|
||||
Article 17 provides that the data subject has the right to request erasure of personal data related to them on any one of a number of grounds within 30 days.
|
||||
This is currently not implemented by he software and has to be done by administrators manually.
|
||||
|
||||
|
||||
|
||||
.. warning::
|
||||
|
||||
All content on this website is intended for general information only, and should not be construed as legal advice.
|
||||
Please seek a lawyer.
|
28
docs/admin/example.telegraf.conf
Normal file
28
docs/admin/example.telegraf.conf
Normal file
@@ -0,0 +1,28 @@
|
||||
# Global tags can be specified here in key="value" format.
|
||||
[global_tags]
|
||||
|
||||
|
||||
# Configuration for telegraf agent
|
||||
[agent]
|
||||
interval = "10s"
|
||||
round_interval = true
|
||||
metric_batch_size = 1000
|
||||
metric_buffer_limit = 10000
|
||||
collection_jitter = "0s"
|
||||
flush_interval = "10s"
|
||||
flush_jitter = "0s"
|
||||
|
||||
# Configuration for sending metrics to InfluxDB
|
||||
[[outputs.influxdb]]
|
||||
urls = ["http://:::8086"]
|
||||
database = "telegraf"
|
||||
skip_database_creation = true
|
||||
username = 'telegraf'
|
||||
password = 'yourpassword'
|
||||
|
||||
[[inputs.http]]
|
||||
urls = ["https://notfellchen.org/metrics/"]
|
||||
name_override = "notfellchen"
|
||||
#Data from HTTP in JSON format
|
||||
data_format = "json"
|
||||
|
8
docs/admin/index.rst
Normal file
8
docs/admin/index.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
Administration
|
||||
--------------
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
GDPR.rst
|
||||
monitoring.rst
|
72
docs/admin/monitoring.rst
Normal file
72
docs/admin/monitoring.rst
Normal file
@@ -0,0 +1,72 @@
|
||||
Monitoring
|
||||
==========
|
||||
|
||||
Notfellchen should, like every other software, be easy to monitor. Therefore a basic metrics are exposed to `https://notfellchen.org/metrics`.
|
||||
The data is encoded in JSON format and is therefore suitable to bea read by humans and it is easy to use it as data source for further processing.
|
||||
|
||||
|
||||
Exposed Metrics
|
||||
---------------
|
||||
|
||||
.. code::
|
||||
|
||||
users: number of users (all roles combined)
|
||||
staff: number of users with staff status
|
||||
adoption_notices: number of adoption notices
|
||||
adoption_notices_by_status: number of adoption notices by major status
|
||||
adoption_notices_without_location: number of location notices that are not geocoded
|
||||
|
||||
Example workflow
|
||||
----------------
|
||||
|
||||
To use the exposed metrics you will usually need a time series database and a visualization tool.
|
||||
|
||||
As time series database we will utilize InfluxDB, the visualization tool will be Grafana.
|
||||
|
||||
InfluxDB and Telegraf
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
First we install InfluxDB (e.g. with docker, be aware of the security risks!).
|
||||
|
||||
.. code::
|
||||
|
||||
# Pull the image
|
||||
$ sudo docker pull influxdb
|
||||
|
||||
# Start influxdb
|
||||
$ sudo docker run -d -p 8086:8086 -v influxdb:/var/lib/influxdb --name influxdb influxdb
|
||||
|
||||
# Start influxdb console
|
||||
$ docker exec -it influxdb influx
|
||||
Connected to http://localhost:8086 version 1.8.3
|
||||
InfluxDB shell version: 1.8.3
|
||||
> create database monitoring
|
||||
> create user "telegraf" with password 'mypassword'
|
||||
> grant all on monitoring to telegraf
|
||||
|
||||
.. note::
|
||||
When creating the user telegraf check the double and single quotes for username an password.
|
||||
|
||||
Now install telegraf and configure `etc/telegraf/telegraf.conf`. Modify the domain and your password for the InfluxDB database.
|
||||
|
||||
.. literalinclude:: example.telegraf.conf
|
||||
:linenos:
|
||||
:language: python
|
||||
|
||||
Graphana
|
||||
^^^^^^^^
|
||||
|
||||
Now we can simply use the InfluxDB as data source in Grafana and configure until you have
|
||||
beautiful plots!
|
||||
|
||||
.. image:: monitoring_grafana.png
|
||||
|
||||
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
|
BIN
docs/admin/monitoring_grafana.png
Normal file
BIN
docs/admin/monitoring_grafana.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 116 KiB |
177
docs/conf.py
Normal file
177
docs/conf.py
Normal file
@@ -0,0 +1,177 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file does only contain a selection of the most common options. For a
|
||||
# full list see the documentation:
|
||||
# http://www.sphinx-doc.org/en/master/config
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
# import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'Notfellchen'
|
||||
copyright = 'CC-BY-SA Julian-Samuel Gebühr'
|
||||
author = 'Julian-Samuel Gebühr'
|
||||
|
||||
# The short X.Y version
|
||||
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.
|
||||
#
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.ifconfig',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = "en"
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
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
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
#
|
||||
# The default sidebars (for documents that don't match any pattern) are
|
||||
# defined by theme itself. Builtin themes are using these templates by
|
||||
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
||||
# 'searchbox.html']``.
|
||||
#
|
||||
# html_sidebars = {}
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ---------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'notfellchen'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'notfellchen.tex', 'Notfellchen Dokumentation',
|
||||
'Julian-Samuel Gebühr', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'notfellchen', 'Notfellchen Dokumentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output ----------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'Notfellchen', 'Notfellchen Documentation',
|
||||
author, 'Notfellchen', 'App für die Vermittlung von Tieren aus Tierschutz.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Epub output -------------------------------------------------
|
||||
|
||||
# Bibliographic Dublin Core info.
|
||||
epub_title = project
|
||||
|
||||
# The unique identifier of the text. This can be a ISBN number
|
||||
# or the project homepage.
|
||||
#
|
||||
# epub_identifier = ''
|
||||
|
||||
# A unique identification for the text.
|
||||
#
|
||||
# epub_uid = ''
|
||||
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ['search.html']
|
||||
|
||||
|
||||
# -- Extension configuration -------------------------------------------------
|
210
docs/dev/backup.rst
Normal file
210
docs/dev/backup.rst
Normal file
@@ -0,0 +1,210 @@
|
||||
Backup & Restore
|
||||
****************
|
||||
|
||||
If you do no heavy modification of the code you should be fine with backing up :file:`/etc/notfellchen/` and the database.
|
||||
Assuming you used a PostgreSQL database the following solution might help you with backups and restores.
|
||||
|
||||
Backup
|
||||
++++++
|
||||
|
||||
The following code is a modification of `this script <https://wiki.postgresql.org/wiki/Automated_Backup_on_Linux>`_
|
||||
licensed under the :ref:`postgresql_license`.
|
||||
|
||||
You will first need to create a backup configuration at :file:`/var/notfellchen/pg_backup.config`.
|
||||
|
||||
.. code-block::
|
||||
|
||||
##############################
|
||||
## POSTGRESQL BACKUP CONFIG ##
|
||||
##############################
|
||||
|
||||
# Optional system user to run backups as. If the user the script is running as doesn't match this
|
||||
# the script terminates. Leave blank to skip check.
|
||||
BACKUP_USER=notfellchen
|
||||
|
||||
# Optional hostname to adhere to pg_hba policies. Will default to "localhost" if none specified.
|
||||
HOSTNAME=localhost
|
||||
|
||||
# Optional username to connect to database as. Will default to "postgres" if none specified.
|
||||
USERNAME=notfellchen
|
||||
|
||||
# This dir will be created if it doesn't exist. This must be writable by the user the script is
|
||||
# running as.
|
||||
BACKUP_DIR=/var/notfellchen/backups/postgresql
|
||||
|
||||
# Enter database to backup
|
||||
DATABSE=notfellchen
|
||||
|
||||
|
||||
#### SETTINGS FOR ROTATED BACKUPS ####
|
||||
|
||||
# Which day to take the weekly backup from (1-7 = Monday-Sunday)
|
||||
DAY_OF_WEEK_TO_KEEP=7
|
||||
|
||||
# Number of days to keep daily backups
|
||||
DAYS_TO_KEEP=7
|
||||
|
||||
# How many weeks to keep weekly backups
|
||||
WEEKS_TO_KEEP=5
|
||||
|
||||
######################################
|
||||
|
||||
And then add the script that will do the actual backup at :file:`/var/notfellchen/backup_rotate.sh`
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
###########################
|
||||
####### LOAD CONFIG #######
|
||||
###########################
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case $1 in
|
||||
-c)
|
||||
CONFIG_FILE_PATH="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
${ECHO} "Unknown Option \"$1\"" 1>&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z $CONFIG_FILE_PATH ] ; then
|
||||
SCRIPTPATH=$(cd ${0%/*} && pwd -P)
|
||||
CONFIG_FILE_PATH="${SCRIPTPATH}/pg_backup.config"
|
||||
fi
|
||||
|
||||
if [ ! -r ${CONFIG_FILE_PATH} ] ; then
|
||||
echo "Could not load config file from ${CONFIG_FILE_PATH}" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source "${CONFIG_FILE_PATH}"
|
||||
|
||||
###########################
|
||||
#### PRE-BACKUP CHECKS ####
|
||||
###########################
|
||||
|
||||
# Make sure we're running as the required backup user
|
||||
if [ "$BACKUP_USER" != "" -a "$(id -un)" != "$BACKUP_USER" ] ; then
|
||||
echo "This script must be run as $BACKUP_USER. Exiting." 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
###########################
|
||||
### INITIALISE DEFAULTS ###
|
||||
###########################
|
||||
|
||||
if [ ! $HOSTNAME ]; then
|
||||
HOSTNAME="localhost"
|
||||
fi;
|
||||
|
||||
if [ ! $USERNAME ]; then
|
||||
USERNAME="postgres"
|
||||
fi;
|
||||
|
||||
|
||||
###########################
|
||||
#### START THE BACKUPS ####
|
||||
###########################
|
||||
|
||||
function perform_backups()
|
||||
{
|
||||
SUFFIX=$1
|
||||
FINAL_BACKUP_DIR=$BACKUP_DIR"`date +\%Y-\%m-\%d`$SUFFIX/"
|
||||
|
||||
echo "Making backup directory in $FINAL_BACKUP_DIR"
|
||||
|
||||
if ! mkdir -p $FINAL_BACKUP_DIR; then
|
||||
echo "Cannot create backup directory in $FINAL_BACKUP_DIR. Go and fix it!" 1>&2
|
||||
exit 1;
|
||||
fi;
|
||||
|
||||
#######################
|
||||
### GLOBALS BACKUPS ###
|
||||
#######################
|
||||
|
||||
echo -e "\n\nPerforming backup"
|
||||
echo -e "--------------------------------------------\n"
|
||||
|
||||
echo "Backup"
|
||||
|
||||
set -o pipefail
|
||||
if ! pg_dump $DATABASE | gzip > $FINAL_BACKUP_DIR"$DATABASE".sql.gz.in_progress; then
|
||||
echo "[!!ERROR!!] Failed to produce globals backup" 1>&2
|
||||
else
|
||||
mv $FINAL_BACKUP_DIR"$DATABASE".sql.gz.in_progress $FINAL_BACKUP_DIR"$DATABSE".sql.gz
|
||||
fi
|
||||
set +o pipefail
|
||||
|
||||
echo -e "\nAll database backups complete!"
|
||||
}
|
||||
|
||||
# MONTHLY BACKUPS
|
||||
|
||||
DAY_OF_MONTH=`date +%d`
|
||||
|
||||
if [ $DAY_OF_MONTH -eq 1 ];
|
||||
then
|
||||
# Delete all expired monthly directories
|
||||
find $BACKUP_DIR -maxdepth 1 -name "*-monthly" -exec rm -rf '{}' ';'
|
||||
|
||||
perform_backups "-monthly"
|
||||
|
||||
exit 0;
|
||||
fi
|
||||
|
||||
# WEEKLY BACKUPS
|
||||
|
||||
DAY_OF_WEEK=`date +%u` #1-7 (Monday-Sunday)
|
||||
EXPIRED_DAYS=`expr $((($WEEKS_TO_KEEP * 7) + 1))`
|
||||
|
||||
if [ $DAY_OF_WEEK = $DAY_OF_WEEK_TO_KEEP ];
|
||||
then
|
||||
# Delete all expired weekly directories
|
||||
find $BACKUP_DIR -maxdepth 1 -mtime +$EXPIRED_DAYS -name "*-weekly" -exec rm -rf '{}' ';'
|
||||
|
||||
perform_backups "-weekly"
|
||||
|
||||
exit 0;
|
||||
fi
|
||||
|
||||
# DAILY BACKUPS
|
||||
|
||||
# Delete daily backups 7 days old or more
|
||||
find $BACKUP_DIR -maxdepth 1 -mtime +$DAYS_TO_KEEP -name "*-daily" -exec rm -rf '{}' ';'
|
||||
|
||||
perform_backups "-daily"
|
||||
|
||||
|
||||
You should make the script executable test it and automate the execution with :program:`crontab`
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ chmod +x backup_rotate.sh
|
||||
$ ./backup_rotate.sh
|
||||
$ crontab -e
|
||||
# enter the following to backup every day at 3am
|
||||
0 3 * * * /var/notfellchen/backup_rotate.sh
|
||||
|
||||
|
||||
|
||||
Restore
|
||||
+++++++
|
||||
|
||||
If you for any reason want to restore a backup you can use the following:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ sudo systemctl stop notfellchen
|
||||
$ pg_dump notfellchen > notfellchen_YYYY_MM_DD-hh_mm.psql # Make a backup for later analysis
|
||||
$ dropdb notfellchen
|
||||
$ cd /path/to/backup
|
||||
$ gzip -d notfellchen.sql.gz
|
||||
$ sudo -u postgres createdb -O notfellchen notfellchen
|
||||
$ psql notfellchen < notfellchen.sql
|
||||
$ systemctl restart notfellchen
|
39
docs/dev/contributing.rst
Normal file
39
docs/dev/contributing.rst
Normal file
@@ -0,0 +1,39 @@
|
||||
Contributing
|
||||
------------
|
||||
|
||||
Report a bug
|
||||
^^^^^^^^^^^^
|
||||
|
||||
To report a bug, file an issue on `Github
|
||||
<https://github.com/moan0s/notfellchen/issues>`_
|
||||
|
||||
Try to include the following information:
|
||||
|
||||
- The information needed to reproduce the problem
|
||||
- What you would expect to happen
|
||||
- What did actually happen
|
||||
- Error messages
|
||||
|
||||
You are also invited to include:
|
||||
|
||||
- Screenshots
|
||||
- Which browser you are using
|
||||
- The URL of the site
|
||||
- How urgent it is
|
||||
- Any additional information you consider useful
|
||||
|
||||
Get involved!
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
To contribute simply clone the directory, make your changes and file a
|
||||
pull request.
|
||||
|
||||
If you want to know what can be done, have a look at the current `Github
|
||||
<https://github.com/moan0s/notfellchen/issues>`_.
|
||||
|
||||
Get in touch!
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
If you have questions, want to contribute or want to message me regarding something else
|
||||
you can find contact information at https://hyteck.de/about/ or directly write
|
||||
an `E-Mail <mailto:info@notfellchen.org>`_
|
261
docs/dev/deployment.rst
Normal file
261
docs/dev/deployment.rst
Normal file
@@ -0,0 +1,261 @@
|
||||
.. highlight:: none
|
||||
|
||||
**********
|
||||
Deployment
|
||||
**********
|
||||
|
||||
There are different ways to deploy Notfellchen. We support an ansible+docker based deployment and manual installation.
|
||||
|
||||
Ansible deployment
|
||||
==================
|
||||
|
||||
Notfellchen can be deployed with the `notfellchen-ansible-role <https://github.com/moan0s/ansible-role-notfellchen>`_ that is based on the
|
||||
official Notfellchen docker image. This role will only install notfellchen itself. If you want a complete setup that includes a
|
||||
database and a webserver with minimal configuration you can use the
|
||||
`mash-playbook <https://github.com/mother-of-all-self-hosting/mash-playbook>`_ by following `it's documentation
|
||||
on Notfellchen <https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/services/notfellchen.md>`_.
|
||||
|
||||
|
||||
|
||||
Manual Deployment
|
||||
=================
|
||||
|
||||
|
||||
This guide describes the installation of a installation of Notfellchen from source. It is inspired by this great guide from
|
||||
pretix_.
|
||||
|
||||
.. warning:: Even though this guide tries to make it as straightforward to run Notfellchen, it still requires some Linux experience to
|
||||
get it right. If you're not feeling comfortable managing a Linux server, check out a managed service_.
|
||||
|
||||
This guide is tested on **Ubuntu20.04** but it should work very similar on other modern systemd based distributions.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
Please set up the following systems beforehand, it will not be explained here in detail (but see these links for external
|
||||
installation guides):
|
||||
|
||||
* A SMTP server to send out mails, e.g. `Postfix`_ on your machine or some third-party server you have credentials for
|
||||
* A HTTP reverse proxy, e.g. `nginx`_ or Traefik to allow HTTPS connections
|
||||
* A `PostgreSQL`_ database server
|
||||
|
||||
Also recommended is, that you use a firewall, although this is not a Notfellchen-specific recommendation. If you're new to
|
||||
Linux and firewalls, it is recommended that you start with `ufw`_.
|
||||
|
||||
.. note:: Please, do not run Notfellchen without HTTPS encryption. You'll handle user data and thanks to `Let's Encrypt`_
|
||||
SSL certificates can be obtained for free these days.
|
||||
|
||||
Unix user
|
||||
---------
|
||||
|
||||
As we do not want to run notfellchen as root, we first create a new unprivileged user::
|
||||
|
||||
# adduser notfellchen --disabled-password --home /var/notfellchen
|
||||
|
||||
In this guide, all code lines prepended with a ``#`` symbol are commands that you need to execute on your server as
|
||||
``root`` user (e.g. using ``sudo``); all lines prepended with a ``$`` symbol should be run by the unprivileged user.
|
||||
|
||||
Database
|
||||
--------
|
||||
|
||||
Having the database server installed, we still need a database and a database user. We can create these with any kind
|
||||
of database managing tool or directly on our database's shell. Please make sure that UTF8 is used as encoding for the
|
||||
best compatibility. You can check this with the following command::
|
||||
|
||||
# sudo -u postgres psql -c 'SHOW SERVER_ENCODING'
|
||||
|
||||
For PostgreSQL database creation, we would do::
|
||||
|
||||
# sudo -u postgres createuser notfellchen
|
||||
# sudo -u postgres createdb -O notfellchen notfellchen
|
||||
# su notfellchen
|
||||
$ psql
|
||||
> ALTER USER notfellchen PASSWORD 'strong_password';
|
||||
|
||||
Package dependencies
|
||||
--------------------
|
||||
|
||||
To build and run notfellchen, you will need the following debian packages::
|
||||
|
||||
# apt-get install git build-essential python-dev python3-venv python3 python3-pip \
|
||||
python3-dev
|
||||
|
||||
Config file
|
||||
-----------
|
||||
|
||||
We now create a config directory and config file for notfellchen::
|
||||
|
||||
# mkdir /etc/notfellchen
|
||||
# touch /etc/notfellchen/notfellchen.cfg
|
||||
# chown -R notfellchen:notfellchen /etc/notfellchen/
|
||||
# chmod 0600 /etc/notfellchen/notfellchen.cfg
|
||||
|
||||
Fill the configuration file ``/etc/notfellchen/notfellchen.cfg`` with the following content (adjusted to your environment)::
|
||||
|
||||
[notfellchen]
|
||||
instance_name=My library
|
||||
url=https://notfellchen.example.com
|
||||
|
||||
[database]
|
||||
backend=postgresql
|
||||
name=notfellchen
|
||||
user=notfellchen
|
||||
|
||||
[locations]
|
||||
static=/var/notfellchen/static
|
||||
|
||||
[mail]
|
||||
; See config file documentation for more options
|
||||
; from=notfellchen@example.com
|
||||
; host=127.0.0.1
|
||||
; user=notfellchen
|
||||
; password=foobar
|
||||
; port=587
|
||||
|
||||
[security]
|
||||
; See https://securitytxt.org/ for reference
|
||||
;Contact=
|
||||
;Expires=
|
||||
;Encryption=
|
||||
;Preferred-Languages=
|
||||
;Scope=
|
||||
;Policy=
|
||||
|
||||
Install notfellchen as package
|
||||
------------------------
|
||||
|
||||
Now we will install notfellchen itself. The following steps are to be executed as the ``notfellchen`` user. Before we
|
||||
actually install notfellchen, we will create a virtual environment to isolate the python packages from your global
|
||||
python installation::
|
||||
|
||||
$ python3 -m venv /var/notfellchen/venv
|
||||
$ source /var/notfellchen/venv/bin/activate
|
||||
(venv)$ pip3 install -U pip setuptools wheel
|
||||
|
||||
We now clone and install notfellchen, its direct dependencies and gunicorn::
|
||||
|
||||
(venv)$ git clone https://github.com/moan0s/Notfellchen2
|
||||
(venv)$ cd Notfellchen2/src/
|
||||
(venv)$ pip3 install -r requirements.txt
|
||||
(venv)$ pip3 install -e .
|
||||
|
||||
Note that you need Python 3.6 or newer. You can find out your Python version using ``python -V``.
|
||||
|
||||
Finally, we compile static files and create the database structure::
|
||||
|
||||
(venv)$ ./manage.py collectstatic
|
||||
(venv)$ ./manage.py migrate
|
||||
(venv)$ django-admin compilemessages --ignore venv
|
||||
|
||||
|
||||
Start notfellchen as a service
|
||||
-------------------------
|
||||
|
||||
You should start notfellchen using systemd to automatically start it after a reboot. Create a file
|
||||
named ``/etc/systemd/system/notfellchen-web.service`` with the following content::
|
||||
|
||||
[Unit]
|
||||
Description=notfellchen web service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=notfellchen
|
||||
Group=notfellchen
|
||||
Environment="VIRTUAL_ENV=/var/notfellchen/venv"
|
||||
Environment="PATH=/var/notfellchen/venv/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
ExecStart=/var/notfellchen/venv/bin/gunicorn notfellchen.wsgi \
|
||||
--name notfellchen --workers 5 \
|
||||
--max-requests 1200 --max-requests-jitter 50 \
|
||||
--log-level=info --bind=127.0.0.1:8345
|
||||
WorkingDirectory=/var/notfellchen
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
You can now run the following commands to enable and start the services::
|
||||
|
||||
# systemctl daemon-reload
|
||||
# systemctl enable notfellchen-web
|
||||
# systemctl start notfellchen-web
|
||||
|
||||
|
||||
SSL
|
||||
---
|
||||
|
||||
The following snippet is an example on how to configure a nginx proxy for notfellchen::
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
if ($scheme = http) {
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
#
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/notfellchen.example.com/cert.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/notfellchen.example.com/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
|
||||
# Set header
|
||||
add_header X-Clacks-Overhead "GNU Terry Pratchett";
|
||||
add_header Permissions-Policy interest-cohort=(); #Anti FLoC
|
||||
add_header Referrer-Policy same-origin;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
|
||||
server_name notfellchen.example.com;
|
||||
location / {
|
||||
proxy_pass http://localhost:8345;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
|
||||
location /static/ {
|
||||
alias /var/notfellchen/static/;
|
||||
access_log off;
|
||||
expires 365d;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
We recommend reading about setting `strong encryption settings`_ for your web server.
|
||||
|
||||
Next steps
|
||||
----------
|
||||
|
||||
Yay, you are done! You should now be able to reach notfellchen at https://notfellchen.example.com/
|
||||
|
||||
Updates
|
||||
-------
|
||||
|
||||
.. warning:: While we try hard not to break things, **please perform a backup before every upgrade**.
|
||||
|
||||
To upgrade to a new notfellchen release, pull the latest code changes and run the following commands::
|
||||
|
||||
$ source /var/notfellchen/venv/bin/activate
|
||||
(venv)$ git pull
|
||||
(venv)$ pg_dump notfellchen > notfellchen.psql
|
||||
(venv)$ python manage.py migrate
|
||||
(venv)$ django-admin compilemessages --ignore venv
|
||||
|
||||
# systemctl restart notfellchen-web
|
||||
|
||||
|
||||
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-16-04
|
||||
.. _nginx: https://botleg.com/stories/https-with-lets-encrypt-and-nginx/
|
||||
.. _Let's Encrypt: https://letsencrypt.org/
|
||||
.. _MySQL: https://dev.mysql.com/doc/refman/5.7/en/linux-installation-apt-repo.html
|
||||
.. _PostgreSQL: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-20-04
|
||||
.. _redis: https://blog.programster.org/debian-8-install-redis-server/
|
||||
.. _ufw: https://en.wikipedia.org/wiki/Uncomplicated_Firewall
|
||||
.. _strong encryption settings: https://mozilla.github.io/server-side-tls/ssl-config-generator/
|
||||
.. _service: hyteck.de/services
|
||||
.. _pretix: https://docs.pretix.eu/en/latest/admin/installation/manual_smallscale.html
|
||||
|
13
docs/dev/index.rst
Normal file
13
docs/dev/index.rst
Normal file
@@ -0,0 +1,13 @@
|
||||
********************************************
|
||||
Installation, customization and contributing
|
||||
********************************************
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
deployment.rst
|
||||
contributing.rst
|
||||
translation.rst
|
||||
release.rst
|
||||
backup.rst
|
14
docs/dev/postgresql_license.rst
Normal file
14
docs/dev/postgresql_license.rst
Normal file
@@ -0,0 +1,14 @@
|
||||
.. _postgresql_license:
|
||||
|
||||
PostgreSQL License
|
||||
******************
|
||||
|
||||
.. code-block::
|
||||
|
||||
PostgreSQL Database Management System (formerly known as Postgres, then as Postgres95)
|
||||
Portions Copyright (c) 1996-2008, The PostgreSQL Global Development Group
|
||||
Portions Copyright (c) 1994, The Regents of the University of California
|
||||
|
||||
Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies.
|
||||
IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
40
docs/dev/release.rst
Normal file
40
docs/dev/release.rst
Normal file
@@ -0,0 +1,40 @@
|
||||
Release
|
||||
-------------
|
||||
|
||||
What qualifies as release?
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
A new release should be announced when a significant number functions, bugfixes or other improvements to the software
|
||||
is made. Notfellchen follows `Semantic Versioning <https://semver.org/>`_.
|
||||
|
||||
What should be done before a release?
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Tested basic functions
|
||||
######################
|
||||
|
||||
Run :command:`nf test src`
|
||||
|
||||
Test upgrade on a copy of a production database
|
||||
###############################################
|
||||
|
||||
.. WARNING::
|
||||
You have to prevent e-mails from being sent, otherwise users could receive duplicate e-mails!
|
||||
|
||||
* Ensure correct migration if necessary
|
||||
* Views correct?
|
||||
|
||||
Release
|
||||
^^^^^^^
|
||||
|
||||
After testing everything you are good to go. Open the file :file:`src/setup.py` with a text editor
|
||||
you can adjust the version number:
|
||||
|
||||
Do a final commit on this change, and tag the commit as release with appropriate version number.
|
||||
|
||||
.. code::
|
||||
|
||||
git tag -a v1.0.0 -m "Releasing version v1.0.0"
|
||||
git push origin v1.0.0
|
||||
|
||||
Make sure the tag is visible on GitHub/Codeberg and celebrate 🥳
|
18
docs/dev/translation.rst
Normal file
18
docs/dev/translation.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
Translation
|
||||
===========
|
||||
Translate HTML-files
|
||||
____________________
|
||||
First you have to add the text "{% load i18n %}" in every html file at the top.
|
||||
|
||||
Write the string in your html file between these two tags: {% translate "String" %}
|
||||
|
||||
Translate python-files
|
||||
______________________
|
||||
The underscore markes the string for translation. e.g. _("String")
|
||||
|
||||
|
||||
Workflow
|
||||
_________
|
||||
- Generate the messages with the command: "django-admin makemessages -l de --ignore venv" de stands in this example for german
|
||||
- Translate the strings in the file src/local/de/LC_MESSAGES/django.po
|
||||
- Convert the strings for django with the command: "django-admin compilemessages --ignore venv"
|
18
docs/index.rst
Normal file
18
docs/index.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
###################################
|
||||
Notfellchen Plattform Dokumentation
|
||||
###################################
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
user/index.rst
|
||||
admin/index.rst
|
||||
dev/index.rst
|
||||
API/index.rst
|
||||
|
||||
.. image:: rtfm.png
|
||||
:name: Ratte lesend
|
||||
:alt: Zeichnung einer lesenden Ratte
|
||||
:align: center
|
||||
|
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
@@ -0,0 +1,35 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
|
||||
:end
|
||||
popd
|
3
docs/requirements.txt
Normal file
3
docs/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
sphinx
|
||||
sphinx-rtd-theme
|
||||
sphinx-autobuild
|
BIN
docs/rtfm.png
Normal file
BIN
docs/rtfm.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 485 KiB |
BIN
docs/user/abonnieren.png
Normal file
BIN
docs/user/abonnieren.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
19
docs/user/benachrichtigungen.rst
Normal file
19
docs/user/benachrichtigungen.rst
Normal file
@@ -0,0 +1,19 @@
|
||||
Benachrichtigungen
|
||||
==================
|
||||
|
||||
Ersteller*innen von Vermittlungen werden über neue Kommentare per Mail benachrichtigt, ebenso alle die die Vermittlung abonniert haben.
|
||||
Jede Vermittlung kann abonniert werden. Dafür klickst du auf die Glocke neben dem Titel der Vermittlung.
|
||||
|
||||
.. image:: abonnieren.png
|
||||
|
||||
Auf der Website
|
||||
+++++++++++++++
|
||||
|
||||
|
||||
|
||||
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.
|
11
docs/user/index.rst
Normal file
11
docs/user/index.rst
Normal file
@@ -0,0 +1,11 @@
|
||||
******************
|
||||
User Dokumentation
|
||||
******************
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Inhalt:
|
||||
|
||||
registrierung.rst
|
||||
vermittlungen.rst
|
||||
moderationskonzept.rst
|
||||
benachrichtigungen.rst
|
29
docs/user/moderationskonzept.rst
Normal file
29
docs/user/moderationskonzept.rst
Normal file
@@ -0,0 +1,29 @@
|
||||
Moderationskonzept
|
||||
==================
|
||||
|
||||
Vertrauen in notfellchen.org ist uns wichtig. Unser Kernziel ist es Tierschutz und Tierwohl zu fördern. Dafür sind drei
|
||||
Grundkonzepte wichtig
|
||||
|
||||
* Aktualität: Informationen auf notfellchen.org müssen aktuell&richtig sein
|
||||
* Tierschutz: Ausschließlich Ratten aus dem Tierschutz werden vermittelt
|
||||
* Moderation: Vermittlungen und Kommentare können gemeldet werden und werden vom Team zügig moderiert.
|
||||
|
||||
Vermittlungen
|
||||
+++++++++++++
|
||||
|
||||
Vermittlungen können von allen Nutzer*innen mit Account erstellt werden. Vermittlungen normaler Nutzer*innen kommen dann in eine Warteschlange und werden vom Admin & Modertionsteam geprüft und sichtbar geschaltet.
|
||||
Tierheime und Pflegestellen können auf Anfrage einen Koordinations-Status bekommen, wodurch sie Vermittlungsanzeigen erstellen können die direkt öffentlich sichtbar sind.
|
||||
|
||||
Jede Vermittlung hat ein "Zuletzt-geprüft" Datum, das anzeigt, wann ein Mensch zuletzt überprüft hat, ob die Anzeige noch aktuell ist.
|
||||
Nach 3 Wochen ohne Prüfung werden Anzeigen automatisch von der Seite entfernt und nur dann wieder freigeschaltet, wenn eine manuelle Prüfung erfolgt.
|
||||
|
||||
Darüber hinaus werden einmal täglich die verlinkten Seiten automatisiert geprüft. Wenn eine Vermittlung auf der Website eines Tierheims oder einer Pflegestelle entfernt wird, wird die Anzeige sofort deaktiviert.
|
||||
|
||||
Vermittlungen können von allen Menschen, auch ohne Account gemeldet werden. Grund für eine Meldung kann sein, dass Informationen veraltet sind oder ein Verdacht von Tierwohlgefärdung. Gemeldete Vermittlungen werden vom Moderationsteam geprüft und ggf. entfernt.
|
||||
|
||||
Kommentare
|
||||
++++++++++
|
||||
|
||||
Die Kommentarfunktion von Vermittlungen ermöglicht es angemeldeten Nutzer*innen zusätzliche Informationen hinzuzufügen oder Fragen zu stellen.
|
||||
|
||||
Kommentare können, wie Vermittlungen, gemeldet werden wenn sie nicht den Regeln entsprechen.
|
10
docs/user/registrierung.rst
Normal file
10
docs/user/registrierung.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
Registrierung
|
||||
================================
|
||||
|
||||
Du kannst dich jederzeit selbst registrieren. Das geht unter https://notfellchen.org/accounts/register/
|
||||
|
||||
Ein Account ermöglicht es dir
|
||||
|
||||
* Kommentare zu hinterlassen
|
||||
* Vermittlungen hinzuzufügen
|
||||
* Vermittlungen zu abonnieren
|
17
docs/user/vermittlungen.rst
Normal file
17
docs/user/vermittlungen.rst
Normal file
@@ -0,0 +1,17 @@
|
||||
Vermittlungen
|
||||
=============
|
||||
|
||||
Vermittlungen können von allen Nutzer*innen mit Account erstellt werden. Vermittlungen normaler Nutzer*innen kommen dann in eine Warteschlange und werden vom Admin & Modertionsteam geprüft und sichtbar geschaltet.
|
||||
Tierheime und Pflegestellen können auf Anfrage einen Koordinations-Status bekommen, wodurch sie Vermittlungsanzeigen erstellen können die direkt öffentlich sichtbar sind.
|
||||
|
||||
Jede Vermittlung hat ein "Zuletzt-geprüft" Datum, das anzeigt, wann ein Mensch zuletzt überprüft hat, ob die Anzeige noch aktuell ist.
|
||||
Nach 3 Wochen ohne Prüfung werden Anzeigen automatisch von der Seite entfernt und nur dann wieder freigeschaltet, wenn eine manuelle Prüfung erfolgt.
|
||||
|
||||
Darüber hinaus werden einmal täglich die verlinkten Seiten automatisiert geprüft. Wenn eine Vermittlungs-Seite bei einem Tierheim oder einer Pflegestelle entfernt wurde, wird die Anzeige ebenfalls deaktiviert.
|
||||
|
||||
Vermittlungen können von allen Menschen, auch ohne Account gemeldet werden. Grund dafür kann sein, dass Informationen veraltet sind oder ein Verdacht von Tierwohlgefärdung. Gemeldete Vermittlungen werden vom Moderationsteam geprüft und ggf. entfernt.
|
||||
|
||||
Die Kommentarfunktion von Vermittlungen ermöglicht es angemeldeten Nutzer*innen zusätzliche Informationen hinzuzufügen oder Fragen zu stellen.
|
||||
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.
|
@@ -18,10 +18,13 @@ media=./media
|
||||
static=./static
|
||||
|
||||
[mail]
|
||||
console-only=true
|
||||
console_only=true
|
||||
|
||||
[logging]
|
||||
app_log_level=INFO
|
||||
django_log_level=INFO
|
||||
|
||||
[geocoding]
|
||||
api_url=https://photon.hyteck.de/api
|
||||
api_format=photon
|
||||
|
||||
|
497
package-lock.json
generated
Normal file
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
10
package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"bulma": "^1.0.4",
|
||||
"sass": "^1.89.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build-bulma": "sass --load-path=node_modules src/fellchensammlung/static/fellchensammlung/css/main.scss src/fellchensammlung/static/fellchensammlung/css/main.css --style compressed",
|
||||
"start": "npm run build-bulma -- --watch"
|
||||
}
|
||||
}
|
@@ -6,15 +6,15 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "notfellchen"
|
||||
description = "A tool to help."
|
||||
description = "A website to help animals to find a loving home. It features organized input of adoption notices and related animals including automated lifecycle, location-based search, roles, and support for easy checking of rescue organizations."
|
||||
authors = [
|
||||
{name = "moanos", email = "julian-samuel@gebuehr.net"},
|
||||
{ name = "moanos", email = "julian-samuel@gebuehr.net" },
|
||||
]
|
||||
maintainers = [
|
||||
{name = "moanos", email = "julian-samuel@gebuehr.net"},
|
||||
{ name = "moanos", email = "julian-samuel@gebuehr.net" },
|
||||
]
|
||||
keywords = ["animal", "adoption", "django", "rescue", ]
|
||||
license = {text = "AGPL-3.0-or-later"}
|
||||
keywords = ["animal", "adoption", "django", "rescue", "rats" ]
|
||||
license = { text = "AGPL-3.0-or-later" }
|
||||
classifiers = [
|
||||
"Environment :: Web",
|
||||
"License :: OSI Approved :: GNU Affero General Public License v3",
|
||||
@@ -24,26 +24,40 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"Django",
|
||||
"coverage",
|
||||
"codecov",
|
||||
"sphinx",
|
||||
"sphinx-rtd-theme",
|
||||
"gunicorn",
|
||||
"fontawesomefree",
|
||||
"whitenoise",
|
||||
"model_bakery",
|
||||
"markdown",
|
||||
"Pillow",
|
||||
"django-registration",
|
||||
"psycopg2-binary",
|
||||
"django-crispy-forms",
|
||||
"crispy-bootstrap4",
|
||||
"djangorestframework",
|
||||
"celery[redis]",
|
||||
"drf-spectacular[sidecar]",
|
||||
"django-widget-tweaks",
|
||||
"django-super-deduper"
|
||||
]
|
||||
|
||||
dynamic = ["version", "readme"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
develop = [
|
||||
"pytest",
|
||||
"coverage",
|
||||
"model_bakery",
|
||||
]
|
||||
docs = [
|
||||
"sphinx",
|
||||
"sphinx-rtd-theme",
|
||||
"sphinx-autobuild"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://hyteck.de"
|
||||
repository = "https://github.com/moan0s/notfellchen/"
|
||||
homepage = "https://notfellchen.org"
|
||||
repository = "https://codeberg.org/moanos/notfellchen/"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
@@ -53,6 +67,6 @@ nf = 'notfellchen.main:main'
|
||||
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "notfellchen.__version__"}
|
||||
readme = {file = "README.md"}
|
||||
version = { attr = "notfellchen.__version__" }
|
||||
readme = { file = "README.md" }
|
||||
|
||||
|
268
scripts/upload_animal_shelters.py
Normal file
268
scripts/upload_animal_shelters.py
Normal file
@@ -0,0 +1,268 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from types import SimpleNamespace
|
||||
|
||||
import requests
|
||||
# TODO: consider using OSMPythonTools instead of requests or overpass library
|
||||
from osmtogeojson import osmtogeojson
|
||||
from tqdm import tqdm
|
||||
|
||||
DEFAULT_OSM_DATA_FILE = "export.geojson"
|
||||
# Search area must be the official name, e.g. "Germany" is not a valid area name in Overpass API
|
||||
# Consider instead finding & using the code within the query itself, e.g. "ISO3166-1"="DE"
|
||||
DEFAULT_OVERPASS_SEARCH_AREA = "Deutschland"
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse command-line arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Download animal shelter data from the Overpass API to the Notfellchen API.")
|
||||
parser.add_argument("--api-token", type=str, help="API token for authentication.")
|
||||
parser.add_argument("--area", type=str, help="Area to search for animal shelters (default: Deutschland).")
|
||||
parser.add_argument("--instance", type=str, help="API instance URL.")
|
||||
parser.add_argument("--data-file", type=str, help="Path to the GeoJSON file containing (only) animal shelters.")
|
||||
parser.add_argument("--use-cached", action='store_true', help="Use the stored GeoJSON file")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def get_config():
|
||||
"""Get configuration from environment variables or command-line arguments."""
|
||||
args = parse_args()
|
||||
|
||||
api_token = args.api_token or os.getenv("NOTFELLCHEN_API_TOKEN")
|
||||
# TODO: document new environment variable NOTFELLCHEN_AREA
|
||||
area = args.area or os.getenv("NOTFELLCHEN_AREA", DEFAULT_OVERPASS_SEARCH_AREA)
|
||||
instance = args.instance or os.getenv("NOTFELLCHEN_INSTANCE")
|
||||
data_file = args.data_file or os.getenv("NOTFELLCHEN_DATA_FILE", DEFAULT_OSM_DATA_FILE)
|
||||
use_cached = args.use_cached or os.getenv("NOTFELLCHEN_USE_CACHED", False)
|
||||
|
||||
if not api_token or not instance:
|
||||
raise ValueError("API token and instance URL must be provided via environment variables or CLI arguments.")
|
||||
|
||||
return api_token, area, instance, data_file, use_cached
|
||||
|
||||
|
||||
def get_or_none(data, key):
|
||||
if key in data["properties"].keys():
|
||||
return data["properties"][key]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_or_empty(data, key):
|
||||
if key in data["properties"].keys():
|
||||
return data["properties"][key]
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def choose(keys, data, replace=False):
|
||||
for key in keys:
|
||||
if key in data.keys():
|
||||
if replace:
|
||||
return data[key].replace(" ", "").replace("-", "").replace("(", "").replace(")", "")
|
||||
else:
|
||||
return data[key]
|
||||
return None
|
||||
|
||||
|
||||
def add(value, platform):
|
||||
if value != "":
|
||||
if value.find(platform) == -1:
|
||||
return f"https://www.{platform}.com/{value}"
|
||||
else:
|
||||
return value
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def https(value):
|
||||
if value is not None and value != "":
|
||||
value = value.replace("http://", "")
|
||||
if value.find("https") == -1:
|
||||
return f"https://{value}"
|
||||
else:
|
||||
return value
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def calc_coordinate_center(coordinates):
|
||||
"""
|
||||
Calculates the center as the arithmetic mean of the list of coordinates.
|
||||
|
||||
Not perfect because earth is a sphere (citation needed) but good enough.
|
||||
"""
|
||||
if not coordinates:
|
||||
return None, None
|
||||
|
||||
lon_sum = 0.0
|
||||
lat_sum = 0.0
|
||||
count = 0
|
||||
|
||||
for lon, lat in coordinates:
|
||||
lon_sum += lon
|
||||
lat_sum += lat
|
||||
count += 1
|
||||
|
||||
return lon_sum / count, lat_sum / count
|
||||
|
||||
|
||||
def get_center_coordinates(geometry):
|
||||
"""
|
||||
Given a GeoJSON geometry dict, return (longitude, latitude)
|
||||
|
||||
If a shape, calculate the center, else reurn the point
|
||||
"""
|
||||
geom_type = geometry["type"]
|
||||
coordinates = geometry["coordinates"]
|
||||
|
||||
if geom_type == "Point":
|
||||
return coordinates[0], coordinates[1]
|
||||
|
||||
elif geom_type == "LineString":
|
||||
return calc_coordinate_center(coordinates)
|
||||
|
||||
elif geom_type == "Polygon":
|
||||
outer_ring = coordinates[0]
|
||||
return calc_coordinate_center(outer_ring)
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unsupported geometry type: {geom_type}")
|
||||
|
||||
|
||||
# TODO: take note of new get_overpass_result function which does the bulk of the new overpass query work
|
||||
def get_overpass_result(area, data_file):
|
||||
"""Build the Overpass query for fetching animal shelters in the specified area."""
|
||||
overpass_endpoint = "https://overpass-api.de/api/interpreter"
|
||||
overpass_query = f"""
|
||||
[out:json][timeout:25];
|
||||
area[name="{area}"]->.searchArea;
|
||||
nwr["amenity"="animal_shelter"](area.searchArea);
|
||||
out body;
|
||||
>;
|
||||
out skel qt;
|
||||
"""
|
||||
r = requests.get(overpass_endpoint, params={'data': overpass_query})
|
||||
if r.status_code == 200:
|
||||
rjson = r.json()
|
||||
result = osmtogeojson.process_osm_json(rjson)
|
||||
with open(data_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(result, f, ensure_ascii=False)
|
||||
return result
|
||||
|
||||
|
||||
def add_if_available(base_data, keys, result):
|
||||
# Loads the data into the org if available
|
||||
for key in keys:
|
||||
if getattr(base_data, key) is not None:
|
||||
result[key] = getattr(base_data, key)
|
||||
return result
|
||||
|
||||
|
||||
def create_location(tierheim, instance, headers):
|
||||
location_data = {
|
||||
"place_id": tierheim["id"],
|
||||
"longitude": get_center_coordinates(tierheim["geometry"])[0],
|
||||
"latitude": get_center_coordinates(tierheim["geometry"])[1],
|
||||
"name": tierheim["properties"]["name"],
|
||||
"city": tierheim["properties"]["addr:city"],
|
||||
"housenumber": get_or_empty(tierheim, "addr:housenumber"),
|
||||
"postcode": get_or_empty(tierheim, "addr:postcode"),
|
||||
"street": get_or_empty(tierheim, "addr:street"),
|
||||
"countrycode": get_or_empty(tierheim, "addr:country"),
|
||||
}
|
||||
|
||||
location_result = requests.post(f"{instance}/api/locations/", json=location_data, headers=headers)
|
||||
|
||||
if location_result.status_code != 201:
|
||||
try:
|
||||
print(
|
||||
f"Location for {tierheim["properties"]["name"]}:{location_result.status_code} {location_result.json()} not created")
|
||||
except requests.exceptions.JSONDecodeError:
|
||||
print(f"Location for {tierheim["properties"]["name"]} could not be created")
|
||||
exit()
|
||||
|
||||
return location_result.json()
|
||||
|
||||
|
||||
def main():
|
||||
api_token, area, instance, data_file, use_cached = get_config()
|
||||
if not use_cached:
|
||||
# Query shelters
|
||||
overpass_result = get_overpass_result(area, data_file)
|
||||
if overpass_result is None:
|
||||
print("Error: get_overpass_result returned None")
|
||||
return
|
||||
else:
|
||||
with open(data_file, 'r', encoding='utf-8') as f:
|
||||
overpass_result = json.load(f)
|
||||
|
||||
# Set headers and endpoint
|
||||
endpoint = f"{instance}/api/organizations/"
|
||||
h = {'Authorization': f'Token {api_token}', "content-type": "application/json"}
|
||||
|
||||
tierheime = overpass_result["features"]
|
||||
|
||||
for idx, tierheim in enumerate(tqdm(tierheime)):
|
||||
# Check if data is low quality
|
||||
if "name" not in tierheim["properties"].keys() or "addr:city" not in tierheim["properties"].keys():
|
||||
continue
|
||||
|
||||
# Load TH data in for easier accessing
|
||||
th_data = SimpleNamespace(
|
||||
name=tierheim["properties"]["name"],
|
||||
email=choose(("contact:email", "email"), tierheim["properties"]),
|
||||
phone_number=choose(("contact:phone", "phone"), tierheim["properties"], replace=True),
|
||||
fediverse_profile=get_or_none(tierheim, "contact:mastodon"),
|
||||
facebook=https(add(get_or_empty(tierheim, "contact:facebook"), "facebook")),
|
||||
instagram=https(add(get_or_empty(tierheim, "contact:instagram"), "instagram")),
|
||||
website=https(choose(("contact:website", "website"), tierheim["properties"])),
|
||||
description=get_or_none(tierheim, "opening_hours"),
|
||||
external_object_identifier=tierheim["id"],
|
||||
EXTERNAL_SOURCE_IDENTIFIER="OSM",
|
||||
)
|
||||
|
||||
# Define here for later
|
||||
optional_data = ["email", "phone_number", "website", "description", "fediverse_profile", "facebook",
|
||||
"instagram"]
|
||||
|
||||
# Check if rescue organization exits
|
||||
search_data = {"external_source_identifier": "OSM",
|
||||
"external_object_identifier": f"{tierheim["id"]}"}
|
||||
search_result = requests.get(f"{instance}/api/organizations", params=search_data, headers=h)
|
||||
if search_result.status_code == 200:
|
||||
org_id = search_result.json()[0]["id"]
|
||||
logging.debug(f"{th_data.name} already exists as ID {org_id}.")
|
||||
org_patch_data = {"id": org_id,
|
||||
"name": th_data.name}
|
||||
if search_result.json()[0]["location"] is None:
|
||||
location = create_location(tierheim, instance, h)
|
||||
org_patch_data["location"] = location["id"]
|
||||
|
||||
org_patch_data = add_if_available(th_data, optional_data, org_patch_data)
|
||||
|
||||
result = requests.patch(endpoint, json=org_patch_data, headers=h)
|
||||
if result.status_code != 200:
|
||||
logging.warning(f"Updating {tierheim['properties']['name']} failed:{result.status_code} {result.json()}")
|
||||
continue
|
||||
else:
|
||||
location = create_location(tierheim, instance, h)
|
||||
org_data = {"name": tierheim["properties"]["name"],
|
||||
"external_object_identifier": f"{tierheim["id"]}",
|
||||
"external_source_identifier": "OSM",
|
||||
"location": location["id"]
|
||||
}
|
||||
|
||||
org_data = add_if_available(th_data, optional_data, org_data)
|
||||
|
||||
result = requests.post(endpoint, json=org_data, headers=h)
|
||||
|
||||
if result.status_code != 201:
|
||||
print(f"{idx} {tierheim["properties"]["name"]}:{result.status_code} {result.json()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@@ -1,11 +1,18 @@
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
import csv
|
||||
|
||||
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice
|
||||
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, ImportantLocation, SocialMediaPost
|
||||
|
||||
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
|
||||
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions
|
||||
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, Notification
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class StatusInline(admin.StackedInline):
|
||||
@@ -14,13 +21,52 @@ class StatusInline(admin.StackedInline):
|
||||
|
||||
@admin.register(AdoptionNotice)
|
||||
class AdoptionNoticeAdmin(admin.ModelAdmin):
|
||||
search_fields = ("name__icontains", "description__icontains")
|
||||
list_filter = ("owner",)
|
||||
inlines = [
|
||||
StatusInline,
|
||||
]
|
||||
actions = ("activate",)
|
||||
|
||||
def activate(self, request, queryset):
|
||||
for obj in queryset:
|
||||
obj.set_active()
|
||||
|
||||
activate.short_description = _("Ausgewählte Vermittlungen aktivieren")
|
||||
|
||||
|
||||
# Re-register UserAdmin
|
||||
admin.site.register(User)
|
||||
@admin.register(User)
|
||||
class UserAdmin(admin.ModelAdmin):
|
||||
search_fields = ("usernamname__icontains", "first_name__icontains", "last_name__icontains", "email__icontains")
|
||||
list_display = ("username", "email", "trust_level", "is_active", "view_adoption_notices")
|
||||
list_filter = ("is_active", "trust_level",)
|
||||
actions = ("export_as_csv",)
|
||||
|
||||
def view_adoption_notices(self, obj):
|
||||
count = obj.adoption_notices.count()
|
||||
url = (
|
||||
reverse("admin:fellchensammlung_adoptionnotice_changelist")
|
||||
+ "?"
|
||||
+ urlencode({"owner__id": f"{obj.id}"})
|
||||
)
|
||||
return format_html('<a href="{}">{} Adoption Notices</a>', url, count)
|
||||
|
||||
def export_as_csv(self, request, queryset):
|
||||
meta = self.model._meta
|
||||
field_names = [field.name for field in meta.fields]
|
||||
|
||||
response = HttpResponse(content_type='text/csv')
|
||||
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
|
||||
writer = csv.writer(response)
|
||||
|
||||
writer.writerow(field_names)
|
||||
for obj in queryset:
|
||||
row = writer.writerow([getattr(obj, field) for field in field_names])
|
||||
|
||||
return response
|
||||
|
||||
export_as_csv.short_description = _("Ausgewählte User exportieren")
|
||||
|
||||
|
||||
def _reported_content_link(obj):
|
||||
@@ -50,15 +96,84 @@ 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", "location__city")
|
||||
list_display = ("name", "trusted", "allows_using_materials", "website")
|
||||
list_filter = ("allows_using_materials", "trusted", ("external_source_identifier", EmptyFieldListFilter))
|
||||
|
||||
inlines = [
|
||||
SpeciesSpecificURLInline,
|
||||
]
|
||||
|
||||
|
||||
@admin.register(Text)
|
||||
class TextAdmin(admin.ModelAdmin):
|
||||
search_fields = ("title__icontains", "text_code__icontains",)
|
||||
|
||||
|
||||
@admin.register(Comment)
|
||||
class CommentAdmin(admin.ModelAdmin):
|
||||
list_filter = ("user",)
|
||||
|
||||
|
||||
@admin.register(Notification)
|
||||
class BaseNotificationAdmin(admin.ModelAdmin):
|
||||
list_filter = ("user_to_notify", "read")
|
||||
|
||||
|
||||
@admin.register(SearchSubscription)
|
||||
class SearchSubscriptionAdmin(admin.ModelAdmin):
|
||||
list_filter = ("owner",)
|
||||
|
||||
|
||||
class ImportantLocationInline(admin.StackedInline):
|
||||
model = ImportantLocation
|
||||
|
||||
|
||||
class IsImportantListFilter(admin.SimpleListFilter):
|
||||
# See https://docs.djangoproject.com/en/5.1/ref/contrib/admin/filters/#modeladmin-list-filters
|
||||
title = _('Is Important Location?')
|
||||
|
||||
parameter_name = 'important'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
('is_important', _('Important Location')),
|
||||
('is_normal', _('Normal Location')),
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() == 'is_important':
|
||||
return queryset.filter(importantlocation__isnull=False)
|
||||
else:
|
||||
return queryset.filter(importantlocation__isnull=True)
|
||||
|
||||
|
||||
@admin.register(Location)
|
||||
class LocationAdmin(admin.ModelAdmin):
|
||||
search_fields = ("name__icontains", "city__icontains")
|
||||
list_filter = [IsImportantListFilter]
|
||||
inlines = [
|
||||
ImportantLocationInline,
|
||||
]
|
||||
|
||||
@admin.register(SocialMediaPost)
|
||||
class SocialMediaPostAdmin(admin.ModelAdmin):
|
||||
list_filter = ("platform",)
|
||||
|
||||
admin.site.register(Animal)
|
||||
admin.site.register(Species)
|
||||
admin.site.register(RescueOrganization)
|
||||
admin.site.register(Location)
|
||||
admin.site.register(Rule)
|
||||
admin.site.register(Image)
|
||||
admin.site.register(ModerationAction)
|
||||
admin.site.register(Language)
|
||||
admin.site.register(Text)
|
||||
admin.site.register(Announcement)
|
||||
admin.site.register(AdoptionNoticeStatus)
|
||||
admin.site.register(Subscriptions)
|
||||
admin.site.register(Log)
|
||||
admin.site.register(Timestamp)
|
||||
|
0
src/fellchensammlung/api/__init__.py
Normal file
0
src/fellchensammlung/api/__init__.py
Normal file
33
src/fellchensammlung/api/renderers.py
Normal file
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)
|
161
src/fellchensammlung/api/serializers.py
Normal file
161
src/fellchensammlung/api/serializers.py
Normal file
@@ -0,0 +1,161 @@
|
||||
from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image, Location
|
||||
from rest_framework import serializers
|
||||
import math
|
||||
|
||||
|
||||
class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
|
||||
location = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
location_details = serializers.StringRelatedField(source='location', read_only=True)
|
||||
organization = serializers.PrimaryKeyRelatedField(
|
||||
queryset=RescueOrganization.objects.all(),
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
organization = serializers.PrimaryKeyRelatedField(
|
||||
queryset=RescueOrganization.objects.all(),
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
photos = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Image.objects.all(),
|
||||
many=True,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = AdoptionNotice
|
||||
fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information",
|
||||
"group_only", "location", "location_details", "organization", "photos"]
|
||||
|
||||
|
||||
class AdoptionNoticeGeoJSONSerializer(serializers.ModelSerializer):
|
||||
species = serializers.SerializerMethodField()
|
||||
title = serializers.CharField(source='name')
|
||||
url = serializers.SerializerMethodField()
|
||||
location_hr = serializers.SerializerMethodField()
|
||||
coordinates = serializers.SerializerMethodField()
|
||||
image_url = serializers.SerializerMethodField()
|
||||
image_alt = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = AdoptionNotice
|
||||
fields = ('id', 'species', 'title', 'description', 'url', 'location_hr', 'coordinates', 'image_url',
|
||||
'image_alt')
|
||||
|
||||
def get_species(self, obj):
|
||||
return "rat"
|
||||
|
||||
def get_url(self, obj):
|
||||
return obj.get_absolute_url()
|
||||
|
||||
def get_image_url(self, obj):
|
||||
photo = obj.get_photo()
|
||||
if photo is not None:
|
||||
return obj.get_photo().image.url
|
||||
return None
|
||||
|
||||
def get_image_alt(self, obj):
|
||||
photo = obj.get_photo()
|
||||
if photo is not None:
|
||||
return obj.get_photo().alt_text
|
||||
return None
|
||||
|
||||
def get_coordinates(self, obj):
|
||||
"""
|
||||
Coordinates are randomly moved around real location, roughly in a circle. The object id is used as angle so that
|
||||
points are always displayed at the same location (as if they were a seed for a random function).
|
||||
|
||||
It's not exactly a circle, because the earth is round.
|
||||
"""
|
||||
if obj.location:
|
||||
longitude_addition = math.sin(obj.id) / 2000
|
||||
latitude_addition = math.cos(obj.id) / 2000
|
||||
return [obj.location.longitude + longitude_addition, obj.location.latitude + latitude_addition]
|
||||
return None
|
||||
|
||||
def get_location_hr(self, obj):
|
||||
if obj.location:
|
||||
return f"{obj.location}"
|
||||
return None
|
||||
|
||||
|
||||
class RescueOrgeGeoJSONSerializer(serializers.ModelSerializer):
|
||||
name = serializers.CharField()
|
||||
url = serializers.SerializerMethodField()
|
||||
location_hr = serializers.SerializerMethodField()
|
||||
coordinates = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = AdoptionNotice
|
||||
fields = ('id', 'name', 'description', 'url', 'location_hr', 'coordinates')
|
||||
|
||||
def get_url(self, obj):
|
||||
return obj.get_absolute_url()
|
||||
|
||||
def get_coordinates(self, obj):
|
||||
"""
|
||||
Coordinates are randomly moved around real location, roughly in a circle. The object id is used as angle so that
|
||||
points are always displayed at the same location (as if they were a seed for a random function).
|
||||
|
||||
It's not exactly a circle, because the earth is round.
|
||||
"""
|
||||
if obj.location:
|
||||
return [obj.location.longitude, obj.location.latitude]
|
||||
return None
|
||||
|
||||
def get_location_hr(self, obj):
|
||||
if obj.location.city:
|
||||
return f"{obj.location.city}"
|
||||
elif obj.location:
|
||||
return f"{obj.location}"
|
||||
return None
|
||||
|
||||
|
||||
class AnimalCreateSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Animal
|
||||
fields = ["name", "date_of_birth", "description", "species", "sex", "adoption_notice"]
|
||||
|
||||
|
||||
class AnimalGetSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Animal
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class RescueOrganizationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = RescueOrganization
|
||||
exclude = ["internal_comment", "allows_using_materials"]
|
||||
|
||||
|
||||
class ImageCreateSerializer(serializers.ModelSerializer):
|
||||
@staticmethod
|
||||
def _animal_or_an(value):
|
||||
if not value in ["animal", "adoption_notice"]:
|
||||
raise serializers.ValidationError(
|
||||
'Set either animal or adoption_notice, depending on what type of object the image should be attached to.')
|
||||
|
||||
attach_to_type = serializers.CharField(validators=[_animal_or_an])
|
||||
attach_to = serializers.IntegerField()
|
||||
|
||||
class Meta:
|
||||
model = Image
|
||||
exclude = ["owner"]
|
||||
|
||||
|
||||
class SpeciesSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Species
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class LocationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = "__all__"
|
20
src/fellchensammlung/api/urls.py
Normal file
20
src/fellchensammlung/api/urls.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from django.urls import path
|
||||
from .views import (
|
||||
AdoptionNoticeApiView,
|
||||
AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView, LocationApiView,
|
||||
AdoptionNoticeGeoJSONView, RescueOrgGeoJSONView
|
||||
)
|
||||
|
||||
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("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"),
|
||||
]
|
385
src/fellchensammlung/api/views.py
Normal file
385
src/fellchensammlung/api/views.py
Normal file
@@ -0,0 +1,385 @@
|
||||
from django.db.models import Q
|
||||
from rest_framework.generics import ListAPIView
|
||||
|
||||
from fellchensammlung.api.serializers import LocationSerializer, AdoptionNoticeGeoJSONSerializer
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from django.db import transaction
|
||||
from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel, Location, AdoptionNoticeStatus
|
||||
from fellchensammlung.tasks import post_adoption_notice_save, post_rescue_org_save
|
||||
from rest_framework import status, serializers
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from .renderers import GeoJSONRenderer
|
||||
from .serializers import (
|
||||
AnimalGetSerializer,
|
||||
AnimalCreateSerializer,
|
||||
RescueOrgeGeoJSONSerializer,
|
||||
AdoptionNoticeSerializer,
|
||||
ImageCreateSerializer,
|
||||
SpeciesSerializer, RescueOrganizationSerializer,
|
||||
)
|
||||
from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image
|
||||
from drf_spectacular.utils import extend_schema, inline_serializer
|
||||
|
||||
|
||||
class AdoptionNoticeApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
{
|
||||
'name': 'id',
|
||||
'required': False,
|
||||
'description': 'ID of the adoption notice to retrieve.',
|
||||
'type': int
|
||||
},
|
||||
],
|
||||
responses={200: AdoptionNoticeSerializer(many=True)}
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Retrieve adoption notices with their related animals and images.
|
||||
"""
|
||||
adoption_notice_id = kwargs.get("id")
|
||||
if adoption_notice_id:
|
||||
try:
|
||||
adoption_notice = AdoptionNotice.objects.get(pk=adoption_notice_id)
|
||||
serializer = AdoptionNoticeSerializer(adoption_notice, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except AdoptionNotice.DoesNotExist:
|
||||
return Response({"error": "Adoption notice not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
adoption_notices = AdoptionNotice.objects.all()
|
||||
serializer = AdoptionNoticeSerializer(adoption_notices, many=True, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@transaction.atomic
|
||||
@extend_schema(
|
||||
request=AdoptionNoticeSerializer,
|
||||
responses={201: 'Adoption notice created successfully!'}
|
||||
)
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
API view to add an adoption notice.
|
||||
"""
|
||||
serializer = AdoptionNoticeSerializer(data=request.data, context={'request': request})
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
adoption_notice = serializer.save(owner=request.user_to_notify)
|
||||
|
||||
# Add the location
|
||||
post_adoption_notice_save.delay_on_commit(adoption_notice.pk)
|
||||
|
||||
# Only set active when user has trust level moderator or higher
|
||||
if request.user_to_notify.trust_level >= TrustLevel.MODERATOR:
|
||||
adoption_notice.set_active()
|
||||
else:
|
||||
adoption_notice.set_unchecked()
|
||||
|
||||
# Log the action
|
||||
Log.objects.create(
|
||||
user=request.user_to_notify,
|
||||
action="add_adoption_notice",
|
||||
text=f"{request.user_to_notify} added adoption notice {adoption_notice.pk} via API",
|
||||
)
|
||||
|
||||
# Return success response with new adoption notice details
|
||||
return Response(
|
||||
{"message": "Adoption notice created successfully!", "id": adoption_notice.pk},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
|
||||
class AnimalApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(
|
||||
responses=AnimalGetSerializer
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Get list of animals or a specific animal by ID.
|
||||
"""
|
||||
animal_id = kwargs.get("id")
|
||||
if animal_id:
|
||||
try:
|
||||
animal = Animal.objects.get(pk=animal_id)
|
||||
serializer = AnimalGetSerializer(animal, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Animal.DoesNotExist:
|
||||
return Response({"error": "Animal not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
animals = Animal.objects.all()
|
||||
serializer = AnimalGetSerializer(animals, many=True, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@transaction.atomic
|
||||
@extend_schema(
|
||||
request=AnimalCreateSerializer,
|
||||
responses={201: inline_serializer(
|
||||
name='Animal',
|
||||
fields={
|
||||
'id': serializers.IntegerField(),
|
||||
"message": serializers.Field()}),
|
||||
400: "json"}
|
||||
)
|
||||
@transaction.atomic
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Create a new animal.
|
||||
"""
|
||||
serializer = AnimalCreateSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
animal = serializer.save(owner=request.user_to_notify)
|
||||
return Response(
|
||||
{"message": "Animal created successfully!", "id": animal.id},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class RescueOrganizationApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
{
|
||||
'name': 'id',
|
||||
'required': False,
|
||||
'description': 'ID of the rescue organization to retrieve.',
|
||||
'type': int
|
||||
},
|
||||
{
|
||||
'name': 'trusted',
|
||||
'required': False,
|
||||
'description': 'Filter by trusted status (true/false).',
|
||||
'type': bool
|
||||
},
|
||||
{
|
||||
'name': 'external_object_identifier',
|
||||
'required': False,
|
||||
'description': 'Filter by external object identifier. Use "None" to filter for an empty field',
|
||||
'type': str
|
||||
},
|
||||
{
|
||||
'name': 'external_source_identifier',
|
||||
'required': False,
|
||||
'description': 'Filter by external source identifier. Use "None" to filter for an empty field',
|
||||
'type': str
|
||||
},
|
||||
{
|
||||
'name': 'search',
|
||||
'required': False,
|
||||
'description': 'Search by organization name or location name/city.',
|
||||
'type': str
|
||||
},
|
||||
],
|
||||
responses={200: RescueOrganizationSerializer(many=True)}
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Get list of rescue organizations or a specific organization by ID or get a list with available filters for
|
||||
- external_object_identifier
|
||||
- external_source_identifier
|
||||
"""
|
||||
org_id = request.query_params.get("id")
|
||||
external_object_identifier = request.query_params.get("external_object_identifier")
|
||||
external_source_identifier = request.query_params.get("external_source_identifier")
|
||||
search_query = request.query_params.get("search")
|
||||
|
||||
if org_id:
|
||||
try:
|
||||
organization = RescueOrganization.objects.get(pk=org_id)
|
||||
serializer = RescueOrganizationSerializer(organization, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except RescueOrganization.DoesNotExist:
|
||||
return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
organizations = RescueOrganization.objects.all()
|
||||
|
||||
if external_object_identifier:
|
||||
if external_object_identifier == "None":
|
||||
external_object_identifier = None
|
||||
organizations = organizations.filter(external_object_identifier=external_object_identifier)
|
||||
|
||||
if external_source_identifier:
|
||||
if external_source_identifier == "None":
|
||||
external_source_identifier = None
|
||||
organizations = organizations.filter(external_source_identifier=external_source_identifier)
|
||||
if search_query:
|
||||
organizations = organizations.filter(
|
||||
Q(name__icontains=search_query) |
|
||||
Q(location_string__icontains=search_query) |
|
||||
Q(location__name__icontains=search_query) |
|
||||
Q(location__city__icontains=search_query)
|
||||
)
|
||||
if organizations.count() == 0:
|
||||
return Response({"error": "No organizations found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
serializer = RescueOrganizationSerializer(organizations, many=True, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@transaction.atomic
|
||||
@extend_schema(
|
||||
request=RescueOrganizationSerializer,
|
||||
responses={201: 'Rescue organization created successfully!'}
|
||||
)
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Create or update a rescue organization.
|
||||
"""
|
||||
serializer = RescueOrganizationSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
rescue_org = serializer.save()
|
||||
if rescue_org.location is None:
|
||||
# Add the location
|
||||
post_rescue_org_save.delay_on_commit(rescue_org.pk)
|
||||
return Response(
|
||||
{"message": "Rescue organization created successfully!", "id": rescue_org.id},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@transaction.atomic
|
||||
@extend_schema(
|
||||
request=RescueOrganizationSerializer,
|
||||
responses={200: 'Rescue organization updated successfully!'}
|
||||
)
|
||||
def patch(self, request, *args, **kwargs):
|
||||
"""
|
||||
Partially update a rescue organization.
|
||||
"""
|
||||
org_id = request.data.get("id")
|
||||
if not org_id:
|
||||
return Response({"error": "ID is required for updating."}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
organization = RescueOrganization.objects.get(pk=org_id)
|
||||
except RescueOrganization.DoesNotExist:
|
||||
return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
serializer = RescueOrganizationSerializer(organization, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response({"message": "Rescue organization updated successfully!"}, status=status.HTTP_200_OK)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class AddImageApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@transaction.atomic
|
||||
@extend_schema(
|
||||
request=ImageCreateSerializer,
|
||||
responses={201: 'Image added successfully!'}
|
||||
)
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Add an image to an animal or adoption notice.
|
||||
"""
|
||||
serializer = ImageCreateSerializer(data=request.data, context={"request": request})
|
||||
if serializer.is_valid():
|
||||
if serializer.validated_data["attach_to_type"] == "animal":
|
||||
object_to_attach_to = Animal.objects.get(id=serializer.validated_data["attach_to"])
|
||||
elif serializer.validated_data["attach_to_type"] == "adoption_notice":
|
||||
object_to_attach_to = AdoptionNotice.objects.get(id=serializer.validated_data["attach_to"])
|
||||
else:
|
||||
raise ValueError("Unknown attach_to_type given, should not happen. Check serializer")
|
||||
serializer.validated_data.pop('attach_to_type', None)
|
||||
serializer.validated_data.pop('attach_to', None)
|
||||
image = serializer.save(owner=request.user_to_notify)
|
||||
object_to_attach_to.photos.add(image)
|
||||
return Response(
|
||||
{"message": "Image added successfully!", "id": image.id},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class SpeciesApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(
|
||||
responses={200: SpeciesSerializer(many=True)}
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Retrieve a list of species.
|
||||
"""
|
||||
species = Species.objects.all()
|
||||
serializer = SpeciesSerializer(species, many=True, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class LocationApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
{
|
||||
'name': 'id',
|
||||
'required': False,
|
||||
'description': 'ID of the location to retrieve.',
|
||||
'type': int
|
||||
},
|
||||
],
|
||||
responses={200: LocationSerializer(many=True)}
|
||||
)
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
Retrieve a location
|
||||
"""
|
||||
location_id = kwargs.get("id")
|
||||
if location_id:
|
||||
try:
|
||||
location = Location.objects.get(pk=location_id)
|
||||
serializer = LocationSerializer(location, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except Location.DoesNotExist:
|
||||
return Response({"error": "Location not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
locations = Location.objects.all()
|
||||
serializer = LocationSerializer(locations, many=True, context={"request": request})
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@transaction.atomic
|
||||
@extend_schema(
|
||||
request=LocationSerializer,
|
||||
responses={201: 'Location created successfully!'}
|
||||
)
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
API view to add a location
|
||||
"""
|
||||
serializer = LocationSerializer(data=request.data, context={'request': request})
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
location = serializer.save()
|
||||
|
||||
# Log the action
|
||||
Log.objects.create(
|
||||
user=request.user,
|
||||
action="add_location",
|
||||
text=f"{request.user} added adoption notice {location.pk} via API",
|
||||
)
|
||||
|
||||
# Return success response with new adoption notice details
|
||||
return Response(
|
||||
{"message": "Location created successfully!", "id": location.pk},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
|
||||
class AdoptionNoticeGeoJSONView(ListAPIView):
|
||||
queryset = AdoptionNotice.objects.select_related('location').filter(location__isnull=False).filter(
|
||||
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE)
|
||||
serializer_class = AdoptionNoticeGeoJSONSerializer
|
||||
renderer_classes = [GeoJSONRenderer]
|
||||
|
||||
|
||||
class RescueOrgGeoJSONView(ListAPIView):
|
||||
queryset = RescueOrganization.objects.select_related('location').filter(location__isnull=False)
|
||||
serializer_class = RescueOrgeGeoJSONSerializer
|
||||
renderer_classes = [GeoJSONRenderer]
|
@@ -15,3 +15,4 @@ class FellchensammlungConfig(AppConfig):
|
||||
except Permission.DoesNotExist:
|
||||
pass
|
||||
post_migrate.connect(ensure_languages, sender=self)
|
||||
import fellchensammlung.receivers
|
||||
|
@@ -1,14 +1,21 @@
|
||||
import datetime
|
||||
|
||||
from django import forms
|
||||
|
||||
from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportComment, ModerationAction, User, Species, \
|
||||
Comment
|
||||
Comment, SexChoicesWithAll, DistanceChoices, 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
|
||||
from crispy_forms.layout import Submit, Layout, Fieldset, HTML, Row, Column, Field, Hidden
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from notfellchen.settings import MEDIA_URL
|
||||
from crispy_forms.layout import Div
|
||||
|
||||
|
||||
def animal_validator(value: str):
|
||||
value = value.lower()
|
||||
animal_list = ["ratte", "farbratte", "katze", "hund", "kaninchen", "hase", "kuh", "fuchs", "cow", "rat", "cat",
|
||||
"dog", "rabbit", "fox", "fancy rat"]
|
||||
if value not in animal_list:
|
||||
raise forms.ValidationError(_("Dieses Tier kenne ich nicht. Probier ein anderes"))
|
||||
|
||||
|
||||
class DateInput(forms.DateInput):
|
||||
@@ -16,92 +23,38 @@ class DateInput(forms.DateInput):
|
||||
|
||||
|
||||
class AdoptionNoticeForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'in_adoption_notice_creation_flow' in kwargs:
|
||||
in_flow = kwargs.pop('in_adoption_notice_creation_flow')
|
||||
else:
|
||||
in_flow = False
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
|
||||
self.helper.form_id = 'form-adoption-notice'
|
||||
self.helper.form_class = 'card'
|
||||
self.helper.form_method = 'post'
|
||||
|
||||
if in_flow:
|
||||
submit = Submit('save-and-add-another-animal', _('Speichern'))
|
||||
|
||||
else:
|
||||
submit = Submit('submit', _('Sepichern'))
|
||||
|
||||
self.helper.layout = Layout(
|
||||
Fieldset(
|
||||
_('Vermittlungsdetails'),
|
||||
'name',
|
||||
'species',
|
||||
'num_animals',
|
||||
'date_of_birth',
|
||||
'sex',
|
||||
'group_only',
|
||||
'searching_since',
|
||||
'location_string',
|
||||
'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"]
|
||||
|
||||
|
||||
class AdoptionNoticeFormWithDateWidget(AdoptionNoticeForm):
|
||||
class Meta:
|
||||
model = AdoptionNotice
|
||||
fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string"]
|
||||
fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string",
|
||||
"organization"]
|
||||
widgets = {
|
||||
'searching_since': DateInput(),
|
||||
'searching_since': DateInput(format=('%Y-%m-%d')),
|
||||
}
|
||||
|
||||
|
||||
|
||||
class AnimalForm(forms.ModelForm):
|
||||
class AdoptionNoticeFormAutoAnimal(AdoptionNoticeForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'in_adoption_notice_creation_flow' in kwargs:
|
||||
adding = kwargs.pop('in_adoption_notice_creation_flow')
|
||||
else:
|
||||
adding = False
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_class = 'form-animal card'
|
||||
if adding:
|
||||
self.helper.add_input(Submit('save-and-add-another-animal', _('Speichern und weiteres Tier hinzufügen')))
|
||||
self.helper.add_input(Submit('save-and-finish', _('Speichern und beenden')))
|
||||
else:
|
||||
self.helper.add_input(Submit('submit', _('Speichern'), css_class="btn"))
|
||||
|
||||
class Meta:
|
||||
model = Animal
|
||||
fields = ["name", "date_of_birth", "species", "sex", "description"]
|
||||
|
||||
|
||||
class AnimalFormWithDateWidget(AnimalForm):
|
||||
class Meta:
|
||||
model = Animal
|
||||
fields = ["name", "date_of_birth", "species", "sex", "description"]
|
||||
widgets = {
|
||||
'date_of_birth': DateInput(),
|
||||
}
|
||||
|
||||
class AdoptionNoticeFormWithDateWidgetAutoAnimal(AdoptionNoticeFormWithDateWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AdoptionNoticeFormWithDateWidgetAutoAnimal, self).__init__(*args, **kwargs)
|
||||
super(AdoptionNoticeFormAutoAnimal, self).__init__(*args, **kwargs)
|
||||
self.fields["num_animals"] = forms.fields.IntegerField(min_value=1, max_value=30, label=_("Zahl der Tiere"))
|
||||
animal_form = AnimalForm()
|
||||
self.fields["species"] = animal_form.fields["species"]
|
||||
self.fields["sex"] = animal_form.fields["sex"]
|
||||
self.fields["date_of_birth"] = animal_form.fields["date_of_birth"]
|
||||
self.fields["date_of_birth"].widget = DateInput()
|
||||
self.fields["date_of_birth"].widget = DateInput(format=('%Y-%m-%d'))
|
||||
|
||||
|
||||
class AnimalForm(forms.ModelForm):
|
||||
template_name = "fellchensammlung/forms/form_snippets.html"
|
||||
|
||||
class Meta:
|
||||
model = Animal
|
||||
fields = ["name", "date_of_birth", "species", "sex", "description"]
|
||||
|
||||
widgets = {
|
||||
'date_of_birth': DateInput(format=('%Y-%m-%d'))
|
||||
}
|
||||
|
||||
|
||||
class ImageForm(forms.ModelForm):
|
||||
@@ -115,11 +68,21 @@ class ImageForm(forms.ModelForm):
|
||||
self.helper.form_id = 'form-animal-photo'
|
||||
self.helper.form_class = 'card'
|
||||
self.helper.form_method = 'post'
|
||||
|
||||
if in_flow:
|
||||
self.helper.add_input(Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')))
|
||||
self.helper.add_input(Submit('submit', _('Speichern')))
|
||||
submits = Div(Submit('submit', _('Speichern')),
|
||||
Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')),
|
||||
css_class="container-edit-buttons")
|
||||
else:
|
||||
self.helper.add_input(Submit('submit', _('Submit')))
|
||||
submits = Fieldset(Submit('submit', _('Speichern')), css_class="container-edit-buttons")
|
||||
self.helper.layout = Layout(
|
||||
Div(
|
||||
'image',
|
||||
'alt_text',
|
||||
css_class="spaced",
|
||||
),
|
||||
submits
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Image
|
||||
@@ -127,29 +90,39 @@ class ImageForm(forms.ModelForm):
|
||||
|
||||
|
||||
class ReportAdoptionNoticeForm(forms.ModelForm):
|
||||
template_name = "fellchensammlung/forms/form_snippets.html"
|
||||
|
||||
class Meta:
|
||||
model = ReportAdoptionNotice
|
||||
fields = ('reported_broken_rules', 'user_comment')
|
||||
|
||||
|
||||
class ReportCommentForm(forms.ModelForm):
|
||||
template_name = "fellchensammlung/forms/form_snippets.html"
|
||||
|
||||
class Meta:
|
||||
model = ReportComment
|
||||
fields = ('reported_broken_rules', 'user_comment')
|
||||
|
||||
|
||||
class CommentForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_class = 'form-comments'
|
||||
self.helper.add_input(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
|
||||
@@ -160,19 +133,17 @@ class CustomRegistrationForm(RegistrationForm):
|
||||
class Meta(RegistrationForm.Meta):
|
||||
model = User
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_id = 'form-registration'
|
||||
self.helper.form_class = 'card'
|
||||
template_name = "fellchensammlung/forms/form_snippets.html"
|
||||
|
||||
self.helper.add_input(Submit('submit', _('Registrieren'), css_class="btn"))
|
||||
|
||||
|
||||
def _get_distances():
|
||||
return {i: i for i in [10, 20, 50, 100, 200, 500]}
|
||||
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):
|
||||
postcode = forms.CharField(max_length=20, label=_("Postleitzahl"))
|
||||
max_distance = forms.ChoiceField(choices=_get_distances, label=_("Max. Distanz"))
|
||||
template_name = "fellchensammlung/forms/form_snippets.html"
|
||||
|
||||
sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False,
|
||||
initial=SexChoicesWithAll.ALL)
|
||||
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED,
|
||||
label=_("Suchradius"))
|
||||
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
|
||||
|
@@ -1,33 +1,43 @@
|
||||
import django.conf.global_settings
|
||||
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.utils.translation import gettext
|
||||
from django.conf import settings
|
||||
from django.core import mail
|
||||
from django.db.models import Q, Min
|
||||
from fellchensammlung.models import User
|
||||
from notfellchen.settings import host
|
||||
from fellchensammlung.models import User, Notification, TrustLevel, NotificationTypeChoices
|
||||
from fellchensammlung.tools.model_helpers import ndm
|
||||
|
||||
NEWLINE = "\r\n"
|
||||
|
||||
def mail_admins_new_report(report):
|
||||
subject = _("Neue Meldung")
|
||||
for moderator in User.objects.filter(trust_level__gt=User.TRUST_LEVEL[User.MODERATOR]):
|
||||
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}")
|
||||
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):
|
||||
if notification_type == NotificationTypeChoices.NEW_REPORT_AN:
|
||||
title = _("Vermittlung gemeldet")
|
||||
elif notification_type == NotificationTypeChoices.NEW_COMMENT:
|
||||
title = _("Kommentar gemeldet")
|
||||
else:
|
||||
reported_rules_text = f"Es wurden keine Regeln angegeben gegen die Verstoßen wurde.{NEWLINE}"
|
||||
if report.user_comment:
|
||||
comment_text = f'Kommentar zum Report: "{report.user_comment}"{NEWLINE}'
|
||||
else:
|
||||
comment_text = f"Es wurde kein Kommentar hinzugefügt.{NEWLINE}"
|
||||
raise NotImplementedError
|
||||
notification = Notification.objects.create(
|
||||
notification_type=notification_type,
|
||||
user_to_notify=moderator,
|
||||
report=report,
|
||||
title=title,
|
||||
)
|
||||
notification.save()
|
||||
|
||||
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])
|
||||
print("Sending email to ", moderator.email)
|
||||
message.send()
|
||||
|
||||
def send_notification_email(notification_pk):
|
||||
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)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
from django.core.management import BaseCommand
|
||||
from fellchensammlung.models import AdoptionNotice, Location
|
||||
from fellchensammlung.tools.geo import clean_locations
|
||||
from fellchensammlung.tools.admin import clean_locations
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -15,4 +15,4 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
clean_locations(quiet=False)
|
||||
clean_locations(quiet=False)
|
||||
|
10
src/fellchensammlung/management/commands/dedup_locations.py
Normal file
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
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()
|
||||
|
@@ -7,7 +7,7 @@ from fellchensammlung import baker_recipes
|
||||
from model_bakery import baker
|
||||
|
||||
from fellchensammlung.models import AdoptionNotice, Species, Animal, Image, ModerationAction, User, Rule, \
|
||||
Report, Comment, ReportAdoptionNotice
|
||||
Report, Comment, ReportAdoptionNotice, TrustLevel
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -101,10 +101,10 @@ class Command(BaseCommand):
|
||||
|
||||
User.objects.create_user('test', password='foobar')
|
||||
admin1 = User.objects.create_superuser(username="admin", password="admin", email="admin1@example.org",
|
||||
trust_level=User.TRUST_LEVEL[User.ADMIN])
|
||||
trust_level=TrustLevel.ADMIN)
|
||||
|
||||
mod1 = User.objects.create_user(username="mod1", password="mod", email="mod1@example.org",
|
||||
trust_level=User.TRUST_LEVEL[User.MODERATOR])
|
||||
trust_level=TrustLevel.MODERATOR)
|
||||
|
||||
comment1 = baker.make(Comment, user=admin1, text="This is a comment", adoption_notice=adoption1)
|
||||
comment2 = baker.make(Comment,
|
||||
|
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.1 on 2024-09-30 12:24
|
||||
|
||||
import datetime
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0005_rescueorganization_allows_using_materials_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='adoptionnotice',
|
||||
name='last_checked',
|
||||
field=models.DateField(default=datetime.datetime.now, verbose_name='Zuletzt überprüft am'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.1 on 2024-09-30 13:10
|
||||
|
||||
import datetime
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0006_adoptionnotice_last_checked'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adoptionnotice',
|
||||
name='last_checked',
|
||||
field=models.DateTimeField(default=datetime.datetime.now, verbose_name='Zuletzt überprüft am'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.1 on 2024-10-10 14:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0007_alter_adoptionnotice_last_checked'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adoptionnoticestatus',
|
||||
name='minor_status',
|
||||
field=models.CharField(choices=[('searching', 'searching'), ('interested', 'interested'), ('waiting_for_review', 'waiting_for_review'), ('needs_additional_info', 'needs_additional_info'), ('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'), ('against_the_rules', 'against_the_rules'), ('missing_information', 'missing_information'), ('technical_error', 'technical_error'), ('unchecked', 'unchecked')], max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='announcement',
|
||||
name='publish_start_time',
|
||||
field=models.DateTimeField(verbose_name='Veröffentlichungszeitpunkt'),
|
||||
),
|
||||
]
|
25
src/fellchensammlung/migrations/0009_log.py
Normal file
25
src/fellchensammlung/migrations/0009_log.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.1.1 on 2024-10-10 21:00
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0008_alter_adoptionnoticestatus_minor_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Log',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action', models.CharField(max_length=255, verbose_name='Aktion')),
|
||||
('text', models.CharField(max_length=1000, verbose_name='Log text')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in')),
|
||||
],
|
||||
),
|
||||
]
|
21
src/fellchensammlung/migrations/0010_timestamp.py
Normal file
21
src/fellchensammlung/migrations/0010_timestamp.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.1.1 on 2024-10-19 18:36
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0009_log'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Timestamp',
|
||||
fields=[
|
||||
('key', models.CharField(max_length=255, primary_key=True, serialize=False, verbose_name='Schlüssel')),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='Zeitstempel')),
|
||||
('data', models.CharField(blank=True, max_length=2000, null=True)),
|
||||
],
|
||||
),
|
||||
]
|
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.1.1 on 2024-10-29 10:44
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0010_timestamp'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adoptionnotice',
|
||||
name='created_at',
|
||||
field=models.DateField(default=django.utils.timezone.now, verbose_name='Erstellt am'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adoptionnotice',
|
||||
name='last_checked',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Zuletzt überprüft am'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,136 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-03 20:07
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0011_alter_adoptionnotice_created_at_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='adoptionnotice',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='adoptionnoticestatus',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='adoptionnoticestatus',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='animal',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='animal',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='announcement',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='basenotification',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='comment',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='image',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='image',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='location',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='location',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='log',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moderationaction',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='report',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rule',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rule',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subscriptions',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
20
src/fellchensammlung/migrations/0013_alter_log_user.py
Normal file
20
src/fellchensammlung/migrations/0013_alter_log_user.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-06 07:02
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0012_adoptionnotice_updated_at_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='log',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-07 20:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0013_alter_log_user'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-Mail'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-09 09:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0014_rescueorganization_email'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='comment',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Kommentar'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-13 15:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0015_rescueorganization_comment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='phone_number',
|
||||
field=models.CharField(blank=True, max_length=15, null=True, verbose_name='Telefonnummer'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-14 06:42
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0016_rescueorganization_phone_number'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='organization_affiliation',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.rescueorganization', verbose_name='Organisation'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-14 17:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0017_user_organization_affiliation'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Beschreibung'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-14 18:30
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0018_rescueorganization_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='rescueorganization',
|
||||
old_name='comment',
|
||||
new_name='internal_comment',
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-14 18:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0019_rename_comment_rescueorganization_internal_comment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='rescueorganization',
|
||||
name='internal_comment',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Interner Kommentar'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-14 20:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0020_alter_rescueorganization_internal_comment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='reason_for_signup',
|
||||
field=models.TextField(default='-', verbose_name='Grund für die Registrierung'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-20 18:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0021_user_reason_for_signup'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='reason_for_signup',
|
||||
field=models.TextField(help_text="Wir würden gerne wissen warum du dich registriertst, ob du dich z.B. Tiere eines bestimmten Tierheim einstellen willst 'nur mal gucken' willst. Beides ist toll! Wenn du für ein Tierheim/eine Pflegestelle arbeitest kontaktieren wir dich ggf. um dir erweiterte Rechte zu geben.", verbose_name='Grund für die Registrierung'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='trust_level',
|
||||
field=models.IntegerField(choices=[(1, 'Member'), (2, 'Coordinator'), (3, 'Moderator'), (4, 'Admin')], default=1),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-20 19:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0022_alter_user_reason_for_signup_alter_user_trust_level'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='email_notifications',
|
||||
field=models.BooleanField(default=True, verbose_name='Benachrichtigung per E-Mail'),
|
||||
),
|
||||
]
|
18
src/fellchensammlung/migrations/0024_alter_animal_sex.py
Normal file
18
src/fellchensammlung/migrations/0024_alter_animal_sex.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-21 19:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0023_user_email_notifications'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='animal',
|
||||
name='sex',
|
||||
field=models.CharField(choices=[('M_N', 'neutered male'), ('M', 'male'), ('F_N', 'neutered female'), ('F', 'female'), ('I', 'intersex')], max_length=20),
|
||||
),
|
||||
]
|
18
src/fellchensammlung/migrations/0025_alter_animal_sex.py
Normal file
18
src/fellchensammlung/migrations/0025_alter_animal_sex.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-21 19:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0024_alter_animal_sex'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='animal',
|
||||
name='sex',
|
||||
field=models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich Kastriert'), ('I', 'Intersex')], max_length=20),
|
||||
),
|
||||
]
|
18
src/fellchensammlung/migrations/0026_alter_animal_sex.py
Normal file
18
src/fellchensammlung/migrations/0026_alter_animal_sex.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-21 21:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0025_alter_animal_sex'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='animal',
|
||||
name='sex',
|
||||
field=models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich, kastriert'), ('I', 'Intergeschlechtlich')], max_length=20),
|
||||
),
|
||||
]
|
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.1.1 on 2024-12-14 07:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0026_alter_animal_sex'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='animal',
|
||||
name='species',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.species', verbose_name='Tierart'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AndoptionNoticeNotification',
|
||||
fields=[
|
||||
('basenotification_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='fellchensammlung.basenotification')),
|
||||
('adoption_notice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')),
|
||||
],
|
||||
bases=('fellchensammlung.basenotification',),
|
||||
),
|
||||
]
|
25
src/fellchensammlung/migrations/0028_searchsubscription.py
Normal file
25
src/fellchensammlung/migrations/0028_searchsubscription.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.1.1 on 2024-12-26 15:56
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0027_alter_animal_species_andoptionnoticenotification'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SearchSubscription',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('sex', models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich, kastriert'), ('I', 'Intergeschlechtlich')], max_length=20)),
|
||||
('radius', models.IntegerField(choices=[(20, '20 km'), (50, '50 km'), (100, '100 km'), (200, '200 km'), (500, '500 km')])),
|
||||
('location', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.location')),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-31 10:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0028_searchsubscription'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='AndoptionNoticeNotification',
|
||||
new_name='AdoptionNoticeNotification',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='searchsubscription',
|
||||
name='sex',
|
||||
field=models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich Kastriert'), ('I', 'Intergeschlechtlich'), ('A', 'Alle')], max_length=20),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-31 12:23
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0029_rename_andoptionnoticenotification_adoptionnoticenotification_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='searchsubscription',
|
||||
old_name='radius',
|
||||
new_name='max_distance',
|
||||
),
|
||||
]
|
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-31 12:42
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0030_rename_radius_searchsubscription_max_distance'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='searchsubscription',
|
||||
name='location',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.location'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='searchsubscription',
|
||||
name='max_distance',
|
||||
field=models.IntegerField(choices=[(20, '20 km'), (50, '50 km'), (100, '100 km'), (200, '200 km'), (500, '500 km')], null=True),
|
||||
),
|
||||
]
|
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-01 22:04
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0031_alter_searchsubscription_location_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='searchsubscription',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='searchsubscription',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-05 18:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0032_searchsubscription_created_at_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='external_object_identifier',
|
||||
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='External Object Identifier'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='external_source_identifier',
|
||||
field=models.CharField(blank=True, choices=[('OSM', 'Open Street Map')], max_length=200, null=True, verbose_name='External Source Identifier'),
|
||||
),
|
||||
]
|
23
src/fellchensammlung/migrations/0034_speciesspecificurl.py
Normal file
23
src/fellchensammlung/migrations/0034_speciesspecificurl.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-05 19:36
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0033_rescueorganization_external_object_identifier_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SpeciesSpecificURL',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('url', models.URLField(verbose_name='Tierartspezifische URL')),
|
||||
('rescues_organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.rescueorganization', verbose_name='Tierschutzorganisation')),
|
||||
('species', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.species', verbose_name='Tierart')),
|
||||
],
|
||||
),
|
||||
]
|
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-11 12:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0034_speciesspecificurl'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='image',
|
||||
name='alt_text',
|
||||
field=models.TextField(max_length=2000, verbose_name='Alternativtext'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='report',
|
||||
name='reported_broken_rules',
|
||||
field=models.ManyToManyField(to='fellchensammlung.rule', verbose_name='Regeln gegen die verstoßen wurde'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='report',
|
||||
name='user_comment',
|
||||
field=models.TextField(blank=True, verbose_name='Kommentar/Zusätzliche Information'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-14 06:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0035_alter_image_alt_text_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='basenotification',
|
||||
name='read_at',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Gelesen am'),
|
||||
),
|
||||
]
|
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.4 on 2025-01-14 06:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0036_basenotification_read_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='basenotification',
|
||||
name='title',
|
||||
field=models.CharField(max_length=100, verbose_name='Titel'),
|
||||
),
|
||||
]
|
@@ -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
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
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'),
|
||||
),
|
||||
]
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user