455 Commits

Author SHA1 Message Date
2589f1c703 fix: Make sure rescue orgs with ans only in hierarch show correctly
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-07-22 13:25:47 +02:00
0edb9094c4 feat: Show parent org 2025-07-21 17:11:13 +02:00
bc8feba701 feat: Show all adoption notices of rescue org and children 2025-07-20 16:58:18 +02:00
f37d74a7d1 feat: Add child orgs to org detail page 2025-07-20 16:29:56 +02:00
fa8612ad1a fix: ensure location is displayed 2025-07-20 16:29:27 +02:00
1d8a054b06 feat: Add number of animals per sex to metrics 2025-07-20 16:09:59 +02:00
5898fbf86d feat: Add number of animals per sex to metrics 2025-07-20 15:43:59 +02:00
cd1cdd2e0b feat: Add link to admin interface 2025-07-20 15:32:13 +02:00
c0f920544b feat: Add automatic post in the evening 2025-07-20 13:49:24 +02:00
36c90531a8 feat: Add option to switch between normal and dq 2025-07-20 13:42:56 +02:00
7f7c5a3b04 fix: post all available pictures 2025-07-20 08:50:00 +02:00
c084e56ad8 chore: Bump version to 1.1.0 2025-07-20 07:59:51 +02:00
84acc3c76e feat: format posts in markdown 2025-07-20 07:58:36 +02:00
e1f0014898 feat: Add "Post to Fediverse" 2025-07-20 07:07:33 +02:00
05b3a470f3 feat: Add warning about deactivated ANs 2025-07-19 09:29:15 +02:00
ebe060646a trans: Add a few translations 2025-07-17 15:57:22 +02:00
bb412be8d3 feat: Add view for specialized rescues 2025-07-14 07:31:08 +02:00
e3c48eac24 feat: fail more gracefully 2025-07-14 07:16:17 +02:00
da89cdceda feat: use simpler m2m relationship for specialization 2025-07-14 07:15:44 +02:00
5a6c2c99e5 feat: add important locations and buying to sitemap and fix 2025-07-14 06:33:12 +02:00
9f53836ce8 feat: add page dedicated to buying animals 2025-07-14 06:18:56 +02:00
5d53d1a1dc feat: add a default order for rescue orgs (very useful when adding ANs to it) 2025-07-13 13:01:58 +02:00
e00dda1dc2 feat: add option to mark a rescue org to be in active communication
That enables to filter them out from a check without forgetting there are to-dos. Most often this will be used when you want to call a rescue org but they can currently not be reached
2025-07-13 12:58:14 +02:00
a93e0c819f fix: use correct user 2025-07-13 12:08:42 +02:00
c87733b37a feat: Open shelters in new tab 2025-07-13 11:02:00 +02:00
9aa964bf05 feat: style external site warning 2025-07-13 10:30:51 +02:00
dcb1d3ec15 feat: Add functionality to deactivate AN with reason 2025-07-13 10:06:13 +02:00
5d9b8f3213 feat: Re-add functionality to set AN as checked 2025-07-13 09:12:24 +02:00
d12989d195 feat: Add display of when last checked 2025-07-13 09:11:51 +02:00
a9f384b50e feat: add subscribe functionality again 2025-07-13 01:08:43 +02:00
afedf2d0bd feat: add button to mark all notifications as read and fix action 2025-07-13 00:31:21 +02:00
a4b8486bd4 feat: make use of new notification mapping 2025-07-13 00:00:30 +02:00
d8bcb8ece6 refactor: remove redundant imports 2025-07-12 17:45:21 +02:00
b01ac219a3 feat: Add notification partials including new mapping system for templates 2025-07-12 17:43:40 +02:00
42320866c4 feat: Show nicer time of creating the notification 2025-07-12 16:46:56 +02:00
e2e6c14d57 feat: Show newest notification first 2025-07-12 16:46:17 +02:00
4761c38cd2 feat: Move cards to bulma notifications 2025-07-12 16:31:15 +02:00
e2bef3efe2 feat: Add dedicated notification page 2025-07-12 14:01:40 +02:00
bbfd4c3800 feat: re-add notification badge 2025-07-12 13:34:18 +02:00
b671d8fbb4 fix: fix filters 2025-07-12 13:34:02 +02:00
1ea04e98e8 fix: fucking wired bug where the url is not displayed in plaintext
that how an e-mail looked before:
Moin,

es wurde ein neuer Useraccount erstellt.

[admin] sgsfg (2025-07-12 09:44:27.251212+00:00)
Related:

http://localhost:8000/user/9/
User anzeigen:

---
2025-07-12 11:46:47 +02:00
c1a7d6790b feat: Reorder notification fields for nicer display in admin 2025-07-12 11:25:08 +02:00
f519f78922 feat: Add unsubscribe link 2025-07-12 11:24:44 +02:00
551b5ed6be feat: add plaintext versions of the e-mail 2025-07-12 09:17:58 +02:00
20cbb0397a Merge branch 'notification_rework' into develop
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-07-11 22:12:57 +02:00
26f999c4cf fix: url 2025-07-11 18:37:43 +02:00
9858dfc1bd fix: search notification 2025-07-11 18:34:30 +02:00
2d1df879dc fix: use complete URL 2025-07-11 18:26:41 +02:00
2f12dc6a5e fix: use complete URL 2025-07-11 18:19:28 +02:00
4b3286f12d fix: revert allowing an empty user to notify 2025-07-11 17:51:39 +02:00
d53c5707b8 feat: remove notification bell that might not get displayed 2025-07-11 17:34:55 +02:00
ba64661217 refactor: remove prints 2025-07-11 17:34:39 +02:00
554de17e9e fix: Add missing migration 2025-07-11 17:34:18 +02:00
5bc4b538e6 refactor: formatting 2025-07-11 17:33:58 +02:00
8a691d59e7 feat: add base url in order to properly do URLs in e-mails 2025-07-11 16:53:01 +02:00
e99798ba5c feat: Completely rework notification framework
This change is needed as
a) there are many different types of notifications. A notification about a new comment does not only relate the comment but also adoption notice and a user that made the comment. Therefore, I introduce a new general notification that has optional links to other objects.
b) this change allows to define a different representation of e-mails vs. display on the website. The new notification type is in charge of representation from now on. Title and text will be deprecated and eventually removed
2025-07-11 11:39:55 +02:00
caa962700b feat: add link to admin site 2025-07-10 20:51:49 +02:00
648466aa70 feat: Move report e-mail to html 2025-07-07 21:45:49 +02:00
25f84bf2ad try: remove html password reset 2025-07-07 21:10:03 +02:00
ac2147095a try: add html password reset 2025-07-07 21:09:36 +02:00
4856c720b1 feat: Make account activation use html e-mail 2025-07-07 21:01:21 +02:00
ba8ff743f2 feat: Add option to send test-html e-mail 2025-07-07 20:23:00 +02:00
1e827af2dd feat: Ensure Default from is always set 2025-07-07 19:57:53 +02:00
7d107abc6a fix: var name 2025-07-07 19:57:16 +02:00
3bb1cd29cd fix: spacing 2025-07-07 19:18:04 +02:00
0388367c7a fix: card styling 2025-07-07 18:39:27 +02:00
a8843dfc8f fix: ensure heading styling 2025-07-07 17:56:30 +02:00
9d1264b6e6 fix: ensure heading styling 2025-07-07 17:56:02 +02:00
8df04519b9 fix: ensure heading styling 2025-07-07 17:54:04 +02:00
112a731cd6 fix: ensure heading styling 2025-07-07 17:51:45 +02:00
8b4ff83921 fix: ensure heading styling 2025-07-07 17:51:00 +02:00
2249b615f4 fix: ensure heading styling 2025-07-07 17:50:06 +02:00
fbfc800453 feat: Add submit functionality and styling 2025-07-07 17:41:52 +02:00
752aaf9b89 fix: format search overview result a bit 2025-07-07 17:24:09 +02:00
f1a0d5f475 fix: ensure heading is styled 2025-07-07 17:17:40 +02:00
fb5f38b3e6 fix: make sure main content does not overwrite content class of bulma 2025-07-07 17:13:50 +02:00
ded5299387 fix: stop overwriting of page number (is string when coming in) 2025-07-07 17:13:26 +02:00
090548905f feat: Add annotations to drf spectacular 2025-07-07 16:59:13 +02:00
12a89b6927 feat: ensure page number is set 2025-07-07 16:42:20 +02:00
fc4c348ff9 feat: translate strings 2025-07-07 16:30:08 +02:00
1cd133e335 feat: improve style of update buttons 2025-07-03 21:27:31 +02:00
b5dc6ca97d fix: remove unsafe link generation 2025-07-03 21:18:54 +02:00
f7b98c9dfe fix: typo 2025-07-03 20:59:53 +02:00
8e9f4e2b2e feat: reduce number of rescue orgs per page 2025-07-03 19:53:26 +02:00
b3faa06c4c feat: simplify pagination by using get_elided_page_range 2025-07-03 19:51:31 +02:00
165aeb6dcc feat: basic pagination 2025-07-03 19:40:52 +02:00
24bae28cec feat: use csrf token 2025-07-03 18:38:27 +02:00
65182d4c2f feat: Add much improved add adoption notice form
TODO: Solve token issue
2025-07-03 17:45:26 +02:00
1d879993c9 feat: Add organization to adoption notice detail page 2025-07-03 16:39:17 +02:00
f3fbf3ba1d feat: Add SpeciesSpecialization to rescue org form 2025-07-03 11:45:25 +02:00
5a24f32327 fix: String representation 2025-07-03 11:45:07 +02:00
d72bd22f6d feat: Add species specialization and parent option to Rescue Orgs 2025-07-03 11:29:11 +02:00
0eb94038f6 docs: add getting started section 2025-07-03 11:07:35 +02:00
3a69397c0a docs: move docs dependencies to optional 2025-07-03 11:07:18 +02:00
d49cb5a783 docs: Minor api docs adjustments 2025-07-03 10:35:25 +02:00
c13805dd75 fix: remove unecessary print 2025-06-29 18:08:56 +02:00
bcf8a0e6e4 fix: add host to image - will break if cdn is used 2025-06-29 18:06:58 +02:00
81f9398da4 fix: add image us property for description 2025-06-29 11:06:08 +02:00
32c90aecd3 fix: canonical url 2025-06-29 10:49:52 +02:00
01d6c1e0f6 feat: add metatags for search 2025-06-29 10:32:53 +02:00
607a442e22 feat: add various metatags 2025-06-29 08:56:46 +02:00
b2a79b3547 fix: tag 2025-06-29 08:39:02 +02:00
63c692d46b feat: start adding canonical urls 2025-06-29 08:26:26 +02:00
3d2ef9e735 refactor: formatting 2025-06-29 08:24:28 +02:00
aee8b0c1e8 feat: add og tags to adoption notice detail 2025-06-28 20:36:29 +02:00
d5e28ba3d9 fix: load tag 2025-06-28 20:19:56 +02:00
1edaf4df14 feat: add og tags 2025-06-28 20:02:04 +02:00
b6d31e3c3b feat: add base for js-ification of add adoption form 2025-06-27 15:36:57 +02:00
ed7b55c090 feat: move print to logging 2025-06-27 15:27:07 +02:00
e66e9ad888 fix(accessibility): Hide report flag aria 2025-06-24 22:10:03 +02:00
4ad8b30e04 fix(accessibility): Adjust link color of footer idem in darmode 2025-06-24 22:09:45 +02:00
ae2ac5c462 fix(accessibility): Make darkmode have sufficient contrast 2025-06-24 13:47:24 +02:00
2330542a85 fix(accessibility): don't use tabindex > 0
See https://developer.mozilla.org/en-US/docs/Web/Accessibility/Guides/Understanding_WCAG/Keyboard?utm_source=devtools&utm_medium=a11y-panel-checks-keyboard#avoid_using_tabindex_attribute_greater_than_zero
2025-06-24 13:47:15 +02:00
7363e1ab30 fix(accessibility): Make main menu focusable 2025-06-24 13:45:14 +02:00
d2131b2c91 feat: don't count excluded rescue orgs in statistics 2025-06-23 22:41:19 +02:00
78866c86cd fix: catch error if there are no rescue orgs 2025-06-23 22:34:44 +02:00
f5dbccb9c4 feat: Make build compressing the css 2025-06-23 22:30:24 +02:00
8ab38cc71b refactor: Move all css to main 2025-06-23 20:39:41 +02:00
7dfcbfe38f feat: Add catogories, FN, website and GEO 2025-06-23 19:21:33 +02:00
cd8471036c feat: Add initial support for contacts 2025-06-23 18:38:15 +02:00
c3ec477a6e feat: Make spacing of FA icons uniform 2025-06-23 17:57:56 +02:00
5a6294adf6 fix: Add asterix for mandatory fields 2025-06-23 17:50:15 +02:00
1ba44cdd67 Add placeholder for contact export 2025-06-23 17:23:12 +02:00
dfeb88f980 feat: ensure spacing 2025-06-23 17:19:29 +02:00
44a724809f feat: enforce maximum width and center 2025-06-23 17:16:02 +02:00
1acd4be953 fix: Unify display of sex badges 2025-06-23 17:15:48 +02:00
930a5383c4 Merge branch 'admin_link' into develop 2025-06-23 12:30:09 +02:00
eda6da7f12 fix: Fix link to admin interface
see https://forum.djangoproject.com/t/reverse-admin-urls/41535/2
2025-06-23 12:24:53 +02:00
448fc395d8 fix: percentage 2025-06-23 12:19:21 +02:00
7e8b665c7c feat: add simple stats 2025-06-22 22:00:39 +02:00
7be61bc9b8 feat: try reverse 2025-06-22 21:27:52 +02:00
6828af76dc feat: Add display of permission 2025-06-22 17:58:38 +02:00
fe92d762be feat: Add form for internal comment in dq view 2025-06-22 17:35:36 +02:00
b91a17e950 feat: Add link to admin interface 2025-06-22 14:18:22 +02:00
aabc549bcf feat: improve display performance of rescue orgs by limiting 2025-06-22 09:06:24 +02:00
4bb4d0386b release: 1.0.1 2025-06-21 08:18:19 +02:00
a74af5e4d3 refactor: don't rebuild logic 2025-06-21 08:07:34 +02:00
a939d53286 refactor: Remove deprecated str_hr 2025-06-21 08:04:52 +02:00
35c6aae552 feat: style buttons 2025-06-21 07:49:50 +02:00
83bd6cf7e6 fix: ensure links as buttons have no underline 2025-06-21 07:46:21 +02:00
17a0cfbde0 feat: allow filtering for rescues orgs without external source 2025-06-21 07:42:51 +02:00
dbcad42da0 feat: allow searching for rescue org based on city 2025-06-21 07:42:18 +02:00
1b48022b63 release: Release v1.0.0 2025-06-20 19:52:30 +02:00
7b8e3061d5 feat: add styled footer items for cards 2025-06-20 19:52:04 +02:00
3f27564075 feat: allow to exclude from check 2025-06-20 19:06:41 +02:00
e7c2746eab feat: add data quality view to add species specific urls 2025-06-20 19:00:53 +02:00
1e243496fb fix: get species urls 2025-06-20 19:00:28 +02:00
53bc433aaa fix: typo 2025-06-20 18:53:09 +02:00
6f5e75a1b3 refactor: change comment form to bulma 2025-06-20 18:42:28 +02:00
f83851b694 docs: Add a more detailed description 2025-06-20 09:19:07 +02:00
3f5a5dceb5 feat: show number of results in search 2025-06-20 09:06:19 +02:00
f9c7dd8c39 feat: decrease size and make levels mobile responsive 2025-06-20 08:50:00 +02:00
f3e437dbd1 Merge branch 'overpass-take-2' into develop
# Conflicts:
#	scripts/upload_animal_shelters.py
2025-06-20 08:08:42 +02:00
5ee1e61eac fix: ensure that search is actually using search params 2025-06-20 08:05:34 +02:00
abd34ec7cb feat: allow using cached results 2025-06-20 08:05:11 +02:00
b73f6db7b6 feat: dump data in geojson format 2025-06-20 08:04:13 +02:00
3b9f10dad7 fix: Make sure that location uses center, don't provide fediverse or description if empty and actually create and use location 2025-06-20 08:03:40 +02:00
Patrick Moore
53c0e8b3b8 feat: Merge moan0s changes
These changes may enhance animal shelter data handling with new add_if_available function and improved organization checks
2025-06-19 23:11:58 -04:00
Patrick Moore
7d264fe131 fix: Update get_overpass_result to accept data_file parameter and save response to file 2025-06-19 23:04:31 -04:00
Patrick Moore
c968b39657 fix: Refactor Overpass query to use requests and osmtogeojson for animal shelter data retrieval 2025-06-19 23:04:31 -04:00
Patrick Moore
ebf116f347 feat: Add Overpass API integration for animal shelter data retrieval 2025-06-19 23:04:25 -04:00
8227866e7a refactor: rename functions 2025-06-19 23:34:20 +02:00
6f5e73b533 fix: correct link 2025-06-19 23:34:05 +02:00
a302a36fd4 feat: Allow excluding rescue org from checking 2025-06-19 23:33:48 +02:00
1307b2ff7b feat: redesign rescue check 2025-06-19 23:22:53 +02:00
d9730e765e feat: add mod tool overview 2025-06-19 23:06:30 +02:00
8420c698d4 feat: add logout button 2025-06-19 22:56:39 +02:00
14be917d43 fix: remove unnecessary classes, don't put link in paragraph to not underline 2025-06-19 22:12:16 +02:00
20da09fb96 fix: show contact data when available 2025-06-19 22:04:32 +02:00
19de7d3e4c fix: view name 2025-06-19 17:57:01 +02:00
41d821b86e fix: template path 2025-06-19 17:56:04 +02:00
b758b54233 fix: external site template 2025-06-19 17:55:32 +02:00
c650266c6b trans: typo update trans files 2025-06-19 17:52:51 +02:00
dae9bb0916 fix: typo 2025-06-19 17:51:47 +02:00
2f2371d8df feat: Rework update queue to use card 2025-06-19 17:50:03 +02:00
baaf4b70ac feat: Rework search subscriptions to use panel 2025-06-19 17:38:05 +02:00
3b1cd800f6 feat: Improve search subscription display 2025-06-19 17:24:46 +02:00
0f5f7216ac fix: fix link 2025-06-19 17:22:46 +02:00
c40872379e feat(a11y): underline links for accessibility 2025-06-19 17:18:12 +02:00
897ac5ceef feat: properly space rules and TOS 2025-06-19 17:17:40 +02:00
eb3dbb3e45 trans: Add general de translation 2025-06-19 17:17:22 +02:00
9eb6042ba7 trans: Finish en translation 2025-06-19 17:07:01 +02:00
075833aa25 trans: Improve translation 4 2025-06-19 16:57:02 +02:00
f84d800bff trans: Improve translation 3 2025-06-19 16:52:29 +02:00
20f814b0ef trans: Improve translation 2 2025-06-19 16:44:01 +02:00
4376a63e93 trans: Improve translation 1 2025-06-19 16:37:06 +02:00
ee3d316175 docs: Document css build 2025-06-19 14:31:47 +02:00
452113b4bf feat: Rewrite search with jquery and use proper dropdown 2025-06-19 14:25:01 +02:00
e9b28ea1c1 feat: restructure search to showhelptext nicer 2025-06-19 11:58:45 +02:00
ba07533667 feat: restrict ANs to 4 on index 2025-06-19 11:48:21 +02:00
1ab5c4885e feat: allow smaller tags 2025-06-19 11:43:53 +02:00
8c977cf255 refactor: normalize name of detail pages 2025-06-19 08:05:40 +02:00
d4c6014e17 refactor: normalize form template names 2025-06-19 08:05:13 +02:00
078e5e28cc refactor: remove deprecated template 2025-06-19 08:04:14 +02:00
7010b4f3d2 refactor: rename lists 2025-06-19 08:03:11 +02:00
43f38b88ce refactor: rename partials 2025-06-19 08:02:43 +02:00
7a12a1a4d6 refactor: remove deprecated template 2025-06-19 08:01:43 +02:00
d784f14c4c refactor: remove deprecated template 2025-06-19 08:00:19 +02:00
339cdf3ea9 fix: don't show age twice 2025-06-19 07:59:44 +02:00
060be3b486 refactor: remove deprecated templates 2025-06-19 07:59:28 +02:00
9f93a19d51 refactor: rename templates of main sites 2025-06-19 07:55:43 +02:00
c131c07afe refactor: rename template search 2025-06-19 07:53:10 +02:00
42dbf5c6f7 feat: Add information on subscribing to search 2025-06-19 06:41:09 +02:00
2e9039a569 feat: Improve display when no contactdata is there + formatting 2025-06-18 20:05:23 +02:00
64b48efafb feat: Split up comments and comment form 2025-06-18 20:04:45 +02:00
37e8dc4bdc feat: add photo and edit option to 2025-06-18 20:04:28 +02:00
61deb96961 fix: fix rendering of adoption notice edit form 2025-06-18 14:59:35 +02:00
3a6ce1d38b feat: restructure contact data view 2025-06-18 14:59:12 +02:00
82fb73ae59 feat: Ensure using local font 2025-06-18 14:58:46 +02:00
4e71c8704f feat: Ensure using local photoswipe 2025-06-18 14:58:37 +02:00
0c1edf647b fix:display ANs 2025-06-18 13:00:58 +02:00
9b38898a8a fix: really ignore files 2025-06-18 13:00:47 +02:00
25348e45e0 feat: Add slider and styles 2025-06-18 12:59:21 +02:00
631c2360e6 feat: Don't track compiled css 2025-06-18 12:59:03 +02:00
6798cf3477 feat: Convert detail user to bulma 2025-06-18 12:55:40 +02:00
cc873d6029 feat: Show link to user profile for logged-in users 2025-06-18 12:55:25 +02:00
5d147a4fc9 feat: expand link until link symbol 2025-06-18 12:55:05 +02:00
640862d8ee feat: don't use card, look shitty 2025-06-18 12:54:21 +02:00
99bb53a7a3 feat: use new style in rss 2025-06-18 12:53:59 +02:00
4f05dc18b9 feat: use new maintainer label 2025-06-18 12:53:44 +02:00
2a2df3bf52 feat: Use scss for bulma 2025-06-18 07:25:09 +02:00
c2b15c2175 fix: Make actual form 2025-06-18 07:12:29 +02:00
edc27b899e fix: Only show rescue orgs and animal shelters when relevant 2025-06-17 23:35:11 +02:00
59d96e36a4 refactor: Make rescue Orgs clustered 2025-06-17 23:07:30 +02:00
2c976f926c refactor: Remove old get abolute URL 2025-06-17 22:46:50 +02:00
671c6ec6f5 feat: Add rescue orgs as geojson via API 2025-06-17 22:43:14 +02:00
ef9ac58c0f feat: indicate link 2025-06-17 22:17:16 +02:00
60e6fdf4e4 refactor: remove old partial for map 2025-06-17 22:15:23 +02:00
06e6455ba0 feat: adjust sizes 2025-06-17 19:49:00 +02:00
007eb3b5a9 fix: remove styleguide also from urls 2025-06-17 17:05:32 +02:00
f3333f2da4 refactor: remove header and base 2025-06-17 17:03:42 +02:00
96b40c5169 refactor: move to bulma 2025-06-17 17:03:26 +02:00
d81408b79c refactor: remove animal detail view 2025-06-17 17:02:42 +02:00
5ae5e90461 refactor: make detail user use bulma 2025-06-17 17:01:39 +02:00
2534ef3319 refactor: remove deprecated detail rescue orgs 2025-06-17 17:01:05 +02:00
0c2e774891 refactor: remove deprecated styleguides 2025-06-17 17:00:30 +02:00
895bb3c901 refactor: remove deprecated map 2025-06-17 16:59:03 +02:00
fca5445aa7 refactor: remove deprecated startpage 2025-06-17 16:58:38 +02:00
31d2b85b2f refactor: remove deprecated external site warning 2025-06-17 16:58:17 +02:00
a8b3214c49 refactor: remove depreacted rescue org view 2025-06-17 16:55:38 +02:00
ccdfd388c4 refactor: remove depreacted about 2025-06-17 16:53:49 +02:00
cad6acd125 refactor: move to bulma 2025-06-17 16:53:11 +02:00
8dc9c1b9e7 refactor: Remove deprecated 2025-06-17 16:52:24 +02:00
cd4de2528f feat: return 403 for unexpected operation 2025-06-17 16:51:29 +02:00
3ef4b98c1c feat: move activation forms 2025-06-17 16:51:12 +02:00
349917e887 feat: Only show buttons if you are allowed to use them 2025-06-17 16:45:54 +02:00
6c200ba076 feat: Add option to add animal on UI 2025-06-17 13:13:55 +02:00
f16aa845d2 feat: Re-add option to add animal 2025-06-17 13:00:17 +02:00
3bccb1e690 feat: Add option to delete animal 2025-06-17 12:35:35 +02:00
10ae697e33 fix: name 2025-06-17 07:20:58 +02:00
baf0d2db72 fix: Always use bulma for ANs 2025-06-17 07:20:22 +02:00
b30123a890 fix: displayed these 2025-06-17 07:17:41 +02:00
44c34d2daa refactor: Remove non-bulma adoption detail 2025-06-17 07:13:03 +02:00
e010fa413b refactor: Remove non-bulma rescue org 2025-06-17 07:07:24 +02:00
e79aca4efa refactor: Move to bulma 2025-06-17 07:05:40 +02:00
037f6529fd refactor: Remove non-bulma map 2025-06-17 07:05:32 +02:00
14752d9746 refactor: Remove non-bulma about 2025-06-17 07:02:24 +02:00
a8b2bd4e90 refactor: Rename 2025-06-17 07:01:42 +02:00
60ae971f14 refactor: Remove non-bulma rescue org view 2025-06-17 07:00:00 +02:00
1920d72821 refactor: Remove non-bulma add image for ANs 2025-06-17 06:58:16 +02:00
01aa8baadd refactor: Remove non-bulma add image 2025-06-17 06:55:57 +02:00
f2f526c9de refactor: Remove non-bulma add-adoption 2025-06-17 06:53:06 +02:00
4d490690e2 refactor: Remove index 2025-06-17 06:49:38 +02:00
d9c7aa8c49 refactor: Remove deprecated search 2025-06-17 06:45:22 +02:00
040299b90c feat: Activate report option 2025-06-17 05:59:48 +02:00
0bd321e5ec feat: Convert report detail to bulma 2025-06-17 05:57:43 +02:00
c038370602 feat: Convert report form to Bulma 2025-06-17 05:40:54 +02:00
c08f7fc792 feat: route to bulma after completion 2025-06-16 23:33:22 +02:00
4dd35c3866 refactor: Simplify Adoption Notice Forms 2025-06-16 23:32:09 +02:00
8bd041d7ea fix: Make sure further information field is displayed 2025-06-16 23:31:20 +02:00
d450ad42c0 fix: Make sure date is pre-filled
https://stackoverflow.com/questions/58294769/django-forms-dateinput-not-populating-from-instance
2025-06-16 23:17:27 +02:00
d34dcada09 feat: Add support for checkbox and textarea 2025-06-16 23:09:57 +02:00
d8448de419 feat: Add links to edit and remove link to animal detail page 2025-06-16 23:03:51 +02:00
35ef6676a2 feat: Make proper animal edit form 2025-06-16 23:03:22 +02:00
e132b1c9f6 feat: Add date support and simplify 2025-06-16 23:01:12 +02:00
5511d8275c feat: Add ideas for actions 2025-06-16 21:00:10 +02:00
accf877375 feat: Show image of AN only when there are some 2025-06-16 20:52:25 +02:00
9ac362fa58 feat: Increase view height to reliably show AN popup 2025-06-15 20:44:37 +02:00
9253fde2e5 feat: Move tags to content and only show gallery when there are pictures 2025-06-15 20:43:39 +02:00
975c962025 feat: Use rat image 2025-06-15 18:37:51 +02:00
ba72b4e59f feat: Get optional photo 2025-06-15 18:01:48 +02:00
89e001bd17 feat: truncate description 2025-06-15 17:42:02 +02:00
623ca8bc0a fix: make sure breakpoints are aligned with bulma mobile 2025-06-15 17:41:52 +02:00
0b483ce630 feat: Feed map with real data 2025-06-15 17:25:55 +02:00
16998b85d5 feat: Improve clustering look 2025-06-15 15:52:42 +02:00
b55952ac67 feat: fucking bug fixed
Needs a font that versatiles provides such as https://tiles.hyteck.de/assets/glyphs/index.json

