Compare commits
	
		
			730 Commits
		
	
	
		
			c1332ee1f0
			...
			develop
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 151ce0d88e | |||
| e07e633651 | |||
| dd3b1fde9d | |||
| 2ffc9b4ba1 | |||
| 22eebd4586 | |||
| e589a048d3 | |||
| 392eb5a7a8 | |||
| 44fa4d4880 | |||
| 9b97cc4cb1 | |||
| 656a24ef02 | |||
| 74643db087 | |||
| 3a6fd3cee1 | |||
| 29e9d1bd8c | |||
| 3c5ca9ae00 | |||
| 3d1ad6112d | |||
| b843e67e9b | |||
| 4cab71e8fb | |||
| 969339a95f | |||
| e06efa1539 | |||
| 2fb6d2782f | |||
| f69eccd0e4 | |||
| e20e6d4b1d | |||
| 0352a60e28 | |||
| abeb14601a | |||
| f52225495d | |||
| 797b2c15f7 | |||
| e81618500b | |||
| f7a5da306c | |||
| 92a9b5c6c9 | |||
| 964aeb97a7 | |||
| 474e9eb0f8 | |||
| 7acc2c6eec | |||
| 5a02837d7f | |||
| 7ff0a9b489 | |||
| 9af4b58a4f | |||
| 7a20890f17 | |||
| f6c9e532f8 | |||
| f1698c4fd3 | |||
| cb82aeffde | |||
| e9c1ef2604 | |||
| d8f0f2b3be | |||
| 65f065f5ce | |||
| 5cba64e500 | |||
| 064784a222 | |||
| b890ef3563 | |||
| 962f2ae86c | |||
| c71a1940dd | |||
| 8b2913a8be | |||
| 111ffc2b2e | |||
| 600aa918ef | |||
| 68e13ed176 | |||
| 0fa4330f2c | |||
| c9289b1e8c | |||
| bb3136bfc7 | |||
| b708e9ecaf | |||
| f3619b2881 | |||
| 7572c92da5 | |||
| 5a2b11b44e | |||
| df15ea100b | |||
| 3da6e90f73 | |||
| f784ab0c78 | |||
| ebaa477cff | |||
| b4be21bf45 | |||
| 0df36df9d8 | |||
| a5754b2633 | |||
| 7c6e01a436 | |||
| ad90429ec7 | |||
| 0e36237890 | |||
| 3261f5a90a | |||
| 1551c1bdf2 | |||
| 996bd7af67 | |||
| 21bd34c94d | |||
| fb581c940b | |||
| b428f46213 | |||
| 38fe55dd86 | |||
| 0da6c425fd | |||
| 81962ab9e7 | |||
| 48dd0a6a19 | |||
| 661827a957 | |||
| 242de5f749 | |||
| bd7f940987 | |||
| 0634671c84 | |||
| 1fb5be0cf8 | |||
| 3f9e4265e5 | |||
| de21b8b5e5 | |||
| fd481fef2e | |||
| 70f077e393 | |||
| 1c7d943a21 | |||
| 41873ebfe5 | |||
| fc2dbde064 | |||
| a372be4af2 | |||
| 5d333b28ab | |||
| 84ad047c01 | |||
| c93b2631cb | |||
| 15dd06a91f | |||
| 30ff26c7ef | |||
| 1434e7502a | |||
| 93b21fb7d0 | |||
| e5c82f392c | |||
| 0626964461 | |||
| 23a724e390 | |||
| 2a9c7cf854 | |||
| 335630e16d | |||
| 6051f7c294 | |||
| c1ea6cd211 | |||
| 6c43b46007 | |||
| dc9e68c4b9 | |||
| 4b03f99971 | |||
| 426f4b3d8b | |||
| 3604233507 | |||
| 8c5099f14a | |||
| d5bc348453 | |||
| bce98cb439 | |||
| 1ed3d27533 | |||
| 39a098af8e | |||
| 62491b84c1 | |||
| 81f7f5bb5d | |||
| 8ce4122160 | |||
| 370ad2ce66 | |||
| f25c425d85 | |||
| d921623f31 | |||
| 2589f1c703 | |||
| 0edb9094c4 | |||
| bc8feba701 | |||
| f37d74a7d1 | |||
| fa8612ad1a | |||
| 1d8a054b06 | |||
| 5898fbf86d | |||
| cd1cdd2e0b | |||
| c0f920544b | |||
| 36c90531a8 | |||
| 7f7c5a3b04 | |||
| c084e56ad8 | |||
| 84acc3c76e | |||
| e1f0014898 | |||
| 05b3a470f3 | |||
| ebe060646a | |||
| bb412be8d3 | |||
| e3c48eac24 | |||
| da89cdceda | |||
| 5a6c2c99e5 | |||
| 9f53836ce8 | |||
| 5d53d1a1dc | |||
| e00dda1dc2 | |||
| a93e0c819f | |||
| c87733b37a | |||
| 9aa964bf05 | |||
| dcb1d3ec15 | |||
| 5d9b8f3213 | |||
| d12989d195 | |||
| a9f384b50e | |||
| afedf2d0bd | |||
| a4b8486bd4 | |||
| d8bcb8ece6 | |||
| b01ac219a3 | |||
| 42320866c4 | |||
| e2e6c14d57 | |||
| 4761c38cd2 | |||
| e2bef3efe2 | |||
| bbfd4c3800 | |||
| b671d8fbb4 | |||
| 1ea04e98e8 | |||
| c1a7d6790b | |||
| f519f78922 | |||
| 551b5ed6be | |||
| 20cbb0397a | |||
| 26f999c4cf | |||
| 9858dfc1bd | |||
| 2d1df879dc | |||
| 2f12dc6a5e | |||
| 4b3286f12d | |||
| d53c5707b8 | |||
| ba64661217 | |||
| 554de17e9e | |||
| 5bc4b538e6 | |||
| 8a691d59e7 | |||
| e99798ba5c | |||
| caa962700b | |||
| 648466aa70 | |||
| 25f84bf2ad | |||
| ac2147095a | |||
| 4856c720b1 | |||
| ba8ff743f2 | |||
| 1e827af2dd | |||
| 7d107abc6a | |||
| 3bb1cd29cd | |||
| 0388367c7a | |||
| a8843dfc8f | |||
| 9d1264b6e6 | |||
| 8df04519b9 | |||
| 112a731cd6 | |||
| 8b4ff83921 | |||
| 2249b615f4 | |||
| fbfc800453 | |||
| 752aaf9b89 | |||
| f1a0d5f475 | |||
| fb5f38b3e6 | |||
| ded5299387 | |||
| 090548905f | |||
| 12a89b6927 | |||
| fc4c348ff9 | |||
| 1cd133e335 | |||
| b5dc6ca97d | |||
| f7b98c9dfe | |||
| 8e9f4e2b2e | |||
| b3faa06c4c | |||
| 165aeb6dcc | |||
| 24bae28cec | |||
| 65182d4c2f | |||
| 1d879993c9 | |||
| f3fbf3ba1d | |||
| 5a24f32327 | |||
| d72bd22f6d | |||
| 0eb94038f6 | |||
| 3a69397c0a | |||
| d49cb5a783 | |||
| c13805dd75 | |||
| bcf8a0e6e4 | |||
| 81f9398da4 | |||
| 32c90aecd3 | |||
| 01d6c1e0f6 | |||
| 607a442e22 | |||
| b2a79b3547 | |||
| 63c692d46b | |||
| 3d2ef9e735 | |||
| aee8b0c1e8 | |||
| d5e28ba3d9 | |||
| 1edaf4df14 | |||
| b6d31e3c3b | |||
| ed7b55c090 | |||
| e66e9ad888 | |||
| 4ad8b30e04 | |||
| ae2ac5c462 | |||
| 2330542a85 | |||
| 7363e1ab30 | |||
| d2131b2c91 | |||
| 78866c86cd | |||
| f5dbccb9c4 | |||
| 8ab38cc71b | |||
| 7dfcbfe38f | |||
| cd8471036c | |||
| c3ec477a6e | |||
| 5a6294adf6 | |||
| 1ba44cdd67 | |||
| dfeb88f980 | |||
| 44a724809f | |||
| 1acd4be953 | |||
| 930a5383c4 | |||
| eda6da7f12 | |||
| 448fc395d8 | |||
| 7e8b665c7c | |||
| 7be61bc9b8 | |||
| 6828af76dc | |||
| fe92d762be | |||
| b91a17e950 | |||
| aabc549bcf | |||
| 4bb4d0386b | |||
| a74af5e4d3 | |||
| a939d53286 | |||
| 35c6aae552 | |||
| 83bd6cf7e6 | |||
| 17a0cfbde0 | |||
| dbcad42da0 | |||
| 1b48022b63 | |||
| 7b8e3061d5 | |||
| 3f27564075 | |||
| e7c2746eab | |||
| 1e243496fb | |||
| 53bc433aaa | |||
| 6f5e75a1b3 | |||
| f83851b694 | |||
| 3f5a5dceb5 | |||
| f9c7dd8c39 | |||
| f3e437dbd1 | |||
| 5ee1e61eac | |||
| abd34ec7cb | |||
| b73f6db7b6 | |||
| 3b9f10dad7 | |||
| 
						 | 
					53c0e8b3b8 | ||
| 
						 | 
					7d264fe131 | ||
| 
						 | 
					c968b39657 | ||
| 
						 | 
					ebf116f347 | ||
| 8227866e7a | |||
| 6f5e73b533 | |||
| a302a36fd4 | |||
| 1307b2ff7b | |||
| d9730e765e | |||
| 8420c698d4 | |||
| 14be917d43 | |||
| 20da09fb96 | |||
| 19de7d3e4c | |||
| 41d821b86e | |||
| b758b54233 | |||
| c650266c6b | |||
| dae9bb0916 | |||
| 2f2371d8df | |||
| baaf4b70ac | |||
| 3b1cd800f6 | |||
| 0f5f7216ac | |||
| c40872379e | |||
| 897ac5ceef | |||
| eb3dbb3e45 | |||
| 9eb6042ba7 | |||
| 075833aa25 | |||
| f84d800bff | |||
| 20f814b0ef | |||
| 4376a63e93 | |||
| ee3d316175 | |||
| 452113b4bf | |||
| e9b28ea1c1 | |||
| ba07533667 | |||
| 1ab5c4885e | |||
| 8c977cf255 | |||
| d4c6014e17 | |||
| 078e5e28cc | |||
| 7010b4f3d2 | |||
| 43f38b88ce | |||
| 7a12a1a4d6 | |||
| d784f14c4c | |||
| 339cdf3ea9 | |||
| 060be3b486 | |||
| 9f93a19d51 | |||
| c131c07afe | |||
| 42dbf5c6f7 | |||
| 2e9039a569 | |||
| 64b48efafb | |||
| 37e8dc4bdc | |||
| 61deb96961 | |||
| 3a6ce1d38b | |||
| 82fb73ae59 | |||
| 4e71c8704f | |||
| 0c1edf647b | |||
| 9b38898a8a | |||
| 25348e45e0 | |||
| 631c2360e6 | |||
| 6798cf3477 | |||
| cc873d6029 | |||
| 5d147a4fc9 | |||
| 640862d8ee | |||
| 99bb53a7a3 | |||
| 4f05dc18b9 | |||
| 2a2df3bf52 | |||
| c2b15c2175 | |||
| edc27b899e | |||
| 59d96e36a4 | |||
| 2c976f926c | |||
| 671c6ec6f5 | |||
| ef9ac58c0f | |||
| 60e6fdf4e4 | |||
| 06e6455ba0 | |||
| 007eb3b5a9 | |||
| f3333f2da4 | |||
| 96b40c5169 | |||
| d81408b79c | |||
| 5ae5e90461 | |||
| 2534ef3319 | |||
| 0c2e774891 | |||
| 895bb3c901 | |||
| fca5445aa7 | |||
| 31d2b85b2f | |||
| a8b3214c49 | |||
| ccdfd388c4 | |||
| cad6acd125 | |||
| 8dc9c1b9e7 | |||
| cd4de2528f | |||
| 3ef4b98c1c | |||
| 349917e887 | |||
| 6c200ba076 | |||
| f16aa845d2 | |||
| 3bccb1e690 | |||
| 10ae697e33 | |||
| baf0d2db72 | |||
| b30123a890 | |||
| 44c34d2daa | |||
| e010fa413b | |||
| e79aca4efa | |||
| 037f6529fd | |||
| 14752d9746 | |||
| a8b2bd4e90 | |||
| 60ae971f14 | |||
| 1920d72821 | |||
| 01aa8baadd | |||
| f2f526c9de | |||
| 4d490690e2 | |||
| d9c7aa8c49 | |||
| 040299b90c | |||
| 0bd321e5ec | |||
| c038370602 | |||
| c08f7fc792 | |||
| 4dd35c3866 | |||
| 8bd041d7ea | |||
| d450ad42c0 | |||
| d34dcada09 | |||
| d8448de419 | |||
| 35ef6676a2 | |||
| e132b1c9f6 | |||
| 5511d8275c | |||
| accf877375 | |||
| 9ac362fa58 | |||
| 9253fde2e5 | |||
| 975c962025 | |||
| ba72b4e59f | |||
| 89e001bd17 | |||
| 623ca8bc0a | |||
| 0b483ce630 | |||
| 16998b85d5 | |||
| b55952ac67 | |||
| 30967dac33 | |||
| 3166faa7eb | |||
| 9bba81be22 | |||
| 18a2d16bf6 | |||
| 9265cdaea9 | |||
| fcb9b60656 | |||
| 599702f50a | |||
| c8453db69d | |||
| 6ad93abe3b | |||
| 3c60782ae7 | |||
| 736f645bf0 | |||
| b0887ab731 | |||
| ada194122d | |||
| b3d1ec142b | |||
| e7a8a163f1 | |||
| c3ef54a267 | |||
| da3b43a713 | |||
| 8cfddd7882 | |||
| 80eafbb014 | |||
| cc2a659767 | |||
| cacfeff3fe | |||
| 9379728b71 | |||
| d30d15c0d4 | |||
| 5343f53661 | |||
| 3126b2b962 | |||
| 43c671018b | |||
| 7a37377a09 | |||
| 19d9dea8b1 | |||
| c50e0b18b5 | |||
| 4c07c0feb2 | |||
| cf15b60bef | |||
| 328f64aa51 | |||
| fdf4e79a69 | |||
| bbc8732112 | |||
| 17dbe85219 | |||
| 3dc011a22c | |||
| f5b89456ab | |||
| 2e4f63b250 | |||
| 3b261ff240 | |||
| f06b00fb9d | |||
| 88987a973e | |||
| 93ffbe09af | |||
| e11848ea72 | |||
| 8bc9d12bfa | |||
| 1dbfdccb89 | |||
| f085f5dcf5 | |||
| 33579e8446 | |||
| a852da365f | |||
| b53095ae17 | |||
| 3d7780e0ba | |||
| 478636bd98 | |||
| d9ebee1e07 | |||
| 23e154bce6 | |||
| 5624f59258 | |||
| 56df942dd0 | |||
| 2dcb5fbf88 | |||
| 7a84b470f9 | |||
| 76232b7a0f | |||
| 349af16075 | |||
| 8641bead80 | |||
| eb930b71d6 | |||
| ae4ba06abf | |||
| a2e237a81f | |||
| f90c8c7e8c | |||
| c316c74aff | |||
| 93dd0ae4f6 | |||
| f79bb355cf | |||
| 45a534a042 | |||
| 2106a3423f | |||
| d3f7274e92 | |||
| 5f576896b7 | |||
| 4a3cbfb8b0 | |||
| 3e93fe1a7a | |||
| 965e055ef1 | |||
| 13a0da6e46 | |||
| 1bb05dbf1c | |||
| 4c9c1e13a5 | |||
| 99cde15966 | |||
| f2edc23e75 | |||
| 8aab4a13ae | |||
| 226102ccaf | |||
| 3d088c55d7 | |||
| bb14a346cb | |||
| f387930dee | |||
| fe63e3b25c | |||
| 23adeb06e6 | |||
| c1bd458c80 | |||
| 2a1d4178d7 | |||
| f9a37b299d | |||
| 9950e87501 | |||
| eff1ba6513 | |||
| bb085aa9a8 | |||
| b0dc0f9d78 | |||
| d1a51b019c | |||
| b7fade55fb | |||
| 79461518a3 | |||
| 8059d5d23f | |||
| 3098eacfb4 | |||
| f3d1e1c203 | |||
| e6a985ddfa | |||
| 388cc327be | |||
| 13adc695f6 | |||
| f2c7943247 | |||
| 112fd52864 | |||
| 8279385966 | |||
| 1a9692949f | |||
| e7af49b309 | |||
| b822914db3 | |||
| 9ad33efe08 | |||
| bd8f9fc1b7 | |||
| 4a2c18be4d | |||
| 479aba0195 | |||
| 1299fcac84 | |||
| 884a07f87b | |||
| 6557e9f9eb | |||
| 602cef1302 | |||
| b400db603a | |||
| 0397311f6e | |||
| abce89c829 | |||
| bbad63a460 | |||
| d940630086 | |||
| 37ecf28f2f | |||
| 12d5a976cc | |||
| 9086e2e75b | |||
| 3607eb0e4e | |||
| 3daf83d725 | |||
| 5ad0cb74cc | |||
| 9ae64e8cb1 | |||
| 1b5a0c71e0 | |||
| 4d4f11c479 | |||
| 835c89d1d4 | |||
| 46bf07dd8d | |||
| f557672586 | |||
| 4e27e1be7f | |||
| 6d390ad21e | |||
| 2f2543160e | |||
| 64a9db133e | |||
| 712c3d32f3 | |||
| 8998bbdf6d | |||
| ff31caa139 | |||
| ad06829c31 | |||
| 03a48da355 | |||
| 885bed888d | |||
| 0051cb07c9 | |||
| 8858cff9cf | |||
| 70e2af6172 | |||
| 461abd2e46 | |||
| 
						 | 
					d7269106db | ||
| 77fb99a527 | |||
| 38a56daa24 | |||
| 
						 | 
					ac0749797f | ||
