Compare commits
	
		
			796 Commits
		
	
	
		
			4e953c83ea
			...
			develop
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| c1332ee1f0 | |||
| f6240a7189 | |||
| 7a02774a29 | |||
| 8945fdc0f4 | |||
| 9f0a18ad91 | |||
| e7f26dd23a | |||
| fc5b1391df | |||
| 70bf8e2053 | |||
| caf98ba60b | |||
| d7e466050a | |||
| 34b707ef20 | |||
| 064a9bf83a | |||
| 93070a3bcd | |||
| 23c35fe7dd | |||
| d2542060a1 | |||
| 89f74cb709 | |||
| ec38012ecb | |||
| 72d45a4f47 | |||
| 8de5f162eb | |||
| dc3859d589 | |||
| b4f52c7876 | |||
| 885622e581 | |||
| a42a3fa177 | |||
| 27541c6fb6 | |||
| 14547ad621 | |||
| 8d2d80c30e | |||
| e6f5a42d15 | |||
| 052e42f76a | |||
| 3eb7dbe984 | |||
| 202dfe46c2 | |||
| 01da0f1e29 | |||
| 8ccdf50bc5 | |||
| d46ab8da6b | |||
| 1dd53a87e9 | |||
| 40bb2e54bd | |||
| 433ad9d4b9 | |||
| 231c27819d | |||
| 890309564f | |||
| e1e1f822c8 | |||
| 7a788f4c90 | |||
| 7efa626b8b | |||
| 08e20e1875 | |||
| f1c79a5f94 | |||
| 5dd1991af8 | |||
| c0edef51bd | |||
| cb703e79ae | |||
| 87066b0cea | |||
| c4976c4b34 | |||
| ee46ff9cda | |||
| d4f27e8f2f | |||
| 4a6584370e | |||
| 82d3f95c99 | |||
| dce3d89c7e | |||
| 5520590145 | |||
| efabebfdbf | |||
| 6c52246bb7 | |||
| 2c11f7c385 | |||
| 9ee0bd8e30 | |||
| 1955476d24 | |||
| 05178da029 | |||
| 7a80cf8df1 | |||
| db94ec41ed | |||
| 5582538a70 | |||
| 7aa364fc38 | |||
| 96ce5963fe | |||
| bf54bc5d51 | |||
| 93ae172431 | |||
| 03d40a5092 | |||
| 993f8f9cd2 | |||
| 8efc0aad21 | |||
| 3a6e7f5344 | |||
| dac9661d51 | |||
| b9bfa8e359 | |||
| d07589464c | |||
| 1880da5151 | 
							
								
								
									
										4
									
								
								.coveragerc
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| [run] | ||||
| omit = | ||||
|     */migrations/* | ||||
|     */tests/* | ||||
							
								
								
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -2,11 +2,18 @@ | ||||
|  | ||||
| # Database | ||||
| notfellchen | ||||
| *.sq3 | ||||
|  | ||||
| # Geojson from imports | ||||
| *.geojson | ||||
|  | ||||
| # Media storage | ||||
| static | ||||
| /static | ||||
| media | ||||
|  | ||||
| # Compiled CSS | ||||
| /src/fellchensammlung/static/fellchensammlung/css/main.css | ||||
| /src/fellchensammlung/static/fellchensammlung/css/main.css.map | ||||
|  | ||||
| # Byte-compiled / optimized / DLL files | ||||
| __pycache__/ | ||||
| @@ -161,3 +168,4 @@ dmypy.json | ||||
|  | ||||
| # Cython debug symbols | ||||
| cython_debug/ | ||||
| /node_modules/ | ||||
|   | ||||
							
								
								
									
										15
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | ||||
| ## Version 0.4.0 | ||||
|  | ||||
| Version 0.4.0 has added support for search-as-you-type when searching for animals to adopt. Furthermore, the display of | ||||
| maps in the search has been majorly improved. | ||||
|  | ||||
| Photon has been added as geocoding source option which allows to use this functionality. | ||||
|  | ||||
| Further improvements include the representation of rescue organizations and tooltips. | ||||
|  | ||||
| One of the biggest features is the addition of search subscriptions. These allow you to not only | ||||
| search for currently active adoption notices but to subscribe to that search so that you get notified if there are new | ||||
| rats in your search area in the future. | ||||
|  | ||||
| For developers the new API documentation might come in handy, it can be found at | ||||
| [/api/schema/swagger-ui/](https://notfellchen.org/api/schema/swagger-ui/) | ||||
| @@ -1,6 +1,6 @@ | ||||
| FROM python:3.11-slim | ||||
| # Use 3.11 to avoid django.core.exceptions.ImproperlyConfigured: Error loading psycopg2 or psycopg module | ||||
| MAINTAINER Julian-Samuel Gebühr | ||||
| LABEL org.opencontainers.image.authors="Julian-Samuel Gebühr" | ||||
|  | ||||
| ENV DOCKER_BUILD=true | ||||
|  | ||||
|   | ||||
							
								
								
									
										93
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -2,7 +2,7 @@ | ||||
|  | ||||
| [notfellchen.org](https://notfellchen.org) ist eine Sammelstelle für Tier-Vermittlungen. Die Idee entstand, da in der | ||||
| deutschsprachigen Rattencommunity ein wilder Mix aus Websites, Foren und Facebookgruppen besteht die Ratten vermitteln. | ||||
| Diese Website soll die bestehende Communities NICHT ersetzten, jedoch ermöglichen, dass Menschen die Ratten aufnehmen | ||||
| Diese Website soll die bestehende Communitys NICHT ersetzten, jedoch ermöglichen, dass Menschen die Ratten aufnehmen | ||||
| wollen Informationen einfach finden und nicht bereits in jeder Gruppe sein müssen. | ||||
|  | ||||
| Wir nehmen Angebote auf die | ||||
| @@ -45,21 +45,62 @@ There is a system for customizing texts in Notfellchen. Not every change of a te | ||||
| Therefore, a solution is used where a number of predefined texts per site are supported. These markdown texts will then be included in the site, if defined. | ||||
|  | ||||
| | Textcode                | Location              | | ||||
| |---------------------|----------| | ||||
| |-------------------------|-----------------------| | ||||
| | `how_to`                | Index                 | | ||||
| | `introduction`          | Index                 | | ||||
| | `privacy_statement`     | About                 | | ||||
| | `terms_of_service`      | About                 | | ||||
| | `imprint`               | About                 | | ||||
| | `about_us`              | About                 | | ||||
| | `external_site_warning` | External Site Warning | | ||||
| | Any rule                | About                 | | ||||
|  | ||||
| # Developer Notes | ||||
|  | ||||
| ## Getting started | ||||
|  | ||||
| ### Clone the project | ||||
|  | ||||
| ``` | ||||
| git clone https://codeberg.org/moanos/notfellchen.git | ||||
| ``` | ||||
|  | ||||
| ### Install dependencies | ||||
| ``` | ||||
| pip install -e '.[all]' | ||||
| ``` | ||||
|  | ||||
| ### Create the database | ||||
|  | ||||
| ``` | ||||
| nf migrate | ||||
| ``` | ||||
|  | ||||
| Because of a wired bug the initial migrations must run two times as the first time the permissions | ||||
| for `create_active_adoption_notice` are created but can not yet be accessed and on the second time this permission will | ||||
| be added to groups. | ||||
|  | ||||
| ### Start the server | ||||
|  | ||||
| ``` | ||||
| nf runserver | ||||
| ``` | ||||
|  | ||||
| ### Build the docs | ||||
|  | ||||
| ``` | ||||
| sphinx-autobuild ./docs ./docs/_build/html | ||||
| ``` | ||||
|  | ||||
|  | ||||
| ## Styling | ||||
|  | ||||
| Bulma is used for styling, including related SCSS. All styles should eventually be migrated to SCSS. | ||||
|  | ||||
| Use `npm run build-bulma` to generate the css file from SCSS. | ||||
| You can use `npm start` during development so that the file is re-generated upon change. | ||||
|  | ||||
|  | ||||
| ## Docker | ||||
|  | ||||
| Build latest image | ||||
| @@ -76,20 +117,37 @@ docker push moanos/notfellchen:latest | ||||
| docker run -p8000:7345 moanos/notfellchen:latest | ||||
| ``` | ||||
|  | ||||
|  | ||||
| ## Testing | ||||
|  | ||||
| Tests can be run with | ||||
|  | ||||
| ```zsh | ||||
| nf test src | ||||
| ``` | ||||
|  | ||||
| If you want to report on code coverage run | ||||
|  | ||||
| ```zsh | ||||
| coverage run --source='.' src/manage.py test src | ||||
| ``` | ||||
|  | ||||
| and | ||||
|  | ||||
| ``` | ||||
| coverage report | ||||
| ``` | ||||
|  | ||||
| ## Geocoding | ||||
|  | ||||
| Geocoding services (search map data by name, address or postcode) are provided via the | ||||
| [Nominatim](https://nominatim.org/) API, powered by [OpenStreetMap](https://openstreetmap.org) data. Notfellchen uses | ||||
| a selfhosted Nominatim instance to avoid overburdening the publicly hosted instance. Due to ressource constraints | ||||
| geocoding is only supported for Germany right now. | ||||
|  | ||||
| ToDos | ||||
| * [ ] Implement a report that shows the number of location strings that could not be converted into a location | ||||
| * [x] Add a management command to re-query location strings to fill location | ||||
| either [Nominatim](https://nominatim.org/) or [photon](https://github.com/komoot/photon) API, powered by [OpenStreetMap](https://openstreetmap.org) data. | ||||
| Notfellchen uses a selfhosted Photon instance to avoid overburdening the publicly hosted instance. | ||||
|  | ||||
| ## Maps | ||||
|  | ||||
| The map on the main homepage is powered by [Versatiles](https://versatiles.org), and rendered using [Maplibre](https://maplibre.org/). | ||||
| The Versatiles server is self-hosted and does not send data to third parties. | ||||
|  | ||||
| ## Translation | ||||
|  | ||||
| @@ -124,3 +182,20 @@ Start beat | ||||
| ```zsh | ||||
|  celery -A notfellchen.celery beat | ||||
| ``` | ||||
|  | ||||
| # Contributing | ||||
|  | ||||
| This project is currently mainly developed by me, moanos. I'd like that to change and will be very happy for contributions | ||||
| and shared responsibilities. Some ideas where you can look for contributing first | ||||
|  | ||||
| * UI improvements: Since a major redesign I'm much happier but the UI could use many, many little tweaks | ||||
| * Docker: If you know how to build a docker container that is a) smaller or b) utilizes staged builds this would be amazing. Any improvement welcome | ||||
| * Testing: Writing tests is always welcome, and it's likely you discover a few bugs | ||||
|  | ||||
| I'm also very happy for all other contributions. Before you do large refactoring efforts or features, best write a short | ||||
| issue for it before you spend a lot of work. | ||||
|  | ||||
| Send PRs either to [codeberg](https://codeberg.org/moanos/notfellchen) (preferred) or [GitHub](https://github.com/moan0s/notfellchen). | ||||
| CI (currently only for documentation) runs via [git.hyteck.de](https://git.hyteck.de), you can also ask moanos for an account there. | ||||
|  | ||||
| Also welcome are new issues with suggestions or bugs and additions to the documentation. | ||||
|   | ||||
| @@ -2,10 +2,10 @@ | ||||
| API Documentation | ||||
| ***************** | ||||
|  | ||||
| The Notfellchen API serves the purpose of supporting 3rd-person applications and anything you can think of basically. | ||||
| The Notfellchen API serves the purpose of supporting 3rd-person applications, whether you want to display data in a custom format or add data from other sources. | ||||
|  | ||||
| .. warning:: | ||||
|     The current API is limited in it's functionality. I you miss a specific feature please contact the developer! | ||||
|     The current API is limited in it's functionality. I you miss a specific feature please contact the developers! | ||||
|  | ||||
| API Access | ||||
| ========== | ||||
| @@ -14,17 +14,94 @@ Via browser | ||||
| ----------- | ||||
|  | ||||
| When a user is logged in, they can easily access the API in their browser, authenticated by their session. | ||||
| The API endpoint can be found at /library/api/ | ||||
| http://notfellchen.org/ | ||||
|  | ||||
| For example: You can check all current adoption notices here: https://notfellchen.org/api/adoption_notice | ||||
|  | ||||
| Via token | ||||
| --------- | ||||
|  | ||||
| .. warning:: | ||||
|     This is currently not supported. | ||||
|  | ||||
| All users are able to generate a token that allows them to use the API. This can be done in the user's profile. | ||||
| An application can then send this token in the request header for authorization. | ||||
|  | ||||
| .. code-block:: | ||||
|     $ curl -X GET http://notfellchen.org/api/adoption_notice -H 'Authorization: Token 49b39856955dc6e5cc04365498d4ad30ea3aed78' | ||||
|  | ||||
|  | ||||
| .. warning:: | ||||
|     Usage or creation of content still has to follow the terms of notfellchen.org. | ||||
|     Copyright of content is often held by rescue organizations, so you are not allowed to simply mirror content. | ||||
|     Talk to the notfellchen team if you want develop such things. | ||||
|  | ||||
|  | ||||
| Endpoints | ||||
| --------- | ||||
|  | ||||
| All Endpoints are documented at  https://notfellchen.org/api/schema/swagger-ui/ or at https://notfellchen.org/api/schema/redoc/ if you prefer redoc. | ||||
| The OpenAI schema can be downloaded at https://notfellchen.org/api/schema/ | ||||
|  | ||||
| Examples are documented here. | ||||
|  | ||||
| Get Adoption Notices | ||||
| ++++++++++++++++++++ | ||||
|  | ||||
| .. code-block:: | ||||
|  | ||||
|   curl --request GET \ | ||||
|   --url https://notfellchen.org/api/adoption_notice \ | ||||
|   --header 'Authorization: {{token}}' | ||||
|  | ||||
| Create Adoption Notice | ||||
| ++++++++++++++++++++++ | ||||
|  | ||||
| .. code-block:: | ||||
|  | ||||
|   curl --request POST \ | ||||
|   --url https://notfellchen.org/api/adoption_notice \ | ||||
|   --header 'Authorization: {{token}}' \ | ||||
|   --header 'content-type: multipart/form-data' \ | ||||
|   --form name=TestAdoption1 \ | ||||
|   --form searching_since=2024-11-19 \ | ||||
|   --form 'description=Lorem ipsum **dolor sit** amet' \ | ||||
|   --form further_information=https://notfellchen.org \ | ||||
|   --form location_string=Berlin \ | ||||
|   --form group_only=true | ||||
|  | ||||
| Add Animal to Adoption Notice | ||||
| +++++++++++++++++++++++++++++ | ||||
|  | ||||
| .. code-block:: | ||||
|  | ||||
|     curl --request POST \ | ||||
|       --url https://notfellchen.org/api/animals/ \ | ||||
|       --header 'Authorization: {{token}}' \ | ||||
|       --header 'content-type: multipart/form-data' \ | ||||
|       --form name=TestAnimal1 \ | ||||
|       --form date_of_birth=2024-11-19 \ | ||||
|       --form 'description=Lorem animal **dolor sit**.' \ | ||||
|       --form sex=F \ | ||||
|       --form species=1 \ | ||||
|       --form adoption_notice=1 | ||||
|  | ||||
| Add picture to Animal or Adoption Notice | ||||
| ++++++++++++++++++++++++++++++++++++++++ | ||||
|  | ||||
| .. code-block:: | ||||
|  | ||||
|     curl -X POST https://notfellchen.org/api/images/ \ | ||||
|     -H "Authorization: Token {{token}}" \ | ||||
|     -F "image=@256-256-crop.jpg" \ | ||||
|     -F "alt_text=Puppy enjoying the sunshine" \ | ||||
|     -F "attach_to_type=animal" \ | ||||
|     -F "attach_to=48 | ||||
|  | ||||
| Species | ||||
| +++++++ | ||||
|  | ||||
| Getting available species is mainly important when creating animals | ||||
|  | ||||
| .. code-block:: | ||||
|  | ||||
|     curl --request GET \ | ||||
|       --url https://notfellchen.org/api/species \ | ||||
|       --header 'Authorization: {{token}}' | ||||
|   | ||||
							
								
								
									
										74
									
								
								docs/_ext/drawio.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,74 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| from pathlib import Path | ||||
|  | ||||
| from docutils import nodes | ||||
| from sphinx.application import Sphinx | ||||
| from sphinx.util.docutils import SphinxDirective | ||||
| from sphinx.util.typing import ExtensionMetadata | ||||
|  | ||||
|  | ||||
| class DrawioDirective(SphinxDirective): | ||||
|     """A directive to show a drawio diagram! | ||||
|  | ||||
|     Usage: | ||||
|     .. drawio:: | ||||
|        example-diagram.drawio.html | ||||
|        example-diagram.drawio.png | ||||
|        :alt: Example of a Draw.io diagram | ||||
|     """ | ||||
|  | ||||
|     has_content = False | ||||
|     required_arguments = 2  # html and png | ||||
|     optional_arguments = 1 | ||||
|     final_argument_whitespace = True  # indicating if the final argument may contain whitespace | ||||
|     option_spec = { | ||||
|         "alt": str, | ||||
|     } | ||||
|  | ||||
|     def run(self) -> list[nodes.Node]: | ||||
|         env = self.state.document.settings.env | ||||
|         builder = env.app.builder | ||||
|  | ||||
|         # Resolve paths relative to the document | ||||
|         docdir = Path(env.doc2path(env.docname)).parent | ||||
|         html_rel = Path(self.arguments[0]) | ||||
|         png_rel = Path(self.arguments[1]) | ||||
|         html_path = (docdir / html_rel).resolve() | ||||
|         png_path = (docdir / png_rel).resolve() | ||||
|  | ||||
|         alt_text = self.options.get("alt", "") | ||||
|  | ||||
|         container = nodes.container() | ||||
|  | ||||
|         # HTML output -> raw HTML node | ||||
|         if builder.format == "html": | ||||
|             # Embed the HTML file contents directly | ||||
|             try: | ||||
|                 html_content = html_path.read_text(encoding="utf-8") | ||||
|             except OSError as e: | ||||
|                 msg = self.state_machine.reporter.error(f"Cannot read HTML file: {e}") | ||||
|                 return [msg] | ||||
|             aria_attribute = f' aria-label="{alt_text}"' if alt_text else "" | ||||
|             raw_html_node = nodes.raw( | ||||
|                 "", | ||||
|                 f'<div class="drawio-diagram"{aria_attribute}>{html_content}</div>', | ||||
|                 format="html", | ||||
|             ) | ||||
|             container += raw_html_node | ||||
|         else: | ||||
|             # Other outputs -> PNG image node | ||||
|             image_node = nodes.image(uri=png_path) | ||||
|             container += image_node | ||||
|  | ||||
|         return [container] | ||||
|  | ||||
|  | ||||
| def setup(app: Sphinx) -> ExtensionMetadata: | ||||
|     app.add_directive("drawio", DrawioDirective) | ||||
|  | ||||
|     return { | ||||
|         "version": "0.2", | ||||
|         "parallel_read_safe": True, | ||||
|         "parallel_write_safe": True, | ||||
|     } | ||||
| @@ -67,5 +67,6 @@ Healthchecks | ||||
| You can configure notfellchen to give a hourly ping to a healthchecks server. If this ping is not received, you will get notified and cna check why the celery jobs are no running. | ||||
| Add the following to your `notfellchen.cfg` and adjust the URL to match your check. | ||||
| .. code:: | ||||
|  | ||||
|   [monitoring] | ||||
|   healthchecks_url=https://health.example.org/ping/5fa7c9b2-753a-4cb3-bcc9-f982f5bc68e8 | ||||
|   | ||||
							
								
								
									
										12
									
								
								docs/conf.py
									
									
									
									
									
								
							
							
						
						| @@ -16,6 +16,10 @@ | ||||
| # import sys | ||||
| # sys.path.insert(0, os.path.abspath('.')) | ||||
|  | ||||
| import sys | ||||
| from pathlib import Path | ||||
|  | ||||
| sys.path.append(str(Path('_ext').resolve())) | ||||
|  | ||||
| # -- Project information ----------------------------------------------------- | ||||
|  | ||||
| @@ -28,7 +32,6 @@ version = '' | ||||
| # The full version, including alpha/beta/rc tags | ||||
| release = '0.2.0' | ||||
|  | ||||
|  | ||||
| # -- General configuration --------------------------------------------------- | ||||
|  | ||||
| # If your documentation needs a minimal Sphinx version, state it here. | ||||
| @@ -40,6 +43,7 @@ release = '0.2.0' | ||||
| # ones. | ||||
| extensions = [ | ||||
|     'sphinx.ext.ifconfig', | ||||
|     'drawio' | ||||
| ] | ||||
|  | ||||
| # Add any paths that contain templates here, relative to this directory. | ||||
| @@ -69,7 +73,6 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] | ||||
| # The name of the Pygments (syntax highlighting) style to use. | ||||
| pygments_style = None | ||||
|  | ||||
|  | ||||
| # -- Options for HTML output ------------------------------------------------- | ||||
|  | ||||
| # The theme to use for HTML and HTML Help pages.  See the documentation for | ||||
| @@ -104,7 +107,6 @@ html_static_path = ['_static'] | ||||
| # Output file base name for HTML help builder. | ||||
| htmlhelp_basename = 'notfellchen' | ||||
|  | ||||
|  | ||||
| # -- Options for LaTeX output ------------------------------------------------ | ||||
|  | ||||
| latex_elements = { | ||||
| @@ -133,7 +135,6 @@ latex_documents = [ | ||||
|      'Julian-Samuel Gebühr', 'manual'), | ||||
| ] | ||||
|  | ||||
|  | ||||
| # -- Options for manual page output ------------------------------------------ | ||||
|  | ||||
| # One entry per manual page. List of tuples | ||||
| @@ -143,7 +144,6 @@ man_pages = [ | ||||
|      [author], 1) | ||||
| ] | ||||
|  | ||||
|  | ||||
| # -- Options for Texinfo output ---------------------------------------------- | ||||
|  | ||||
| # Grouping the document tree into Texinfo files. List of tuples | ||||
| @@ -155,7 +155,6 @@ texinfo_documents = [ | ||||
|      'Miscellaneous'), | ||||
| ] | ||||
|  | ||||
|  | ||||
| # -- Options for Epub output ------------------------------------------------- | ||||
|  | ||||
| # Bibliographic Dublin Core info. | ||||
| @@ -173,5 +172,4 @@ epub_title = project | ||||
| # A list of files that should not be packed into the epub file. | ||||
| epub_exclude_files = ['search.html'] | ||||
|  | ||||
|  | ||||
| # -- Extension configuration ------------------------------------------------- | ||||
|   | ||||
| @@ -5,7 +5,7 @@ Report a bug | ||||
| ^^^^^^^^^^^^ | ||||
|  | ||||
| To report a bug, file an issue on `Github | ||||
| <https://codeberg.org/moanos/notfellchen/issues>`_ | ||||
| <https://github.com/moan0s/notfellchen/issues>`_ | ||||
|  | ||||
| Try to include the following information: | ||||
|  | ||||
| @@ -29,7 +29,7 @@ To contribute simply clone the directory, make your changes and file a | ||||
| pull request. | ||||
|  | ||||
| If you want to know what can be done, have a look at the current `Github | ||||
| <https://codeberg.org/moanos/notfellchen/issues>`_. | ||||
| <https://github.com/moan0s/notfellchen/issues>`_. | ||||
|  | ||||
| Get in touch! | ||||
| ^^^^^^^^^^^^^ | ||||
|   | ||||
| @@ -5,8 +5,7 @@ What qualifies as release? | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
|  | ||||
| A new release should be announced when a significant number functions, bugfixes or other improvements to the software | ||||
| is made. Usually this indicates a minor release. | ||||
| Major releases are yet to be determined. | ||||
| is made. Notfellchen follows `Semantic Versioning <https://semver.org/>`_. | ||||
|  | ||||
| What should be done before a release? | ||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||||
| @@ -14,7 +13,7 @@ What should be done before a release? | ||||
| Tested basic functions | ||||
| ###################### | ||||
|  | ||||
| Run :command:`pytest` | ||||
| Run :command:`nf test src` | ||||
|  | ||||
| Test upgrade on a copy of a production database | ||||
| ############################################### | ||||
| @@ -38,4 +37,4 @@ Do a final commit on this change, and tag the commit as release with appropriate | ||||
|     git tag -a v1.0.0 -m "Releasing version v1.0.0" | ||||
|     git push origin v1.0.0 | ||||
|  | ||||
| Make sure the tag is visible on Codeberg and celebrate 🥳 | ||||
| Make sure the tag is visible on GitHub/Codeberg and celebrate 🥳 | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								docs/user/Screenshot-Moderationstools.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 53 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/user/Screenshot-hilfreiche-Links.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 18 KiB | 
							
								
								
									
										11
									
								
								docs/user/Tiere-in-Vermittlung-entdecken.drawio.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								docs/user/Tiere-in-Vermittlung-entdecken.drawio.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 120 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/user/Vermittlung-Lifecycle.drawio.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 150 KiB | 
							
								
								
									
										11
									
								
								docs/user/Vermittlung_Lifecycle.drawio.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -6,14 +6,27 @@ Jede Vermittlung kann abonniert werden. Dafür klickst du auf die Glocke neben d | ||||
|  | ||||
| .. image:: abonnieren.png | ||||
|  | ||||
|  | ||||
| Einstellungen | ||||
| ------------- | ||||
|  | ||||
| Du kannst E-Mail Benachrichtigungen in den Einstellungen deaktivieren. | ||||
|  | ||||
| .. image:: | ||||
|    einstellungen-benachrichtigungen.png | ||||
|    :alt: Screenshot der Profileinstellungen in Notfellchen. Ein roter Pfeil zeigt auf einen Schalter "E-Mail Benachrichtigungen" | ||||
|  | ||||
| Auf der Website | ||||
| +++++++++++++++ | ||||
|  | ||||
| .. image:: | ||||
|    screenshot-benachrichtigungen.png | ||||
|    :alt: Screenshot der Menüleiste von Notfellchen.org. Neben dem Symbol einer Glocke steht die Zahl 27. | ||||
|  | ||||
|  | ||||
|  | ||||
| E-Mail | ||||
| ++++++ | ||||
|  | ||||
| Mit während deiner :doc:`registrierung` gibst du eine E-Mail Addresse an. | ||||
|  | ||||
| Benachrichtigungen senden wir per Mail - du kannst das jederzeit in den Einstellungen deaktivieren. | ||||
| Mit während deiner :doc:`registrierung` gibst du eine E-Mail Adresse an. An diese senden wir Benachrichtigungen, außer | ||||
| du deaktiviert dies wie oben beschrieben. | ||||
							
								
								
									
										
											BIN
										
									
								
								docs/user/einstellungen-benachrichtigungen.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 46 KiB | 
							
								
								
									
										58
									
								
								docs/user/erste-schritte.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,58 @@ | ||||
| Erste Schritte | ||||
| ============== | ||||
|  | ||||
| Tiere zum Adoptieren suchen | ||||
| --------------------------- | ||||
|  | ||||
| Wenn du Tiere zum adoptieren suchst, brauchst du keinen Account. Du kannst bequem die `Suche <https://notfellchen.org/suchen/>`_ nutzen, um Tiere zur Adoption in deiner Nähe zu finden. | ||||
| Wenn dich eine Vermittlung interessiert, kannst du folgendes tun | ||||
|  | ||||
| * die Vermittlung aufrufen um Details zu sehen | ||||
| * den Link :guilabel:`Weitere Informationen` anklicken um auf der Tierheimwebsite mehr zu erfahren | ||||
| * per Kommentar weitere Informationen erfragen oder hinzufügen | ||||
|  | ||||
| Wenn du die Tiere tatsächlich informieren willst, folge der Anleitung unter :guilabel:`Adoptionsprozess`. | ||||
| Dieser kann sich je nach Tierschutzorganisation unterscheiden. | ||||
|  | ||||
| .. image:: | ||||
|    screenshot-adoptionsprozess.png | ||||
|    :alt: Screenshot der Sektion "Adoptionsprozess" einer Vermittlungsanzeige. Der Prozess ist folgendermaßen: 1. Link zu "Weiteren Informationen" prüfen, 2.  Organization kontaktieren, 3. Bei erfolgreicher Vermittlung: Vermittlung als geschlossen melden | ||||
|  | ||||
| Suchen abonnieren | ||||
| +++++++++++++++++ | ||||
|  | ||||
| Es kann sein, dass es in deiner Umgebung keine passenden Tiere für deine Suche gibt. Damit du nicht ständig wieder Suchen musst, gibt es die Funktion "Suche abonnieren". | ||||
| Wenn du eine Suche abonnierst, wirst du für neue Vermittlungen, die den Kriterien der Suche entsprechen, benachrichtigt. | ||||
|  | ||||
| .. image:: | ||||
|    screenshot-suche-abonnieren.png | ||||
|    :alt: Screenshot der Suchmaske auf Notfellchen.org . Ein roter Pfeil zeigt auf den Button "Suche abonnieren" | ||||
|  | ||||
| .. important:: | ||||
|  | ||||
|    Um Suchen zu abonnieren brauchst du einen Account. Wie du einen Account erstellst erfährst du hier: :doc:`registrierung`. | ||||
|  | ||||
| .. hint:: | ||||
|  | ||||
|    Mehr über Benachrichtigungen findest du hier: :doc:`benachrichtigungen`. | ||||
|  | ||||
| Vermittlungen hinzufügen | ||||
| ------------------------ | ||||
|  | ||||
| Gehe zu `Vermittlung hinzufügen <https://notfellchen.org/vermitteln/>`_ um eine neue Vermittlung einzustellen. | ||||
| Füge alle Informationen die du hast hinzu. | ||||
|  | ||||
| .. important:: | ||||
|  | ||||
|    Um Vermittlungen hinzuzufügen brauchst du einen Account. | ||||
|    Wie du einen Account erstellst erfährst du hier: :doc:`registrierung`. | ||||
|  | ||||
|  | ||||
| .. important:: | ||||
|  | ||||
|    Vermittlungen die du einstellst müssen erst durch Moderator\*innen freigeschaltet werden. Das passiert normalerweise | ||||
|    innerhalb von 24 Stunden. Wenn deine Vermittlung dann noch nicht freigeschaltet ist, prüfe bitte dein E-Mail Postfach, | ||||
|    es könnte sein, dass die Moderator\*innen Rückfragen haben. Melde dich gerne unter info@notfellchen.org, wenn deine | ||||
|    Vermittlung nach 24 Stunden nicht freigeschaltet ist. | ||||
|  | ||||
|  | ||||
| @@ -1,11 +1,17 @@ | ||||
| ****************** | ||||
| User Dokumentation | ||||
| ****************** | ||||
| **************** | ||||
| Benutzerhandbuch | ||||
| **************** | ||||
|  | ||||
| Im Benutzerhandbuch findest du Informationen zur Benutzung von `notfellchen.org <https://notfellchen.org>`_. | ||||
| Solltest du darüber hinaus Fragen haben, komm gerne auf uns zu: info@notfellchen.org | ||||
|  | ||||
| .. toctree:: | ||||
|    :maxdepth: 2 | ||||
|    :caption: Inhalt: | ||||
|  | ||||
|    erste-schritte.rst | ||||
|    registrierung.rst | ||||
|    vermittlungen.rst | ||||
|    moderationskonzept.rst | ||||
|    benachrichtigungen.rst | ||||
|    organisationen-pruefen.rst | ||||
|   | ||||
							
								
								
									
										55
									
								
								docs/user/organisationen-pruefen.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,55 @@ | ||||
| Tiere in Vermittlung systematisch entdecken & eintragen | ||||
| ======================================================= | ||||
|  | ||||
| Notfellchen hat eine Liste der meisten deutschen Tierheime und anderer Tierschutzorganisationen. | ||||
| Die meisten dieser Organisationen nehmen Tiere auf die bei Notfellchen eingetragen werden können. | ||||
| Es ist daher das Ziel, diese Organisationen alle zwei Wochen auf neue Tiere zu prüfen. | ||||
|  | ||||
|  | ||||
| +-------------------------------------------------+---------+----------------------+ | ||||
| | Gruppe                                          | Anzahl  | Zuletzt aktualisiert | | ||||
| +=================================================+=========+======================+ | ||||
| |  Tierschutzorganisationen im Verzeichnis        | 550     | Oktober 2025         | | ||||
| +-------------------------------------------------+---------+----------------------+ | ||||
| | Tierschutzorganisationen in regelmäßigerPrüfung | 412     | Oktober 2025         | | ||||
| +-------------------------------------------------+---------+----------------------+ | ||||
|  | ||||
| .. warning:: | ||||
|  | ||||
|    Organisationen auf neue Tiere zu prüfen ist eine Funktion für Moderator\*innen. Falls du Lust hast mitzuhelfen, | ||||
|    meld dich unter info@notfellchen.org | ||||
|  | ||||
| Als Moderator\*in kannst du direkt auf den `Moderations-Check <https://notfellchen.org/organization-check/>`_ zugreifen | ||||
| oder findest ihn in unter :menuselection:`Hilfreiche Links --> Moderationstools`: | ||||
|  | ||||
| .. image:: | ||||
|    Screenshot-hilfreiche-Links.png | ||||
|    :alt: Screenshot der Hilfreichen Links. Zur Auswahl stehen "Tierheime in der Nähe","Moderationstools" und "Admin-Bereich" | ||||
|  | ||||
| .. image:: | ||||
|   Screenshot-Moderationstools.png | ||||
|   :alt: Screenshot der Moderationstools. Zur Auswahl stehen "Moderationswarteschlange", "Up-to-Date Check", "Organisations-Check" und "Vermittlung ins Fediverse posten". | ||||
|  | ||||
|  | ||||
| Arbeitsmodus | ||||
| ------------ | ||||
|  | ||||
| .. drawio:: | ||||
|    Tiere-in-Vermittlung-entdecken.drawio.html | ||||
|    Tiere-in-Vermittlung-entdecken.drawio.png | ||||
|  | ||||
| Shortcuts | ||||
| --------- | ||||
|  | ||||
| Um die Prüfung schneller zu gestalten, gibt es eine Reihe von Shortcuts die du nutzen kannst. Aus Gründen der | ||||
| Übersichtlichkeit sind im Folgenden auch Shortcuts im Browser aufgeführt. | ||||
|  | ||||
| +------------------------------------------------------+---------------+ | ||||
| | Aktion                                               | Shortcut      | | ||||
| +======================================================+===============+ | ||||
| |  Website der ersten Tierschutzorganisation öffnen    | :kbd:`O`      | | ||||
| +------------------------------------------------------+---------------+ | ||||
| | Tab schließen (Firefox/Chrome)                       | :kbd:`STRG+W` | | ||||
| +------------------------------------------------------+---------------+ | ||||
| | Erste Tierschutzorganisationa als geprüft markieren  | :kbd:`C`      | | ||||
| +------------------------------------------------------+---------------+ | ||||
							
								
								
									
										
											BIN
										
									
								
								docs/user/screenshot-adoptionsprozess.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 34 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/user/screenshot-benachrichtigungen.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 205 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/user/screenshot-suche-abonnieren.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 24 KiB | 
							
								
								
									
										
											BIN
										
									
								
								docs/user/screenshot-suche.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 20 KiB | 
| @@ -1,7 +1,7 @@ | ||||
| Vermittlungen | ||||
| ============= | ||||
|  | ||||
| Vermittlungen können von allen Nutzer*innen mit Account erstellt werden. Vermittlungen normaler Nutzer*innen kommen dann in eine Warteschlange und werden vom Admin & Modertionsteam geprüft und sichtbar geschaltet. | ||||
| Vermittlungen können von allen Nutzer\*innen mit Account erstellt werden. Vermittlungen normaler Nutzer*innen kommen dann in eine Warteschlange und werden vom Admin & Modertionsteam geprüft und sichtbar geschaltet. | ||||
| Tierheime und Pflegestellen können auf Anfrage einen Koordinations-Status bekommen, wodurch sie Vermittlungsanzeigen erstellen können die direkt öffentlich sichtbar sind. | ||||
|  | ||||
| Jede Vermittlung hat ein "Zuletzt-geprüft" Datum, das anzeigt, wann ein Mensch zuletzt überprüft hat, ob die Anzeige noch aktuell ist. | ||||
| @@ -15,3 +15,114 @@ Die Kommentarfunktion von Vermittlungen ermöglicht es angemeldeten Nutzer*innen | ||||
| Ersteller*innen von Vermittlungen werden über neue Kommentare per Mail benachrichtigt, ebenso alle die die Vermittlung abonniert haben. | ||||
|  | ||||
| Kommentare können, wie Vermittlungen, gemeldet werden. | ||||
|  | ||||
| .. drawio:: | ||||
|    Vermittlung_Lifecycle.drawio.html | ||||
|    Vermittlung-Lifecycle.drawio.png | ||||
|    :alt: Diagramm das den Prozess der Vermittlungen zeigt. | ||||
|  | ||||
|  | ||||
| Adoption Notice Status Choices | ||||
| ++++++++++++++++++++++++++++++ | ||||
|  | ||||
| Aktiv | ||||
| ----- | ||||
|  | ||||
| Aktive Vermittlungen die über die Suche auffindbar sind. | ||||
|  | ||||
| .. list-table:: | ||||
|    :header-rows: 1 | ||||
|    :width: 100% | ||||
|    :widths: 1 1 2 | ||||
|  | ||||
|    * - Value | ||||
|      - Label | ||||
|      - Description | ||||
|  | ||||
|    * - ``active_searching`` | ||||
|      - Searching | ||||
|      - | ||||
|  | ||||
|    * - ``active_interested`` | ||||
|      - Interested | ||||
|      - Jemand hat bereits Interesse an den Tieren. | ||||
|  | ||||
| Warte auf Aktion | ||||
| ---------------- | ||||
|  | ||||
| Vermittlungen in diesem Status warten darauf, dass ein Mensch sie überprüft. Sie können nicht über die Suche gefunden werden. | ||||
|  | ||||
| .. list-table:: | ||||
|    :header-rows: 1 | ||||
|    :width: 100% | ||||
|    :widths: 1 1 2 | ||||
|  | ||||
|    * - ``awaiting_action_waiting_for_review`` | ||||
|      - Waiting for review | ||||
|      - Neue Vermittlung die deaktiviert ist bis Moderator*innen sie überprüfen. | ||||
|  | ||||
|    * - ``awaiting_action_needs_additional_info`` | ||||
|      - Needs additional info | ||||
|      - Deaktiviert bis Informationen nachgetragen werden. | ||||
|  | ||||
|    * - ``disabled_unchecked`` | ||||
|      - Unchecked | ||||
|      - Vermittlung deaktiviert bis sie vom Team auf Aktualität geprüft wurde. | ||||
|  | ||||
| Geschlossen | ||||
| ----------- | ||||
|  | ||||
| Geschlossene Vermittlungen tauchen in keiner Suche auf. Sie werden aber weiterhin angezeigt, wenn der Link zu ihnen direkt aufgerufen wird. | ||||
|  | ||||
| .. list-table:: | ||||
|    :header-rows: 1 | ||||
|    :width: 100% | ||||
|    :widths: 1 1 2 | ||||
|  | ||||
|    * - ``closed_successful_with_notfellchen`` | ||||
|      - Successful (with Notfellchen) | ||||
|      - Vermittlung erfolgreich abgeschlossen. | ||||
|  | ||||
|    * - ``closed_successful_without_notfellchen`` | ||||
|      - Successful (without Notfellchen) | ||||
|      - Vermittlung erfolgreich abgeschlossen. | ||||
|  | ||||
|    * - ``closed_animal_died`` | ||||
|      - Animal died | ||||
|      - Die zu vermittelnden Tiere sind über die Regenbrücke gegangen. | ||||
|  | ||||
|    * - ``closed_for_other_adoption_notice`` | ||||
|      - Closed for other adoption notice | ||||
|      - Vermittlung wurde zugunsten einer anderen geschlossen. | ||||
|  | ||||
|    * - ``closed_not_open_for_adoption_anymore`` | ||||
|      - Not open for adoption anymore | ||||
|      - Tier(e) stehen nicht mehr zur Vermittlung bereit. | ||||
|  | ||||
|    * - ``closed_link_to_more_info_not_reachable`` | ||||
|      - Der Link zu weiteren Informationen ist nicht mehr erreichbar. | ||||
|      - Der Link zu weiteren Informationen ist nicht mehr erreichbar, die Vermittlung wurde daher automatisch deaktiviert. | ||||
|  | ||||
|    * - ``closed_other`` | ||||
|      - Other (closed) | ||||
|      - Vermittlung geschlossen. | ||||
|  | ||||
| Deaktiviert | ||||
| ----------- | ||||
|  | ||||
| Deaktivierte Vermittlungen werden nur noch Moderator\*innen und Administrator\*innen angezeigt. | ||||
|  | ||||
| .. list-table:: | ||||
|    :header-rows: 1 | ||||
|    :width: 100% | ||||
|    :widths: 1 1 2 | ||||
|  | ||||
|    * - ``disabled_against_the_rules`` | ||||
|      - Against the rules | ||||
|      - Vermittlung deaktiviert da sie gegen die Regeln verstößt. | ||||
|  | ||||
|    * - ``disabled_other`` | ||||
|      - Other (disabled) | ||||
|      - Vermittlung deaktiviert. | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -18,10 +18,16 @@ media=./media | ||||
| static=./static | ||||
|  | ||||
| [mail] | ||||
| console-only=true | ||||
| console_only=true | ||||
|  | ||||
| [logging] | ||||
| app_log_level=INFO | ||||
| django_log_level=INFO | ||||
|  | ||||
| [geocoding] | ||||
| api_url=https://photon.hyteck.de/api | ||||
| api_format=photon | ||||
|  | ||||
| [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,15 +6,15 @@ build-backend = "setuptools.build_meta" | ||||
|  | ||||
| [project] | ||||
| name = "notfellchen" | ||||
| description = "A tool to help." | ||||
| description = "A website to help animals to find a loving home. It features organized input of adoption notices and related animals including automated lifecycle, location-based search, roles, and support for easy checking of rescue organizations." | ||||
| authors = [ | ||||
|   {name = "moanos", email = "julian-samuel@gebuehr.net"}, | ||||
|     { name = "moanos", email = "julian-samuel@gebuehr.net" }, | ||||
| ] | ||||
| maintainers = [ | ||||
|   {name = "moanos", email = "julian-samuel@gebuehr.net"}, | ||||
|     { name = "moanos", email = "julian-samuel@gebuehr.net" }, | ||||
| ] | ||||
| keywords = ["animal", "adoption", "django", "rescue", ] | ||||
| license = {text = "AGPL-3.0-or-later"} | ||||
| keywords = ["animal", "adoption", "django", "rescue", "rats" ] | ||||
| license = { text = "AGPL-3.0-or-later" } | ||||
| classifiers = [ | ||||
|     "Environment :: Web", | ||||
|     "License :: OSI Approved :: GNU Affero General Public License v3", | ||||
| @@ -24,22 +24,22 @@ classifiers = [ | ||||
| ] | ||||
| dependencies = [ | ||||
|     "Django", | ||||
|     "coverage", | ||||
|     "codecov", | ||||
|     "sphinx", | ||||
|     "sphinx-rtd-theme", | ||||
|     "gunicorn", | ||||
|     "fontawesomefree", | ||||
|     "whitenoise", | ||||
|     "model_bakery", | ||||
|     "markdown", | ||||
|     "Pillow", | ||||
|     "django-registration", | ||||
|     "psycopg2", | ||||
|     "psycopg2-binary", | ||||
|     "django-crispy-forms", | ||||
|     "crispy-bootstrap4", | ||||
|     "djangorestframework", | ||||
|     "celery[redis]" | ||||
|     "celery[redis]", | ||||
|     "drf-spectacular[sidecar]", | ||||
|     "django-widget-tweaks", | ||||
|     "django-super-deduper", | ||||
|     "django-allauth[mfa]" | ||||
| ] | ||||
|  | ||||
| dynamic = ["version", "readme"] | ||||
| @@ -47,6 +47,13 @@ dynamic = ["version", "readme"] | ||||
| [project.optional-dependencies] | ||||
| develop = [ | ||||
|     "pytest", | ||||
|     "coverage", | ||||
|     "model_bakery", | ||||
| ] | ||||
| docs = [ | ||||
|     "sphinx", | ||||
|     "sphinx-rtd-theme", | ||||
|     "sphinx-autobuild" | ||||
| ] | ||||
|  | ||||
| [project.urls] | ||||
| @@ -61,6 +68,6 @@ nf = 'notfellchen.main:main' | ||||
|  | ||||
|  | ||||
| [tool.setuptools.dynamic] | ||||
| version = {attr = "notfellchen.__version__"} | ||||
| readme = {file = "README.md"} | ||||
| version = { attr = "notfellchen.__version__" } | ||||
| readme = { file = "README.md" } | ||||
|  | ||||
|   | ||||
							
								
								
									
										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,27 +1,68 @@ | ||||
| from django.contrib.auth.admin import UserAdmin as BaseUserAdmin | ||||
| from django.contrib import admin | ||||
| from django.utils.html import format_html | ||||
| import csv | ||||
|  | ||||
| from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp | ||||
| from django.contrib import admin | ||||
| from django.contrib.admin import EmptyFieldListFilter | ||||
| from django.http import HttpResponse | ||||
| from django.utils.html import format_html | ||||
| from django.urls import reverse | ||||
| from django.utils.http import urlencode | ||||
|  | ||||
| from .models import Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \ | ||||
|     SpeciesSpecificURL, ImportantLocation, SocialMediaPost | ||||
|  | ||||
| from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \ | ||||
|     Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions | ||||
|     Comment, Announcement, User, Subscriptions, Notification | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
|  | ||||
|  | ||||
| class StatusInline(admin.StackedInline): | ||||
|     model = AdoptionNoticeStatus | ||||
| from .tools.model_helpers import AdoptionNoticeStatusChoices | ||||
|  | ||||
|  | ||||
| @admin.register(AdoptionNotice) | ||||
| class AdoptionNoticeAdmin(admin.ModelAdmin): | ||||
|     search_fields = ("name__icontains", "description__icontains") | ||||
|     inlines = [ | ||||
|         StatusInline, | ||||
|     ] | ||||
|     list_filter = ("owner",) | ||||
|     actions = ("activate",) | ||||
|  | ||||
|     def activate(self, request, queryset): | ||||
|         for obj in queryset: | ||||
|             obj.adoption_notice_status = AdoptionNoticeStatusChoices.Active.SEARCHING | ||||
|             obj.save() | ||||
|  | ||||
|     activate.short_description = _("Ausgewählte Vermittlungen aktivieren") | ||||
|  | ||||
|  | ||||
| # Re-register UserAdmin | ||||
| admin.site.register(User) | ||||
| @admin.register(User) | ||||
| class UserAdmin(admin.ModelAdmin): | ||||
|     search_fields = ("usernamname__icontains", "first_name__icontains", "last_name__icontains", "email__icontains") | ||||
|     list_display = ("username", "email", "trust_level", "is_active", "view_adoption_notices") | ||||
|     list_filter = ("is_active", "trust_level",) | ||||
|     actions = ("export_as_csv",) | ||||
|  | ||||
|     def view_adoption_notices(self, obj): | ||||
|         count = obj.adoption_notices.count() | ||||
|         url = ( | ||||
|                 reverse("admin:fellchensammlung_adoptionnotice_changelist") | ||||
|                 + "?" | ||||
|                 + urlencode({"owner__id": f"{obj.id}"}) | ||||
|         ) | ||||
|         return format_html('<a href="{}">{} Adoption Notices</a>', url, count) | ||||
|  | ||||
|     def export_as_csv(self, request, queryset): | ||||
|         meta = self.model._meta | ||||
|         field_names = [field.name for field in meta.fields] | ||||
|  | ||||
|         response = HttpResponse(content_type='text/csv') | ||||
|         response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta) | ||||
|         writer = csv.writer(response) | ||||
|  | ||||
|         writer.writerow(field_names) | ||||
|         for obj in queryset: | ||||
|             row = writer.writerow([getattr(obj, field) for field in field_names]) | ||||
|  | ||||
|         return response | ||||
|  | ||||
|     export_as_csv.short_description = _("Ausgewählte User exportieren") | ||||
|  | ||||
|  | ||||
| def _reported_content_link(obj): | ||||
| @@ -51,11 +92,19 @@ class ReportAdoptionNoticeAdmin(admin.ModelAdmin): | ||||
|     reported_content_link.short_description = "Reported Content" | ||||
|  | ||||
|  | ||||
| class SpeciesSpecificURLInline(admin.StackedInline): | ||||
|     model = SpeciesSpecificURL | ||||
|  | ||||
|  | ||||
| @admin.register(RescueOrganization) | ||||
| class RescueOrganizationAdmin(admin.ModelAdmin): | ||||
|     search_fields = ("name__icontains",) | ||||
|     search_fields = ("name", "description", "internal_comment", "location_string", "location__city") | ||||
|     list_display = ("name", "trusted", "allows_using_materials", "website") | ||||
|     list_filter = ("allows_using_materials", "trusted",) | ||||
|     list_filter = ("allows_using_materials", "trusted", ("external_source_identifier", EmptyFieldListFilter)) | ||||
|  | ||||
|     inlines = [ | ||||
|         SpeciesSpecificURLInline, | ||||
|     ] | ||||
|  | ||||
|  | ||||
| @admin.register(Text) | ||||
| @@ -63,15 +112,65 @@ class TextAdmin(admin.ModelAdmin): | ||||
|     search_fields = ("title__icontains", "text_code__icontains",) | ||||
|  | ||||
|  | ||||
| @admin.register(Comment) | ||||
| class CommentAdmin(admin.ModelAdmin): | ||||
|     list_filter = ("user",) | ||||
|  | ||||
|  | ||||
| @admin.register(Notification) | ||||
| class BaseNotificationAdmin(admin.ModelAdmin): | ||||
|     list_filter = ("user_to_notify", "read") | ||||
|  | ||||
|  | ||||
| @admin.register(SearchSubscription) | ||||
| class SearchSubscriptionAdmin(admin.ModelAdmin): | ||||
|     list_filter = ("owner",) | ||||
|  | ||||
|  | ||||
| class ImportantLocationInline(admin.StackedInline): | ||||
|     model = ImportantLocation | ||||
|  | ||||
|  | ||||
| class IsImportantListFilter(admin.SimpleListFilter): | ||||
|     # See https://docs.djangoproject.com/en/5.1/ref/contrib/admin/filters/#modeladmin-list-filters | ||||
|     title = _('Is Important Location?') | ||||
|  | ||||
|     parameter_name = 'important' | ||||
|  | ||||
|     def lookups(self, request, model_admin): | ||||
|         return ( | ||||
|             ('is_important', _('Important Location')), | ||||
|             ('is_normal', _('Normal Location')), | ||||
|         ) | ||||
|  | ||||
|     def queryset(self, request, queryset): | ||||
|         if self.value() == 'is_important': | ||||
|             return queryset.filter(importantlocation__isnull=False) | ||||
|         else: | ||||
|             return queryset.filter(importantlocation__isnull=True) | ||||
|  | ||||
|  | ||||
| @admin.register(Location) | ||||
| class LocationAdmin(admin.ModelAdmin): | ||||
|     search_fields = ("name__icontains", "city__icontains") | ||||
|     list_filter = [IsImportantListFilter] | ||||
|     inlines = [ | ||||
|         ImportantLocationInline, | ||||
|     ] | ||||
|  | ||||
|  | ||||
| @admin.register(SocialMediaPost) | ||||
| class SocialMediaPostAdmin(admin.ModelAdmin): | ||||
|     list_filter = ("platform",) | ||||
|  | ||||
|  | ||||
| admin.site.register(Animal) | ||||
| admin.site.register(Species) | ||||
| admin.site.register(Location) | ||||
| admin.site.register(Rule) | ||||
| admin.site.register(Image) | ||||
| admin.site.register(ModerationAction) | ||||
| admin.site.register(Language) | ||||
| admin.site.register(Announcement) | ||||
| admin.site.register(AdoptionNoticeStatus) | ||||
| admin.site.register(Subscriptions) | ||||
| admin.site.register(Log) | ||||
| admin.site.register(Timestamp) | ||||
|   | ||||
							
								
								
									
										33
									
								
								src/fellchensammlung/api/renderers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,33 @@ | ||||
| from rest_framework.renderers import BaseRenderer | ||||
| import json | ||||
|  | ||||
|  | ||||
| class GeoJSONRenderer(BaseRenderer): | ||||
|     media_type = 'application/json' | ||||
|     format = 'geojson' | ||||
|     charset = 'utf-8' | ||||
|  | ||||
|     def render(self, data, accepted_media_type=None, renderer_context=None): | ||||
|         features = [] | ||||
|         for item in data: | ||||
|             coords = item["coordinates"] | ||||
|             if coords: | ||||
|                 feature = { | ||||
|                     "type": "Feature", | ||||
|                     "geometry": { | ||||
|                         "type": "Point", | ||||
|                         "coordinates": coords | ||||
|                     }, | ||||
|                     "properties": { | ||||
|                         k: v for k, v in item.items() | ||||
|                     }, | ||||
|                     "id": f"{item['id']}" | ||||
|                 } | ||||
|                 features.append(feature) | ||||
|  | ||||
|         geojson = { | ||||
|             "type": "FeatureCollection", | ||||
|             "generator": "notfellchen", | ||||
|             "features": features | ||||
|         } | ||||
|         return json.dumps(geojson) | ||||
| @@ -1,10 +1,177 @@ | ||||
| from ..models import AdoptionNotice | ||||
| from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image, Location | ||||
| from rest_framework import serializers | ||||
| import math | ||||
|  | ||||
|  | ||||
| class ImageSerializer(serializers.ModelSerializer): | ||||
|     width = serializers.SerializerMethodField() | ||||
|     height = serializers.SerializerMethodField() | ||||
|  | ||||
|     class Meta: | ||||
|         model = Image | ||||
|         fields = ['id', 'image', 'alt_text', 'width', 'height'] | ||||
|  | ||||
|     def get_width(self, obj): | ||||
|         return obj.image.width | ||||
|  | ||||
|     def get_height(self, obj): | ||||
|         return obj.image.height | ||||
|  | ||||
|  | ||||
| class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer): | ||||
|     location = serializers.PrimaryKeyRelatedField( | ||||
|         queryset=Location.objects.all(), | ||||
|         required=False, | ||||
|         allow_null=True | ||||
|     ) | ||||
|     location_details = serializers.StringRelatedField(source='location', read_only=True) | ||||
|     organization = serializers.PrimaryKeyRelatedField( | ||||
|         queryset=RescueOrganization.objects.all(), | ||||
|         required=False, | ||||
|         allow_null=True | ||||
|     ) | ||||
|     organization = serializers.PrimaryKeyRelatedField( | ||||
|         queryset=RescueOrganization.objects.all(), | ||||
|         required=False, | ||||
|         allow_null=True | ||||
|     ) | ||||
|     url = serializers.SerializerMethodField() | ||||
|  | ||||
|     photos = ImageSerializer(many=True, read_only=True) | ||||
|  | ||||
|     def get_url(self, obj): | ||||
|         return obj.get_full_url() | ||||
|  | ||||
|     class Meta: | ||||
|         model = AdoptionNotice | ||||
|         fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information", "group_only"] | ||||
|         fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information", | ||||
|                   "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 Meta: | ||||
|         model = Animal | ||||
|         fields = ["name", "date_of_birth", "description", "species", "sex", "adoption_notice"] | ||||
|  | ||||
|  | ||||
| class AnimalGetSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Animal | ||||
|         fields = "__all__" | ||||
|  | ||||
|  | ||||
| class RescueOrganizationSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = RescueOrganization | ||||
|         exclude = ["internal_comment", "allows_using_materials"] | ||||
|  | ||||
|  | ||||
| class ImageCreateSerializer(serializers.ModelSerializer): | ||||
|     @staticmethod | ||||
|     def _animal_or_an(value): | ||||
|         if not value in ["animal", "adoption_notice"]: | ||||
|             raise serializers.ValidationError( | ||||
|                 'Set either animal or adoption_notice, depending on what type of object the image should be attached to.') | ||||
|  | ||||
|     attach_to_type = serializers.CharField(validators=[_animal_or_an]) | ||||
|     attach_to = serializers.IntegerField() | ||||
|  | ||||
|     class Meta: | ||||
|         model = Image | ||||
|         exclude = ["owner"] | ||||
|  | ||||
|  | ||||
| class SpeciesSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Species | ||||
|         fields = "__all__" | ||||
|  | ||||
|  | ||||
| class LocationSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Location | ||||
|         fields = "__all__" | ||||
|   | ||||
| @@ -1,8 +1,21 @@ | ||||
| from django.urls import path | ||||
| from .views import ( | ||||
|     AdoptionNoticeApiView | ||||
|     AdoptionNoticeApiView, | ||||
|     AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView, LocationApiView, | ||||
|     AdoptionNoticeGeoJSONView, RescueOrgGeoJSONView, AdoptionNoticePerOrgApiView | ||||
| ) | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path('adoption_notice', AdoptionNoticeApiView.as_view()), | ||||
|     path("adoption_notice", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-list"), | ||||
|     path("adoption_notice.geojson", AdoptionNoticeGeoJSONView.as_view(), name="api-adoption-notice-list-geojson"), | ||||
|     path("adoption_notice/<int:id>/", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-detail"), | ||||
|     path("animals/", AnimalApiView.as_view(), name="api-animal-list"), | ||||
|     path("animals/<int:id>/", AnimalApiView.as_view(), name="api-animal-detail"), | ||||
|     path("organizations/", RescueOrganizationApiView.as_view(), name="api-organization-list"), | ||||
|     path("organizations.geojson", RescueOrgGeoJSONView.as_view(), name="api-organization-list-geojson"), | ||||
|     path("organizations/<int:id>/", RescueOrganizationApiView.as_view(), name="api-organization-detail"), | ||||
|     path("organizations/<int:id>/adoption-notices", AdoptionNoticePerOrgApiView.as_view(), name="api-organization-adoption-notices"), | ||||
|     path("images/", AddImageApiView.as_view(), name="api-add-image"), | ||||
|     path("species/", SpeciesApiView.as_view(), name="api-species-list"), | ||||
|     path("locations/", LocationApiView.as_view(), name="api-locations-list"), | ||||
| ] | ||||
|   | ||||
| @@ -1,37 +1,452 @@ | ||||
| from django.contrib.auth.models import User | ||||
| from django.db.models import Q | ||||
| from drf_spectacular.types import OpenApiTypes | ||||
| from rest_framework.generics import ListAPIView | ||||
|  | ||||
| from fellchensammlung.api.serializers import LocationSerializer, AdoptionNoticeGeoJSONSerializer | ||||
| from rest_framework.views import APIView | ||||
| from rest_framework.response import Response | ||||
| from rest_framework import status | ||||
| from rest_framework import permissions | ||||
| from ..models import AdoptionNotice | ||||
| from .serializers import AdoptionNoticeSerializer | ||||
| from django.db import transaction | ||||
| from fellchensammlung.models import Log, TrustLevel, Location, AdoptionNoticeStatusChoices | ||||
| from fellchensammlung.tasks import post_adoption_notice_save, post_rescue_org_save | ||||
| from rest_framework import status, serializers | ||||
| from rest_framework.permissions import IsAuthenticated | ||||
|  | ||||
| from .renderers import GeoJSONRenderer | ||||
| from .serializers import ( | ||||
|     AnimalGetSerializer, | ||||
|     AnimalCreateSerializer, | ||||
|     RescueOrgeGeoJSONSerializer, | ||||
|     AdoptionNoticeSerializer, | ||||
|     ImageCreateSerializer, | ||||
|     SpeciesSerializer, RescueOrganizationSerializer, | ||||
| ) | ||||
| from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image | ||||
| from drf_spectacular.utils import extend_schema, inline_serializer, OpenApiParameter | ||||
|  | ||||
|  | ||||
| class AdoptionNoticeApiView(APIView): | ||||
|     permission_classes = [permissions.IsAuthenticated] | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     @extend_schema( | ||||
|         parameters=[ | ||||
|             { | ||||
|                 'name': 'id', | ||||
|                 'required': False, | ||||
|                 'description': 'ID of the adoption notice to retrieve.', | ||||
|                 'type': int | ||||
|             }, | ||||
|         ], | ||||
|         responses={200: AdoptionNoticeSerializer(many=True)} | ||||
|     ) | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         serializer_context = { | ||||
|             'request': request, | ||||
|         } | ||||
|         """ | ||||
|         Retrieve adoption notices with their related animals and images. | ||||
|         """ | ||||
|         adoption_notice_id = kwargs.get("id") | ||||
|         if adoption_notice_id: | ||||
|             try: | ||||
|                 adoption_notice = AdoptionNotice.objects.get(pk=adoption_notice_id) | ||||
|                 serializer = AdoptionNoticeSerializer(adoption_notice, context={"request": request}) | ||||
|                 return Response(serializer.data, status=status.HTTP_200_OK) | ||||
|             except AdoptionNotice.DoesNotExist: | ||||
|                 return Response({"error": "Adoption notice not found."}, status=status.HTTP_404_NOT_FOUND) | ||||
|         adoption_notices = AdoptionNotice.objects.all() | ||||
|         serializer = AdoptionNoticeSerializer(adoption_notices, many=True, context=serializer_context) | ||||
|         serializer = AdoptionNoticeSerializer(adoption_notices, many=True, context={"request": request}) | ||||
|         return Response(serializer.data, status=status.HTTP_200_OK) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     @extend_schema( | ||||
|         request=AdoptionNoticeSerializer, | ||||
|         responses={201: 'Adoption notice created successfully!'} | ||||
|     ) | ||||
|     def post(self, request, *args, **kwargs): | ||||
|         data = { | ||||
|             'name': request.data.get('name'), | ||||
|             "searching_since": request.data.get('searching_since'), | ||||
|             "description": request.data.get('description'), | ||||
|             "organization": request.data.get('organization'), | ||||
|             "further_information": request.data.get('further_information'), | ||||
|             "location_string": request.data.get('location_string'), | ||||
|             "group_only": request.data.get('group_only'), | ||||
|             "owner": request.data.get('owner') | ||||
|         } | ||||
|         serializer = AdoptionNoticeSerializer(data=data) | ||||
|         """ | ||||
|         API view to add an adoption notice. | ||||
|         """ | ||||
|         serializer = AdoptionNoticeSerializer(data=request.data, context={'request': request}) | ||||
|         if not serializer.is_valid(): | ||||
|             return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) | ||||
|  | ||||
|         adoption_notice = serializer.save(owner=request.user_to_notify) | ||||
|  | ||||
|         # Add the location | ||||
|         post_adoption_notice_save.delay_on_commit(adoption_notice.pk) | ||||
|  | ||||
|         # Only set active when user has trust level moderator or higher | ||||
|         if request.user_to_notify.trust_level >= TrustLevel.MODERATOR: | ||||
|             adoption_notice.adoption_notice_status = AdoptionNoticeStatusChoices.Active.SEARCHING | ||||
|         else: | ||||
|             adoption_notice.adoption_notice_status = AdoptionNoticeStatusChoices.AwaitingAction.WAITING_FOR_REVIEW | ||||
|  | ||||
|         # Log the action | ||||
|         Log.objects.create( | ||||
|             user=request.user_to_notify, | ||||
|             action="add_adoption_notice", | ||||
|             text=f"{request.user_to_notify} added adoption notice {adoption_notice.pk} via API", | ||||
|         ) | ||||
|  | ||||
|         # Return success response with new adoption notice details | ||||
|         return Response( | ||||
|             {"message": "Adoption notice created successfully!", "id": adoption_notice.pk}, | ||||
|             status=status.HTTP_201_CREATED, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class AnimalApiView(APIView): | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     @extend_schema( | ||||
|         responses=AnimalGetSerializer | ||||
|     ) | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         """ | ||||
|         Get list of animals or a specific animal by ID. | ||||
|         """ | ||||
|         animal_id = kwargs.get("id") | ||||
|         if animal_id: | ||||
|             try: | ||||
|                 animal = Animal.objects.get(pk=animal_id) | ||||
|                 serializer = AnimalGetSerializer(animal, context={"request": request}) | ||||
|                 return Response(serializer.data, status=status.HTTP_200_OK) | ||||
|             except Animal.DoesNotExist: | ||||
|                 return Response({"error": "Animal not found."}, status=status.HTTP_404_NOT_FOUND) | ||||
|         animals = Animal.objects.all() | ||||
|         serializer = AnimalGetSerializer(animals, many=True, context={"request": request}) | ||||
|         return Response(serializer.data, status=status.HTTP_200_OK) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     @extend_schema( | ||||
|         request=AnimalCreateSerializer, | ||||
|         responses={201: inline_serializer( | ||||
|             name='Animal', | ||||
|             fields={ | ||||
|                 'id': serializers.IntegerField(), | ||||
|                 "message": serializers.Field()}), | ||||
|             400: "json"} | ||||
|     ) | ||||
|     @transaction.atomic | ||||
|     def post(self, request, *args, **kwargs): | ||||
|         """ | ||||
|         Create a new animal. | ||||
|         """ | ||||
|         serializer = AnimalCreateSerializer(data=request.data, context={"request": request}) | ||||
|         if serializer.is_valid(): | ||||
|             serializer.save() | ||||
|             return Response(serializer.data, status=status.HTTP_201_CREATED) | ||||
|             animal = serializer.save(owner=request.user_to_notify) | ||||
|             return Response( | ||||
|                 {"message": "Animal created successfully!", "id": animal.id}, | ||||
|                 status=status.HTTP_201_CREATED, | ||||
|             ) | ||||
|         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) | ||||
|  | ||||
|  | ||||
| class RescueOrganizationApiView(APIView): | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     @extend_schema( | ||||
|         parameters=[ | ||||
|             { | ||||
|                 'name': 'id', | ||||
|                 'required': False, | ||||
|                 'description': 'ID of the rescue organization to retrieve.', | ||||
|                 'type': int | ||||
|             }, | ||||
|             { | ||||
|                 'name': 'trusted', | ||||
|                 'required': False, | ||||
|                 'description': 'Filter by trusted status (true/false).', | ||||
|                 'type': bool | ||||
|             }, | ||||
|             { | ||||
|                 'name': 'external_object_identifier', | ||||
|                 'required': False, | ||||
|                 'description': 'Filter by external object identifier. Use "None" to filter for an empty field', | ||||
|                 'type': str | ||||
|             }, | ||||
|             { | ||||
|                 'name': 'external_source_identifier', | ||||
|                 'required': False, | ||||
|                 'description': 'Filter by external source identifier. Use "None" to filter for an empty field', | ||||
|                 'type': str | ||||
|             }, | ||||
|             { | ||||
|                 'name': 'search', | ||||
|                 'required': False, | ||||
|                 'description': 'Search by organization name or location name/city.', | ||||
|                 'type': str | ||||
|             }, | ||||
|         ], | ||||
|         responses={200: RescueOrganizationSerializer(many=True)} | ||||
|     ) | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         """ | ||||
|         Get list of rescue organizations or a specific organization by ID or get a list with available filters for | ||||
|         - external_object_identifier | ||||
|         - external_source_identifier | ||||
|         """ | ||||
|         org_id = request.query_params.get("id") | ||||
|         external_object_identifier = request.query_params.get("external_object_identifier") | ||||
|         external_source_identifier = request.query_params.get("external_source_identifier") | ||||
|         search_query = request.query_params.get("search") | ||||
|  | ||||
|         if org_id: | ||||
|             try: | ||||
|                 organization = RescueOrganization.objects.get(pk=org_id) | ||||
|                 serializer = RescueOrganizationSerializer(organization, context={"request": request}) | ||||
|                 return Response(serializer.data, status=status.HTTP_200_OK) | ||||
|             except RescueOrganization.DoesNotExist: | ||||
|                 return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND) | ||||
|  | ||||
|         organizations = RescueOrganization.objects.all() | ||||
|  | ||||
|         if external_object_identifier: | ||||
|             if external_object_identifier == "None": | ||||
|                 external_object_identifier = None | ||||
|             organizations = organizations.filter(external_object_identifier=external_object_identifier) | ||||
|  | ||||
|         if external_source_identifier: | ||||
|             if external_source_identifier == "None": | ||||
|                 external_source_identifier = None | ||||
|             organizations = organizations.filter(external_source_identifier=external_source_identifier) | ||||
|         if search_query: | ||||
|             organizations = organizations.filter( | ||||
|                 Q(name__icontains=search_query) | | ||||
|                 Q(location_string__icontains=search_query) | | ||||
|                 Q(location__name__icontains=search_query) | | ||||
|                 Q(location__city__icontains=search_query) | ||||
|             ) | ||||
|         if organizations.count() == 0: | ||||
|             return Response({"error": "No organizations found."}, status=status.HTTP_404_NOT_FOUND) | ||||
|  | ||||
|         serializer = RescueOrganizationSerializer(organizations, many=True, context={"request": request}) | ||||
|         return Response(serializer.data, status=status.HTTP_200_OK) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     @extend_schema( | ||||
|         request=RescueOrganizationSerializer, | ||||
|         responses={201: 'Rescue organization created successfully!'} | ||||
|     ) | ||||
|     def post(self, request, *args, **kwargs): | ||||
|         """ | ||||
|         Create or update a rescue organization. | ||||
|         """ | ||||
|         serializer = RescueOrganizationSerializer(data=request.data, context={"request": request}) | ||||
|         if serializer.is_valid(): | ||||
|             rescue_org = serializer.save() | ||||
|             if rescue_org.location is None: | ||||
|                 # Add the location | ||||
|                 post_rescue_org_save.delay_on_commit(rescue_org.pk) | ||||
|             return Response( | ||||
|                 {"message": "Rescue organization created successfully!", "id": rescue_org.id}, | ||||
|                 status=status.HTTP_201_CREATED, | ||||
|             ) | ||||
|  | ||||
|         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     @extend_schema( | ||||
|         request=RescueOrganizationSerializer, | ||||
|         responses={200: 'Rescue organization updated successfully!'} | ||||
|     ) | ||||
|     def patch(self, request, *args, **kwargs): | ||||
|         """ | ||||
|         Partially update a rescue organization. | ||||
|         """ | ||||
|         org_id = request.data.get("id") | ||||
|         if not org_id: | ||||
|             return Response({"error": "ID is required for updating."}, status=status.HTTP_400_BAD_REQUEST) | ||||
|  | ||||
|         try: | ||||
|             organization = RescueOrganization.objects.get(pk=org_id) | ||||
|         except RescueOrganization.DoesNotExist: | ||||
|             return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND) | ||||
|  | ||||
|         serializer = RescueOrganizationSerializer(organization, data=request.data, partial=True) | ||||
|         if serializer.is_valid(): | ||||
|             serializer.save() | ||||
|             return Response({"message": "Rescue organization updated successfully!"}, status=status.HTTP_200_OK) | ||||
|  | ||||
|         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) | ||||
|  | ||||
|  | ||||
| class AddImageApiView(APIView): | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     @transaction.atomic | ||||
|     @extend_schema( | ||||
|         request=ImageCreateSerializer, | ||||
|         responses={201: 'Image added successfully!'} | ||||
|     ) | ||||
|     def post(self, request, *args, **kwargs): | ||||
|         """ | ||||
|         Add an image to an animal or adoption notice. | ||||
|         """ | ||||
|         serializer = ImageCreateSerializer(data=request.data, context={"request": request}) | ||||
|         if serializer.is_valid(): | ||||
|             if serializer.validated_data["attach_to_type"] == "animal": | ||||
|                 object_to_attach_to = Animal.objects.get(id=serializer.validated_data["attach_to"]) | ||||
|             elif serializer.validated_data["attach_to_type"] == "adoption_notice": | ||||
|                 object_to_attach_to = AdoptionNotice.objects.get(id=serializer.validated_data["attach_to"]) | ||||
|             else: | ||||
|                 raise ValueError("Unknown attach_to_type given, should not happen. Check serializer") | ||||
|             serializer.validated_data.pop('attach_to_type', None) | ||||
|             serializer.validated_data.pop('attach_to', None) | ||||
|             image = serializer.save(owner=request.user_to_notify) | ||||
|             object_to_attach_to.photos.add(image) | ||||
|             return Response( | ||||
|                 {"message": "Image added successfully!", "id": image.id}, | ||||
|                 status=status.HTTP_201_CREATED, | ||||
|             ) | ||||
|         return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) | ||||
|  | ||||
|  | ||||
| class SpeciesApiView(APIView): | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     @extend_schema( | ||||
|         responses={200: SpeciesSerializer(many=True)} | ||||
|     ) | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         """ | ||||
|         Retrieve a list of species. | ||||
|         """ | ||||
|         species = Species.objects.all() | ||||
|         serializer = SpeciesSerializer(species, many=True, context={"request": request}) | ||||
|         return Response(serializer.data, status=status.HTTP_200_OK) | ||||
|  | ||||
|  | ||||
| class LocationApiView(APIView): | ||||
|     permission_classes = [IsAuthenticated] | ||||
|  | ||||
|     @extend_schema( | ||||
|         parameters=[ | ||||
|             { | ||||
|                 'name': 'id', | ||||
|                 'required': False, | ||||
|                 'description': 'ID of the location to retrieve.', | ||||
|                 'type': int | ||||
|             }, | ||||
|         ], | ||||
|         responses={200: LocationSerializer(many=True)} | ||||
|     ) | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         """ | ||||
|         Retrieve a location | ||||
|         """ | ||||
|         location_id = kwargs.get("id") | ||||
|         if location_id: | ||||
|             try: | ||||
|                 location = Location.objects.get(pk=location_id) | ||||
|                 serializer = LocationSerializer(location, context={"request": request}) | ||||
|                 return Response(serializer.data, status=status.HTTP_200_OK) | ||||
|             except Location.DoesNotExist: | ||||
|                 return Response({"error": "Location not found."}, status=status.HTTP_404_NOT_FOUND) | ||||
|         locations = Location.objects.all() | ||||
|         serializer = LocationSerializer(locations, many=True, context={"request": request}) | ||||
|         return Response(serializer.data, status=status.HTTP_200_OK) | ||||
|  | ||||
|     @transaction.atomic | ||||
|     @extend_schema( | ||||
|         request=LocationSerializer, | ||||
|         responses={201: 'Location created successfully!'} | ||||
|     ) | ||||
|     def post(self, request, *args, **kwargs): | ||||
|         """ | ||||
|         API view to add a location | ||||
|         """ | ||||
|         serializer = LocationSerializer(data=request.data, context={'request': request}) | ||||
|         if not serializer.is_valid(): | ||||
|             return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) | ||||
|  | ||||
|         location = serializer.save() | ||||
|  | ||||
|         # Log the action | ||||
|         Log.objects.create( | ||||
|             user=request.user, | ||||
|             action="add_location", | ||||
|             text=f"{request.user} added adoption notice {location.pk} via API", | ||||
|         ) | ||||
|  | ||||
|         # Return success response with new adoption notice details | ||||
|         return Response( | ||||
|             {"message": "Location created successfully!", "id": location.pk}, | ||||
|             status=status.HTTP_201_CREATED, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class AdoptionNoticeGeoJSONView(ListAPIView): | ||||
|     queryset = AdoptionNotice.objects.select_related('location').filter(location__isnull=False).filter( | ||||
|         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) | ||||
|   | ||||
| @@ -15,3 +15,4 @@ class FellchensammlungConfig(AppConfig): | ||||
|         except Permission.DoesNotExist: | ||||
|             pass | ||||
|         post_migrate.connect(ensure_languages, sender=self) | ||||
|         import fellchensammlung.receivers | ||||
|   | ||||
							
								
								
									
										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,21 @@ | ||||
| from django import forms | ||||
|  | ||||
| from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportComment, ModerationAction, User, Species, \ | ||||
|     Comment | ||||
|     Comment, SexChoicesWithAll, DistanceChoices, SpeciesSpecificURL, RescueOrganization | ||||
| from django_registration.forms import RegistrationForm | ||||
| from crispy_forms.helper import FormHelper | ||||
| from crispy_forms.layout import Submit, Layout, Fieldset, HTML, Row, Column, Field, Hidden | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from notfellchen.settings import MEDIA_URL | ||||
| from crispy_forms.layout import Div | ||||
|  | ||||
|  | ||||
| def animal_validator(value: str): | ||||
|     value = value.lower() | ||||
|     animal_list = ["ratte", "farbratte", "katze", "hund", "kaninchen", "hase", "kuh", "fuchs", "cow", "rat", "cat", | ||||
|                    "dog", "rabbit", "fox", "fancy rat"] | ||||
|     if value not in animal_list: | ||||
|         raise forms.ValidationError(_("Dieses Tier kenne ich nicht. Probier ein anderes")) | ||||
|  | ||||
|  | ||||
| class DateInput(forms.DateInput): | ||||
| @@ -14,92 +23,46 @@ class DateInput(forms.DateInput): | ||||
|  | ||||
|  | ||||
| class AdoptionNoticeForm(forms.ModelForm): | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         if 'in_adoption_notice_creation_flow' in kwargs: | ||||
|             in_flow = kwargs.pop('in_adoption_notice_creation_flow') | ||||
|         else: | ||||
|             in_flow = False | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self.helper = FormHelper() | ||||
|  | ||||
|         self.helper.form_id = 'form-adoption-notice' | ||||
|         self.helper.form_class = 'card' | ||||
|         self.helper.form_method = 'post' | ||||
|  | ||||
|         if in_flow: | ||||
|             submit = Submit('save-and-add-another-animal', _('Speichern')) | ||||
|  | ||||
|         else: | ||||
|             submit = Submit('submit', _('Speichern')) | ||||
|  | ||||
|         self.helper.layout = Layout( | ||||
|             Fieldset( | ||||
|                 _('Vermittlungsdetails'), | ||||
|                 'name', | ||||
|                 'species', | ||||
|                 'num_animals', | ||||
|                 'date_of_birth', | ||||
|                 'sex', | ||||
|                 'group_only', | ||||
|                 'searching_since', | ||||
|                 'location_string', | ||||
|                 'description', | ||||
|                 'further_information', | ||||
|             ), | ||||
|             submit) | ||||
|     template_name = "fellchensammlung/forms/form_snippets.html" | ||||
|  | ||||
|     class Meta: | ||||
|         model = AdoptionNotice | ||||
|         fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string"] | ||||
|  | ||||
|  | ||||
| class AdoptionNoticeFormWithDateWidget(AdoptionNoticeForm): | ||||
|     class Meta: | ||||
|         model = AdoptionNotice | ||||
|         fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string"] | ||||
|         fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string", | ||||
|                   "organization"] | ||||
|         widgets = { | ||||
|             'searching_since': DateInput(), | ||||
|             'searching_since': DateInput(format=('%Y-%m-%d')), | ||||
|         } | ||||
|  | ||||
|  | ||||
|  | ||||
| class AnimalForm(forms.ModelForm): | ||||
| class AdoptionNoticeFormAutoAnimal(AdoptionNoticeForm): | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         if 'in_adoption_notice_creation_flow' in kwargs: | ||||
|             adding = kwargs.pop('in_adoption_notice_creation_flow') | ||||
|         else: | ||||
|             adding = False | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self.helper = FormHelper() | ||||
|         self.helper.form_class = 'form-animal card' | ||||
|         if adding: | ||||
|             self.helper.add_input(Submit('save-and-add-another-animal', _('Speichern und weiteres Tier hinzufügen'))) | ||||
|             self.helper.add_input(Submit('save-and-finish', _('Speichern und beenden'))) | ||||
|         else: | ||||
|             self.helper.add_input(Submit('submit', _('Speichern'), css_class="btn")) | ||||
|  | ||||
|     class Meta: | ||||
|         model = Animal | ||||
|         fields = ["name", "date_of_birth", "species", "sex", "description"] | ||||
|  | ||||
|  | ||||
| class AnimalFormWithDateWidget(AnimalForm): | ||||
|     class Meta: | ||||
|         model = Animal | ||||
|         fields = ["name", "date_of_birth", "species", "sex", "description"] | ||||
|         widgets = { | ||||
|             'date_of_birth': DateInput(), | ||||
|         } | ||||
|  | ||||
| class AdoptionNoticeFormWithDateWidgetAutoAnimal(AdoptionNoticeFormWithDateWidget): | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super(AdoptionNoticeFormWithDateWidgetAutoAnimal, self).__init__(*args, **kwargs) | ||||
|         super(AdoptionNoticeFormAutoAnimal, self).__init__(*args, **kwargs) | ||||
|         self.fields["num_animals"] = forms.fields.IntegerField(min_value=1, max_value=30, label=_("Zahl der Tiere")) | ||||
|         animal_form = AnimalForm() | ||||
|         self.fields["species"] = animal_form.fields["species"] | ||||
|         self.fields["sex"] = animal_form.fields["sex"] | ||||
|         self.fields["date_of_birth"] = animal_form.fields["date_of_birth"] | ||||
|         self.fields["date_of_birth"].widget = DateInput() | ||||
|         self.fields["date_of_birth"].widget = DateInput(format=('%Y-%m-%d')) | ||||
|  | ||||
|  | ||||
| class AnimalForm(forms.ModelForm): | ||||
|     template_name = "fellchensammlung/forms/form_snippets.html" | ||||
|  | ||||
|     class Meta: | ||||
|         model = Animal | ||||
|         fields = ["name", "date_of_birth", "species", "sex", "description"] | ||||
|  | ||||
|         widgets = { | ||||
|             'date_of_birth': DateInput(format=('%Y-%m-%d')) | ||||
|         } | ||||
|  | ||||
|  | ||||
| class UpdateRescueOrgRegularCheckStatus(forms.ModelForm): | ||||
|     template_name = "fellchensammlung/forms/form_snippets.html" | ||||
|  | ||||
|     class Meta: | ||||
|         model = RescueOrganization | ||||
|         fields = ["regular_check_status"] | ||||
|  | ||||
|  | ||||
| class ImageForm(forms.ModelForm): | ||||
| @@ -113,11 +76,21 @@ class ImageForm(forms.ModelForm): | ||||
|         self.helper.form_id = 'form-animal-photo' | ||||
|         self.helper.form_class = 'card' | ||||
|         self.helper.form_method = 'post' | ||||
|  | ||||
|         if in_flow: | ||||
|             self.helper.add_input(Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen'))) | ||||
|             self.helper.add_input(Submit('submit', _('Speichern'))) | ||||
|             submits = Div(Submit('submit', _('Speichern')), | ||||
|                           Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')), | ||||
|                           css_class="container-edit-buttons") | ||||
|         else: | ||||
|             self.helper.add_input(Submit('submit', _('Submit'))) | ||||
|             submits = Fieldset(Submit('submit', _('Speichern')), css_class="container-edit-buttons") | ||||
|         self.helper.layout = Layout( | ||||
|             Div( | ||||
|                 'image', | ||||
|                 'alt_text', | ||||
|                 css_class="spaced", | ||||
|             ), | ||||
|             submits | ||||
|         ) | ||||
|  | ||||
|     class Meta: | ||||
|         model = Image | ||||
| @@ -125,53 +98,76 @@ class ImageForm(forms.ModelForm): | ||||
|  | ||||
|  | ||||
| class ReportAdoptionNoticeForm(forms.ModelForm): | ||||
|     template_name = "fellchensammlung/forms/form_snippets.html" | ||||
|  | ||||
|     class Meta: | ||||
|         model = ReportAdoptionNotice | ||||
|         fields = ('reported_broken_rules', 'user_comment') | ||||
|  | ||||
|  | ||||
| class ReportCommentForm(forms.ModelForm): | ||||
|     template_name = "fellchensammlung/forms/form_snippets.html" | ||||
|  | ||||
|     class Meta: | ||||
|         model = ReportComment | ||||
|         fields = ('reported_broken_rules', 'user_comment') | ||||
|  | ||||
|  | ||||
| class CommentForm(forms.ModelForm): | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self.helper = FormHelper() | ||||
|         self.helper.form_class = 'form-comments' | ||||
|         self.helper.add_input(Hidden('action', 'comment')) | ||||
|         self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="btn2")) | ||||
|  | ||||
|     class Meta: | ||||
|         model = Comment | ||||
|         fields = ('text',) | ||||
|  | ||||
|  | ||||
| class SpeciesURLForm(forms.ModelForm): | ||||
|     class Meta: | ||||
|         model = SpeciesSpecificURL | ||||
|         fields = ('species', 'url') | ||||
|  | ||||
|  | ||||
| class RescueOrgInternalComment(forms.ModelForm): | ||||
|     class Meta: | ||||
|         model = RescueOrganization | ||||
|         fields = ('internal_comment',) | ||||
|  | ||||
|  | ||||
| class ModerationActionForm(forms.ModelForm): | ||||
|     class Meta: | ||||
|         model = ModerationAction | ||||
|         fields = ('action', 'public_comment', 'private_comment') | ||||
|  | ||||
|  | ||||
| class AddedRegistrationForm(forms.Form): | ||||
|     captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_( | ||||
|         "Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um sicherzustellen, dass du kein Roboter bist.")) | ||||
|  | ||||
|     def signup(self, request, user): | ||||
|         pass | ||||
|  | ||||
|  | ||||
| class CustomRegistrationForm(RegistrationForm): | ||||
|     class Meta(RegistrationForm.Meta): | ||||
|         model = User | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self.helper = FormHelper() | ||||
|         self.helper.form_id = 'form-registration' | ||||
|         self.helper.form_class = 'card' | ||||
|     template_name = "fellchensammlung/forms/form_snippets.html" | ||||
|  | ||||
|         self.helper.add_input(Submit('submit', _('Registrieren'), css_class="btn")) | ||||
|  | ||||
|  | ||||
| def _get_distances(): | ||||
|     return {i: i for i in [10, 20, 50, 100, 200, 500]} | ||||
|     captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_( | ||||
|         "Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um sicherzustellen, dass du kein Roboter bist.")) | ||||
|  | ||||
|  | ||||
| class AdoptionNoticeSearchForm(forms.Form): | ||||
|     location = forms.CharField(max_length=20, label=_("Stadt")) | ||||
|     max_distance = forms.ChoiceField(choices=_get_distances, label=_("Max. Distanz")) | ||||
|     template_name = "fellchensammlung/forms/form_snippets.html" | ||||
|  | ||||
|     sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False, | ||||
|                             initial=SexChoicesWithAll.ALL) | ||||
|     max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED, | ||||
|                                      label=_("Suchradius")) | ||||
|     location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False) | ||||
|  | ||||
|  | ||||
| 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,33 +1,43 @@ | ||||
| import django.conf.global_settings | ||||
| from django.db.models.signals import post_save | ||||
| from django.dispatch import receiver | ||||
| from django.template.loader import render_to_string | ||||
| from django.utils.html import strip_tags | ||||
|  | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from django.utils.translation import gettext | ||||
| from django.conf import settings | ||||
| from django.core import mail | ||||
| from django.db.models import Q, Min | ||||
| from fellchensammlung.models import User | ||||
| from notfellchen.settings import host | ||||
| from fellchensammlung.models import User, Notification, TrustLevel, NotificationTypeChoices | ||||
| from fellchensammlung.tools.model_helpers import ndm | ||||
|  | ||||
| NEWLINE = "\r\n" | ||||
|  | ||||
| def mail_admins_new_report(report): | ||||
|     subject = _("Neue Meldung") | ||||
|     for moderator in User.objects.filter(trust_level__gt=User.TRUST_LEVEL[User.MODERATOR]): | ||||
|         greeting = _("Moin,") + "{NEWLINE}" | ||||
|         new_report_text = _("es wurde ein Regelverstoß gemeldet.") + "{NEWLINE}" | ||||
|         if len(report.reported_broken_rules.all()) > 0: | ||||
|             reported_rules_text = (f"Ein Verstoß gegen die folgenden Regeln wurde gemeldet:{NEWLINE}" | ||||
|                                    f"- {f'{NEWLINE} - '.join([str(r) for r in report.reported_broken_rules.all()])}{NEWLINE}") | ||||
| def notify_mods_new_report(report, notification_type): | ||||
|     """ | ||||
|     Sends an e-mail to all users that should handle the report. | ||||
|     """ | ||||
|     for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR): | ||||
|         if notification_type == NotificationTypeChoices.NEW_REPORT_AN: | ||||
|             title = _("Vermittlung gemeldet") | ||||
|         elif notification_type == NotificationTypeChoices.NEW_REPORT_COMMENT: | ||||
|             title = _("Kommentar gemeldet") | ||||
|         else: | ||||
|             reported_rules_text = f"Es wurden keine Regeln angegeben gegen die Verstoßen wurde.{NEWLINE}" | ||||
|         if report.user_comment: | ||||
|             comment_text = f'Kommentar zum Report: "{report.user_comment}"{NEWLINE}' | ||||
|         else: | ||||
|             comment_text = f"Es wurde kein Kommentar hinzugefügt.{NEWLINE}" | ||||
|             raise NotImplementedError | ||||
|         notification = Notification.objects.create( | ||||
|             notification_type=notification_type, | ||||
|             user_to_notify=moderator, | ||||
|             report=report, | ||||
|             title=title, | ||||
|         ) | ||||
|         notification.save() | ||||
|  | ||||
|         report_url = "https://" + host + report.get_absolute_url() | ||||
|         link_text = f"Um alle Details zu sehen, geh bitte auf: {report_url}" | ||||
|         body_text = greeting + new_report_text + reported_rules_text + comment_text + link_text | ||||
|         message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [moderator.email]) | ||||
|         print("Sending email to ", moderator.email) | ||||
|         message.send() | ||||
|  | ||||
| def send_notification_email(notification_pk): | ||||
|     notification = Notification.objects.get(pk=notification_pk) | ||||
|  | ||||
|     subject = f"{notification.title}" | ||||
|     context = {"notification": notification, } | ||||
|     html_message = render_to_string(ndm[notification.notification_type].email_html_template, context) | ||||
|     plain_message = render_to_string(ndm[notification.notification_type].email_plain_template, context) | ||||
|  | ||||
|     mail.send_mail(subject, plain_message, settings.DEFAULT_FROM_EMAIL, | ||||
|                    [notification.user_to_notify.email], | ||||
|                    html_message=html_message) | ||||
|   | ||||
							
								
								
									
										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) | ||||
| @@ -7,7 +7,7 @@ from fellchensammlung import baker_recipes | ||||
| from model_bakery import baker | ||||
|  | ||||
| from fellchensammlung.models import AdoptionNotice, Species, Animal, Image, ModerationAction, User, Rule, \ | ||||
|     Report, Comment, ReportAdoptionNotice | ||||
|     Report, Comment, ReportAdoptionNotice, TrustLevel | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
| @@ -101,10 +101,10 @@ class Command(BaseCommand): | ||||
|  | ||||
|         User.objects.create_user('test', password='foobar') | ||||
|         admin1 = User.objects.create_superuser(username="admin", password="admin", email="admin1@example.org", | ||||
|                                                trust_level=User.TRUST_LEVEL[User.ADMIN]) | ||||
|                                                trust_level=TrustLevel.ADMIN) | ||||
|  | ||||
|         mod1 = User.objects.create_user(username="mod1", password="mod", email="mod1@example.org", | ||||
|                                         trust_level=User.TRUST_LEVEL[User.MODERATOR]) | ||||
|                                         trust_level=TrustLevel.MODERATOR) | ||||
|  | ||||
|         comment1 = baker.make(Comment, user=admin1, text="This is a comment", adoption_notice=adoption1) | ||||
|         comment2 = baker.make(Comment, | ||||
|   | ||||
							
								
								
									
										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,18 @@ | ||||
| # Generated by Django 5.1.1 on 2024-11-13 15:33 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('fellchensammlung', '0015_rescueorganization_comment'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='rescueorganization', | ||||
|             name='phone_number', | ||||
|             field=models.CharField(blank=True, max_length=15, null=True, verbose_name='Telefonnummer'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,19 @@ | ||||
| # Generated by Django 5.1.1 on 2024-11-14 06:42 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('fellchensammlung', '0016_rescueorganization_phone_number'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='organization_affiliation', | ||||
|             field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.rescueorganization', verbose_name='Organisation'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 5.1.1 on 2024-11-14 17:58 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('fellchensammlung', '0017_user_organization_affiliation'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='rescueorganization', | ||||
|             name='description', | ||||
|             field=models.TextField(blank=True, null=True, verbose_name='Beschreibung'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 5.1.1 on 2024-11-14 18:30 | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('fellchensammlung', '0018_rescueorganization_description'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RenameField( | ||||
|             model_name='rescueorganization', | ||||
|             old_name='comment', | ||||
|             new_name='internal_comment', | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 5.1.1 on 2024-11-14 18:31 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('fellchensammlung', '0019_rename_comment_rescueorganization_internal_comment'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='rescueorganization', | ||||
|             name='internal_comment', | ||||
|             field=models.TextField(blank=True, null=True, verbose_name='Interner Kommentar'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,19 @@ | ||||
| # Generated by Django 5.1.1 on 2024-11-14 20:15 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('fellchensammlung', '0020_alter_rescueorganization_internal_comment'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='reason_for_signup', | ||||
|             field=models.TextField(default='-', verbose_name='Grund für die Registrierung'), | ||||
|             preserve_default=False, | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,23 @@ | ||||
| # Generated by Django 5.1.1 on 2024-11-20 18:50 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('fellchensammlung', '0021_user_reason_for_signup'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='user', | ||||
|             name='reason_for_signup', | ||||
|             field=models.TextField(help_text="Wir würden gerne wissen warum du dich registriertst, ob du dich z.B. Tiere eines bestimmten Tierheim einstellen willst 'nur mal gucken' willst. Beides ist toll! Wenn du für ein Tierheim/eine Pflegestelle arbeitest kontaktieren wir dich ggf. um dir erweiterte Rechte zu geben.", verbose_name='Grund für die Registrierung'), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='user', | ||||
|             name='trust_level', | ||||
|             field=models.IntegerField(choices=[(1, 'Member'), (2, 'Coordinator'), (3, 'Moderator'), (4, 'Admin')], default=1), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 5.1.1 on 2024-11-20 19:25 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('fellchensammlung', '0022_alter_user_reason_for_signup_alter_user_trust_level'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='email_notifications', | ||||
|             field=models.BooleanField(default=True, verbose_name='Benachrichtigung per E-Mail'), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										18
									
								
								src/fellchensammlung/migrations/0024_alter_animal_sex.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 5.1.1 on 2024-11-21 19:34 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('fellchensammlung', '0023_user_email_notifications'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='animal', | ||||
|             name='sex', | ||||
|             field=models.CharField(choices=[('M_N', 'neutered male'), ('M', 'male'), ('F_N', 'neutered female'), ('F', 'female'), ('I', 'intersex')], max_length=20), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										18
									
								
								src/fellchensammlung/migrations/0025_alter_animal_sex.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 5.1.1 on 2024-11-21 19:41 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('fellchensammlung', '0024_alter_animal_sex'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='animal', | ||||
|             name='sex', | ||||
|             field=models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich Kastriert'), ('I', 'Intersex')], max_length=20), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										18
									
								
								src/fellchensammlung/migrations/0026_alter_animal_sex.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,18 @@ | ||||
| # Generated by Django 5.1.1 on 2024-11-21 21:49 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('fellchensammlung', '0025_alter_animal_sex'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='animal', | ||||
|             name='sex', | ||||
|             field=models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich, kastriert'), ('I', 'Intergeschlechtlich')], max_length=20), | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,27 @@ | ||||
| # Generated by Django 5.1.1 on 2024-12-14 07:57 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('fellchensammlung', '0026_alter_animal_sex'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='animal', | ||||
|             name='species', | ||||
|             field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.species', verbose_name='Tierart'), | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name='AndoptionNoticeNotification', | ||||
|             fields=[ | ||||
|                 ('basenotification_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='fellchensammlung.basenotification')), | ||||
|                 ('adoption_notice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')), | ||||
|             ], | ||||
|             bases=('fellchensammlung.basenotification',), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										25
									
								
								src/fellchensammlung/migrations/0028_searchsubscription.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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), | ||||
|     ] | ||||