Doesn't work with fallback
2025-06-11 17:56:54 +02:00
30967dac33 feat: use clustering (fucking bug not fixed) 2025-06-10 22:38:46 +02:00
3166faa7eb feat: Make search specific for cities and center around germany 2025-06-10 21:04:55 +02:00
9bba81be22 refactor(bulma): Ensure search suggestions are not overshadowed by autocomplete 2025-06-10 21:04:26 +02:00
18a2d16bf6 refactor(bulma): Add action dropdown, remove card layout 2025-06-10 20:35:24 +02:00
9265cdaea9 refactor(bulma): move registration/login to end of navbar 2025-05-25 17:16:48 +02:00
fcb9b60656 refactor(bulma): fix close option 2025-05-25 15:27:01 +02:00
599702f50a refactor(bulma): add announcement template 2025-05-25 15:19:43 +02:00
c8453db69d refactor(bulma): add rescue orgs to footer 2025-05-25 11:37:16 +02:00
6ad93abe3b refactor(bulma): convert check rescue org 2025-05-25 11:36:47 +02:00
3c60782ae7 refactor(bulma): move contact details to partial 2025-05-25 11:04:29 +02:00
736f645bf0 refactor(bulma): convert external site warning 2025-05-25 00:20:53 +02:00
b0887ab731 refactor(bulma): convert instance healthcheck 2025-05-25 00:15:55 +02:00
ada194122d refactor(bulma): convert all account related templates 2025-05-25 00:10:47 +02:00
b3d1ec142b feat: Make all forms to be rendered bycustom renderer 2025-05-25 00:10:12 +02:00
e7a8a163f1 feat: Add helptexts to custom form renderer 2025-05-24 23:40:58 +02:00
c3ef54a267 fix: Re-add location 2025-05-24 19:45:21 +02:00
da3b43a713 feat: Make sure patch doesn't overwrite 2025-05-24 19:41:53 +02:00
8cfddd7882 refactor: formatting 2025-05-24 17:56:36 +02:00
80eafbb014 fix: get id correctly 2025-05-24 17:55:28 +02:00
cc2a659767 feat: only fetch the location if none is given 2025-05-24 13:06:23 +02:00
cacfeff3fe feat: support all types of geometry types 2025-05-24 13:05:32 +02:00
9379728b71 feat: comments 2025-05-24 12:09:12 +02:00
d30d15c0d4 feat: exclude geojson from git 2025-05-24 12:05:30 +02:00
5343f53661 feat: skip rescues that already exist 2025-05-23 20:03:28 +02:00
3126b2b962 feat: return 404 when no organizations are found 2025-05-23 20:01:46 +02:00
43c671018b refactor: remove redundant serializer 2025-05-23 19:51:28 +02:00
7a37377a09 feat: make location string optional but enforce either location or location string 2025-05-23 19:49:52 +02:00
19d9dea8b1 feat: add location deduplication 2025-05-23 18:13:23 +02:00
c50e0b18b5 refactor:naming 2025-05-23 16:50:45 +02:00
4c07c0feb2 feat: Add location as object when uploading shelters 2025-05-22 22:01:58 +02:00
cf15b60bef refactor(bulma): Add support for rescue organizations 2025-05-22 19:46:24 +02:00
328f64aa51 refactor(bulma): various improvements on AN display in list 2025-05-22 17:17:39 +02:00
fdf4e79a69 refactor(bulma): use main-thumbnail view for AN photos 2025-05-18 14:27:00 +02:00
bbc8732112 refactor(bulma): use generic gallery to avoid having multiple same ids 2025-05-18 14:23:09 +02:00
17dbe85219 refactor(bulma): Fix urls of images 2025-05-18 14:08:58 +02:00
3dc011a22c refactor(bulma): Move animals to separate column 2025-05-18 14:07:31 +02:00
f5b89456ab refactor(bulma): make thumbnail row use same width images and crop when necessary 2025-05-18 13:16:47 +02:00
2e4f63b250 refactor(bulma): Work on images 2025-05-18 13:05:37 +02:00
3b261ff240 feat: Add widget tweaks dependency for bulma forms 2025-05-14 19:25:40 +02:00
f06b00fb9d feat: Add image upload form in bulma 2025-05-14 19:25:18 +02:00
88987a973e feat: Manually craft the add adoption form to work with bulma
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-05-11 16:21:06 +02:00
93ffbe09af feat: Link to new layout 2025-05-11 13:48:25 +02:00
e11848ea72 feat: Add js to close notifications 2025-05-11 13:43:26 +02:00
8bc9d12bfa feat: Add basic bulma form to add adoptions 2025-05-11 13:43:10 +02:00
1dbfdccb89 feat: Add rules to TOS page 2025-05-11 13:42:51 +02:00
f085f5dcf5 feat: Remove leftover span 2025-05-11 09:11:07 +02:00
33579e8446 feat: Allow searching for rescue orgs 2025-05-11 08:55:29 +02:00
a852da365f feat: Remove about us from main menu 2025-05-10 14:13:00 +02:00
b53095ae17 feat: Add onpagers for imprint, privacy and terms of service 2025-05-10 13:35:53 +02:00
3d7780e0ba feat: style 2025-05-10 13:15:37 +02:00
478636bd98 feat: add bulma about 2025-05-10 13:12:58 +02:00
d9ebee1e07 feat: add bulma comment section 2025-05-10 12:02:11 +02:00
23e154bce6 feat: further restructure search 2025-05-10 09:26:49 +02:00
5624f59258 feat: Add image for animal selter 2025-05-10 08:56:00 +02:00
56df942dd0 feat: map 2025-05-10 08:55:23 +02:00
2dcb5fbf88 feat: Structure list a bit more 2025-05-09 21:54:06 +02:00
7a84b470f9 feat: Structure list a bit more 2025-05-09 21:53:59 +02:00
76232b7a0f feat: make sure maps extends over all available height, round corners 2025-05-09 21:16:54 +02:00
349af16075 feat: pad content 2025-05-09 21:16:17 +02:00
8641bead80 feat: make display of location nicer 2025-05-09 21:03:21 +02:00
eb930b71d6 fix: remove debug statements 2025-05-09 21:03:00 +02:00
ae4ba06abf fix: use normal partial 2025-05-09 21:01:33 +02:00
a2e237a81f feat: Make card heading more noticeable 2025-05-09 20:58:23 +02:00
f90c8c7e8c feat: Add name to header 2025-05-09 20:57:58 +02:00
c316c74aff feat: fix map title 2025-05-09 20:41:04 +02:00
93dd0ae4f6 feat: Add bulma map 2025-05-09 20:40:54 +02:00
f79bb355cf feat: Link to bulma url 2025-05-09 20:20:04 +02:00
45a534a042 feat: Add index page in bulma 2025-05-09 20:12:12 +02:00
2106a3423f feat: Remove compass and add fullscreen option 2025-05-09 18:34:09 +02:00
d3f7274e92 feat: Restructure search and add blocks 2025-05-09 18:27:52 +02:00
5f576896b7 feat: Add custom form rendering to support bulma 2025-05-09 18:15:17 +02:00
4a3cbfb8b0 feat: wrap blocks 2025-05-09 17:15:16 +02:00
3e93fe1a7a feat: Remove redundant heading "Pictures" 2025-05-09 17:14:55 +02:00
965e055ef1 feat: Add bulma search 2025-05-09 17:14:34 +02:00
13a0da6e46 feat: Move sex overview to partial 2025-05-09 17:13:31 +02:00
1bb05dbf1c feat: Add tags for sex 2025-05-01 18:41:35 +02:00
4c9c1e13a5 feat: further redesign 2025-05-01 18:15:25 +02:00
99cde15966 feat: Add filter for important locations 2025-04-28 22:46:18 +02:00
f2edc23e75 feat: Make cities visible at lower zoomlevels 2025-04-27 23:34:13 +02:00
8aab4a13ae feat: Exchange pin with circle
Allows to still see a cities label
2025-04-27 23:33:41 +02:00
226102ccaf feat: Display proper 404 when location is not found 2025-04-27 15:05:22 +02:00
3d088c55d7 fix: Adjust to use new versatiles structure
See https://docs.versatiles.org/compendium/specification_frontend.html
2025-04-27 14:31:53 +02:00
bb14a346cb feat: Add important locations to search around 2025-04-27 14:06:17 +02:00
f387930dee feat: Allow longer placids with no restrictions on int 2025-04-27 00:21:58 +02:00
fe63e3b25c feat: Link organization Website directly 2025-04-26 23:03:02 +02:00
23adeb06e6 feat: Allow getting and setting photos with ANs in API 2025-04-25 19:23:45 +02:00
c1bd458c80 feat: Allow adding locations and organizations to ANs in API 2025-04-25 19:18:06 +02:00
2a1d4178d7 feat: Allow creating locations via API 2025-04-24 22:35:38 +02:00
f9a37b299d feat: Extend location model to allow specifying address 2025-04-24 19:43:48 +02:00
9950e87501 feat: Make use of footer items 2025-04-24 18:50:48 +02:00
eff1ba6513 feat: Make use of footer items 2025-04-23 21:14:16 +02:00
bb085aa9a8 feat: Add basic bulma version of comment form 2025-04-23 21:12:22 +02:00
b0dc0f9d78 feat: Make photos to be in card 2025-04-23 20:56:19 +02:00
d1a51b019c feat: Make columns to stack vertically on mobile 2025-04-23 20:53:26 +02:00
b7fade55fb feat: Make header of description card header title 2025-04-23 20:20:24 +02:00
79461518a3 feat: add bulma animal cards 2025-04-10 15:12:39 +02:00
8059d5d23f feat: add photoswipe to adoption notice detail page 2025-04-10 15:12:21 +02:00
3098eacfb4 feat: only exclude the static folder in root from VSC 2025-04-10 15:11:26 +02:00
f3d1e1c203 feat: make headings strong 2025-04-07 21:32:46 +02:00
e6a985ddfa feat: Add initial bulma version of adoption notice detail page 2025-04-07 21:30:14 +02:00
388cc327be feat: Add cards 2025-04-06 10:48:03 +02:00
13adc695f6 feat: Style headings and add change langueg form 2025-04-06 10:25:37 +02:00
f2c7943247 fix: Add missing div 2025-04-06 10:03:52 +02:00
112fd52864 feat: Add footer 2025-04-06 10:03:35 +02:00
8279385966 feat: Rename bulma styleguide and add navigation 2025-04-06 09:05:50 +02:00
1a9692949f feat: add alt text to logo 2025-04-06 09:05:00 +02:00
e7af49b309 feat: add optional address data to location 2025-04-06 08:38:34 +02:00
b822914db3 feat: Allow updating existing rescue organizations 2025-03-21 16:11:58 +01:00
9ad33efe08 feat: Allow filtering for external object id and source 2025-03-21 15:53:58 +01:00
bd8f9fc1b7 feat: Ensure External object identifier and external source identifier are unique together 2025-03-21 15:26:01 +01:00
4a2c18be4d feat: replace upload script with version of g 2025-03-21 12:56:34 +01:00
479aba0195 fix: Adjust maplibre paths for versatiles 0.15.X
See https://github.com/versatiles-org/versatiles-docker/issues/16
2025-03-21 00:51:58 +01:00
1299fcac84 refactor: Rename 2025-03-21 00:29:23 +01:00
884a07f87b feat: Use choices and fix bug where default was not honored 2025-03-21 00:28:15 +01:00
6557e9f9eb feat: Auto-add location to rescue org 2025-03-20 23:26:16 +01:00
602cef1302 refactor: use bulma.min.css 2025-03-20 19:20:40 +01:00
b400db603a refactor: Remove js part -> model 2025-03-20 19:12:32 +01:00
0397311f6e feat: Add bulma and base for bulma styleguide 2025-03-20 19:12:32 +01:00
abce89c829 feat: Add bulma and base for bulma styleguide 2025-03-20 18:58:40 +01:00
bbad63a460 fix: Adjust site language based on selected language 2025-03-20 18:34:25 +01:00
d940630086 refactor: formatting 2025-03-17 21:08:41 +01:00
37ecf28f2f feat: add link to original content
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-03-10 22:12:35 +01:00
12d5a976cc feat: add link to original content 2025-03-10 22:12:28 +01:00
9086e2e75b fix: Show name of reported content 2025-03-10 22:04:14 +01:00
3607eb0e4e feat: Add message when no comment is added to report 2025-03-10 21:09:06 +01:00
3daf83d725 tests: Fix testing for edit buttons 2025-03-09 18:05:29 +01:00
5ad0cb74cc feat: Only show edit buttons when mod 2025-03-09 18:05:15 +01:00
9ae64e8cb1 feat: Auto add now the date oflast checked
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-03-09 17:46:13 +01:00
1b5a0c71e0 feat: Add rescue organization check 2025-03-09 09:46:56 +01:00
4d4f11c479 feat: Make string translatable 2025-03-09 09:16:37 +01:00
835c89d1d4 test: Add test for details of comment reports 2025-02-05 22:32:18 +01:00
46bf07dd8d test: Add test for reporting comments anon and logged in 2025-02-05 22:11:59 +01:00
f557672586 test: Add test for reporting rules anon 2025-02-05 20:17:36 +01:00
4e27e1be7f test: Add test for about page and for reporting rules logged in 2025-01-27 18:51:14 +01:00
6d390ad21e test: Add test for (un)subscribes of searches 2025-01-26 20:19:18 +01:00
2f2543160e test: Add test for unauthenticated (un) subscribes of searches 2025-01-26 19:01:39 +01:00
64a9db133e feat: default to photon for geocoding 2025-01-26 18:16:39 +01:00
712c3d32f3 feat: Add styleguide setup 2025-01-26 18:16:39 +01:00
8998bbdf6d Merge pull request #9 from moan0s/shelter-fixes
Add script to upload data of animal shelters
2025-01-26 18:04:48 +01:00
ff31caa139 refactor: Move script to dedicated folder 2025-01-26 18:01:38 +01:00
ad06829c31 feat: Add debug information 2025-01-26 18:00:23 +01:00
03a48da355 feat: Make instance and secrets CLI argument 2025-01-26 17:59:48 +01:00
885bed888d feat: Raise connection error upon unexpected error
This e.g. makes sure the API is not bombarded with unauthorized calls if the token is wrong
2025-01-26 17:07:50 +01:00
0051cb07c9 refactor: formatting 2025-01-26 17:06:41 +01:00
8858cff9cf fix: Construct necessary location string
I'd be better to directly create a location here but I for now want to make as little modifications as possible
2025-01-26 17:05:51 +01:00
70e2af6172 fix: fix key 2025-01-26 17:04:56 +01:00
461abd2e46 fix: don't try to save owner 2025-01-26 17:04:17 +01:00
Salil
d7269106db Added - animal_shelter Get Data
Import all German animal shelters
2025-01-24 12:22:28 +05:30
77fb99a527 Merge pull request #6 from Deadpool2000/patch-1
Create robots.txt
2025-01-22 20:28:39 +01:00
38a56daa24 feat: Add sitemap 2025-01-22 11:01:31 +01:00
Salil
ac0749797f Create robots.txt
- robots.txt file added in src/fellchensammlung/static/
2025-01-22 10:03:30 +05:30
f193f7d7ca test: Add tests for AN edit 2025-01-19 23:11:07 +01:00
43657e0862 test: Add tests for comment 2025-01-19 22:07:21 +01:00
68ad366f74 test: Add tests for comment 2025-01-19 09:00:53 +01:00
350d2c5da9 feat: Return HTTP response 2025-01-19 09:00:26 +01:00
462bb8f485 refactor: formatting 2025-01-18 21:43:37 +01:00
ea4d15b99a tests: Add test for index view 2025-01-18 21:43:00 +01:00
de30dfcb8b tests: Add test for unsubscribe 2025-01-18 21:39:45 +01:00
36a979954c feat: make sure owner also gets notified, add test 2025-01-18 18:53:55 +01:00
71ef17dc97 feat: Move pytest, coverage and model bakery to develop dependencies 2025-01-18 18:30:02 +01:00
206cd282e6 feat: Add coverage report instructions 2025-01-18 18:29:04 +01:00
e399346c3e feat: Add tests for subscription functionality 2025-01-18 16:17:22 +01:00
929c6dfff0 feat: Add registration button 2025-01-18 15:47:07 +01:00
841b57fea2 feat: Make sure subscribe leads to login flow 2025-01-18 15:25:27 +01:00
9e5446ff1d feat: Test adding a adoption as user 2025-01-18 15:22:08 +01:00
3b79809b8c docs: various 2025-01-18 09:07:33 +01:00
53e6db3655 refactor: Remove print 2025-01-14 07:31:00 +01:00
424f91e919 feat: Add a timestamp for when a notification is read 2025-01-14 07:30:36 +01:00
84ce5f54b2 refactor: Remove unnecessary print 2025-01-14 07:27:03 +01:00
202 changed files with 12629 additions and 5424 deletions

4
.coveragerc Normal file
View File