| f193f7d7ca | |||
| 43657e0862 | |||
| 68ad366f74 | |||
| 350d2c5da9 | |||
| 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 | 
							
								
								
									
										4
									
								
								.coveragerc
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					[run]
 | 
				
			||||||
 | 
					omit =
 | 
				
			||||||
 | 
					    */migrations/*
 | 
				
			||||||
 | 
					    */tests/*
 | 
				
			||||||
							
								
								
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -2,11 +2,18 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Database
 | 
					# Database
 | 
				
			||||||
notfellchen
 | 
					notfellchen
 | 
				
			||||||
 | 
					*.sq3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Geojson from imports
 | 
				
			||||||
 | 
					*.geojson
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Media storage
 | 
					# Media storage
 | 
				
			||||||
static
 | 
					/static
 | 
				
			||||||
media
 | 
					media
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Compiled CSS
 | 
				
			||||||
 | 
					/src/fellchensammlung/static/fellchensammlung/css/main.css
 | 
				
			||||||
 | 
					/src/fellchensammlung/static/fellchensammlung/css/main.css.map
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Byte-compiled / optimized / DLL files
 | 
					# Byte-compiled / optimized / DLL files
 | 
				
			||||||
__pycache__/
 | 
					__pycache__/
 | 
				
			||||||
@@ -161,3 +168,4 @@ dmypy.json
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Cython debug symbols
 | 
					# Cython debug symbols
 | 
				
			||||||
cython_debug/
 | 
					cython_debug/
 | 
				
			||||||
 | 
					/node_modules/
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										15
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					## Version 0.4.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Version 0.4.0 has added support for search-as-you-type when searching for animals to adopt. Furthermore, the display of
 | 
				
			||||||
 | 
					maps in the search has been majorly improved.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Photon has been added as geocoding source option which allows to use this functionality.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Further improvements include the representation of rescue organizations and tooltips.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					One of the biggest features is the addition of search subscriptions. These allow you to not only
 | 
				
			||||||
 | 
					search for currently active adoption notices but to subscribe to that search so that you get notified if there are new
 | 
				
			||||||
 | 
					rats in your search area in the future.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For developers the new API documentation might come in handy, it can be found at
 | 
				
			||||||
 | 
					[/api/schema/swagger-ui/](https://notfellchen.org/api/schema/swagger-ui/)
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
FROM python:3.11-slim
 | 
					FROM python:3.11-slim
 | 
				
			||||||
# Use 3.11 to avoid django.core.exceptions.ImproperlyConfigured: Error loading psycopg2 or psycopg module
 | 
					# Use 3.11 to avoid django.core.exceptions.ImproperlyConfigured: Error loading psycopg2 or psycopg module
 | 
				
			||||||
MAINTAINER Julian-Samuel Gebühr
 | 
					LABEL org.opencontainers.image.authors="Julian-Samuel Gebühr"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ENV DOCKER_BUILD=true
 | 
					ENV DOCKER_BUILD=true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										90
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -2,7 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[notfellchen.org](https://notfellchen.org) ist eine Sammelstelle für Tier-Vermittlungen. Die Idee entstand, da in der
 | 
					[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.
 | 
					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.
 | 
					wollen Informationen einfach finden und nicht bereits in jeder Gruppe sein müssen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Wir nehmen Angebote auf die
 | 
					Wir nehmen Angebote auf die
 | 
				
			||||||
@@ -57,10 +57,50 @@ Therefore, a solution is used where a number of predefined texts per site are su
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Developer Notes
 | 
					# 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
 | 
					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
 | 
					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.
 | 
					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
 | 
					## Docker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Build latest image
 | 
					Build latest image
 | 
				
			||||||
@@ -77,20 +117,37 @@ docker push moanos/notfellchen:latest
 | 
				
			|||||||
docker run -p8000:7345 moanos/notfellchen:latest
 | 
					docker run -p8000:7345 moanos/notfellchen:latest
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Testing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Tests can be run with
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```zsh
 | 
				
			||||||
 | 
					nf test src
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you want to report on code coverage run
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```zsh
 | 
				
			||||||
 | 
					coverage run --source='.' src/manage.py test src
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					and
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					coverage report
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Geocoding
 | 
					## Geocoding
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Geocoding services (search map data by name, address or postcode) are provided via the
 | 
					Geocoding services (search map data by name, address or postcode) are provided via the
 | 
				
			||||||
[Nominatim](https://nominatim.org/) API, powered by [OpenStreetMap](https://openstreetmap.org) data. Notfellchen uses
 | 
					either [Nominatim](https://nominatim.org/) or [photon](https://github.com/komoot/photon) API, powered by [OpenStreetMap](https://openstreetmap.org) data.
 | 
				
			||||||
a selfhosted Nominatim instance to avoid overburdening the publicly hosted instance. Due to ressource constraints
 | 
					Notfellchen uses a selfhosted Photon instance to avoid overburdening the publicly hosted instance.
 | 
				
			||||||
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
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Maps
 | 
					## Maps
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The map on the main homepage is powered by [Versatiles](https://versatiles.org), and rendered using [Maplibre](https://maplibre.org/).
 | 
					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
 | 
					## Translation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -125,3 +182,20 @@ Start beat
 | 
				
			|||||||
```zsh
 | 
					```zsh
 | 
				
			||||||
 celery -A notfellchen.celery beat
 | 
					 celery -A notfellchen.celery beat
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Contributing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This project is currently mainly developed by me, moanos. I'd like that to change and will be very happy for contributions
 | 
				
			||||||
 | 
					and shared responsibilities. Some ideas where you can look for contributing first
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* UI improvements: Since a major redesign I'm much happier but the UI could use many, many little tweaks
 | 
				
			||||||
 | 
					* Docker: If you know how to build a docker container that is a) smaller or b) utilizes staged builds this would be amazing. Any improvement welcome
 | 
				
			||||||
 | 
					* Testing: Writing tests is always welcome, and it's likely you discover a few bugs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					I'm also very happy for all other contributions. Before you do large refactoring efforts or features, best write a short
 | 
				
			||||||
 | 
					issue for it before you spend a lot of work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Send PRs either to [codeberg](https://codeberg.org/moanos/notfellchen) (preferred) or [GitHub](https://github.com/moan0s/notfellchen).
 | 
				
			||||||
 | 
					CI (currently only for documentation) runs via [git.hyteck.de](https://git.hyteck.de), you can also ask moanos for an account there.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Also welcome are new issues with suggestions or bugs and additions to the documentation.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,10 +2,10 @@
 | 
				
			|||||||
API Documentation
 | 
					API Documentation
 | 
				
			||||||
*****************
 | 
					*****************
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The Notfellchen API serves the purpose of supporting 3rd-person applications and anything you can think of basically.
 | 
					The Notfellchen API serves the purpose of supporting 3rd-person applications, whether you want to display data in a custom format or add data from other sources.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. warning::
 | 
					.. warning::
 | 
				
			||||||
    The current API is limited in it's functionality. I you miss a specific feature please contact the developer!
 | 
					    The current API is limited in it's functionality. I you miss a specific feature please contact the developers!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
API Access
 | 
					API Access
 | 
				
			||||||
==========
 | 
					==========
 | 
				
			||||||
@@ -14,8 +14,8 @@ Via browser
 | 
				
			|||||||
-----------
 | 
					-----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
When a user is logged in, they can easily access the API in their browser, authenticated by their session.
 | 
					When a user is logged in, they can easily access the API in their browser, authenticated by their session.
 | 
				
			||||||
The API endpoint can be found at /library/api/
 | 
					
 | 
				
			||||||
http://notfellchen.org/
 | 
					For example: You can check all current adoption notices here: https://notfellchen.org/api/adoption_notice
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Via token
 | 
					Via token
 | 
				
			||||||
---------
 | 
					---------
 | 
				
			||||||
@@ -27,23 +27,37 @@ An application can then send this token in the request header for authorization.
 | 
				
			|||||||
.. code-block::
 | 
					.. code-block::
 | 
				
			||||||
    $ curl -X GET http://notfellchen.org/api/adoption_notice -H 'Authorization: Token 49b39856955dc6e5cc04365498d4ad30ea3aed78'
 | 
					    $ 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
 | 
					Endpoints
 | 
				
			||||||
---------
 | 
					---------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					All Endpoints are documented at  https://notfellchen.org/api/schema/swagger-ui/ or at https://notfellchen.org/api/schema/redoc/ if you prefer redoc.
 | 
				
			||||||
 | 
					The OpenAI schema can be downloaded at https://notfellchen.org/api/schema/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Examples are documented here.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Get Adoption Notices
 | 
					Get Adoption Notices
 | 
				
			||||||
++++++++++++++++++++
 | 
					++++++++++++++++++++
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. code-block::
 | 
					.. code-block::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  curl --request GET \
 | 
					  curl --request GET \
 | 
				
			||||||
  --url http://localhost:8000/api/adoption_notice \
 | 
					  --url https://notfellchen.org/api/adoption_notice \
 | 
				
			||||||
  --header 'Authorization: {{token}}'
 | 
					  --header 'Authorization: {{token}}'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Create Adoption Notice
 | 
					Create Adoption Notice
 | 
				
			||||||
++++++++++++++++++++++
 | 
					++++++++++++++++++++++
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. code-block::
 | 
					.. code-block::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  curl --request POST \
 | 
					  curl --request POST \
 | 
				
			||||||
  --url http://localhost:8000/api/adoption_notice \
 | 
					  --url https://notfellchen.org/api/adoption_notice \
 | 
				
			||||||
  --header 'Authorization: {{token}}' \
 | 
					  --header 'Authorization: {{token}}' \
 | 
				
			||||||
  --header 'content-type: multipart/form-data' \
 | 
					  --header 'content-type: multipart/form-data' \
 | 
				
			||||||
  --form name=TestAdoption1 \
 | 
					  --form name=TestAdoption1 \
 | 
				
			||||||
@@ -52,3 +66,42 @@ Create Adoption Notice
 | 
				
			|||||||
  --form further_information=https://notfellchen.org \
 | 
					  --form further_information=https://notfellchen.org \
 | 
				
			||||||
  --form location_string=Berlin \
 | 
					  --form location_string=Berlin \
 | 
				
			||||||
  --form group_only=true
 | 
					  --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}}'
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										74
									
								
								docs/_ext/drawio.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,74 @@
 | 
				
			|||||||
 | 
					from __future__ import annotations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from docutils import nodes
 | 
				
			||||||
 | 
					from sphinx.application import Sphinx
 | 
				
			||||||
 | 
					from sphinx.util.docutils import SphinxDirective
 | 
				
			||||||
 | 
					from sphinx.util.typing import ExtensionMetadata
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DrawioDirective(SphinxDirective):
 | 
				
			||||||
 | 
					    """A directive to show a drawio diagram!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Usage:
 | 
				
			||||||
 | 
					    .. drawio::
 | 
				
			||||||
 | 
					       example-diagram.drawio.html
 | 
				
			||||||
 | 
					       example-diagram.drawio.png
 | 
				
			||||||
 | 
					       :alt: Example of a Draw.io diagram
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    has_content = False
 | 
				
			||||||
 | 
					    required_arguments = 2  # html and png
 | 
				
			||||||
 | 
					    optional_arguments = 1
 | 
				
			||||||
 | 
					    final_argument_whitespace = True  # indicating if the final argument may contain whitespace
 | 
				
			||||||
 | 
					    option_spec = {
 | 
				
			||||||
 | 
					        "alt": str,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def run(self) -> list[nodes.Node]:
 | 
				
			||||||
 | 
					        env = self.state.document.settings.env
 | 
				
			||||||
 | 
					        builder = env.app.builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Resolve paths relative to the document
 | 
				
			||||||
 | 
					        docdir = Path(env.doc2path(env.docname)).parent
 | 
				
			||||||
 | 
					        html_rel = Path(self.arguments[0])
 | 
				
			||||||
 | 
					        png_rel = Path(self.arguments[1])
 | 
				
			||||||
 | 
					        html_path = (docdir / html_rel).resolve()
 | 
				
			||||||
 | 
					        png_path = (docdir / png_rel).resolve()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        alt_text = self.options.get("alt", "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        container = nodes.container()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # HTML output -> raw HTML node
 | 
				
			||||||
 | 
					        if builder.format == "html":
 | 
				
			||||||
 | 
					            # Embed the HTML file contents directly
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                html_content = html_path.read_text(encoding="utf-8")
 | 
				
			||||||
 | 
					            except OSError as e:
 | 
				
			||||||
 | 
					                msg = self.state_machine.reporter.error(f"Cannot read HTML file: {e}")
 | 
				
			||||||
 | 
					                return [msg]
 | 
				
			||||||
 | 
					            aria_attribute = f' aria-label="{alt_text}"' if alt_text else ""
 | 
				
			||||||
 | 
					            raw_html_node = nodes.raw(
 | 
				
			||||||
 | 
					                "",
 | 
				
			||||||
 | 
					                f'<div class="drawio-diagram"{aria_attribute}>{html_content}</div>',
 | 
				
			||||||
 | 
					                format="html",
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            container += raw_html_node
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            # Other outputs -> PNG image node
 | 
				
			||||||
 | 
					            image_node = nodes.image(uri=png_path)
 | 
				
			||||||
 | 
					            container += image_node
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [container]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def setup(app: Sphinx) -> ExtensionMetadata:
 | 
				
			||||||
 | 
					    app.add_directive("drawio", DrawioDirective)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        "version": "0.2",
 | 
				
			||||||
 | 
					        "parallel_read_safe": True,
 | 
				
			||||||
 | 
					        "parallel_write_safe": True,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
@@ -67,5 +67,6 @@ Healthchecks
 | 
				
			|||||||
You can configure notfellchen to give a hourly ping to a healthchecks server. If this ping is not received, you will get notified and cna check why the celery jobs are no running.
 | 
					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.
 | 
					Add the following to your `notfellchen.cfg` and adjust the URL to match your check.
 | 
				
			||||||
.. code::
 | 
					.. code::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  [monitoring]
 | 
					  [monitoring]
 | 
				
			||||||
  healthchecks_url=https://health.example.org/ping/5fa7c9b2-753a-4cb3-bcc9-f982f5bc68e8
 | 
					  healthchecks_url=https://health.example.org/ping/5fa7c9b2-753a-4cb3-bcc9-f982f5bc68e8
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								docs/conf.py
									
									
									
									
									
								
							
							
						
						@@ -16,6 +16,10 @@
 | 
				
			|||||||
# import sys
 | 
					# import sys
 | 
				
			||||||
# sys.path.insert(0, os.path.abspath('.'))
 | 
					# sys.path.insert(0, os.path.abspath('.'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sys.path.append(str(Path('_ext').resolve()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# -- Project information -----------------------------------------------------
 | 
					# -- Project information -----------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -28,7 +32,6 @@ version = ''
 | 
				
			|||||||
# The full version, including alpha/beta/rc tags
 | 
					# The full version, including alpha/beta/rc tags
 | 
				
			||||||
release = '0.2.0'
 | 
					release = '0.2.0'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
# -- General configuration ---------------------------------------------------
 | 
					# -- General configuration ---------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# If your documentation needs a minimal Sphinx version, state it here.
 | 
					# If your documentation needs a minimal Sphinx version, state it here.
 | 
				
			||||||
@@ -40,6 +43,7 @@ release = '0.2.0'
 | 
				
			|||||||
# ones.
 | 
					# ones.
 | 
				
			||||||
extensions = [
 | 
					extensions = [
 | 
				
			||||||
    'sphinx.ext.ifconfig',
 | 
					    'sphinx.ext.ifconfig',
 | 
				
			||||||
 | 
					    'drawio'
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Add any paths that contain templates here, relative to this directory.
 | 
					# Add any paths that contain templates here, relative to this directory.
 | 
				
			||||||
@@ -69,7 +73,6 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
 | 
				
			|||||||
# The name of the Pygments (syntax highlighting) style to use.
 | 
					# The name of the Pygments (syntax highlighting) style to use.
 | 
				
			||||||
pygments_style = None
 | 
					pygments_style = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
# -- Options for HTML output -------------------------------------------------
 | 
					# -- Options for HTML output -------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# The theme to use for HTML and HTML Help pages.  See the documentation for
 | 
					# The theme to use for HTML and HTML Help pages.  See the documentation for
 | 
				
			||||||
@@ -104,7 +107,6 @@ html_static_path = ['_static']
 | 
				
			|||||||
# Output file base name for HTML help builder.
 | 
					# Output file base name for HTML help builder.
 | 
				
			||||||
htmlhelp_basename = 'notfellchen'
 | 
					htmlhelp_basename = 'notfellchen'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
# -- Options for LaTeX output ------------------------------------------------
 | 
					# -- Options for LaTeX output ------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
latex_elements = {
 | 
					latex_elements = {
 | 
				
			||||||
@@ -133,7 +135,6 @@ latex_documents = [
 | 
				
			|||||||
     'Julian-Samuel Gebühr', 'manual'),
 | 
					     'Julian-Samuel Gebühr', 'manual'),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
# -- Options for manual page output ------------------------------------------
 | 
					# -- Options for manual page output ------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# One entry per manual page. List of tuples
 | 
					# One entry per manual page. List of tuples
 | 
				
			||||||
@@ -143,7 +144,6 @@ man_pages = [
 | 
				
			|||||||
     [author], 1)
 | 
					     [author], 1)
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
# -- Options for Texinfo output ----------------------------------------------
 | 
					# -- Options for Texinfo output ----------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Grouping the document tree into Texinfo files. List of tuples
 | 
					# Grouping the document tree into Texinfo files. List of tuples
 | 
				
			||||||
@@ -155,7 +155,6 @@ texinfo_documents = [
 | 
				
			|||||||
     'Miscellaneous'),
 | 
					     'Miscellaneous'),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
# -- Options for Epub output -------------------------------------------------
 | 
					# -- Options for Epub output -------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Bibliographic Dublin Core info.
 | 
					# Bibliographic Dublin Core info.
 | 
				
			||||||
@@ -173,5 +172,4 @@ epub_title = project
 | 
				
			|||||||
# A list of files that should not be packed into the epub file.
 | 
					# A list of files that should not be packed into the epub file.
 | 
				
			||||||
epub_exclude_files = ['search.html']
 | 
					epub_exclude_files = ['search.html']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
# -- Extension configuration -------------------------------------------------
 | 
					# -- Extension configuration -------------------------------------------------
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ Report a bug
 | 
				
			|||||||
^^^^^^^^^^^^
 | 
					^^^^^^^^^^^^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
To report a bug, file an issue on `Github
 | 
					To report a bug, file an issue on `Github
 | 
				
			||||||
<https://codeberg.org/moanos/notfellchen/issues>`_
 | 
					<https://github.com/moan0s/notfellchen/issues>`_
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Try to include the following information:
 | 
					Try to include the following information:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -29,7 +29,7 @@ To contribute simply clone the directory, make your changes and file a
 | 
				
			|||||||
pull request.
 | 
					pull request.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
If you want to know what can be done, have a look at the current `Github
 | 
					If you want to know what can be done, have a look at the current `Github
 | 
				
			||||||
<https://codeberg.org/moanos/notfellchen/issues>`_.
 | 
					<https://github.com/moan0s/notfellchen/issues>`_.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Get in touch!
 | 
					Get in touch!
 | 
				
			||||||
^^^^^^^^^^^^^
 | 
					^^^^^^^^^^^^^
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,8 +5,7 @@ What qualifies as release?
 | 
				
			|||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
 | 
					^^^^^^^^^^^^^^^^^^^^^^^^^^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
A new release should be announced when a significant number functions, bugfixes or other improvements to the software
 | 
					A new release should be announced when a significant number functions, bugfixes or other improvements to the software
 | 
				
			||||||
is made. Usually this indicates a minor release.
 | 
					is made. Notfellchen follows `Semantic Versioning <https://semver.org/>`_.
 | 
				
			||||||
Major releases are yet to be determined.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
What should be done before a release?
 | 
					What should be done before a release?
 | 
				
			||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 | 
					^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 | 
				
			||||||
@@ -14,7 +13,7 @@ What should be done before a release?
 | 
				
			|||||||
Tested basic functions
 | 
					Tested basic functions
 | 
				
			||||||
######################
 | 
					######################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Run :command:`pytest`
 | 
					Run :command:`nf test src`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Test upgrade on a copy of a production database
 | 
					Test upgrade on a copy of a production database
 | 
				
			||||||
###############################################
 | 
					###############################################
 | 
				
			||||||
@@ -38,4 +37,4 @@ Do a final commit on this change, and tag the commit as release with appropriate
 | 
				
			|||||||
    git tag -a v1.0.0 -m "Releasing version v1.0.0"
 | 
					    git tag -a v1.0.0 -m "Releasing version v1.0.0"
 | 
				
			||||||
    git push origin v1.0.0
 | 
					    git push origin v1.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Make sure the tag is visible on Codeberg and celebrate 🥳
 | 
					Make sure the tag is visible on GitHub/Codeberg and celebrate 🥳
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								docs/user/Screenshot-Moderationstools.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 53 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								docs/user/Screenshot-hilfreiche-Links.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 18 KiB  | 
							
								
								
									
										11
									
								
								docs/user/Tiere-in-Vermittlung-entdecken.drawio.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								docs/user/Tiere-in-Vermittlung-entdecken.drawio.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 120 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								docs/user/Vermittlung-Lifecycle.drawio.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 150 KiB  | 
							
								
								
									
										11
									
								
								docs/user/Vermittlung_Lifecycle.drawio.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -6,14 +6,27 @@ Jede Vermittlung kann abonniert werden. Dafür klickst du auf die Glocke neben d
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.. image:: abonnieren.png
 | 
					.. image:: abonnieren.png
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Einstellungen
 | 
				
			||||||
 | 
					-------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Du kannst E-Mail Benachrichtigungen in den Einstellungen deaktivieren.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. image::
 | 
				
			||||||
 | 
					   einstellungen-benachrichtigungen.png
 | 
				
			||||||
 | 
					   :alt: Screenshot der Profileinstellungen in Notfellchen. Ein roter Pfeil zeigt auf einen Schalter "E-Mail Benachrichtigungen"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Auf der Website
 | 
					Auf der Website
 | 
				
			||||||
+++++++++++++++
 | 
					+++++++++++++++
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. image::
 | 
				
			||||||
 | 
					   screenshot-benachrichtigungen.png
 | 
				
			||||||
 | 
					   :alt: Screenshot der Menüleiste von Notfellchen.org. Neben dem Symbol einer Glocke steht die Zahl 27.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
E-Mail
 | 
					E-Mail
 | 
				
			||||||
++++++
 | 
					++++++
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Mit während deiner :doc:`registrierung` gibst du eine E-Mail Addresse an.
 | 
					Mit während deiner :doc:`registrierung` gibst du eine E-Mail Adresse an. An diese senden wir Benachrichtigungen, außer
 | 
				
			||||||
 | 
					du deaktiviert dies wie oben beschrieben.
 | 
				
			||||||
Benachrichtigungen senden wir per Mail - du kannst das jederzeit in den Einstellungen deaktivieren.
 | 
					 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								docs/user/einstellungen-benachrichtigungen.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 46 KiB  | 
							
								
								
									
										58
									
								
								docs/user/erste-schritte.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					Erste Schritte
 | 
				
			||||||
 | 
					==============
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Tiere zum Adoptieren suchen
 | 
				
			||||||
 | 
					---------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Wenn du Tiere zum adoptieren suchst, brauchst du keinen Account. Du kannst bequem die `Suche <https://notfellchen.org/suchen/>`_ nutzen, um Tiere zur Adoption in deiner Nähe zu finden.
 | 
				
			||||||
 | 
					Wenn dich eine Vermittlung interessiert, kannst du folgendes tun
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* die Vermittlung aufrufen um Details zu sehen
 | 
				
			||||||
 | 
					* den Link :guilabel:`Weitere Informationen` anklicken um auf der Tierheimwebsite mehr zu erfahren
 | 
				
			||||||
 | 
					* per Kommentar weitere Informationen erfragen oder hinzufügen
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Wenn du die Tiere tatsächlich informieren willst, folge der Anleitung unter :guilabel:`Adoptionsprozess`.
 | 
				
			||||||
 | 
					Dieser kann sich je nach Tierschutzorganisation unterscheiden.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. image::
 | 
				
			||||||
 | 
					   screenshot-adoptionsprozess.png
 | 
				
			||||||
 | 
					   :alt: Screenshot der Sektion "Adoptionsprozess" einer Vermittlungsanzeige. Der Prozess ist folgendermaßen: 1. Link zu "Weiteren Informationen" prüfen, 2.  Organization kontaktieren, 3. Bei erfolgreicher Vermittlung: Vermittlung als geschlossen melden
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Suchen abonnieren
 | 
				
			||||||
 | 
					+++++++++++++++++
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Es kann sein, dass es in deiner Umgebung keine passenden Tiere für deine Suche gibt. Damit du nicht ständig wieder Suchen musst, gibt es die Funktion "Suche abonnieren".
 | 
				
			||||||
 | 
					Wenn du eine Suche abonnierst, wirst du für neue Vermittlungen, die den Kriterien der Suche entsprechen, benachrichtigt.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. image::
 | 
				
			||||||
 | 
					   screenshot-suche-abonnieren.png
 | 
				
			||||||
 | 
					   :alt: Screenshot der Suchmaske auf Notfellchen.org . Ein roter Pfeil zeigt auf den Button "Suche abonnieren"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. important::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   Um Suchen zu abonnieren brauchst du einen Account. Wie du einen Account erstellst erfährst du hier: :doc:`registrierung`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. hint::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   Mehr über Benachrichtigungen findest du hier: :doc:`benachrichtigungen`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Vermittlungen hinzufügen
 | 
				
			||||||
 | 
					------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Gehe zu `Vermittlung hinzufügen <https://notfellchen.org/vermitteln/>`_ um eine neue Vermittlung einzustellen.
 | 
				
			||||||
 | 
					Füge alle Informationen die du hast hinzu.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. important::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   Um Vermittlungen hinzuzufügen brauchst du einen Account.
 | 
				
			||||||
 | 
					   Wie du einen Account erstellst erfährst du hier: :doc:`registrierung`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. important::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   Vermittlungen die du einstellst müssen erst durch Moderator\*innen freigeschaltet werden. Das passiert normalerweise
 | 
				
			||||||
 | 
					   innerhalb von 24 Stunden. Wenn deine Vermittlung dann noch nicht freigeschaltet ist, prüfe bitte dein E-Mail Postfach,
 | 
				
			||||||
 | 
					   es könnte sein, dass die Moderator\*innen Rückfragen haben. Melde dich gerne unter info@notfellchen.org, wenn deine
 | 
				
			||||||
 | 
					   Vermittlung nach 24 Stunden nicht freigeschaltet ist.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1,11 +1,17 @@
 | 
				
			|||||||
******************
 | 
					****************
 | 
				
			||||||
User Dokumentation
 | 
					Benutzerhandbuch
 | 
				
			||||||
******************
 | 
					****************
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Im Benutzerhandbuch findest du Informationen zur Benutzung von `notfellchen.org <https://notfellchen.org>`_.
 | 
				
			||||||
 | 
					Solltest du darüber hinaus Fragen haben, komm gerne auf uns zu: info@notfellchen.org
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. toctree::
 | 
					.. toctree::
 | 
				
			||||||
   :maxdepth: 2
 | 
					   :maxdepth: 2
 | 
				
			||||||
   :caption: Inhalt:
 | 
					   :caption: Inhalt:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   erste-schritte.rst
 | 
				
			||||||
   registrierung.rst
 | 
					   registrierung.rst
 | 
				
			||||||
   vermittlungen.rst
 | 
					   vermittlungen.rst
 | 
				
			||||||
   moderationskonzept.rst
 | 
					   moderationskonzept.rst
 | 
				
			||||||
   benachrichtigungen.rst
 | 
					   benachrichtigungen.rst
 | 
				
			||||||
 | 
					   organisationen-pruefen.rst
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										55
									
								
								docs/user/organisationen-pruefen.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					Tiere in Vermittlung systematisch entdecken & eintragen
 | 
				
			||||||
 | 
					=======================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Notfellchen hat eine Liste der meisten deutschen Tierheime und anderer Tierschutzorganisationen.
 | 
				
			||||||
 | 
					Die meisten dieser Organisationen nehmen Tiere auf die bei Notfellchen eingetragen werden können.
 | 
				
			||||||
 | 
					Es ist daher das Ziel, diese Organisationen alle zwei Wochen auf neue Tiere zu prüfen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					+-------------------------------------------------+---------+----------------------+
 | 
				
			||||||
 | 
					| Gruppe                                          | Anzahl  | Zuletzt aktualisiert |
 | 
				
			||||||
 | 
					+=================================================+=========+======================+
 | 
				
			||||||
 | 
					|  Tierschutzorganisationen im Verzeichnis        | 550     | Oktober 2025         |
 | 
				
			||||||
 | 
					+-------------------------------------------------+---------+----------------------+
 | 
				
			||||||
 | 
					| Tierschutzorganisationen in regelmäßigerPrüfung | 412     | Oktober 2025         |
 | 
				
			||||||
 | 
					+-------------------------------------------------+---------+----------------------+
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. warning::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   Organisationen auf neue Tiere zu prüfen ist eine Funktion für Moderator\*innen. Falls du Lust hast mitzuhelfen,
 | 
				
			||||||
 | 
					   meld dich unter info@notfellchen.org
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Als Moderator\*in kannst du direkt auf den `Moderations-Check <https://notfellchen.org/organization-check/>`_ zugreifen
 | 
				
			||||||
 | 
					oder findest ihn in unter :menuselection:`Hilfreiche Links --> Moderationstools`:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. image::
 | 
				
			||||||
 | 
					   Screenshot-hilfreiche-Links.png
 | 
				
			||||||
 | 
					   :alt: Screenshot der Hilfreichen Links. Zur Auswahl stehen "Tierheime in der Nähe","Moderationstools" und "Admin-Bereich"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. image::
 | 
				
			||||||
 | 
					  Screenshot-Moderationstools.png
 | 
				
			||||||
 | 
					  :alt: Screenshot der Moderationstools. Zur Auswahl stehen "Moderationswarteschlange", "Up-to-Date Check", "Organisations-Check" und "Vermittlung ins Fediverse posten".
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Arbeitsmodus
 | 
				
			||||||
 | 
					------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. drawio::
 | 
				
			||||||
 | 
					   Tiere-in-Vermittlung-entdecken.drawio.html
 | 
				
			||||||
 | 
					   Tiere-in-Vermittlung-entdecken.drawio.png
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Shortcuts
 | 
				
			||||||
 | 
					---------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Um die Prüfung schneller zu gestalten, gibt es eine Reihe von Shortcuts die du nutzen kannst. Aus Gründen der
 | 
				
			||||||
 | 
					Übersichtlichkeit sind im Folgenden auch Shortcuts im Browser aufgeführt.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					+------------------------------------------------------+---------------+
 | 
				
			||||||
 | 
					| Aktion                                               | Shortcut      |
 | 
				
			||||||
 | 
					+======================================================+===============+
 | 
				
			||||||
 | 
					|  Website der ersten Tierschutzorganisation öffnen    | :kbd:`O`      |
 | 
				
			||||||
 | 
					+------------------------------------------------------+---------------+
 | 
				
			||||||
 | 
					| Tab schließen (Firefox/Chrome)                       | :kbd:`STRG+W` |
 | 
				
			||||||
 | 
					+------------------------------------------------------+---------------+
 | 
				
			||||||
 | 
					| Erste Tierschutzorganisationa als geprüft markieren  | :kbd:`C`      |
 | 
				
			||||||
 | 
					+------------------------------------------------------+---------------+
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								docs/user/screenshot-adoptionsprozess.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 34 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								docs/user/screenshot-benachrichtigungen.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 205 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								docs/user/screenshot-suche-abonnieren.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 24 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								docs/user/screenshot-suche.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 20 KiB  | 
@@ -1,7 +1,7 @@
 | 
				
			|||||||
Vermittlungen
 | 
					Vermittlungen
 | 
				
			||||||
=============
 | 
					=============
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Vermittlungen können von allen Nutzer*innen mit Account erstellt werden. Vermittlungen normaler Nutzer*innen kommen dann in eine Warteschlange und werden vom Admin & Modertionsteam geprüft und sichtbar geschaltet.
 | 
					Vermittlungen können von allen Nutzer\*innen mit Account erstellt werden. Vermittlungen normaler Nutzer*innen kommen dann in eine Warteschlange und werden vom Admin & Modertionsteam geprüft und sichtbar geschaltet.
 | 
				
			||||||
Tierheime und Pflegestellen können auf Anfrage einen Koordinations-Status bekommen, wodurch sie Vermittlungsanzeigen erstellen können die direkt öffentlich sichtbar sind.
 | 
					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.
 | 
					Jede Vermittlung hat ein "Zuletzt-geprüft" Datum, das anzeigt, wann ein Mensch zuletzt überprüft hat, ob die Anzeige noch aktuell ist.
 | 
				
			||||||
@@ -15,3 +15,114 @@ Die Kommentarfunktion von Vermittlungen ermöglicht es angemeldeten Nutzer*innen
 | 
				
			|||||||
Ersteller*innen von Vermittlungen werden über neue Kommentare per Mail benachrichtigt, ebenso alle die die Vermittlung abonniert haben.
 | 
					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.
 | 
					Kommentare können, wie Vermittlungen, gemeldet werden.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. drawio::
 | 
				
			||||||
 | 
					   Vermittlung_Lifecycle.drawio.html
 | 
				
			||||||
 | 
					   Vermittlung-Lifecycle.drawio.png
 | 
				
			||||||
 | 
					   :alt: Diagramm das den Prozess der Vermittlungen zeigt.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Adoption Notice Status Choices
 | 
				
			||||||
 | 
					++++++++++++++++++++++++++++++
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Aktiv
 | 
				
			||||||
 | 
					-----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Aktive Vermittlungen die über die Suche auffindbar sind.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. list-table::
 | 
				
			||||||
 | 
					   :header-rows: 1
 | 
				
			||||||
 | 
					   :width: 100%
 | 
				
			||||||
 | 
					   :widths: 1 1 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   * - Value
 | 
				
			||||||
 | 
					     - Label
 | 
				
			||||||
 | 
					     - Description
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   * - ``active_searching``
 | 
				
			||||||
 | 
					     - Searching
 | 
				
			||||||
 | 
					     -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   * - ``active_interested``
 | 
				
			||||||
 | 
					     - Interested
 | 
				
			||||||
 | 
					     - Jemand hat bereits Interesse an den Tieren.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Warte auf Aktion
 | 
				
			||||||
 | 
					----------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Vermittlungen in diesem Status warten darauf, dass ein Mensch sie überprüft. Sie können nicht über die Suche gefunden werden.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. list-table::
 | 
				
			||||||
 | 
					   :header-rows: 1
 | 
				
			||||||
 | 
					   :width: 100%
 | 
				
			||||||
 | 
					   :widths: 1 1 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   * - ``awaiting_action_waiting_for_review``
 | 
				
			||||||
 | 
					     - Waiting for review
 | 
				
			||||||
 | 
					     - Neue Vermittlung die deaktiviert ist bis Moderator*innen sie überprüfen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   * - ``awaiting_action_needs_additional_info``
 | 
				
			||||||
 | 
					     - Needs additional info
 | 
				
			||||||
 | 
					     - Deaktiviert bis Informationen nachgetragen werden.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   * - ``disabled_unchecked``
 | 
				
			||||||
 | 
					     - Unchecked
 | 
				
			||||||
 | 
					     - Vermittlung deaktiviert bis sie vom Team auf Aktualität geprüft wurde.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Geschlossen
 | 
				
			||||||
 | 
					-----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Geschlossene Vermittlungen tauchen in keiner Suche auf. Sie werden aber weiterhin angezeigt, wenn der Link zu ihnen direkt aufgerufen wird.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. list-table::
 | 
				
			||||||
 | 
					   :header-rows: 1
 | 
				
			||||||
 | 
					   :width: 100%
 | 
				
			||||||
 | 
					   :widths: 1 1 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   * - ``closed_successful_with_notfellchen``
 | 
				
			||||||
 | 
					     - Successful (with Notfellchen)
 | 
				
			||||||
 | 
					     - Vermittlung erfolgreich abgeschlossen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   * - ``closed_successful_without_notfellchen``
 | 
				
			||||||
 | 
					     - Successful (without Notfellchen)
 | 
				
			||||||
 | 
					     - Vermittlung erfolgreich abgeschlossen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   * - ``closed_animal_died``
 | 
				
			||||||
 | 
					     - Animal died
 | 
				
			||||||
 | 
					     - Die zu vermittelnden Tiere sind über die Regenbrücke gegangen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   * - ``closed_for_other_adoption_notice``
 | 
				
			||||||
 | 
					     - Closed for other adoption notice
 | 
				
			||||||
 | 
					     - Vermittlung wurde zugunsten einer anderen geschlossen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   * - ``closed_not_open_for_adoption_anymore``
 | 
				
			||||||
 | 
					     - Not open for adoption anymore
 | 
				
			||||||
 | 
					     - Tier(e) stehen nicht mehr zur Vermittlung bereit.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   * - ``closed_link_to_more_info_not_reachable``
 | 
				
			||||||
 | 
					     - Der Link zu weiteren Informationen ist nicht mehr erreichbar.
 | 
				
			||||||
 | 
					     - Der Link zu weiteren Informationen ist nicht mehr erreichbar, die Vermittlung wurde daher automatisch deaktiviert.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   * - ``closed_other``
 | 
				
			||||||
 | 
					     - Other (closed)
 | 
				
			||||||
 | 
					     - Vermittlung geschlossen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Deaktiviert
 | 
				
			||||||
 | 
					-----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Deaktivierte Vermittlungen werden nur noch Moderator\*innen und Administrator\*innen angezeigt.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. list-table::
 | 
				
			||||||
 | 
					   :header-rows: 1
 | 
				
			||||||
 | 
					   :width: 100%
 | 
				
			||||||
 | 
					   :widths: 1 1 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   * - ``disabled_against_the_rules``
 | 
				
			||||||
 | 
					     - Against the rules
 | 
				
			||||||
 | 
					     - Vermittlung deaktiviert da sie gegen die Regeln verstößt.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   * - ``disabled_other``
 | 
				
			||||||
 | 
					     - Other (disabled)
 | 
				
			||||||
 | 
					     - Vermittlung deaktiviert.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ host=localhost
 | 
				
			|||||||
[django]
 | 
					[django]
 | 
				
			||||||
secret=CHANGE-ME
 | 
					secret=CHANGE-ME
 | 
				
			||||||
debug=True
 | 
					debug=True
 | 
				
			||||||
 | 
					internal_ips=["127.0.0.1"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[database]
 | 
					[database]
 | 
				
			||||||
backend=sqlite3
 | 
					backend=sqlite3
 | 
				
			||||||
@@ -18,10 +19,16 @@ media=./media
 | 
				
			|||||||
static=./static
 | 
					static=./static
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[mail]
 | 
					[mail]
 | 
				
			||||||
console-only=true
 | 
					console_only=true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[logging]
 | 
					[logging]
 | 
				
			||||||
app_log_level=INFO
 | 
					app_log_level=INFO
 | 
				
			||||||
django_log_level=INFO
 | 
					django_log_level=INFO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[geocoding]
 | 
				
			||||||
 | 
					api_url=https://photon.hyteck.de/api
 | 
				
			||||||
 | 
					api_format=photon
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[security]
 | 
				
			||||||
 | 
					totp_issuer="NF Localhost"
 | 
				
			||||||
 | 
					webauth_allow_insecure_origin=True
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										497
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,497 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "name": "notfellchen",
 | 
				
			||||||
 | 
					  "lockfileVersion": 3,
 | 
				
			||||||
 | 
					  "requires": true,
 | 
				
			||||||
 | 
					  "packages": {
 | 
				
			||||||
 | 
					    "": {
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "bulma": "^1.0.4",
 | 
				
			||||||
 | 
					        "sass": "^1.89.2"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@parcel/watcher": {
 | 
				
			||||||
 | 
					      "version": "2.5.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
 | 
				
			||||||
 | 
					      "hasInstallScript": true,
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "detect-libc": "^1.0.3",
 | 
				
			||||||
 | 
					        "is-glob": "^4.0.3",
 | 
				
			||||||
 | 
					        "micromatch": "^4.0.5",
 | 
				
			||||||
 | 
					        "node-addon-api": "^7.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 10.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "type": "opencollective",
 | 
				
			||||||
 | 
					        "url": "https://opencollective.com/parcel"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "optionalDependencies": {
 | 
				
			||||||
 | 
					        "@parcel/watcher-android-arm64": "2.5.1",
 | 
				
			||||||
 | 
					        "@parcel/watcher-darwin-arm64": "2.5.1",
 | 
				
			||||||
 | 
					        "@parcel/watcher-darwin-x64": "2.5.1",
 | 
				
			||||||
 | 
					        "@parcel/watcher-freebsd-x64": "2.5.1",
 | 
				
			||||||
 | 
					        "@parcel/watcher-linux-arm-glibc": "2.5.1",
 | 
				
			||||||
 | 
					        "@parcel/watcher-linux-arm-musl": "2.5.1",
 | 
				
			||||||
 | 
					        "@parcel/watcher-linux-arm64-glibc": "2.5.1",
 | 
				
			||||||
 | 
					        "@parcel/watcher-linux-arm64-musl": "2.5.1",
 | 
				
			||||||
 | 
					        "@parcel/watcher-linux-x64-glibc": "2.5.1",
 | 
				
			||||||
 | 
					        "@parcel/watcher-linux-x64-musl": "2.5.1",
 | 
				
			||||||
 | 
					        "@parcel/watcher-win32-arm64": "2.5.1",
 | 
				
			||||||
 | 
					        "@parcel/watcher-win32-ia32": "2.5.1",
 | 
				
			||||||
 | 
					        "@parcel/watcher-win32-x64": "2.5.1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@parcel/watcher-android-arm64": {
 | 
				
			||||||
 | 
					      "version": "2.5.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "arm64"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "android"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 10.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "type": "opencollective",
 | 
				
			||||||
 | 
					        "url": "https://opencollective.com/parcel"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@parcel/watcher-darwin-arm64": {
 | 
				
			||||||
 | 
					      "version": "2.5.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "arm64"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "darwin"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 10.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "type": "opencollective",
 | 
				
			||||||
 | 
					        "url": "https://opencollective.com/parcel"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@parcel/watcher-darwin-x64": {
 | 
				
			||||||
 | 
					      "version": "2.5.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "x64"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "darwin"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 10.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "type": "opencollective",
 | 
				
			||||||
 | 
					        "url": "https://opencollective.com/parcel"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@parcel/watcher-freebsd-x64": {
 | 
				
			||||||
 | 
					      "version": "2.5.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "x64"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "freebsd"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 10.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "type": "opencollective",
 | 
				
			||||||
 | 
					        "url": "https://opencollective.com/parcel"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@parcel/watcher-linux-arm-glibc": {
 | 
				
			||||||
 | 
					      "version": "2.5.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "arm"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "linux"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 10.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "type": "opencollective",
 | 
				
			||||||
 | 
					        "url": "https://opencollective.com/parcel"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@parcel/watcher-linux-arm-musl": {
 | 
				
			||||||
 | 
					      "version": "2.5.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "arm"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "linux"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 10.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "type": "opencollective",
 | 
				
			||||||
 | 
					        "url": "https://opencollective.com/parcel"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@parcel/watcher-linux-arm64-glibc": {
 | 
				
			||||||
 | 
					      "version": "2.5.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "arm64"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "linux"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 10.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "type": "opencollective",
 | 
				
			||||||
 | 
					        "url": "https://opencollective.com/parcel"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@parcel/watcher-linux-arm64-musl": {
 | 
				
			||||||
 | 
					      "version": "2.5.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "arm64"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "linux"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 10.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "type": "opencollective",
 | 
				
			||||||
 | 
					        "url": "https://opencollective.com/parcel"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@parcel/watcher-linux-x64-glibc": {
 | 
				
			||||||
 | 
					      "version": "2.5.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "x64"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "linux"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 10.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "type": "opencollective",
 | 
				
			||||||
 | 
					        "url": "https://opencollective.com/parcel"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@parcel/watcher-linux-x64-musl": {
 | 
				
			||||||
 | 
					      "version": "2.5.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "x64"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "linux"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 10.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "type": "opencollective",
 | 
				
			||||||
 | 
					        "url": "https://opencollective.com/parcel"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@parcel/watcher-win32-arm64": {
 | 
				
			||||||
 | 
					      "version": "2.5.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "arm64"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "win32"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 10.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "type": "opencollective",
 | 
				
			||||||
 | 
					        "url": "https://opencollective.com/parcel"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@parcel/watcher-win32-ia32": {
 | 
				
			||||||
 | 
					      "version": "2.5.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "ia32"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "win32"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 10.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "type": "opencollective",
 | 
				
			||||||
 | 
					        "url": "https://opencollective.com/parcel"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@parcel/watcher-win32-x64": {
 | 
				
			||||||
 | 
					      "version": "2.5.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
 | 
				
			||||||
 | 
					      "cpu": [
 | 
				
			||||||
 | 
					        "x64"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "os": [
 | 
				
			||||||
 | 
					        "win32"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 10.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "type": "opencollective",
 | 
				
			||||||
 | 
					        "url": "https://opencollective.com/parcel"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/braces": {
 | 
				
			||||||
 | 
					      "version": "3.0.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "fill-range": "^7.1.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=8"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/bulma": {
 | 
				
			||||||
 | 
					      "version": "1.0.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-Ffb6YGXDiZYX3cqvSbHWqQ8+LkX6tVoTcZuVB3lm93sbAVXlO0D6QlOTMnV6g18gILpAXqkG2z9hf9z4hCjz2g==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/chokidar": {
 | 
				
			||||||
 | 
					      "version": "4.0.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "readdirp": "^4.0.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 14.16.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://paulmillr.com/funding/"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/detect-libc": {
 | 
				
			||||||
 | 
					      "version": "1.0.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
 | 
				
			||||||
 | 
					      "license": "Apache-2.0",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "bin": {
 | 
				
			||||||
 | 
					        "detect-libc": "bin/detect-libc.js"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=0.10"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/fill-range": {
 | 
				
			||||||
 | 
					      "version": "7.1.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "to-regex-range": "^5.0.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=8"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/immutable": {
 | 
				
			||||||
 | 
					      "version": "5.1.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
 | 
				
			||||||
 | 
					      "license": "MIT"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/is-extglob": {
 | 
				
			||||||
 | 
					      "version": "2.1.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=0.10.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/is-glob": {
 | 
				
			||||||
 | 
					      "version": "4.0.3",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "is-extglob": "^2.1.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=0.10.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/is-number": {
 | 
				
			||||||
 | 
					      "version": "7.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=0.12.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/micromatch": {
 | 
				
			||||||
 | 
					      "version": "4.0.8",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "braces": "^3.0.3",
 | 
				
			||||||
 | 
					        "picomatch": "^2.3.1"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=8.6"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/node-addon-api": {
 | 
				
			||||||
 | 
					      "version": "7.1.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/picomatch": {
 | 
				
			||||||
 | 
					      "version": "2.3.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=8.6"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "url": "https://github.com/sponsors/jonschlinkert"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/readdirp": {
 | 
				
			||||||
 | 
					      "version": "4.1.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">= 14.18.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "funding": {
 | 
				
			||||||
 | 
					        "type": "individual",
 | 
				
			||||||
 | 
					        "url": "https://paulmillr.com/funding/"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/sass": {
 | 
				
			||||||
 | 
					      "version": "1.89.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "chokidar": "^4.0.0",
 | 
				
			||||||
 | 
					        "immutable": "^5.0.2",
 | 
				
			||||||
 | 
					        "source-map-js": ">=0.6.2 <2.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "bin": {
 | 
				
			||||||
 | 
					        "sass": "sass.js"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=14.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "optionalDependencies": {
 | 
				
			||||||
 | 
					        "@parcel/watcher": "^2.4.1"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/source-map-js": {
 | 
				
			||||||
 | 
					      "version": "1.2.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
 | 
				
			||||||
 | 
					      "license": "BSD-3-Clause",
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=0.10.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/to-regex-range": {
 | 
				
			||||||
 | 
					      "version": "5.0.1",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
 | 
				
			||||||
 | 
					      "license": "MIT",
 | 
				
			||||||
 | 
					      "optional": true,
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "is-number": "^7.0.0"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      "engines": {
 | 
				
			||||||
 | 
					        "node": ">=8.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "bulma": "^1.0.4",
 | 
				
			||||||
 | 
					    "sass": "^1.89.2"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "scripts": {
 | 
				
			||||||
 | 
					    "build-bulma": "sass --load-path=node_modules src/fellchensammlung/static/fellchensammlung/css/main.scss src/fellchensammlung/static/fellchensammlung/css/main.css --style compressed",
 | 
				
			||||||
 | 
					    "start": "npm run build-bulma -- --watch"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -6,14 +6,14 @@ build-backend = "setuptools.build_meta"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[project]
 | 
					[project]
 | 
				
			||||||
name = "notfellchen"
 | 
					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 = [
 | 
					authors = [
 | 
				
			||||||
    { name = "moanos", email = "julian-samuel@gebuehr.net" },
 | 
					    { name = "moanos", email = "julian-samuel@gebuehr.net" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
maintainers = [
 | 
					maintainers = [
 | 
				
			||||||
    { name = "moanos", email = "julian-samuel@gebuehr.net" },
 | 
					    { name = "moanos", email = "julian-samuel@gebuehr.net" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
keywords = ["animal", "adoption", "django", "rescue", ]
 | 
					keywords = ["animal", "adoption", "django", "rescue", "rats" ]
 | 
				
			||||||
license = { text = "AGPL-3.0-or-later" }
 | 
					license = { text = "AGPL-3.0-or-later" }
 | 
				
			||||||
classifiers = [
 | 
					classifiers = [
 | 
				
			||||||
    "Environment :: Web",
 | 
					    "Environment :: Web",
 | 
				
			||||||
@@ -24,14 +24,10 @@ classifiers = [
 | 
				
			|||||||
]
 | 
					]
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
    "Django",
 | 
					    "Django",
 | 
				
			||||||
    "coverage",
 | 
					 | 
				
			||||||
    "codecov",
 | 
					    "codecov",
 | 
				
			||||||
    "sphinx",
 | 
					 | 
				
			||||||
    "sphinx-rtd-theme",
 | 
					 | 
				
			||||||
    "gunicorn",
 | 
					    "gunicorn",
 | 
				
			||||||
    "fontawesomefree",
 | 
					    "fontawesomefree",
 | 
				
			||||||
    "whitenoise",
 | 
					    "whitenoise",
 | 
				
			||||||
    "model_bakery",
 | 
					 | 
				
			||||||
    "markdown",
 | 
					    "markdown",
 | 
				
			||||||
    "Pillow",
 | 
					    "Pillow",
 | 
				
			||||||
    "django-registration",
 | 
					    "django-registration",
 | 
				
			||||||
@@ -39,7 +35,11 @@ dependencies = [
 | 
				
			|||||||
    "django-crispy-forms",
 | 
					    "django-crispy-forms",
 | 
				
			||||||
    "crispy-bootstrap4",
 | 
					    "crispy-bootstrap4",
 | 
				
			||||||
    "djangorestframework",
 | 
					    "djangorestframework",
 | 
				
			||||||
    "celery[redis]"
 | 
					    "celery[redis]",
 | 
				
			||||||
 | 
					    "drf-spectacular[sidecar]",
 | 
				
			||||||
 | 
					    "django-widget-tweaks",
 | 
				
			||||||
 | 
					    "django-super-deduper",
 | 
				
			||||||
 | 
					    "django-allauth[mfa]",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dynamic = ["version", "readme"]
 | 
					dynamic = ["version", "readme"]
 | 
				
			||||||
@@ -47,6 +47,14 @@ dynamic = ["version", "readme"]
 | 
				
			|||||||
[project.optional-dependencies]
 | 
					[project.optional-dependencies]
 | 
				
			||||||
develop = [
 | 
					develop = [
 | 
				
			||||||
    "pytest",
 | 
					    "pytest",
 | 
				
			||||||
 | 
					    "coverage",
 | 
				
			||||||
 | 
					    "model_bakery",
 | 
				
			||||||
 | 
					    "debug_toolbar",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					docs = [
 | 
				
			||||||
 | 
					    "sphinx",
 | 
				
			||||||
 | 
					    "sphinx-rtd-theme",
 | 
				
			||||||
 | 
					    "sphinx-autobuild"
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[project.urls]
 | 
					[project.urls]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										275
									
								
								scripts/upload_animal_shelters.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,275 @@
 | 
				
			|||||||
 | 
					import argparse
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					from types import SimpleNamespace
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import requests
 | 
				
			||||||
 | 
					# TODO: consider using OSMPythonTools instead of requests or overpass library
 | 
				
			||||||
 | 
					from osmtogeojson import osmtogeojson
 | 
				
			||||||
 | 
					from tqdm import tqdm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DEFAULT_OSM_DATA_FILE = "export.geojson"
 | 
				
			||||||
 | 
					# Search area must be the official name, e.g. "Germany" is not a valid area name in Overpass API
 | 
				
			||||||
 | 
					# Consider instead finding & using the code within the query itself, e.g. "ISO3166-1"="DE"
 | 
				
			||||||
 | 
					DEFAULT_OVERPASS_SEARCH_AREA = "Deutschland"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def parse_args():
 | 
				
			||||||
 | 
					    """Parse command-line arguments."""
 | 
				
			||||||
 | 
					    parser = argparse.ArgumentParser(
 | 
				
			||||||
 | 
					        description="Download animal shelter data from the Overpass API to the Notfellchen API.")
 | 
				
			||||||
 | 
					    parser.add_argument("--api-token", type=str, help="API token for authentication.")
 | 
				
			||||||
 | 
					    parser.add_argument("--area", type=str, help="Area to search for animal shelters (default: Deutschland).")
 | 
				
			||||||
 | 
					    parser.add_argument("--instance", type=str, help="API instance URL.")
 | 
				
			||||||
 | 
					    parser.add_argument("--data-file", type=str, help="Path to the GeoJSON file containing (only) animal shelters.")
 | 
				
			||||||
 | 
					    parser.add_argument("--use-cached", action='store_true', help="Use the stored GeoJSON file")
 | 
				
			||||||
 | 
					    return parser.parse_args()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_config():
 | 
				
			||||||
 | 
					    """Get configuration from environment variables or command-line arguments."""
 | 
				
			||||||
 | 
					    args = parse_args()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    api_token = args.api_token or os.getenv("NOTFELLCHEN_API_TOKEN")
 | 
				
			||||||
 | 
					    # TODO: document new environment variable NOTFELLCHEN_AREA
 | 
				
			||||||
 | 
					    area = args.area or os.getenv("NOTFELLCHEN_AREA", DEFAULT_OVERPASS_SEARCH_AREA)
 | 
				
			||||||
 | 
					    instance = args.instance or os.getenv("NOTFELLCHEN_INSTANCE")
 | 
				
			||||||
 | 
					    data_file = args.data_file or os.getenv("NOTFELLCHEN_DATA_FILE", DEFAULT_OSM_DATA_FILE)
 | 
				
			||||||
 | 
					    use_cached = args.use_cached or os.getenv("NOTFELLCHEN_USE_CACHED", False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not api_token or not instance:
 | 
				
			||||||
 | 
					        raise ValueError("API token and instance URL must be provided via environment variables or CLI arguments.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return api_token, area, instance, data_file, use_cached
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_or_none(data, key):
 | 
				
			||||||
 | 
					    if key in data["properties"].keys():
 | 
				
			||||||
 | 
					        return data["properties"][key]
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_or_empty(data, key):
 | 
				
			||||||
 | 
					    if key in data["properties"].keys():
 | 
				
			||||||
 | 
					        return data["properties"][key]
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        return ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def choose(keys, data, replace=False):
 | 
				
			||||||
 | 
					    for key in keys:
 | 
				
			||||||
 | 
					        if key in data.keys():
 | 
				
			||||||
 | 
					            if replace:
 | 
				
			||||||
 | 
					                return data[key].replace(" ", "").replace("-", "").replace("(", "").replace(")", "")
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                return data[key]
 | 
				
			||||||
 | 
					    return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def add(value, platform):
 | 
				
			||||||
 | 
					    if value != "":
 | 
				
			||||||
 | 
					        if value.find(platform) == -1:
 | 
				
			||||||
 | 
					            return f"https://www.{platform}.com/{value}"
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return value
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def https(value):
 | 
				
			||||||
 | 
					    if value is not None and value != "":
 | 
				
			||||||
 | 
					        value = value.replace("http://", "")
 | 
				
			||||||
 | 
					        if value.find("https") == -1:
 | 
				
			||||||
 | 
					            return f"https://{value}"
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return value
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def calc_coordinate_center(coordinates):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Calculates the center as the arithmetic mean of the list of coordinates.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Not perfect because earth is a sphere (citation needed) but good enough.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    if not coordinates:
 | 
				
			||||||
 | 
					        return None, None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    lon_sum = 0.0
 | 
				
			||||||
 | 
					    lat_sum = 0.0
 | 
				
			||||||
 | 
					    count = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for lon, lat in coordinates:
 | 
				
			||||||
 | 
					        lon_sum += lon
 | 
				
			||||||
 | 
					        lat_sum += lat
 | 
				
			||||||
 | 
					        count += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return lon_sum / count, lat_sum / count
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_center_coordinates(geometry):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Given a GeoJSON geometry dict, return (longitude, latitude)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    If a shape, calculate the center, else reurn the point
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    geom_type = geometry["type"]
 | 
				
			||||||
 | 
					    coordinates = geometry["coordinates"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if geom_type == "Point":
 | 
				
			||||||
 | 
					        return coordinates[0], coordinates[1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    elif geom_type == "LineString":
 | 
				
			||||||
 | 
					        return calc_coordinate_center(coordinates)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    elif geom_type == "Polygon":
 | 
				
			||||||
 | 
					        outer_ring = coordinates[0]
 | 
				
			||||||
 | 
					        return calc_coordinate_center(outer_ring)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        raise ValueError(f"Unsupported geometry type: {geom_type}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# TODO: take note of new get_overpass_result function which does the bulk of the new overpass query work
 | 
				
			||||||
 | 
					def get_overpass_result(area, data_file):
 | 
				
			||||||
 | 
					    """Build the Overpass query for fetching animal shelters in the specified area."""
 | 
				
			||||||
 | 
					    overpass_endpoint = "https://overpass-api.de/api/interpreter"
 | 
				
			||||||
 | 
					    overpass_query = f"""
 | 
				
			||||||
 | 
					        [out:json][timeout:25];
 | 
				
			||||||
 | 
					        area[name="{area}"]->.searchArea;
 | 
				
			||||||
 | 
					        nwr["amenity"="animal_shelter"](area.searchArea);
 | 
				
			||||||
 | 
					        out body;
 | 
				
			||||||
 | 
					        >;
 | 
				
			||||||
 | 
					        out skel qt;
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					    r = requests.get(overpass_endpoint, params={'data': overpass_query})
 | 
				
			||||||
 | 
					    if r.status_code == 200:
 | 
				
			||||||
 | 
					        rjson = r.json()
 | 
				
			||||||
 | 
					        result = osmtogeojson.process_osm_json(rjson)
 | 
				
			||||||
 | 
					        with open(data_file, 'w', encoding='utf-8') as f:
 | 
				
			||||||
 | 
					            json.dump(result, f, ensure_ascii=False)
 | 
				
			||||||
 | 
					        return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def add_if_available(base_data, keys, result):
 | 
				
			||||||
 | 
					    # Loads the data into the org if available
 | 
				
			||||||
 | 
					    for key in keys:
 | 
				
			||||||
 | 
					        if getattr(base_data, key) is not None:
 | 
				
			||||||
 | 
					            result[key] = getattr(base_data, key)
 | 
				
			||||||
 | 
					    return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def create_location(tierheim, instance, headers):
 | 
				
			||||||
 | 
					    location_data = {
 | 
				
			||||||
 | 
					        "place_id": tierheim["id"],
 | 
				
			||||||
 | 
					        "longitude": get_center_coordinates(tierheim["geometry"])[0],
 | 
				
			||||||
 | 
					        "latitude": get_center_coordinates(tierheim["geometry"])[1],
 | 
				
			||||||
 | 
					        "name": tierheim["properties"]["name"],
 | 
				
			||||||
 | 
					        "city": tierheim["properties"]["addr:city"],
 | 
				
			||||||
 | 
					        "housenumber": get_or_empty(tierheim, "addr:housenumber"),
 | 
				
			||||||
 | 
					        "postcode": get_or_empty(tierheim, "addr:postcode"),
 | 
				
			||||||
 | 
					        "street": get_or_empty(tierheim, "addr:street"),
 | 
				
			||||||
 | 
					        "countrycode": get_or_empty(tierheim, "addr:country"),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    location_result = requests.post(f"{instance}/api/locations/", json=location_data, headers=headers)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if location_result.status_code != 201:
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            print(
 | 
				
			||||||
 | 
					                f"Location for {tierheim["properties"]["name"]}:{location_result.status_code} {location_result.json()} not created")
 | 
				
			||||||
 | 
					        except requests.exceptions.JSONDecodeError:
 | 
				
			||||||
 | 
					            print(f"Location for {tierheim["properties"]["name"]} could not be created")
 | 
				
			||||||
 | 
					            exit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return location_result.json()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main():
 | 
				
			||||||
 | 
					    api_token, area, instance, data_file, use_cached = get_config()
 | 
				
			||||||
 | 
					    if not use_cached:
 | 
				
			||||||
 | 
					        # Query shelters
 | 
				
			||||||
 | 
					        overpass_result = get_overpass_result(area, data_file)
 | 
				
			||||||
 | 
					        if overpass_result is None:
 | 
				
			||||||
 | 
					            print("Error: get_overpass_result returned None")
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        with open(data_file, 'r', encoding='utf-8') as f:
 | 
				
			||||||
 | 
					            overpass_result = json.load(f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Set headers and endpoint
 | 
				
			||||||
 | 
					    endpoint = f"{instance}/api/organizations/"
 | 
				
			||||||
 | 
					    h = {'Authorization': f'Token {api_token}', "content-type": "application/json"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    tierheime = overpass_result["features"]
 | 
				
			||||||
 | 
					    stats = {"num_updated_orgs": 0,
 | 
				
			||||||
 | 
					             "num_inserted_orgs": 0}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for idx, tierheim in enumerate(tqdm(tierheime)):
 | 
				
			||||||
 | 
					        # Check if data is low quality
 | 
				
			||||||
 | 
					        if "name" not in tierheim["properties"].keys() or "addr:city" not in tierheim["properties"].keys():
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Load TH data in for easier accessing
 | 
				
			||||||
 | 
					        th_data = SimpleNamespace(
 | 
				
			||||||
 | 
					            name=tierheim["properties"]["name"],
 | 
				
			||||||
 | 
					            email=choose(("contact:email", "email"), tierheim["properties"]),
 | 
				
			||||||
 | 
					            phone_number=choose(("contact:phone", "phone"), tierheim["properties"], replace=True),
 | 
				
			||||||
 | 
					            fediverse_profile=get_or_none(tierheim, "contact:mastodon"),
 | 
				
			||||||
 | 
					            facebook=https(add(get_or_empty(tierheim, "contact:facebook"), "facebook")),
 | 
				
			||||||
 | 
					            instagram=https(add(get_or_empty(tierheim, "contact:instagram"), "instagram")),
 | 
				
			||||||
 | 
					            website=https(choose(("contact:website", "website"), tierheim["properties"])),
 | 
				
			||||||
 | 
					            description=get_or_none(tierheim, "opening_hours"),
 | 
				
			||||||
 | 
					            external_object_identifier=tierheim["id"],
 | 
				
			||||||
 | 
					            EXTERNAL_SOURCE_IDENTIFIER="OSM",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Define here for later
 | 
				
			||||||
 | 
					        optional_data = ["email", "phone_number", "website", "description", "fediverse_profile", "facebook",
 | 
				
			||||||
 | 
					                         "instagram"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Check if rescue organization exists
 | 
				
			||||||
 | 
					        search_data = {"external_source_identifier": "OSM",
 | 
				
			||||||
 | 
					                       "external_object_identifier": f"{tierheim["id"]}"}
 | 
				
			||||||
 | 
					        search_result = requests.get(f"{instance}/api/organizations", params=search_data, headers=h)
 | 
				
			||||||
 | 
					        # Rescue organization exits
 | 
				
			||||||
 | 
					        if search_result.status_code == 200:
 | 
				
			||||||
 | 
					            stats["num_updated_orgs"] += 1
 | 
				
			||||||
 | 
					            org_id = search_result.json()[0]["id"]
 | 
				
			||||||
 | 
					            logging.debug(f"{th_data.name} already exists as ID {org_id}.")
 | 
				
			||||||
 | 
					            org_patch_data = {"id": org_id,
 | 
				
			||||||
 | 
					                              "name": th_data.name}
 | 
				
			||||||
 | 
					            if search_result.json()[0]["location"] is None:
 | 
				
			||||||
 | 
					                location = create_location(tierheim, instance, h)
 | 
				
			||||||
 | 
					                org_patch_data["location"] = location["id"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            org_patch_data = add_if_available(th_data, optional_data, org_patch_data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            result = requests.patch(endpoint, json=org_patch_data, headers=h)
 | 
				
			||||||
 | 
					            if result.status_code != 200:
 | 
				
			||||||
 | 
					                logging.warning(f"Updating {tierheim['properties']['name']} failed:{result.status_code} {result.json()}")
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        # Rescue organization does not exist
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            stats["num_inserted_orgs"] += 1
 | 
				
			||||||
 | 
					            location = create_location(tierheim, instance, h)
 | 
				
			||||||
 | 
					            org_data = {"name": tierheim["properties"]["name"],
 | 
				
			||||||
 | 
					                        "external_object_identifier": f"{tierheim["id"]}",
 | 
				
			||||||
 | 
					                        "external_source_identifier": "OSM",
 | 
				
			||||||
 | 
					                        "location": location["id"]
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            org_data = add_if_available(th_data, optional_data, org_data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            result = requests.post(endpoint, json=org_data, headers=h)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if result.status_code != 201:
 | 
				
			||||||
 | 
					                print(f"{idx} {tierheim["properties"]["name"]}:{result.status_code} {result.json()}")
 | 
				
			||||||
 | 
					    print(f"Upload finished. Inserted {stats['num_inserted_orgs']} new orgs and updated {stats['num_updated_orgs']} orgs.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    main()
 | 
				
			||||||
@@ -1,34 +1,32 @@
 | 
				
			|||||||
import csv
 | 
					import csv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.contrib import admin
 | 
					from django.contrib import admin
 | 
				
			||||||
 | 
					from django.contrib.admin import EmptyFieldListFilter
 | 
				
			||||||
from django.http import HttpResponse
 | 
					from django.http import HttpResponse
 | 
				
			||||||
from django.utils.html import format_html
 | 
					from django.utils.html import format_html
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
from django.utils.http import urlencode
 | 
					from django.utils.http import urlencode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp
 | 
					from .models import Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
 | 
				
			||||||
 | 
					    SpeciesSpecificURL, ImportantLocation, SocialMediaPost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
 | 
					from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
 | 
				
			||||||
    Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions
 | 
					    Comment, Announcement, User, Subscriptions, Notification
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .tools.model_helpers import AdoptionNoticeStatusChoices
 | 
				
			||||||
class StatusInline(admin.StackedInline):
 | 
					 | 
				
			||||||
    model = AdoptionNoticeStatus
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@admin.register(AdoptionNotice)
 | 
					@admin.register(AdoptionNotice)
 | 
				
			||||||
class AdoptionNoticeAdmin(admin.ModelAdmin):
 | 
					class AdoptionNoticeAdmin(admin.ModelAdmin):
 | 
				
			||||||
    search_fields = ("name__icontains", "description__icontains")
 | 
					    search_fields = ("name__icontains", "description__icontains")
 | 
				
			||||||
    list_filter = ("owner",)
 | 
					    list_filter = ("owner",)
 | 
				
			||||||
    inlines = [
 | 
					 | 
				
			||||||
        StatusInline,
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
    actions = ("activate",)
 | 
					    actions = ("activate",)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def activate(self, request, queryset):
 | 
					    def activate(self, request, queryset):
 | 
				
			||||||
        for obj in queryset:
 | 
					        for obj in queryset:
 | 
				
			||||||
            obj.set_active()
 | 
					            obj.adoption_notice_status = AdoptionNoticeStatusChoices.Active.SEARCHING
 | 
				
			||||||
 | 
					            obj.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    activate.short_description = _("Ausgewählte Vermittlungen aktivieren")
 | 
					    activate.short_description = _("Ausgewählte Vermittlungen aktivieren")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -66,6 +64,7 @@ class UserAdmin(admin.ModelAdmin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    export_as_csv.short_description = _("Ausgewählte User exportieren")
 | 
					    export_as_csv.short_description = _("Ausgewählte User exportieren")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _reported_content_link(obj):
 | 
					def _reported_content_link(obj):
 | 
				
			||||||
    reported_content = obj.reported_content
 | 
					    reported_content = obj.reported_content
 | 
				
			||||||
    return format_html(f'<a href="{reported_content.get_absolute_url}">{reported_content}</a>')
 | 
					    return format_html(f'<a href="{reported_content.get_absolute_url}">{reported_content}</a>')
 | 
				
			||||||
@@ -93,30 +92,91 @@ class ReportAdoptionNoticeAdmin(admin.ModelAdmin):
 | 
				
			|||||||
    reported_content_link.short_description = "Reported Content"
 | 
					    reported_content_link.short_description = "Reported Content"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SpeciesSpecificURLInline(admin.StackedInline):
 | 
				
			||||||
 | 
					    model = SpeciesSpecificURL
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@admin.register(RescueOrganization)
 | 
					@admin.register(RescueOrganization)
 | 
				
			||||||
class RescueOrganizationAdmin(admin.ModelAdmin):
 | 
					class RescueOrganizationAdmin(admin.ModelAdmin):
 | 
				
			||||||
    search_fields = ("name__icontains",)
 | 
					    search_fields = ("name", "description", "internal_comment", "location_string", "location__city")
 | 
				
			||||||
    list_display = ("name", "trusted", "allows_using_materials", "website")
 | 
					    list_display = ("name", "trusted", "allows_using_materials", "website")
 | 
				
			||||||
    list_filter = ("allows_using_materials", "trusted",)
 | 
					    list_filter = ("allows_using_materials", "trusted", ("external_source_identifier", EmptyFieldListFilter))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    inlines = [
 | 
				
			||||||
 | 
					        SpeciesSpecificURLInline,
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@admin.register(Text)
 | 
					@admin.register(Text)
 | 
				
			||||||
class TextAdmin(admin.ModelAdmin):
 | 
					class TextAdmin(admin.ModelAdmin):
 | 
				
			||||||
    search_fields = ("title__icontains", "text_code__icontains",)
 | 
					    search_fields = ("title__icontains", "text_code__icontains",)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@admin.register(Comment)
 | 
					@admin.register(Comment)
 | 
				
			||||||
class CommentAdmin(admin.ModelAdmin):
 | 
					class CommentAdmin(admin.ModelAdmin):
 | 
				
			||||||
    list_filter = ("user",)
 | 
					    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.register(Log)
 | 
				
			||||||
 | 
					class LogAdmin(admin.ModelAdmin):
 | 
				
			||||||
 | 
					    ordering = ["-created_at"]
 | 
				
			||||||
 | 
					    list_filter = ("action",)
 | 
				
			||||||
 | 
					    list_display = ("action", "user", "created_at")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
admin.site.register(Animal)
 | 
					admin.site.register(Animal)
 | 
				
			||||||
admin.site.register(Species)
 | 
					admin.site.register(Species)
 | 
				
			||||||
admin.site.register(Location)
 | 
					 | 
				
			||||||
admin.site.register(Rule)
 | 
					admin.site.register(Rule)
 | 
				
			||||||
admin.site.register(Image)
 | 
					admin.site.register(Image)
 | 
				
			||||||
admin.site.register(ModerationAction)
 | 
					admin.site.register(ModerationAction)
 | 
				
			||||||
admin.site.register(Language)
 | 
					admin.site.register(Language)
 | 
				
			||||||
admin.site.register(Announcement)
 | 
					admin.site.register(Announcement)
 | 
				
			||||||
admin.site.register(AdoptionNoticeStatus)
 | 
					 | 
				
			||||||
admin.site.register(Subscriptions)
 | 
					admin.site.register(Subscriptions)
 | 
				
			||||||
admin.site.register(Log)
 | 
					 | 
				
			||||||
admin.site.register(Timestamp)
 | 
					admin.site.register(Timestamp)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										33
									
								
								src/fellchensammlung/api/renderers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					from rest_framework.renderers import BaseRenderer
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GeoJSONRenderer(BaseRenderer):
 | 
				
			||||||
 | 
					    media_type = 'application/json'
 | 
				
			||||||
 | 
					    format = 'geojson'
 | 
				
			||||||
 | 
					    charset = 'utf-8'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def render(self, data, accepted_media_type=None, renderer_context=None):
 | 
				
			||||||
 | 
					        features = []
 | 
				
			||||||
 | 
					        for item in data:
 | 
				
			||||||
 | 
					            coords = item["coordinates"]
 | 
				
			||||||
 | 
					            if coords:
 | 
				
			||||||
 | 
					                feature = {
 | 
				
			||||||
 | 
					                    "type": "Feature",
 | 
				
			||||||
 | 
					                    "geometry": {
 | 
				
			||||||
 | 
					                        "type": "Point",
 | 
				
			||||||
 | 
					                        "coordinates": coords
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    "properties": {
 | 
				
			||||||
 | 
					                        k: v for k, v in item.items()
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    "id": f"{item['id']}"
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                features.append(feature)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        geojson = {
 | 
				
			||||||
 | 
					            "type": "FeatureCollection",
 | 
				
			||||||
 | 
					            "generator": "notfellchen",
 | 
				
			||||||
 | 
					            "features": features
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return json.dumps(geojson)
 | 
				
			||||||
@@ -1,12 +1,135 @@
 | 
				
			|||||||
from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image
 | 
					from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image, Location
 | 
				
			||||||
from rest_framework import serializers
 | 
					from rest_framework import serializers
 | 
				
			||||||
 | 
					import math
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ImageSerializer(serializers.ModelSerializer):
 | 
				
			||||||
 | 
					    width = serializers.SerializerMethodField()
 | 
				
			||||||
 | 
					    height = serializers.SerializerMethodField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = Image
 | 
				
			||||||
 | 
					        fields = ['id', 'image', 'alt_text', 'width', 'height']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_width(self, obj):
 | 
				
			||||||
 | 
					        return obj.image.width
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_height(self, obj):
 | 
				
			||||||
 | 
					        return obj.image.height
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
 | 
					class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
 | 
				
			||||||
 | 
					    location = serializers.PrimaryKeyRelatedField(
 | 
				
			||||||
 | 
					        queryset=Location.objects.all(),
 | 
				
			||||||
 | 
					        required=False,
 | 
				
			||||||
 | 
					        allow_null=True
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    location_details = serializers.StringRelatedField(source='location', read_only=True)
 | 
				
			||||||
 | 
					    organization = serializers.PrimaryKeyRelatedField(
 | 
				
			||||||
 | 
					        queryset=RescueOrganization.objects.all(),
 | 
				
			||||||
 | 
					        required=False,
 | 
				
			||||||
 | 
					        allow_null=True
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    organization = serializers.PrimaryKeyRelatedField(
 | 
				
			||||||
 | 
					        queryset=RescueOrganization.objects.all(),
 | 
				
			||||||
 | 
					        required=False,
 | 
				
			||||||
 | 
					        allow_null=True
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    url = serializers.SerializerMethodField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    photos = ImageSerializer(many=True, read_only=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_url(self, obj):
 | 
				
			||||||
 | 
					        return obj.get_full_url()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = AdoptionNotice
 | 
					        model = AdoptionNotice
 | 
				
			||||||
        fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information",
 | 
					        fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information",
 | 
				
			||||||
                  "group_only"]
 | 
					                  "group_only", "location", "location_details", "organization", "photos", "adoption_notice_status",
 | 
				
			||||||
 | 
					                  "url"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AdoptionNoticeGeoJSONSerializer(serializers.ModelSerializer):
 | 
				
			||||||
 | 
					    species = serializers.SerializerMethodField()
 | 
				
			||||||
 | 
					    title = serializers.CharField(source='name')
 | 
				
			||||||
 | 
					    url = serializers.SerializerMethodField()
 | 
				
			||||||
 | 
					    location_hr = serializers.SerializerMethodField()
 | 
				
			||||||
 | 
					    coordinates = serializers.SerializerMethodField()
 | 
				
			||||||
 | 
					    image_url = serializers.SerializerMethodField()
 | 
				
			||||||
 | 
					    image_alt = serializers.SerializerMethodField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = AdoptionNotice
 | 
				
			||||||
 | 
					        fields = ('id', 'species', 'title', 'description', 'url', 'location_hr', 'coordinates', 'image_url',
 | 
				
			||||||
 | 
					                  'image_alt')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_species(self, obj):
 | 
				
			||||||
 | 
					        return "rat"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_url(self, obj):
 | 
				
			||||||
 | 
					        return obj.get_absolute_url()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_image_url(self, obj):
 | 
				
			||||||
 | 
					        photo = obj.get_photo()
 | 
				
			||||||
 | 
					        if photo is not None:
 | 
				
			||||||
 | 
					            return obj.get_photo().image.url
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_image_alt(self, obj):
 | 
				
			||||||
 | 
					        photo = obj.get_photo()
 | 
				
			||||||
 | 
					        if photo is not None:
 | 
				
			||||||
 | 
					            return obj.get_photo().alt_text
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_coordinates(self, obj):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Coordinates are randomly moved around real location, roughly in a circle. The object id is used as angle so that
 | 
				
			||||||
 | 
					        points are always displayed at the same location (as if they were a seed for a random function).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        It's not exactly a circle, because the earth is round.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if obj.location:
 | 
				
			||||||
 | 
					            longitude_addition = math.sin(obj.id) / 2000
 | 
				
			||||||
 | 
					            latitude_addition = math.cos(obj.id) / 2000
 | 
				
			||||||
 | 
					            return [obj.location.longitude + longitude_addition, obj.location.latitude + latitude_addition]
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_location_hr(self, obj):
 | 
				
			||||||
 | 
					        if obj.location:
 | 
				
			||||||
 | 
					            return f"{obj.location}"
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RescueOrgeGeoJSONSerializer(serializers.ModelSerializer):
 | 
				
			||||||
 | 
					    name = serializers.CharField()
 | 
				
			||||||
 | 
					    url = serializers.SerializerMethodField()
 | 
				
			||||||
 | 
					    location_hr = serializers.SerializerMethodField()
 | 
				
			||||||
 | 
					    coordinates = serializers.SerializerMethodField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = AdoptionNotice
 | 
				
			||||||
 | 
					        fields = ('id', 'name', 'description', 'url', 'location_hr', 'coordinates')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_url(self, obj):
 | 
				
			||||||
 | 
					        return obj.get_absolute_url()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_coordinates(self, obj):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Coordinates are randomly moved around real location, roughly in a circle. The object id is used as angle so that
 | 
				
			||||||
 | 
					        points are always displayed at the same location (as if they were a seed for a random function).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        It's not exactly a circle, because the earth is round.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if obj.location:
 | 
				
			||||||
 | 
					            return [obj.location.longitude, obj.location.latitude]
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_location_hr(self, obj):
 | 
				
			||||||
 | 
					        if obj.location.city:
 | 
				
			||||||
 | 
					            return f"{obj.location.city}"
 | 
				
			||||||
 | 
					        elif obj.location:
 | 
				
			||||||
 | 
					            return f"{obj.location}"
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AnimalCreateSerializer(serializers.ModelSerializer):
 | 
					class AnimalCreateSerializer(serializers.ModelSerializer):
 | 
				
			||||||
@@ -46,3 +169,9 @@ class SpeciesSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Species
 | 
					        model = Species
 | 
				
			||||||
        fields = "__all__"
 | 
					        fields = "__all__"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LocationSerializer(serializers.ModelSerializer):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = Location
 | 
				
			||||||
 | 
					        fields = "__all__"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,16 +1,21 @@
 | 
				
			|||||||
from django.urls import path
 | 
					from django.urls import path
 | 
				
			||||||
from .views import (
 | 
					from .views import (
 | 
				
			||||||
    AdoptionNoticeApiView,
 | 
					    AdoptionNoticeApiView,
 | 
				
			||||||
    AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView
 | 
					    AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView, LocationApiView,
 | 
				
			||||||
 | 
					    AdoptionNoticeGeoJSONView, RescueOrgGeoJSONView, AdoptionNoticePerOrgApiView
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
urlpatterns = [
 | 
					urlpatterns = [
 | 
				
			||||||
    path("adoption_notice", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-list"),
 | 
					    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("adoption_notice/<int:id>/", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-detail"),
 | 
				
			||||||
    path("animals/", AnimalApiView.as_view(), name="api-animal-list"),
 | 
					    path("animals/", AnimalApiView.as_view(), name="api-animal-list"),
 | 
				
			||||||
    path("animals/<int:id>/", AnimalApiView.as_view(), name="api-animal-detail"),
 | 
					    path("animals/<int:id>/", AnimalApiView.as_view(), name="api-animal-detail"),
 | 
				
			||||||
    path("organizations/", RescueOrganizationApiView.as_view(), name="api-organization-list"),
 | 
					    path("organizations/", RescueOrganizationApiView.as_view(), name="api-organization-list"),
 | 
				
			||||||
 | 
					    path("organizations.geojson", RescueOrgGeoJSONView.as_view(), name="api-organization-list-geojson"),
 | 
				
			||||||
    path("organizations/<int:id>/", RescueOrganizationApiView.as_view(), name="api-organization-detail"),
 | 
					    path("organizations/<int:id>/", RescueOrganizationApiView.as_view(), name="api-organization-detail"),
 | 
				
			||||||
 | 
					    path("organizations/<int:id>/adoption-notices", AdoptionNoticePerOrgApiView.as_view(), name="api-organization-adoption-notices"),
 | 
				
			||||||
    path("images/", AddImageApiView.as_view(), name="api-add-image"),
 | 
					    path("images/", AddImageApiView.as_view(), name="api-add-image"),
 | 
				
			||||||
    path("species/", SpeciesApiView.as_view(), name="api-species-list"),
 | 
					    path("species/", SpeciesApiView.as_view(), name="api-species-list"),
 | 
				
			||||||
 | 
					    path("locations/", LocationApiView.as_view(), name="api-locations-list"),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,28 +1,43 @@
 | 
				
			|||||||
 | 
					from django.db.models import Q
 | 
				
			||||||
 | 
					from drf_spectacular.types import OpenApiTypes
 | 
				
			||||||
 | 
					from rest_framework.generics import ListAPIView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from fellchensammlung.api.serializers import LocationSerializer, AdoptionNoticeGeoJSONSerializer
 | 
				
			||||||
from rest_framework.views import APIView
 | 
					from rest_framework.views import APIView
 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
from rest_framework import status
 | 
					 | 
				
			||||||
from rest_framework import permissions
 | 
					 | 
				
			||||||
from rest_framework.decorators import api_view, permission_classes
 | 
					 | 
				
			||||||
from rest_framework.permissions import IsAuthenticated
 | 
					 | 
				
			||||||
from django.db import transaction
 | 
					from django.db import transaction
 | 
				
			||||||
from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel
 | 
					from fellchensammlung.models import Log, TrustLevel, Location, AdoptionNoticeStatusChoices
 | 
				
			||||||
from fellchensammlung.tasks import add_adoption_notice_location
 | 
					from fellchensammlung.tasks import post_adoption_notice_save, post_rescue_org_save
 | 
				
			||||||
from rest_framework import status
 | 
					from rest_framework import status, serializers
 | 
				
			||||||
from rest_framework.permissions import IsAuthenticated
 | 
					from rest_framework.permissions import IsAuthenticated
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .renderers import GeoJSONRenderer
 | 
				
			||||||
from .serializers import (
 | 
					from .serializers import (
 | 
				
			||||||
    AnimalGetSerializer,
 | 
					    AnimalGetSerializer,
 | 
				
			||||||
    AnimalCreateSerializer,
 | 
					    AnimalCreateSerializer,
 | 
				
			||||||
    RescueOrganizationSerializer,
 | 
					    RescueOrgeGeoJSONSerializer,
 | 
				
			||||||
    AdoptionNoticeSerializer,
 | 
					    AdoptionNoticeSerializer,
 | 
				
			||||||
    ImageCreateSerializer,
 | 
					    ImageCreateSerializer,
 | 
				
			||||||
    SpeciesSerializer,
 | 
					    SpeciesSerializer, RescueOrganizationSerializer,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image
 | 
					from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image
 | 
				
			||||||
 | 
					from drf_spectacular.utils import extend_schema, inline_serializer, OpenApiParameter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AdoptionNoticeApiView(APIView):
 | 
					class AdoptionNoticeApiView(APIView):
 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @extend_schema(
 | 
				
			||||||
 | 
					        parameters=[
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                'name': 'id',
 | 
				
			||||||
 | 
					                'required': False,
 | 
				
			||||||
 | 
					                'description': 'ID of the adoption notice to retrieve.',
 | 
				
			||||||
 | 
					                'type': int
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        responses={200: AdoptionNoticeSerializer(many=True)}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    def get(self, request, *args, **kwargs):
 | 
					    def get(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Retrieve adoption notices with their related animals and images.
 | 
					        Retrieve adoption notices with their related animals and images.
 | 
				
			||||||
@@ -40,30 +55,34 @@ class AdoptionNoticeApiView(APIView):
 | 
				
			|||||||
        return Response(serializer.data, status=status.HTTP_200_OK)
 | 
					        return Response(serializer.data, status=status.HTTP_200_OK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @transaction.atomic
 | 
					    @transaction.atomic
 | 
				
			||||||
 | 
					    @extend_schema(
 | 
				
			||||||
 | 
					        request=AdoptionNoticeSerializer,
 | 
				
			||||||
 | 
					        responses={201: 'Adoption notice created successfully!'}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					    def post(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        API view to add an adoption notice.b
 | 
					        API view to add an adoption notice.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        serializer = AdoptionNoticeSerializer(data=request.data, context={'request': request})
 | 
					        serializer = AdoptionNoticeSerializer(data=request.data, context={'request': request})
 | 
				
			||||||
        if not serializer.is_valid():
 | 
					        if not serializer.is_valid():
 | 
				
			||||||
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 | 
					            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        adoption_notice = serializer.save(owner=request.user)
 | 
					        adoption_notice = serializer.save(owner=request.user_to_notify)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Add the location
 | 
					        # Add the location
 | 
				
			||||||
        add_adoption_notice_location.delay_on_commit(adoption_notice.pk)
 | 
					        post_adoption_notice_save.delay_on_commit(adoption_notice.pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Only set active when user has trust level moderator or higher
 | 
					        # Only set active when user has trust level moderator or higher
 | 
				
			||||||
        if request.user.trust_level >= TrustLevel.MODERATOR:
 | 
					        if request.user_to_notify.trust_level >= TrustLevel.MODERATOR:
 | 
				
			||||||
            adoption_notice.set_active()
 | 
					            adoption_notice.adoption_notice_status = AdoptionNoticeStatusChoices.Active.SEARCHING
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            adoption_notice.set_unchecked()
 | 
					            adoption_notice.adoption_notice_status = AdoptionNoticeStatusChoices.AwaitingAction.WAITING_FOR_REVIEW
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Log the action
 | 
					        # Log the action
 | 
				
			||||||
        Log.objects.create(
 | 
					        Log.objects.create(
 | 
				
			||||||
            user=request.user,
 | 
					            user=request.user_to_notify,
 | 
				
			||||||
            action="add_adoption_notice",
 | 
					            action="add_adoption_notice",
 | 
				
			||||||
            text=f"{request.user} added adoption notice {adoption_notice.pk} via API",
 | 
					            text=f"{request.user_to_notify} added adoption notice {adoption_notice.pk} via API",
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Return success response with new adoption notice details
 | 
					        # Return success response with new adoption notice details
 | 
				
			||||||
@@ -76,6 +95,9 @@ class AdoptionNoticeApiView(APIView):
 | 
				
			|||||||
class AnimalApiView(APIView):
 | 
					class AnimalApiView(APIView):
 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @extend_schema(
 | 
				
			||||||
 | 
					        responses=AnimalGetSerializer
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    def get(self, request, *args, **kwargs):
 | 
					    def get(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Get list of animals or a specific animal by ID.
 | 
					        Get list of animals or a specific animal by ID.
 | 
				
			||||||
@@ -92,6 +114,16 @@ class AnimalApiView(APIView):
 | 
				
			|||||||
        serializer = AnimalGetSerializer(animals, many=True, context={"request": request})
 | 
					        serializer = AnimalGetSerializer(animals, many=True, context={"request": request})
 | 
				
			||||||
        return Response(serializer.data, status=status.HTTP_200_OK)
 | 
					        return Response(serializer.data, status=status.HTTP_200_OK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @transaction.atomic
 | 
				
			||||||
 | 
					    @extend_schema(
 | 
				
			||||||
 | 
					        request=AnimalCreateSerializer,
 | 
				
			||||||
 | 
					        responses={201: inline_serializer(
 | 
				
			||||||
 | 
					            name='Animal',
 | 
				
			||||||
 | 
					            fields={
 | 
				
			||||||
 | 
					                'id': serializers.IntegerField(),
 | 
				
			||||||
 | 
					                "message": serializers.Field()}),
 | 
				
			||||||
 | 
					            400: "json"}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    @transaction.atomic
 | 
					    @transaction.atomic
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					    def post(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -99,7 +131,7 @@ class AnimalApiView(APIView):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        serializer = AnimalCreateSerializer(data=request.data, context={"request": request})
 | 
					        serializer = AnimalCreateSerializer(data=request.data, context={"request": request})
 | 
				
			||||||
        if serializer.is_valid():
 | 
					        if serializer.is_valid():
 | 
				
			||||||
            animal = serializer.save(owner=request.user)
 | 
					            animal = serializer.save(owner=request.user_to_notify)
 | 
				
			||||||
            return Response(
 | 
					            return Response(
 | 
				
			||||||
                {"message": "Animal created successfully!", "id": animal.id},
 | 
					                {"message": "Animal created successfully!", "id": animal.id},
 | 
				
			||||||
                status=status.HTTP_201_CREATED,
 | 
					                status=status.HTTP_201_CREATED,
 | 
				
			||||||
@@ -110,11 +142,52 @@ class AnimalApiView(APIView):
 | 
				
			|||||||
class RescueOrganizationApiView(APIView):
 | 
					class RescueOrganizationApiView(APIView):
 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					    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):
 | 
					    def get(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Get list of rescue organizations or a specific organization by ID.
 | 
					        Get list of rescue organizations or a specific organization by ID or get a list with available filters for
 | 
				
			||||||
 | 
					        - external_object_identifier
 | 
				
			||||||
 | 
					        - external_source_identifier
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        org_id = kwargs.get("id")
 | 
					        org_id = request.query_params.get("id")
 | 
				
			||||||
 | 
					        external_object_identifier = request.query_params.get("external_object_identifier")
 | 
				
			||||||
 | 
					        external_source_identifier = request.query_params.get("external_source_identifier")
 | 
				
			||||||
 | 
					        search_query = request.query_params.get("search")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if org_id:
 | 
					        if org_id:
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                organization = RescueOrganization.objects.get(pk=org_id)
 | 
					                organization = RescueOrganization.objects.get(pk=org_id)
 | 
				
			||||||
@@ -122,15 +195,87 @@ class RescueOrganizationApiView(APIView):
 | 
				
			|||||||
                return Response(serializer.data, status=status.HTTP_200_OK)
 | 
					                return Response(serializer.data, status=status.HTTP_200_OK)
 | 
				
			||||||
            except RescueOrganization.DoesNotExist:
 | 
					            except RescueOrganization.DoesNotExist:
 | 
				
			||||||
                return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
 | 
					                return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        organizations = RescueOrganization.objects.all()
 | 
					        organizations = RescueOrganization.objects.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if external_object_identifier:
 | 
				
			||||||
 | 
					            if external_object_identifier == "None":
 | 
				
			||||||
 | 
					                external_object_identifier = None
 | 
				
			||||||
 | 
					            organizations = organizations.filter(external_object_identifier=external_object_identifier)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if external_source_identifier:
 | 
				
			||||||
 | 
					            if external_source_identifier == "None":
 | 
				
			||||||
 | 
					                external_source_identifier = None
 | 
				
			||||||
 | 
					            organizations = organizations.filter(external_source_identifier=external_source_identifier)
 | 
				
			||||||
 | 
					        if search_query:
 | 
				
			||||||
 | 
					            organizations = organizations.filter(
 | 
				
			||||||
 | 
					                Q(name__icontains=search_query) |
 | 
				
			||||||
 | 
					                Q(location_string__icontains=search_query) |
 | 
				
			||||||
 | 
					                Q(location__name__icontains=search_query) |
 | 
				
			||||||
 | 
					                Q(location__city__icontains=search_query)
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        if organizations.count() == 0:
 | 
				
			||||||
 | 
					            return Response({"error": "No organizations found."}, status=status.HTTP_404_NOT_FOUND)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        serializer = RescueOrganizationSerializer(organizations, many=True, context={"request": request})
 | 
					        serializer = RescueOrganizationSerializer(organizations, many=True, context={"request": request})
 | 
				
			||||||
        return Response(serializer.data, status=status.HTTP_200_OK)
 | 
					        return Response(serializer.data, status=status.HTTP_200_OK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @transaction.atomic
 | 
				
			||||||
 | 
					    @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):
 | 
					class AddImageApiView(APIView):
 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @transaction.atomic
 | 
					    @transaction.atomic
 | 
				
			||||||
 | 
					    @extend_schema(
 | 
				
			||||||
 | 
					        request=ImageCreateSerializer,
 | 
				
			||||||
 | 
					        responses={201: 'Image added successfully!'}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					    def post(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Add an image to an animal or adoption notice.
 | 
					        Add an image to an animal or adoption notice.
 | 
				
			||||||
@@ -139,13 +284,13 @@ class AddImageApiView(APIView):
 | 
				
			|||||||
        if serializer.is_valid():
 | 
					        if serializer.is_valid():
 | 
				
			||||||
            if serializer.validated_data["attach_to_type"] == "animal":
 | 
					            if serializer.validated_data["attach_to_type"] == "animal":
 | 
				
			||||||
                object_to_attach_to = Animal.objects.get(id=serializer.validated_data["attach_to"])
 | 
					                object_to_attach_to = Animal.objects.get(id=serializer.validated_data["attach_to"])
 | 
				
			||||||
            elif serializer.fields["attach_to_type"] == "adoption_notice":
 | 
					            elif serializer.validated_data["attach_to_type"] == "adoption_notice":
 | 
				
			||||||
                object_to_attach_to = AdoptionNotice.objects.get(id=serializer.validated_data["attach_to"])
 | 
					                object_to_attach_to = AdoptionNotice.objects.get(id=serializer.validated_data["attach_to"])
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                raise ValueError("Unknown attach_to_type given, should not happen. Check serializer")
 | 
					                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_type', None)
 | 
				
			||||||
            serializer.validated_data.pop('attach_to', None)
 | 
					            serializer.validated_data.pop('attach_to', None)
 | 
				
			||||||
            image = serializer.save(owner=request.user)
 | 
					            image = serializer.save(owner=request.user_to_notify)
 | 
				
			||||||
            object_to_attach_to.photos.add(image)
 | 
					            object_to_attach_to.photos.add(image)
 | 
				
			||||||
            return Response(
 | 
					            return Response(
 | 
				
			||||||
                {"message": "Image added successfully!", "id": image.id},
 | 
					                {"message": "Image added successfully!", "id": image.id},
 | 
				
			||||||
@@ -157,6 +302,9 @@ class AddImageApiView(APIView):
 | 
				
			|||||||
class SpeciesApiView(APIView):
 | 
					class SpeciesApiView(APIView):
 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @extend_schema(
 | 
				
			||||||
 | 
					        responses={200: SpeciesSerializer(many=True)}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    def get(self, request, *args, **kwargs):
 | 
					    def get(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Retrieve a list of species.
 | 
					        Retrieve a list of species.
 | 
				
			||||||
@@ -164,3 +312,141 @@ class SpeciesApiView(APIView):
 | 
				
			|||||||
        species = Species.objects.all()
 | 
					        species = Species.objects.all()
 | 
				
			||||||
        serializer = SpeciesSerializer(species, many=True, context={"request": request})
 | 
					        serializer = SpeciesSerializer(species, many=True, context={"request": request})
 | 
				
			||||||
        return Response(serializer.data, status=status.HTTP_200_OK)
 | 
					        return Response(serializer.data, status=status.HTTP_200_OK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class LocationApiView(APIView):
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @extend_schema(
 | 
				
			||||||
 | 
					        parameters=[
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                'name': 'id',
 | 
				
			||||||
 | 
					                'required': False,
 | 
				
			||||||
 | 
					                'description': 'ID of the location to retrieve.',
 | 
				
			||||||
 | 
					                'type': int
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        responses={200: LocationSerializer(many=True)}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    def get(self, request, *args, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Retrieve a location
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        location_id = kwargs.get("id")
 | 
				
			||||||
 | 
					        if location_id:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                location = Location.objects.get(pk=location_id)
 | 
				
			||||||
 | 
					                serializer = LocationSerializer(location, context={"request": request})
 | 
				
			||||||
 | 
					                return Response(serializer.data, status=status.HTTP_200_OK)
 | 
				
			||||||
 | 
					            except Location.DoesNotExist:
 | 
				
			||||||
 | 
					                return Response({"error": "Location not found."}, status=status.HTTP_404_NOT_FOUND)
 | 
				
			||||||
 | 
					        locations = Location.objects.all()
 | 
				
			||||||
 | 
					        serializer = LocationSerializer(locations, many=True, context={"request": request})
 | 
				
			||||||
 | 
					        return Response(serializer.data, status=status.HTTP_200_OK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @transaction.atomic
 | 
				
			||||||
 | 
					    @extend_schema(
 | 
				
			||||||
 | 
					        request=LocationSerializer,
 | 
				
			||||||
 | 
					        responses={201: 'Location created successfully!'}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    def post(self, request, *args, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        API view to add a location
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        serializer = LocationSerializer(data=request.data, context={'request': request})
 | 
				
			||||||
 | 
					        if not serializer.is_valid():
 | 
				
			||||||
 | 
					            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        location = serializer.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Log the action
 | 
				
			||||||
 | 
					        Log.objects.create(
 | 
				
			||||||
 | 
					            user=request.user,
 | 
				
			||||||
 | 
					            action="add_location",
 | 
				
			||||||
 | 
					            text=f"{request.user} added adoption notice {location.pk} via API",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Return success response with new adoption notice details
 | 
				
			||||||
 | 
					        return Response(
 | 
				
			||||||
 | 
					            {"message": "Location created successfully!", "id": location.pk},
 | 
				
			||||||
 | 
					            status=status.HTTP_201_CREATED,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AdoptionNoticeGeoJSONView(ListAPIView):
 | 
				
			||||||
 | 
					    queryset = AdoptionNotice.objects.select_related('location').filter(location__isnull=False).filter(
 | 
				
			||||||
 | 
					        adoption_notice_status__in=AdoptionNoticeStatusChoices.Active.values)
 | 
				
			||||||
 | 
					    serializer_class = AdoptionNoticeGeoJSONSerializer
 | 
				
			||||||
 | 
					    renderer_classes = [GeoJSONRenderer]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RescueOrgGeoJSONView(ListAPIView):
 | 
				
			||||||
 | 
					    queryset = RescueOrganization.objects.select_related('location').filter(location__isnull=False)
 | 
				
			||||||
 | 
					    serializer_class = RescueOrgeGeoJSONSerializer
 | 
				
			||||||
 | 
					    renderer_classes = [GeoJSONRenderer]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AdoptionNoticePerOrgApiView(APIView):
 | 
				
			||||||
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @extend_schema(
 | 
				
			||||||
 | 
					        parameters=[
 | 
				
			||||||
 | 
					            OpenApiParameter(
 | 
				
			||||||
 | 
					                name='id',
 | 
				
			||||||
 | 
					                required=False,
 | 
				
			||||||
 | 
					                description='ID of the rescue organization from which to retrieve adoption notices.',
 | 
				
			||||||
 | 
					                type=OpenApiTypes.INT
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            OpenApiParameter(
 | 
				
			||||||
 | 
					                name='in_hierarchy',
 | 
				
			||||||
 | 
					                type=OpenApiTypes.BOOL,
 | 
				
			||||||
 | 
					                required=False,
 | 
				
			||||||
 | 
					                description='Show all Adoption Notices in hierarchy.',
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            OpenApiParameter(
 | 
				
			||||||
 | 
					                name='status',
 | 
				
			||||||
 | 
					                type=OpenApiTypes.STR,
 | 
				
			||||||
 | 
					                required=False,
 | 
				
			||||||
 | 
					                description='Show all Adoption Notices in a certain status. Comma separated list of values e.g. '
 | 
				
			||||||
 | 
					                            '"active,closed"',
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        responses={200: AdoptionNoticeSerializer(many=True)}
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    def get(self, request, *args, **kwargs):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Retrieve adoption notices with their related animals and images.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        org_id = kwargs.get("id")
 | 
				
			||||||
 | 
					        in_hierarchy = request.query_params.get("in_hierarchy")
 | 
				
			||||||
 | 
					        an_status = request.query_params.get("status")
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            org = RescueOrganization.objects.get(id=org_id)
 | 
				
			||||||
 | 
					        except RescueOrganization.DoesNotExist:
 | 
				
			||||||
 | 
					            return Response({"error": "Rescue Organization notice not found."}, status=status.HTTP_404_NOT_FOUND)
 | 
				
			||||||
 | 
					        if in_hierarchy:
 | 
				
			||||||
 | 
					            adoption_notices = org.adoption_notices_in_hierarchy
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            adoption_notices = AdoptionNotice.objects.filter(organization=org)
 | 
				
			||||||
 | 
					        if an_status:
 | 
				
			||||||
 | 
					            status_list = an_status.lower().strip().split(",")
 | 
				
			||||||
 | 
					            temporary_an_storage = []
 | 
				
			||||||
 | 
					            if "active" in status_list:
 | 
				
			||||||
 | 
					                active_ans = [adoption_notice for adoption_notice in adoption_notices if
 | 
				
			||||||
 | 
					                              adoption_notice.adoption_notice_status in AdoptionNoticeStatusChoices.Active.values]
 | 
				
			||||||
 | 
					                temporary_an_storage.extend(active_ans)
 | 
				
			||||||
 | 
					            if "closed" in status_list:
 | 
				
			||||||
 | 
					                closed_ans = [adoption_notice for adoption_notice in adoption_notices if
 | 
				
			||||||
 | 
					                              adoption_notice.adoption_notice_status in AdoptionNoticeStatusChoices.Closed.values]
 | 
				
			||||||
 | 
					                temporary_an_storage.extend(closed_ans)
 | 
				
			||||||
 | 
					            if "disabled" in status_list:
 | 
				
			||||||
 | 
					                disabled_ans = [adoption_notice for adoption_notice in adoption_notices if
 | 
				
			||||||
 | 
					                                adoption_notice.adoption_notice_status in AdoptionNoticeStatusChoices.Disabled.values]
 | 
				
			||||||
 | 
					                temporary_an_storage.extend(disabled_ans)
 | 
				
			||||||
 | 
					            if "awaiting_action" in status_list:
 | 
				
			||||||
 | 
					                awaiting_action_ans = [adoption_notice for adoption_notice in adoption_notices if
 | 
				
			||||||
 | 
					                                       adoption_notice.adoption_notice_status in AdoptionNoticeStatusChoices.AwaitingAction.values]
 | 
				
			||||||
 | 
					                temporary_an_storage.extend(awaiting_action_ans)
 | 
				
			||||||
 | 
					            adoption_notices = temporary_an_storage
 | 
				
			||||||
 | 
					        serializer = AdoptionNoticeSerializer(adoption_notices, many=True, context={"request": request})
 | 
				
			||||||
 | 
					        return Response(serializer.data, status=status.HTTP_200_OK)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										0
									
								
								src/fellchensammlung/aviews/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										37
									
								
								src/fellchensammlung/aviews/embeddables.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					from django.shortcuts import get_object_or_404, render
 | 
				
			||||||
 | 
					from django.views.decorators.clickjacking import xframe_options_exempt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from fellchensammlung.aviews.helpers import headers
 | 
				
			||||||
 | 
					from fellchensammlung.models import RescueOrganization, AdoptionNotice, Species
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@xframe_options_exempt
 | 
				
			||||||
 | 
					@headers({"X-Robots-Tag": "noindex"})
 | 
				
			||||||
 | 
					def list_ans_per_rescue_organization(request, rescue_organization_id, species_slug=None, active=True):
 | 
				
			||||||
 | 
					    expand = request.GET.get("expand")
 | 
				
			||||||
 | 
					    background_color = request.GET.get("background_color")
 | 
				
			||||||
 | 
					    if expand is not None:
 | 
				
			||||||
 | 
					        expand = True
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        expand = False
 | 
				
			||||||
 | 
					    org = get_object_or_404(RescueOrganization, pk=rescue_organization_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Get only active adoption notices or all
 | 
				
			||||||
 | 
					    if active:
 | 
				
			||||||
 | 
					        adoption_notices_of_org = org.adoption_notices_in_hierarchy_divided_by_status[0]
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        adoption_notices_of_org = org.adoption_notices
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Filter for Species if necessary
 | 
				
			||||||
 | 
					    if species_slug is None:
 | 
				
			||||||
 | 
					        adoption_notices = adoption_notices_of_org
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        species = get_object_or_404(Species, slug=species_slug)
 | 
				
			||||||
 | 
					        adoption_notices = [adoption_notice for adoption_notice in adoption_notices_of_org if
 | 
				
			||||||
 | 
					                            species in adoption_notice.species]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    template = 'fellchensammlung/embeddables/list-adoption-notices.html'
 | 
				
			||||||
 | 
					    return render(request, template,
 | 
				
			||||||
 | 
					                  context={"adoption_notices": adoption_notices,
 | 
				
			||||||
 | 
					                           "expand": expand,
 | 
				
			||||||
 | 
					                           "background_color": background_color})
 | 
				
			||||||
							
								
								
									
										23
									
								
								src/fellchensammlung/aviews/helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					def headers(headers):
 | 
				
			||||||
 | 
					    """Decorator adding arbitrary HTTP headers to the response.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This decorator adds HTTP headers specified in the argument (map), to the
 | 
				
			||||||
 | 
					    HTTPResponse returned by the function being decorated.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Example:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @headers({'Refresh': '10', 'X-Bender': 'Bite my shiny, metal ass!'})
 | 
				
			||||||
 | 
					    def index(request):
 | 
				
			||||||
 | 
					        ....
 | 
				
			||||||
 | 
					    Source: https://djangosnippets.org/snippets/275/
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    def headers_wrapper(fun):
 | 
				
			||||||
 | 
					        def wrapped_function(*args, **kwargs):
 | 
				
			||||||
 | 
					            response = fun(*args, **kwargs)
 | 
				
			||||||
 | 
					            for key in headers:
 | 
				
			||||||
 | 
					                response[key] = headers[key]
 | 
				
			||||||
 | 
					            return response
 | 
				
			||||||
 | 
					        return wrapped_function
 | 
				
			||||||
 | 
					    return headers_wrapper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/fellchensammlung/aviews/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					from django.urls import path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from . import embeddables
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					urlpatterns = [
 | 
				
			||||||
 | 
					    path("tierschutzorganisationen/<int:rescue_organization_id>/vermittlungen/",
 | 
				
			||||||
 | 
					         embeddables.list_ans_per_rescue_organization,
 | 
				
			||||||
 | 
					         name="list-adoption-notices-for-rescue-organization"),
 | 
				
			||||||
 | 
					    path("tierschutzorganisationen/<int:rescue_organization_id>/vermittlungen/<slug:species_slug>/",
 | 
				
			||||||
 | 
					         embeddables.list_ans_per_rescue_organization,
 | 
				
			||||||
 | 
					         name="list-adoption-notices-for-rescue-organization-species"),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
@@ -1,12 +1,16 @@
 | 
				
			|||||||
from django import forms
 | 
					from django import forms
 | 
				
			||||||
 | 
					from django.forms.widgets import Textarea
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportComment, ModerationAction, User, Species, \
 | 
					from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportComment, ModerationAction, User, Species, \
 | 
				
			||||||
    Comment, SexChoicesWithAll
 | 
					    Comment, SexChoicesWithAll, DistanceChoices, SpeciesSpecificURL, RescueOrganization
 | 
				
			||||||
from django_registration.forms import RegistrationForm
 | 
					from django_registration.forms import RegistrationForm
 | 
				
			||||||
from crispy_forms.helper import FormHelper
 | 
					from crispy_forms.helper import FormHelper
 | 
				
			||||||
from crispy_forms.layout import Submit, Layout, Fieldset, HTML, Row, Column, Field, Hidden
 | 
					from crispy_forms.layout import Submit, Layout, Fieldset, HTML, Row, Column, Field, Hidden
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
from notfellchen.settings import MEDIA_URL
 | 
					from notfellchen.settings import MEDIA_URL
 | 
				
			||||||
 | 
					from crispy_forms.layout import Div
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .tools.model_helpers import reason_for_signup_label, reason_for_signup_help_text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def animal_validator(value: str):
 | 
					def animal_validator(value: str):
 | 
				
			||||||
@@ -22,95 +26,46 @@ class DateInput(forms.DateInput):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AdoptionNoticeForm(forms.ModelForm):
 | 
					class AdoptionNoticeForm(forms.ModelForm):
 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					    template_name = "fellchensammlung/forms/form_snippets.html"
 | 
				
			||||||
        if 'in_adoption_notice_creation_flow' in kwargs:
 | 
					 | 
				
			||||||
            in_flow = kwargs.pop('in_adoption_notice_creation_flow')
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            in_flow = False
 | 
					 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					 | 
				
			||||||
        self.helper = FormHelper()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.helper.form_id = 'form-adoption-notice'
 | 
					 | 
				
			||||||
        self.helper.form_class = 'card'
 | 
					 | 
				
			||||||
        self.helper.form_method = 'post'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if in_flow:
 | 
					 | 
				
			||||||
            submit = Submit('save-and-add-another-animal', _('Speichern'))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            submit = Submit('submit', _('Speichern'))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.helper.layout = Layout(
 | 
					 | 
				
			||||||
            Fieldset(
 | 
					 | 
				
			||||||
                _('Vermittlungsdetails'),
 | 
					 | 
				
			||||||
                'name',
 | 
					 | 
				
			||||||
                'species',
 | 
					 | 
				
			||||||
                'num_animals',
 | 
					 | 
				
			||||||
                'date_of_birth',
 | 
					 | 
				
			||||||
                'sex',
 | 
					 | 
				
			||||||
                'group_only',
 | 
					 | 
				
			||||||
                'searching_since',
 | 
					 | 
				
			||||||
                'location_string',
 | 
					 | 
				
			||||||
                'organization',
 | 
					 | 
				
			||||||
                'description',
 | 
					 | 
				
			||||||
                'further_information',
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            submit)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					 | 
				
			||||||
        model = AdoptionNotice
 | 
					 | 
				
			||||||
        fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string",
 | 
					 | 
				
			||||||
                  "organization"]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class AdoptionNoticeFormWithDateWidget(AdoptionNoticeForm):
 | 
					 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = AdoptionNotice
 | 
					        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"]
 | 
					                  "organization"]
 | 
				
			||||||
        widgets = {
 | 
					        widgets = {
 | 
				
			||||||
            'searching_since': DateInput(),
 | 
					            'searching_since': DateInput(format=('%Y-%m-%d')),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AnimalForm(forms.ModelForm):
 | 
					class AdoptionNoticeFormAutoAnimal(AdoptionNoticeForm):
 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
        if 'in_adoption_notice_creation_flow' in kwargs:
 | 
					        super(AdoptionNoticeFormAutoAnimal, self).__init__(*args, **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)
 | 
					 | 
				
			||||||
        self.fields["num_animals"] = forms.fields.IntegerField(min_value=1, max_value=30, label=_("Zahl der Tiere"))
 | 
					        self.fields["num_animals"] = forms.fields.IntegerField(min_value=1, max_value=30, label=_("Zahl der Tiere"))
 | 
				
			||||||
        animal_form = AnimalForm()
 | 
					        animal_form = AnimalForm()
 | 
				
			||||||
        self.fields["species"] = animal_form.fields["species"]
 | 
					        self.fields["species"] = animal_form.fields["species"]
 | 
				
			||||||
        self.fields["sex"] = animal_form.fields["sex"]
 | 
					        self.fields["sex"] = animal_form.fields["sex"]
 | 
				
			||||||
        self.fields["date_of_birth"] = animal_form.fields["date_of_birth"]
 | 
					        self.fields["date_of_birth"] = animal_form.fields["date_of_birth"]
 | 
				
			||||||
        self.fields["date_of_birth"].widget = DateInput()
 | 
					        self.fields["date_of_birth"].widget = DateInput(format=('%Y-%m-%d'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AnimalForm(forms.ModelForm):
 | 
				
			||||||
 | 
					    template_name = "fellchensammlung/forms/form_snippets.html"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = Animal
 | 
				
			||||||
 | 
					        fields = ["name", "date_of_birth", "species", "sex", "description"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        widgets = {
 | 
				
			||||||
 | 
					            'date_of_birth': DateInput(format=('%Y-%m-%d'))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UpdateRescueOrgRegularCheckStatus(forms.ModelForm):
 | 
				
			||||||
 | 
					    template_name = "fellchensammlung/forms/form_snippets.html"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = RescueOrganization
 | 
				
			||||||
 | 
					        fields = ["regular_check_status"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ImageForm(forms.ModelForm):
 | 
					class ImageForm(forms.ModelForm):
 | 
				
			||||||
@@ -124,11 +79,21 @@ class ImageForm(forms.ModelForm):
 | 
				
			|||||||
        self.helper.form_id = 'form-animal-photo'
 | 
					        self.helper.form_id = 'form-animal-photo'
 | 
				
			||||||
        self.helper.form_class = 'card'
 | 
					        self.helper.form_class = 'card'
 | 
				
			||||||
        self.helper.form_method = 'post'
 | 
					        self.helper.form_method = 'post'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if in_flow:
 | 
					        if in_flow:
 | 
				
			||||||
            self.helper.add_input(Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')))
 | 
					            submits = Div(Submit('submit', _('Speichern')),
 | 
				
			||||||
            self.helper.add_input(Submit('submit', _('Speichern')))
 | 
					                          Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')),
 | 
				
			||||||
 | 
					                          css_class="container-edit-buttons")
 | 
				
			||||||
        else:
 | 
					        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:
 | 
					    class Meta:
 | 
				
			||||||
        model = Image
 | 
					        model = Image
 | 
				
			||||||
@@ -136,57 +101,80 @@ class ImageForm(forms.ModelForm):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReportAdoptionNoticeForm(forms.ModelForm):
 | 
					class ReportAdoptionNoticeForm(forms.ModelForm):
 | 
				
			||||||
 | 
					    template_name = "fellchensammlung/forms/form_snippets.html"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = ReportAdoptionNotice
 | 
					        model = ReportAdoptionNotice
 | 
				
			||||||
        fields = ('reported_broken_rules', 'user_comment')
 | 
					        fields = ('reported_broken_rules', 'user_comment')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReportCommentForm(forms.ModelForm):
 | 
					class ReportCommentForm(forms.ModelForm):
 | 
				
			||||||
 | 
					    template_name = "fellchensammlung/forms/form_snippets.html"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = ReportComment
 | 
					        model = ReportComment
 | 
				
			||||||
        fields = ('reported_broken_rules', 'user_comment')
 | 
					        fields = ('reported_broken_rules', 'user_comment')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CommentForm(forms.ModelForm):
 | 
					class CommentForm(forms.ModelForm):
 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					 | 
				
			||||||
        self.helper = FormHelper()
 | 
					 | 
				
			||||||
        self.helper.form_class = 'form-comments'
 | 
					 | 
				
			||||||
        self.helper.add_input(Hidden('action', 'comment'))
 | 
					 | 
				
			||||||
        self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="btn2"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Comment
 | 
					        model = Comment
 | 
				
			||||||
        fields = ('text',)
 | 
					        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 ModerationActionForm(forms.ModelForm):
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = ModerationAction
 | 
					        model = ModerationAction
 | 
				
			||||||
        fields = ('action', 'public_comment', 'private_comment')
 | 
					        fields = ('action', 'public_comment', 'private_comment')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AddedRegistrationForm(forms.Form):
 | 
				
			||||||
 | 
					    reason_for_signup = forms.CharField(label=reason_for_signup_label,
 | 
				
			||||||
 | 
					                                        help_text=reason_for_signup_help_text,
 | 
				
			||||||
 | 
					                                        widget=Textarea)
 | 
				
			||||||
 | 
					    captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_(
 | 
				
			||||||
 | 
					        "Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um "
 | 
				
			||||||
 | 
					        "sicherzustellen, dass du kein Roboter bist."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def signup(self, request, user):
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CustomRegistrationForm(RegistrationForm):
 | 
					class CustomRegistrationForm(RegistrationForm):
 | 
				
			||||||
    class Meta(RegistrationForm.Meta):
 | 
					    class Meta(RegistrationForm.Meta):
 | 
				
			||||||
        model = User
 | 
					        model = User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_("Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um sicherzustellen, dass du kein Roboter bist."))
 | 
					    template_name = "fellchensammlung/forms/form_snippets.html"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					    captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_(
 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					        "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."))
 | 
				
			||||||
        self.helper = FormHelper()
 | 
					 | 
				
			||||||
        self.helper.form_id = 'form-registration'
 | 
					 | 
				
			||||||
        self.helper.form_class = 'card'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.helper.add_input(Submit('submit', _('Registrieren'), css_class="btn"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def _get_distances():
 | 
					 | 
				
			||||||
    return {i: i for i in [20, 50, 100, 200, 500]}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AdoptionNoticeSearchForm(forms.Form):
 | 
					class AdoptionNoticeSearchForm(forms.Form):
 | 
				
			||||||
    location = forms.CharField(max_length=20, label=_("Stadt"), required=False)
 | 
					    template_name = "fellchensammlung/forms/form_snippets.html"
 | 
				
			||||||
    max_distance = forms.ChoiceField(choices=_get_distances, label=_("Max. Distanz"))
 | 
					
 | 
				
			||||||
    sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False,
 | 
					    sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False,
 | 
				
			||||||
                            initial=SexChoicesWithAll.ALL)
 | 
					                            initial=SexChoicesWithAll.ALL)
 | 
				
			||||||
 | 
					    max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED,
 | 
				
			||||||
 | 
					                                     label=_("Suchradius"))
 | 
				
			||||||
 | 
					    location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RescueOrgSearchForm(forms.Form):
 | 
				
			||||||
 | 
					    template_name = "fellchensammlung/forms/form_snippets.html"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
 | 
				
			||||||
 | 
					    max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.TWENTY,
 | 
				
			||||||
 | 
					                                     label=_("Suchradius"))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,43 +1,43 @@
 | 
				
			|||||||
from django.db.models.signals import post_save
 | 
					from django.db.models.signals import post_save
 | 
				
			||||||
from django.dispatch import receiver
 | 
					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_lazy as _
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.core import mail
 | 
					from django.core import mail
 | 
				
			||||||
from fellchensammlung.models import User, CommentNotification, BaseNotification, TrustLevel
 | 
					from fellchensammlung.models import User, Notification, TrustLevel, NotificationTypeChoices
 | 
				
			||||||
from notfellchen.settings import host
 | 
					from fellchensammlung.tools.model_helpers import ndm
 | 
				
			||||||
 | 
					 | 
				
			||||||
NEWLINE = "\r\n"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def mail_admins_new_report(report):
 | 
					def notify_mods_new_report(report, notification_type):
 | 
				
			||||||
    subject = _("Neue Meldung")
 | 
					    """
 | 
				
			||||||
 | 
					    Sends an e-mail to all users that should handle the report.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
    for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
 | 
					    for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
 | 
				
			||||||
        greeting = _("Moin,") + "{NEWLINE}"
 | 
					        if notification_type == NotificationTypeChoices.NEW_REPORT_AN:
 | 
				
			||||||
        new_report_text = _("es wurde ein Regelverstoß gemeldet.") + "{NEWLINE}"
 | 
					            title = _("Vermittlung gemeldet")
 | 
				
			||||||
        if len(report.reported_broken_rules.all()) > 0:
 | 
					        elif notification_type == NotificationTypeChoices.NEW_REPORT_COMMENT:
 | 
				
			||||||
            reported_rules_text = (f"Ein Verstoß gegen die folgenden Regeln wurde gemeldet:{NEWLINE}"
 | 
					            title = _("Kommentar gemeldet")
 | 
				
			||||||
                                   f"- {f'{NEWLINE} - '.join([str(r) for r in report.reported_broken_rules.all()])}{NEWLINE}")
 | 
					 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            reported_rules_text = f"Es wurden keine Regeln angegeben gegen die Verstoßen wurde.{NEWLINE}"
 | 
					            raise NotImplementedError
 | 
				
			||||||
        if report.user_comment:
 | 
					        notification = Notification.objects.create(
 | 
				
			||||||
            comment_text = f'Kommentar zum Report: "{report.user_comment}"{NEWLINE}'
 | 
					            notification_type=notification_type,
 | 
				
			||||||
        else:
 | 
					            user_to_notify=moderator,
 | 
				
			||||||
            comment_text = f"Es wurde kein Kommentar hinzugefügt.{NEWLINE}"
 | 
					            report=report,
 | 
				
			||||||
 | 
					            title=title,
 | 
				
			||||||
        report_url = "https://" + host + report.get_absolute_url()
 | 
					        )
 | 
				
			||||||
        link_text = f"Um alle Details zu sehen, geh bitte auf: {report_url}"
 | 
					        notification.save()
 | 
				
			||||||
        body_text = greeting + new_report_text + reported_rules_text + comment_text + link_text
 | 
					 | 
				
			||||||
        message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [moderator.email])
 | 
					 | 
				
			||||||
        message.send()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def send_notification_email(notification_pk):
 | 
					def send_notification_email(notification_pk):
 | 
				
			||||||
    try:
 | 
					    notification = Notification.objects.get(pk=notification_pk)
 | 
				
			||||||
        notification = CommentNotification.objects.get(pk=notification_pk)
 | 
					
 | 
				
			||||||
    except CommentNotification.DoesNotExist:
 | 
					    subject = f"{notification.title}"
 | 
				
			||||||
        notification = BaseNotification.objects.get(pk=notification_pk)
 | 
					    context = {"notification": notification, }
 | 
				
			||||||
    subject = f"🔔 {notification.title}"
 | 
					    html_message = render_to_string(ndm[notification.notification_type].email_html_template, context)
 | 
				
			||||||
    body_text = notification.text
 | 
					    plain_message = render_to_string(ndm[notification.notification_type].email_plain_template, context)
 | 
				
			||||||
    message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [notification.user.email])
 | 
					
 | 
				
			||||||
    message.send()
 | 
					    mail.send_mail(subject, plain_message, settings.DEFAULT_FROM_EMAIL,
 | 
				
			||||||
 | 
					                   [notification.user_to_notify.email],
 | 
				
			||||||
 | 
					                   html_message=html_message)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										10
									
								
								src/fellchensammlung/management/commands/dedup_locations.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					from django.core.management import BaseCommand
 | 
				
			||||||
 | 
					from fellchensammlung.tools.admin import dedup_locations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Command(BaseCommand):
 | 
				
			||||||
 | 
					    help = 'Deduplicate locations based on place_id'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handle(self, *args, **options):
 | 
				
			||||||
 | 
					        dedup_locations()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										11
									
								
								src/fellchensammlung/management/commands/export_contacts.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					from django.core.management import BaseCommand
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from fellchensammlung.tools.admin import export_orgs_as_vcf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Command(BaseCommand):
 | 
				
			||||||
 | 
					    help = 'Export organizations with phone number as contacts in vcf format'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handle(self, *args, **options):
 | 
				
			||||||
 | 
					        export_orgs_as_vcf()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					from django.core.management import BaseCommand
 | 
				
			||||||
 | 
					from fellchensammlung.tools.admin import mask_organization_contact_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Command(BaseCommand):
 | 
				
			||||||
 | 
					    help = 'Mask e-mail addresses and phone numbers of organizations for testing purposes.'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_arguments(self, parser):
 | 
				
			||||||
 | 
					        parser.add_argument("domain", type=str)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handle(self, *args, **options):
 | 
				
			||||||
 | 
					        domain = options["domain"]
 | 
				
			||||||
 | 
					        mask_organization_contact_data(domain)
 | 
				
			||||||
							
								
								
									
										19
									
								
								src/fellchensammlung/management/commands/sync_to_twenty.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					from django.core.management import BaseCommand
 | 
				
			||||||
 | 
					from tqdm import tqdm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from fellchensammlung.models import RescueOrganization
 | 
				
			||||||
 | 
					from fellchensammlung.tools.twenty import sync_rescue_org_to_twenty
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Command(BaseCommand):
 | 
				
			||||||
 | 
					    help = 'Send rescue organizations as companies to twenty'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_arguments(self, parser):
 | 
				
			||||||
 | 
					        parser.add_argument("base_url", type=str)
 | 
				
			||||||
 | 
					        parser.add_argument("token", type=str)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handle(self, *args, **options):
 | 
				
			||||||
 | 
					        base_url = options["base_url"]
 | 
				
			||||||
 | 
					        token = options["token"]
 | 
				
			||||||
 | 
					        for rescue_org in tqdm(RescueOrganization.objects.all()):
 | 
				
			||||||
 | 
					            sync_rescue_org_to_twenty(rescue_org, base_url, token)
 | 
				
			||||||
@@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.1 on 2024-12-14 07:57
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0026_alter_animal_sex'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='animal',
 | 
				
			||||||
 | 
					            name='species',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.species', verbose_name='Tierart'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='AndoptionNoticeNotification',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('basenotification_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='fellchensammlung.basenotification')),
 | 
				
			||||||
 | 
					                ('adoption_notice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            bases=('fellchensammlung.basenotification',),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										25
									
								
								src/fellchensammlung/migrations/0028_searchsubscription.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.1 on 2024-12-26 15:56
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0027_alter_animal_species_andoptionnoticenotification'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='SearchSubscription',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('sex', models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich, kastriert'), ('I', 'Intergeschlechtlich')], max_length=20)),
 | 
				
			||||||
 | 
					                ('radius', models.IntegerField(choices=[(20, '20 km'), (50, '50 km'), (100, '100 km'), (200, '200 km'), (500, '500 km')])),
 | 
				
			||||||
 | 
					                ('location', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.location')),
 | 
				
			||||||
 | 
					                ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.4 on 2024-12-31 10:37
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0028_searchsubscription'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.RenameModel(
 | 
				
			||||||
 | 
					            old_name='AndoptionNoticeNotification',
 | 
				
			||||||
 | 
					            new_name='AdoptionNoticeNotification',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='searchsubscription',
 | 
				
			||||||
 | 
					            name='sex',
 | 
				
			||||||
 | 
					            field=models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich Kastriert'), ('I', 'Intergeschlechtlich'), ('A', 'Alle')], max_length=20),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.4 on 2024-12-31 12:23
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0029_rename_andoptionnoticenotification_adoptionnoticenotification_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.RenameField(
 | 
				
			||||||
 | 
					            model_name='searchsubscription',
 | 
				
			||||||
 | 
					            old_name='radius',
 | 
				
			||||||
 | 
					            new_name='max_distance',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.4 on 2024-12-31 12:42
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0030_rename_radius_searchsubscription_max_distance'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='searchsubscription',
 | 
				
			||||||
 | 
					            name='location',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.location'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='searchsubscription',
 | 
				
			||||||
 | 
					            name='max_distance',
 | 
				
			||||||
 | 
					            field=models.IntegerField(choices=[(20, '20 km'), (50, '50 km'), (100, '100 km'), (200, '200 km'), (500, '500 km')], null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.4 on 2025-01-01 22:04
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.utils.timezone
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0031_alter_searchsubscription_location_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='searchsubscription',
 | 
				
			||||||
 | 
					            name='created_at',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
 | 
				
			||||||
 | 
					            preserve_default=False,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='searchsubscription',
 | 
				
			||||||
 | 
					            name='updated_at',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(auto_now=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.4 on 2025-01-05 18:18
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0032_searchsubscription_created_at_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='rescueorganization',
 | 
				
			||||||
 | 
					            name='external_object_identifier',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=200, null=True, verbose_name='External Object Identifier'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='rescueorganization',
 | 
				
			||||||
 | 
					            name='external_source_identifier',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, choices=[('OSM', 'Open Street Map')], max_length=200, null=True, verbose_name='External Source Identifier'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										23
									
								
								src/fellchensammlung/migrations/0034_speciesspecificurl.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.4 on 2025-01-05 19:36
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0033_rescueorganization_external_object_identifier_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='SpeciesSpecificURL',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('url', models.URLField(verbose_name='Tierartspezifische URL')),
 | 
				
			||||||
 | 
					                ('rescues_organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.rescueorganization', verbose_name='Tierschutzorganisation')),
 | 
				
			||||||
 | 
					                ('species', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.species', verbose_name='Tierart')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.4 on 2025-01-11 12:58
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0034_speciesspecificurl'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='image',
 | 
				
			||||||
 | 
					            name='alt_text',
 | 
				
			||||||
 | 
					            field=models.TextField(max_length=2000, verbose_name='Alternativtext'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='report',
 | 
				
			||||||
 | 
					            name='reported_broken_rules',
 | 
				
			||||||
 | 
					            field=models.ManyToManyField(to='fellchensammlung.rule', verbose_name='Regeln gegen die verstoßen wurde'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='report',
 | 
				
			||||||
 | 
					            name='user_comment',
 | 
				
			||||||
 | 
					            field=models.TextField(blank=True, verbose_name='Kommentar/Zusätzliche Information'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.4 on 2025-01-14 06:28
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0035_alter_image_alt_text_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='basenotification',
 | 
				
			||||||
 | 
					            name='read_at',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(blank=True, null=True, verbose_name='Gelesen am'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.4 on 2025-01-14 06:28
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0036_basenotification_read_at'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='basenotification',
 | 
				
			||||||
 | 
					            name='title',
 | 
				
			||||||
 | 
					            field=models.CharField(max_length=100, verbose_name='Titel'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.4 on 2025-03-09 08:31
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.utils.timezone
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0037_alter_basenotification_title'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='rescueorganization',
 | 
				
			||||||
 | 
					            name='last_checked',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Datum der letzten Prüfung'),
 | 
				
			||||||
 | 
					            preserve_default=False,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.4 on 2025-03-09 16:44
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0038_rescueorganization_last_checked'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='rescueorganization',
 | 
				
			||||||
 | 
					            name='last_checked',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(auto_now_add=True, verbose_name='Datum der letzten Prüfung'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.4 on 2025-03-20 23:27
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0039_alter_rescueorganization_last_checked'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='rescueorganization',
 | 
				
			||||||
 | 
					            name='allows_using_materials',
 | 
				
			||||||
 | 
					            field=models.CharField(choices=[('allowed', 'Usage allowed'), ('requested', 'Usage requested'), ('denied', 'Usage denied'), ('other', "It's complicated"), ('not_asked', 'Not asked')], default='not_asked', max_length=200, verbose_name='Erlaubt Nutzung von Inhalten'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.4 on 2025-04-06 06:37
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0040_alter_rescueorganization_allows_using_materials'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='location',
 | 
				
			||||||
 | 
					            name='city',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=200, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='location',
 | 
				
			||||||
 | 
					            name='country',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, help_text='Standardisierter Ländercode nach ISO 3166-1 ALPHA-2', max_length=2, null=True, verbose_name='Ländercode'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='location',
 | 
				
			||||||
 | 
					            name='housenumber',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=20, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='location',
 | 
				
			||||||
 | 
					            name='postcode',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=20, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='location',
 | 
				
			||||||
 | 
					            name='street',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=200, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterUniqueTogether(
 | 
				
			||||||
 | 
					            name='rescueorganization',
 | 
				
			||||||
 | 
					            unique_together={('external_object_identifier', 'external_source_identifier')},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										18
									
								
								src/fellchensammlung/migrations/0042_location_county.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.4 on 2025-04-24 17:13
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0041_location_city_location_country_location_housenumber_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='location',
 | 
				
			||||||
 | 
					            name='county',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=200, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.4 on 2025-04-24 17:41
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0042_location_county'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.RenameField(
 | 
				
			||||||
 | 
					            model_name='location',
 | 
				
			||||||
 | 
					            old_name='country',
 | 
				
			||||||
 | 
					            new_name='countrycode',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.4 on 2025-04-26 22:21
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0043_rename_country_location_countrycode'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='location',
 | 
				
			||||||
 | 
					            name='place_id',
 | 
				
			||||||
 | 
					            field=models.CharField(max_length=200),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										23
									
								
								src/fellchensammlung/migrations/0045_importantlocation.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.4 on 2025-04-27 11:31
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0044_alter_location_place_id'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='ImportantLocation',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('slug', models.SlugField(unique=True)),
 | 
				
			||||||
 | 
					                ('name', models.CharField(max_length=200)),
 | 
				
			||||||
 | 
					                ('location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.location')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.1.4 on 2025-04-27 11:51
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0045_importantlocation'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='importantlocation',
 | 
				
			||||||
 | 
					            name='location',
 | 
				
			||||||
 | 
					            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.location'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-05-23 16:07
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0046_alter_importantlocation_location'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='adoptionnotice',
 | 
				
			||||||
 | 
					            name='further_information',
 | 
				
			||||||
 | 
					            field=models.URLField(blank=True, help_text='Verlinke hier die Quelle der Vermittlung (z.B. die Website des Tierheims', null=True, verbose_name='Link zu mehr Informationen'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='adoptionnotice',
 | 
				
			||||||
 | 
					            name='name',
 | 
				
			||||||
 | 
					            field=models.CharField(max_length=200, verbose_name='Titel der Vermittlung'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='rescueorganization',
 | 
				
			||||||
 | 
					            name='location_string',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Ort der Organisation'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-06-19 15:51
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0047_alter_adoptionnotice_further_information_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='adoptionnotice',
 | 
				
			||||||
 | 
					            name='further_information',
 | 
				
			||||||
 | 
					            field=models.URLField(blank=True, help_text='Verlinke hier die Quelle der Vermittlung (z.B. die Website des Tierheims)', null=True, verbose_name='Link zu mehr Informationen'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-06-19 21:26
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0048_alter_adoptionnotice_further_information'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='rescueorganization',
 | 
				
			||||||
 | 
					            name='exclude_from_check',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False, help_text='Organisation von der manuellen Überprüfung ausschließen, z.B. weil Tiere nicht online geführt werden', verbose_name='Von Prüfung ausschließen'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-06-20 16:52
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0049_rescueorganization_exclude_from_check'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.RenameField(
 | 
				
			||||||
 | 
					            model_name='speciesspecificurl',
 | 
				
			||||||
 | 
					            old_name='rescues_organization',
 | 
				
			||||||
 | 
					            new_name='rescue_organization',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-07-03 09:28
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0050_rename_rescues_organization_speciesspecificurl_rescue_organization'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='rescueorganization',
 | 
				
			||||||
 | 
					            name='parent_org',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.rescueorganization'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='SpeciesSpecialization',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('rescue_organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.rescueorganization', verbose_name='Tierschutzorganisation')),
 | 
				
			||||||
 | 
					                ('species', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.species', verbose_name='Tierart')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-07-11 09:01
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0051_rescueorganization_parent_org_speciesspecialization'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='basenotification',
 | 
				
			||||||
 | 
					            name='user',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='commentnotification',
 | 
				
			||||||
 | 
					            name='basenotification_ptr',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name='commentnotification',
 | 
				
			||||||
 | 
					            name='comment',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='Notification',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('created_at', models.DateTimeField(auto_now_add=True)),
 | 
				
			||||||
 | 
					                ('updated_at', models.DateTimeField(auto_now=True)),
 | 
				
			||||||
 | 
					                ('read_at', models.DateTimeField(blank=True, null=True, verbose_name='Gelesen am')),
 | 
				
			||||||
 | 
					                ('notification_type', models.CharField(choices=[('new_user', 'Useraccount wurde erstellt'), ('new_report_an', 'Vermittlung wurde gemeldet'), ('new_report_comment', 'Kommentar wurde gemeldet'), ('an_is_to_be_checked', 'Vermittlung muss überprüft werden'), ('an_was_deactivated', 'Vermittlung wurde deaktiviert'), ('an_for_search_found', 'Vermittlung für Suche gefunden')], max_length=200, verbose_name='Benachrichtigungsgrund')),
 | 
				
			||||||
 | 
					                ('title', models.CharField(max_length=100, verbose_name='Titel')),
 | 
				
			||||||
 | 
					                ('text', models.TextField(verbose_name='Inhalt')),
 | 
				
			||||||
 | 
					                ('read', models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                ('adoption_notice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')),
 | 
				
			||||||
 | 
					                ('comment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.comment', verbose_name='Antwort')),
 | 
				
			||||||
 | 
					                ('report', models.ForeignKey(help_text='Report auf den sich die Benachrichtigung bezieht.', on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.report', verbose_name='Report')),
 | 
				
			||||||
 | 
					                ('user_related', models.ForeignKey(help_text='Useraccount auf den sich die Benachrichtigung bezieht.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Verwandter Useraccount')),
 | 
				
			||||||
 | 
					                ('user_to_notify', models.ForeignKey(help_text='Useraccount der Benachrichtigt wird', on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='AdoptionNoticeNotification',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='BaseNotification',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='CommentNotification',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-07-11 09:10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0052_remove_basenotification_user_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='notification',
 | 
				
			||||||
 | 
					            name='adoption_notice',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='notification',
 | 
				
			||||||
 | 
					            name='notification_type',
 | 
				
			||||||
 | 
					            field=models.CharField(choices=[('new_user', 'Useraccount wurde erstellt'), ('new_report_an', 'Vermittlung wurde gemeldet'), ('new_report_comment', 'Kommentar wurde gemeldet'), ('an_is_to_be_checked', 'Vermittlung muss überprüft werden'), ('an_was_deactivated', 'Vermittlung wurde deaktiviert'), ('an_for_search_found', 'Vermittlung für Suche gefunden'), ('new_comment', 'Neuer Kommentar')], max_length=200, verbose_name='Benachrichtigungsgrund'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='notification',
 | 
				
			||||||
 | 
					            name='report',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(blank=True, help_text='Report auf den sich die Benachrichtigung bezieht.', null=True, on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.report', verbose_name='Report'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='notification',
 | 
				
			||||||
 | 
					            name='user_related',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(blank=True, help_text='Useraccount auf den sich die Benachrichtigung bezieht.', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Verwandter Useraccount'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='notification',
 | 
				
			||||||
 | 
					            name='user_to_notify',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(help_text='Useraccount der Benachrichtigt wird', on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-07-11 11:47
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0053_alter_notification_adoption_notice_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='notification',
 | 
				
			||||||
 | 
					            name='comment',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.comment', verbose_name='Antwort'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-07-13 10:54
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0054_alter_notification_comment'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='rescueorganization',
 | 
				
			||||||
 | 
					            name='ongoing_communication',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False, help_text='Es findet gerade Kommunikation zwischen Notfellchen und der Organisation statt.', verbose_name='In aktiver Kommunikation'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='notification',
 | 
				
			||||||
 | 
					            name='user_to_notify',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(help_text='Useraccount der Benachrichtigt wird', on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL, verbose_name='Empfänger*in'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-07-14 05:12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0055_rescueorganization_ongoing_communication_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name='rescueorganization',
 | 
				
			||||||
 | 
					            options={'ordering': ['name']},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='rescueorganization',
 | 
				
			||||||
 | 
					            name='specializations',
 | 
				
			||||||
 | 
					            field=models.ManyToManyField(to='fellchensammlung.species'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-07-14 05:15
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0056_alter_rescueorganization_options_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='SpeciesSpecialization',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										25
									
								
								src/fellchensammlung/migrations/0058_socialmediapost.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-07-19 17:48
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					import django.utils.timezone
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0057_delete_speciesspecialization'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name='SocialMediaPost',
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
				
			||||||
 | 
					                ('created_at', models.DateField(default=django.utils.timezone.now, verbose_name='Erstellt am')),
 | 
				
			||||||
 | 
					                ('platform', models.CharField(choices=[('fediverse', 'Fediverse')], max_length=255, verbose_name='Social Media Platform')),
 | 
				
			||||||
 | 
					                ('url', models.URLField(verbose_name='URL')),
 | 
				
			||||||
 | 
					                ('adoption_notice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-08-02 09:33
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0058_socialmediapost'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='rescueorganization',
 | 
				
			||||||
 | 
					            name='twenty_id',
 | 
				
			||||||
 | 
					            field=models.UUIDField(blank=True, help_text='ID der der Organisation in Twenty', null=True, verbose_name='Twenty-ID'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='rescueorganization',
 | 
				
			||||||
 | 
					            name='specializations',
 | 
				
			||||||
 | 
					            field=models.ManyToManyField(blank=True, to='fellchensammlung.species'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,87 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-08-30 21:49
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0059_rescueorganization_twenty_id_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name='adoptionnotice',
 | 
				
			||||||
 | 
					            options={'permissions': [('create_active_adoption_notice', 'Can create an active adoption notice')], 'verbose_name': 'Vermittlung', 'verbose_name_plural': 'Vermittlungen'},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name='adoptionnoticestatus',
 | 
				
			||||||
 | 
					            options={'verbose_name': 'Vermittlungsstatus', 'verbose_name_plural': 'Vermittlungsstati'},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name='animal',
 | 
				
			||||||
 | 
					            options={'verbose_name': 'Tier', 'verbose_name_plural': 'Tiere'},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name='announcement',
 | 
				
			||||||
 | 
					            options={'verbose_name': 'Banner', 'verbose_name_plural': 'Banner'},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name='comment',
 | 
				
			||||||
 | 
					            options={'verbose_name': 'Kommentar', 'verbose_name_plural': 'Kommentare'},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name='image',
 | 
				
			||||||
 | 
					            options={'verbose_name': 'Bild', 'verbose_name_plural': 'Bilder'},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name='importantlocation',
 | 
				
			||||||
 | 
					            options={'verbose_name': 'Wichtiger Standort', 'verbose_name_plural': 'Wichtige Standorte'},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name='location',
 | 
				
			||||||
 | 
					            options={'verbose_name': 'Standort', 'verbose_name_plural': 'Standorte'},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name='moderationaction',
 | 
				
			||||||
 | 
					            options={'verbose_name': 'Moderationsaktion', 'verbose_name_plural': 'Moderationsaktionen'},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name='notification',
 | 
				
			||||||
 | 
					            options={'verbose_name': 'Benachrichtigung', 'verbose_name_plural': 'Benachrichtigungen'},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name='report',
 | 
				
			||||||
 | 
					            options={'verbose_name': 'Meldung', 'verbose_name_plural': 'Meldungen'},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name='rescueorganization',
 | 
				
			||||||
 | 
					            options={'ordering': ['name'], 'verbose_name': 'Tierschutzorganisation', 'verbose_name_plural': 'Tierschutzorganisationen'},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name='rule',
 | 
				
			||||||
 | 
					            options={'verbose_name': 'Regel', 'verbose_name_plural': 'Regeln'},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name='searchsubscription',
 | 
				
			||||||
 | 
					            options={'verbose_name': 'Abonnierte Suche', 'verbose_name_plural': 'Abonnierte Suchen'},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name='speciesspecificurl',
 | 
				
			||||||
 | 
					            options={'verbose_name': 'Tierartspezifische URL', 'verbose_name_plural': 'Tierartspezifische URLs'},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name='subscriptions',
 | 
				
			||||||
 | 
					            options={'verbose_name': 'Abonnement', 'verbose_name_plural': 'Abonnements'},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name='timestamp',
 | 
				
			||||||
 | 
					            options={'verbose_name': 'Zeitstempel', 'verbose_name_plural': 'Zeitstempel'},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='adoptionnotice',
 | 
				
			||||||
 | 
					            name='adoption_notice_status',
 | 
				
			||||||
 | 
					            field=models.TextField(choices=[('active_searching', 'Searching'), ('active_interested', 'Interested'), ('awaiting_action_waiting_for_review', 'Waiting for review'), ('awaiting_action_needs_additional_info', 'Needs additional info'), ('closed_successful_with_notfellchen', 'Successful (with Notfellchen)'), ('closed_successful_without_notfellchen', 'Successful (without Notfellchen)'), ('closed_animal_died', 'Animal died'), ('closed_for_other_adoption_notice', 'Closed for other adoption notice'), ('closed_not_open_for_adoption_anymore', 'Not open for adoption anymore'), ('closed_other', 'Other (closed)'), ('disabled_against_the_rules', 'Against the rules'), ('disabled_unchecked', 'Unchecked'), ('disabled_other', 'Other (disabled)')], default='disabled_other', max_length=64, verbose_name='Status'),
 | 
				
			||||||
 | 
					            preserve_default=False,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,63 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-08-30 21:51
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def map_status(adoption_notice_status):
 | 
				
			||||||
 | 
					    minor = adoption_notice_status.minor_status
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if minor == "searching":
 | 
				
			||||||
 | 
					        return "active_searching"
 | 
				
			||||||
 | 
					    if minor == "interested":
 | 
				
			||||||
 | 
					        return "active_interested"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if minor == "waiting_for_review":
 | 
				
			||||||
 | 
					        return "awaiting_action_waiting_for_review"
 | 
				
			||||||
 | 
					    if minor == "needs_additional_info":
 | 
				
			||||||
 | 
					        return "awaiting_action_needs_additional_info"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if minor == "successful_with_notfellchen":
 | 
				
			||||||
 | 
					        return "closed_successful_with_notfellchen"
 | 
				
			||||||
 | 
					    if minor == "successful_without_notfellchen":
 | 
				
			||||||
 | 
					        return "closed_successful_without_notfellchen"
 | 
				
			||||||
 | 
					    if minor == "animal_died":
 | 
				
			||||||
 | 
					        return "closed_animal_died"
 | 
				
			||||||
 | 
					    if minor == "closed_for_other_adoption_notice":
 | 
				
			||||||
 | 
					        return "closed_for_other_adoption_notice"
 | 
				
			||||||
 | 
					    if minor == "not_open_for_adoption_anymore":
 | 
				
			||||||
 | 
					        return "closed_not_open_for_adoption_anymore"
 | 
				
			||||||
 | 
					    if minor == "other":
 | 
				
			||||||
 | 
					        return "closed_other"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if minor == "against_the_rules":
 | 
				
			||||||
 | 
					        return "disabled_against_the_rules"
 | 
				
			||||||
 | 
					    if minor == "unchecked":
 | 
				
			||||||
 | 
					        return "disabled_unchecked"
 | 
				
			||||||
 | 
					    if minor in ["missing_information", "technical_error"]:
 | 
				
			||||||
 | 
					        return "disabled_other"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def migrate_status(apps, schema_editor):
 | 
				
			||||||
 | 
					    # We can't import the model directly as it may be a newer
 | 
				
			||||||
 | 
					    # version than this migration expects. We use the historical version.
 | 
				
			||||||
 | 
					    AdoptionNoticeStatus = apps.get_model("fellchensammlung", "AdoptionNoticeStatus")
 | 
				
			||||||
 | 
					    AdoptionNotice = apps.get_model("fellchensammlung", "AdoptionNotice")
 | 
				
			||||||
 | 
					    for ans in AdoptionNoticeStatus.objects.all():
 | 
				
			||||||
 | 
					        adoption_notice = AdoptionNotice.objects.get(id=ans.adoption_notice.id)
 | 
				
			||||||
 | 
					        new_status = map_status(ans)
 | 
				
			||||||
 | 
					        logging.debug(f"{ans.minor_status} -> {new_status}")
 | 
				
			||||||
 | 
					        adoption_notice.adoption_notice_status = map_status(ans)
 | 
				
			||||||
 | 
					        adoption_notice.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0060_alter_adoptionnotice_options_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.RunPython(migrate_status),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-08-30 22:50
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0061_datamigration_status_model_to_field'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='adoptionnotice',
 | 
				
			||||||
 | 
					            name='adoption_notice_status',
 | 
				
			||||||
 | 
					            field=models.TextField(choices=[('active_searching', 'Searching'), ('active_interested', 'Interested'), ('awaiting_action_waiting_for_review', 'Waiting for review'), ('awaiting_action_needs_additional_info', 'Needs additional info'), ('closed_successful_with_notfellchen', 'Successful (with Notfellchen)'), ('closed_successful_without_notfellchen', 'Successful (without Notfellchen)'), ('closed_animal_died', 'Animal died'), ('closed_for_other_adoption_notice', 'Closed for other adoption notice'), ('closed_not_open_for_adoption_anymore', 'Not open for adoption anymore'), ('closed_link_to_more_info_not_reachable', 'Der Link zu weiteren Informationen ist nicht mehr erreichbar.'), ('closed_other', 'Other (closed)'), ('disabled_against_the_rules', 'Against the rules'), ('disabled_unchecked', 'Unchecked'), ('disabled_other', 'Other (disabled)')], max_length=64, verbose_name='Status'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.DeleteModel(
 | 
				
			||||||
 | 
					            name='AdoptionNoticeStatus',
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-09-05 14:04
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0062_alter_adoptionnotice_adoption_notice_status_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='adoptionnotice',
 | 
				
			||||||
 | 
					            name='adoption_process',
 | 
				
			||||||
 | 
					            field=models.TextField(blank=True, choices=[('contact_person_in_an', 'Kontaktiere die Person im Vermittlungstext')], max_length=64, null=True, verbose_name='Adoptionsprozess'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,140 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-09-06 11:11
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0063_adoptionnotice_adoption_process'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='animal',
 | 
				
			||||||
 | 
					            name='name',
 | 
				
			||||||
 | 
					            field=models.CharField(max_length=200, verbose_name='Name'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='animal',
 | 
				
			||||||
 | 
					            name='photos',
 | 
				
			||||||
 | 
					            field=models.ManyToManyField(blank=True, to='fellchensammlung.image', verbose_name='Fotos'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='animal',
 | 
				
			||||||
 | 
					            name='sex',
 | 
				
			||||||
 | 
					            field=models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich, kastriert'), ('I', 'Intergeschlechtlich')], max_length=20, verbose_name='Geschlecht'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='comment',
 | 
				
			||||||
 | 
					            name='adoption_notice',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='image',
 | 
				
			||||||
 | 
					            name='alt_text',
 | 
				
			||||||
 | 
					            field=models.TextField(help_text='Beschreibe das Bild für blinde und sehbehinderte Menschen', max_length=2000, verbose_name='Alternativtext'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='location',
 | 
				
			||||||
 | 
					            name='city',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Stadt'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='location',
 | 
				
			||||||
 | 
					            name='county',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Landkreis'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='location',
 | 
				
			||||||
 | 
					            name='housenumber',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Hausnummer'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='location',
 | 
				
			||||||
 | 
					            name='latitude',
 | 
				
			||||||
 | 
					            field=models.FloatField(verbose_name='Breitengrad'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='location',
 | 
				
			||||||
 | 
					            name='longitude',
 | 
				
			||||||
 | 
					            field=models.FloatField(verbose_name='Längengrad'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='location',
 | 
				
			||||||
 | 
					            name='postcode',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Postleitzahl'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='location',
 | 
				
			||||||
 | 
					            name='street',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Straße'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='notification',
 | 
				
			||||||
 | 
					            name='user_to_notify',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(help_text='Useraccount der benachrichtigt wird', on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL, verbose_name='Empfänger*in'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='report',
 | 
				
			||||||
 | 
					            name='created_at',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='report',
 | 
				
			||||||
 | 
					            name='updated_at',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert am'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='rule',
 | 
				
			||||||
 | 
					            name='created_at',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='rule',
 | 
				
			||||||
 | 
					            name='language',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.language', verbose_name='Sprache'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='rule',
 | 
				
			||||||
 | 
					            name='rule_identifier',
 | 
				
			||||||
 | 
					            field=models.CharField(help_text='Ein eindeutiger Identifikator der Regel. Ein Regelobjekt derselben Regel in einer anderen Sprache muss den gleichen Identifikator haben', max_length=24, verbose_name='Regel-ID'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='rule',
 | 
				
			||||||
 | 
					            name='rule_text',
 | 
				
			||||||
 | 
					            field=models.TextField(verbose_name='Regeltext'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='rule',
 | 
				
			||||||
 | 
					            name='updated_at',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert am'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='searchsubscription',
 | 
				
			||||||
 | 
					            name='created_at',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='searchsubscription',
 | 
				
			||||||
 | 
					            name='sex',
 | 
				
			||||||
 | 
					            field=models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich Kastriert'), ('I', 'Intergeschlechtlich'), ('A', 'Alle')], max_length=20, verbose_name='Geschlecht'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='searchsubscription',
 | 
				
			||||||
 | 
					            name='updated_at',
 | 
				
			||||||
 | 
					            field=models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert am'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='subscriptions',
 | 
				
			||||||
 | 
					            name='adoption_notice',
 | 
				
			||||||
 | 
					            field=models.ForeignKey(help_text='Vermittlung die abonniert wurde', on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='text',
 | 
				
			||||||
 | 
					            name='title',
 | 
				
			||||||
 | 
					            field=models.CharField(max_length=100, verbose_name='Titel'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										18
									
								
								src/fellchensammlung/migrations/0065_species_slug.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-09-06 13:02
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0064_alter_animal_name_alter_animal_photos_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='species',
 | 
				
			||||||
 | 
					            name='slug',
 | 
				
			||||||
 | 
					            field=models.SlugField(null=True, unique=True, verbose_name='Slug'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										20
									
								
								src/fellchensammlung/migrations/0066_add_slug_to_species.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-09-06 13:05
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def migrate_slug(apps, schema_editor):
 | 
				
			||||||
 | 
					    Species = apps.get_model("fellchensammlung", "Species")
 | 
				
			||||||
 | 
					    for species in Species.objects.all():
 | 
				
			||||||
 | 
					        species.slug = f"species-{species.id}"
 | 
				
			||||||
 | 
					        species.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0065_species_slug'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.RunPython(migrate_slug),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										18
									
								
								src/fellchensammlung/migrations/0067_alter_species_slug.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-09-06 13:12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0066_add_slug_to_species'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='species',
 | 
				
			||||||
 | 
					            name='slug',
 | 
				
			||||||
 | 
					            field=models.SlugField(unique=True, verbose_name='Slug'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-09-29 15:33
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0067_alter_species_slug'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='adoptionnotice',
 | 
				
			||||||
 | 
					            name='adoption_notice_status',
 | 
				
			||||||
 | 
					            field=models.TextField(choices=[('active_searching', 'Searching'), ('active_interested', 'Interested'), ('awaiting_action_waiting_for_review', 'Waiting for review'), ('awaiting_action_needs_additional_info', 'Needs additional info'), ('awaiting_action_unchecked', 'Unchecked'), ('closed_successful_with_notfellchen', 'Successful (with Notfellchen)'), ('closed_successful_without_notfellchen', 'Successful (without Notfellchen)'), ('closed_animal_died', 'Animal died'), ('closed_for_other_adoption_notice', 'Closed for other adoption notice'), ('closed_not_open_for_adoption_anymore', 'Not open for adoption anymore'), ('closed_link_to_more_info_not_reachable', 'Der Link zu weiteren Informationen ist nicht mehr erreichbar.'), ('closed_other', 'Other (closed)'), ('disabled_against_the_rules', 'Against the rules'), ('disabled_other', 'Other (disabled)')], max_length=64, verbose_name='Status'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='image',
 | 
				
			||||||
 | 
					            name='image',
 | 
				
			||||||
 | 
					            field=models.ImageField(help_text='Wähle ein Bild aus', upload_to='images', verbose_name='Bild'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 5.2.1 on 2025-10-20 08:43
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('fellchensammlung', '0068_alter_adoptionnotice_adoption_notice_status_and_more'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='rescueorganization',
 | 
				
			||||||
 | 
					            name='regular_check_status',
 | 
				
			||||||
 | 
					            field=models.CharField(choices=[('regular_check', 'Wird regelmäßig geprüft'), ('excluded_no_online_listing', 'Exkludiert: Tiere werden nicht online gelistet'), ('excluded_other_org', 'Exkludiert: Andere Organisation wird geprüft'), ('excluded_scope', 'Exkludiert: Organisation hat nie Notfellchen-relevanten Vermittlungen'), ('excluded_other', 'Exkludiert: Anderer Grund')], default='regular_check', help_text='Organisationen können, durch ändern dieser Einstellung, von der regelmäßigen Prüfung ausgeschlossen werden.', max_length=30, verbose_name='Status der regelmäßigen Prüfung'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -1,19 +1,23 @@
 | 
				
			|||||||
from django.db.models.signals import post_save
 | 
					from django.db.models.signals import post_save
 | 
				
			||||||
from django.dispatch import receiver
 | 
					from django.dispatch import receiver
 | 
				
			||||||
from fellchensammlung.models import BaseNotification, CommentNotification, User, TrustLevel
 | 
					from fellchensammlung.models import Notification, User, TrustLevel, RescueOrganization, \
 | 
				
			||||||
 | 
					    NotificationTypeChoices
 | 
				
			||||||
from .tasks import task_send_notification_email
 | 
					from .tasks import task_send_notification_email
 | 
				
			||||||
from notfellchen.settings import host
 | 
					from notfellchen.settings import host
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@receiver(post_save, sender=CommentNotification)
 | 
					@receiver(post_save, sender=Notification)
 | 
				
			||||||
def comment_notification_receiver(sender, instance: BaseNotification, created: bool, **kwargs):
 | 
					def base_notification_receiver(sender, instance: Notification, created: bool, **kwargs):
 | 
				
			||||||
    base_notification_receiver(sender, instance, created, **kwargs)
 | 
					    if not created or not instance.user_to_notify.email_notifications:
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        task_send_notification_email.delay(instance.pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@receiver(post_save, sender=BaseNotification)
 | 
					@receiver(post_save, sender=RescueOrganization)
 | 
				
			||||||
def base_notification_receiver(sender, instance: BaseNotification, created: bool, **kwargs):
 | 
					def rescue_org_receiver(sender, instance: RescueOrganization, created: bool, **kwargs):
 | 
				
			||||||
    if not created or not instance.user.email_notifications:
 | 
					    if instance.location:
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        task_send_notification_email.delay(instance.pk)
 | 
					        task_send_notification_email.delay(instance.pk)
 | 
				
			||||||
@@ -33,5 +37,9 @@ def notification_new_user(sender, instance: User, created: bool, **kwargs):
 | 
				
			|||||||
    link_text = f"Um alle Details zu sehen, geh bitte auf: {user_url}"
 | 
					    link_text = f"Um alle Details zu sehen, geh bitte auf: {user_url}"
 | 
				
			||||||
    body_text = new_user_text + user_detail_text + link_text
 | 
					    body_text = new_user_text + user_detail_text + link_text
 | 
				
			||||||
    for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
 | 
					    for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
 | 
				
			||||||
        notification = BaseNotification.objects.create(title=subject, text=body_text, user=moderator)
 | 
					        notification = Notification.objects.create(title=subject,
 | 
				
			||||||
 | 
					                                                   text=body_text,
 | 
				
			||||||
 | 
					                                                   notification_type=NotificationTypeChoices.NEW_USER,
 | 
				
			||||||
 | 
					                                                   user_to_notify=moderator,
 | 
				
			||||||
 | 
					                                                   user_related=instance)
 | 
				
			||||||
        notification.save()
 | 
					        notification.save()
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										29
									
								
								src/fellchensammlung/registration_views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					from django.utils.html import strip_tags
 | 
				
			||||||
 | 
					from django_registration.backends.activation.views import RegistrationView
 | 
				
			||||||
 | 
					from django.core.mail import EmailMultiAlternatives
 | 
				
			||||||
 | 
					from django.template.loader import render_to_string
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class HTMLMailRegistrationView(RegistrationView):
 | 
				
			||||||
 | 
					    def send_activation_email(self, user):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        overwrites the function in django registration
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        activation_key = self.get_activation_key(user)
 | 
				
			||||||
 | 
					        context = self.get_email_context(activation_key)
 | 
				
			||||||
 | 
					        context["user"] = user
 | 
				
			||||||
 | 
					        subject = render_to_string(
 | 
				
			||||||
 | 
					            template_name=self.email_subject_template,
 | 
				
			||||||
 | 
					            context=context,
 | 
				
			||||||
 | 
					            request=self.request,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        # Force subject to a single line to avoid header-injection issues.
 | 
				
			||||||
 | 
					        subject = "".join(subject.splitlines())
 | 
				
			||||||
 | 
					        message = render_to_string(
 | 
				
			||||||
 | 
					            template_name=self.email_body_template,
 | 
				
			||||||
 | 
					            context=context,
 | 
				
			||||||
 | 
					            request=self.request,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        plain_message = strip_tags(message)
 | 
				
			||||||
 | 
					        user.email_user(subject, plain_message, settings.DEFAULT_FROM_EMAIL, html_message=message)
 | 
				
			||||||
							
								
								
									
										45
									
								
								src/fellchensammlung/sitemap.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					from django.contrib.sitemaps import Sitemap
 | 
				
			||||||
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					from .models import AdoptionNotice, RescueOrganization, ImportantLocation, Animal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StaticViewSitemap(Sitemap):
 | 
				
			||||||
 | 
					    priority = 0.8
 | 
				
			||||||
 | 
					    changefreq = "weekly"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def items(self):
 | 
				
			||||||
 | 
					        return ["index", "search", "map", "about", "rescue-organizations", "buying", "imprint", "terms-of-service",
 | 
				
			||||||
 | 
					                "privacy"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def location(self, item):
 | 
				
			||||||
 | 
					        return reverse(item)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AdoptionNoticeSitemap(Sitemap):
 | 
				
			||||||
 | 
					    priority = 0.5
 | 
				
			||||||
 | 
					    changefreq = "daily"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def items(self):
 | 
				
			||||||
 | 
					        return AdoptionNotice.get_active_ANs()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def lastmod(self, obj):
 | 
				
			||||||
 | 
					        return obj.updated_at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RescueOrganizationSitemap(Sitemap):
 | 
				
			||||||
 | 
					    priority = 0.3
 | 
				
			||||||
 | 
					    changefreq = "weekly"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def items(self):
 | 
				
			||||||
 | 
					        return RescueOrganization.objects.all()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def lastmod(self, obj):
 | 
				
			||||||
 | 
					        return obj.updated_at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SearchSitemap(Sitemap):
 | 
				
			||||||
 | 
					    priority = 0.5
 | 
				
			||||||
 | 
					    chanfreq = "daily"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def items(self):
 | 
				
			||||||
 | 
					        return ImportantLocation.objects.all()
 | 
				
			||||||
							
								
								
									
										363
									
								
								src/fellchensammlung/static/fellchensammlung/css/main.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,363 @@
 | 
				
			|||||||
 | 
					$primary: #6CD4FF;
 | 
				
			||||||
 | 
					$link: #292a2c;
 | 
				
			||||||
 | 
					$grey-light: #c4c6ce;
 | 
				
			||||||
 | 
					$grey-dark: #262728;
 | 
				
			||||||
 | 
					$confirm: hsl(133deg, 100%, calc(41% + 0%));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Path to Bulma's sass folder
 | 
				
			||||||
 | 
					@use "bulma/sass" with (
 | 
				
			||||||
 | 
					  $family-primary: '"Nunito", sans-serif',
 | 
				
			||||||
 | 
					  $grey-dark: $grey-dark,
 | 
				
			||||||
 | 
					  $grey-light: $grey-light,
 | 
				
			||||||
 | 
					  $primary: $primary,
 | 
				
			||||||
 | 
					  $link: $link,
 | 
				
			||||||
 | 
					  $control-border-width: 2px,
 | 
				
			||||||
 | 
					  $input-shadow: none
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@use "bulma/sass/utilities/css-variables" as cv;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@include cv.system-theme($name: "dark") {
 | 
				
			||||||
 | 
					  .navbar-item > img {
 | 
				
			||||||
 | 
					    background-color: $grey-light !important;
 | 
				
			||||||
 | 
					    border-radius: 5px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  .card-header {
 | 
				
			||||||
 | 
					    background-color: $grey-dark;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  a.card-footer-item.is-danger {
 | 
				
			||||||
 | 
					    color: black;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  .tag {
 | 
				
			||||||
 | 
					    color: $grey-dark;
 | 
				
			||||||
 | 
					    background-color: $grey-light;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// General Styles
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.main-content {
 | 
				
			||||||
 | 
					  margin: auto;
 | 
				
			||||||
 | 
					  max-width: 80em;
 | 
				
			||||||
 | 
					  padding: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					p > a {
 | 
				
			||||||
 | 
					  text-decoration: underline;
 | 
				
			||||||
 | 
					  word-break: break-all;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					p > a.button {
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Cards
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.card-header {
 | 
				
			||||||
 | 
					  background-color: $primary;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Search form suggestion dropdown
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#location-result-list {
 | 
				
			||||||
 | 
					  display: inline; //ensures that the dropdown is not restricted in width WTF
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.result-item {
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.result-item:hover {
 | 
				
			||||||
 | 
					  background-color: #b2aaaa;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Toggle switch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toggle-switch {
 | 
				
			||||||
 | 
					  display: inline-block;
 | 
				
			||||||
 | 
					  background: #ccc;
 | 
				
			||||||
 | 
					  border-radius: 16px;
 | 
				
			||||||
 | 
					  width: 58px;
 | 
				
			||||||
 | 
					  height: 32px;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  vertical-align: middle;
 | 
				
			||||||
 | 
					  transition: background 0.25s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toggle-switch:before, .toggle-switch:after {
 | 
				
			||||||
 | 
					  content: "";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toggle-switch:before {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  background: linear-gradient(to bottom, #fff 0%, #eee 100%);
 | 
				
			||||||
 | 
					  border-radius: 50%;
 | 
				
			||||||
 | 
					  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
 | 
				
			||||||
 | 
					  width: 24px;
 | 
				
			||||||
 | 
					  height: 24px;
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 4px;
 | 
				
			||||||
 | 
					  left: 4px;
 | 
				
			||||||
 | 
					  transition: left 0.25s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toggle:hover .toggle-switch:before {
 | 
				
			||||||
 | 
					  background: linear-gradient(to bottom, #fff 0%, #fff 100%);
 | 
				
			||||||
 | 
					  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.checked + .toggle-switch {
 | 
				
			||||||
 | 
					  background: #56c080;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.checked + .toggle-switch:before {
 | 
				
			||||||
 | 
					  left: 30px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toggle-checkbox {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  visibility: hidden;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.slider-label {
 | 
				
			||||||
 | 
					  margin-left: 5px;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  top: 2px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Button in card footer
 | 
				
			||||||
 | 
					.card-footer {
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.card-footer .card-footer-item.is-confirm {
 | 
				
			||||||
 | 
					  background-color: $confirm;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.card-footer .card-footer-item.is-confirm:hover {
 | 
				
			||||||
 | 
					  filter: brightness(0.9);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.card-footer .card-footer-item.is-danger {
 | 
				
			||||||
 | 
					  background-color: sass.$danger;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.card-footer .card-footer-item.is-danger:hover {
 | 
				
			||||||
 | 
					  filter: brightness(0.9);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.card-footer .card-footer-item.is-warning {
 | 
				
			||||||
 | 
					  background-color: sass.$warning;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.card-footer .card-footer-item.is-warning:hover {
 | 
				
			||||||
 | 
					  filter: brightness(0.9);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*******/
 | 
				
			||||||
 | 
					/* MAP */
 | 
				
			||||||
 | 
					/*******/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.map {
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 100%
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.marker {
 | 
				
			||||||
 | 
					  background-image: url('../img/logo_transparent.png');
 | 
				
			||||||
 | 
					  background-size: cover;
 | 
				
			||||||
 | 
					  width: 50px;
 | 
				
			||||||
 | 
					  height: 50px;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.animal-shelter-marker {
 | 
				
			||||||
 | 
					  background-image: url('../img/animal_shelter.png');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.map-in-content #map {
 | 
				
			||||||
 | 
					  max-height: 500px;
 | 
				
			||||||
 | 
					  width: 90%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media only screen and (min-width: 768px) {
 | 
				
			||||||
 | 
					  .maplibregl-popup {
 | 
				
			||||||
 | 
					    max-width: 280px !important;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media only screen and (max-width: 768px) {
 | 
				
			||||||
 | 
					  .maplibregl-popup {
 | 
				
			||||||
 | 
					    max-width: 150px !important;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.maplibregl-popup-close-button {
 | 
				
			||||||
 | 
					  all: unset; /* Remove all inherited styles */
 | 
				
			||||||
 | 
					  font-size: 1.2rem;
 | 
				
			||||||
 | 
					  background: none;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  color: black;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 0.25rem;
 | 
				
			||||||
 | 
					  right: 0.5rem;
 | 
				
			||||||
 | 
					  padding: 0.25rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.popup-content {
 | 
				
			||||||
 | 
					  line-height: 1.4;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*****
 | 
				
			||||||
 | 
					IMAGES
 | 
				
			||||||
 | 
					 *****/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.gallery .main-photo img {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 150px;
 | 
				
			||||||
 | 
					  object-fit: cover; /* Crops the images */
 | 
				
			||||||
 | 
					  border-radius: 6px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.thumbnail-row {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  gap: 10px;
 | 
				
			||||||
 | 
					  margin-top: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.thumbnail img {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 70px;
 | 
				
			||||||
 | 
					  object-fit: cover; /* Crops the images */
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Ensure each thumbnail takes equal width */
 | 
				
			||||||
 | 
					.thumbnail {
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					AN Cards
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.an-card {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Fonts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					  font-family: 'Nunito';
 | 
				
			||||||
 | 
					  font-style: normal;
 | 
				
			||||||
 | 
					  font-weight: 400;
 | 
				
			||||||
 | 
					  src: url(../fonts/nunito.woff2) format('woff2');
 | 
				
			||||||
 | 
					  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.new-animal-ad fieldset {
 | 
				
			||||||
 | 
					  border-top: 4px solid var(--bulma-text-weak);
 | 
				
			||||||
 | 
					  margin-top: 2em;
 | 
				
			||||||
 | 
					  padding-top: 1em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.new-animal-ad * {
 | 
				
			||||||
 | 
					  transition: all ease 0.5s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.new-animal-ad .open {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.new-animal-ad .closed {
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.new-animal-ad legend {
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					  padding-right: 0.2em;
 | 
				
			||||||
 | 
					  color: var(--bulma-label-color);
 | 
				
			||||||
 | 
					  font-size: 130%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.feedback-backdrop {
 | 
				
			||||||
 | 
					  position: fixed;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  bottom: 0;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  right: 0;
 | 
				
			||||||
 | 
					  background-color: rgba(0, 0, 0, 0.4);
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.feedback-add-new {
 | 
				
			||||||
 | 
					  width: 40ch;
 | 
				
			||||||
 | 
					  min-height: 40ch;
 | 
				
			||||||
 | 
					  padding: 1.5em;
 | 
				
			||||||
 | 
					  background-color: var(--bulma-info-on-scheme);
 | 
				
			||||||
 | 
					  color: black;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.feedback-add-new.error {
 | 
				
			||||||
 | 
					  background-color: var(--bulma-danger-on-scheme);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.feedback-add-new.success {
 | 
				
			||||||
 | 
					  background-color: var(--bulma-success-on-scheme);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.notification-container {
 | 
				
			||||||
 | 
					    display: inline-block;
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.notification-label {
 | 
				
			||||||
 | 
					  padding: 2px 5px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Make the badge float in the top right corner of the button */
 | 
				
			||||||
 | 
					.notification-badge {
 | 
				
			||||||
 | 
					    background-color: #fa3e3e;
 | 
				
			||||||
 | 
					    border-radius: 2px;
 | 
				
			||||||
 | 
					    color: white;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    padding: 1px 3px;
 | 
				
			||||||
 | 
					    font-size: 8px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    position: absolute; /* Position the badge within the relatively positioned button */
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    right: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Embedding Specifics */
 | 
				
			||||||
 | 
					.embed-main-content {
 | 
				
			||||||
 | 
					  padding: 20px 10px 20px 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FLOATING BUTTON
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.floating {
 | 
				
			||||||
 | 
					  position: fixed;
 | 
				
			||||||
 | 
					  border-radius: 0.3rem;
 | 
				
			||||||
 | 
					  bottom: 4.5rem;
 | 
				
			||||||
 | 
					  right: 1rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										420
									
								
								src/fellchensammlung/static/fellchensammlung/css/photoswipe.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,420 @@
 | 
				
			|||||||
 | 
					/*! PhotoSwipe main CSS by Dmytro Semenov | photoswipe.com */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp {
 | 
				
			||||||
 | 
					  --pswp-bg: #000;
 | 
				
			||||||
 | 
					  --pswp-placeholder-bg: #222;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  --pswp-root-z-index: 100000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  --pswp-preloader-color: rgba(79, 79, 79, 0.4);
 | 
				
			||||||
 | 
					  --pswp-preloader-color-secondary: rgba(255, 255, 255, 0.9);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /* defined via js:
 | 
				
			||||||
 | 
					  --pswp-transition-duration: 333ms; */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  --pswp-icon-color: #fff;
 | 
				
			||||||
 | 
					  --pswp-icon-color-secondary: #4f4f4f;
 | 
				
			||||||
 | 
					  --pswp-icon-stroke-color: #4f4f4f;
 | 
				
			||||||
 | 
					  --pswp-icon-stroke-width: 2px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  --pswp-error-text-color: var(--pswp-icon-color);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
						Styles for basic PhotoSwipe (pswp) functionality (sliding area, open/close transitions)
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp {
 | 
				
			||||||
 | 
						position: fixed;
 | 
				
			||||||
 | 
						z-index: var(--pswp-root-z-index);
 | 
				
			||||||
 | 
						display: none;
 | 
				
			||||||
 | 
						touch-action: none;
 | 
				
			||||||
 | 
						outline: 0;
 | 
				
			||||||
 | 
						opacity: 0.003;
 | 
				
			||||||
 | 
						contain: layout style size;
 | 
				
			||||||
 | 
						-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Prevents focus outline on the root element,
 | 
				
			||||||
 | 
					  (it may be focused initially) */
 | 
				
			||||||
 | 
					.pswp:focus {
 | 
				
			||||||
 | 
					  outline: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp * {
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp img {
 | 
				
			||||||
 | 
					  max-width: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp--open {
 | 
				
			||||||
 | 
						display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp,
 | 
				
			||||||
 | 
					.pswp__bg {
 | 
				
			||||||
 | 
						transform: translateZ(0);
 | 
				
			||||||
 | 
						will-change: opacity;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp__bg {
 | 
				
			||||||
 | 
					  opacity: 0.005;
 | 
				
			||||||
 | 
						background: var(--pswp-bg);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp,
 | 
				
			||||||
 | 
					.pswp__scroll-wrap {
 | 
				
			||||||
 | 
						overflow: hidden;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp,
 | 
				
			||||||
 | 
					.pswp__scroll-wrap,
 | 
				
			||||||
 | 
					.pswp__bg,
 | 
				
			||||||
 | 
					.pswp__container,
 | 
				
			||||||
 | 
					.pswp__item,
 | 
				
			||||||
 | 
					.pswp__content,
 | 
				
			||||||
 | 
					.pswp__img,
 | 
				
			||||||
 | 
					.pswp__zoom-wrap {
 | 
				
			||||||
 | 
						position: absolute;
 | 
				
			||||||
 | 
						top: 0;
 | 
				
			||||||
 | 
						left: 0;
 | 
				
			||||||
 | 
						width: 100%;
 | 
				
			||||||
 | 
						height: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp {
 | 
				
			||||||
 | 
						position: fixed;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp__img,
 | 
				
			||||||
 | 
					.pswp__zoom-wrap {
 | 
				
			||||||
 | 
						width: auto;
 | 
				
			||||||
 | 
						height: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp--click-to-zoom.pswp--zoom-allowed .pswp__img {
 | 
				
			||||||
 | 
						cursor: -webkit-zoom-in;
 | 
				
			||||||
 | 
						cursor: -moz-zoom-in;
 | 
				
			||||||
 | 
						cursor: zoom-in;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp--click-to-zoom.pswp--zoomed-in .pswp__img {
 | 
				
			||||||
 | 
						cursor: move;
 | 
				
			||||||
 | 
						cursor: -webkit-grab;
 | 
				
			||||||
 | 
						cursor: -moz-grab;
 | 
				
			||||||
 | 
						cursor: grab;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp--click-to-zoom.pswp--zoomed-in .pswp__img:active {
 | 
				
			||||||
 | 
					  cursor: -webkit-grabbing;
 | 
				
			||||||
 | 
					  cursor: -moz-grabbing;
 | 
				
			||||||
 | 
					  cursor: grabbing;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* :active to override grabbing cursor */
 | 
				
			||||||
 | 
					.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img,
 | 
				
			||||||
 | 
					.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img:active,
 | 
				
			||||||
 | 
					.pswp__img {
 | 
				
			||||||
 | 
						cursor: -webkit-zoom-out;
 | 
				
			||||||
 | 
						cursor: -moz-zoom-out;
 | 
				
			||||||
 | 
						cursor: zoom-out;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Prevent selection and tap highlights */
 | 
				
			||||||
 | 
					.pswp__container,
 | 
				
			||||||
 | 
					.pswp__img,
 | 
				
			||||||
 | 
					.pswp__button,
 | 
				
			||||||
 | 
					.pswp__counter {
 | 
				
			||||||
 | 
						-webkit-user-select: none;
 | 
				
			||||||
 | 
						-moz-user-select: none;
 | 
				
			||||||
 | 
						-ms-user-select: none;
 | 
				
			||||||
 | 
						user-select: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp__item {
 | 
				
			||||||
 | 
						/* z-index for fade transition */
 | 
				
			||||||
 | 
						z-index: 1;
 | 
				
			||||||
 | 
						overflow: hidden;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp__hidden {
 | 
				
			||||||
 | 
						display: none !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Allow to click through pswp__content element, but not its children */
 | 
				
			||||||
 | 
					.pswp__content {
 | 
				
			||||||
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.pswp__content > * {
 | 
				
			||||||
 | 
					  pointer-events: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  PhotoSwipe UI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
						Error message appears when image is not loaded
 | 
				
			||||||
 | 
						(JS option errorMsg controls markup)
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					.pswp__error-msg-container {
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.pswp__error-msg {
 | 
				
			||||||
 | 
						margin: auto;
 | 
				
			||||||
 | 
						font-size: 1em;
 | 
				
			||||||
 | 
						line-height: 1;
 | 
				
			||||||
 | 
						color: var(--pswp-error-text-color);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					class pswp__hide-on-close is applied to elements that
 | 
				
			||||||
 | 
					should hide (for example fade out) when PhotoSwipe is closed
 | 
				
			||||||
 | 
					and show (for example fade in) when PhotoSwipe is opened
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					.pswp .pswp__hide-on-close {
 | 
				
			||||||
 | 
						opacity: 0.005;
 | 
				
			||||||
 | 
						will-change: opacity;
 | 
				
			||||||
 | 
						transition: opacity var(--pswp-transition-duration) cubic-bezier(0.4, 0, 0.22, 1);
 | 
				
			||||||
 | 
						z-index: 10; /* always overlap slide content */
 | 
				
			||||||
 | 
						pointer-events: none; /* hidden elements should not be clickable */
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* class pswp--ui-visible is added when opening or closing transition starts */
 | 
				
			||||||
 | 
					.pswp--ui-visible .pswp__hide-on-close {
 | 
				
			||||||
 | 
						opacity: 1;
 | 
				
			||||||
 | 
						pointer-events: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* <button> styles, including css reset */
 | 
				
			||||||
 | 
					.pswp__button {
 | 
				
			||||||
 | 
						position: relative;
 | 
				
			||||||
 | 
						display: block;
 | 
				
			||||||
 | 
						width: 50px;
 | 
				
			||||||
 | 
						height: 60px;
 | 
				
			||||||
 | 
						padding: 0;
 | 
				
			||||||
 | 
						margin: 0;
 | 
				
			||||||
 | 
						overflow: hidden;
 | 
				
			||||||
 | 
						cursor: pointer;
 | 
				
			||||||
 | 
						background: none;
 | 
				
			||||||
 | 
						border: 0;
 | 
				
			||||||
 | 
						box-shadow: none;
 | 
				
			||||||
 | 
						opacity: 0.85;
 | 
				
			||||||
 | 
						-webkit-appearance: none;
 | 
				
			||||||
 | 
						-webkit-touch-callout: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp__button:hover,
 | 
				
			||||||
 | 
					.pswp__button:active,
 | 
				
			||||||
 | 
					.pswp__button:focus {
 | 
				
			||||||
 | 
					  transition: none;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  background: none;
 | 
				
			||||||
 | 
					  border: 0;
 | 
				
			||||||
 | 
					  box-shadow: none;
 | 
				
			||||||
 | 
					  opacity: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp__button:disabled {
 | 
				
			||||||
 | 
					  opacity: 0.3;
 | 
				
			||||||
 | 
					  cursor: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp__icn {
 | 
				
			||||||
 | 
					  fill: var(--pswp-icon-color);
 | 
				
			||||||
 | 
					  color: var(--pswp-icon-color-secondary);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp__icn {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 14px;
 | 
				
			||||||
 | 
					  left: 9px;
 | 
				
			||||||
 | 
					  width: 32px;
 | 
				
			||||||
 | 
					  height: 32px;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp__icn-shadow {
 | 
				
			||||||
 | 
					  stroke: var(--pswp-icon-stroke-color);
 | 
				
			||||||
 | 
					  stroke-width: var(--pswp-icon-stroke-width);
 | 
				
			||||||
 | 
					  fill: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp__icn:focus {
 | 
				
			||||||
 | 
						outline: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
						div element that matches size of large image,
 | 
				
			||||||
 | 
						large image loads on top of it,
 | 
				
			||||||
 | 
						used when msrc is not provided
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					div.pswp__img--placeholder,
 | 
				
			||||||
 | 
					.pswp__img--with-bg {
 | 
				
			||||||
 | 
						background: var(--pswp-placeholder-bg);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp__top-bar {
 | 
				
			||||||
 | 
						position: absolute;
 | 
				
			||||||
 | 
						left: 0;
 | 
				
			||||||
 | 
						top: 0;
 | 
				
			||||||
 | 
						width: 100%;
 | 
				
			||||||
 | 
						height: 60px;
 | 
				
			||||||
 | 
						display: flex;
 | 
				
			||||||
 | 
					  flex-direction: row;
 | 
				
			||||||
 | 
					  justify-content: flex-end;
 | 
				
			||||||
 | 
						z-index: 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/* allow events to pass through top bar itself */
 | 
				
			||||||
 | 
						pointer-events: none !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.pswp__top-bar > * {
 | 
				
			||||||
 | 
					  pointer-events: auto;
 | 
				
			||||||
 | 
					  /* this makes transition significantly more smooth,
 | 
				
			||||||
 | 
					     even though inner elements are not animated */
 | 
				
			||||||
 | 
					  will-change: opacity;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Close button
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					.pswp__button--close {
 | 
				
			||||||
 | 
					  margin-right: 6px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Arrow buttons
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					.pswp__button--arrow {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  width: 75px;
 | 
				
			||||||
 | 
					  height: 100px;
 | 
				
			||||||
 | 
					  top: 50%;
 | 
				
			||||||
 | 
					  margin-top: -50px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp__button--arrow:disabled {
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					  cursor: default;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp__button--arrow .pswp__icn {
 | 
				
			||||||
 | 
					  top: 50%;
 | 
				
			||||||
 | 
					  margin-top: -30px;
 | 
				
			||||||
 | 
					  width: 60px;
 | 
				
			||||||
 | 
					  height: 60px;
 | 
				
			||||||
 | 
					  background: none;
 | 
				
			||||||
 | 
					  border-radius: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp--one-slide .pswp__button--arrow {
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* hide arrows on touch screens */
 | 
				
			||||||
 | 
					.pswp--touch .pswp__button--arrow {
 | 
				
			||||||
 | 
					  visibility: hidden;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* show arrows only after mouse was used */
 | 
				
			||||||
 | 
					.pswp--has_mouse .pswp__button--arrow {
 | 
				
			||||||
 | 
					  visibility: visible;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp__button--arrow--prev {
 | 
				
			||||||
 | 
					  right: auto;
 | 
				
			||||||
 | 
					  left: 0px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp__button--arrow--next {
 | 
				
			||||||
 | 
					  right: 0px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.pswp__button--arrow--next .pswp__icn {
 | 
				
			||||||
 | 
					  left: auto;
 | 
				
			||||||
 | 
					  right: 14px;
 | 
				
			||||||
 | 
					  /* flip horizontally */
 | 
				
			||||||
 | 
					  transform: scale(-1, 1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Zoom button
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					.pswp__button--zoom {
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp--zoom-allowed .pswp__button--zoom {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* "+" => "-" */
 | 
				
			||||||
 | 
					.pswp--zoomed-in .pswp__zoom-icn-bar-v {
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Loading indicator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					.pswp__preloader {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  width: 50px;
 | 
				
			||||||
 | 
					  height: 60px;
 | 
				
			||||||
 | 
					  margin-right: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp__preloader .pswp__icn {
 | 
				
			||||||
 | 
					  opacity: 0;
 | 
				
			||||||
 | 
					  transition: opacity 0.2s linear;
 | 
				
			||||||
 | 
					  animation: pswp-clockwise 600ms linear infinite;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp__preloader--active .pswp__icn {
 | 
				
			||||||
 | 
					  opacity: 0.85;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes pswp-clockwise {
 | 
				
			||||||
 | 
					  0% { transform: rotate(0deg); }
 | 
				
			||||||
 | 
					  100% { transform: rotate(360deg); }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  "1 of 10" counter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
 | 
					.pswp__counter {
 | 
				
			||||||
 | 
					  height: 30px;
 | 
				
			||||||
 | 
					  margin: 15px 0 0 20px;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  line-height: 30px;
 | 
				
			||||||
 | 
					  color: var(--pswp-icon-color);
 | 
				
			||||||
 | 
					  text-shadow: 1px 1px 3px var(--pswp-icon-color-secondary);
 | 
				
			||||||
 | 
					  opacity: 0.85;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.pswp--one-slide .pswp__counter {
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,27 +1,15 @@
 | 
				
			|||||||
:root {
 | 
					:root {
 | 
				
			||||||
    --primary-light-one: #5daa68;
 | 
					    --primary: #6CD4FF;
 | 
				
			||||||
    --primary-light-two: #4a9455;
 | 
					    --link: #292a2c;
 | 
				
			||||||
    --primary-dark-one: #17311b;
 | 
					    --grey-light: #c4c6ce;
 | 
				
			||||||
    --secondary-light-one: #faf1cf;
 | 
					    --grey-dark: #262728;
 | 
				
			||||||
    --secondary-light-two: #e1d7b5;
 | 
					 | 
				
			||||||
    --background-one: var(--primary-light-one);
 | 
					 | 
				
			||||||
    --background-two: var(--primary-light-two);
 | 
					 | 
				
			||||||
    --background-three: var(--secondary-light-one);
 | 
					 | 
				
			||||||
    --background-four: var(--primary-dark-one);
 | 
					 | 
				
			||||||
    --highlight-one: var(--primary-dark-one);
 | 
					 | 
				
			||||||
    --highlight-one-text: var(--secondary-light-one);
 | 
					 | 
				
			||||||
    --text-one: var(--secondary-light-one);
 | 
					 | 
				
			||||||
    --shadow-one: var(--primary-dark-one);
 | 
					 | 
				
			||||||
    --text-two: var(--primary-dark-one);
 | 
					 | 
				
			||||||
    --text-three: var(--primary-light-one);
 | 
					 | 
				
			||||||
    --shadow-three: var(--primary-dark-one);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
body {
 | 
					body {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    flex-direction: column;
 | 
					    flex-direction: column;
 | 
				
			||||||
    background-color: var(--background-one);
 | 
					    background-color: hsl(221, 14%, 100%)r;
 | 
				
			||||||
    color: var(--text-one);
 | 
					    color: #000000;
 | 
				
			||||||
    margin: 0;
 | 
					    margin: 0;
 | 
				
			||||||
    height: 100%;
 | 
					    height: 100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -32,24 +20,22 @@ body {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
alert-box {
 | 
					alert-box {
 | 
				
			||||||
    color: var(--highlight-one);
 | 
					 | 
				
			||||||
    display: block;
 | 
					    display: block;
 | 
				
			||||||
    margin: 3rem 0;
 | 
					    margin: 3rem 0;
 | 
				
			||||||
    padding: 2rem 3rem;
 | 
					    padding: 2rem 3rem;
 | 
				
			||||||
    border: 1px solid var(--highlight-one);
 | 
					    border: 1px solid var(--highlight-one);
 | 
				
			||||||
    border-left-width: .5rem;
 | 
					    border-left-width: .5rem;
 | 
				
			||||||
    border-radius: .4rem;
 | 
					    border-radius: .4rem;
 | 
				
			||||||
    background-color: var(--background-three);
 | 
					    background-color: var(--primary);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    a {
 | 
					    a {
 | 
				
			||||||
        color: var(--text-three);
 | 
					        text-decoration: underline;
 | 
				
			||||||
        text-decoration: none;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
a {
 | 
					a {
 | 
				
			||||||
    color: inherit;
 | 
					 | 
				
			||||||
    text-decoration: none;
 | 
					    text-decoration: none;
 | 
				
			||||||
 | 
					    color: var(--link);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -65,7 +51,7 @@ a {
 | 
				
			|||||||
    margin: 1rem;
 | 
					    margin: 1rem;
 | 
				
			||||||
    padding: 5px;
 | 
					    padding: 5px;
 | 
				
			||||||
    border-radius: .4rem;
 | 
					    border-radius: .4rem;
 | 
				
			||||||
    background-color: var(--background-one);
 | 
					    border: 3px solid var(--primary);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.post-summary h1 {
 | 
					.post-summary h1 {
 | 
				
			||||||
@@ -79,8 +65,7 @@ a {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.navigation-sticky {
 | 
					.navigation-sticky {
 | 
				
			||||||
    background-color: var(--secondary-light-one);
 | 
					    background-color: var(--primary);
 | 
				
			||||||
    color: var(--primary-light-one);
 | 
					 | 
				
			||||||
    padding: 16px;
 | 
					    padding: 16px;
 | 
				
			||||||
    margin: 0;
 | 
					    margin: 0;
 | 
				
			||||||
    border-bottom-right-radius: 8px;
 | 
					    border-bottom-right-radius: 8px;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -37,13 +37,49 @@ body {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a {
 | 
				
			||||||
 | 
					    color: inherit;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					h1, h2, h3 {
 | 
				
			||||||
 | 
					    margin-bottom: 5px;
 | 
				
			||||||
 | 
					    margin-top: 5px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
table {
 | 
					table {
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
a {
 | 
					@media screen and (max-width: 600px) {
 | 
				
			||||||
    text-decoration: none;
 | 
					    .responsive thead {
 | 
				
			||||||
    color: inherit;
 | 
					        visibility: hidden;
 | 
				
			||||||
 | 
					        height: 0;
 | 
				
			||||||
 | 
					        position: absolute;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .responsive tr {
 | 
				
			||||||
 | 
					        display: block;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .responsive td {
 | 
				
			||||||
 | 
					        border: 1px solid;
 | 
				
			||||||
 | 
					        border-bottom: none;
 | 
				
			||||||
 | 
					        display: block;
 | 
				
			||||||
 | 
					        font-size: .8em;
 | 
				
			||||||
 | 
					        text-align: right;
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .responsive td::before {
 | 
				
			||||||
 | 
					        content: attr(data-label);
 | 
				
			||||||
 | 
					        float: left;
 | 
				
			||||||
 | 
					        font-weight: bold;
 | 
				
			||||||
 | 
					        text-transform: uppercase;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .responsive td:last-child {
 | 
				
			||||||
 | 
					        border-bottom: 1px solid;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
table {
 | 
					table {
 | 
				
			||||||
@@ -65,7 +101,7 @@ td {
 | 
				
			|||||||
    padding: 5px;
 | 
					    padding: 5px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
th {
 | 
					thead td {
 | 
				
			||||||
    border: 3px solid black;
 | 
					    border: 3px solid black;
 | 
				
			||||||
    border-collapse: collapse;
 | 
					    border-collapse: collapse;
 | 
				
			||||||
    padding: 8px;
 | 
					    padding: 8px;
 | 
				
			||||||
@@ -99,11 +135,16 @@ textarea {
 | 
				
			|||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.container-cards h1,
 | 
				
			||||||
 | 
					.container-cards h2 {
 | 
				
			||||||
 | 
					    width: 100%; /* Make sure heading fills complete line */
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.card {
 | 
					.card {
 | 
				
			||||||
    flex: 1 25%;
 | 
					    flex: 1 25%;
 | 
				
			||||||
    margin: 10px;
 | 
					    margin: 10px;
 | 
				
			||||||
    border-radius: 8px;
 | 
					    border-radius: 8px;
 | 
				
			||||||
    padding: 5px;
 | 
					    padding: 8px;
 | 
				
			||||||
    background: var(--background-three);
 | 
					    background: var(--background-three);
 | 
				
			||||||
    color: var(--text-two);
 | 
					    color: var(--text-two);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -117,8 +158,8 @@ textarea {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.spaced {
 | 
					.spaced > * {
 | 
				
			||||||
    margin-bottom: 30px;
 | 
					    margin: 10px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*******************************/
 | 
					/*******************************/
 | 
				
			||||||
@@ -201,7 +242,11 @@ select, .button {
 | 
				
			|||||||
    display: block;
 | 
					    display: block;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.btn2 {
 | 
					a.btn, a.btn2, a.nav-link {
 | 
				
			||||||
 | 
					    text-decoration: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn2, .btn3 {
 | 
				
			||||||
    background-color: var(--secondary-light-one);
 | 
					    background-color: var(--secondary-light-one);
 | 
				
			||||||
    color: var(--primary-dark-one);
 | 
					    color: var(--primary-dark-one);
 | 
				
			||||||
    padding: 8px;
 | 
					    padding: 8px;
 | 
				
			||||||
@@ -210,6 +255,163 @@ select, .button {
 | 
				
			|||||||
    margin: 5px;
 | 
					    margin: 5px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn3 {
 | 
				
			||||||
 | 
					    border: 1px solid black;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.btn-small {
 | 
				
			||||||
 | 
					    font-size: medium;
 | 
				
			||||||
 | 
					    padding: 6px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.checkmark {
 | 
				
			||||||
 | 
					    display: inline-block;
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    left: 0.2rem;
 | 
				
			||||||
 | 
					    bottom: 0.075rem;
 | 
				
			||||||
 | 
					    background-color: var(--primary-light-one);
 | 
				
			||||||
 | 
					    color: var(--secondary-light-one);
 | 
				
			||||||
 | 
					    border-radius: 0.5rem;
 | 
				
			||||||
 | 
					    width: 1.5rem;
 | 
				
			||||||
 | 
					    height: 1.5rem;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.switch {
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					    display: inline-block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toggle-switch {
 | 
				
			||||||
 | 
					    display: inline-block;
 | 
				
			||||||
 | 
					    background: #ccc;
 | 
				
			||||||
 | 
					    border-radius: 16px;
 | 
				
			||||||
 | 
					    width: 58px;
 | 
				
			||||||
 | 
					    height: 32px;
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    vertical-align: middle;
 | 
				
			||||||
 | 
					    transition: background 0.25s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toggle-switch:before, .toggle-switch:after {
 | 
				
			||||||
 | 
					    content: "";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toggle-switch:before {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					    background: linear-gradient(to bottom, #fff 0%, #eee 100%);
 | 
				
			||||||
 | 
					    border-radius: 50%;
 | 
				
			||||||
 | 
					    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
 | 
				
			||||||
 | 
					    width: 24px;
 | 
				
			||||||
 | 
					    height: 24px;
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 4px;
 | 
				
			||||||
 | 
					    left: 4px;
 | 
				
			||||||
 | 
					    transition: left 0.25s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toggle:hover .toggle-switch:before {
 | 
				
			||||||
 | 
					    background: linear-gradient(to bottom, #fff 0%, #fff 100%);
 | 
				
			||||||
 | 
					    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.checked + .toggle-switch {
 | 
				
			||||||
 | 
					    background: #56c080;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.checked + .toggle-switch:before {
 | 
				
			||||||
 | 
					    left: 30px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toggle-checkbox {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    visibility: hidden;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.slider-label {
 | 
				
			||||||
 | 
					    margin-left: 5px;
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    top: 2px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Refactor tooltip based on https://luigicavalieri.com/blog/css-tooltip-appearing-from-any-direction/ to allow different directions */
 | 
				
			||||||
 | 
					.tooltip {
 | 
				
			||||||
 | 
					    display: inline-flex;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tooltip:hover .tooltiptext {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					    visibility: visible;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tooltip .tooltiptext {
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					    bottom: calc(100% + 0.6em + 2px);
 | 
				
			||||||
 | 
					    box-shadow: 0px 2px 4px #07172258;
 | 
				
			||||||
 | 
					    background-color: var(--primary-dark-one);
 | 
				
			||||||
 | 
					    color: var(--secondary-light-one);
 | 
				
			||||||
 | 
					    font-size: 0.68rem;
 | 
				
			||||||
 | 
					    justify-content: center;
 | 
				
			||||||
 | 
					    line-height: 1.35em;
 | 
				
			||||||
 | 
					    padding: 0.5em 0.7em;
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					    width: 7rem;
 | 
				
			||||||
 | 
					    z-index: 1;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    opacity: 0;
 | 
				
			||||||
 | 
					    transition: all 0.3s ease-in;
 | 
				
			||||||
 | 
					    visibility: hidden;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tooltip .tooltiptext::before {
 | 
				
			||||||
 | 
					    border-width: 0.6em 0.8em 0;
 | 
				
			||||||
 | 
					    border-color: transparent;
 | 
				
			||||||
 | 
					    border-top-color: var(--primary-dark-one);
 | 
				
			||||||
 | 
					    content: "";
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					    border-style: solid;
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Makes the tooltip fly from above */
 | 
				
			||||||
 | 
					.tooltip.top .tooltiptext {
 | 
				
			||||||
 | 
					    margin-bottom: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tooltip.top:hover .tooltiptext {
 | 
				
			||||||
 | 
					    margin-bottom: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Make adjustments for bottom */
 | 
				
			||||||
 | 
					.tooltip.bottom .tooltiptext {
 | 
				
			||||||
 | 
					    top: calc(100% + 0.6em + 2px);
 | 
				
			||||||
 | 
					    margin-top: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tooltip.bottom:hover .tooltiptext {
 | 
				
			||||||
 | 
					    margin-top: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tooltip.bottom .tooltiptext::before {
 | 
				
			||||||
 | 
					    transform: rotate(180deg);
 | 
				
			||||||
 | 
					    /* 100% of the height of .tooltip */
 | 
				
			||||||
 | 
					    bottom: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tooltip:not(.top) .tooltiptext {
 | 
				
			||||||
 | 
					    bottom: auto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tooltip:not(.top) .tooltiptext::before {
 | 
				
			||||||
 | 
					    top: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*********************/
 | 
					/*********************/
 | 
				
			||||||
/* UNIQUE COMPONENTS */
 | 
					/* UNIQUE COMPONENTS */
 | 
				
			||||||
@@ -543,12 +745,22 @@ select, .button {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.header-card-adoption-notice {
 | 
					.header-card-adoption-notice {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    justify-content: space-evenly;
 | 
					    justify-content: space-between;
 | 
				
			||||||
    align-items: center;
 | 
					    align-items: center;
 | 
				
			||||||
    flex-wrap: wrap;
 | 
					    flex-wrap: wrap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.search-subscription-header {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    justify-content: space-evenly;
 | 
				
			||||||
 | 
					    flex-wrap: wrap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    h3 {
 | 
				
			||||||
 | 
					        width: 80%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.table-adoption-notice-info {
 | 
					.table-adoption-notice-info {
 | 
				
			||||||
    margin-top: 10px;
 | 
					    margin-top: 10px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -593,15 +805,30 @@ select, .button {
 | 
				
			|||||||
.adoption-card-report-link, .notification-card-mark-read {
 | 
					.adoption-card-report-link, .notification-card-mark-read {
 | 
				
			||||||
    margin-left: auto;
 | 
					    margin-left: auto;
 | 
				
			||||||
    font-size: 2rem;
 | 
					    font-size: 2rem;
 | 
				
			||||||
    padding: 10px;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.adoption-card-report-link {
 | 
				
			||||||
 | 
					    margin-right: 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.notification-card-mark-read {
 | 
					.notification-card-mark-read {
 | 
				
			||||||
    display: inline;
 | 
					    display: inline;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.heading-card-adoption-notice {
 | 
					.inline-container {
 | 
				
			||||||
 | 
					    display: inline-block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.inline-container > * {
 | 
				
			||||||
 | 
					    vertical-align: middle;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					h2.heading-card-adoption-notice {
 | 
				
			||||||
 | 
					    font-size: 2rem;
 | 
				
			||||||
 | 
					    line-height: 2rem;
 | 
				
			||||||
    word-wrap: anywhere;
 | 
					    word-wrap: anywhere;
 | 
				
			||||||
 | 
					    width: 80%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.tags {
 | 
					.tags {
 | 
				
			||||||
@@ -616,17 +843,15 @@ select, .button {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.detail-adoption-notice-header h1 {
 | 
					.detail-adoption-notice-header h1 {
 | 
				
			||||||
    width: 50%;
 | 
					 | 
				
			||||||
    display: inline-block;
 | 
					    display: inline-block;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.detail-adoption-notice-header a {
 | 
					.detail-adoption-notice-header a {
 | 
				
			||||||
    display: inline-block;
 | 
					 | 
				
			||||||
    float: right;
 | 
					    float: right;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media (max-width: 920px) {
 | 
					@media (max-width: 920px) {
 | 
				
			||||||
    .detail-adoption-notice-header h1 {
 | 
					    .detail-adoption-notice-header .inline-container {
 | 
				
			||||||
        width: 100%;
 | 
					        width: 100%;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -644,7 +869,7 @@ select, .button {
 | 
				
			|||||||
    padding: 5px;
 | 
					    padding: 5px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.comment, .notification {
 | 
					.comment, .notification, .search-subscription {
 | 
				
			||||||
    flex: 1 100%;
 | 
					    flex: 1 100%;
 | 
				
			||||||
    margin: 10px;
 | 
					    margin: 10px;
 | 
				
			||||||
    border-radius: 8px;
 | 
					    border-radius: 8px;
 | 
				
			||||||
@@ -660,7 +885,16 @@ select, .button {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.announcement {
 | 
					.announcement-header {
 | 
				
			||||||
 | 
					    font-size: 1.2rem;
 | 
				
			||||||
 | 
					    margin: 0px;
 | 
				
			||||||
 | 
					    padding: 0px;
 | 
				
			||||||
 | 
					    color: var(--text-two);
 | 
				
			||||||
 | 
					    text-shadow: none;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					div.announcement {
 | 
				
			||||||
    flex: 1 100%;
 | 
					    flex: 1 100%;
 | 
				
			||||||
    margin: 10px;
 | 
					    margin: 10px;
 | 
				
			||||||
    border-radius: 8px;
 | 
					    border-radius: 8px;
 | 
				
			||||||
@@ -668,13 +902,6 @@ select, .button {
 | 
				
			|||||||
    background: var(--background-three);
 | 
					    background: var(--background-three);
 | 
				
			||||||
    color: var(--text-two);
 | 
					    color: var(--text-two);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    h1 {
 | 
					 | 
				
			||||||
        font-size: 1.2rem;
 | 
					 | 
				
			||||||
        margin: 0px;
 | 
					 | 
				
			||||||
        padding: 0px;
 | 
					 | 
				
			||||||
        color: var(--text-two);
 | 
					 | 
				
			||||||
        text-shadow: none;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -688,6 +915,43 @@ select, .button {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.half {
 | 
				
			||||||
 | 
					    width: 49%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#results {
 | 
				
			||||||
 | 
					    margin-top: 10px;
 | 
				
			||||||
 | 
					    list-style-type: none;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.result-item {
 | 
				
			||||||
 | 
					    padding: 8px;
 | 
				
			||||||
 | 
					    margin: 4px 0;
 | 
				
			||||||
 | 
					    background-color: #ddd1a5;
 | 
				
			||||||
 | 
					    cursor: pointer;
 | 
				
			||||||
 | 
					    border-radius: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.result-item:hover {
 | 
				
			||||||
 | 
					    background-color: #ede1b5;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.label {
 | 
				
			||||||
 | 
					    border-radius: 8px;
 | 
				
			||||||
 | 
					    padding: 4px;
 | 
				
			||||||
 | 
					    color: #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.active-adoption {
 | 
				
			||||||
 | 
					    background-color: #4a9455;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.inactive-adoption {
 | 
				
			||||||
 | 
					    background-color: #000;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/************************/
 | 
					/************************/
 | 
				
			||||||
/* GENERAL HIGHLIGHTING */
 | 
					/* GENERAL HIGHLIGHTING */
 | 
				
			||||||
/************************/
 | 
					/************************/
 | 
				
			||||||
@@ -704,6 +968,14 @@ select, .button {
 | 
				
			|||||||
    border: rgba(17, 58, 224, 0.51) 4px solid;
 | 
					    border: rgba(17, 58, 224, 0.51) 4px solid;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.error {
 | 
				
			||||||
 | 
					    color: #370707;
 | 
				
			||||||
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.error::before {
 | 
				
			||||||
 | 
					    content: "⚠️";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*******/
 | 
					/*******/
 | 
				
			||||||
/* MAP */
 | 
					/* MAP */
 | 
				
			||||||
@@ -717,6 +989,11 @@ select, .button {
 | 
				
			|||||||
    cursor: pointer;
 | 
					    cursor: pointer;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.animal-shelter-marker {
 | 
				
			||||||
 | 
					    background-image: url('../img/animal_shelter.png');
 | 
				
			||||||
 | 
					!important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.maplibregl-popup {
 | 
					.maplibregl-popup {
 | 
				
			||||||
    max-width: 600px !important;
 | 
					    max-width: 600px !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								src/fellchensammlung/static/fellchensammlung/fonts/nunito.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 6.5 KiB  |