Compare commits

...

282 Commits

Author SHA1 Message Date
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
160 changed files with 6448 additions and 5379 deletions

7
.gitignore vendored
View File

@@ -3,10 +3,16 @@
# Database
notfellchen
# Geojson from imports
*.geojson
# Media storage
/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,7 @@ docker push moanos/notfellchen:latest
docker run -p8000:7345 moanos/notfellchen:latest
```
## Testing
Tests can be run with
@@ -144,17 +185,17 @@ Start beat
# Contributing
This project is currently solely developed by me, moanos. I'd like that to change and will be very happy for contributions
This project is currently mainly developed by me, moanos. I'd like that to change and will be very happy for contributions
and shared responsibilities. Some ideas where you can look for contributing first
* CSS structure: It's a hot mess right now, and I'm happy it somehow works. As you might see, there is much room for improvement. Refactoring this and streamlining the look across the app would be amazing.
* UI improvements: Since a major redesign I'm much happier but the UI could use many, many little tweaks
* Docker: If you know how to build a docker container that is a) smaller or b) utilizes staged builds this would be amazing. Any improvement welcome
* Testing: Writing tests is always welcome, and it's likely you discover a few bugs
I'm also very happy for all other contributions. Before you do large refactoring efforts or features, best write a short
issue for it before you spend a lot of work.
Send PRs either to [codeberg](https://codeberg.org/moanos/notfellchen) (preferred) or [Github](https://github.com/moan0s/notfellchen).
CI (currently only for dcumentation) runs via [git.hyteck.de](https://git.hyteck.de), you can also ask moanos for an account there.
Send PRs either to [codeberg](https://codeberg.org/moanos/notfellchen) (preferred) or [GitHub](https://github.com/moan0s/notfellchen).
CI (currently only for documentation) runs via [git.hyteck.de](https://git.hyteck.de), you can also ask moanos for an account there.
Also welcome are new issues with suggestions or bugs and additions to the documentation.

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,9 +29,9 @@ An application can then send this token in the request header for authorization.
.. warning::
Usage or creation of content still has to follow the terms of Notfellchen.org
Usage or creation of content still has to follow the terms of notfellchen.org.
Copyright of content is often held by rescue organizations, so you are not allowed to simply mirror content.
Talk to the Notfellchen-Team if you want develop such things.
Talk to the notfellchen team if you want develop such things.
Endpoints
@@ -45,7 +46,8 @@ Get Adoption Notices
++++++++++++++++++++
.. code-block::
curl --request GET \
curl --request GET \
--url https://notfellchen.org/api/adoption_notice \
--header 'Authorization: {{token}}'
@@ -53,7 +55,8 @@ Create Adoption Notice
++++++++++++++++++++++
.. code-block::
curl --request POST \
curl --request POST \
--url https://notfellchen.org/api/adoption_notice \
--header 'Authorization: {{token}}' \
--header 'content-type: multipart/form-data' \
@@ -68,6 +71,7 @@ Add Animal to Adoption Notice
+++++++++++++++++++++++++++++
.. code-block::
curl --request POST \
--url https://notfellchen.org/api/animals/ \
--header 'Authorization: {{token}}' \
@@ -83,6 +87,7 @@ Add picture to Animal or Adoption Notice
++++++++++++++++++++++++++++++++++++++++
.. code-block::
curl -X POST https://notfellchen.org/api/images/ \
-H "Authorization: Token {{token}}" \
-F "image=@256-256-crop.jpg" \
@@ -96,6 +101,7 @@ Species
Getting available species is mainly important when creating animals
.. code-block::
curl --request GET \
--url https://notfellchen.org/api/species \
--header 'Authorization: {{token}}'

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

@@ -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,14 +6,14 @@ build-backend = "setuptools.build_meta"
[project]
name = "notfellchen"
description = "A tool to help."
description = "A website to help animals to find a loving home. It features organized input of adoption notices and related animals including automated lifecycle, location-based search, roles, and support for easy checking of rescue organizations."
authors = [
{ name = "moanos", email = "julian-samuel@gebuehr.net" },
]
maintainers = [
{ name = "moanos", email = "julian-samuel@gebuehr.net" },
]
keywords = ["animal", "adoption", "django", "rescue", ]
keywords = ["animal", "adoption", "django", "rescue", "rats" ]
license = { text = "AGPL-3.0-or-later" }
classifiers = [
"Environment :: Web",
@@ -25,8 +25,6 @@ classifiers = [
dependencies = [
"Django",
"codecov",
"sphinx",
"sphinx-rtd-theme",
"gunicorn",
"fontawesomefree",
"whitenoise",
@@ -38,7 +36,9 @@ dependencies = [
"crispy-bootstrap4",
"djangorestframework",
"celery[redis]",
"drf-spectacular[sidecar]"
"drf-spectacular[sidecar]",
"django-widget-tweaks",
"django-super-deduper"
]
dynamic = ["version", "readme"]
@@ -49,6 +49,11 @@ develop = [
"coverage",
"model_bakery",
]
docs = [
"sphinx",
"sphinx-rtd-theme",
"sphinx-autobuild"
]
[project.urls]
homepage = "https://notfellchen.org"

View File

@@ -1,18 +1,29 @@
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="Upload animal shelter data to the Notfellchen API.")
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()
@@ -21,16 +32,26 @@ def get_config():
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, instance, data_file
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:
@@ -68,37 +89,174 @@ def https(value):
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:
print(
f"Location for {tierheim["properties"]["name"]}:{location_result.status_code} {location_result.json()} not created")
return location_result.json()
def main():
api_token, instance, data_file = get_config()
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"}
with open(data_file, encoding="utf8") as f:
d = json.load(f)
for idx, tierheim in tqdm(enumerate(d["features"])):
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
data = {"name": tierheim["properties"]["name"],
"location_string": f"{get_or_none(tierheim, "addr:street")} {get_or_none(tierheim, "addr:housenumber")}, {get_or_none(tierheim, "addr:postcode")} {tierheim["properties"]["addr:city"]}",
"phone_number": choose(("contact:phone", "phone"), tierheim["properties"], replace=True),
"fediverse_profile": get_or_none(tierheim, "contact:mastodon"),
"facebook": https(add(get_or_none(tierheim, "contact:facebook"), "facebook")),
"instagram": https(add(get_or_none(tierheim, "contact:instagram"), "instagram")),
"website": https(choose(("contact:website", "website"), tierheim["properties"])),
"email": choose(("contact:email", "email"), tierheim["properties"]),
"description": get_or_none(tierheim, "opening_hours"),
"external_object_identifier": f"{tierheim["id"]}",
"external_source_identifier": "OSM"
}
# 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",
)
result = requests.post(endpoint, json=data, headers=h)
# Define here for later
optional_data = ["email", "phone_number", "website", "description", "fediverse_profile", "facebook",
"instagram"]
if result.status_code != 201:
print(f"{idx} {tierheim["properties"]["name"]}:{result.status_code} {result.json()}")
# 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__":

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, ImportantLocation
SpeciesSpecificURL, ImportantLocation, SpeciesSpecialization
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 _
@@ -99,13 +100,19 @@ class SpeciesSpecificURLInline(admin.StackedInline):
model = SpeciesSpecificURL
class SpeciesSpecializationInline(admin.StackedInline):
model = SpeciesSpecialization
extra = 0
@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 = [
SpeciesSpecializationInline,
SpeciesSpecificURLInline,
]
@@ -120,9 +127,9 @@ 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)

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,5 +1,6 @@
from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image, Location
from rest_framework import serializers
import math
class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
@@ -32,19 +33,95 @@ class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
"group_only", "location", "location_details", "organization", "photos"]
class AdoptionNoticeGeoJSONSerializer(serializers.ModelSerializer):
species = serializers.SerializerMethodField()
title = serializers.CharField(source='name')
url = serializers.SerializerMethodField()
location_hr = serializers.SerializerMethodField()
coordinates = serializers.SerializerMethodField()
image_url = serializers.SerializerMethodField()
image_alt = serializers.SerializerMethodField()
class Meta:
model = AdoptionNotice
fields = ('id', 'species', 'title', 'description', 'url', 'location_hr', 'coordinates', 'image_url',
'image_alt')
def get_species(self, obj):
return "rat"
def get_url(self, obj):
return obj.get_absolute_url()
def get_image_url(self, obj):
photo = obj.get_photo()
if photo is not None:
return obj.get_photo().image.url
return None
def get_image_alt(self, obj):
photo = obj.get_photo()
if photo is not None:
return obj.get_photo().alt_text
return None
def get_coordinates(self, obj):
"""
Coordinates are randomly moved around real location, roughly in a circle. The object id is used as angle so that
points are always displayed at the same location (as if they were a seed for a random function).
It's not exactly a circle, because the earth is round.
"""
if obj.location:
longitude_addition = math.sin(obj.id) / 2000
latitude_addition = math.cos(obj.id) / 2000
return [obj.location.longitude + longitude_addition, obj.location.latitude + latitude_addition]
return None
def get_location_hr(self, obj):
if obj.location:
return f"{obj.location}"
return None
class RescueOrgeGeoJSONSerializer(serializers.ModelSerializer):
name = serializers.CharField()
url = serializers.SerializerMethodField()
location_hr = serializers.SerializerMethodField()
coordinates = serializers.SerializerMethodField()
class Meta:
model = AdoptionNotice
fields = ('id', 'name', 'description', 'url', 'location_hr', 'coordinates')
def get_url(self, obj):
return obj.get_absolute_url()
def get_coordinates(self, obj):
"""
Coordinates are randomly moved around real location, roughly in a circle. The object id is used as angle so that
points are always displayed at the same location (as if they were a seed for a random function).
It's not exactly a circle, because the earth is round.
"""
if obj.location:
return [obj.location.longitude, obj.location.latitude]
return None
def get_location_hr(self, obj):
if obj.location.city:
return f"{obj.location.city}"
elif obj.location:
return f"{obj.location}"
return None
class AnimalCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Animal
fields = ["name", "date_of_birth", "description", "species", "sex", "adoption_notice"]
class 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:
model = Animal

View File

@@ -1,15 +1,18 @@
from django.urls import path
from .views import (
AdoptionNoticeApiView,
AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView, LocationApiView
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"),

View File

@@ -1,23 +1,26 @@
from django.db.models import Q
from rest_framework.generics import ListAPIView
from fellchensammlung.api.serializers import LocationSerializer
from fellchensammlung.api.serializers import LocationSerializer, AdoptionNoticeGeoJSONSerializer
from rest_framework.views import APIView
from rest_framework.response import Response
from django.db import transaction
from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel, Location
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
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):
@@ -63,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
@@ -91,6 +94,9 @@ 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.
@@ -107,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):
"""
@@ -114,7 +130,7 @@ 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,
@@ -197,24 +213,27 @@ class RescueOrganizationApiView(APIView):
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,
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()
# Add the location
post_rescue_org_save.delay_on_commit(rescue_org.pk)
if rescue_org.location is None:
# Add the location
post_rescue_org_save.delay_on_commit(rescue_org.pk)
return Response(
{"message": "Rescue organization created successfully!", "id": rescue_org.id},
status=status.HTTP_201_CREATED,
@@ -224,14 +243,14 @@ class RescueOrganizationApiView(APIView):
@transaction.atomic
@extend_schema(
request=RescueOrgSerializer,
request=RescueOrganizationSerializer,
responses={200: 'Rescue organization updated successfully!'}
)
def patch(self, request, *args, **kwargs):
"""
Partially update a rescue organization.
"""
org_id = kwargs.get("id")
org_id = request.data.get("id")
if not org_id:
return Response({"error": "ID is required for updating."}, status=status.HTTP_400_BAD_REQUEST)
@@ -240,13 +259,14 @@ class RescueOrganizationApiView(APIView):
except RescueOrganization.DoesNotExist:
return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
serializer = RescueOrgSerializer(organization, data=request.data, partial=True, context={"request": request})
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]
@@ -269,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},
@@ -292,6 +312,7 @@ class SpeciesApiView(APIView):
serializer = SpeciesSerializer(species, many=True, context={"request": request})
return Response(serializer.data, status=status.HTTP_200_OK)
class LocationApiView(APIView):
permission_classes = [IsAuthenticated]
@@ -339,9 +360,9 @@ class LocationApiView(APIView):
# Log the action
Log.objects.create(
user=request.user,
user=request.user_to_notify,
action="add_location",
text=f"{request.user} added adoption notice {location.pk} via API",
text=f"{request.user_to_notify} added adoption notice {location.pk} via API",
)
# Return success response with new adoption notice details
@@ -351,3 +372,14 @@ class LocationApiView(APIView):
)
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
@@ -22,105 +22,39 @@ class DateInput(forms.DateInput):
input_type = 'date'
class BulmaAdoptionNoticeForm(forms.ModelForm):
class AdoptionNoticeForm(forms.ModelForm):
template_name = "fellchensammlung/forms/form_snippets.html"
class Meta:
model = AdoptionNotice
fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string",
"organization"]
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)
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):
@@ -156,30 +90,39 @@ class ImageForm(forms.ModelForm):
class ReportAdoptionNoticeForm(forms.ModelForm):
template_name = "fellchensammlung/forms/form_snippets.html"
class Meta:
model = ReportAdoptionNotice
fields = ('reported_broken_rules', 'user_comment')
class ReportCommentForm(forms.ModelForm):
template_name = "fellchensammlung/forms/form_snippets.html"
class Meta:
model = ReportComment
fields = ('reported_broken_rules', 'user_comment')
class CommentForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_class = 'form-comments'
self.helper.add_input(Hidden('action', 'comment'))
self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="button is-primary"))
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
@@ -190,17 +133,11 @@ class CustomRegistrationForm(RegistrationForm):
class Meta(RegistrationForm.Meta):
model = User
template_name = "fellchensammlung/forms/form_snippets.html"
captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_(
"Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um sicherzustellen, dass du kein Roboter bist."))
def __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"))
class AdoptionNoticeSearchForm(forms.Form):
template_name = "fellchensammlung/forms/form_snippets.html"

View File

@@ -1,43 +1,60 @@
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
from fellchensammlung.models import User, Notification, TrustLevel, NotificationTypeChoices
from notfellchen.settings import base_url
NEWLINE = "\r\n"
def mail_admins_new_report(report):
subject = _("Neue Meldung")
"""
Sends an e-mail to all users that should handle the report.
"""
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
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}")
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 = base_url + report.get_absolute_url()
context = {"report_url": report_url,
"user_comment": report.user_comment, }
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()
subject = _("Neue Meldung")
html_message = render_to_string('fellchensammlung/mail/notifications/report.html', context)
plain_message = strip_tags(html_message)
mail.send_mail(subject,
plain_message,
from_email="info@notfellchen.org",
recipient_list=[moderator.email],
html_message=html_message)
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, }
if notification.notification_type == NotificationTypeChoices.NEW_REPORT_COMMENT or notification.notification_type == NotificationTypeChoices.NEW_REPORT_AN:
context["user_comment"] = notification.report.user_comment
context["report_url"] = f"{base_url}{notification.report.get_absolute_url()}"
html_message = render_to_string('fellchensammlung/mail/notifications/report.html', context)
elif notification.notification_type == NotificationTypeChoices.NEW_USER:
html_message = render_to_string('fellchensammlung/mail/notifications/new-user.html', context)
elif notification.notification_type == NotificationTypeChoices.AN_IS_TO_BE_CHECKED:
html_message = render_to_string('fellchensammlung/mail/notifications/an-to-be-checked.html', context)
elif notification.notification_type == NotificationTypeChoices.AN_WAS_DEACTIVATED:
html_message = render_to_string('fellchensammlung/mail/notifications/an-deactivated.html', context)
elif notification.notification_type == NotificationTypeChoices.AN_FOR_SEARCH_FOUND:
html_message = render_to_string('fellchensammlung/mail/notifications/an-for-search-found.html', context)
elif notification.notification_type == NotificationTypeChoices.NEW_COMMENT:
html_message = render_to_string('fellchensammlung/mail/notifications/new-comment.html', context)
else:
raise NotImplementedError("Unknown notification type")
plain_message = strip_tags(html_message)
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,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

@@ -11,9 +11,10 @@ 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
@@ -61,16 +62,12 @@ class Location(models.Model):
if self.city and self.postcode:
return f"{self.city} ({self.postcode})"
else:
return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
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:
@@ -131,7 +128,7 @@ class RescueOrganization(models.Model):
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'))
@@ -149,10 +146,19 @@ 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"))
parent_org = models.ForeignKey("RescueOrganization", on_delete=models.PROTECT, blank=True, null=True)
class Meta:
unique_together = ('external_object_identifier', 'external_source_identifier',)
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)])
@@ -173,6 +179,8 @@ class RescueOrganization(models.Model):
return ""
if len(self.description) > 200:
return self.description[:200] + _(f" ... [weiterlesen]({self.get_absolute_url()})")
else:
return self.description
def set_checked(self):
self.last_checked = timezone.now()
@@ -185,7 +193,18 @@ class RescueOrganization(models.Model):
@property
def species_urls(self):
return SpeciesSpecificURL.objects.filter(organization=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()
# Admins can perform all actions and have the highest trust associated with them
@@ -237,10 +256,10 @@ class User(AbstractUser):
return self.get_absolute_url()
def get_unread_notifications(self):
return BaseNotification.objects.filter(user=self, read=False)
return Notification.objects.filter(user=self, read=False)
def get_num_unread_notifications(self):
return BaseNotification.objects.filter(user=self, read=False).count()
return Notification.objects.filter(user=self, read=False).count()
@property
def adoption_notices(self):
@@ -303,7 +322,9 @@ class AdoptionNotice(models.Model):
verbose_name=_('Organisation'))
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"))
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"))
@@ -368,9 +389,9 @@ 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_absolute_url_bulma(self):
"""Returns the url to access a detailed page for the adoption notice."""
return reverse('adoption-notice-detail-bulma', 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."""
@@ -428,13 +449,6 @@ 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'):
@@ -469,7 +483,11 @@ 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)
class AdoptionNoticeStatus(models.Model):
@@ -892,20 +910,48 @@ class Comment(models.Model):
return self.adoption_notice.get_absolute_url()
class BaseNotification(models.Model):
class NotificationTypeChoices(models.TextChoices):
NEW_USER = "new_user", _("Useraccount wurde erstellt")
NEW_REPORT_AN = "new_report_an", _("Vermittlung wurde gemeldet")
NEW_REPORT_COMMENT = "new_report_comment", _("Kommentar wurde gemeldet")
AN_IS_TO_BE_CHECKED = "an_is_to_be_checked", _("Vermittlung muss überprüft werden")
AN_WAS_DEACTIVATED = "an_was_deactivated", _("Vermittlung wurde deaktiviert")
AN_FOR_SEARCH_FOUND = "an_for_search_found", _("Vermittlung für Suche gefunden")
NEW_COMMENT = "new_comment", _("Neuer Kommentar")
class Notification(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
read_at = models.DateTimeField(blank=True, null=True, verbose_name=_("Gelesen am"))
notification_type = models.CharField(max_length=200,
choices=NotificationTypeChoices.choices,
verbose_name=_('Benachrichtigungsgrund'))
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'))
user_to_notify = models.ForeignKey(User,
on_delete=models.CASCADE,
verbose_name=_('Nutzer*in'),
help_text=_("Useraccount der Benachrichtigt wird"),
related_name='user')
read = models.BooleanField(default=False)
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
@@ -913,22 +959,6 @@ class BaseNotification(models.Model):
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
class Subscriptions(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
@@ -961,7 +991,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}"
@@ -970,6 +1000,18 @@ 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 SpeciesSpecialization(models.Model):
"""
Model that allows to specify if a rescue organization has a specialization for dedicated species
"""
species = models.ForeignKey(Species, on_delete=models.CASCADE, verbose_name=_("Tierart"))
rescue_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE,
verbose_name=_("Tierschutzorganisation"))
def __str__(self):
return f"{_('Spezialisierung')} {self.species}"

View File

@@ -1,23 +1,20 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from fellchensammlung.models import BaseNotification, CommentNotification, User, TrustLevel, RescueOrganization
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=BaseNotification)
def base_notification_receiver(sender, instance: BaseNotification, created: bool, **kwargs):
if not created or not instance.user.email_notifications:
@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=RescueOrganization)
def rescue_org_receiver(sender, instance: RescueOrganization, created: bool, **kwargs):
if instance.location:
@@ -40,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

@@ -1,60 +0,0 @@
/***************/
/* MAIN COLORS */
/***************/
:root {
--primary-light-one: #5daa68;
--primary-light-two: #4a9455;
--primary-semidark-one: #356c3c;
--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);
--highlight-two: var(--primary-semidark-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);
}
.content {
padding: 10px;
}
/*******/
/* 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');
!important;
}
.maplibregl-popup {
max-width: 600px !important;
}
.map-in-content #map {
max-height: 500px;
width: 90%;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,322 @@
$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);
}

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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 B

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

View File

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

View File

@@ -29,4 +29,44 @@ document.addEventListener('DOMContentLoaded', () => {
$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

@@ -48,6 +48,7 @@ def post_adoption_notice_save(pk):
notify_search_subscribers(instance, only_if_active=True)
notify_of_AN_to_be_checked(instance)
@celery_app.task(name="tools.healthcheck")
def task_healthcheck():
healthcheck_ok()
@@ -58,9 +59,10 @@ def task_healthcheck():
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}")
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,25 +1,31 @@
{% load custom_tags %}
{% load i18n %}
{% load static %}
{% get_current_language as LANGUAGE_CODE%}
{% get_current_language as LANGUAGE_CODE %}
<!DOCTYPE html>
<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 -->
<link rel="stylesheet" href="{% static 'fellchensammlung/css/bulma-styles.css' %}">
<link rel="stylesheet" href="{% static 'fellchensammlung/css/bulma.min.css' %}">
<link rel="stylesheet" href="https://unpkg.com/photoswipe@5.2.2/dist/photoswipe.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' %}">
@@ -29,15 +35,15 @@
</head>
<body>
{% block header %}
{% include "fellchensammlung/bulma-header.html" %}
{% include "fellchensammlung/header.html" %}
{% endblock %}
<div class="content">
<div class="main-content">
{% block content %}{% endblock %}
</div>
{% block footer %}
{% include "fellchensammlung/bulma-footer.html" %}
{% include "fellchensammlung/footer.html" %}
{% endblock %}
</body>
</html>

View File

@@ -1,37 +0,0 @@
{% load custom_tags %}
{% load i18n %}
{% get_current_language as LANGUAGE_CODE%}
<!DOCTYPE html>
<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" %}">
<!-- Add additional CSS in static file -->
{% load static %}
<link rel="stylesheet" href="{% static 'fellchensammlung/css/styles.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>
<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' %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% static 'fellchensammlung/favicon/favicon-16x16.png' %}">
{% get_oxitraffic_script_if_enabled %}
</head>
<body>
{% 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>
</body>
</html>

View File

@@ -1,27 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% load custom_tags %}
{% block title %}<title>{% translate "Über uns" %}</title>{% endblock %}
{% block content %}
{% if about_us %}
<div class="block">
<h1 class="title is-1">{{ about_us.title }}</h1>
<div class="content">
{{ about_us.content | render_markdown }}
</div>
</div>
{% endif %}
{% if faq %}
<div class="card">
<div class="card-header">
<h2 class="card-header-title">{{ faq.title }}</h2>
</div>
<div class="card-content">
{{ faq.content | render_markdown }}
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -1,43 +0,0 @@
{% load static %}
{% load i18n %}
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="{% url 'index-bulma' %}">
<img src="{% static 'fellchensammlung/img/logo_transparent.png' %}" alt="{% trans 'Notfellchen Logo' %}">
<h1 class="title is-4">notfellchen.org</h1>
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="{% url 'search-bulma' %}">
<i class="fas fa-search"></i> {% translate 'Suchen' %}
</a>
<a class="navbar-item" href="{% url "add-adoption-bulma" %}">
<i class="fas fa-feather"></i> {% translate 'Vermittlung hinzufügen' %}
</a>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<a class="button is-primary" href="{% url "django_registration_register" %}">
<strong>{% translate "Registrieren" %}</strong>
</a>
<a class="button is-light" href="{% url "login" %}">
<strong>{% translate "Login" %}</strong>
</a>
</div>
</div>
</div>
</div>
</div>
</nav>

View File

@@ -1,34 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% load custom_tags %}
{% block title %}<title>{% translate "Notfellchen - Farbratten aus dem Tierschutz adoptieren" %}</title>{% endblock %}
{% block content %}
{% for announcement in announcements %}
{% include "fellchensammlung/partials/bulma-partial-announcement.html" %}
{% endfor %}
{% if introduction %}
<h1>{{ introduction.title }}</h1>
{{ introduction.content | render_markdown }}
{% endif %}
<h2>{% translate "Aktuelle Vermittlungen" %}</h2>
<div class="block">
{% include "fellchensammlung/lists/bulma-list-adoption-notices.html" %}
<a class="button is-primary" href="{% url 'search' %}">{% translate "Mehr Vermittlungen" %}</a>
</div>
<div class="block" style="height: 50vh">
{% include "fellchensammlung/partials/bulma-partial-map.html" %}
</div>
{% if how_to %}
<div class="card">
<h1>{{ how_to.title }}</h1>
{{ how_to.content | render_markdown }}
</div>
{% endif %}
{% endblock %}

View File

@@ -1,9 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% block title %}<title>{% translate "Karte" %}</title>{% endblock %}
{% block content %}
<div style="height:70vh">
{% include "fellchensammlung/partials/bulma-partial-map.html" %}
</div>
{% endblock %}

View File

@@ -1,97 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% block title %}<title>{% translate "Suche" %}</title>{% endblock %}
{% block content %}
{% get_current_language as LANGUAGE_CODE_CURRENT %}
<div class="columns">
<div class="column is-two-thirds">
<div style="height: 50vh">
{% include "fellchensammlung/partials/bulma-partial-map.html" %}
</div>
</div>
<div class="column">
<form class="block" method="post">
{% csrf_token %}
<input type="hidden" name="longitude" maxlength="200" id="longitude">
<input type="hidden" name="latitude" maxlength="200" id="latitude">
<input type="hidden" id="place_id" name="place_id">
<!--- https://docs.djangoproject.com/en/5.2/topics/forms/#reusable-form-templates -->
{{ search_form }}
<ul id="results"></ul>
<button class="button is-primary" type="submit" value="search" name="search">
<i class="fas fa-search"></i> {% trans 'Suchen' %}
</button>
{% if searched %}
{% if subscribed_search %}
<button class="button" type="submit" value="{{ subscribed_search.pk }}"
name="unsubscribe_to_search">
<i class="fas fa-bell-slash"></i> {% trans 'Suche nicht mehr abonnieren' %}
</button>
{% else %}
<button class="button" type="submit" name="subscribe_to_search">
<i class="fas fa-bell"></i> {% trans 'Suche abonnieren' %}
</button>
{% endif %}
{% endif %}
</form>
<div class="block">
{% if place_not_found %}
<div class="block notification is-warning">
<p>
{% trans 'Ort nicht gefunden' %}
</p>
</div>
{% endif %}
</div>
</div>
</div>
<div class="">
{% include "fellchensammlung/lists/bulma-list-adoption-notices.html" %}
</div>
<script>
const locationInput = document.getElementById('id_location_string');
const resultsList = document.getElementById('results');
const placeIdInput = document.getElementById('place_id');
locationInput.addEventListener('input', async function () {
const query = locationInput.value.trim();
if (query.length < 3) {
resultsList.innerHTML = ''; // Don't search for or show results if input is less than 3 characters
return;
}
try {
const response = await fetch(`{{ geocoding_api_url }}/?q=${encodeURIComponent(query)}&limit=5&lang={{ LANGUAGE_CODE_CURRENT }}`);
const data = await response.json();
if (data && data.features) {
resultsList.innerHTML = ''; // Clear previous results
const locations = data.features.slice(0, 5); // Show only the first 5 results
locations.forEach(location => {
const listItem = document.createElement('li');
listItem.classList.add('result-item');
listItem.textContent = geojson_to_summary(location);
// Add event when user clicks on a result location
listItem.addEventListener('click', () => {
locationInput.value = geojson_to_searchable_string(location); // Set input field to selected location
resultsList.innerHTML = ''; // Clear the results after selecting a location
});
resultsList.appendChild(listItem);
});
}
} catch (error) {
console.error('Error fetching location data:', error);
resultsList.innerHTML = '<li class="result-item">Error fetching data. Please try again.</li>';
}
});
</script>
{% endblock %}

View File

@@ -1,120 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% load static %}
{% block title %}<title>{% translate "Styleguide für Bulma" %}</title>{% endblock %}
{% block content %}
<section class="section">
<div class="container">
<h1 class="title">
Hello World
</h1>
<p class="subtitle">
Notfellchen bald mit <strong>Bulma</strong>?
</p>
</div>
<div class="grid">
<div class="card">
<div class="card-image">
<figure class="image">
<img
src="{% static 'fellchensammlung/img/example_rat_single.png' %}"
alt="Placeholder image"
/>
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img
src="https://bulma.io/assets/images/placeholders/96x96.png"
alt="Placeholder image"
/>
</figure>
</div>
<div class="media-content">
<p class="title is-4">John Smith</p>
<p class="subtitle is-6">@johnsmith</p>
</div>
</div>
<div class="content">
Süße Ratte sucht Zuhause
<a href="#">#responsive</a>
<br/>
<time datetime="2016-1-1">11:09 PM - 1 Jan 2016</time>
</div>
</div>
</div>
<div class="card">
<div class="card-image">
<figure class="image">
<img
src="{% static 'fellchensammlung/img/example_rat_single.png' %}"
alt="Placeholder image"
/>
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img
src="https://bulma.io/assets/images/placeholders/96x96.png"
alt="Placeholder image"
/>
</figure>
</div>
<div class="media-content">
<p class="title is-4">John Smith</p>
<p class="subtitle is-6">@johnsmith</p>
</div>
</div>
<div class="content">
Süßeste Ratte sucht Zuhause
<a href="#">#responsive</a>
<br/>
<time datetime="2016-1-1">11:09 PM - 1 Jan 2016</time>
</div>
</div>
</div>
<div class="card">
<div class="card-image">
<figure class="image">
<img
src="{% static 'fellchensammlung/img/example_rat_single.png' %}"
alt="Placeholder image"
/>
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img
src="https://bulma.io/assets/images/placeholders/96x96.png"
alt="Placeholder image"
/>
</figure>
</div>
<div class="media-content">
<p class="title is-4">John Smith</p>
<p class="subtitle is-6">@johnsmith</p>
</div>
</div>
<div class="content">
Süßere Ratte sucht Zuhause
<a href="#">#responsive</a>
<br/>
<time datetime="2016-1-1">11:09 PM - 1 Jan 2016</time>
</div>
</div>
</div>
</div>
</section>
{% endblock %}

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

@@ -1,113 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load custom_tags %}
{% load i18n %}
{% load static %}
{% block title %}<title>{{ adoption_notice.name }}</title>{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h1 class="card-header-title title is-2">{{ adoption_notice.name }}</h1>
</div>
<div class="card-content">
<div class="grid">
<div class="cell">
<!--- General Information --->
<div class="grid">
<div class="cell">
<h2><strong>{% translate "Ort" %}</strong></h2>
<p>{% if adoption_notice.location %}
{{ adoption_notice.location }}
{% else %}
{{ adoption_notice.location_string }}
{% endif %}</p>
</div>
<div class="cell">
{% include "fellchensammlung/partials/bulma-sex-overview.html" %}
</div>
</div>
</div>
</div>
<div class="columns">
<!--- Images --->
<div class="column block">
<div class="card">
<div class="grid card-content">
<div class="cell" id="my-gallery">
{% for photo in adoption_notice.get_photos %}
<a href="{{ MEDIA_URL }}/{{ photo.image }}"
data-pswp-width="{{ photo.image.width }}"
data-pswp-height="{{ photo.image.height }}"
target="_blank">
<img style="height: 12rem" src="{{ MEDIA_URL }}/{{ photo.image }}"
alt="{ photo.alt_text }}"/>
</a>
{% endfor %}
</div>
</div>
</div>
</div>
<!--- Description --->
<div class="column block">
<div class="card">
<div class="card-header">
<h1 class="card-header-title title is-2">{% 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>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer">
{% if has_edit_permission %}
<div class="card-footer-item">
<div class="column">
<a class="button is-primary is-light"
href="{% url 'adoption-notice-add-photo' adoption_notice_id=adoption_notice.pk %}">
{% translate 'Foto hinzufügen' %}
</a>
</div>
<div class="card-footer-item">
<a class="button is-primary"
href="{% url 'adoption-notice-edit' adoption_notice_id=adoption_notice.pk %}">
{% translate 'Bearbeiten' %}
</a>
</div>
</div>
{% endif %}
</div>
</div>
<div class="columns">
{% for animal in adoption_notice.animals %}
<div class="column">
{% include "fellchensammlung/partials/bulma-partial-animal-card.html" %}
</div>
{% endfor %}
</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"></i>
</button>
</form>
{% endif %}
</div>
<div class="block">
{% include "fellchensammlung/partials/bulma-partial-comment-section.html" %}
</div>
{% endblock %}

View File

@@ -0,0 +1,202 @@
{% extends "fellchensammlung/base.html" %}
{% load custom_tags %}
{% 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 %}
<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 has_edit_permission %}
<a class="dropdown-item">
<i class="fas fa-check"
aria-hidden="true"></i> {% trans 'Als aktiv bestätigen' %}
</a>
<a class="dropdown-item"
href="{% url 'adoption-notice-edit' adoption_notice_id=adoption_notice.pk %}">
<i class="fas fa-pencil"
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"
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"
aria-hidden="true"></i> {% trans 'Tier hinzufügen' %}
</a>
<a class="dropdown-item">
<i class="fas fa-circle-xmark"
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>
</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>
</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,62 +1,57 @@
{% 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>
</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>
<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 %}

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

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

View File

@@ -1,61 +1,62 @@
{% load static %}
{% load i18n %}
{% load custom_tags %}
<footer class="footer">
<div class="columns">
<div class="column">
<div class="block">
<h3 class="bd-footer-title title is-3 has-text-left">
<h3 class="title is-3 has-text-left">
Notfellchen
</h3>
<!-- footer content -->
<p class="bd-footer-link
has-text-left">
<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="bd-footer-title title is-5">
<h3 class="title is-5">
{% trans 'Sprache ändern' %}
</h3>
{% include "fellchensammlung/forms/change_language.html" %}
{% include "fellchensammlung/forms/form_change_language.html" %}
</div>
</div>
<div class="column">
<h4 class="bd-footer-title title is-4 has-text-justify">
<h4 class="title is-4 has-text-justify">
{% translate 'Über uns' %}
</h4>
<a class="bd-footer-link" href="{% url "about-bulma" %}">
<a href="{% url "about" %}">
{% translate 'Das Notfellchen Projekt' %}
</a>
<br/>
<a class="bd-footer-link" href="{% url "terms-of-service" %}">
<a href="{% url "terms-of-service" %}">
{% translate 'Nutzungsbedingungen' %}
</a>
<br/>
<a class="bd-footer-link" href="{% url "privacy" %}">
<a href="{% url "privacy" %}">
{% translate 'Datenschutz' %}
</a>
<br/>
<a class="bd-footer-link" href="{% url "imprint" %}">
<a href="{% url "imprint" %}">
{% translate 'Impressum' %}
</a>
<br/>
</div>
<div class="column">
<h4 class="bd-footer-title title is-4 has-text-justify">
<h4 class="title is-4 has-text-justify">
Technisches
</h4>
<p class="bd-footer-link">
<div>
<a class="nav-link " href="{% url "rss" %}">
<i class="fa-solid fa-rss"></i> {% translate 'RSS' %}
<i class="fa-solid fa-rss fa-fw"></i> {% translate 'RSS' %}
</a>
<br/>
@@ -77,7 +78,28 @@
<span>{% trans 'Code' %}</span>
</span>
</a>
</p>
</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

@@ -1,95 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load widget_tweaks %}
{% block title %}<title>{% translate "Vermittlung hinzufügen" %}</title>{% endblock %}
{% block content %}
<h1>{% translate "Vermitteln" %}</h1>
<div class="notification">
<button class="delete"></button>
<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>
<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' %}</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 }}</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 }}</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 }}</label>
{{ form.searching_since|add_class:"input"|attr:"id:an-searching-since"|attr:"type:date" }}
</div>
<div class="notification">
<button class="delete"></button>
<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' %}</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 }}</label>
{{ form.num_animals|add_class:"input"|attr:"id:an-num-animals" }}
</div>
<div class="field">
<label class="label" for="an-sex">{% translate 'Geschlecht' %}</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 }}</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>
{% endblock %}

View File

@@ -1,15 +0,0 @@
{% load i18n %}
{% load crispy_forms_tags %}
<div class="card">
<div class="card-header">
<div class="card-header-title">
{% blocktrans %}
Als {{ user }} kommentieren
{% endblocktrans %}
</div>
</div>
<div class="card-content">
{% crispy comment_form %}
</div>
</div>

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,23 +1,35 @@
<!--- See https://docs.djangoproject.com/en/5.2/topics/forms/#reusable-form-templates -->
{% load custom_tags %}
{% load widget_tweaks %}
{% for field in form %}
<div class="field">
<label class="label">
{{ field.label }}
</label>
<div class="control">
{% if field|widget_type == 'TextInput' %}
{{ field|add_class:"input" }}
{% elif field|widget_type == 'Select' %}
{% if field|widget_type == 'Select' %}
<div class="select">
{{ field }}
</div>
{% elif field|widget_type == 'selectmultiple' %}
<div class="select is-multiple is-fullwidth">
{{ field }}
</div>
{% elif field|widget_type == 'dateinput' %}
{{ field|add_class:"input"|attr:"type:date" }}
{% elif field|widget_type == 'textarea' %}
{{ field|add_class:"input textarea"|attr:"rows:3" }}
{% elif field|widget_type == 'checkboxinput' %}
{{ field|add_class:"checkbox" }}
{% else %}
{{ field|add_class:"input" }}
{% endif %}
</div>
<div class="help">
{{ field.help_text }}
</div>
<div class="help is-danger">
{{ field.errors }}
</div>

View File

@@ -1,59 +1,53 @@
{% load static %}
{% load i18n %}
<section class="header">
<div>
<a href="{% url "index" %}" class="logo">
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" tabindex="0" href="{% url 'index' %}">
<img src="{% static 'fellchensammlung/img/logo_transparent.png' %}" alt="{% trans 'Notfellchen Logo' %}">
<h1 class="title is-4">notfellchen.org</h1>
</a>
<a role="button" class="navbar-burger" aria-label="{% trans 'Hauptmenü' %}" tabindex="0" aria-expanded="false" data-target="navbarBasicExample">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div class="profile-card">
<div id="header-change-language">
{% include "fellchensammlung/forms/change_language.html" %}
<div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="{% url 'search' %}">
<i class="fas fa-search fa-fw"></i> {% translate 'Suchen' %}
</a>
<a class="navbar-item" href="{% url "add-adoption" %}">
<i class="fas fa-feather fa-fw"></i> {% translate 'Vermittlung hinzufügen' %}
</a>
</div>
{% if user.is_authenticated %}
<div class="btn2 button_darken btn-notification">
<a href="{{ user.get_notifications_url }}">
<i class="fa fa-bell" aria-hidden="true"></i>
</a>
{% if user.get_num_unread_notifications > 0 %}
<span class="button__badge">{{ user.get_num_unread_notifications }}</span>
{% endif %}
</div>
<div class="navbar-end">
{% if user.is_authenticated %}
<a class="btn2" href="{% url 'user-me' %}"><i aria-hidden="true" class="fas fa-user"></i></a>
<form class="btn2 button_darken" id="header-sign-out" action="{% url 'logout' %}" method="post">
{% csrf_token %}
<button class="button" type="submit"><i aria-hidden="true" class="fas fa-sign-out"></i></button>
</form>
{% else %}
<a class="btn2" href="{% url "django_registration_register" %}">{% translate "Registrieren" %}</a>
<a class="btn2" href="{% url "login" %}"><i class="fa fa-sign-in" aria-label="Login"></i></a>
{% endif %}
<input id="menu-toggle" type="checkbox"/>
<label class='menu-button-container' for="menu-toggle">
<div class='menu-button'></div>
</label>
<nav id="main-menu">
<ul class="menu">
<li>
<a class="nav-link " href="{% url "search" %}">
<i class="fas fa-search"></i> {% translate 'Suchen' %}
<div class="navbar-item">
<a href="{% url 'user-me' %}">
<i class="fas fa-user fa-fw"></i> {{ user }}
</a>
</li>
<li><a class="nav-link " href="{% url "add-adoption" %}"><i
class="fas fa-feather"></i> {% translate 'Vermittlung hinzufügen' %}</a></li>
<li><a class="nav-link " href="{% url "about" %}"><i
class="fas fa-info"></i> {% translate 'Über uns' %}
</a>
</li>
<li><a class="nav-link " href="{% url "rss" %}"><i
class="fa-solid fa-rss"></i> {% translate 'RSS' %}
</a>
</li>
</ul>
</nav>
</div>
{% else %}
<div class="navbar-item">
<div class="buttons">
<a class="button is-primary" href="{% url "django_registration_register" %}">
<strong>{% translate "Registrieren" %}</strong>
</a>
<a class="button is-light" href="{% url "login" %}">
<strong>{% translate "Login" %}</strong>
</a>
</div>
</div>
{% endif %}
</div>
</div>
</section>
</nav>

View File

@@ -1,8 +1,21 @@
{% extends "fellchensammlung/base_generic.html" %}
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load custom_tags %}
{% load static %}
{% block title %}<title>{% translate "Notfellchen - Farbratten aus dem Tierschutz adoptieren" %}</title>{% endblock %}
{% block og_title %}
<meta property="og:title"
content="{% translate "Notfellchen - Farbratten aus dem Tierschutz adoptieren" %}"/>{% endblock %}
{% block description %}
<meta name="description"
content="{% translate "notfellchen.org listet Farbratten in Tierheimen und Pflegestellen die ein Zuhause suchen. Adoptieren statt kaufen!" %}">{% endblock %}
{% block og_description %}
<meta name="og:description"
content="{% translate "notfellchen.org listet Farbratten in Tierheimen und Pflegestellen die ein Zuhause suchen. Adoptieren statt kaufen!" %}">{% endblock %}
{% block og_image %}
<meta property="og:image" content="{% host %}{% static 'fellchensammlung/img/link_preview.png' %}"/>{% endblock %}
{% block canonical_url %}{% host %}{% url 'index' %}{% endblock %}
{% block content %}
{% for announcement in announcements %}
@@ -13,24 +26,27 @@
{{ introduction.content | render_markdown }}
{% endif %}
<h2>{% translate "Aktuelle Vermittlungen" %}</h2>
<h2 class="title is-2">{% translate "Aktuelle Vermittlungen" %}</h2>
<div class="container-cards">
<div class="block">
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
<a class="btn2" href="{% url 'search' %}">{% translate "Mehr Vermittlungen" %}</a>
<a class="button is-primary" href="{% url 'search' %}">{% translate "Mehr Vermittlungen" %}</a>
</div>
<div class="map-in-content">
<div class="card">
<h1>{% translate 'Karte' %}</h1>
{% include "fellchensammlung/partials/partial-map.html" %}
</div>
<div class="block" style="height: 50vh">
{% include "fellchensammlung/partials/partial-map.html" %}
</div>
{% if how_to %}
<div class="card">
<h1>{{ how_to.title }}</h1>
{{ how_to.content | render_markdown }}
<div class="card-header">
<div class="card-header-title">
<h2 class="title is-1">{{ how_to.title }}</h2>
</div>
</div>
<div class="card-content">
{{ how_to.content | render_markdown }}
</div>
</div>
{% endif %}

View File

@@ -1,12 +1,11 @@
{% extends "fellchensammlung/base_generic.html" %}
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% block title %}<title>{% translate "Instanz-Check" %}</title> {% endblock %}
{% block content %}
<div class="card">
<h1>{% translate "Instanz-Check" %}</h1>
{% if missing_texts|length > 0 %}
<h2>{% trans "Fehlende Texte" %}</h2>
<p>
<h1 class="title is-1">{% translate "Instanz-Check" %}</h1>
{% if missing_texts|length > 0 %}
<h2 class="title is-2">{% trans "Fehlende Texte" %}</h2>
<div class="block">
<table>
<tr>
<th>{% translate "Text Code" %}</th>
@@ -19,14 +18,14 @@
</tr>
{% endfor %}
</table>
</p>
{% else %}
<p>{% translate "Texte scheinen vollständig" %}</p>
{% endif %}
</div>
{% else %}
<p>{% translate "Texte scheinen vollständig" %}</p>
{% endif %}
<h2>{% trans "Zeitstempel" %}</h2>
{% if timestamps|length > 0 %}
<p>
<h2 class="title is-2">{% trans "Zeitstempel" %}</h2>
{% if timestamps|length > 0 %}
<div class="block">
<table>
<tr>
<th>{% translate "Key" %}</th>
@@ -41,81 +40,102 @@
</tr>
{% endfor %}
</table>
</p>
{% else %}
<p>{% translate "Keine Zeitstempel geloggt." %}</p>
{% endif %}
</div>
{% else %}
<p>{% translate "Keine Zeitstempel geloggt." %}</p>
{% endif %}
<h2>{% translate "Nicht-lokalisierte Vermittlungen" %}</h2>
{% if number_not_geocoded_adoption_notices > 0 %}
<details>
<summary>{{ number_not_geocoded_adoption_notices }}/{{ number_of_adoption_notices }}</summary>
<ul>
{% for adoption_notice in none_geocoded_adoption_notices %}
<li>
<a href="{{ adoption_notice.get_absolute_url }}">{{ adoption_notice.name }}</a>
</li>
{% endfor %}
</ul>
</details>
{% else %}
<p>{{ number_not_geocoded_adoption_notices }}/{{ number_of_adoption_notices }}</p>
{% endif %}
<h2 class="title is-2">{% translate "Nicht-lokalisierte Vermittlungen" %}</h2>
{% if number_not_geocoded_adoption_notices > 0 %}
<details>
<summary>{{ number_not_geocoded_adoption_notices }}/{{ number_of_adoption_notices }}</summary>
<ul>
{% for adoption_notice in none_geocoded_adoption_notices %}
<li>
<a href="{{ adoption_notice.get_absolute_url }}">{{ adoption_notice.name }}</a>
</li>
{% endfor %}
</ul>
</details>
{% else %}
<p>{{ number_not_geocoded_adoption_notices }}/{{ number_of_adoption_notices }}</p>
{% endif %}
<h2>{% translate "Nicht-lokalisierte Tierschutzorganisationen" %}</h2>
{% if number_not_geocoded_rescue_orgs > 0 %}
<details>
<summary>{{ number_not_geocoded_rescue_orgs }}/{{ number_of_rescue_orgs }}</summary>
<ul>
{% for rescue_org in none_geocoded_rescue_orgs %}
<li>
<a href="{{ rescue_org.get_absolute_url }}">{{ rescue_org.name }}</a>
</li>
{% endfor %}
</ul>
</details>
{% else %}
<p>{{ number_not_geocoded_rescue_orgs }}/{{ number_of_rescue_orgs }}</p>
{% endif %}
<h2 class="title is-2">{% translate "Nicht-lokalisierte Tierschutzorganisationen" %}</h2>
{% if number_not_geocoded_rescue_orgs > 0 %}
<details>
<summary>{{ number_not_geocoded_rescue_orgs }}/{{ number_of_rescue_orgs }}</summary>
<ul>
{% for rescue_org in none_geocoded_rescue_orgs %}
<li>
<a href="{{ rescue_org.get_absolute_url }}">{{ rescue_org.name }}</a>
</li>
{% endfor %}
</ul>
</details>
{% else %}
<p>{{ number_not_geocoded_rescue_orgs }}/{{ number_of_rescue_orgs }}</p>
{% endif %}
<h2>{% translate "Nicht-geprüfte Vermittlungen" %}</h2>
{% if number_unchecked_ans > 0 %}
<details>
<summary>{{ number_unchecked_ans }}</summary>
<ul>
{% for unchecked_an in unchecked_ans %}
<li>
<a href="{{ unchecked_an.get_absolute_url }}">{{ unchecked_an.name }}</a>
</li>
{% endfor %}
</ul>
</details>
{% else %}
<p>{{ number_unchecked_ans }}</p>
{% endif %}
<h2 class="title is-2">{% translate "Nicht-geprüfte Vermittlungen" %}</h2>
{% if number_unchecked_ans > 0 %}
<details>
<summary>{{ number_unchecked_ans }}</summary>
<ul>
{% for unchecked_an in unchecked_ans %}
<li>
<a href="{{ unchecked_an.get_absolute_url }}">{{ unchecked_an.name }}</a>
</li>
{% endfor %}
</ul>
</details>
{% else %}
<p>{{ number_unchecked_ans }}</p>
{% endif %}
<form class="notification-card-mark-read" method="post">
<div class="grid">
<form class="cell" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="clean_locations">
<button class="btn" type="submit" id="submit">
<i class="fa-solid fa-broom"></i> {% translate "Erneut lokalisieren" %}
<button class="button is-primary" type="submit" id="submit">
<i class="fa-solid fa-location fa-fw"></i> {% translate "Erneut lokalisieren" %}
</button>
</form>
<form class="notification-card-mark-read" method="post">
<form class="cell" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="deactivate_unchecked_adoption_notices">
<button class="btn" type="submit" id="submit">
<i class="fa-solid fa-broom"></i> {% translate "Deaktiviere ungeprüfte Vermittlungen" %}
<button class="button is-primary" type="submit" id="submit">
<i class="fa-solid fa-broom fa-fw"></i> {% translate "Deaktiviere ungeprüfte Vermittlungen" %}
</button>
</form>
<form class="notification-card-mark-read" method="post">
<form class="cell" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="deactivate_404">
<button class="btn" type="submit" id="submit">
<i class="fa-solid fa-broom"></i> {% translate "Deaktiviere 404 Vermittlungen" %}
<button class="button is-primary" type="submit" id="submit">
<i class="fa-solid fa-broom fa-fw"></i> {% translate "Deaktiviere 404 Vermittlungen" %}
</button>
</form>
</div>
<div class="block">
<h2 class="title is-2">{% translate 'Testemail versenden' %}</h2>
<form method="post">
{% csrf_token %}
<input type="hidden" name="action" value="send_test_email">
<div class="field has-addons">
<p class="control">
{% trans 'E-Mail Addresse an die die Test E-Mail gesendet werden soll' as target_email %}
<label hidden="hidden" aria-hidden="false" for="test_email_address">{{ target_email }}</label>
<input class="input" id="test_email_address" name="test_email_address" type="text" placeholder="me@example.org">
</p>
<p class="control">
<button type="submit" class="button is-primary">
{% translate 'E-Mail senden testen' %}
</button>
</p>
</div>
</form>
</div>
{% endblock content %}

View File

@@ -1,12 +0,0 @@
{% load i18n %}
{% if adoption_notices %}
<div class="grid">
{% for adoption_notice in adoption_notices %}
<div class="cell">
{% include "fellchensammlung/partials/bulma-partial-adoption-notice-minimal.html" %}
</div>
{% endfor %}
</div>
{% else %}
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
{% endif %}

View File

@@ -1,5 +0,0 @@
<div class="container-cards">
{% for rule in rules %}
{% include "fellchensammlung/partials/bulma-partial-rule.html" %}
{% endfor %}
</div>

View File

@@ -1,10 +1,12 @@
{% load i18n %}
<div class="container-cards">
{% if adoption_notices %}
{% if adoption_notices %}
<div class="grid is-col-min-10">
{% for adoption_notice in adoption_notices %}
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
<div class="cell">
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
</div>
{% endfor %}
{% else %}
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
{% endif %}
</div>
</div>
{% else %}
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
{% endif %}

View File

@@ -1,8 +1,10 @@
{% load i18n %}
<div class="container-cards spaced">
<div class="grid is-col-min-10">
{% if rescue_organizations %}
{% for rescue_organization in rescue_organizations %}
{% include "fellchensammlung/partials/partial-rescue-organization.html" %}
<div class="cell">
{% include "fellchensammlung/partials/partial-rescue-organization.html" %}
</div>
{% endfor %}
{% else %}
<p>{% translate "Keine Tierschutzorganisationen gefunden." %}</p>

View File

@@ -1,5 +1,4 @@
<div class="container-cards">
{% for moderation_action in moderation_actions %}
<hr>
{% include "fellchensammlung/partials/partial-moderation-action.html" %}
{% endfor %}
</div>
{% endfor %}

View File

@@ -1,5 +1,5 @@
{% load i18n %}
<div class="container-cards">
<div class="block">
{% if search_subscriptions %}
{% for search_subscription in search_subscriptions %}
{% include "fellchensammlung/partials/partial-search-subscription.html" %}

View File

@@ -0,0 +1,24 @@
{% extends "fellchensammlung/mail/base.html" %}
{% load i18n %}
{% block title %}
{{ site.name }}: {% trans "Account aktivieren" %}
{% endblock %}
{% block content %}
<p>
{% trans 'Hier ist dein Aktivierungs-Key. Mit diesem kannst du deinen Account freischalten.' %}
</p>
<code>
{{ activation_key }}
</code>
<p>
{% trans "Öffne den folgenden link im Browser und gib den Aktivierungs-Key dort ein" %}
</p>
<a href="https://{{ site.domain }}{% url 'django_registration_activate' %}"
class="cta-button">{% translate 'Jetzt aktivieren' %}</a>
<p>
{% blocktrans %}Der Link ist für {{ expiration_days }} Tage gültig.{% endblocktrans %}
</p>
{% endblock %}

View File

@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE }}">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{% block title %}{% endblock %}</title>
<style>
body {
margin: 0;
padding: 0;
background-color: #f6f6f6;
font-family: Nunito, Arial, sans-serif;
}
.email-wrapper {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
border: 1px solid #e0e0e0;
}
.header {
background-color: #6CD4FF; /* $primary */
color: #292a2c; /* $link */
padding: 20px;
text-align: center;
font-size: 24px;
font-weight: bold;
}
.content {
padding: 30px 20px;
color: #262728; /* $grey-dark */
font-size: 16px;
line-height: 1.5;
}
.cta-button {
display: inline-block;
padding: 12px 20px;
background-color: hsl(133deg, 100%, 41%); /* $confirm */
color: #ffffff;
text-decoration: none;
border-radius: 4px;
margin-top: 20px;
font-weight: bold;
}
.footer {
background-color: #c4c6ce; /* $grey-light */
color: #292a2c;
text-align: center;
padding: 15px 10px;
font-size: 14px;
}
@media (max-width: 600px) {
.content, .header, .footer {
padding: 20px 15px;
}
}
</style>
</head>
<body>
<div class="email-wrapper">
{% block header %}
{% include "fellchensammlung/mail/header.html" %}
{% endblock %}
<div class="content">
{% block content %}{% endblock %}
</div>
{% include "fellchensammlung/mail/footer.html" %}
</div>
</body>
</html>

View File

@@ -0,0 +1,3 @@
<div class="footer">
🐀 notfellchen.org | Für Menschen die Ratten aus dem Tierschutz ein liebendes Zuhause geben wollen.
</div>

View File

@@ -0,0 +1,3 @@
<div class="header">
notfellchen.org
</div>

View File

@@ -0,0 +1,16 @@
{% extends "fellchensammlung/mail/base.html" %}
{% load i18n %}
{% block title %}
{% translate 'Vermittlung wurde deaktiviert' %}
{% endblock %}
{% block content %}
<p>Moin,</p>
<p>
die Vermittlung {{ notification.adoption_notice }} wurde deaktiviert.
</p>
<p>
<a href="{{ notification.adoption_notice.get_full_url }}" class="cta-button">{% translate 'Vermittlung anzeigen' %}</a>
</p>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends "fellchensammlung/mail/base.html" %}
{% load i18n %}
{% block title %}
{% translate 'Neue Vermittlung gefunden' %}
{% endblock %}
{% block content %}
<p>Moin,</p>
<p>
es wurde eine neue Vermittlung gefunden die deinen Kriterien entspricht: {{ notification.adoption_notice }}
</p>
<p>
<a href="{{ notification.adoption_notice.get_full_url }}" class="cta-button">{% translate 'Vermittlung anzeigen' %}</a>
</p>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends "fellchensammlung/mail/base.html" %}
{% load i18n %}
{% block title %}
{% translate 'Vermittlung muss überprüft werden' %}
{% endblock %}
{% block content %}
<p>Moin,</p>
<p>
die Vermittlung {{ notification.adoption_notice }} muss überprüft werden.
</p>
<p>
<a href="{{ notification.adoption_notice.get_full_url }}" class="cta-button">{% translate 'Vermittlung anzeigen' %}</a>
</p>
{% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends "fellchensammlung/mail/base.html" %}
{% load i18n %}
{% load custom_tags %}
{% block title %}
{% translate 'Vermittlung muss überprüft werden' %}
{% endblock %}
{% block content %}
<p>Moin,</p>
<p>
folgender Kommentar wurde zur Vermittlung {{ notification.adoption_notice }} hinzugefügt:
</p>
<p><i>
{{ notification.comment.text | render_markdown }}
</i></p>
<p>
<a href="{{ notification.adoption_notice.get_full_url }}" class="cta-button">{% translate 'Vermittlung anzeigen' %}</a>
</p>
{% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends "fellchensammlung/mail/base.html" %}
{% load i18n %}
{% block title %}
{% translate 'Neuer User' %}
{% endblock %}
{% block content %}
<p>Moin,</p>
<p>
es wurde ein neuer Useraccount erstellt.
</p>
<p>
Details findest du hier
</p>
<p>
<a href="{{ notification.user_related.get_absolute_url }}" class="cta-button">{% translate 'User anzeigen' %}</a>
</p>
{% endblock %}

View File

@@ -0,0 +1,25 @@
{% extends "fellchensammlung/mail/base.html" %}
{% load i18n %}
{% block title %}
{% translate 'Neue Meldung' %}
{% endblock %}
{% block content %}
<p>Moin,</p>
<p>
es gibt eine neue Meldung. Folgende Nachricht wurde zur Meldung hinzugefügt:
</p>
<p>
<i>
{{ user_comment }}
</i>
</p>
<p>
Bitte bearbeite die Meldung möglichst bald.
</p>
<p>
<a href="{{ report_url }}" class="cta-button">{% translate 'Report bearbeiten' %}</a>
</p>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends "fellchensammlung/mail/base.html" %}
{% load i18n %}
{% block title %}
{% translate 'Test-E-Mail' %}
{% endblock %}
{% block content %}
<p>Moin,</p>
<p>
das ist eine Test-E-Mail.
</p>
<p>
Hier ist ein total wichtiger Button. Klick den mal!
</p>
<p>
<a href="https://notfellchen.org" class="cta-button">{% translate 'Zur Website' %}</a>
</p>
{% endblock %}

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