@@ -0,0 +1,4 @@
[run]
omit =
*/migrations/*
*/tests/*

9
.gitignore vendored
View File

@@ -3,10 +3,16 @@
# Database
notfellchen
# Geojson from imports
*.geojson
# Media storage
static
/static
media
# Compiled CSS
/src/fellchensammlung/static/fellchensammlung/css/main.css
/src/fellchensammlung/static/fellchensammlung/css/main.css.map
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -161,3 +167,4 @@ dmypy.json
# Cython debug symbols
cython_debug/
/node_modules/

View File

@@ -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

View File

@@ -2,7 +2,7 @@
[notfellchen.org](https://notfellchen.org) ist eine Sammelstelle für Tier-Vermittlungen. Die Idee entstand, da in der
deutschsprachigen Rattencommunity ein wilder Mix aus Websites, Foren und Facebookgruppen besteht die Ratten vermitteln.
Diese Website soll die bestehende Communities NICHT ersetzten, jedoch ermöglichen, dass Menschen die Ratten aufnehmen
Diese Website soll die bestehende Communitys NICHT ersetzten, jedoch ermöglichen, dass Menschen die Ratten aufnehmen
wollen Informationen einfach finden und nicht bereits in jeder Gruppe sein müssen.
Wir nehmen Angebote auf die
@@ -57,10 +57,50 @@ Therefore, a solution is used where a number of predefined texts per site are su
# Developer Notes
## Getting started
### Clone the project
```
git clone https://codeberg.org/moanos/notfellchen.git
```
### Install dependencies
```
pip install -e '.[all]'
```
### Create the database
```
nf migrate
```
Because of a wired bug the initial migrations must run two times as the first time the permissions
for `create_active_adoption_notice` are created but can not yet be accessed and on the second time this permission will
be added to groups.
### Start the server
```
nf runserver
```
### Build the docs
```
sphinx-autobuild ./docs ./docs/_build/html
```
## Styling
Bulma is used for styling, including related SCSS. All styles should eventually be migrated to SCSS.
Use `npm run build-bulma` to generate the css file from SCSS.
You can use `npm start` during development so that the file is re-generated upon change.
## Docker
Build latest image
@@ -77,6 +117,27 @@ 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
@@ -124,17 +185,17 @@ Start beat
# Contributing
This project is currently solely developed by me, moanos. I'd like that to change and will be very happy for contributions
This project is currently mainly developed by me, moanos. I'd like that to change and will be very happy for contributions
and shared responsibilities. Some ideas where you can look for contributing first
* CSS structure: It's a hot mess right now, and I'm happy it somehow works. As you might see, there is much room for improvement. Refactoring this and streamlining the look across the app would be amazing.
* UI improvements: Since a major redesign I'm much happier but the UI could use many, many little tweaks
* Docker: If you know how to build a docker container that is a) smaller or b) utilizes staged builds this would be amazing. Any improvement welcome
* Testing: Writing tests is always welcome, and it's likely you discover a few bugs
I'm also very happy for all other contributions. Before you do large refactoring efforts or features, best write a short
issue for it before you spend a lot of work.
Send PRs either to [codeberg](https://codeberg.org/moanos/notfellchen) (preferred) or [Github](https://github.com/moan0s/notfellchen).
CI (currently only for dcumentation) runs via [git.hyteck.de](https://git.hyteck.de), you can also ask moanos for an account there.
Send PRs either to [codeberg](https://codeberg.org/moanos/notfellchen) (preferred) or [GitHub](https://github.com/moan0s/notfellchen).
CI (currently only for documentation) runs via [git.hyteck.de](https://git.hyteck.de), you can also ask moanos for an account there.
Also welcome are new issues with suggestions or bugs and additions to the documentation.

View File

@@ -14,7 +14,8 @@ Via browser
-----------
When a user is logged in, they can easily access the API in their browser, authenticated by their session.
The API endpoint can be found at http://notfellchen.org/api/adoption_notices
For example: You can check all current adoption notices here: https://notfellchen.org/api/adoption_notice
Via token
---------
@@ -28,19 +29,25 @@ An application can then send this token in the request header for authorization.
.. warning::
Usage or creation of content still has to follow the terms of Notfellchen.org
Usage or creation of content still has to follow the terms of notfellchen.org.
Copyright of content is often held by rescue organizations, so you are not allowed to simply mirror content.
Talk to the Notfellchen-Team if you want develop such things.
Talk to the notfellchen team if you want develop such things.
Endpoints
---------
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 \
curl --request GET \
--url https://notfellchen.org/api/adoption_notice \
--header 'Authorization: {{token}}'
@@ -48,7 +55,8 @@ Create Adoption Notice
++++++++++++++++++++++
.. code-block::
curl --request POST \
curl --request POST \
--url https://notfellchen.org/api/adoption_notice \
--header 'Authorization: {{token}}' \
--header 'content-type: multipart/form-data' \
@@ -63,6 +71,7 @@ Add Animal to Adoption Notice
+++++++++++++++++++++++++++++
.. code-block::
curl --request POST \
--url https://notfellchen.org/api/animals/ \
--header 'Authorization: {{token}}' \
@@ -78,6 +87,7 @@ Add picture to Animal or Adoption Notice
++++++++++++++++++++++++++++++++++++++++
.. code-block::
curl -X POST https://notfellchen.org/api/images/ \
-H "Authorization: Token {{token}}" \
-F "image=@256-256-crop.jpg" \
@@ -91,6 +101,7 @@ Species
Getting available species is mainly important when creating animals
.. code-block::
curl --request GET \
--url https://notfellchen.org/api/species \
--header 'Authorization: {{token}}'

View File

@@ -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

View File

@@ -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!
^^^^^^^^^^^^^

View File

@@ -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 🥳

View File

@@ -18,7 +18,7 @@ media=./media
static=./static
[mail]
console-only=true
console_only=true
[logging]
app_log_level=INFO

497
package-lock.json generated Normal file
View 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
View 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"
}
}

View File

@@ -6,15 +6,15 @@ build-backend = "setuptools.build_meta"
[project]
name = "notfellchen"
description = "A tool to help."
description = "A website to help animals to find a loving home. It features organized input of adoption notices and related animals including automated lifecycle, location-based search, roles, and support for easy checking of rescue organizations."
authors = [
{name = "moanos", email = "julian-samuel@gebuehr.net"},
{ name = "moanos", email = "julian-samuel@gebuehr.net" },
]
maintainers = [
{name = "moanos", email = "julian-samuel@gebuehr.net"},
{ name = "moanos", email = "julian-samuel@gebuehr.net" },
]
keywords = ["animal", "adoption", "django", "rescue", ]
license = {text = "AGPL-3.0-or-later"}
keywords = ["animal", "adoption", "django", "rescue", "rats" ]
license = { text = "AGPL-3.0-or-later" }
classifiers = [
"Environment :: Web",
"License :: OSI Approved :: GNU Affero General Public License v3",
@@ -24,14 +24,10 @@ classifiers = [
]
dependencies = [
"Django",
"coverage",
"codecov",
"sphinx",
"sphinx-rtd-theme",
"gunicorn",
"fontawesomefree",
"whitenoise",
"model_bakery",
"markdown",
"Pillow",
"django-registration",
@@ -40,14 +36,23 @@ dependencies = [
"crispy-bootstrap4",
"djangorestframework",
"celery[redis]",
"drf-spectacular[sidecar]"
"drf-spectacular[sidecar]",
"django-widget-tweaks",
"django-super-deduper"
]
dynamic = ["version", "readme"]
[project.optional-dependencies]
develop = [
"pytest",
"pytest",
"coverage",
"model_bakery",
]
docs = [
"sphinx",
"sphinx-rtd-theme",
"sphinx-autobuild"
]
[project.urls]
@@ -62,6 +67,6 @@ nf = 'notfellchen.main:main'
[tool.setuptools.dynamic]
version = {attr = "notfellchen.__version__"}
readme = {file = "README.md"}
version = { attr = "notfellchen.__version__" }
readme = { file = "README.md" }

View File

@@ -0,0 +1,268 @@
import argparse
import json
import logging
import os
from types import SimpleNamespace
import requests
# TODO: consider using OSMPythonTools instead of requests or overpass library
from osmtogeojson import osmtogeojson
from tqdm import tqdm
DEFAULT_OSM_DATA_FILE = "export.geojson"
# Search area must be the official name, e.g. "Germany" is not a valid area name in Overpass API
# Consider instead finding & using the code within the query itself, e.g. "ISO3166-1"="DE"
DEFAULT_OVERPASS_SEARCH_AREA = "Deutschland"
def parse_args():
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="Download animal shelter data from the Overpass API to the Notfellchen API.")
parser.add_argument("--api-token", type=str, help="API token for authentication.")
parser.add_argument("--area", type=str, help="Area to search for animal shelters (default: Deutschland).")
parser.add_argument("--instance", type=str, help="API instance URL.")
parser.add_argument("--data-file", type=str, help="Path to the GeoJSON file containing (only) animal shelters.")
parser.add_argument("--use-cached", action='store_true', help="Use the stored GeoJSON file")
return parser.parse_args()
def get_config():
"""Get configuration from environment variables or command-line arguments."""
args = parse_args()
api_token = args.api_token or os.getenv("NOTFELLCHEN_API_TOKEN")
# TODO: document new environment variable NOTFELLCHEN_AREA
area = args.area or os.getenv("NOTFELLCHEN_AREA", DEFAULT_OVERPASS_SEARCH_AREA)
instance = args.instance or os.getenv("NOTFELLCHEN_INSTANCE")
data_file = args.data_file or os.getenv("NOTFELLCHEN_DATA_FILE", DEFAULT_OSM_DATA_FILE)
use_cached = args.use_cached or os.getenv("NOTFELLCHEN_USE_CACHED", False)
if not api_token or not instance:
raise ValueError("API token and instance URL must be provided via environment variables or CLI arguments.")
return api_token, area, instance, data_file, use_cached
def get_or_none(data, key):
if key in data["properties"].keys():
return data["properties"][key]
else:
return None
def get_or_empty(data, key):
if key in data["properties"].keys():
return data["properties"][key]
else:
return ""
def choose(keys, data, replace=False):
for key in keys:
if key in data.keys():
if replace:
return data[key].replace(" ", "").replace("-", "").replace("(", "").replace(")", "")
else:
return data[key]
return None
def add(value, platform):
if value != "":
if value.find(platform) == -1:
return f"https://www.{platform}.com/{value}"
else:
return value
else:
return None
def https(value):
if value is not None and value != "":
value = value.replace("http://", "")
if value.find("https") == -1:
return f"https://{value}"
else:
return value
else:
return None
def calc_coordinate_center(coordinates):
"""
Calculates the center as the arithmetic mean of the list of coordinates.
Not perfect because earth is a sphere (citation needed) but good enough.
"""
if not coordinates:
return None, None
lon_sum = 0.0
lat_sum = 0.0
count = 0
for lon, lat in coordinates:
lon_sum += lon
lat_sum += lat
count += 1
return lon_sum / count, lat_sum / count
def get_center_coordinates(geometry):
"""
Given a GeoJSON geometry dict, return (longitude, latitude)
If a shape, calculate the center, else reurn the point
"""
geom_type = geometry["type"]
coordinates = geometry["coordinates"]
if geom_type == "Point":
return coordinates[0], coordinates[1]
elif geom_type == "LineString":
return calc_coordinate_center(coordinates)
elif geom_type == "Polygon":
outer_ring = coordinates[0]
return calc_coordinate_center(outer_ring)
else:
raise ValueError(f"Unsupported geometry type: {geom_type}")
# TODO: take note of new get_overpass_result function which does the bulk of the new overpass query work
def get_overpass_result(area, data_file):
"""Build the Overpass query for fetching animal shelters in the specified area."""
overpass_endpoint = "https://overpass-api.de/api/interpreter"
overpass_query = f"""
[out:json][timeout:25];
area[name="{area}"]->.searchArea;
nwr["amenity"="animal_shelter"](area.searchArea);
out body;
>;
out skel qt;
"""
r = requests.get(overpass_endpoint, params={'data': overpass_query})
if r.status_code == 200:
rjson = r.json()
result = osmtogeojson.process_osm_json(rjson)
with open(data_file, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False)
return result
def add_if_available(base_data, keys, result):
# Loads the data into the org if available
for key in keys:
if getattr(base_data, key) is not None:
result[key] = getattr(base_data, key)
return result
def create_location(tierheim, instance, headers):
location_data = {
"place_id": tierheim["id"],
"longitude": get_center_coordinates(tierheim["geometry"])[0],
"latitude": get_center_coordinates(tierheim["geometry"])[1],
"name": tierheim["properties"]["name"],
"city": tierheim["properties"]["addr:city"],
"housenumber": get_or_empty(tierheim, "addr:housenumber"),
"postcode": get_or_empty(tierheim, "addr:postcode"),
"street": get_or_empty(tierheim, "addr:street"),
"countrycode": get_or_empty(tierheim, "addr:country"),
}
location_result = requests.post(f"{instance}/api/locations/", json=location_data, headers=headers)
if location_result.status_code != 201:
try:
print(
f"Location for {tierheim["properties"]["name"]}:{location_result.status_code} {location_result.json()} not created")
except requests.exceptions.JSONDecodeError:
print(f"Location for {tierheim["properties"]["name"]} could not be created")
exit()
return location_result.json()
def main():
api_token, area, instance, data_file, use_cached = get_config()
if not use_cached:
# Query shelters
overpass_result = get_overpass_result(area, data_file)
if overpass_result is None:
print("Error: get_overpass_result returned None")
return
else:
with open(data_file, 'r', encoding='utf-8') as f:
overpass_result = json.load(f)
# Set headers and endpoint
endpoint = f"{instance}/api/organizations/"
h = {'Authorization': f'Token {api_token}', "content-type": "application/json"}
tierheime = overpass_result["features"]
for idx, tierheim in enumerate(tqdm(tierheime)):
# Check if data is low quality
if "name" not in tierheim["properties"].keys() or "addr:city" not in tierheim["properties"].keys():
continue
# Load TH data in for easier accessing
th_data = SimpleNamespace(
name=tierheim["properties"]["name"],
email=choose(("contact:email", "email"), tierheim["properties"]),
phone_number=choose(("contact:phone", "phone"), tierheim["properties"], replace=True),
fediverse_profile=get_or_none(tierheim, "contact:mastodon"),
facebook=https(add(get_or_empty(tierheim, "contact:facebook"), "facebook")),
instagram=https(add(get_or_empty(tierheim, "contact:instagram"), "instagram")),
website=https(choose(("contact:website", "website"), tierheim["properties"])),
description=get_or_none(tierheim, "opening_hours"),
external_object_identifier=tierheim["id"],
EXTERNAL_SOURCE_IDENTIFIER="OSM",
)
# Define here for later
optional_data = ["email", "phone_number", "website", "description", "fediverse_profile", "facebook",
"instagram"]
# Check if rescue organization exits
search_data = {"external_source_identifier": "OSM",
"external_object_identifier": f"{tierheim["id"]}"}
search_result = requests.get(f"{instance}/api/organizations", params=search_data, headers=h)
if search_result.status_code == 200:
org_id = search_result.json()[0]["id"]
logging.debug(f"{th_data.name} already exists as ID {org_id}.")
org_patch_data = {"id": org_id,
"name": th_data.name}
if search_result.json()[0]["location"] is None:
location = create_location(tierheim, instance, h)
org_patch_data["location"] = location["id"]
org_patch_data = add_if_available(th_data, optional_data, org_patch_data)
result = requests.patch(endpoint, json=org_patch_data, headers=h)
if result.status_code != 200:
logging.warning(f"Updating {tierheim['properties']['name']} failed:{result.status_code} {result.json()}")
continue
else:
location = create_location(tierheim, instance, h)
org_data = {"name": tierheim["properties"]["name"],
"external_object_identifier": f"{tierheim["id"]}",
"external_source_identifier": "OSM",
"location": location["id"]
}
org_data = add_if_available(th_data, optional_data, org_data)
result = requests.post(endpoint, json=org_data, headers=h)
if result.status_code != 201:
print(f"{idx} {tierheim["properties"]["name"]}:{result.status_code} {result.json()}")
if __name__ == "__main__":
main()

View File

@@ -1,16 +1,17 @@
import csv
from django.contrib import admin
from django.contrib.admin import EmptyFieldListFilter
from django.http import HttpResponse
from django.utils.html import format_html
from django.urls import reverse
from django.utils.http import urlencode
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
SpeciesSpecificURL
SpeciesSpecificURL, ImportantLocation, SocialMediaPost
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, BaseNotification
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, Notification
from django.utils.translation import gettext_lazy as _
@@ -94,14 +95,16 @@ class ReportAdoptionNoticeAdmin(admin.ModelAdmin):
reported_content_link.short_description = "Reported Content"
class SpeciesSpecificURLInline(admin.StackedInline):
model = SpeciesSpecificURL
@admin.register(RescueOrganization)
class RescueOrganizationAdmin(admin.ModelAdmin):
search_fields = ("name","description", "internal_comment", "location_string")
search_fields = ("name", "description", "internal_comment", "location_string", "location__city")
list_display = ("name", "trusted", "allows_using_materials", "website")
list_filter = ("allows_using_materials", "trusted",)
list_filter = ("allows_using_materials", "trusted", ("external_source_identifier", EmptyFieldListFilter))
inlines = [
SpeciesSpecificURLInline,
@@ -118,18 +121,53 @@ class CommentAdmin(admin.ModelAdmin):
list_filter = ("user",)
@admin.register(BaseNotification)
@admin.register(Notification)
class BaseNotificationAdmin(admin.ModelAdmin):
list_filter = ("user", "read")
list_filter = ("user_to_notify", "read")
@admin.register(SearchSubscription)
class SearchSubscriptionAdmin(admin.ModelAdmin):
list_filter = ("owner",)
class ImportantLocationInline(admin.StackedInline):
model = ImportantLocation
class IsImportantListFilter(admin.SimpleListFilter):
# See https://docs.djangoproject.com/en/5.1/ref/contrib/admin/filters/#modeladmin-list-filters
title = _('Is Important Location?')
parameter_name = 'important'
def lookups(self, request, model_admin):
return (
('is_important', _('Important Location')),
('is_normal', _('Normal Location')),
)
def queryset(self, request, queryset):
if self.value() == 'is_important':
return queryset.filter(importantlocation__isnull=False)
else:
return queryset.filter(importantlocation__isnull=True)
@admin.register(Location)
class LocationAdmin(admin.ModelAdmin):
search_fields = ("name__icontains", "city__icontains")
list_filter = [IsImportantListFilter]
inlines = [
ImportantLocationInline,
]
@admin.register(SocialMediaPost)
class SocialMediaPostAdmin(admin.ModelAdmin):
list_filter = ("platform",)
admin.site.register(Animal)
admin.site.register(Species)
admin.site.register(Location)
admin.site.register(Rule)
admin.site.register(Image)
admin.site.register(ModerationAction)

View 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)

View File

@@ -1,12 +1,119 @@
from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image
from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image, Location
from rest_framework import serializers
import math
class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
location = serializers.PrimaryKeyRelatedField(
queryset=Location.objects.all(),
required=False,
allow_null=True
)
location_details = serializers.StringRelatedField(source='location', read_only=True)
organization = serializers.PrimaryKeyRelatedField(
queryset=RescueOrganization.objects.all(),
required=False,
allow_null=True
)
organization = serializers.PrimaryKeyRelatedField(
queryset=RescueOrganization.objects.all(),
required=False,
allow_null=True
)
photos = serializers.PrimaryKeyRelatedField(
queryset=Image.objects.all(),
many=True,
required=False
)
class Meta:
model = AdoptionNotice
fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information",
"group_only"]
"group_only", "location", "location_details", "organization", "photos"]
class AdoptionNoticeGeoJSONSerializer(serializers.ModelSerializer):
species = serializers.SerializerMethodField()
title = serializers.CharField(source='name')
url = serializers.SerializerMethodField()
location_hr = serializers.SerializerMethodField()
coordinates = serializers.SerializerMethodField()
image_url = serializers.SerializerMethodField()
image_alt = serializers.SerializerMethodField()
class Meta:
model = AdoptionNotice
fields = ('id', 'species', 'title', 'description', 'url', 'location_hr', 'coordinates', 'image_url',
'image_alt')
def get_species(self, obj):
return "rat"
def get_url(self, obj):
return obj.get_absolute_url()
def get_image_url(self, obj):
photo = obj.get_photo()
if photo is not None:
return obj.get_photo().image.url
return None
def get_image_alt(self, obj):
photo = obj.get_photo()
if photo is not None:
return obj.get_photo().alt_text
return None
def get_coordinates(self, obj):
"""
Coordinates are randomly moved around real location, roughly in a circle. The object id is used as angle so that
points are always displayed at the same location (as if they were a seed for a random function).
It's not exactly a circle, because the earth is round.
"""
if obj.location:
longitude_addition = math.sin(obj.id) / 2000
latitude_addition = math.cos(obj.id) / 2000
return [obj.location.longitude + longitude_addition, obj.location.latitude + latitude_addition]
return None
def get_location_hr(self, obj):
if obj.location:
return f"{obj.location}"
return None
class RescueOrgeGeoJSONSerializer(serializers.ModelSerializer):
name = serializers.CharField()
url = serializers.SerializerMethodField()
location_hr = serializers.SerializerMethodField()
coordinates = serializers.SerializerMethodField()
class Meta:
model = AdoptionNotice
fields = ('id', 'name', 'description', 'url', 'location_hr', 'coordinates')
def get_url(self, obj):
return obj.get_absolute_url()
def get_coordinates(self, obj):
"""
Coordinates are randomly moved around real location, roughly in a circle. The object id is used as angle so that
points are always displayed at the same location (as if they were a seed for a random function).
It's not exactly a circle, because the earth is round.
"""
if obj.location:
return [obj.location.longitude, obj.location.latitude]
return None
def get_location_hr(self, obj):
if obj.location.city:
return f"{obj.location.city}"
elif obj.location:
return f"{obj.location}"
return None
class AnimalCreateSerializer(serializers.ModelSerializer):
@@ -14,11 +121,6 @@ class AnimalCreateSerializer(serializers.ModelSerializer):
model = Animal
fields = ["name", "date_of_birth", "description", "species", "sex", "adoption_notice"]
class RescueOrgSerializer(serializers.ModelSerializer):
class Meta:
model = RescueOrganization
fields = ["name", "location_string", "instagram", "facebook", "fediverse_profile", "email", "phone_number",
"website", "description", "external_object_identifier", "external_source_identifier"]
class AnimalGetSerializer(serializers.ModelSerializer):
class Meta:
@@ -51,3 +153,9 @@ class SpeciesSerializer(serializers.ModelSerializer):
class Meta:
model = Species
fields = "__all__"
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Location
fields = "__all__"

View File

@@ -1,16 +1,20 @@
from django.urls import path
from .views import (
AdoptionNoticeApiView,
AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView
AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView, LocationApiView,
AdoptionNoticeGeoJSONView, RescueOrgGeoJSONView
)
urlpatterns = [
path("adoption_notice", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-list"),
path("adoption_notice.geojson", AdoptionNoticeGeoJSONView.as_view(), name="api-adoption-notice-list-geojson"),
path("adoption_notice/<int:id>/", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-detail"),
path("animals/", AnimalApiView.as_view(), name="api-animal-list"),
path("animals/<int:id>/", AnimalApiView.as_view(), name="api-animal-detail"),
path("organizations/", RescueOrganizationApiView.as_view(), name="api-organization-list"),
path("organizations.geojson", RescueOrgGeoJSONView.as_view(), name="api-organization-list-geojson"),
path("organizations/<int:id>/", RescueOrganizationApiView.as_view(), name="api-organization-detail"),
path("images/", AddImageApiView.as_view(), name="api-add-image"),
path("species/", SpeciesApiView.as_view(), name="api-species-list"),
path("locations/", LocationApiView.as_view(), name="api-locations-list"),
]

View File

@@ -1,20 +1,27 @@
from django.db.models import Q
from rest_framework.generics import ListAPIView
from fellchensammlung.api.serializers import LocationSerializer, AdoptionNoticeGeoJSONSerializer
from rest_framework.views import APIView
from rest_framework.response import Response
from django.db import transaction
from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel
from fellchensammlung.tasks import post_adoption_notice_save
from rest_framework import status
from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel, Location, AdoptionNoticeStatus
from fellchensammlung.tasks import post_adoption_notice_save, post_rescue_org_save
from rest_framework import status, serializers
from rest_framework.permissions import IsAuthenticated
from .renderers import GeoJSONRenderer
from .serializers import (
AnimalGetSerializer,
AnimalCreateSerializer,
RescueOrganizationSerializer,
RescueOrgeGeoJSONSerializer,
AdoptionNoticeSerializer,
ImageCreateSerializer,
SpeciesSerializer, RescueOrgSerializer,
SpeciesSerializer, RescueOrganizationSerializer,
)
from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image
from drf_spectacular.utils import extend_schema
from drf_spectacular.utils import extend_schema, inline_serializer
class AdoptionNoticeApiView(APIView):
permission_classes = [IsAuthenticated]
@@ -59,22 +66,22 @@ class AdoptionNoticeApiView(APIView):
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
adoption_notice = serializer.save(owner=request.user)
adoption_notice = serializer.save(owner=request.user_to_notify)
# Add the location
post_adoption_notice_save.delay_on_commit(adoption_notice.pk)
# Only set active when user has trust level moderator or higher
if request.user.trust_level >= TrustLevel.MODERATOR:
if request.user_to_notify.trust_level >= TrustLevel.MODERATOR:
adoption_notice.set_active()
else:
adoption_notice.set_unchecked()
# Log the action
Log.objects.create(
user=request.user,
user=request.user_to_notify,
action="add_adoption_notice",
text=f"{request.user} added adoption notice {adoption_notice.pk} via API",
text=f"{request.user_to_notify} added adoption notice {adoption_notice.pk} via API",
)
# Return success response with new adoption notice details
@@ -84,10 +91,12 @@ class AdoptionNoticeApiView(APIView):
)
class AnimalApiView(APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
responses=AnimalGetSerializer
)
def get(self, request, *args, **kwargs):
"""
Get list of animals or a specific animal by ID.
@@ -104,6 +113,16 @@ class AnimalApiView(APIView):
serializer = AnimalGetSerializer(animals, many=True, context={"request": request})
return Response(serializer.data, status=status.HTTP_200_OK)
@transaction.atomic
@extend_schema(
request=AnimalCreateSerializer,
responses={201: inline_serializer(
name='Animal',
fields={
'id': serializers.IntegerField(),
"message": serializers.Field()}),
400: "json"}
)
@transaction.atomic
def post(self, request, *args, **kwargs):
"""
@@ -111,13 +130,14 @@ class AnimalApiView(APIView):
"""
serializer = AnimalCreateSerializer(data=request.data, context={"request": request})
if serializer.is_valid():
animal = serializer.save(owner=request.user)
animal = serializer.save(owner=request.user_to_notify)
return Response(
{"message": "Animal created successfully!", "id": animal.id},
status=status.HTTP_201_CREATED,
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class RescueOrganizationApiView(APIView):
permission_classes = [IsAuthenticated]
@@ -129,14 +149,44 @@ class RescueOrganizationApiView(APIView):
'description': 'ID of the rescue organization to retrieve.',
'type': int
},
{
'name': 'trusted',
'required': False,
'description': 'Filter by trusted status (true/false).',
'type': bool
},
{
'name': 'external_object_identifier',
'required': False,
'description': 'Filter by external object identifier. Use "None" to filter for an empty field',
'type': str
},
{
'name': 'external_source_identifier',
'required': False,
'description': 'Filter by external source identifier. Use "None" to filter for an empty field',
'type': str
},
{
'name': 'search',
'required': False,
'description': 'Search by organization name or location name/city.',
'type': str
},
],
responses={200: RescueOrganizationSerializer(many=True)}
)
def get(self, request, *args, **kwargs):
"""
Get list of rescue organizations or a specific organization by ID.
Get list of rescue organizations or a specific organization by ID or get a list with available filters for
- external_object_identifier
- external_source_identifier
"""
org_id = kwargs.get("id")
org_id = request.query_params.get("id")
external_object_identifier = request.query_params.get("external_object_identifier")
external_source_identifier = request.query_params.get("external_source_identifier")
search_query = request.query_params.get("search")
if org_id:
try:
organization = RescueOrganization.objects.get(pk=org_id)
@@ -144,28 +194,79 @@ class RescueOrganizationApiView(APIView):
return Response(serializer.data, status=status.HTTP_200_OK)
except RescueOrganization.DoesNotExist:
return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
organizations = RescueOrganization.objects.all()
if external_object_identifier:
if external_object_identifier == "None":
external_object_identifier = None
organizations = organizations.filter(external_object_identifier=external_object_identifier)
if external_source_identifier:
if external_source_identifier == "None":
external_source_identifier = None
organizations = organizations.filter(external_source_identifier=external_source_identifier)
if search_query:
organizations = organizations.filter(
Q(name__icontains=search_query) |
Q(location_string__icontains=search_query) |
Q(location__name__icontains=search_query) |
Q(location__city__icontains=search_query)
)
if organizations.count() == 0:
return Response({"error": "No organizations found."}, status=status.HTTP_404_NOT_FOUND)
serializer = RescueOrganizationSerializer(organizations, many=True, context={"request": request})
return Response(serializer.data, status=status.HTTP_200_OK)
@transaction.atomic
@extend_schema(
request=RescueOrgSerializer, # Document the request body
responses={201: 'Rescue organization created/updated successfully!'}
request=RescueOrganizationSerializer,
responses={201: 'Rescue organization created successfully!'}
)
def post(self, request, *args, **kwargs):
"""
Create or update a rescue organization.
"""
serializer = RescueOrgSerializer(data=request.data, context={"request": request})
serializer = RescueOrganizationSerializer(data=request.data, context={"request": request})
if serializer.is_valid():
rescue_org = serializer.save(owner=request.user)
rescue_org = serializer.save()
if rescue_org.location is None:
# Add the location
post_rescue_org_save.delay_on_commit(rescue_org.pk)
return Response(
{"message": "Rescue organization created/updated successfully!", "id": rescue_org.id},
{"message": "Rescue organization created successfully!", "id": rescue_org.id},
status=status.HTTP_201_CREATED,
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@transaction.atomic
@extend_schema(
request=RescueOrganizationSerializer,
responses={200: 'Rescue organization updated successfully!'}
)
def patch(self, request, *args, **kwargs):
"""
Partially update a rescue organization.
"""
org_id = request.data.get("id")
if not org_id:
return Response({"error": "ID is required for updating."}, status=status.HTTP_400_BAD_REQUEST)
try:
organization = RescueOrganization.objects.get(pk=org_id)
except RescueOrganization.DoesNotExist:
return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
serializer = RescueOrganizationSerializer(organization, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response({"message": "Rescue organization updated successfully!"}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class AddImageApiView(APIView):
permission_classes = [IsAuthenticated]
@@ -188,7 +289,7 @@ class AddImageApiView(APIView):
raise ValueError("Unknown attach_to_type given, should not happen. Check serializer")
serializer.validated_data.pop('attach_to_type', None)
serializer.validated_data.pop('attach_to', None)
image = serializer.save(owner=request.user)
image = serializer.save(owner=request.user_to_notify)
object_to_attach_to.photos.add(image)
return Response(
{"message": "Image added successfully!", "id": image.id},
@@ -210,3 +311,75 @@ class SpeciesApiView(APIView):
species = Species.objects.all()
serializer = SpeciesSerializer(species, many=True, context={"request": request})
return Response(serializer.data, status=status.HTTP_200_OK)
class LocationApiView(APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
parameters=[
{
'name': 'id',
'required': False,
'description': 'ID of the location to retrieve.',
'type': int
},
],
responses={200: LocationSerializer(many=True)}
)
def get(self, request, *args, **kwargs):
"""
Retrieve a location
"""
location_id = kwargs.get("id")
if location_id:
try:
location = Location.objects.get(pk=location_id)
serializer = LocationSerializer(location, context={"request": request})
return Response(serializer.data, status=status.HTTP_200_OK)
except Location.DoesNotExist:
return Response({"error": "Location not found."}, status=status.HTTP_404_NOT_FOUND)
locations = Location.objects.all()
serializer = LocationSerializer(locations, many=True, context={"request": request})
return Response(serializer.data, status=status.HTTP_200_OK)
@transaction.atomic
@extend_schema(
request=LocationSerializer,
responses={201: 'Location created successfully!'}
)
def post(self, request, *args, **kwargs):
"""
API view to add a location
"""
serializer = LocationSerializer(data=request.data, context={'request': request})
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
location = serializer.save()
# Log the action
Log.objects.create(
user=request.user,
action="add_location",
text=f"{request.user} added adoption notice {location.pk} via API",
)
# Return success response with new adoption notice details
return Response(
{"message": "Location created successfully!", "id": location.pk},
status=status.HTTP_201_CREATED,
)
class AdoptionNoticeGeoJSONView(ListAPIView):
queryset = AdoptionNotice.objects.select_related('location').filter(location__isnull=False).filter(
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE)
serializer_class = AdoptionNoticeGeoJSONSerializer
renderer_classes = [GeoJSONRenderer]
class RescueOrgGeoJSONView(ListAPIView):
queryset = RescueOrganization.objects.select_related('location').filter(location__isnull=False)
serializer_class = RescueOrgeGeoJSONSerializer
renderer_classes = [GeoJSONRenderer]

View File

@@ -1,7 +1,7 @@
from django import forms
from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportComment, ModerationAction, User, Species, \
Comment, SexChoicesWithAll, DistanceChoices
Comment, SexChoicesWithAll, DistanceChoices, SpeciesSpecificURL, RescueOrganization
from django_registration.forms import RegistrationForm
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Layout, Fieldset, HTML, Row, Column, Field, Hidden
@@ -23,95 +23,38 @@ class DateInput(forms.DateInput):
class AdoptionNoticeForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
if 'in_adoption_notice_creation_flow' in kwargs:
in_flow = kwargs.pop('in_adoption_notice_creation_flow')
else:
in_flow = False
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_id = 'form-adoption-notice'
self.helper.form_class = 'card'
self.helper.form_method = 'post'
if in_flow:
submit = Submit('save-and-add-another-animal', _('Speichern'))
else:
submit = Submit('submit', _('Speichern'))
self.helper.layout = Layout(
Fieldset(
_('Vermittlungsdetails'),
'name',
'species',
'num_animals',
'date_of_birth',
'sex',
'group_only',
'searching_since',
'location_string',
'organization',
'description',
'further_information',
),
submit)
template_name = "fellchensammlung/forms/form_snippets.html"
class Meta:
model = AdoptionNotice
fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string",
"organization"]
class AdoptionNoticeFormWithDateWidget(AdoptionNoticeForm):
class Meta:
model = AdoptionNotice
fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string",
"organization"]
widgets = {
'searching_since': DateInput(),
'searching_since': DateInput(format=('%Y-%m-%d')),
}
class AnimalForm(forms.ModelForm):
class AdoptionNoticeFormAutoAnimal(AdoptionNoticeForm):
def __init__(self, *args, **kwargs):
if 'in_adoption_notice_creation_flow' in kwargs:
adding = kwargs.pop('in_adoption_notice_creation_flow')
else:
adding = False
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_class = 'form-animal card'
if adding:
self.helper.add_input(Submit('save-and-add-another-animal', _('Speichern und weiteres Tier hinzufügen')))
self.helper.add_input(Submit('save-and-finish', _('Speichern und beenden')))
else:
self.helper.add_input(Submit('submit', _('Speichern'), css_class="btn"))
class Meta:
model = Animal
fields = ["name", "date_of_birth", "species", "sex", "description"]
class AnimalFormWithDateWidget(AnimalForm):
class Meta:
model = Animal
fields = ["name", "date_of_birth", "species", "sex", "description"]
widgets = {
'date_of_birth': DateInput(),
}
class AdoptionNoticeFormWithDateWidgetAutoAnimal(AdoptionNoticeFormWithDateWidget):
def __init__(self, *args, **kwargs):
super(AdoptionNoticeFormWithDateWidgetAutoAnimal, self).__init__(*args, **kwargs)
super(AdoptionNoticeFormAutoAnimal, self).__init__(*args, **kwargs)
self.fields["num_animals"] = forms.fields.IntegerField(min_value=1, max_value=30, label=_("Zahl der Tiere"))
animal_form = AnimalForm()
self.fields["species"] = animal_form.fields["species"]
self.fields["sex"] = animal_form.fields["sex"]
self.fields["date_of_birth"] = animal_form.fields["date_of_birth"]
self.fields["date_of_birth"].widget = DateInput()
self.fields["date_of_birth"].widget = DateInput(format=('%Y-%m-%d'))
class AnimalForm(forms.ModelForm):
template_name = "fellchensammlung/forms/form_snippets.html"
class Meta:
model = Animal
fields = ["name", "date_of_birth", "species", "sex", "description"]
widgets = {
'date_of_birth': DateInput(format=('%Y-%m-%d'))
}
class ImageForm(forms.ModelForm):
@@ -127,8 +70,9 @@ class ImageForm(forms.ModelForm):
self.helper.form_method = 'post'
if in_flow:
submits= Div(Submit('submit', _('Speichern')),
Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')), css_class="container-edit-buttons")
submits = Div(Submit('submit', _('Speichern')),
Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')),
css_class="container-edit-buttons")
else:
submits = Fieldset(Submit('submit', _('Speichern')), css_class="container-edit-buttons")
self.helper.layout = Layout(
@@ -140,37 +84,45 @@ class ImageForm(forms.ModelForm):
submits
)
class Meta:
model = Image
fields = ('image', 'alt_text')
class ReportAdoptionNoticeForm(forms.ModelForm):
template_name = "fellchensammlung/forms/form_snippets.html"
class Meta:
model = ReportAdoptionNotice
fields = ('reported_broken_rules', 'user_comment')
class ReportCommentForm(forms.ModelForm):
template_name = "fellchensammlung/forms/form_snippets.html"
class Meta:
model = ReportComment
fields = ('reported_broken_rules', 'user_comment')
class CommentForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_class = 'form-comments'
self.helper.add_input(Hidden('action', 'comment'))
self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="btn2"))
class Meta:
model = Comment
fields = ('text',)
class SpeciesURLForm(forms.ModelForm):
class Meta:
model = SpeciesSpecificURL
fields = ('species', 'url')
class RescueOrgInternalComment(forms.ModelForm):
class Meta:
model = RescueOrganization
fields = ('internal_comment',)
class ModerationActionForm(forms.ModelForm):
class Meta:
model = ModerationAction
@@ -181,19 +133,17 @@ class CustomRegistrationForm(RegistrationForm):
class Meta(RegistrationForm.Meta):
model = User
captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_("Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um sicherzustellen, dass du kein Roboter bist."))
template_name = "fellchensammlung/forms/form_snippets.html"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_id = 'form-registration'
self.helper.form_class = 'card'
self.helper.add_input(Submit('submit', _('Registrieren'), css_class="btn"))
captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_(
"Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um sicherzustellen, dass du kein Roboter bist."))
class AdoptionNoticeSearchForm(forms.Form):
template_name = "fellchensammlung/forms/form_snippets.html"
sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False,
initial=SexChoicesWithAll.ALL)
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED, label=_("Suchradius"))
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED,
label=_("Suchradius"))
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)

View File

@@ -1,43 +1,43 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.core import mail
from fellchensammlung.models import User, CommentNotification, BaseNotification, TrustLevel
from notfellchen.settings import host
NEWLINE = "\r\n"
from fellchensammlung.models import User, Notification, TrustLevel, NotificationTypeChoices
from fellchensammlung.tools.model_helpers import ndm
def mail_admins_new_report(report):
subject = _("Neue Meldung")
def notify_mods_new_report(report, notification_type):
"""
Sends an e-mail to all users that should handle the report.
"""
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
greeting = _("Moin,") + "{NEWLINE}"
new_report_text = _("es wurde ein Regelverstoß gemeldet.") + "{NEWLINE}"
if len(report.reported_broken_rules.all()) > 0:
reported_rules_text = (f"Ein Verstoß gegen die folgenden Regeln wurde gemeldet:{NEWLINE}"
f"- {f'{NEWLINE} - '.join([str(r) for r in report.reported_broken_rules.all()])}{NEWLINE}")
if notification_type == NotificationTypeChoices.NEW_REPORT_AN:
title = _("Vermittlung gemeldet")
elif notification_type == NotificationTypeChoices.NEW_COMMENT:
title = _("Kommentar gemeldet")
else:
reported_rules_text = f"Es wurden keine Regeln angegeben gegen die Verstoßen wurde.{NEWLINE}"
if report.user_comment:
comment_text = f'Kommentar zum Report: "{report.user_comment}"{NEWLINE}'
else:
comment_text = f"Es wurde kein Kommentar hinzugefügt.{NEWLINE}"
report_url = "https://" + host + report.get_absolute_url()
link_text = f"Um alle Details zu sehen, geh bitte auf: {report_url}"
body_text = greeting + new_report_text + reported_rules_text + comment_text + link_text
message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [moderator.email])
message.send()
raise NotImplementedError
notification = Notification.objects.create(
notification_type=notification_type,
user_to_notify=moderator,
report=report,
title=title,
)
notification.save()
def send_notification_email(notification_pk):
try:
notification = CommentNotification.objects.get(pk=notification_pk)
except CommentNotification.DoesNotExist:
notification = BaseNotification.objects.get(pk=notification_pk)
subject = f"🔔 {notification.title}"
body_text = notification.text
message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [notification.user.email])
message.send()
notification = Notification.objects.get(pk=notification_pk)
subject = f"{notification.title}"
context = {"notification": notification, }
html_message = render_to_string(ndm[notification.notification_type].email_html_template, context)
plain_message = render_to_string(ndm[notification.notification_type].email_plain_template, context)
mail.send_mail(subject, plain_message, settings.DEFAULT_FROM_EMAIL,
[notification.user_to_notify.email],
html_message=html_message)

View 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()

View 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()

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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,
),
]

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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')},
),
]

View 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),
),
]

View File

@@ -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',
),
]

View File

@@ -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),
),
]

View 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')),
],
),
]

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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',
),
]

View File

@@ -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')),
],
),
]

View File

@@ -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',
),
]

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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',
),
]

View 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')),
],
),
]

View File

@@ -3,6 +3,7 @@ from random import choices
from tabnanny import verbose
from django.db import models
from django.template.defaultfilters import slugify
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
@@ -10,11 +11,14 @@ from django.dispatch import receiver
from django.db.models.signals import post_save
from django.contrib.auth.models import Group
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
from .tools import misc, geo
from notfellchen.settings import MEDIA_URL
from notfellchen.settings import MEDIA_URL, base_url
from .tools.geo import LocationProxy, Position
from .tools.misc import age_as_hr_string, time_since_as_hr_string
from .tools.model_helpers import NotificationTypeChoices
from .tools.model_helpers import ndm as NotificationDisplayMapping
class Language(models.Model):
@@ -39,24 +43,33 @@ class Language(models.Model):
class Location(models.Model):
place_id = models.IntegerField() # OSM id
place_id = models.CharField(max_length=200) # OSM id
latitude = models.FloatField()
longitude = models.FloatField()
name = models.CharField(max_length=2000)
city = models.CharField(max_length=200, blank=True, null=True)
housenumber = models.CharField(max_length=20, blank=True, null=True)
postcode = models.CharField(max_length=20, blank=True, null=True)
street = models.CharField(max_length=200, blank=True, null=True)
county = models.CharField(max_length=200, blank=True, null=True)
# Country code as per ISO 3166-1 alpha-2
# https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes
countrycode = models.CharField(max_length=2, verbose_name=_("Ländercode"),
help_text=_("Standardisierter Ländercode nach ISO 3166-1 ALPHA-2"),
blank=True, null=True)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
if self.city and self.postcode:
return f"{self.city} ({self.postcode})"
else:
return f"{self.name}"
@property
def position(self):
return (self.latitude, self.longitude)
@property
def str_hr(self):
return f"{self.name.split(',')[0]}"
@staticmethod
def get_location_from_string(location_string):
try:
@@ -73,6 +86,11 @@ class Location(models.Model):
latitude=proxy.latitude,
longitude=proxy.longitude,
name=proxy.name,
postcode=proxy.postcode,
city=proxy.city,
street=proxy.street,
county=proxy.county,
countrycode=proxy.countrycode,
)
return location
@@ -84,35 +102,51 @@ class Location(models.Model):
instance.save()
class ImportantLocation(models.Model):
location = models.OneToOneField(Location, on_delete=models.CASCADE)
slug = models.SlugField(unique=True)
name = models.CharField(max_length=200)
def get_absolute_url(self):
return reverse('search-by-location', kwargs={'important_location_slug': self.slug})
class ExternalSourceChoices(models.TextChoices):
OSM = "OSM", _("Open Street Map")
class RescueOrganization(models.Model):
class AllowUseOfMaterialsChices(models.TextChoices):
USE_MATERIALS_ALLOWED = "allowed", _("Usage allowed")
USE_MATERIALS_REQUESTED = "requested", _("Usage requested")
USE_MATERIALS_DENIED = "denied", _("Usage denied")
USE_MATERIALS_OTHER = "other", _("It's complicated")
USE_MATERIALS_NOT_ASKED = "not_asked", _("Not asked")
class Species(models.Model):
"""Model representing a species of animal."""
name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
verbose_name=_('Name'))
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.name}"
"""String for representing the Model object."""
return self.name
USE_MATERIALS_ALLOWED = "allowed"
USE_MATERIALS_REQUESTED = "requested"
USE_MATERIALS_DENIED = "denied"
USE_MATERIALS_OTHER = "other"
USE_MATERIALS_NOT_ASKED = "not_asked"
class Meta:
verbose_name = _('Tierart')
verbose_name_plural = _('Tierarten')
ALLOW_USE_MATERIALS_CHOICE = {
USE_MATERIALS_ALLOWED: "Usage allowed",
USE_MATERIALS_REQUESTED: "Usage requested",
USE_MATERIALS_DENIED: "Usage denied",
USE_MATERIALS_OTHER: "It's complicated",
USE_MATERIALS_NOT_ASKED: "Not asked"
}
class RescueOrganization(models.Model):
name = models.CharField(max_length=200)
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
allows_using_materials = models.CharField(max_length=200,
default=ALLOW_USE_MATERIALS_CHOICE[USE_MATERIALS_NOT_ASKED],
choices=ALLOW_USE_MATERIALS_CHOICE,
default=AllowUseOfMaterialsChices.USE_MATERIALS_NOT_ASKED,
choices=AllowUseOfMaterialsChices.choices,
verbose_name=_('Erlaubt Nutzung von Inhalten'))
location_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation"))
location_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation"), null=True, blank=True, )
location = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True)
instagram = models.URLField(null=True, blank=True, verbose_name=_('Instagram Profil'))
facebook = models.URLField(null=True, blank=True, verbose_name=_('Facebook Profil'))
@@ -122,6 +156,7 @@ class RescueOrganization(models.Model):
website = models.URLField(null=True, blank=True, verbose_name=_('Website'))
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
last_checked = models.DateTimeField(auto_now_add=True, verbose_name=_('Datum der letzten Prüfung'))
internal_comment = models.TextField(verbose_name=_("Interner Kommentar"), null=True, blank=True, )
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung')) # Markdown allowed
external_object_identifier = models.CharField(max_length=200, null=True, blank=True,
@@ -129,6 +164,27 @@ class RescueOrganization(models.Model):
external_source_identifier = models.CharField(max_length=200, null=True, blank=True,
choices=ExternalSourceChoices.choices,
verbose_name=_('External Source Identifier'))
exclude_from_check = models.BooleanField(default=False, verbose_name=_('Von Prüfung ausschließen'),
help_text=_("Organisation von der manuellen Überprüfung ausschließen, "
"z.B. weil Tiere nicht online geführt werden"))
ongoing_communication = models.BooleanField(default=False, verbose_name=_('In aktiver Kommunikation'),
help_text=_(
"Es findet gerade Kommunikation zwischen Notfellchen und der Organisation statt."))
parent_org = models.ForeignKey("RescueOrganization", on_delete=models.PROTECT, blank=True, null=True)
# allows to specify if a rescue organization has a specialization for dedicated species
specializations = models.ManyToManyField(Species, blank=True)
class Meta:
unique_together = ('external_object_identifier', 'external_source_identifier',)
ordering = ['name']
def __str__(self):
return f"{self.name}"
def clean(self):
super().clean()
if self.location is None and self.location_string is None:
raise ValidationError(_('Location or Location String must be set'))
def get_absolute_url(self):
return reverse("rescue-organization-detail", args=[str(self.pk)])
@@ -137,6 +193,17 @@ class RescueOrganization(models.Model):
def adoption_notices(self):
return AdoptionNotice.objects.filter(organization=self)
@property
def adoption_notices_in_hierarchy(self):
"""
Shows all adoption notices of this rescue organization and all child organizations.
"""
adoption_notices_discovered = list(self.adoption_notices)
if self.child_organizations:
for child in self.child_organizations:
adoption_notices_discovered.extend(child.adoption_notices_in_hierarchy)
return adoption_notices_discovered
@property
def position(self):
if self.location:
@@ -149,7 +216,37 @@ class RescueOrganization(models.Model):
if self.description is None:
return ""
if len(self.description) > 200:
return self.description[:200] + f" ... [weiterlesen]({self.get_absolute_url()})"
return self.description[:200] + _(f" ... [weiterlesen]({self.get_absolute_url()})")
else:
return self.description
def set_checked(self):
self.last_checked = timezone.now()
self.save()
@property
def last_checked_hr(self):
time_since_last_checked = timezone.now() - self.last_checked
return time_since_as_hr_string(time_since_last_checked)
@property
def species_urls(self):
return SpeciesSpecificURL.objects.filter(rescue_organization=self)
@property
def has_contact_data(self):
"""
Returns true if at least one type of contact data is available.
"""
return self.instagram or self.facebook or self.website or self.phone_number or self.email or self.fediverse_profile
def set_exclusion_from_checks(self):
self.exclude_from_check = True
self.save()
@property
def child_organizations(self):
return RescueOrganization.objects.filter(parent_org=self)
# Admins can perform all actions and have the highest trust associated with them
@@ -197,14 +294,17 @@ class User(AbstractUser):
def get_absolute_url(self):
return reverse("user-detail", args=[str(self.pk)])
def get_full_url(self):
return f"{base_url}{self.get_absolute_url()}"
def get_notifications_url(self):
return self.get_absolute_url()
def get_unread_notifications(self):
return BaseNotification.objects.filter(user=self, read=False)
return Notification.objects.filter(user_to_notify=self, read=False)
def get_num_unread_notifications(self):
return BaseNotification.objects.filter(user=self, read=False).count()
return Notification.objects.filter(user_to_notify=self, read=False).count()
@property
def adoption_notices(self):
@@ -230,22 +330,6 @@ class Image(models.Model):
return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">'
class Species(models.Model):
"""Model representing a species of animal."""
name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
verbose_name=_('Name'))
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
"""String for representing the Model object."""
return self.name
class Meta:
verbose_name = _('Tierart')
verbose_name_plural = _('Tierarten')
class AdoptionNotice(models.Model):
class Meta:
permissions = [
@@ -261,11 +345,15 @@ class AdoptionNotice(models.Model):
updated_at = models.DateTimeField(auto_now=True)
last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=timezone.now)
searching_since = models.DateField(verbose_name=_('Sucht nach einem Zuhause seit'))
name = models.CharField(max_length=200)
name = models.CharField(max_length=200, verbose_name=_('Titel der Vermittlung'))
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
organization = models.ForeignKey(RescueOrganization, blank=True, null=True, on_delete=models.SET_NULL,
verbose_name=_('Organisation'))
further_information = models.URLField(null=True, blank=True, verbose_name=_('Link zu mehr Informationen'))
further_information = models.URLField(null=True, blank=True,
verbose_name=_('Link zu mehr Informationen'),
help_text=_(
"Verlinke hier die Quelle der Vermittlung (z.B. die Website des "
"Tierheims)"))
group_only = models.BooleanField(default=False, verbose_name=_('Ausschließlich Gruppenadoption'))
photos = models.ManyToManyField(Image, blank=True)
location_string = models.CharField(max_length=200, verbose_name=_("Ortsangabe"))
@@ -283,6 +371,13 @@ class AdoptionNotice(models.Model):
sexes.add(animal.sex)
return sexes
@property
def num_per_sex(self):
num_per_sex = dict()
for sex in SexChoices:
num_per_sex[sex] = self.animals.filter(sex=sex).count()
return num_per_sex
@property
def last_checked_hr(self):
time_since_last_checked = timezone.now() - self.last_checked
@@ -323,6 +418,10 @@ class AdoptionNotice(models.Model):
"""Returns the url to access a detailed page for the adoption notice."""
return reverse('adoption-notice-detail', args=[str(self.id)])
def get_full_url(self):
"""Returns the url including protocol and domain"""
return f"{base_url}{self.get_absolute_url()}"
def get_report_url(self):
"""Returns the url to report an adoption notice."""
return reverse('report-adoption-notice', args=[str(self.id)])
@@ -379,29 +478,34 @@ class AdoptionNotice(models.Model):
distance = geo.calculate_distance_between_coordinates(self.position, position)
return distance < max_distance
@property
def link_to_more_information(self):
from urllib.parse import urlparse
domain = urlparse(self.further_information).netloc
return f"<a href='{self.further_information}'>{domain}</a>"
@property
def is_active(self):
if not hasattr(self, 'adoptionnoticestatus'):
return False
return self.adoptionnoticestatus.is_active
@property
def is_disabled(self):
if not hasattr(self, 'adoptionnoticestatus'):
return False
return self.adoptionnoticestatus.is_disabled
@property
def is_closed(self):
if not hasattr(self, 'adoptionnoticestatus'):
return False
return self.adoptionnoticestatus.is_closed
@property
def is_disabled_unchecked(self):
if not hasattr(self, 'adoptionnoticestatus'):
return False
return self.adoptionnoticestatus.is_disabled_unchecked
def set_closed(self):
def set_closed(self, minor_status=None):
self.last_checked = timezone.now()
self.save()
self.adoptionnoticestatus.set_closed()
self.adoptionnoticestatus.set_closed(minor_status)
def set_active(self):
self.last_checked = timezone.now()
@@ -420,7 +524,19 @@ class AdoptionNotice(models.Model):
for subscription in self.get_subscriptions():
notification_title = _("Vermittlung deaktiviert:") + f" {self.name}"
text = _("Die folgende Vermittlung wurde deaktiviert: ") + f"[{self.name}]({self.get_absolute_url()})"
BaseNotification.objects.create(user=subscription.owner, text=text, title=notification_title)
Notification.objects.create(user_to_notify=subscription.owner,
notification_type=NotificationTypeChoices.AN_WAS_DEACTIVATED,
adoption_notice=self,
text=text,
title=notification_title)
def last_posted(self, platform=None):
if platform is None:
last_post = SocialMediaPost.objects.filter(adoption_notice=self).order_by('-created_at').first()
else:
last_post = SocialMediaPost.objects.filter(adoption_notice=self, platform=platform).order_by(
'-created_at').first()
return last_post.created_at
class AdoptionNoticeStatus(models.Model):
@@ -485,6 +601,14 @@ class AdoptionNoticeStatus(models.Model):
def is_active(self):
return self.major_status == self.ACTIVE
@property
def is_disabled(self):
return self.major_status == self.DISABLED
@property
def is_closed(self):
return self.major_status == self.CLOSED
@property
def is_disabled_unchecked(self):
return self.major_status == self.DISABLED and self.minor_status == "unchecked"
@@ -502,9 +626,12 @@ class AdoptionNoticeStatus(models.Model):
minor_status=minor_status,
adoption_notice=an_instance)
def set_closed(self):
def set_closed(self, minor_status=None):
self.major_status = self.MAJOR_STATUS_CHOICES[self.CLOSED]
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"]
if minor_status is None:
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"]
else:
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED][minor_status]
self.save()
def set_unchecked(self):
@@ -660,12 +787,40 @@ class Report(models.Model):
"""Returns the url to access a detailed page for the report."""
return reverse('report-detail', args=[str(self.id)])
def get_full_url(self):
return f"{base_url}{self.get_absolute_url()}"
def get_reported_rules(self):
return self.reported_broken_rules.all()
def get_moderation_actions(self):
return ModerationAction.objects.filter(report=self)
@property
def reported_content(self):
"""
Dynamically fetch the reported content based on subclass.
The alternative would be to use the ContentType framework:
https://docs.djangoproject.com/en/5.1/ref/contrib/contenttypes/
"""
if hasattr(self, "reportadoptionnotice"):
return self.reportadoptionnotice.adoption_notice
elif hasattr(self, "reportcomment"):
return self.reportcomment.reported_comment
return None
@property
def reported_content_url(self):
"""
Same as reported_content, just for url
"""
if hasattr(self, "reportadoptionnotice"):
print(self.reportadoptionnotice.adoption_notice.get_absolute_url)
return self.reportadoptionnotice.adoption_notice.get_absolute_url
elif hasattr(self, "reportcomment"):
return self.reportcomment.reported_comment.get_absolute_url
return None
class ReportAdoptionNotice(Report):
adoption_notice = models.ForeignKey("AdoptionNotice", on_delete=models.CASCADE)
@@ -674,6 +829,9 @@ class ReportAdoptionNotice(Report):
def reported_content(self):
return self.adoption_notice
def __str__(self):
return f"Report der Vermittlung {self.adoption_notice}"
class ReportComment(Report):
reported_comment = models.ForeignKey("Comment", on_delete=models.CASCADE)
@@ -815,35 +973,47 @@ class Comment(models.Model):
return self.adoption_notice.get_absolute_url()
class BaseNotification(models.Model):
class Notification(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
title = models.CharField(max_length=100)
notification_type = models.CharField(max_length=200,
choices=NotificationTypeChoices.choices,
verbose_name=_('Benachrichtigungsgrund'))
user_to_notify = models.ForeignKey(User,
on_delete=models.CASCADE,
verbose_name=_('Empfänger*in'),
help_text=_("Useraccount der Benachrichtigt wird"),
related_name='user')
title = models.CharField(max_length=100, verbose_name=_("Titel"))
text = models.TextField(verbose_name="Inhalt")
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
read = models.BooleanField(default=False)
read_at = models.DateTimeField(blank=True, null=True, verbose_name=_("Gelesen am"))
comment = models.ForeignKey(Comment, blank=True, null=True, on_delete=models.CASCADE, verbose_name=_('Antwort'))
adoption_notice = models.ForeignKey(AdoptionNotice, blank=True, null=True, on_delete=models.CASCADE,
verbose_name=_('Vermittlung'))
user_related = models.ForeignKey(User,
blank=True, null=True,
on_delete=models.CASCADE, verbose_name=_('Verwandter Useraccount'),
help_text=_("Useraccount auf den sich die Benachrichtigung bezieht."))
report = models.ForeignKey(Report,
blank=True, null=True,
on_delete=models.CASCADE,
verbose_name=_('Report'),
help_text=_("Report auf den sich die Benachrichtigung bezieht."))
def __str__(self):
return f"[{self.user}] {self.title} ({self.created_at})"
return f"[{self.user_to_notify}] {self.title} ({self.created_at})"
def get_absolute_url(self):
self.user.get_notifications_url()
self.user_to_notify.get_notifications_url()
def mark_read(self):
self.read = True
self.read_at = timezone.now()
self.save()
class CommentNotification(BaseNotification):
comment = models.ForeignKey(Comment, on_delete=models.CASCADE, verbose_name=_('Antwort'))
@property
def url(self):
return self.comment.get_absolute_url
class AdoptionNoticeNotification(BaseNotification):
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
@property
def url(self):
return self.adoption_notice.get_absolute_url
def get_body_part(self):
return NotificationDisplayMapping[self.notification_type].web_partial
class Subscriptions(models.Model):
@@ -878,7 +1048,7 @@ class Timestamp(models.Model):
timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("Zeitstempel"))
data = models.CharField(max_length=2000, blank=True, null=True)
def ___str__(self):
def __str__(self):
return f"[{self.key}] - {self.timestamp.strftime('%H:%M:%S %d-%m-%Y ')} - {self.data}"
@@ -887,6 +1057,27 @@ class SpeciesSpecificURL(models.Model):
Model that allows to specify a URL for a rescue organization where a certain species can be found
"""
species = models.ForeignKey(Species, on_delete=models.CASCADE, verbose_name=_("Tierart"))
rescues_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE,
verbose_name=_("Tierschutzorganisation"))
rescue_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE,
verbose_name=_("Tierschutzorganisation"))
url = models.URLField(verbose_name=_("Tierartspezifische URL"))
class PlatformChoices(models.TextChoices):
FEDIVERSE = "fediverse", _("Fediverse")
class SocialMediaPost(models.Model):
created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
platform = models.CharField(max_length=255, verbose_name=_("Social Media Platform"),
choices=PlatformChoices.choices)
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
url = models.URLField(verbose_name=_("URL"))
@staticmethod
def get_an_to_post():
adoption_notices_without_post = AdoptionNotice.objects.filter(socialmediapost__isnull=True,
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE)
return adoption_notices_without_post.first()
def __str__(self):
return f"{self.platform} - {self.adoption_notice}"

View File

@@ -1,19 +1,23 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from fellchensammlung.models import BaseNotification, CommentNotification, User, TrustLevel
from fellchensammlung.models import Notification, User, TrustLevel, RescueOrganization, \
NotificationTypeChoices
from .tasks import task_send_notification_email
from notfellchen.settings import host
from django.utils.translation import gettext_lazy as _
@receiver(post_save, sender=CommentNotification)
def comment_notification_receiver(sender, instance: BaseNotification, created: bool, **kwargs):
base_notification_receiver(sender, instance, created, **kwargs)
@receiver(post_save, sender=Notification)
def base_notification_receiver(sender, instance: Notification, created: bool, **kwargs):
if not created or not instance.user_to_notify.email_notifications:
return
else:
task_send_notification_email.delay(instance.pk)
@receiver(post_save, sender=BaseNotification)
def base_notification_receiver(sender, instance: BaseNotification, created: bool, **kwargs):
if not created or not instance.user.email_notifications:
@receiver(post_save, sender=RescueOrganization)
def rescue_org_receiver(sender, instance: RescueOrganization, created: bool, **kwargs):
if instance.location:
return
else:
task_send_notification_email.delay(instance.pk)
@@ -33,5 +37,9 @@ def notification_new_user(sender, instance: User, created: bool, **kwargs):
link_text = f"Um alle Details zu sehen, geh bitte auf: {user_url}"
body_text = new_user_text + user_detail_text + link_text
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
notification = BaseNotification.objects.create(title=subject, text=body_text, user=moderator)
notification = Notification.objects.create(title=subject,
text=body_text,
notification_type=NotificationTypeChoices.NEW_USER,
user_to_notify=moderator,
user_related=instance)
notification.save()

View File

@@ -0,0 +1,29 @@
from django.utils.html import strip_tags
from django_registration.backends.activation.views import RegistrationView
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.conf import settings
class HTMLMailRegistrationView(RegistrationView):
def send_activation_email(self, user):
"""
overwrites the function in django registration
"""
activation_key = self.get_activation_key(user)
context = self.get_email_context(activation_key)
context["user"] = user
subject = render_to_string(
template_name=self.email_subject_template,
context=context,
request=self.request,
)
# Force subject to a single line to avoid header-injection issues.
subject = "".join(subject.splitlines())
message = render_to_string(
template_name=self.email_body_template,
context=context,
request=self.request,
)
plain_message = strip_tags(message)
user.email_user(subject, plain_message, settings.DEFAULT_FROM_EMAIL, html_message=message)

View File

@@ -0,0 +1,45 @@
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
from .models import AdoptionNotice, RescueOrganization, ImportantLocation, Animal
class StaticViewSitemap(Sitemap):
priority = 0.8
changefreq = "weekly"
def items(self):
return ["index", "search", "map", "about", "rescue-organizations", "buying", "imprint", "terms-of-service",
"privacy"]
def location(self, item):
return reverse(item)
class AdoptionNoticeSitemap(Sitemap):
priority = 0.5
changefreq = "daily"
def items(self):
return AdoptionNotice.get_active_ANs()
def lastmod(self, obj):
return obj.updated_at
class RescueOrganizationSitemap(Sitemap):
priority = 0.3
changefreq = "weekly"
def items(self):
return RescueOrganization.objects.all()
def lastmod(self, obj):
return obj.updated_at
class SearchSitemap(Sitemap):
priority = 0.5
chanfreq = "daily"
def items(self):
return ImportantLocation.objects.all()

View File

@@ -0,0 +1,348 @@
$primary: #6CD4FF;
$link: #292a2c;
$grey-light: #c4c6ce;
$grey-dark: #262728;
$confirm: hsl(133deg, 100%, calc(41% + 0%));
// Path to Bulma's sass folder
@use "bulma/sass" with (
$family-primary: '"Nunito", sans-serif',
$grey-dark: $grey-dark,
$grey-light: $grey-light,
$primary: $primary,
$link: $link,
$control-border-width: 2px,
$input-shadow: none
);
@use "bulma/sass/utilities/css-variables" as cv;
@include cv.system-theme($name: "dark") {
.navbar-item > img {
background-color: $grey-light !important;
border-radius: 5px;
}
.card-header {
background-color: $grey-dark;
}
a.card-footer-item.is-danger {
color: black;
}
.tag {
color: $grey-dark;
background-color: $grey-light;
}
}
// General Styles
.main-content {
margin: auto;
max-width: 80em;
padding: 10px;
}
p > a {
text-decoration: underline;
}
p > a.button {
text-decoration: none;
}
// Cards
.card-header {
background-color: $primary;
}
// Search form suggestion dropdown
#location-result-list {
display: inline; //ensures that the dropdown is not restricted in width WTF
}
.result-item {
cursor: pointer;
}
.result-item:hover {
background-color: #b2aaaa;
}
// Toggle switch
.toggle-switch {
display: inline-block;
background: #ccc;
border-radius: 16px;
width: 58px;
height: 32px;
position: relative;
vertical-align: middle;
transition: background 0.25s;
}
.toggle-switch:before, .toggle-switch:after {
content: "";
}
.toggle-switch:before {
display: block;
background: linear-gradient(to bottom, #fff 0%, #eee 100%);
border-radius: 50%;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
width: 24px;
height: 24px;
position: absolute;
top: 4px;
left: 4px;
transition: left 0.25s;
}
.toggle:hover .toggle-switch:before {
background: linear-gradient(to bottom, #fff 0%, #fff 100%);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
}
.checked + .toggle-switch {
background: #56c080;
}
.checked + .toggle-switch:before {
left: 30px;
}
.toggle-checkbox {
position: absolute;
visibility: hidden;
}
.slider-label {
margin-left: 5px;
position: relative;
top: 2px;
}
// Button in card footer
.card-footer {
overflow: hidden;
}
.card-footer .card-footer-item.is-confirm {
background-color: $confirm;
}
.card-footer .card-footer-item.is-confirm:hover {
filter: brightness(0.9);
}
.card-footer .card-footer-item.is-danger {
background-color: sass.$danger;
}
.card-footer .card-footer-item.is-danger:hover {
filter: brightness(0.9);
}
.card-footer .card-footer-item.is-warning {
background-color: sass.$warning;
}
.card-footer .card-footer-item.is-warning:hover {
filter: brightness(0.9);
}
/*******/
/* MAP */
/*******/
.map {
border-radius: 8px;
width: 100%;
height: 100%
}
.marker {
background-image: url('../img/logo_transparent.png');
background-size: cover;
width: 50px;
height: 50px;
cursor: pointer;
}
.animal-shelter-marker {
background-image: url('../img/animal_shelter.png');
}
.map-in-content #map {
max-height: 500px;
width: 90%;
}
@media only screen and (min-width: 768px) {
.maplibregl-popup {
max-width: 280px !important;
}
}
@media only screen and (max-width: 768px) {
.maplibregl-popup {
max-width: 150px !important;
}
}
.maplibregl-popup-close-button {
all: unset; /* Remove all inherited styles */
font-size: 1.2rem;
background: none;
border: none;
color: black;
cursor: pointer;
position: absolute;
top: 0.25rem;
right: 0.5rem;
padding: 0.25rem;
}
.popup-content {
line-height: 1.4;
}
/*****
IMAGES
*****/
.gallery .main-photo img {
width: 100%;
height: 150px;
object-fit: cover; /* Crops the images */
border-radius: 6px;
}
.thumbnail-row {
display: flex;
gap: 10px;
margin-top: 10px;
}
.thumbnail img {
width: 100%;
height: 50px;
object-fit: cover; /* Crops the images */
border-radius: 4px;
}
/* Ensure each thumbnail takes equal width */
.thumbnail {
flex: 1;
}
/**
AN Cards
*/
.an-card {
width: 100%;
height: 100%;
}
// Fonts
@font-face {
font-family: 'Nunito';
font-style: normal;
font-weight: 400;
src: url(../fonts/nunito.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
.new-animal-ad fieldset {
border-top: 4px solid var(--bulma-text-weak);
margin-top: 2em;
padding-top: 1em;
}
.new-animal-ad * {
transition: all ease 0.5s;
}
.new-animal-ad .open {
display: block;
}
.new-animal-ad .closed {
display: none;
overflow: hidden;
}
.new-animal-ad legend {
font-weight: bold;
padding-right: 0.2em;
color: var(--bulma-label-color);
font-size: 130%;
}
.feedback-backdrop {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
}
.feedback-add-new {
width: 40ch;
min-height: 40ch;
padding: 1.5em;
background-color: var(--bulma-info-on-scheme);
color: black;
}
.feedback-add-new.error {
background-color: var(--bulma-danger-on-scheme);
}
.feedback-add-new.success {
background-color: var(--bulma-success-on-scheme);
}
.notification-container {
display: inline-block;
position: relative;
padding: 0;
}
.notification-label {
padding: 2px 5px;
}
/* Make the badge float in the top right corner of the button */
.notification-badge {
background-color: #fa3e3e;
border-radius: 2px;
color: white;
padding: 1px 3px;
font-size: 8px;
position: absolute; /* Position the badge within the relatively positioned button */
top: 0;
right: 0;
}

View File

@@ -0,0 +1,420 @@
/*! PhotoSwipe main CSS by Dmytro Semenov | photoswipe.com */
.pswp {
--pswp-bg: #000;
--pswp-placeholder-bg: #222;
--pswp-root-z-index: 100000;
--pswp-preloader-color: rgba(79, 79, 79, 0.4);
--pswp-preloader-color-secondary: rgba(255, 255, 255, 0.9);
/* defined via js:
--pswp-transition-duration: 333ms; */
--pswp-icon-color: #fff;
--pswp-icon-color-secondary: #4f4f4f;
--pswp-icon-stroke-color: #4f4f4f;
--pswp-icon-stroke-width: 2px;
--pswp-error-text-color: var(--pswp-icon-color);
}
/*
Styles for basic PhotoSwipe (pswp) functionality (sliding area, open/close transitions)
*/
.pswp {
position: fixed;
z-index: var(--pswp-root-z-index);
display: none;
touch-action: none;
outline: 0;
opacity: 0.003;
contain: layout style size;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* Prevents focus outline on the root element,
(it may be focused initially) */
.pswp:focus {
outline: 0;
}
.pswp * {
box-sizing: border-box;
}
.pswp img {
max-width: none;
}
.pswp--open {
display: block;
}
.pswp,
.pswp__bg {
transform: translateZ(0);
will-change: opacity;
}
.pswp__bg {
opacity: 0.005;
background: var(--pswp-bg);
}
.pswp,
.pswp__scroll-wrap {
overflow: hidden;
}
.pswp,
.pswp__scroll-wrap,
.pswp__bg,
.pswp__container,
.pswp__item,
.pswp__content,
.pswp__img,
.pswp__zoom-wrap {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.pswp {
position: fixed;
}
.pswp__img,
.pswp__zoom-wrap {
width: auto;
height: auto;
}
.pswp--click-to-zoom.pswp--zoom-allowed .pswp__img {
cursor: -webkit-zoom-in;
cursor: -moz-zoom-in;
cursor: zoom-in;
}
.pswp--click-to-zoom.pswp--zoomed-in .pswp__img {
cursor: move;
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.pswp--click-to-zoom.pswp--zoomed-in .pswp__img:active {
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* :active to override grabbing cursor */
.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img,
.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img:active,
.pswp__img {
cursor: -webkit-zoom-out;
cursor: -moz-zoom-out;
cursor: zoom-out;
}
/* Prevent selection and tap highlights */
.pswp__container,
.pswp__img,
.pswp__button,
.pswp__counter {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.pswp__item {
/* z-index for fade transition */
z-index: 1;
overflow: hidden;
}
.pswp__hidden {
display: none !important;
}
/* Allow to click through pswp__content element, but not its children */
.pswp__content {
pointer-events: none;
}
.pswp__content > * {
pointer-events: auto;
}
/*
PhotoSwipe UI
*/
/*
Error message appears when image is not loaded
(JS option errorMsg controls markup)
*/
.pswp__error-msg-container {
display: grid;
}
.pswp__error-msg {
margin: auto;
font-size: 1em;
line-height: 1;
color: var(--pswp-error-text-color);
}
/*
class pswp__hide-on-close is applied to elements that
should hide (for example fade out) when PhotoSwipe is closed
and show (for example fade in) when PhotoSwipe is opened
*/
.pswp .pswp__hide-on-close {
opacity: 0.005;
will-change: opacity;
transition: opacity var(--pswp-transition-duration) cubic-bezier(0.4, 0, 0.22, 1);
z-index: 10; /* always overlap slide content */
pointer-events: none; /* hidden elements should not be clickable */
}
/* class pswp--ui-visible is added when opening or closing transition starts */
.pswp--ui-visible .pswp__hide-on-close {
opacity: 1;
pointer-events: auto;
}
/* <button> styles, including css reset */
.pswp__button {
position: relative;
display: block;
width: 50px;
height: 60px;
padding: 0;
margin: 0;
overflow: hidden;
cursor: pointer;
background: none;
border: 0;
box-shadow: none;
opacity: 0.85;
-webkit-appearance: none;
-webkit-touch-callout: none;
}
.pswp__button:hover,
.pswp__button:active,
.pswp__button:focus {
transition: none;
padding: 0;
background: none;
border: 0;
box-shadow: none;
opacity: 1;
}
.pswp__button:disabled {
opacity: 0.3;
cursor: auto;
}
.pswp__icn {
fill: var(--pswp-icon-color);
color: var(--pswp-icon-color-secondary);
}
.pswp__icn {
position: absolute;
top: 14px;
left: 9px;
width: 32px;
height: 32px;
overflow: hidden;
pointer-events: none;
}
.pswp__icn-shadow {
stroke: var(--pswp-icon-stroke-color);
stroke-width: var(--pswp-icon-stroke-width);
fill: none;
}
.pswp__icn:focus {
outline: 0;
}
/*
div element that matches size of large image,
large image loads on top of it,
used when msrc is not provided
*/
div.pswp__img--placeholder,
.pswp__img--with-bg {
background: var(--pswp-placeholder-bg);
}
.pswp__top-bar {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 60px;
display: flex;
flex-direction: row;
justify-content: flex-end;
z-index: 10;
/* allow events to pass through top bar itself */
pointer-events: none !important;
}
.pswp__top-bar > * {
pointer-events: auto;
/* this makes transition significantly more smooth,
even though inner elements are not animated */
will-change: opacity;
}
/*
Close button
*/
.pswp__button--close {
margin-right: 6px;
}
/*
Arrow buttons
*/
.pswp__button--arrow {
position: absolute;
top: 0;
width: 75px;
height: 100px;
top: 50%;
margin-top: -50px;
}
.pswp__button--arrow:disabled {
display: none;
cursor: default;
}
.pswp__button--arrow .pswp__icn {
top: 50%;
margin-top: -30px;
width: 60px;
height: 60px;
background: none;
border-radius: 0;
}
.pswp--one-slide .pswp__button--arrow {
display: none;
}
/* hide arrows on touch screens */
.pswp--touch .pswp__button--arrow {
visibility: hidden;
}
/* show arrows only after mouse was used */
.pswp--has_mouse .pswp__button--arrow {
visibility: visible;
}
.pswp__button--arrow--prev {
right: auto;
left: 0px;
}
.pswp__button--arrow--next {
right: 0px;
}
.pswp__button--arrow--next .pswp__icn {
left: auto;
right: 14px;
/* flip horizontally */
transform: scale(-1, 1);
}
/*
Zoom button
*/
.pswp__button--zoom {
display: none;
}
.pswp--zoom-allowed .pswp__button--zoom {
display: block;
}
/* "+" => "-" */
.pswp--zoomed-in .pswp__zoom-icn-bar-v {
display: none;
}
/*
Loading indicator
*/
.pswp__preloader {
position: relative;
overflow: hidden;
width: 50px;
height: 60px;
margin-right: auto;
}
.pswp__preloader .pswp__icn {
opacity: 0;
transition: opacity 0.2s linear;
animation: pswp-clockwise 600ms linear infinite;
}
.pswp__preloader--active .pswp__icn {
opacity: 0.85;
}
@keyframes pswp-clockwise {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/*
"1 of 10" counter
*/
.pswp__counter {
height: 30px;
margin: 15px 0 0 20px;
font-size: 14px;
line-height: 30px;
color: var(--pswp-icon-color);
text-shadow: 1px 1px 3px var(--pswp-icon-color-secondary);
opacity: 0.85;
}
.pswp--one-slide .pswp__counter {
display: none;
}

View File

@@ -1,27 +1,15 @@
:root {
--primary-light-one: #5daa68;
--primary-light-two: #4a9455;
--primary-dark-one: #17311b;
--secondary-light-one: #faf1cf;
--secondary-light-two: #e1d7b5;
--background-one: var(--primary-light-one);
--background-two: var(--primary-light-two);
--background-three: var(--secondary-light-one);
--background-four: var(--primary-dark-one);
--highlight-one: var(--primary-dark-one);
--highlight-one-text: var(--secondary-light-one);
--text-one: var(--secondary-light-one);
--shadow-one: var(--primary-dark-one);
--text-two: var(--primary-dark-one);
--text-three: var(--primary-light-one);
--shadow-three: var(--primary-dark-one);
--primary: #6CD4FF;
--link: #292a2c;
--grey-light: #c4c6ce;
--grey-dark: #262728;
}
body {
display: flex;
flex-direction: column;
background-color: var(--background-one);
color: var(--text-one);
background-color: hsl(221, 14%, 100%)r;
color: #000000;
margin: 0;
height: 100%;
}
@@ -32,24 +20,22 @@ body {
alert-box {
color: var(--highlight-one);
display: block;
margin: 3rem 0;
padding: 2rem 3rem;
border: 1px solid var(--highlight-one);
border-left-width: .5rem;
border-radius: .4rem;
background-color: var(--background-three);
background-color: var(--primary);
a {
color: var(--text-three);
text-decoration: none;
text-decoration: underline;
}
}
a {
color: inherit;
text-decoration: none;
color: var(--link);
}
@@ -65,7 +51,7 @@ a {
margin: 1rem;
padding: 5px;
border-radius: .4rem;
background-color: var(--background-one);
border: 3px solid var(--primary);
}
.post-summary h1 {
@@ -79,8 +65,7 @@ a {
}
.navigation-sticky {
background-color: var(--secondary-light-one);
color: var(--primary-light-one);
background-color: var(--primary);
padding: 16px;
margin: 0;
border-bottom-right-radius: 8px;

View File

@@ -259,6 +259,11 @@ a.btn, a.btn2, a.nav-link {
border: 1px solid black;
}
.btn-small {
font-size: medium;
padding: 6px;
}
.checkmark {
display: inline-block;
position: relative;

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,423 @@
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
document.addEventListener('DOMContentLoaded', function () {
// ------------------------------------------------ functions
var show = function (elem) {
// Get the natural height of the element
var getHeight = function () {
elem.style.display = 'block'; // Make it visible
var height = elem.scrollHeight + 'px'; // Get its height
elem.style.display = ''; // Hide it again
return height;
};
var height = getHeight(); // Get the natural height
elem.classList.remove('closed');
elem.classList.add('open'); // Make the element visible
elem.setAttribute('aria-hidden', 'false');
elem.style.height = height; // Update the max-height
// Once the transition is complete, remove the inline max-height so the content can scale responsively
window.setTimeout(function () {
elem.style.height = '';
}, 500);
};
var hide = function (elem) {
// Give the element a height to change from
elem.style.height = elem.scrollHeight + 'px';
// Set the height back to 0
window.setTimeout(function () {
elem.style.height = '0';
}, 1);
// When the transition is complete, hide it
window.setTimeout(function () {
elem.classList.remove('open');
elem.classList.add('closed');
elem.setAttribute('aria-hidden', 'true');
}, 500);
};
var toggle = function (elem, timing) {
// If the element is visible, hide it
if (elem.classList.contains('open')) {
hide(elem);
return;
}
// Otherwise, show it
show(elem);
};
// ------------------------------------------------ build form
let orig_form = document.querySelector('form');
orig_form.style.display = 'none';
let an_max = 6;
let an_fieldset = document.createElement('fieldset');
an_fieldset.classList.add('cell');
let an_fieldset_legend = document.createElement('legend');
an_fieldset_legend.innerHTML = "Allgemeines";
an_fieldset.appendChild(an_fieldset_legend);
let an_name = document.createElement('input');
an_name.setAttribute('type', 'text');
an_name.setAttribute('name', 'name');
an_name.setAttribute('class', 'input');
an_name.setAttribute('maxlength', 200);
an_name.setAttribute('required', 'required');
let an_name_label = document.createElement('label');
an_name_label.setAttribute('class', 'label');
an_name_label.innerHTML = 'Titel der Vermittlung';
an_name_label.appendChild(an_name);
let an_location_string = document.createElement('input');
an_location_string.setAttribute('type', 'text');
an_location_string.setAttribute('name', 'location_string');
an_location_string.setAttribute('class', 'input');
an_location_string.setAttribute('maxlength', 200);
an_location_string.setAttribute('required', 'required');
let an_location_string_label = document.createElement('label');
an_location_string_label.setAttribute('class', 'label');
an_location_string_label.innerHTML = 'Ortsangabe';
an_location_string_label.appendChild(an_location_string);
let an_further_information = document.createElement('input');
an_further_information.setAttribute('type', 'url');
an_further_information.setAttribute('name', 'further_information');
an_further_information.setAttribute('class', 'input');
an_further_information.setAttribute('maxlength', 200);
let an_further_information_label = document.createElement('label');
an_further_information_label.setAttribute('class', 'label');
an_further_information_label.innerHTML = 'Link zu mehr Informationen';
an_further_information_label.appendChild(an_further_information);
let an_species = document.createElement('select');
let an_species_rat = document.createElement('option');
an_species_rat.value = 1;
an_species_rat.innerHTML = "Farbratte";
an_species.appendChild(an_species_rat);
an_species.setAttribute('name', 'species');
an_species.setAttribute('class', 'input');
an_species.setAttribute('required', 'required');
let an_species_label = document.createElement('label');
an_species_label.setAttribute('class', 'label');
an_species_label.innerHTML = 'Tierart';
an_species_label.appendChild(an_species);
let an_number = document.createElement('input');
an_number.setAttribute('type', 'number');
an_number.setAttribute('name', 'number');
an_number.setAttribute('class', 'input');
an_number.setAttribute('min', 1);
an_number.setAttribute('max', an_max);
an_number.setAttribute('required', 'required');
let an_number_label = document.createElement('label');
an_number_label.setAttribute('class', 'label');
an_number_label.innerHTML = 'Anzahl Tiere';
an_number_label.appendChild(an_number);
let an_dateofbirth = document.createElement('input');
an_dateofbirth.setAttribute('type', 'date');
an_dateofbirth.setAttribute('name', 'dateofbirth');
an_dateofbirth.setAttribute('class', 'input');
an_dateofbirth.setAttribute('maxlength', 200);
an_dateofbirth.setAttribute('required', 'required');
let an_dateofbirth_label = document.createElement('label');
an_dateofbirth_label.setAttribute('class', 'label');
an_dateofbirth_label.innerHTML = 'Geburtsdatum';
an_dateofbirth_label.appendChild(an_dateofbirth);
let an_sex = document.createElement('select');
let an_sex_F = document.createElement('option');
an_sex_F.value = 'F';
an_sex_F.innerHTML = "Weiblich";
an_sex.appendChild(an_sex_F);
let an_sex_M = document.createElement('option');
an_sex_M.value = 'M';
an_sex_M.innerHTML = "Männlich";
an_sex.appendChild(an_sex_M);
let an_sex_F_N = document.createElement('option');
an_sex_F_N.value = 'F_N';
an_sex_F_N.innerHTML = "Weiblich, kastriert";
an_sex.appendChild(an_sex_F_N);
let an_sex_M_N = document.createElement('option');
an_sex_M_N.value = 'M_N';
an_sex_M_N.innerHTML = "Männlich, kastriert";
an_sex.appendChild(an_sex_M_N);
let an_sex_I = document.createElement('option');
an_sex_I.value = 'I';
an_sex_I.innerHTML = "Intergeschlechtlich";
an_sex.appendChild(an_sex_I);
an_sex.setAttribute('name', 'sex');
an_sex.setAttribute('class', 'input');
an_sex.setAttribute('required', 'required');
let an_sex_label = document.createElement('label');
an_sex_label.setAttribute('class', 'label');
an_sex_label.innerHTML = 'Geschlecht';
an_sex_label.appendChild(an_sex);
let an_searching_since = document.createElement('input');
an_searching_since.setAttribute('type', 'date');
an_searching_since.setAttribute('name', 'searching_since');
an_searching_since.setAttribute('class', 'input');
an_searching_since.setAttribute('maxlength', 200);
an_searching_since.setAttribute('required', 'required');
let an_searching_since_label = document.createElement('label');
an_searching_since_label.setAttribute('class', 'label');
an_searching_since_label.innerHTML = 'neues Zuhause gesucht seit';
an_searching_since_label.appendChild(an_searching_since);
let an_group_only = document.createElement('select');
let an_group_only_yes = document.createElement('option');
an_group_only_yes.value = 1;
an_group_only_yes.innerHTML = "nur zusammen";
let an_group_only_no = document.createElement('option');
an_group_only_no.value = 0;
an_group_only_no.innerHTML = "auch einzeln";
an_group_only.appendChild(an_group_only_yes);
an_group_only.appendChild(an_group_only_no);
an_group_only.setAttribute('name', 'group_only');
an_group_only.setAttribute('class', 'input');
an_group_only.setAttribute('required', 'required');
let an_group_only_label = document.createElement('label');
an_group_only_label.setAttribute('class', 'label');
an_group_only_label.innerHTML = 'Gruppenvermittlung';
an_group_only_label.appendChild(an_group_only);
let animals = document.createElement('fieldset');
animals.classList.add('cell', 'is-col-span-2');
let animals_legend = document.createElement('legend');
animals_legend.innerHTML = 'Angaben zu den Tieren';
animals.appendChild(animals_legend);
let noteNumber = document.createElement('p');
noteNumber.setAttribute('id', 'noteNumber');
noteNumber.innerHTML = 'Bitte Anzahl Tiere angeben';
animals.appendChild(noteNumber);
let an_description = document.createElement('textarea');
an_description.setAttribute('name', 'an_description');
an_description.classList.add('input', 'textarea');
let an_description_label = document.createElement('label');
an_description_label.innerHTML = 'Beschreibung der Gruppe';
an_description_label.classList.add('label');
an_description_label.appendChild(an_description);
animals.appendChild(an_group_only_label);
animals.appendChild(an_description_label);
for (let i = 0; i < an_max; i++) {
let an_fieldset_$i = document.createElement('fieldset');
an_fieldset_$i.classList.add('animal-' + i, 'animal');
an_fieldset_$i.appendChild(document.createElement('legend'));
an_fieldset_$i.querySelector('legend').innerHTML = 'Tier ' + parseInt(i + 1);
let an_name_$i = document.createElement('input');
an_name_$i.setAttribute('type', 'text');
an_name_$i.setAttribute('name', 'name-' + i);
an_name_$i.setAttribute('class', 'input');
an_name_$i.setAttribute('maxlength', 200);
an_name_$i.setAttribute('required', 'required');
let an_name_$i_label = document.createElement('label');
an_name_$i_label.setAttribute('class', 'label');
an_name_$i_label.innerHTML = 'Name';
an_name_$i_label.appendChild(an_name_$i);
let an_dateofbirth_$i = document.createElement('input');
an_dateofbirth_$i.setAttribute('type', 'date');
an_dateofbirth_$i.setAttribute('name', 'dateofbirth');
an_dateofbirth_$i.setAttribute('class', 'input');
an_dateofbirth_$i.setAttribute('maxlength', 200);
an_dateofbirth_$i.setAttribute('required', 'required');
let an_dateofbirth_$i_label = document.createElement('label');
an_dateofbirth_$i_label.setAttribute('class', 'label');
an_dateofbirth_$i_label.innerHTML = 'Geburtsdatum';
an_dateofbirth_$i_label.appendChild(an_dateofbirth_$i);
let an_sex_$i = document.createElement('select');
let an_sex_F = document.createElement('option');
an_sex_F.value = 'F';
an_sex_F.innerHTML = "Weiblich";
an_sex_$i.appendChild(an_sex_F);
let an_sex_M = document.createElement('option');
an_sex_M.value = 'M';
an_sex_M.innerHTML = "Männlich";
an_sex_$i.appendChild(an_sex_M);
let an_sex_F_N = document.createElement('option');
an_sex_F_N.value = 'F_N';
an_sex_F_N.innerHTML = "Weiblich, kastriert";
an_sex_$i.appendChild(an_sex_F_N);
let an_sex_M_N = document.createElement('option');
an_sex_M_N.value = 'M_N';
an_sex_M_N.innerHTML = "Männlich, kastriert";
an_sex_$i.appendChild(an_sex_M_N);
let an_sex_I = document.createElement('option');
an_sex_I.value = 'I';
an_sex_I.innerHTML = "Intergeschlechtlich";
an_sex_$i.appendChild(an_sex_I);
an_sex_$i.setAttribute('name', 'sex');
an_sex_$i.setAttribute('class', 'input');
an_sex_$i.setAttribute('required', 'required');
let an_sex_$i_label = document.createElement('label');
an_sex_$i_label.setAttribute('class', 'label');
an_sex_$i_label.innerHTML = 'Geschlecht';
an_sex_$i_label.appendChild(an_sex_$i);
let an_description_$i = document.createElement('textarea');
an_description_$i.setAttribute('name', 'an_description');
an_description_$i.classList.add('input', 'textarea');
let an_description_$i_label = document.createElement('label');
an_description_$i_label.innerHTML = 'Beschreibung';
an_description_$i_label.classList.add('label');
an_description_$i_label.appendChild(an_description_$i);
an_fieldset_$i.appendChild(an_description_$i_label);
an_fieldset_$i.appendChild(an_name_$i_label);
an_fieldset_$i.appendChild(an_dateofbirth_$i_label);
an_fieldset_$i.appendChild(an_sex_$i_label);
an_fieldset_$i.appendChild(an_description_$i_label);
animals.appendChild(an_fieldset_$i);
}
an_fieldset.appendChild(an_name_label);
an_fieldset.appendChild(an_location_string_label);
an_fieldset.appendChild(an_further_information_label);
an_fieldset.appendChild(an_species_label);
an_fieldset.appendChild(an_number_label);
an_fieldset.appendChild(an_dateofbirth_label);
an_fieldset.appendChild(an_sex_label);
an_fieldset.appendChild(an_searching_since_label);
let new_form = document.createElement('form');
new_form.classList.add('new-animal-ad', 'fixed-grid', 'has-3-cols', 'has-1-cols-mobile');
let div = document.createElement('div');
div.classList.add('grid');
let sButton = document.createElement('button');
sButton.classList.add('button');
sButton.innerHTML = "Abschicken";
div.appendChild(an_fieldset);
div.appendChild(animals);
div.appendChild(sButton);
new_form.appendChild(div);
document.querySelector('.main-content').appendChild(new_form);
// ------------------------------------------------ listeners
// number of animals
let tmpAnimal;
an_number.addEventListener('change', function () {
if (an_number.value > 0) {
hide(noteNumber);
} else {
show(noteNumber);
}
if (an_number.value < 2) {
hide(an_description_label);
hide(an_group_only_label);
an_group_only.selectedIndex = 1;
} else {
show(an_description_label);
show(an_group_only_label);
an_group_only.selectedIndex = 0;
}
for (let i = 0; i < an_max; i++) {
tmpAnimal = document.querySelector('.animal-' + i);
if (i < an_number.value) {
tmpAnimal.removeAttribute('disabled');
show(tmpAnimal);
} else {
tmpAnimal.setAttribute('disabled', 'true');
hide(tmpAnimal);
}
}
});
// sex
an_sex.addEventListener('change', function () {
for (let i = 0; i < an_max; i++) {
let selList = document.querySelector('.animal-' + i).querySelector('[name="sex"]');
for (let j = 0; j < selList.options.length; j++) {
if (selList.options[j].value == an_sex.value) {
selList.selectedIndex = j;
break;
}
}
}
});
// date of birth
an_dateofbirth.addEventListener('change', function () {
for (let i = 0; i < an_max; i++) {
document.querySelector('.animal-' + i).querySelector('[name="dateofbirth"]').value = an_dateofbirth.value;
}
});
// ------------------------------------------------ initialise
show(noteNumber);
hide(an_description_label);
hide(an_group_only_label);
for (let i = 0; i < an_max; i++) {
hide(document.querySelector('.animal-' + i));
}
// ---------------------------------------------------- submit
new_form.addEventListener('submit', function (event) {
event.preventDefault();
let date = new Date();
let postDate = date.toISOString().slice(0, 10);
const path = '';
let elResultsBd = document.createElement('div');
elResultsBd.classList.add('feedback-backdrop');
let elResults = document.createElement('div');
elResults.classList.add('feedback-add-new');
elResultsBd.appendChild(elResults);
document.querySelector('body').appendChild(elResultsBd);
let data = JSON.stringify({
"created_at": postDate,
"searching_since": an_searching_since.value,
"name": an_name.value,
"description": an_description.value,
"further_information": an_further_information.value,
"group_only": an_group_only.value,
"location_string": an_location_string.value,
});
async function submitAN() {
const csrftoken = getCookie('csrftoken');
let response = await fetch('http://localhost:8000/api/adoption_notice', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
'X-CSRFToken': csrftoken,
},
body: data,
});
console.log(response.status);
if (response.status === 201) {
let result = await response.json();
elResults.textContent = result.message + '<br>neue Id: ' + result.id;
elResults.classList.add('success');
} else {
elResults.textContent = 'Fehler! Status Code: ' + response.status;
elResults.classList.add('error');
}
}
submitAN();
});
});

View File

@@ -19,3 +19,6 @@ function geojson_to_searchable_string(location) {
return ifdef(location.properties.name, "", ", ") + ifdef(location.properties.street, "", ifdef(location.properties.housenumber, " ",", ")) + ifdef(location.properties.city, "", ", ") + ifdef(location.properties.country, "", "")
}
function truncate(str, n, url){
return (str.length > n) ? str.slice(0, n-1) + '<a href="' + url + '">&hellip;</a>' : str;
};

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
import PhotoSwipeLightbox from './photoswipe-lightbox.esm.js';
const lightbox = new PhotoSwipeLightbox({
gallery: '.gallery',
children: 'a',
pswpModule: () => import('https://unpkg.com/photoswipe'),
});
lightbox.init();

View File

@@ -0,0 +1,72 @@
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Add a click event on each of them
$navbarBurgers.forEach( el => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target;
const $target = document.getElementById(target);
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
});
});
});
// Looks for all notifications with a delete and allows closing them when pressing delete
document.addEventListener('DOMContentLoaded', () => {
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
const $notification = $delete.parentNode;
$delete.addEventListener('click', () => {
$notification.parentNode.removeChild($notification);
});
});
});
document.addEventListener('DOMContentLoaded', () => {
(document.querySelectorAll('.message .delete') || []).forEach(($delete) => {
$delete.addEventListener('click', () => {
const message = $delete.closest('.message');
if (message) {
message.remove();
}
});
});
// DROPDOWNS
const $clickableDropdowns = document.querySelectorAll(
".dropdown:not(.is-hoverable)",
);
if ($clickableDropdowns.length > 0) {
$clickableDropdowns.forEach(($dropdown) => {
if (!$dropdown.querySelector("button")) {
return;
}
$dropdown.querySelector("button").addEventListener("click", (event) => {
event.stopPropagation();
$dropdown.classList.toggle("is-active");
});
});
document.addEventListener("click", () => {
closeDropdowns();
});
}
function closeDropdowns() {
$clickableDropdowns.forEach(($el) => {
$el.classList.remove("is-active");
});
}
});

View File

@@ -0,0 +1,7 @@
User-agent: *
Disallow: /admin/
User-agent: OpenAI
Disallow: /
Sitemap: https://notfellchen.org/sitemap.xml

View File

@@ -5,9 +5,10 @@ from django.utils import timezone
from notfellchen.celery import app as celery_app
from .mail import send_notification_email
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices
from .tools.fedi import post_an_to_fedi
from .tools.misc import healthcheck_ok
from .models import Location, AdoptionNotice, Timestamp
from .tools.notifications import notify_moderators_of_AN_to_be_checked
from .models import Location, AdoptionNotice, Timestamp, RescueOrganization, SocialMediaPost
from .tools.notifications import notify_of_AN_to_be_checked
from .tools.search import notify_search_subscribers
@@ -38,6 +39,13 @@ def task_deactivate_unchecked():
set_timestamp("task_deactivate_404_adoption_notices")
@celery_app.task(name="social_media.post_fedi")
def task_post_to_fedi():
adoption_notice = SocialMediaPost.get_an_to_post()
post_an_to_fedi(adoption_notice)
set_timestamp("task_social_media.post_fedi")
@celery_app.task(name="commit.post_an_save")
def post_adoption_notice_save(pk):
instance = AdoptionNotice.objects.get(pk=pk)
@@ -46,7 +54,8 @@ def post_adoption_notice_save(pk):
logging.info(f"Location was added to Adoption notice {pk}")
notify_search_subscribers(instance, only_if_active=True)
notify_moderators_of_AN_to_be_checked(instance)
notify_of_AN_to_be_checked(instance)
@celery_app.task(name="tools.healthcheck")
def task_healthcheck():
@@ -57,3 +66,11 @@ def task_healthcheck():
@shared_task
def task_send_notification_email(notification_pk):
send_notification_email(notification_pk)
@celery_app.task(name="commit.post_rescue_org_save")
def post_rescue_org_save(pk):
instance = RescueOrganization.objects.get(pk=pk)
Location.add_location_to_object(instance)
set_timestamp("add_rescue_org_location")
logging.info(f"Location was added to Rescue Organization {pk}")

View File

@@ -1,55 +1,38 @@
{% extends "fellchensammlung/base_generic.html" %}
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load custom_tags %}
{% block title %}<title>{% translate "Über uns und Regeln" %}</title>{% endblock %}
{% block title %}<title>{% translate "Über uns" %}</title>{% endblock %}
{% block og_title %}
<meta property="og:title" content="{% translate "Über uns" %} - Notfellchen"/>
{% endblock %}
{% block description %}
<meta name="description" content="{% translate 'Erfahre mehr über das Notfellchen-Projekt. Im FAQ werden häufige Fragen beantwortet.' %}">
{% endblock %}
{% block og_description %}
<meta name="og:description" content="{% translate 'Erfahre mehr über das Notfellchen-Projekt. Im FAQ werden häufige Fragen beantwortet.' %}">
{% endblock %}
{% block canonical_url %}{% host %}{% url 'about' %}{% endblock %}
{% block content %}
{% if about_us %}
<div class="card">
<h1>{{ about_us.title }}</h1>
<p>
<div class="block">
<h1 class="title is-1">{{ about_us.title }}</h1>
<div class="content">
{{ about_us.content | render_markdown }}
</p>
</div>
</div>
{% endif %}
<h2>{% translate "Regeln" %}</h2>
{% include "fellchensammlung/lists/list-rules.html" %}
{% if faq %}
<div class="card">
<h2>{{ faq.title }}</h2>
<p>
<div class="card-header">
<h2 class="card-header-title">{{ faq.title }}</h2>
</div>
<div class="card-content">
{{ faq.content | render_markdown }}
</p>
</div>
{% endif %}
{% if privacy_statement %}
<div class="card">
<h2>{{ privacy_statement.title }}</h2>
<p>
{{ privacy_statement.content | render_markdown }}
</p>
</div>
{% endif %}
{% if terms_of_service %}
<div class="card">
<h2>{{ terms_of_service.title }}</h2>
<p>
{{ terms_of_service.content | render_markdown }}
</p>
</div>
{% endif %}
{% if imprint %}
<div class="card">
<h2>{{ imprint.title }}</h2>
<p>
{{ imprint.content | render_markdown }}
</p>
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -1,13 +1,55 @@
{% extends "fellchensammlung/base_generic.html" %}
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load custom_tags %}
{% block title %}<title>{% translate "Tierschutzorganisationen" %}</title>{% endblock %}
{% block og_title %}
<meta property="og:title" content="{% translate "Tierschutzorganisationen" %} - Notfellchen"/>
{% endblock %}
{% block description %}
<meta name="description" content="{% translate 'Finde Tierschutzorganisationen in deiner Gegend.' %}">
{% endblock %}
{% block og_description %}
<meta name="og:description" content="{% translate 'Finde Tierschutzorganisationen in deiner Gegend.' %}">
{% endblock %}
{% block canonical_url %}{% host %}{% url 'rescue-organizations' %}{% endblock %}
{% block content %}
<div class="container-cards">
<div class="card">
<div class="block">
<div style="height: 70vh">
{% include "fellchensammlung/partials/partial-map.html" %}
</div>
</div>
{% include "fellchensammlung/lists/list-animal-shelters.html" %}
<div class="block">
{% with rescue_organizations=rescue_organizations_to_list %}
{% include "fellchensammlung/lists/list-animal-shelters.html" %}
{% endwith %}
</div>
<nav class="pagination" role="navigation" aria-label="{% trans 'Paginierung' %}">
{% if rescue_organizations_to_list.has_previous %}
<a class="pagination-previous"
href="?page={{ rescue_organizations_to_list.previous_page_number }}">{% trans 'Vorherige' %}</a>
{% endif %}
{% if rescue_organizations_to_list.has_next %}
<a class="pagination-next" href="?page={{ rescue_organizations_to_list.next_page_number }}">{% trans 'Nächste' %}</a>
{% endif %}
<ul class="pagination-list">
{% for page in elided_page_range %}
{% if page != "…" %}
<li>
<a href="?page={{ page }}"
class="pagination-link {% if page == rescue_organizations_to_list.number %} is-current{% endif %}"
aria-label="{% blocktranslate %}Gehe zu Seite {{ page }}.{% endblocktranslate %}">
{{ page }}
</a>
</li>
{% else %}
<li>
<span aria-hidden="true" class="pagination-ellipsis">&hellip;</span>
</li>
{% endif %}
{% endfor %}
</ul>
</nav>
{% endblock %}

View File

@@ -1,20 +1,32 @@
{% load custom_tags %}
{% load i18n %}
{% load static %}
{% get_current_language as LANGUAGE_CODE %}
<!DOCTYPE html>
<html lang="en">
<html lang="{{ LANGUAGE_CODE }}">
<head>
{% block title %}{% endblock %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{% translate "Farbratten aus dem Tierschutz finden und adoptieren" %}">
{% block title %}{% endblock %}
{% block og_title %}{% endblock %}
{% block description %}{% endblock %}
{% block og_description %}{% endblock %}
{% block og_image %}{% endblock %}
<link rel="canonical" href="{% block canonical_url %}{% endblock %}">
<!-- Add additional CSS in static file -->
{% load static %}
<link rel="stylesheet" href="{% static 'fellchensammlung/css/styles.css' %}">
<link rel="stylesheet" href="{% static 'fellchensammlung/css/main.css' %}">
<link rel="stylesheet" href="{% static 'fellchensammlung/css/photoswipe.css' %}">
<link href="{% static 'fontawesomefree/css/fontawesome.css' %}" rel="stylesheet" type="text/css">
<link href="{% static 'fontawesomefree/css/brands.css' %}" rel="stylesheet" type="text/css">
<link href="{% static 'fontawesomefree/css/solid.css' %}" rel="stylesheet" type="text/css">
<script src="{% static 'fellchensammlung/js/custom.js' %}"></script>
<script src="{% static 'fellchensammlung/js/toggles.js' %}"></script>
<script src="{% static 'fellchensammlung/js/jquery.min.js' %}"></script>
<script type="module" src="{% static 'fellchensammlung/js/photoswipe.js' %}"></script>
{% block additional_scrips %}{% endblock %}
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'fellchensammlung/favicon/apple-touch-icon.png' %}">
<link rel="icon" type="image/png" sizes="32x32" href="{% static 'fellchensammlung/favicon/favicon-32x32.png' %}">
@@ -25,12 +37,13 @@
{% block header %}
{% include "fellchensammlung/header.html" %}
{% endblock %}
<div class="container-fluid">
<div class="row">
<div class="col-sm-10 content-box">
{% block content %}{% endblock %}
</div>
</div>
<div class="main-content">
{% block content %}{% endblock %}
</div>
{% block footer %}
{% include "fellchensammlung/footer.html" %}
{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,13 @@
{% for contact in contacts %}
BEGIN:VCARD
VERSION:4.0
N:{{contact.name|safe}};;;;
FN:{{contact.name|safe}}
{% if contact.location %}GEO:geo:{{contact.location.latitude}},{{contact.location.longitude}}
{% endif %}TEL;TYPE=work:{{ contact.phone_number }}
{% if contact.email %}EMAIL:{{ contact.email }}
{% endif %}REV:{{ current_time|date:'Ymd' }}T{{ current_time|date:'His' }}Z
CATEGORIES:{{ categories }}
{% if contact.website %}URL:{{ contact.website }}
{% endif %}END:VCARD
{% endfor %}

View File

@@ -0,0 +1,260 @@
{% extends "fellchensammlung/base.html" %}
{% load custom_tags %}
{% load admin_urls %}
{% load i18n %}
{% load static %}
{% block title %}<title>{{ adoption_notice.name }}</title>{% endblock %}
{% block og_title %}
<meta property="og:title" content="{{ adoption_notice.name }} - Notfellchen"/>
{% endblock %}
{% block description %}
<meta name="description" content="{{ adoption_notice.description }}">
{% endblock %}
{% block og_description %}
<meta name="og:description" content="{{ adoption_notice.description }}">
{% endblock %}
{% block canonical_url %}{% host %}
{% url 'adoption-notice-detail' adoption_notice_id=adoption_notice.id %}{% endblock %}
{% block og_image %}
{% if adoption_notice.get_photos %}
<meta property="og:image" content="{{ MEDIA_URL }}{{ adoption_notice.get_photos.0.image }}"/>
{% else %}
<meta property="og:image" content="{% static 'fellchensammlung/img/link_preview.png' %}"/>
{% endif %}
{% endblock %}
{% block content %}
{% if adoption_notice.is_closed %}
<article class="message is-warning">
<div class="message-header">
<p>{% translate 'Vermittlung deaktiviert' %}</p>
</div>
<div class="message-body content">
{% blocktranslate %}
Diese Vermittlung wurde deaktiviert. Typischerweise passiert das, wenn die Tiere erfolgreich
vermittelt wurden.
In den Kommentaren findest du ggf. mehr Informationen.
{% endblocktranslate %}
</div>
</article>
{% endif %}
<div class="columns">
<div class="column is-two-thirds">
<!--- Title level (including action dropdown) -->
<div class="level">
<div class="level-left">
<div class="level-item">
<p class="title is-3 is-size-4-mobile">{{ adoption_notice.name }}</p>
</div>
</div>
<div class="level-right">
<div class="level-item">
<div class="dropdown is-right">
<div class="dropdown-trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu4">
<span><i class="fas fa-gear" aria-label="{% trans 'Aktionen' %}"></i></span>
<span class="icon is-small">
<i class="fas fa-angle-down" aria-hidden="true"></i>
</span>
</button>
</div>
<!--- Action menu (dropdown) --->
<div class="dropdown-menu" role="menu">
<div class="dropdown-content">
{% if is_subscribed %}
<form class="dropdown-item" method="POST">
{% csrf_token %}
<input type="hidden" name="action" value="unsubscribe">
<button type="submit" id="submit">
<i class="fas fa-bell-slash fa-fw"
aria-hidden="true"></i> {% trans 'Deabonnieren' %}
</button>
</form>
{% else %}
<form class="dropdown-item" method="POST">
{% csrf_token %}
<input type="hidden" name="action" value="subscribe">
<button type="submit" id="submit">
<i class="fas fa-bell fa-fw"
aria-hidden="true"></i> {% trans 'Abonnieren' %}
</button>
</form>
{% endif %}
<hr class="dropdown-divider">
{% if has_edit_permission %}
<form class="dropdown-item" method="POST">
{% csrf_token %}
<input type="hidden" name="action" value="checked_active">
<button type="submit" id="submit">
<i class="fas fa-check fa-fw"
aria-hidden="true"></i> {% trans 'Als aktiv bestätigen' %}
</button>
</form>
<a class="dropdown-item"
href="{% url 'adoption-notice-edit' adoption_notice_id=adoption_notice.pk %}">
<i class="fas fa-pencil fa-fw"
aria-hidden="true"></i> {% translate 'Bearbeiten' %}
</a>
<a class="dropdown-item"
href="{% url 'adoption-notice-add-photo' adoption_notice.pk %}">
<i class="fas fa-image fa-fw"
aria-hidden="true"></i> {% trans 'Bilder hinzufügen' %}
</a>
<a class="dropdown-item"
href="{% url 'adoption-notice-add-animal' adoption_notice.pk %}">
<i class="fas fa-plus fa-fw"
aria-hidden="true"></i> {% trans 'Tier hinzufügen' %}
</a>
<a class="dropdown-item"
href="{% url 'adoption-notice-close' adoption_notice_id=adoption_notice.pk %}">
<i class="fas fa-circle-xmark fa-fw"
aria-hidden="true"></i> {% trans 'Deaktivieren' %}
</a>
<hr class="dropdown-divider">
{% endif %}
<a class="dropdown-item" href="{{ adoption_notice.get_report_url }}">
<i class="fas fa-flag"
aria-hidden="true"></i> {% trans 'Melden' %}
</a>
{% if request.user.is_superuser %}
<hr class="dropdown-divider">
<a class="dropdown-item is-warning"
href="{% url adoption_notice_meta|admin_urlname:'change' adoption_notice.pk %}">
<i class="fa-solid fa-tools fa-fw"></i> Admin interface
</a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<!--- General Information --->
<div class="grid">
<div class="cell">
<div class="grid">
{% if adoption_notice.organization %}
<div class="cell">
<span>
<i class="fa-solid fa-building fa-fw" aria-label="{% trans 'Tierschutzorganisation' %}"></i>
<a href="{{ adoption_notice.organization.get_absolute_url }}"> {{ adoption_notice.organization }}</a>
</span>
</div>
{% endif %}
<div class="cell">
<span>
<i class="fa-solid fa-location-dot fa-fw" aria-label="{% trans 'Ort' %}"></i>
{% if adoption_notice.location %}
{{ adoption_notice.location }}
{% else %}
{{ adoption_notice.location_string }}
{% endif %}
</span>
</div>
<div class="cell">
{% include "fellchensammlung/partials/sex-overview.html" %}
</div>
</div>
</div>
</div>
<!--- Images and Description --->
<div class="columns">
<!--- Images --->
{% if adoption_notice.get_photos %}
<div class="column block">
<div class="card">
<div class="grid card-content">
<div class="gallery">
{% with photo=adoption_notice.get_photos.0 %}
<div class="main-photo">
<a href="{{ MEDIA_URL }}{{ photo.image }}"
data-pswp-width="{{ photo.image.width }}"
data-pswp-height="{{ photo.image.height }}"
target="_blank">
<img src="{{ MEDIA_URL }}{{ photo.image }}"
alt="{{ photo.alt_text }}">
</a>
</div>
{% endwith %}
<div class="thumbnail-row">
{% for photo in adoption_notice.get_photos|slice:"1:4" %}
<div class="thumbnail">
<a href="{{ MEDIA_URL }}{{ photo.image }}"
data-pswp-width="{{ photo.image.width }}"
data-pswp-height="{{ photo.image.height }}"
target="_blank">
<img src="{{ MEDIA_URL }}{{ photo.image }}"
alt="{{ photo.alt_text }}">
</a>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!--- Description --->
<div class="column block">
<div class="card">
<div class="card-header">
<h1 class="card-header-title title is-4">{% translate "Beschreibung" %}</h1>
</div>
<div class="card-content">
<p class="expandable">{% if adoption_notice.description %}
{{ adoption_notice.description | render_markdown }}
{% else %}
{% translate "Keine Beschreibung angegeben" %}
{% endif %}
</p>
<hr>
<p>
<strong>
{% translate 'Zuletzt auf Aktualität überprüft:' %}
</strong>
{{ adoption_notice.last_checked|time_since_hr }}
</p>
</div>
</div>
</div>
</div>
<div class="block">
{% if adoption_notice.further_information %}
<form method="get" action="{% url 'external-site' %}">
<input type="hidden" name="url" value="{{ adoption_notice.further_information }}">
<button class="button is-primary is-fullwidth" type="submit" id="submit">
{{ adoption_notice.further_information | domain }} <i
class="fa-solid fa-arrow-up-right-from-square fa-fw"></i>
</button>
</form>
{% endif %}
</div>
</div>
<div class="column">
{% for animal in adoption_notice.animals %}
<div class="block">
{% include "fellchensammlung/partials/partial-animal-card.html" %}
</div>
{% endfor %}
</div>
</div>
<div class="block">
{% include "fellchensammlung/partials/partial-comment-section.html" %}
</div>
{% endblock %}

View File

@@ -2,7 +2,7 @@
{% load i18n %}
<div class="detail-animal"></div>
<div class="detail-animal-header">
<h1>{{ animal.name }}</h1>
<h1 class="title is-1">{{ animal.name }}</h1>
<div class="tag">{{ animal.species }}</div>
</div>
<div>
@@ -18,7 +18,7 @@
</table>
</div>
<p>{{ animal.description }}</p>
<h2>{% translate "Bilder" %}</h2>
<h2 class="title is-2">{% translate "Bilder" %}</h2>
<div class="photos">
{% for image in animal.get_photos %}
<div class="card-photo">
@@ -28,5 +28,4 @@
{% if not animal.get_photos %}
{% translate "Keine Bilder" %}
{% endif %}
</div>
</div>

View File

@@ -1,9 +1,9 @@
{% extends "fellchensammlung/base_generic.html" %}
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% block content %}
{% if form_complete %}
<h1>{% translate "Erfolgreich gemeldet" %}</h1>
<h1 class="title is-2">{% translate "Erfolgreich gemeldet" %}</h1>
{% blocktranslate %}
Wenn du sehen willst welche Moderationsentscheidungen getroffen werden, schau zu einem späteren Zeitpunkt
wieder auf dieser Seite vorbei.
@@ -11,8 +11,10 @@
<a href="mailto:info@notfellchen.org">info@notfellchen.org</a> Einspruch einlegen.
{% endblocktranslate %}
{% endif %}
<hr>
{% include "fellchensammlung/partials/partial-report.html" %}
<h2>{% translate "Moderationsverlauf" %}</h2>
<hr>
<h1 class="title is-1">{% translate "Moderationsverlauf" %}</h1>
{% if report.get_moderation_actions %}
{% include "fellchensammlung/lists/list-moderation-action.html" %}
{% else %}

View File

@@ -1,65 +1,94 @@
{% extends "fellchensammlung/base_generic.html" %}
{% extends "fellchensammlung/base.html" %}
{% load custom_tags %}
{% load i18n %}
{% load admin_urls %}
{% block title %}<title>{{ org.name }}</title>{% endblock %}
{% block og_title %}
<meta property="og:title" content="{{ org.name }} - Notfellchen"/>
{% endblock %}
{% block description %}
<meta name="description" content="{{ org.name }}: {% translate 'Kontaktdaten und Tiere zur Adoption' %}">
{% endblock %}
{% block og_description %}
<meta property="og:description" content="{{ org.name }}: {% translate 'Kontaktdaten und Tiere zur Adoption' %}">
{% endblock %}
{% block canonical_url %}{% host %}{% url 'rescue-organization-detail' rescue_organization_id=org.id %}{% endblock %}
{% block content %}
<div class="container-cards">
<div class="card half">
<h1>{{ org.name }}</h1>
<b><i class="fa-solid fa-location-dot"></i></b>
{% if org.location %}
{{ org.location.str_hr }}
{% else %}
{{ org.location_string }}
{% endif %}
<p>{{ org.description | render_markdown }}</p>
<table class="responsive">
<thead>
<tr>
{% if org.website %}
<td>{% translate "Website" %}</td>
{% endif %}
{% if org.phone_number %}
<td>{% translate "Telefonnummer" %}</td>
{% endif %}
{% if org.email %}
<td>{% translate "E-Mail" %}</td>
{% endif %}
</tr>
</thead>
<tr>
{% if org.website %}
<td data-label="{% trans 'Website' %} ">
{{ org.website }}
</td>
{% endif %}
{% if org.phone_number %}
<td data-label="{% trans 'Telefonnummer' %}">
{{ org.phone_number }}
</td>
{% endif %}
{% if org.email %}
<td data-label="{% trans 'E-Mail' %}">
{{ org.email }}
</td>
{% endif %}
</tr>
</table>
<div class="columns">
<div class="column">
<div class="block">
<div class="card">
<div class="card-header">
<h1 class="card-header-title">{{ org.name }}</h1>
</div>
<div class="card-content">
<div class="block">
<b><i class="fa-solid fa-location-dot"></i></b>
{% if org.location %}
{{ org.location }}
{% else %}
{{ org.location_string }}
{% endif %}
{% if org.description %}
<p>{{ org.description | render_markdown }}</p>
{% endif %}
</div>
{% if org.specializations %}
<div class="block">
<h3 class="title is-5">{% translate 'Spezialisierung' %}</h3>
<div class="content">
<ul>
{% for specialization in org.specializations.all %}
<li>{{ specialization }}</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% if org.parent_org %}
<div class="block">
<h3 class="title is-5">{% translate 'Übergeordnete Organisation' %}</h3>
<p>
<span>
<i class="fa-solid fa-building fa-fw" aria-label="{% trans 'Tierschutzorganisation' %}"></i>
<a href="{{ org.parent_org.get_absolute_url }}"> {{ org.parent_org }}</a>
</span>
</p>
</div>
{% endif %}
</div>
</div>
</div>
<div class="block">
{% include "fellchensammlung/partials/partial-rescue-organization-contact.html" %}
</div>
<div class="block">
<a class="button is-warning is-fullwidth" href="{% url org_meta|admin_urlname:'change' org.pk %}">
<i class="fa-solid fa-tools fa-fw"></i> Admin interface
</a>
</div>
</div>
<div class="card half">
<div class="column">
{% include "fellchensammlung/partials/partial-map.html" %}
</div>
</div>
{% if org.child_organizations %}
<div class="block">
<h2 class="title is-2">{% translate 'Unterorganisationen' %}</h2>
{% with rescue_organizations=org.child_organizations %}
{% include "fellchensammlung/lists/list-animal-shelters.html" %}
{% endwith %}
</div>
{% endif %}
<h2>{% translate 'Vermittlungen der Organisation' %}</h2>
<h2 class="title is-2">{% translate 'Vermittlungen der Organisation' %}</h2>
<div class="container-cards">
{% if org.adoption_notices %}
{% for adoption_notice in org.adoption_notices %}
{% if org.adoption_notices_in_hierarchy %}
{% for adoption_notice in org.adoption_notices_in_hierarchy %}
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
{% endfor %}
{% else %}

View File

@@ -1,83 +1,93 @@
{% extends "fellchensammlung/base_generic.html" %}
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% block title %}<title>{{ user.get_full_name }}</title>{% endblock %}
{% block content %}
<h1>{{ user.get_full_name }}</h1>
<div class="spaced">
<div class="container-cards">
<h2>{% trans 'Daten' %}</h2>
<div class="card">
<p><strong>{% translate "Username" %}:</strong> {{ user.username }}</p>
<p><strong>{% translate "E-Mail" %}:</strong> {{ user.email }}</p>
<div class="level is-mobile">
<div class="level-left">
<div class="level-item">
<h1 class="title is-1"><i class="fas fa-user"></i> {{ user.get_full_name }}</h1>
</div>
</div>
<div class="container-cards">
<h2>{% trans 'Profil verwalten' %}</h2>
<div class="container-comment-form">
<p>
<a class="btn2" href="{% url 'password_change' %}">{% translate "Change password" %}</a>
<a class="btn2" href="{% url 'user-me-export' %}">{% translate "Daten exportieren" %}</a>
</p>
</div>
</div>
{% if user.id is request.user.id %}
<div class="detail-animal-header"><h2>{% trans 'Einstellungen' %}</h2></div>
<div class="container-cards">
<form class="card" action="" method="POST">
<div class="level-right">
<div class="level-item">
<form class="" action="{% url 'logout' %}" method="post">
{% csrf_token %}
{% if user.email_notifications %}
<label class="toggle">
<input type="submit" class="toggle-checkbox checked" name="toggle_email_notifications">
<div class="toggle-switch round "></div>
<span class="slider-label">
{% translate 'E-Mail Benachrichtigungen' %}
</span>
</label>
{% else %}
<label class="toggle">
<input type="submit" class="toggle-checkbox" name="toggle_email_notifications">
<div class="toggle-switch round"></div>
<span class="slider-label">
{% translate 'E-Mail Benachrichtigungen' %}
</span>
</label>
{% endif %}
<button class="button" type="submit">
<i aria-hidden="true" class="fas fa-sign-out fa-fw"></i> Logout
</button>
</form>
<div class="card">
</div>
</div>
</div>
<div class="block">
<h2 class="title is-2">{% trans 'Profil verwalten' %}</h2>
<p><strong>{% translate "E-Mail" %}:</strong> {{ user.email }}</p>
<div class="">
<p>
<a class="button is-warning" href="{% url 'password_change' %}">{% translate "Change password" %}</a>
<a class="button is-info" href="{% url 'user-me-export' %}">{% translate "Daten exportieren" %}</a>
</p>
</div>
</div>
{% if user.id is request.user.id %}
<div class="block">
<h2 class="title is-2">{% trans 'Einstellungen' %}</h2>
<form class="block" action="" method="POST">
{% csrf_token %}
{% if user.email_notifications %}
<label class="toggle">
<input type="submit" class="toggle-checkbox checked" name="toggle_email_notifications">
<div class="toggle-switch round "></div>
<span class="slider-label">
{% translate 'E-Mail Benachrichtigungen' %}
</span>
</label>
{% else %}
<label class="toggle">
<input type="submit" class="toggle-checkbox" name="toggle_email_notifications">
<div class="toggle-switch round"></div>
<span class="slider-label">
{% translate 'E-Mail Benachrichtigungen' %}
</span>
</label>
{% endif %}
</form>
<details>
<summary><strong>{% trans 'Erweiterte Einstellungen' %}</strong></summary>
<div class="block">
{% if token %}
<form action="" method="POST">
{% csrf_token %}
<p class="text-muted"><strong>{% translate "API token:" %}</strong> {{ token }}</p>
<input class="btn" type="submit" name="delete_token"
<input class="button is-danger" type="submit" name="delete_token"
value={% translate "Delete API token" %}>
</form>
{% else %}
<p>{% translate "Kein API-Token vorhanden." %}</p>
<form action="" method="POST">
{% csrf_token %}
<input class="btn" type="submit" name="create_token"
<input class="button is-primary" type="submit" name="create_token"
value={% translate "Create API token" %}>
</form>
{% endif %}
</div>
</div>
</details>
<h2>{% translate 'Benachrichtigungen' %}</h2>
{% include "fellchensammlung/lists/list-notifications.html" %}
</div>
<h2>{% translate 'Abonnierte Suchen' %}</h2>
{% include "fellchensammlung/lists/list-search-subscriptions.html" %}
<h2 class="title is-2">{% translate 'Benachrichtigungen' %}</h2>
{% include "fellchensammlung/lists/list-notifications.html" %}
<h2>{% translate 'Meine Vermittlungen' %}</h2>
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
<h2 class="title is-2">{% translate 'Abonnierte Suchen' %}</h2>
{% include "fellchensammlung/lists/list-search-subscriptions.html" %}
{% endif %}
</div>
<h2 class="title is-2">{% translate 'Meine Vermittlungen' %}</h2>
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
{% endif %}
{% endblock %}

View File

@@ -1,133 +0,0 @@
{% extends "fellchensammlung/base_generic.html" %}
{% load custom_tags %}
{% load i18n %}
{% block title %}<title>{{ adoption_notice.name }}</title>{% endblock %}
{% block content %}
<div class="detail-adoption-notice-header">
<div class="inline-container">
<h1>{{ adoption_notice.name }}</h1>
{% if not is_subscribed %}
<div class="tooltip bottom">
<form class="notification-card-mark-read" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="subscribe">
<input type="hidden" name="adoption_notice_id" value="{{ adoption_notice.pk }}">
<button class="btn2" type="submit" id="submit"><i class="fa-solid fa-bell"></i></button>
</form>
<span class="tooltiptext">
{% translate 'Abonniere diese Vermittlung um bei Kommentaren oder Statusänderungen benachrichtigt zu werden' %}
</span>
</div>
{% else %}
<div class="tooltip bottom">
<form class="notification-card-mark-read" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="unsubscribe">
<input type="hidden" name="adoption_notice_id" value="{{ adoption_notice.pk }}">
<button class="btn2" type="submit" id="submit"><i class="fa-solid fa-bell-slash"></i></button>
</form>
<span class="tooltiptext">
{% translate 'Deabonnieren. Du bekommst keine Benachrichtigungen zu dieser Vermittlung mehr' %}
</span>
</div>
{% endif %}
{% if adoption_notice.is_active %}
<span id="submit" class="label active-adoption" style=>{% trans 'Aktive Vermittlung' %}</span>
{% else %}
<span id="submit" class="label inactive-adoption" style=>{% trans 'Vermittlung inaktiv' %}</span>
{% endif %}
</div>
{% if has_edit_permission %}
<a class="btn2"
href="{% url 'adoption-notice-add-photo' adoption_notice_id=adoption_notice.pk %}">{% translate 'Foto hinzufügen' %}</a>
<a class="btn2 detail-adoption-notice-header"
href="{% url 'adoption-notice-edit' adoption_notice_id=adoption_notice.pk %}">{% translate 'Bearbeiten' %}</a>
{% endif %}
</div>
<div class="table-adoption-notice-info">
<table class="responsive">
<thead>
<tr>
<td>{% translate "Ort" %}</td>
{% if adoption_notice.organization %}
<td>{% translate "Organisation" %}</td>
{% endif %}
<td>{% translate "Suchen seit" %}</td>
<td>{% translate "Zuletzt aktualisiert" %}</td>
<td>{% translate "Weitere Informationen" %}</td>
</tr>
</thead>
<tr>
<td data-label="{% trans 'Ort' %} ">
{% if adoption_notice.location %}
{{ adoption_notice.location }}
{% else %}
{{ adoption_notice.location_string }}
{% endif %}
</td>
{% if adoption_notice.organization %}
<td data-label="{% trans 'Organisation' %}">
<div>
<a href="{{ adoption_notice.organization.get_absolute_url }}">{{ adoption_notice.organization }}</a>
{% if adoption_notice.organization.trusted %}
<div class="tooltip top">
<div class="checkmark"><i class="fa-solid fa-check"></i></div>
<span class="tooltiptext">
{% translate 'Diese Organisation kennt sich mit Ratten aus und achtet auf gute Abgabebedingungen' %}
</span>
</div>
{% endif %}
</div>
</td>
{% endif %}
<td data-label="{% trans 'Suchen seit' %}">{{ adoption_notice.searching_since }}</td>
<td data-label="{% trans 'Zuletzt aktualisiert' %}">
{{ adoption_notice.last_checked_hr }}
</td>
<td data-label="{% trans 'Weitere Informationen' %}">
{% if adoption_notice.further_information %}
<form method="get" action="{% url 'external-site' %}">
<input type="hidden" name="url" value="{{ adoption_notice.further_information }}">
<button class="btn" type="submit" id="submit">
{{ adoption_notice.further_information | domain }} <i
class="fa-solid fa-arrow-up-right-from-square"></i>
</button>
</form>
{% else %}
-
{% endif %}
</td>
</tr>
</table>
</div>
<div class="card">
<h1>{% translate "Bilder" %}</h1>
{% for photo in adoption_notice.get_photos %}
<img src="{{ MEDIA_URL }}/{{ photo.image }}" alt="{{ photo.alt_text }}">
{% endfor %}
</div>
<div class="card">
<h1>{% translate "Beschreibung" %}</h1>
<p>{% if adoption_notice.description %}
{{ adoption_notice.description | render_markdown }}
{% else %}
{% translate "Keine Beschreibung angegeben" %}
{% endif %}
</p>
</div>
<div>
{% for animal in adoption_notice.animals %}
{% include "fellchensammlung/partials/partial-animal-card.html" %}
{% endfor %}
</div>
{% include "fellchensammlung/partials/partial-comment-section.html" %}
{% endblock %}

View File

@@ -1,9 +0,0 @@
{% extends "fellchensammlung/base_generic.html" %}
{% load custom_tags %}
{% load i18n %}
{% block title %}<title>{{ animal.name }}</title>{% endblock %}
{% block content %}
{% include "fellchensammlung/details/detail-animal-partial.html" %}
{% endblock %}

View File

@@ -1,11 +1,11 @@
{% extends "fellchensammlung/base_generic.html" %}
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load custom_tags %}
{% block title %}<title>{% translate "403 Forbidden" %}</title>{% endblock %}
{% block content %}
<h1>403 Forbidden</h1>
<h1 class="title is-1">403 Forbidden</h1>
<p>
{% blocktranslate %}
Diese Aktion ist dir nicht erlaubt. Logge dich ein oder nutze einen anderen Account. Wenn du denkst, dass hier

View File

@@ -0,0 +1,14 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load custom_tags %}
{% block title %}<title>{% translate "403 Forbidden" %}</title>{% endblock %}
{% block content %}
<h1 class="title is-1">404 Not Found</h1>
<p>
{% blocktranslate %}
Diese Seite existiert nicht.
{% endblocktranslate %}
</p>
{% endblock %}

View File

@@ -0,0 +1,28 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load custom_tags %}
{% block content %}
<div class="block">
<div class="message is-warning">
{% if external_site_warning %}
<h1 class="message-header">
{{ external_site_warning.title }}
</h1>
<div class="message-body">
{{ external_site_warning.content | render_markdown }}
</div>
{% else %}
<h1 class="message-header">
{% trans 'Achtung du verlässt notfellchen.org' %}
</h1>
<div class="message-body">
{% trans 'Sichere Abgabebedingungen können von uns, trotz vieler Bemühungen, nicht garantiert werden. Nimm Kontakt zu einer Rattenhilfe oder dem VdRD e.V. auf, die dich beraten können.' %}
</div>
{% endif %}
</div>
<div class="block">
<a href="{{ url }}" class="button is-primary is-fullwidth">{% translate "Weiter" %}<i class="fa fa-arrow-right fa-fw" aria-hidden="true"></i> </a>
</div>
</div>
{% endblock content %}

View File

@@ -1,15 +0,0 @@
{% extends "fellchensammlung/base_generic.html" %}
{% load i18n %}
{% load custom_tags %}
{% block content %}
<div class="card">
{% if external_site_warning %}
{{ external_site_warning.content | render_markdown }}
{% else %}
{% blocktranslate %}
<p>Achtung du verlässt notfellchen.org</p>
{% endblocktranslate %}
{% endif %}
<a href="{{ url }}" class="btn button">{% translate "Weiter" %}</a>
</div>
{% endblock content %}

View File

@@ -0,0 +1,109 @@
{% load static %}
{% load i18n %}
{% load custom_tags %}
<footer class="footer">
<div class="columns">
<div class="column">
<div class="block">
<h3 class="title is-3 has-text-left">
Notfellchen
</h3>
<!-- footer content -->
<p class="has-text-left">
Für Menschen die Ratten aus dem Tierschutz ein liebendes Zuhause geben wollen.
</p>
</div>
<div class="block">
<h3 class="title is-5">
{% trans 'Sprache ändern' %}
</h3>
{% include "fellchensammlung/forms/form_change_language.html" %}
</div>
</div>
<div class="column">
<h4 class="title is-4 has-text-justify">
{% translate 'Über uns' %}
</h4>
<a href="{% url "about" %}">
{% translate 'Das Notfellchen Projekt' %}
</a>
<br/>
<a href="{% url "buying" %}">
{% translate 'Ratten kaufen' %}
</a>
<br/>
<a href="{% url "terms-of-service" %}">
{% translate 'Nutzungsbedingungen' %}
</a>
<br/>
<a href="{% url "privacy" %}">
{% translate 'Datenschutz' %}
</a>
<br/>
<a href="{% url "imprint" %}">
{% translate 'Impressum' %}
</a>
<br/>
</div>
<div class="column">
<h4 class="title is-4 has-text-justify">
Technisches
</h4>
<div>
<a class="nav-link " href="{% url "rss" %}">
<i class="fa-solid fa-rss fa-fw"></i> {% translate 'RSS' %}
</a>
<br/>
<a href="https://dokumentation.notfellchen.org/">
<span class="icon-text">
<span>{% translate 'Dokumentation' %}</span>
</span>
</a>
<br/>
<a href="mailto:info@notfellchen.org">
<span class="icon-text">
<span>{% translate 'Probleme melden' %}</span>
</span>
</a>
<br/>
<a href="https://codeberg.org/moanos/notfellchen">
<span class="icon-text">
<span>{% trans 'Code' %}</span>
</span>
</a>
</div>
</div>
<div class="column">
<h4 class="title is-4 has-text-justify">
{% trans 'Hilfreiche Links' %}
</h4>
<a class="nav-link " href="{% url "rescue-organizations" %}">
{% translate 'Tierheime in der Nähe' %}
</a>
<br/>
{% trust_level "MODERATOR" as coordinator_trust_level %}
{% if request.user.trust_level >= coordinator_trust_level %}
<a class="nav-link " href="{% url "modtools" %}">
{% translate 'Moderationstools' %}
</a>
{% endif %}
<br/>
{% if request.user.is_superuser %}
<a class="nav-link " href="{% url "admin:index" %}">
<i class="fa-solid fa-screwdriver-wrench fa-fw"></i>{% translate 'Admin-Bereich' %}
</a>
{% endif %}
</div>
</div>
</footer>

View File

@@ -0,0 +1,138 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load widget_tweaks %}
{% load static %}
{% block title %}<title>{% translate "Vermittlung hinzufügen" %}</title>{% endblock %}
{% block additional_scrips %}
<script src="{% static 'fellchensammlung/js/adoption-notice-form.js' %}"></script>
{% endblock %}
{% block content %}
<h1 class="title is-1">{% translate "Vermitteln" %}</h1>
<div class="notification is-info">
<p>
{% url 'terms-of-service' as rules_url %}
{% trans "Regeln" as rules_text %}
{% blocktranslate with rules_link='<a href="'|add:rules_url|add:'">'|add:rules_text|add:'</a>'|safe %}
Bitte mach dich zunächst mit unseren {{ rules_link }} vertraut. Dann trage hier die ersten Informationen
ein.
Fotos kannst du im nächsten Schritt hinzufügen.
{% endblocktranslate %}
</p>
</div>
<div id="add-adoption-notice-form">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="field">
<label class="label" for="an-name">{{ form.name.label }}
{% if form.name.field.required %}<span class="special_class">*</span>{% endif %}</label>
{{ form.name|add_class:"input"|attr:"id:an-name" }}
</div>
<div class="field">
<label class="label" for="an-description">
{% translate 'Beschreibung' %}
{% if form.description.field.required %}<span class="special_class">*</span>{% endif %}
</label>
{{ form.description|add_class:"input textarea"|attr:"rows:3"|attr:"id:an-description" }}
</div>
<div class="field">
<label class="label" for="an-location">
{{ form.location_string.label }}
{% if form.location_string.field.required %}<span class="special_class">*</span>{% endif %}
</label>
{{ form.location_string|add_class:"input"|attr:"id:an-location" }}
</div>
<div class="field">
<label class="checkbox" for="an-group-only">
{{ form.group_only.label }}
{% if form.group_only.field.required %}<span class="special_class">*</span>{% endif %}
</label>
{{ form.group_only|add_class:"checkbox"|attr:"id:an-group-only" }}
</div>
<div class="field">
<label class="label" for="an-searching-since">
{{ form.searching_since.label }}
{% if form.searching_since.field.required %}<span class="special_class">*</span>{% endif %}
</label>
{{ form.searching_since|add_class:"input"|attr:"id:an-searching-since"|attr:"type:date" }}
</div>
<div class="field">
<label class="label" for="an-further-information">
{{ form.further_information.label }}
{% if form.further_information.field.required %}<span class="special_class">*</span>{% endif %}
</label>
{{ form.further_information|add_class:"input"|attr:"id:an-further-information"|attr:"type:url" }}
<div class="help">
{{ form.further_information.help_text }}
</div>
</div>
<div class="notification is-info">
<p>
{% blocktranslate %}
Gibt hier schonmal erste Details zu den Tieren an.
Wenn du Details und Fotos zu den Tieren hinzufügen willst oder ihr Geschlecht und Geburtsdatum
anpassen
willst,
kannst du das im nächsten Schritt tun.
{% endblocktranslate %}
</p>
</div>
<div class="field">
<label class="label" for="an-species">
{% translate 'Tierart' %}
{% if form.species.field.required %}<span class="special_class">*</span>{% endif %}
</label>
<div class="select">
{{ form.species|attr:"id:an-species" }}
</div>
</div>
<div class="field">
<label class="label" for="an-num-animals">
{{ form.num_animals.label }}
{% if form.num_animals.field.required %}<span class="special_class">*</span>
</label>
{{ form.num_animals|add_class:"input"|attr:"id:an-num-animals" }}{% endif %}
</div>
<div class="field">
<label class="label" for="an-sex">
{% translate 'Geschlecht' %}
{% if form.sex.field.required %}<span class="special_class">*</span>{% endif %}
</label>
<div class="select">
{{ form.sex|attr:"id:an-sex" }}
</div>
</div>
<div class="field">
<label class="label" for="an-date-of-birth">
{{ form.date_of_birth.label }}
{% if form.date_of_birth.field.required %}<span class="special_class">*</span>{% endif %}
</label>
{{ form.date_of_birth|add_class:"input"|attr:"id:an-date-of-birth"|attr:"type:date" }}
</div>
<input class="button is-primary" type="submit" value="{% translate "Speichern" %}">
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,17 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% block content %}
<h1 class="title is-1">{% translate "Tiere hinzufügen" %}</h1>
{% blocktranslate %}
Hier kannst du jetzt einzelne Tiere zu deiner Vermittlung hinzufügen.
{% endblocktranslate %}
<form method="POST">
{% csrf_token %}
{{ form }}
<input class="button is-primary is-light" type="submit" value="{% translate "Speichern" %}">
<input class="button is-primary" type="submit" name="save-and-add-another-animal" value="{% translate "Speichern und weiteres Tier hinzufügen" %}">
</form>
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load widget_tweaks %}
{% block content %}
<h1>{{ adoption_notice }}</h1>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
<input class="button is-primary" type="submit" value="{% translate "Speichern" %}">
</form>
{% endblock %}

View File

@@ -1,8 +0,0 @@
{% extends "fellchensammlung/base_generic.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block content %}
<h1>{% translate "Vermittlungsanzeige" %}</h1>
{% crispy form %}
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load widget_tweaks %}
{% block content %}
<h1 class="title is-1">{{ animal }}</h1>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
<input class="button is-primary" type="submit" value="{% translate "Speichern" %}">
<a class="button is-danger" href="{% url 'animal-delete' animal_id=animal.pk %}">{% translate "Löschen" %}</a>
</form>
{% endblock %}

View File

@@ -1,13 +1,19 @@
{% load i18n %}
{% load crispy_forms_tags %}
<div class="container-comment-form">
<p>
<b>
{% blocktrans %}
Als <i>{{ user }}</i> kommentieren
{% endblocktrans %}
</b>
{% crispy comment_form %}
</p>
<div class="card">
<div class="card-header">
<div class="card-header-title">
{% blocktrans %}
Als {{ user }} kommentieren
{% endblocktrans %}
</div>
</div>
<div class="card-content">
<form method="POST">
{% csrf_token %}
<input type="hidden" name="action" value="comment">
{{ comment_form }}
<input type="submit" class="button is-primary" value="{% trans 'Kommentieren' %}">
</form>
</div>
</div>

View File

@@ -0,0 +1,17 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load widget_tweaks %}
{% block title %}
<title>Löschen von {{ animal }} bestätigen</title>
{% endblock %}
{% block content %}
<h1 class="title is-1">Löschen von {{ animal }} bestätigen</h1>
<form method="post">
{% csrf_token %}
<a class="button" href="{% url 'animal-edit' animal_id=animal.pk %}">{% translate "Zurück (nicht löschen)" %}</a>
<input class="button is-danger" type="submit" name="delete" value={% translate "Löschen" %}>
</form>
{% endblock %}

View File

@@ -1,17 +1,29 @@
{% extends "fellchensammlung/base_generic.html" %}
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load widget_tweaks %}
{% block content %}
<p>
<div>
{% blocktranslate %}
Lade hier ein Foto hoch - wähle den Titel wie du willst und mach bitte eine Bildbeschreibung,
damit die Fotos auch für blinde und sehbehinderte Personen zugänglich sind.
{% endblocktranslate %}
<p><a class="btn2" href="https://www.dbsv.org/bildbeschreibung-4-regeln.html">{% translate 'Anleitung für Bildbeschreibungen' %}</a></p>
</p>
<div class="container-form">
{% crispy form %}
<p><a class="button" target="_blank"
href="https://www.dbsv.org/bildbeschreibung-4-regeln.html">{% translate 'Anleitung für Bildbeschreibungen' %}</a>
</p>
</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="field">
<label class="label" for="image">{{ form.image.label }}</label>
{{ form.image|add_class:"input"|attr:"id:image" }}
</div>
<div class="field">
<label class="label" for="alt-text">{{ form.alt_text.label }}</label>
{{ form.alt_text|add_class:"textarea"|attr:"id:alt-text"|attr:"rows:3" }}
<div class="is-danger">{{ form.alt_text.errors }}</div>
</div>
<input class="button is-primary" type="submit" value="{% translate "Speichern" %}">
</form>
{% endblock %}

View File

@@ -1,12 +1,12 @@
{% extends "fellchensammlung/base_generic.html" %}
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% block content %}
<h1>{% translate "Melden" %}</h1>
Wenn diese Vermittlung nicht unseren <a href='{% url "about" %}'>Regeln</a> entspricht, wähle bitte eine der folgenden Regeln aus und hinterlasse einen Kommentar der es detaillierter erklärt, insbesondere wenn der Regelverstoß nicht offensichtlich ist.
<h1 class="title is-1">{% translate "Melden" %}</h1>
Wenn dieser Inhalt nicht unseren <a href='{% url "about" %}'>Regeln</a> entspricht, wähle bitte eine der folgenden Regeln aus und hinterlasse einen Kommentar der es detaillierter erklärt, insbesondere wenn der Regelverstoß nicht offensichtlich ist.
<form method = "post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button class="btn2" type="submit">{% translate "Melden" %}</button>
{{ form }}
<button class="button is-primary" type="submit">{% translate "Melden" %}</button>
</form>
{% endblock %}

View File

@@ -1,17 +0,0 @@
{% extends "fellchensammlung/base_generic.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}<title>{% translate "Vermittlung hinzufügen" %}</title>{% endblock %}
{% block content %}
<h1>{% translate "Vermitteln" %}</h1>
<p>
{% blocktranslate %}
Bitte mach dich zunächst mit unseren Regeln vertraut. Dann trage hier die ersten Informationen ein.
Wenn du Details und Fotos zu den Tieren hinzufügen willst oder ihr Geschlecht und Geburtsadatum anpassen willst,
kannst du das im nächsten Schritt tun.
{% endblocktranslate %}
</p>
{% crispy form %}
{% endblock %}

View File

@@ -1,13 +0,0 @@
{% extends "fellchensammlung/base_generic.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block content %}
<h1>{% translate "Vermitteln" %}</h1>
{% blocktranslate %}
Hier kannst du jetzt einzelne Tiere zu deiner Vermittlung hinzufügen. Lad auch gerne Fotos hoch. Gruppenfotos
kannst
du im nächsten Schritt hochladen.
{% endblocktranslate %}
{% crispy form %}
{% endblock %}

View File

@@ -1,5 +1,5 @@
{% load i18n %}
<form class="btn2" action="{% url 'change-language' %}" method="post" onchange='this.form.submit()'>
<form class="btn2 select" action="{% url 'change-language' %}" method="post" onchange='this.form.submit()'>
{% csrf_token %}
<select name="language" onchange='this.form.submit()'>
{% get_current_language as LANGUAGE_CODE_CURRENT %}

Some files were not shown because too many files have changed in this diff Show More