Compare commits

165 Commits

Author SHA1 Message Date
151ce0d88e fix: massively reduce number of db queries by caching num_per_sex #27
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-11-03 17:00:16 +01:00
e07e633651 style: explicitly return none 2025-11-03 16:53:59 +01:00
dd3b1fde9d feat: Add logs for checking rescue orgs and remove deprecated exclusion 2025-11-03 16:15:11 +01:00
2ffc9b4ba1 feat: Ad django debug toolbar 2025-11-03 16:14:51 +01:00
22eebd4586 feat: Add simple profiler capability 2025-11-03 16:14:05 +01:00
e589a048d3 feat: Make logs in Admin more usable 2025-11-03 16:11:48 +01:00
392eb5a7a8 feat: Use unified explanation for reason for signup 2025-11-02 08:15:11 +01:00
44fa4d4880 feat: Remove requirement to retype password 2025-11-02 08:14:41 +01:00
9b97cc4cb1 fix: Ensure javascript for login is loaded 2025-11-02 08:14:03 +01:00
656a24ef02 feat: Make settings configurable
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-10-21 03:37:36 +02:00
74643db087 feat: Add nicer display of passkeys based on panels 2025-10-21 03:27:08 +02:00
3a6fd3cee1 feat: Add nicer badge 2025-10-21 03:24:19 +02:00
29e9d1bd8c feat: Don't make input for radio button 2025-10-21 03:24:07 +02:00
3c5ca9ae00 feat: Fix display of 2fa options 2025-10-21 02:12:34 +02:00
3d1ad6112d feat: Add link to 2fa options 2025-10-21 02:12:17 +02:00
b843e67e9b feat: put buttons in group 2025-10-21 01:47:17 +02:00
4cab71e8fb feat: Style allauth templates 2025-10-21 01:28:31 +02:00
969339a95f feat: Use allauth and add passkey support 2025-10-21 00:40:10 +02:00
e06efa1539 feat: limit to 10 2025-10-21 14:47:13 +02:00
2fb6d2782f fix: Align button description with function 2025-10-20 21:56:04 +02:00
f69eccd0e4 feat: add page for updating the exclusion reason where it's not set yet 2025-10-20 18:33:15 +02:00
e20e6d4b1d fix: typo 2025-10-20 18:31:10 +02:00
0352a60e28 feat: Add reason why rescue org was excluded from check
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-10-20 10:54:23 +02:00
abeb14601a docs: Add general explenation 2025-10-20 10:12:40 +02:00
f52225495d docs: Add number of rescueorgs in table 2025-10-20 10:09:58 +02:00
797b2c15f7 docs: Add adoption notice lifecycle
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-10-19 22:53:29 +02:00
e81618500b docs: Add getting started
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-10-19 21:02:51 +02:00
f7a5da306c docs: Add details to notifications 2025-10-19 21:02:33 +02:00
92a9b5c6c9 fix: typo 2025-10-19 17:46:47 +02:00
964aeb97a7 docs: documentation on checking rescue orgs
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
2025-10-19 09:41:19 +02:00
474e9eb0f8 feat: Add extension to sphinx that includes drawio diagrams 2025-10-19 09:40:51 +02:00
7acc2c6eec feat: Document shortcuts in org-check und documentation 2025-10-19 08:35:19 +02:00
5a02837d7f feat: Fix age display and test 2025-10-18 18:45:17 +02:00
7ff0a9b489 feat: Add stats 2025-10-03 18:37:54 +02:00
9af4b58a4f feat: Add stats 2025-10-03 09:39:48 +02:00
7a20890f17 feat: Add organization to form 2025-10-01 06:13:01 +02:00
f6c9e532f8 feat: Use status-specific description 2025-10-01 05:58:30 +02:00
f1698c4fd3 fix: use correct new status 2025-10-01 05:54:43 +02:00
cb82aeffde fix: use correct new status 2025-10-01 05:33:49 +02:00
e9c1ef2604 feat: typo 2025-09-29 17:34:46 +02:00
d8f0f2b3be feat: Use status description to more accuratly describe status 2025-09-29 17:34:38 +02:00
65f065f5ce feat: Move unchecked to awaiting action 2025-09-29 17:34:13 +02:00
5cba64e500 feat: allow links to break anywhere 2025-09-29 17:32:05 +02:00
064784a222 feat: Use contact data of parent org if org doesn't have specified 2025-09-27 15:35:39 +02:00
b890ef3563 fix: Use block only when description exists 2025-09-27 15:34:32 +02:00
962f2ae86c feat: Add contact site 2025-09-17 20:02:20 +02:00
c71a1940dd feat: Add url of adoption notice to API 2025-09-17 20:02:13 +02:00
8b2913a8be feat: Add Image width and height to API 2025-09-16 16:38:29 +02:00
111ffc2b2e feat: Add Image alt text to API 2025-09-13 16:53:13 +02:00
600aa918ef feat: Add Image URL to API 2025-09-13 16:49:00 +02:00
68e13ed176 feat: Add API to get adoption notices of organization 2025-09-13 16:05:49 +02:00
0fa4330f2c feat: Add more clear when no Adoption notice is found 2025-09-10 23:29:56 +02:00
c9289b1e8c feat: Add padding to iframe content 2025-09-10 23:29:30 +02:00
bb3136bfc7 feat: Allow setting a background color for embedding 2025-09-10 22:54:24 +02:00
b708e9ecaf feat: Get all adoption notices in hierarchy 2025-09-10 20:18:10 +02:00
f3619b2881 fix: Exclude embeddable from xframe options 2025-09-10 19:28:36 +02:00
7572c92da5 feat: Add expand option 2025-09-10 19:28:19 +02:00
5a2b11b44e feat: Add basic embeddable view for ans of rescue orgs 2025-09-10 17:44:33 +02:00
df15ea100b feat: Add stub to translate ANs to sharepics 2025-09-10 13:17:50 +02:00
3da6e90f73 feat: Show short description for ANs with one picture 2025-09-07 20:36:41 +02:00
f784ab0c78 feat: Restyle the image upload 2025-09-06 16:06:12 +02:00
ebaa477cff feat: Use slug to get specialization 2025-09-06 15:35:17 +02:00
b4be21bf45 feat: Add slug to species 2025-09-06 15:14:04 +02:00
0df36df9d8 fix: has search criteria had twisted logic 2025-09-06 14:08:30 +02:00
a5754b2633 feat: Optimize search for larger number of ANs 2025-09-06 13:35:19 +02:00
7c6e01a436 feat: Add various verbose names and helptexts 2025-09-06 13:12:06 +02:00
ad90429ec7 feat: Add local db backups to gitignore 2025-09-05 16:19:13 +02:00
0e36237890 feat: Add adoption notice template for contacting a person directly 2025-09-05 16:07:21 +02:00
3261f5a90a fix: Allow to actually close AN 2025-09-03 06:43:12 +02:00
1551c1bdf2 fix: correctly filter for adoption notices 2025-09-03 06:28:35 +02:00
996bd7af67 feat: Re-style comment field to be inside comment box 2025-09-03 06:28:13 +02:00
21bd34c94d feat: Add divider 2025-09-03 06:27:48 +02:00
fb581c940b fix: fix action menu hidden behind animal 2025-09-01 21:57:59 +02:00
b428f46213 fix: unify 2025-08-31 23:21:56 +02:00
38fe55dd86 fix: Only post to fedi if there is a adoption notice to post 2025-08-31 22:05:26 +02:00
0da6c425fd feat: Add link to oxitraffic 2025-08-31 22:04:12 +02:00
81962ab9e7 fix: Make sure tasks are only executed once per hour 2025-08-31 20:41:10 +02:00
48dd0a6a19 fix: Add a block 2025-08-31 20:34:48 +02:00
661827a957 chore: Bump 1.2.1 2025-08-31 18:05:29 +02:00
242de5f749 fix: filter for active adoption notices 2025-08-31 18:05:02 +02:00
bd7f940987 chore: Bump version to 1.2.0 2025-08-31 17:07:03 +02:00
0634671c84 feat: Style further information more clearly 2025-08-31 17:06:01 +02:00
1fb5be0cf8 feat: Add description of adoption process 2025-08-31 17:04:08 +02:00
3f9e4265e5 refactor: move descriptions to dedicated mapping 2025-08-31 16:18:49 +02:00
de21b8b5e5 refactor: remove unused imports 2025-08-31 10:54:48 +02:00
fd481fef2e feat: Fully replace the Adoption Notice Status model with the field 2025-08-31 10:52:48 +02:00
70f077e393 feat: Add general field-based status and migrate data 2025-08-31 00:28:44 +02:00
1c7d943a21 feat: Add image preview for uploading new images 2025-08-29 08:47:34 +02:00
41873ebfe5 fix: Send actually new user notification 2025-08-12 17:17:35 +02:00
fc2dbde064 feat: make 20km default 2025-08-12 06:28:03 +02:00
a372be4af2 fix: don't use search when checking specialized rescue orgs 2025-08-12 06:16:27 +02:00
5d333b28ab feat: Fix pagination when searching 2025-08-12 06:12:27 +02:00
84ad047c01 feat: Add search for rescue orgs 2025-08-12 00:06:42 +02:00
c93b2631cb feat: Add shortcut to open rescue org website 2025-08-11 22:16:26 +02:00
15dd06a91f feat: Add shortcut to mark rescue org as checked 2025-08-11 21:51:30 +02:00
30ff26c7ef feat: Divide adoption notices of org by active and inactive 2025-08-11 12:58:02 +02:00
1434e7502a fix: Limit upload of fediverse images to 6 2025-08-11 12:43:15 +02:00
93b21fb7d0 fix: Only try to access trust level when authenticated 2025-08-10 18:32:15 +02:00
e5c82f392c feat(test): Add test for map and metrics 2025-08-10 18:31:53 +02:00
0626964461 feat(test): Add test for token showing 2025-08-10 18:11:10 +02:00
23a724e390 fix: Ensure users of higher trust level are also allowed 2025-08-10 17:51:25 +02:00
2a9c7cf854 feat(test): Add basic tests for user views 2025-08-10 17:51:05 +02:00
335630e16d feat(test): Add test for search by location 2025-08-10 17:50:17 +02:00
6051f7c294 feat(test): Exclude from coverage check 2025-08-10 10:17:59 +02:00
c1ea6cd211 feat(test): Add AN form to basic check 2025-08-10 08:44:28 +02:00
6c43b46007 refactor: break out search test into own file 2025-08-10 08:43:49 +02:00
dc9e68c4b9 refactor: remoive print 2025-08-10 08:22:57 +02:00
4b03f99971 feat(test): Add rss feed test 2025-08-09 16:46:07 +02:00
426f4b3d8b fix: Make sure e-mail is sent when comment is reported 2025-08-09 12:30:42 +02:00
3604233507 fix (test): Rules are now shown on terms of service page 2025-08-09 12:30:22 +02:00
8c5099f14a fix (test): Notification framework changed 2025-08-09 12:16:13 +02:00
d5bc348453 feat: allow marking read with very ugly double delete class 2025-08-03 10:40:42 +02:00
bce98cb439 trans: Translate models 2025-08-03 10:29:47 +02:00
1ed3d27533 feat: add option to sync to twenty 2025-08-03 10:00:42 +02:00
39a098af8e feat: Add option to mask e-mails and phone numbers
This is a prerequisite to do tests on DEv and UAT systems
2025-08-01 20:28:51 +02:00
62491b84c1 feat(seo): Add basic description 2025-08-01 19:32:02 +02:00
81f7f5bb5d fix: Use correct heading hierarchy 2025-08-01 19:31:37 +02:00
8ce4122160 feat: raise 404 when AN not found 2025-07-30 08:01:34 +02:00
370ad2ce66 feat: add warning when an is waiting for review 2025-07-30 06:57:31 +02:00
f25c425d85 feat: add warning when someone is interested 2025-07-30 06:51:29 +02:00
d921623f31 fix: Make sure content class is used when rendering markdown 2025-07-25 22:38:49 +02:00
2589f1c703 fix: Make sure rescue orgs with ans only in hierarch show correctly
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-07-22 13:25:47 +02:00
0edb9094c4 feat: Show parent org 2025-07-21 17:11:13 +02:00
bc8feba701 feat: Show all adoption notices of rescue org and children 2025-07-20 16:58:18 +02:00
f37d74a7d1 feat: Add child orgs to org detail page 2025-07-20 16:29:56 +02:00
fa8612ad1a fix: ensure location is displayed 2025-07-20 16:29:27 +02:00
1d8a054b06 feat: Add number of animals per sex to metrics 2025-07-20 16:09:59 +02:00
5898fbf86d feat: Add number of animals per sex to metrics 2025-07-20 15:43:59 +02:00
cd1cdd2e0b feat: Add link to admin interface 2025-07-20 15:32:13 +02:00
c0f920544b feat: Add automatic post in the evening 2025-07-20 13:49:24 +02:00
36c90531a8 feat: Add option to switch between normal and dq 2025-07-20 13:42:56 +02:00
7f7c5a3b04 fix: post all available pictures 2025-07-20 08:50:00 +02:00
c084e56ad8 chore: Bump version to 1.1.0 2025-07-20 07:59:51 +02:00
84acc3c76e feat: format posts in markdown 2025-07-20 07:58:36 +02:00
e1f0014898 feat: Add "Post to Fediverse" 2025-07-20 07:07:33 +02:00
05b3a470f3 feat: Add warning about deactivated ANs 2025-07-19 09:29:15 +02:00
ebe060646a trans: Add a few translations 2025-07-17 15:57:22 +02:00
bb412be8d3 feat: Add view for specialized rescues 2025-07-14 07:31:08 +02:00
e3c48eac24 feat: fail more gracefully 2025-07-14 07:16:17 +02:00
da89cdceda feat: use simpler m2m relationship for specialization 2025-07-14 07:15:44 +02:00
5a6c2c99e5 feat: add important locations and buying to sitemap and fix 2025-07-14 06:33:12 +02:00
9f53836ce8 feat: add page dedicated to buying animals 2025-07-14 06:18:56 +02:00
5d53d1a1dc feat: add a default order for rescue orgs (very useful when adding ANs to it) 2025-07-13 13:01:58 +02:00
e00dda1dc2 feat: add option to mark a rescue org to be in active communication
That enables to filter them out from a check without forgetting there are to-dos. Most often this will be used when you want to call a rescue org but they can currently not be reached
2025-07-13 12:58:14 +02:00
a93e0c819f fix: use correct user 2025-07-13 12:08:42 +02:00
c87733b37a feat: Open shelters in new tab 2025-07-13 11:02:00 +02:00
9aa964bf05 feat: style external site warning 2025-07-13 10:30:51 +02:00
dcb1d3ec15 feat: Add functionality to deactivate AN with reason 2025-07-13 10:06:13 +02:00
5d9b8f3213 feat: Re-add functionality to set AN as checked 2025-07-13 09:12:24 +02:00
d12989d195 feat: Add display of when last checked 2025-07-13 09:11:51 +02:00
a9f384b50e feat: add subscribe functionality again 2025-07-13 01:08:43 +02:00
afedf2d0bd feat: add button to mark all notifications as read and fix action 2025-07-13 00:31:21 +02:00
a4b8486bd4 feat: make use of new notification mapping 2025-07-13 00:00:30 +02:00
d8bcb8ece6 refactor: remove redundant imports 2025-07-12 17:45:21 +02:00
b01ac219a3 feat: Add notification partials including new mapping system for templates 2025-07-12 17:43:40 +02:00
42320866c4 feat: Show nicer time of creating the notification 2025-07-12 16:46:56 +02:00
e2e6c14d57 feat: Show newest notification first 2025-07-12 16:46:17 +02:00
4761c38cd2 feat: Move cards to bulma notifications 2025-07-12 16:31:15 +02:00
e2bef3efe2 feat: Add dedicated notification page 2025-07-12 14:01:40 +02:00
bbfd4c3800 feat: re-add notification badge 2025-07-12 13:34:18 +02:00
b671d8fbb4 fix: fix filters 2025-07-12 13:34:02 +02:00
1ea04e98e8 fix: fucking wired bug where the url is not displayed in plaintext
that how an e-mail looked before:
Moin,

es wurde ein neuer Useraccount erstellt.

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

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

---
2025-07-12 11:46:47 +02:00
c1a7d6790b feat: Reorder notification fields for nicer display in admin 2025-07-12 11:25:08 +02:00
f519f78922 feat: Add unsubscribe link 2025-07-12 11:24:44 +02:00
551b5ed6be feat: add plaintext versions of the e-mail 2025-07-12 09:17:58 +02:00
162 changed files with 4533 additions and 1419 deletions

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
# Database # Database
notfellchen notfellchen
*.sq3
# Geojson from imports # Geojson from imports
*.geojson *.geojson

74
docs/_ext/drawio.py Normal file
View File

@@ -0,0 +1,74 @@
from __future__ import annotations
from pathlib import Path
from docutils import nodes
from sphinx.application import Sphinx
from sphinx.util.docutils import SphinxDirective
from sphinx.util.typing import ExtensionMetadata
class DrawioDirective(SphinxDirective):
"""A directive to show a drawio diagram!
Usage:
.. drawio::
example-diagram.drawio.html
example-diagram.drawio.png
:alt: Example of a Draw.io diagram
"""
has_content = False
required_arguments = 2 # html and png
optional_arguments = 1
final_argument_whitespace = True # indicating if the final argument may contain whitespace
option_spec = {
"alt": str,
}
def run(self) -> list[nodes.Node]:
env = self.state.document.settings.env
builder = env.app.builder
# Resolve paths relative to the document
docdir = Path(env.doc2path(env.docname)).parent
html_rel = Path(self.arguments[0])
png_rel = Path(self.arguments[1])
html_path = (docdir / html_rel).resolve()
png_path = (docdir / png_rel).resolve()
alt_text = self.options.get("alt", "")
container = nodes.container()
# HTML output -> raw HTML node
if builder.format == "html":
# Embed the HTML file contents directly
try:
html_content = html_path.read_text(encoding="utf-8")
except OSError as e:
msg = self.state_machine.reporter.error(f"Cannot read HTML file: {e}")
return [msg]
aria_attribute = f' aria-label="{alt_text}"' if alt_text else ""
raw_html_node = nodes.raw(
"",
f'<div class="drawio-diagram"{aria_attribute}>{html_content}</div>',
format="html",
)
container += raw_html_node
else:
# Other outputs -> PNG image node
image_node = nodes.image(uri=png_path)
container += image_node
return [container]
def setup(app: Sphinx) -> ExtensionMetadata:
app.add_directive("drawio", DrawioDirective)
return {
"version": "0.2",
"parallel_read_safe": True,
"parallel_write_safe": True,
}

View File

@@ -16,6 +16,10 @@
# import sys # import sys
# sys.path.insert(0, os.path.abspath('.')) # sys.path.insert(0, os.path.abspath('.'))
import sys
from pathlib import Path
sys.path.append(str(Path('_ext').resolve()))
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
@@ -28,7 +32,6 @@ version = ''
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
release = '0.2.0' release = '0.2.0'
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here. # If your documentation needs a minimal Sphinx version, state it here.
@@ -40,6 +43,7 @@ release = '0.2.0'
# ones. # ones.
extensions = [ extensions = [
'sphinx.ext.ifconfig', 'sphinx.ext.ifconfig',
'drawio'
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
@@ -69,7 +73,6 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use. # The name of the Pygments (syntax highlighting) style to use.
pygments_style = None pygments_style = None
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
@@ -104,7 +107,6 @@ html_static_path = ['_static']
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'notfellchen' htmlhelp_basename = 'notfellchen'
# -- Options for LaTeX output ------------------------------------------------ # -- Options for LaTeX output ------------------------------------------------
latex_elements = { latex_elements = {
@@ -133,7 +135,6 @@ latex_documents = [
'Julian-Samuel Gebühr', 'manual'), 'Julian-Samuel Gebühr', 'manual'),
] ]
# -- Options for manual page output ------------------------------------------ # -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
@@ -143,7 +144,6 @@ man_pages = [
[author], 1) [author], 1)
] ]
# -- Options for Texinfo output ---------------------------------------------- # -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples # Grouping the document tree into Texinfo files. List of tuples
@@ -155,7 +155,6 @@ texinfo_documents = [
'Miscellaneous'), 'Miscellaneous'),
] ]
# -- Options for Epub output ------------------------------------------------- # -- Options for Epub output -------------------------------------------------
# Bibliographic Dublin Core info. # Bibliographic Dublin Core info.
@@ -173,5 +172,4 @@ epub_title = project
# A list of files that should not be packed into the epub file. # A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html'] epub_exclude_files = ['search.html']
# -- Extension configuration ------------------------------------------------- # -- Extension configuration -------------------------------------------------

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

File diff suppressed because one or more lines are too long

View File

@@ -6,14 +6,27 @@ Jede Vermittlung kann abonniert werden. Dafür klickst du auf die Glocke neben d
.. image:: abonnieren.png .. image:: abonnieren.png
Einstellungen
-------------
Du kannst E-Mail Benachrichtigungen in den Einstellungen deaktivieren.
.. image::
einstellungen-benachrichtigungen.png
:alt: Screenshot der Profileinstellungen in Notfellchen. Ein roter Pfeil zeigt auf einen Schalter "E-Mail Benachrichtigungen"
Auf der Website Auf der Website
+++++++++++++++ +++++++++++++++
.. image::
screenshot-benachrichtigungen.png
:alt: Screenshot der Menüleiste von Notfellchen.org. Neben dem Symbol einer Glocke steht die Zahl 27.
E-Mail E-Mail
++++++ ++++++
Mit während deiner :doc:`registrierung` gibst du eine E-Mail Addresse an. Mit während deiner :doc:`registrierung` gibst du eine E-Mail Adresse an. An diese senden wir Benachrichtigungen, außer
du deaktiviert dies wie oben beschrieben.
Benachrichtigungen senden wir per Mail - du kannst das jederzeit in den Einstellungen deaktivieren.

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1,58 @@
Erste Schritte
==============
Tiere zum Adoptieren suchen
---------------------------
Wenn du Tiere zum adoptieren suchst, brauchst du keinen Account. Du kannst bequem die `Suche <https://notfellchen.org/suchen/>`_ nutzen, um Tiere zur Adoption in deiner Nähe zu finden.
Wenn dich eine Vermittlung interessiert, kannst du folgendes tun
* die Vermittlung aufrufen um Details zu sehen
* den Link :guilabel:`Weitere Informationen` anklicken um auf der Tierheimwebsite mehr zu erfahren
* per Kommentar weitere Informationen erfragen oder hinzufügen
Wenn du die Tiere tatsächlich informieren willst, folge der Anleitung unter :guilabel:`Adoptionsprozess`.
Dieser kann sich je nach Tierschutzorganisation unterscheiden.
.. image::
screenshot-adoptionsprozess.png
:alt: Screenshot der Sektion "Adoptionsprozess" einer Vermittlungsanzeige. Der Prozess ist folgendermaßen: 1. Link zu "Weiteren Informationen" prüfen, 2. Organization kontaktieren, 3. Bei erfolgreicher Vermittlung: Vermittlung als geschlossen melden
Suchen abonnieren
+++++++++++++++++
Es kann sein, dass es in deiner Umgebung keine passenden Tiere für deine Suche gibt. Damit du nicht ständig wieder Suchen musst, gibt es die Funktion "Suche abonnieren".
Wenn du eine Suche abonnierst, wirst du für neue Vermittlungen, die den Kriterien der Suche entsprechen, benachrichtigt.
.. image::
screenshot-suche-abonnieren.png
:alt: Screenshot der Suchmaske auf Notfellchen.org . Ein roter Pfeil zeigt auf den Button "Suche abonnieren"
.. important::
Um Suchen zu abonnieren brauchst du einen Account. Wie du einen Account erstellst erfährst du hier: :doc:`registrierung`.
.. hint::
Mehr über Benachrichtigungen findest du hier: :doc:`benachrichtigungen`.
Vermittlungen hinzufügen
------------------------
Gehe zu `Vermittlung hinzufügen <https://notfellchen.org/vermitteln/>`_ um eine neue Vermittlung einzustellen.
Füge alle Informationen die du hast hinzu.
.. important::
Um Vermittlungen hinzuzufügen brauchst du einen Account.
Wie du einen Account erstellst erfährst du hier: :doc:`registrierung`.
.. important::
Vermittlungen die du einstellst müssen erst durch Moderator\*innen freigeschaltet werden. Das passiert normalerweise
innerhalb von 24 Stunden. Wenn deine Vermittlung dann noch nicht freigeschaltet ist, prüfe bitte dein E-Mail Postfach,
es könnte sein, dass die Moderator\*innen Rückfragen haben. Melde dich gerne unter info@notfellchen.org, wenn deine
Vermittlung nach 24 Stunden nicht freigeschaltet ist.

View File

@@ -1,11 +1,17 @@
****************** ****************
User Dokumentation Benutzerhandbuch
****************** ****************
Im Benutzerhandbuch findest du Informationen zur Benutzung von `notfellchen.org <https://notfellchen.org>`_.
Solltest du darüber hinaus Fragen haben, komm gerne auf uns zu: info@notfellchen.org
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
:caption: Inhalt: :caption: Inhalt:
erste-schritte.rst
registrierung.rst registrierung.rst
vermittlungen.rst vermittlungen.rst
moderationskonzept.rst moderationskonzept.rst
benachrichtigungen.rst benachrichtigungen.rst
organisationen-pruefen.rst

View File

@@ -0,0 +1,55 @@
Tiere in Vermittlung systematisch entdecken & eintragen
=======================================================
Notfellchen hat eine Liste der meisten deutschen Tierheime und anderer Tierschutzorganisationen.
Die meisten dieser Organisationen nehmen Tiere auf die bei Notfellchen eingetragen werden können.
Es ist daher das Ziel, diese Organisationen alle zwei Wochen auf neue Tiere zu prüfen.
+-------------------------------------------------+---------+----------------------+
| Gruppe | Anzahl | Zuletzt aktualisiert |
+=================================================+=========+======================+
| Tierschutzorganisationen im Verzeichnis | 550 | Oktober 2025 |
+-------------------------------------------------+---------+----------------------+
| Tierschutzorganisationen in regelmäßigerPrüfung | 412 | Oktober 2025 |
+-------------------------------------------------+---------+----------------------+
.. warning::
Organisationen auf neue Tiere zu prüfen ist eine Funktion für Moderator\*innen. Falls du Lust hast mitzuhelfen,
meld dich unter info@notfellchen.org
Als Moderator\*in kannst du direkt auf den `Moderations-Check <https://notfellchen.org/organization-check/>`_ zugreifen
oder findest ihn in unter :menuselection:`Hilfreiche Links --> Moderationstools`:
.. image::
Screenshot-hilfreiche-Links.png
:alt: Screenshot der Hilfreichen Links. Zur Auswahl stehen "Tierheime in der Nähe","Moderationstools" und "Admin-Bereich"
.. image::
Screenshot-Moderationstools.png
:alt: Screenshot der Moderationstools. Zur Auswahl stehen "Moderationswarteschlange", "Up-to-Date Check", "Organisations-Check" und "Vermittlung ins Fediverse posten".
Arbeitsmodus
------------
.. drawio::
Tiere-in-Vermittlung-entdecken.drawio.html
Tiere-in-Vermittlung-entdecken.drawio.png
Shortcuts
---------
Um die Prüfung schneller zu gestalten, gibt es eine Reihe von Shortcuts die du nutzen kannst. Aus Gründen der
Übersichtlichkeit sind im Folgenden auch Shortcuts im Browser aufgeführt.
+------------------------------------------------------+---------------+
| Aktion | Shortcut |
+======================================================+===============+
| Website der ersten Tierschutzorganisation öffnen | :kbd:`O` |
+------------------------------------------------------+---------------+
| Tab schließen (Firefox/Chrome) | :kbd:`STRG+W` |
+------------------------------------------------------+---------------+
| Erste Tierschutzorganisationa als geprüft markieren | :kbd:`C` |
+------------------------------------------------------+---------------+

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,7 +1,7 @@
Vermittlungen Vermittlungen
============= =============
Vermittlungen können von allen Nutzer*innen mit Account erstellt werden. Vermittlungen normaler Nutzer*innen kommen dann in eine Warteschlange und werden vom Admin & Modertionsteam geprüft und sichtbar geschaltet. Vermittlungen können von allen Nutzer\*innen mit Account erstellt werden. Vermittlungen normaler Nutzer*innen kommen dann in eine Warteschlange und werden vom Admin & Modertionsteam geprüft und sichtbar geschaltet.
Tierheime und Pflegestellen können auf Anfrage einen Koordinations-Status bekommen, wodurch sie Vermittlungsanzeigen erstellen können die direkt öffentlich sichtbar sind. Tierheime und Pflegestellen können auf Anfrage einen Koordinations-Status bekommen, wodurch sie Vermittlungsanzeigen erstellen können die direkt öffentlich sichtbar sind.
Jede Vermittlung hat ein "Zuletzt-geprüft" Datum, das anzeigt, wann ein Mensch zuletzt überprüft hat, ob die Anzeige noch aktuell ist. Jede Vermittlung hat ein "Zuletzt-geprüft" Datum, das anzeigt, wann ein Mensch zuletzt überprüft hat, ob die Anzeige noch aktuell ist.
@@ -15,3 +15,114 @@ Die Kommentarfunktion von Vermittlungen ermöglicht es angemeldeten Nutzer*innen
Ersteller*innen von Vermittlungen werden über neue Kommentare per Mail benachrichtigt, ebenso alle die die Vermittlung abonniert haben. Ersteller*innen von Vermittlungen werden über neue Kommentare per Mail benachrichtigt, ebenso alle die die Vermittlung abonniert haben.
Kommentare können, wie Vermittlungen, gemeldet werden. Kommentare können, wie Vermittlungen, gemeldet werden.
.. drawio::
Vermittlung_Lifecycle.drawio.html
Vermittlung-Lifecycle.drawio.png
:alt: Diagramm das den Prozess der Vermittlungen zeigt.
Adoption Notice Status Choices
++++++++++++++++++++++++++++++
Aktiv
-----
Aktive Vermittlungen die über die Suche auffindbar sind.
.. list-table::
:header-rows: 1
:width: 100%
:widths: 1 1 2
* - Value
- Label
- Description
* - ``active_searching``
- Searching
-
* - ``active_interested``
- Interested
- Jemand hat bereits Interesse an den Tieren.
Warte auf Aktion
----------------
Vermittlungen in diesem Status warten darauf, dass ein Mensch sie überprüft. Sie können nicht über die Suche gefunden werden.
.. list-table::
:header-rows: 1
:width: 100%
:widths: 1 1 2
* - ``awaiting_action_waiting_for_review``
- Waiting for review
- Neue Vermittlung die deaktiviert ist bis Moderator*innen sie überprüfen.
* - ``awaiting_action_needs_additional_info``
- Needs additional info
- Deaktiviert bis Informationen nachgetragen werden.
* - ``disabled_unchecked``
- Unchecked
- Vermittlung deaktiviert bis sie vom Team auf Aktualität geprüft wurde.
Geschlossen
-----------
Geschlossene Vermittlungen tauchen in keiner Suche auf. Sie werden aber weiterhin angezeigt, wenn der Link zu ihnen direkt aufgerufen wird.
.. list-table::
:header-rows: 1
:width: 100%
:widths: 1 1 2
* - ``closed_successful_with_notfellchen``
- Successful (with Notfellchen)
- Vermittlung erfolgreich abgeschlossen.
* - ``closed_successful_without_notfellchen``
- Successful (without Notfellchen)
- Vermittlung erfolgreich abgeschlossen.
* - ``closed_animal_died``
- Animal died
- Die zu vermittelnden Tiere sind über die Regenbrücke gegangen.
* - ``closed_for_other_adoption_notice``
- Closed for other adoption notice
- Vermittlung wurde zugunsten einer anderen geschlossen.
* - ``closed_not_open_for_adoption_anymore``
- Not open for adoption anymore
- Tier(e) stehen nicht mehr zur Vermittlung bereit.
* - ``closed_link_to_more_info_not_reachable``
- Der Link zu weiteren Informationen ist nicht mehr erreichbar.
- Der Link zu weiteren Informationen ist nicht mehr erreichbar, die Vermittlung wurde daher automatisch deaktiviert.
* - ``closed_other``
- Other (closed)
- Vermittlung geschlossen.
Deaktiviert
-----------
Deaktivierte Vermittlungen werden nur noch Moderator\*innen und Administrator\*innen angezeigt.
.. list-table::
:header-rows: 1
:width: 100%
:widths: 1 1 2
* - ``disabled_against_the_rules``
- Against the rules
- Vermittlung deaktiviert da sie gegen die Regeln verstößt.
* - ``disabled_other``
- Other (disabled)
- Vermittlung deaktiviert.

View File

@@ -8,6 +8,7 @@ host=localhost
[django] [django]
secret=CHANGE-ME secret=CHANGE-ME
debug=True debug=True
internal_ips=["127.0.0.1"]
[database] [database]
backend=sqlite3 backend=sqlite3
@@ -28,3 +29,6 @@ django_log_level=INFO
api_url=https://photon.hyteck.de/api api_url=https://photon.hyteck.de/api
api_format=photon api_format=photon
[security]
totp_issuer="NF Localhost"
webauth_allow_insecure_origin=True

View File

@@ -38,7 +38,8 @@ dependencies = [
"celery[redis]", "celery[redis]",
"drf-spectacular[sidecar]", "drf-spectacular[sidecar]",
"django-widget-tweaks", "django-widget-tweaks",
"django-super-deduper" "django-super-deduper",
"django-allauth[mfa]",
] ]
dynamic = ["version", "readme"] dynamic = ["version", "readme"]
@@ -48,6 +49,7 @@ develop = [
"pytest", "pytest",
"coverage", "coverage",
"model_bakery", "model_bakery",
"debug_toolbar",
] ]
docs = [ docs = [
"sphinx", "sphinx",

View File

@@ -178,8 +178,13 @@ def create_location(tierheim, instance, headers):
location_result = requests.post(f"{instance}/api/locations/", json=location_data, headers=headers) location_result = requests.post(f"{instance}/api/locations/", json=location_data, headers=headers)
if location_result.status_code != 201: if location_result.status_code != 201:
try:
print( print(
f"Location for {tierheim["properties"]["name"]}:{location_result.status_code} {location_result.json()} not created") f"Location for {tierheim["properties"]["name"]}:{location_result.status_code} {location_result.json()} not created")
except requests.exceptions.JSONDecodeError:
print(f"Location for {tierheim["properties"]["name"]} could not be created")
exit()
return location_result.json() return location_result.json()
@@ -200,6 +205,8 @@ def main():
h = {'Authorization': f'Token {api_token}', "content-type": "application/json"} h = {'Authorization': f'Token {api_token}', "content-type": "application/json"}
tierheime = overpass_result["features"] tierheime = overpass_result["features"]
stats = {"num_updated_orgs": 0,
"num_inserted_orgs": 0}
for idx, tierheim in enumerate(tqdm(tierheime)): for idx, tierheim in enumerate(tqdm(tierheime)):
# Check if data is low quality # Check if data is low quality
@@ -224,11 +231,13 @@ def main():
optional_data = ["email", "phone_number", "website", "description", "fediverse_profile", "facebook", optional_data = ["email", "phone_number", "website", "description", "fediverse_profile", "facebook",
"instagram"] "instagram"]
# Check if rescue organization exits # Check if rescue organization exists
search_data = {"external_source_identifier": "OSM", search_data = {"external_source_identifier": "OSM",
"external_object_identifier": f"{tierheim["id"]}"} "external_object_identifier": f"{tierheim["id"]}"}
search_result = requests.get(f"{instance}/api/organizations", params=search_data, headers=h) search_result = requests.get(f"{instance}/api/organizations", params=search_data, headers=h)
# Rescue organization exits
if search_result.status_code == 200: if search_result.status_code == 200:
stats["num_updated_orgs"] += 1
org_id = search_result.json()[0]["id"] org_id = search_result.json()[0]["id"]
logging.debug(f"{th_data.name} already exists as ID {org_id}.") logging.debug(f"{th_data.name} already exists as ID {org_id}.")
org_patch_data = {"id": org_id, org_patch_data = {"id": org_id,
@@ -243,7 +252,9 @@ def main():
if result.status_code != 200: if result.status_code != 200:
logging.warning(f"Updating {tierheim['properties']['name']} failed:{result.status_code} {result.json()}") logging.warning(f"Updating {tierheim['properties']['name']} failed:{result.status_code} {result.json()}")
continue continue
# Rescue organization does not exist
else: else:
stats["num_inserted_orgs"] += 1
location = create_location(tierheim, instance, h) location = create_location(tierheim, instance, h)
org_data = {"name": tierheim["properties"]["name"], org_data = {"name": tierheim["properties"]["name"],
"external_object_identifier": f"{tierheim["id"]}", "external_object_identifier": f"{tierheim["id"]}",
@@ -257,6 +268,7 @@ def main():
if result.status_code != 201: if result.status_code != 201:
print(f"{idx} {tierheim["properties"]["name"]}:{result.status_code} {result.json()}") print(f"{idx} {tierheim["properties"]["name"]}:{result.status_code} {result.json()}")
print(f"Upload finished. Inserted {stats['num_inserted_orgs']} new orgs and updated {stats['num_updated_orgs']} orgs.")
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -7,30 +7,26 @@ from django.utils.html import format_html
from django.urls import reverse from django.urls import reverse
from django.utils.http import urlencode from django.utils.http import urlencode
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \ from .models import Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
SpeciesSpecificURL, ImportantLocation, SpeciesSpecialization SpeciesSpecificURL, ImportantLocation, SocialMediaPost
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \ from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, Notification Comment, Announcement, User, Subscriptions, Notification
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .tools.model_helpers import AdoptionNoticeStatusChoices
class StatusInline(admin.StackedInline):
model = AdoptionNoticeStatus
@admin.register(AdoptionNotice) @admin.register(AdoptionNotice)
class AdoptionNoticeAdmin(admin.ModelAdmin): class AdoptionNoticeAdmin(admin.ModelAdmin):
search_fields = ("name__icontains", "description__icontains") search_fields = ("name__icontains", "description__icontains")
list_filter = ("owner",) list_filter = ("owner",)
inlines = [
StatusInline,
]
actions = ("activate",) actions = ("activate",)
def activate(self, request, queryset): def activate(self, request, queryset):
for obj in queryset: for obj in queryset:
obj.set_active() obj.adoption_notice_status = AdoptionNoticeStatusChoices.Active.SEARCHING
obj.save()
activate.short_description = _("Ausgewählte Vermittlungen aktivieren") activate.short_description = _("Ausgewählte Vermittlungen aktivieren")
@@ -100,11 +96,6 @@ class SpeciesSpecificURLInline(admin.StackedInline):
model = SpeciesSpecificURL model = SpeciesSpecificURL
class SpeciesSpecializationInline(admin.StackedInline):
model = SpeciesSpecialization
extra = 0
@admin.register(RescueOrganization) @admin.register(RescueOrganization)
class RescueOrganizationAdmin(admin.ModelAdmin): class RescueOrganizationAdmin(admin.ModelAdmin):
search_fields = ("name", "description", "internal_comment", "location_string", "location__city") search_fields = ("name", "description", "internal_comment", "location_string", "location__city")
@@ -112,7 +103,6 @@ class RescueOrganizationAdmin(admin.ModelAdmin):
list_filter = ("allows_using_materials", "trusted", ("external_source_identifier", EmptyFieldListFilter)) list_filter = ("allows_using_materials", "trusted", ("external_source_identifier", EmptyFieldListFilter))
inlines = [ inlines = [
SpeciesSpecializationInline,
SpeciesSpecificURLInline, SpeciesSpecificURLInline,
] ]
@@ -169,6 +159,18 @@ class LocationAdmin(admin.ModelAdmin):
] ]
@admin.register(SocialMediaPost)
class SocialMediaPostAdmin(admin.ModelAdmin):
list_filter = ("platform",)
@admin.register(Log)
class LogAdmin(admin.ModelAdmin):
ordering = ["-created_at"]
list_filter = ("action",)
list_display = ("action", "user", "created_at")
admin.site.register(Animal) admin.site.register(Animal)
admin.site.register(Species) admin.site.register(Species)
admin.site.register(Rule) admin.site.register(Rule)
@@ -176,7 +178,5 @@ admin.site.register(Image)
admin.site.register(ModerationAction) admin.site.register(ModerationAction)
admin.site.register(Language) admin.site.register(Language)
admin.site.register(Announcement) admin.site.register(Announcement)
admin.site.register(AdoptionNoticeStatus)
admin.site.register(Subscriptions) admin.site.register(Subscriptions)
admin.site.register(Log)
admin.site.register(Timestamp) admin.site.register(Timestamp)

View File

@@ -3,6 +3,21 @@ from rest_framework import serializers
import math import math
class ImageSerializer(serializers.ModelSerializer):
width = serializers.SerializerMethodField()
height = serializers.SerializerMethodField()
class Meta:
model = Image
fields = ['id', 'image', 'alt_text', 'width', 'height']
def get_width(self, obj):
return obj.image.width
def get_height(self, obj):
return obj.image.height
class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer): class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
location = serializers.PrimaryKeyRelatedField( location = serializers.PrimaryKeyRelatedField(
queryset=Location.objects.all(), queryset=Location.objects.all(),
@@ -20,17 +35,18 @@ class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
required=False, required=False,
allow_null=True allow_null=True
) )
url = serializers.SerializerMethodField()
photos = serializers.PrimaryKeyRelatedField( photos = ImageSerializer(many=True, read_only=True)
queryset=Image.objects.all(),
many=True, def get_url(self, obj):
required=False return obj.get_full_url()
)
class Meta: class Meta:
model = AdoptionNotice model = AdoptionNotice
fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information", fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information",
"group_only", "location", "location_details", "organization", "photos"] "group_only", "location", "location_details", "organization", "photos", "adoption_notice_status",
"url"]
class AdoptionNoticeGeoJSONSerializer(serializers.ModelSerializer): class AdoptionNoticeGeoJSONSerializer(serializers.ModelSerializer):

View File

@@ -2,7 +2,7 @@ from django.urls import path
from .views import ( from .views import (
AdoptionNoticeApiView, AdoptionNoticeApiView,
AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView, LocationApiView, AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView, LocationApiView,
AdoptionNoticeGeoJSONView, RescueOrgGeoJSONView AdoptionNoticeGeoJSONView, RescueOrgGeoJSONView, AdoptionNoticePerOrgApiView
) )
urlpatterns = [ urlpatterns = [
@@ -14,6 +14,7 @@ urlpatterns = [
path("organizations/", RescueOrganizationApiView.as_view(), name="api-organization-list"), path("organizations/", RescueOrganizationApiView.as_view(), name="api-organization-list"),
path("organizations.geojson", RescueOrgGeoJSONView.as_view(), name="api-organization-list-geojson"), path("organizations.geojson", RescueOrgGeoJSONView.as_view(), name="api-organization-list-geojson"),
path("organizations/<int:id>/", RescueOrganizationApiView.as_view(), name="api-organization-detail"), path("organizations/<int:id>/", RescueOrganizationApiView.as_view(), name="api-organization-detail"),
path("organizations/<int:id>/adoption-notices", AdoptionNoticePerOrgApiView.as_view(), name="api-organization-adoption-notices"),
path("images/", AddImageApiView.as_view(), name="api-add-image"), path("images/", AddImageApiView.as_view(), name="api-add-image"),
path("species/", SpeciesApiView.as_view(), name="api-species-list"), path("species/", SpeciesApiView.as_view(), name="api-species-list"),
path("locations/", LocationApiView.as_view(), name="api-locations-list"), path("locations/", LocationApiView.as_view(), name="api-locations-list"),

View File

@@ -1,11 +1,12 @@
from django.db.models import Q from django.db.models import Q
from drf_spectacular.types import OpenApiTypes
from rest_framework.generics import ListAPIView from rest_framework.generics import ListAPIView
from fellchensammlung.api.serializers import LocationSerializer, AdoptionNoticeGeoJSONSerializer from fellchensammlung.api.serializers import LocationSerializer, AdoptionNoticeGeoJSONSerializer
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from django.db import transaction from django.db import transaction
from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel, Location, AdoptionNoticeStatus from fellchensammlung.models import Log, TrustLevel, Location, AdoptionNoticeStatusChoices
from fellchensammlung.tasks import post_adoption_notice_save, post_rescue_org_save from fellchensammlung.tasks import post_adoption_notice_save, post_rescue_org_save
from rest_framework import status, serializers from rest_framework import status, serializers
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
@@ -20,7 +21,7 @@ from .serializers import (
SpeciesSerializer, RescueOrganizationSerializer, SpeciesSerializer, RescueOrganizationSerializer,
) )
from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image
from drf_spectacular.utils import extend_schema, inline_serializer from drf_spectacular.utils import extend_schema, inline_serializer, OpenApiParameter
class AdoptionNoticeApiView(APIView): class AdoptionNoticeApiView(APIView):
@@ -73,9 +74,9 @@ class AdoptionNoticeApiView(APIView):
# Only set active when user has trust level moderator or higher # Only set active when user has trust level moderator or higher
if request.user_to_notify.trust_level >= TrustLevel.MODERATOR: if request.user_to_notify.trust_level >= TrustLevel.MODERATOR:
adoption_notice.set_active() adoption_notice.adoption_notice_status = AdoptionNoticeStatusChoices.Active.SEARCHING
else: else:
adoption_notice.set_unchecked() adoption_notice.adoption_notice_status = AdoptionNoticeStatusChoices.AwaitingAction.WAITING_FOR_REVIEW
# Log the action # Log the action
Log.objects.create( Log.objects.create(
@@ -360,9 +361,9 @@ class LocationApiView(APIView):
# Log the action # Log the action
Log.objects.create( Log.objects.create(
user=request.user_to_notify, user=request.user,
action="add_location", action="add_location",
text=f"{request.user_to_notify} added adoption notice {location.pk} via API", text=f"{request.user} added adoption notice {location.pk} via API",
) )
# Return success response with new adoption notice details # Return success response with new adoption notice details
@@ -374,7 +375,7 @@ class LocationApiView(APIView):
class AdoptionNoticeGeoJSONView(ListAPIView): class AdoptionNoticeGeoJSONView(ListAPIView):
queryset = AdoptionNotice.objects.select_related('location').filter(location__isnull=False).filter( queryset = AdoptionNotice.objects.select_related('location').filter(location__isnull=False).filter(
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE) adoption_notice_status__in=AdoptionNoticeStatusChoices.Active.values)
serializer_class = AdoptionNoticeGeoJSONSerializer serializer_class = AdoptionNoticeGeoJSONSerializer
renderer_classes = [GeoJSONRenderer] renderer_classes = [GeoJSONRenderer]
@@ -383,3 +384,69 @@ class RescueOrgGeoJSONView(ListAPIView):
queryset = RescueOrganization.objects.select_related('location').filter(location__isnull=False) queryset = RescueOrganization.objects.select_related('location').filter(location__isnull=False)
serializer_class = RescueOrgeGeoJSONSerializer serializer_class = RescueOrgeGeoJSONSerializer
renderer_classes = [GeoJSONRenderer] renderer_classes = [GeoJSONRenderer]
class AdoptionNoticePerOrgApiView(APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
parameters=[
OpenApiParameter(
name='id',
required=False,
description='ID of the rescue organization from which to retrieve adoption notices.',
type=OpenApiTypes.INT
),
OpenApiParameter(
name='in_hierarchy',
type=OpenApiTypes.BOOL,
required=False,
description='Show all Adoption Notices in hierarchy.',
),
OpenApiParameter(
name='status',
type=OpenApiTypes.STR,
required=False,
description='Show all Adoption Notices in a certain status. Comma separated list of values e.g. '
'"active,closed"',
),
],
responses={200: AdoptionNoticeSerializer(many=True)}
)
def get(self, request, *args, **kwargs):
"""
Retrieve adoption notices with their related animals and images.
"""
org_id = kwargs.get("id")
in_hierarchy = request.query_params.get("in_hierarchy")
an_status = request.query_params.get("status")
try:
org = RescueOrganization.objects.get(id=org_id)
except RescueOrganization.DoesNotExist:
return Response({"error": "Rescue Organization notice not found."}, status=status.HTTP_404_NOT_FOUND)
if in_hierarchy:
adoption_notices = org.adoption_notices_in_hierarchy
else:
adoption_notices = AdoptionNotice.objects.filter(organization=org)
if an_status:
status_list = an_status.lower().strip().split(",")
temporary_an_storage = []
if "active" in status_list:
active_ans = [adoption_notice for adoption_notice in adoption_notices if
adoption_notice.adoption_notice_status in AdoptionNoticeStatusChoices.Active.values]
temporary_an_storage.extend(active_ans)
if "closed" in status_list:
closed_ans = [adoption_notice for adoption_notice in adoption_notices if
adoption_notice.adoption_notice_status in AdoptionNoticeStatusChoices.Closed.values]
temporary_an_storage.extend(closed_ans)
if "disabled" in status_list:
disabled_ans = [adoption_notice for adoption_notice in adoption_notices if
adoption_notice.adoption_notice_status in AdoptionNoticeStatusChoices.Disabled.values]
temporary_an_storage.extend(disabled_ans)
if "awaiting_action" in status_list:
awaiting_action_ans = [adoption_notice for adoption_notice in adoption_notices if
adoption_notice.adoption_notice_status in AdoptionNoticeStatusChoices.AwaitingAction.values]
temporary_an_storage.extend(awaiting_action_ans)
adoption_notices = temporary_an_storage
serializer = AdoptionNoticeSerializer(adoption_notices, many=True, context={"request": request})
return Response(serializer.data, status=status.HTTP_200_OK)

View File

View File

@@ -0,0 +1,37 @@
from django.shortcuts import get_object_or_404, render
from django.views.decorators.clickjacking import xframe_options_exempt
from fellchensammlung.aviews.helpers import headers
from fellchensammlung.models import RescueOrganization, AdoptionNotice, Species
@xframe_options_exempt
@headers({"X-Robots-Tag": "noindex"})
def list_ans_per_rescue_organization(request, rescue_organization_id, species_slug=None, active=True):
expand = request.GET.get("expand")
background_color = request.GET.get("background_color")
if expand is not None:
expand = True
else:
expand = False
org = get_object_or_404(RescueOrganization, pk=rescue_organization_id)
# Get only active adoption notices or all
if active:
adoption_notices_of_org = org.adoption_notices_in_hierarchy_divided_by_status[0]
else:
adoption_notices_of_org = org.adoption_notices
# Filter for Species if necessary
if species_slug is None:
adoption_notices = adoption_notices_of_org
else:
species = get_object_or_404(Species, slug=species_slug)
adoption_notices = [adoption_notice for adoption_notice in adoption_notices_of_org if
species in adoption_notice.species]
template = 'fellchensammlung/embeddables/list-adoption-notices.html'
return render(request, template,
context={"adoption_notices": adoption_notices,
"expand": expand,
"background_color": background_color})

View File

@@ -0,0 +1,23 @@
def headers(headers):
"""Decorator adding arbitrary HTTP headers to the response.
This decorator adds HTTP headers specified in the argument (map), to the
HTTPResponse returned by the function being decorated.
Example:
@headers({'Refresh': '10', 'X-Bender': 'Bite my shiny, metal ass!'})
def index(request):
....
Source: https://djangosnippets.org/snippets/275/
"""
def headers_wrapper(fun):
def wrapped_function(*args, **kwargs):
response = fun(*args, **kwargs)
for key in headers:
response[key] = headers[key]
return response
return wrapped_function
return headers_wrapper

View File

@@ -0,0 +1,12 @@
from django.urls import path
from . import embeddables
urlpatterns = [
path("tierschutzorganisationen/<int:rescue_organization_id>/vermittlungen/",
embeddables.list_ans_per_rescue_organization,
name="list-adoption-notices-for-rescue-organization"),
path("tierschutzorganisationen/<int:rescue_organization_id>/vermittlungen/<slug:species_slug>/",
embeddables.list_ans_per_rescue_organization,
name="list-adoption-notices-for-rescue-organization-species"),
]

View File

@@ -1,4 +1,5 @@
from django import forms from django import forms
from django.forms.widgets import Textarea
from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportComment, ModerationAction, User, Species, \ from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportComment, ModerationAction, User, Species, \
Comment, SexChoicesWithAll, DistanceChoices, SpeciesSpecificURL, RescueOrganization Comment, SexChoicesWithAll, DistanceChoices, SpeciesSpecificURL, RescueOrganization
@@ -9,6 +10,8 @@ from django.utils.translation import gettext_lazy as _
from notfellchen.settings import MEDIA_URL from notfellchen.settings import MEDIA_URL
from crispy_forms.layout import Div from crispy_forms.layout import Div
from .tools.model_helpers import reason_for_signup_label, reason_for_signup_help_text
def animal_validator(value: str): def animal_validator(value: str):
value = value.lower() value = value.lower()
@@ -57,6 +60,14 @@ class AnimalForm(forms.ModelForm):
} }
class UpdateRescueOrgRegularCheckStatus(forms.ModelForm):
template_name = "fellchensammlung/forms/form_snippets.html"
class Meta:
model = RescueOrganization
fields = ["regular_check_status"]
class ImageForm(forms.ModelForm): class ImageForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if 'in_flow' in kwargs: if 'in_flow' in kwargs:
@@ -129,6 +140,18 @@ class ModerationActionForm(forms.ModelForm):
fields = ('action', 'public_comment', 'private_comment') fields = ('action', 'public_comment', 'private_comment')
class AddedRegistrationForm(forms.Form):
reason_for_signup = forms.CharField(label=reason_for_signup_label,
help_text=reason_for_signup_help_text,
widget=Textarea)
captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_(
"Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um "
"sicherzustellen, dass du kein Roboter bist."))
def signup(self, request, user):
pass
class CustomRegistrationForm(RegistrationForm): class CustomRegistrationForm(RegistrationForm):
class Meta(RegistrationForm.Meta): class Meta(RegistrationForm.Meta):
model = User model = User
@@ -147,3 +170,11 @@ class AdoptionNoticeSearchForm(forms.Form):
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED, max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED,
label=_("Suchradius")) label=_("Suchradius"))
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False) location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
class RescueOrgSearchForm(forms.Form):
template_name = "fellchensammlung/forms/form_snippets.html"
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.TWENTY,
label=_("Suchradius"))

View File

@@ -7,29 +7,27 @@ from django.utils.translation import gettext_lazy as _
from django.conf import settings from django.conf import settings
from django.core import mail from django.core import mail
from fellchensammlung.models import User, Notification, TrustLevel, NotificationTypeChoices from fellchensammlung.models import User, Notification, TrustLevel, NotificationTypeChoices
from notfellchen.settings import base_url from fellchensammlung.tools.model_helpers import ndm
NEWLINE = "\r\n"
def mail_admins_new_report(report): def notify_mods_new_report(report, notification_type):
""" """
Sends an e-mail to all users that should handle the report. Sends an e-mail to all users that should handle the report.
""" """
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR): for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
report_url = base_url + report.get_absolute_url() if notification_type == NotificationTypeChoices.NEW_REPORT_AN:
context = {"report_url": report_url, title = _("Vermittlung gemeldet")
"user_comment": report.user_comment, } elif notification_type == NotificationTypeChoices.NEW_REPORT_COMMENT:
title = _("Kommentar gemeldet")
subject = _("Neue Meldung") else:
html_message = render_to_string('fellchensammlung/mail/notifications/report.html', context) raise NotImplementedError
plain_message = strip_tags(html_message) notification = Notification.objects.create(
notification_type=notification_type,
mail.send_mail(subject, user_to_notify=moderator,
plain_message, report=report,
from_email="info@notfellchen.org", title=title,
recipient_list=[moderator.email], )
html_message=html_message) notification.save()
def send_notification_email(notification_pk): def send_notification_email(notification_pk):
@@ -37,24 +35,9 @@ def send_notification_email(notification_pk):
subject = f"{notification.title}" subject = f"{notification.title}"
context = {"notification": notification, } context = {"notification": notification, }
if notification.notification_type == NotificationTypeChoices.NEW_REPORT_COMMENT or notification.notification_type == NotificationTypeChoices.NEW_REPORT_AN: html_message = render_to_string(ndm[notification.notification_type].email_html_template, context)
context["user_comment"] = notification.report.user_comment plain_message = render_to_string(ndm[notification.notification_type].email_plain_template, context)
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, mail.send_mail(subject, plain_message, settings.DEFAULT_FROM_EMAIL,
[notification.user_to_notify.email], [notification.user_to_notify.email],
html_message=html_message) html_message=html_message)

View File

@@ -0,0 +1,13 @@
from django.core.management import BaseCommand
from fellchensammlung.tools.admin import mask_organization_contact_data
class Command(BaseCommand):
help = 'Mask e-mail addresses and phone numbers of organizations for testing purposes.'
def add_arguments(self, parser):
parser.add_argument("domain", type=str)
def handle(self, *args, **options):
domain = options["domain"]
mask_organization_contact_data(domain)

View File

@@ -0,0 +1,19 @@
from django.core.management import BaseCommand
from tqdm import tqdm
from fellchensammlung.models import RescueOrganization
from fellchensammlung.tools.twenty import sync_rescue_org_to_twenty
class Command(BaseCommand):
help = 'Send rescue organizations as companies to twenty'
def add_arguments(self, parser):
parser.add_argument("base_url", type=str)
parser.add_argument("token", type=str)
def handle(self, *args, **options):
base_url = options["base_url"]
token = options["token"]
for rescue_org in tqdm(RescueOrganization.objects.all()):
sync_rescue_org_to_twenty(rescue_org, base_url, token)

View File

@@ -0,0 +1,25 @@
# Generated by Django 5.2.1 on 2025-07-13 10:54
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0054_alter_notification_comment'),
]
operations = [
migrations.AddField(
model_name='rescueorganization',
name='ongoing_communication',
field=models.BooleanField(default=False, help_text='Es findet gerade Kommunikation zwischen Notfellchen und der Organisation statt.', verbose_name='In aktiver Kommunikation'),
),
migrations.AlterField(
model_name='notification',
name='user_to_notify',
field=models.ForeignKey(help_text='Useraccount der Benachrichtigt wird', on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL, verbose_name='Empfänger*in'),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.2.1 on 2025-07-14 05:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0055_rescueorganization_ongoing_communication_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='rescueorganization',
options={'ordering': ['name']},
),
migrations.AddField(
model_name='rescueorganization',
name='specializations',
field=models.ManyToManyField(to='fellchensammlung.species'),
),
]

View File

@@ -0,0 +1,16 @@
# Generated by Django 5.2.1 on 2025-07-14 05:15
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0056_alter_rescueorganization_options_and_more'),
]
operations = [
migrations.DeleteModel(
name='SpeciesSpecialization',
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 5.2.1 on 2025-07-19 17:48
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0057_delete_speciesspecialization'),
]
operations = [
migrations.CreateModel(
name='SocialMediaPost',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateField(default=django.utils.timezone.now, verbose_name='Erstellt am')),
('platform', models.CharField(choices=[('fediverse', 'Fediverse')], max_length=255, verbose_name='Social Media Platform')),
('url', models.URLField(verbose_name='URL')),
('adoption_notice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')),
],
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.1 on 2025-08-02 09:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0058_socialmediapost'),
]
operations = [
migrations.AddField(
model_name='rescueorganization',
name='twenty_id',
field=models.UUIDField(blank=True, help_text='ID der der Organisation in Twenty', null=True, verbose_name='Twenty-ID'),
),
migrations.AlterField(
model_name='rescueorganization',
name='specializations',
field=models.ManyToManyField(blank=True, to='fellchensammlung.species'),
),
]

View File

@@ -0,0 +1,87 @@
# Generated by Django 5.2.1 on 2025-08-30 21:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0059_rescueorganization_twenty_id_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='adoptionnotice',
options={'permissions': [('create_active_adoption_notice', 'Can create an active adoption notice')], 'verbose_name': 'Vermittlung', 'verbose_name_plural': 'Vermittlungen'},
),
migrations.AlterModelOptions(
name='adoptionnoticestatus',
options={'verbose_name': 'Vermittlungsstatus', 'verbose_name_plural': 'Vermittlungsstati'},
),
migrations.AlterModelOptions(
name='animal',
options={'verbose_name': 'Tier', 'verbose_name_plural': 'Tiere'},
),
migrations.AlterModelOptions(
name='announcement',
options={'verbose_name': 'Banner', 'verbose_name_plural': 'Banner'},
),
migrations.AlterModelOptions(
name='comment',
options={'verbose_name': 'Kommentar', 'verbose_name_plural': 'Kommentare'},
),
migrations.AlterModelOptions(
name='image',
options={'verbose_name': 'Bild', 'verbose_name_plural': 'Bilder'},
),
migrations.AlterModelOptions(
name='importantlocation',
options={'verbose_name': 'Wichtiger Standort', 'verbose_name_plural': 'Wichtige Standorte'},
),
migrations.AlterModelOptions(
name='location',
options={'verbose_name': 'Standort', 'verbose_name_plural': 'Standorte'},
),
migrations.AlterModelOptions(
name='moderationaction',
options={'verbose_name': 'Moderationsaktion', 'verbose_name_plural': 'Moderationsaktionen'},
),
migrations.AlterModelOptions(
name='notification',
options={'verbose_name': 'Benachrichtigung', 'verbose_name_plural': 'Benachrichtigungen'},
),
migrations.AlterModelOptions(
name='report',
options={'verbose_name': 'Meldung', 'verbose_name_plural': 'Meldungen'},
),
migrations.AlterModelOptions(
name='rescueorganization',
options={'ordering': ['name'], 'verbose_name': 'Tierschutzorganisation', 'verbose_name_plural': 'Tierschutzorganisationen'},
),
migrations.AlterModelOptions(
name='rule',
options={'verbose_name': 'Regel', 'verbose_name_plural': 'Regeln'},
),
migrations.AlterModelOptions(
name='searchsubscription',
options={'verbose_name': 'Abonnierte Suche', 'verbose_name_plural': 'Abonnierte Suchen'},
),
migrations.AlterModelOptions(
name='speciesspecificurl',
options={'verbose_name': 'Tierartspezifische URL', 'verbose_name_plural': 'Tierartspezifische URLs'},
),
migrations.AlterModelOptions(
name='subscriptions',
options={'verbose_name': 'Abonnement', 'verbose_name_plural': 'Abonnements'},
),
migrations.AlterModelOptions(
name='timestamp',
options={'verbose_name': 'Zeitstempel', 'verbose_name_plural': 'Zeitstempel'},
),
migrations.AddField(
model_name='adoptionnotice',
name='adoption_notice_status',
field=models.TextField(choices=[('active_searching', 'Searching'), ('active_interested', 'Interested'), ('awaiting_action_waiting_for_review', 'Waiting for review'), ('awaiting_action_needs_additional_info', 'Needs additional info'), ('closed_successful_with_notfellchen', 'Successful (with Notfellchen)'), ('closed_successful_without_notfellchen', 'Successful (without Notfellchen)'), ('closed_animal_died', 'Animal died'), ('closed_for_other_adoption_notice', 'Closed for other adoption notice'), ('closed_not_open_for_adoption_anymore', 'Not open for adoption anymore'), ('closed_other', 'Other (closed)'), ('disabled_against_the_rules', 'Against the rules'), ('disabled_unchecked', 'Unchecked'), ('disabled_other', 'Other (disabled)')], default='disabled_other', max_length=64, verbose_name='Status'),
preserve_default=False,
),
]

View File

@@ -0,0 +1,63 @@
# Generated by Django 5.2.1 on 2025-08-30 21:51
import logging
from django.db import migrations
def map_status(adoption_notice_status):
minor = adoption_notice_status.minor_status
if minor == "searching":
return "active_searching"
if minor == "interested":
return "active_interested"
if minor == "waiting_for_review":
return "awaiting_action_waiting_for_review"
if minor == "needs_additional_info":
return "awaiting_action_needs_additional_info"
if minor == "successful_with_notfellchen":
return "closed_successful_with_notfellchen"
if minor == "successful_without_notfellchen":
return "closed_successful_without_notfellchen"
if minor == "animal_died":
return "closed_animal_died"
if minor == "closed_for_other_adoption_notice":
return "closed_for_other_adoption_notice"
if minor == "not_open_for_adoption_anymore":
return "closed_not_open_for_adoption_anymore"
if minor == "other":
return "closed_other"
if minor == "against_the_rules":
return "disabled_against_the_rules"
if minor == "unchecked":
return "disabled_unchecked"
if minor in ["missing_information", "technical_error"]:
return "disabled_other"
return None
def migrate_status(apps, schema_editor):
# We can't import the model directly as it may be a newer
# version than this migration expects. We use the historical version.
AdoptionNoticeStatus = apps.get_model("fellchensammlung", "AdoptionNoticeStatus")
AdoptionNotice = apps.get_model("fellchensammlung", "AdoptionNotice")
for ans in AdoptionNoticeStatus.objects.all():
adoption_notice = AdoptionNotice.objects.get(id=ans.adoption_notice.id)
new_status = map_status(ans)
logging.debug(f"{ans.minor_status} -> {new_status}")
adoption_notice.adoption_notice_status = map_status(ans)
adoption_notice.save()
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0060_alter_adoptionnotice_options_and_more'),
]
operations = [
migrations.RunPython(migrate_status),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 5.2.1 on 2025-08-30 22:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0061_datamigration_status_model_to_field'),
]
operations = [
migrations.AlterField(
model_name='adoptionnotice',
name='adoption_notice_status',
field=models.TextField(choices=[('active_searching', 'Searching'), ('active_interested', 'Interested'), ('awaiting_action_waiting_for_review', 'Waiting for review'), ('awaiting_action_needs_additional_info', 'Needs additional info'), ('closed_successful_with_notfellchen', 'Successful (with Notfellchen)'), ('closed_successful_without_notfellchen', 'Successful (without Notfellchen)'), ('closed_animal_died', 'Animal died'), ('closed_for_other_adoption_notice', 'Closed for other adoption notice'), ('closed_not_open_for_adoption_anymore', 'Not open for adoption anymore'), ('closed_link_to_more_info_not_reachable', 'Der Link zu weiteren Informationen ist nicht mehr erreichbar.'), ('closed_other', 'Other (closed)'), ('disabled_against_the_rules', 'Against the rules'), ('disabled_unchecked', 'Unchecked'), ('disabled_other', 'Other (disabled)')], max_length=64, verbose_name='Status'),
),
migrations.DeleteModel(
name='AdoptionNoticeStatus',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.1 on 2025-09-05 14:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0062_alter_adoptionnotice_adoption_notice_status_and_more'),
]
operations = [
migrations.AddField(
model_name='adoptionnotice',
name='adoption_process',
field=models.TextField(blank=True, choices=[('contact_person_in_an', 'Kontaktiere die Person im Vermittlungstext')], max_length=64, null=True, verbose_name='Adoptionsprozess'),
),
]

View File

@@ -0,0 +1,140 @@
# Generated by Django 5.2.1 on 2025-09-06 11:11
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0063_adoptionnotice_adoption_process'),
]
operations = [
migrations.AlterField(
model_name='animal',
name='name',
field=models.CharField(max_length=200, verbose_name='Name'),
),
migrations.AlterField(
model_name='animal',
name='photos',
field=models.ManyToManyField(blank=True, to='fellchensammlung.image', verbose_name='Fotos'),
),
migrations.AlterField(
model_name='animal',
name='sex',
field=models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich, kastriert'), ('I', 'Intergeschlechtlich')], max_length=20, verbose_name='Geschlecht'),
),
migrations.AlterField(
model_name='comment',
name='adoption_notice',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung'),
),
migrations.AlterField(
model_name='image',
name='alt_text',
field=models.TextField(help_text='Beschreibe das Bild für blinde und sehbehinderte Menschen', max_length=2000, verbose_name='Alternativtext'),
),
migrations.AlterField(
model_name='location',
name='city',
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Stadt'),
),
migrations.AlterField(
model_name='location',
name='county',
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Landkreis'),
),
migrations.AlterField(
model_name='location',
name='housenumber',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Hausnummer'),
),
migrations.AlterField(
model_name='location',
name='latitude',
field=models.FloatField(verbose_name='Breitengrad'),
),
migrations.AlterField(
model_name='location',
name='longitude',
field=models.FloatField(verbose_name='Längengrad'),
),
migrations.AlterField(
model_name='location',
name='postcode',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Postleitzahl'),
),
migrations.AlterField(
model_name='location',
name='street',
field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Straße'),
),
migrations.AlterField(
model_name='notification',
name='user_to_notify',
field=models.ForeignKey(help_text='Useraccount der benachrichtigt wird', on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL, verbose_name='Empfänger*in'),
),
migrations.AlterField(
model_name='report',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am'),
),
migrations.AlterField(
model_name='report',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert am'),
),
migrations.AlterField(
model_name='rule',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am'),
),
migrations.AlterField(
model_name='rule',
name='language',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.language', verbose_name='Sprache'),
),
migrations.AlterField(
model_name='rule',
name='rule_identifier',
field=models.CharField(help_text='Ein eindeutiger Identifikator der Regel. Ein Regelobjekt derselben Regel in einer anderen Sprache muss den gleichen Identifikator haben', max_length=24, verbose_name='Regel-ID'),
),
migrations.AlterField(
model_name='rule',
name='rule_text',
field=models.TextField(verbose_name='Regeltext'),
),
migrations.AlterField(
model_name='rule',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert am'),
),
migrations.AlterField(
model_name='searchsubscription',
name='created_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am'),
),
migrations.AlterField(
model_name='searchsubscription',
name='sex',
field=models.CharField(choices=[('F', 'Weiblich'), ('M', 'Männlich'), ('M_N', 'Männlich, kastriert'), ('F_N', 'Weiblich Kastriert'), ('I', 'Intergeschlechtlich'), ('A', 'Alle')], max_length=20, verbose_name='Geschlecht'),
),
migrations.AlterField(
model_name='searchsubscription',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='Zuletzt geändert am'),
),
migrations.AlterField(
model_name='subscriptions',
name='adoption_notice',
field=models.ForeignKey(help_text='Vermittlung die abonniert wurde', on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung'),
),
migrations.AlterField(
model_name='text',
name='title',
field=models.CharField(max_length=100, verbose_name='Titel'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.1 on 2025-09-06 13:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0064_alter_animal_name_alter_animal_photos_and_more'),
]
operations = [
migrations.AddField(
model_name='species',
name='slug',
field=models.SlugField(null=True, unique=True, verbose_name='Slug'),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.2.1 on 2025-09-06 13:05
from django.db import migrations
def migrate_slug(apps, schema_editor):
Species = apps.get_model("fellchensammlung", "Species")
for species in Species.objects.all():
species.slug = f"species-{species.id}"
species.save()
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0065_species_slug'),
]
operations = [
migrations.RunPython(migrate_slug),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.1 on 2025-09-06 13:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0066_add_slug_to_species'),
]
operations = [
migrations.AlterField(
model_name='species',
name='slug',
field=models.SlugField(unique=True, verbose_name='Slug'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.1 on 2025-09-29 15:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0067_alter_species_slug'),
]
operations = [
migrations.AlterField(
model_name='adoptionnotice',
name='adoption_notice_status',
field=models.TextField(choices=[('active_searching', 'Searching'), ('active_interested', 'Interested'), ('awaiting_action_waiting_for_review', 'Waiting for review'), ('awaiting_action_needs_additional_info', 'Needs additional info'), ('awaiting_action_unchecked', 'Unchecked'), ('closed_successful_with_notfellchen', 'Successful (with Notfellchen)'), ('closed_successful_without_notfellchen', 'Successful (without Notfellchen)'), ('closed_animal_died', 'Animal died'), ('closed_for_other_adoption_notice', 'Closed for other adoption notice'), ('closed_not_open_for_adoption_anymore', 'Not open for adoption anymore'), ('closed_link_to_more_info_not_reachable', 'Der Link zu weiteren Informationen ist nicht mehr erreichbar.'), ('closed_other', 'Other (closed)'), ('disabled_against_the_rules', 'Against the rules'), ('disabled_other', 'Other (disabled)')], max_length=64, verbose_name='Status'),
),
migrations.AlterField(
model_name='image',
name='image',
field=models.ImageField(help_text='Wähle ein Bild aus', upload_to='images', verbose_name='Bild'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.1 on 2025-10-20 08:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0068_alter_adoptionnotice_adoption_notice_status_and_more'),
]
operations = [
migrations.AddField(
model_name='rescueorganization',
name='regular_check_status',
field=models.CharField(choices=[('regular_check', 'Wird regelmäßig geprüft'), ('excluded_no_online_listing', 'Exkludiert: Tiere werden nicht online gelistet'), ('excluded_other_org', 'Exkludiert: Andere Organisation wird geprüft'), ('excluded_scope', 'Exkludiert: Organisation hat nie Notfellchen-relevanten Vermittlungen'), ('excluded_other', 'Exkludiert: Anderer Grund')], default='regular_check', help_text='Organisationen können, durch ändern dieser Einstellung, von der regelmäßigen Prüfung ausgeschlossen werden.', max_length=30, verbose_name='Status der regelmäßigen Prüfung'),
),
]

View File

@@ -1,22 +1,21 @@
import uuid import uuid
from random import choices
from tabnanny import verbose
from django.db import models from django.db import models
from django.template.defaultfilters import slugify
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone from django.utils import timezone
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 Group
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
import base64
from .tools import misc, geo from .tools import misc, geo
from notfellchen.settings import MEDIA_URL, base_url from notfellchen.settings import MEDIA_URL, base_url
from .tools.geo import LocationProxy, Position from .tools.geo import LocationProxy, Position
from .tools.misc import age_as_hr_string, time_since_as_hr_string from .tools.misc import time_since_as_hr_string
from .tools.model_helpers import NotificationTypeChoices, AdoptionNoticeStatusChoices, AdoptionProcess, \
AdoptionNoticeStatusChoicesDescriptions, RegularCheckStatusChoices, reason_for_signup_label, \
reason_for_signup_help_text
from .tools.model_helpers import ndm as NotificationDisplayMapping
class Language(models.Model): class Language(models.Model):
@@ -42,14 +41,14 @@ class Language(models.Model):
class Location(models.Model): class Location(models.Model):
place_id = models.CharField(max_length=200) # OSM id place_id = models.CharField(max_length=200) # OSM id
latitude = models.FloatField() latitude = models.FloatField(verbose_name=_("Breitengrad"))
longitude = models.FloatField() longitude = models.FloatField(verbose_name=_("Längengrad"))
name = models.CharField(max_length=2000) name = models.CharField(max_length=2000)
city = models.CharField(max_length=200, blank=True, null=True) city = models.CharField(max_length=200, blank=True, null=True, verbose_name=_('Stadt'))
housenumber = models.CharField(max_length=20, blank=True, null=True) housenumber = models.CharField(max_length=20, blank=True, null=True, verbose_name=_("Hausnummer"))
postcode = models.CharField(max_length=20, blank=True, null=True) postcode = models.CharField(max_length=20, blank=True, null=True, verbose_name=_("Postleitzahl"))
street = models.CharField(max_length=200, blank=True, null=True) street = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Straße"))
county = models.CharField(max_length=200, blank=True, null=True) county = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Landkreis"))
# Country code as per ISO 3166-1 alpha-2 # Country code as per ISO 3166-1 alpha-2
# https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes # https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes
countrycode = models.CharField(max_length=2, verbose_name=_("Ländercode"), countrycode = models.CharField(max_length=2, verbose_name=_("Ländercode"),
@@ -58,6 +57,10 @@ class Location(models.Model):
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = _("Standort")
verbose_name_plural = _("Standorte")
def __str__(self): def __str__(self):
if self.city and self.postcode: if self.city and self.postcode:
return f"{self.city} ({self.postcode})" return f"{self.city} ({self.postcode})"
@@ -101,10 +104,17 @@ class Location(models.Model):
class ImportantLocation(models.Model): class ImportantLocation(models.Model):
class Meta:
verbose_name = _("Wichtiger Standort")
verbose_name_plural = _("Wichtige Standorte")
location = models.OneToOneField(Location, on_delete=models.CASCADE) location = models.OneToOneField(Location, on_delete=models.CASCADE)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
def get_absolute_url(self):
return reverse('search-by-location', kwargs={'important_location_slug': self.slug})
class ExternalSourceChoices(models.TextChoices): class ExternalSourceChoices(models.TextChoices):
OSM = "OSM", _("Open Street Map") OSM = "OSM", _("Open Street Map")
@@ -118,10 +128,24 @@ class AllowUseOfMaterialsChices(models.TextChoices):
USE_MATERIALS_NOT_ASKED = "not_asked", _("Not asked") USE_MATERIALS_NOT_ASKED = "not_asked", _("Not asked")
class RescueOrganization(models.Model): class Species(models.Model):
def __str__(self): """Model representing a species of animal."""
return f"{self.name}" name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
verbose_name=_('Name'))
slug = models.SlugField(unique=True, verbose_name=_('Slug'))
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
"""String for representing the Model object."""
return self.name
class Meta:
verbose_name = _('Tierart')
verbose_name_plural = _('Tierarten')
class RescueOrganization(models.Model):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig')) trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
allows_using_materials = models.CharField(max_length=200, allows_using_materials = models.CharField(max_length=200,
@@ -149,10 +173,29 @@ class RescueOrganization(models.Model):
exclude_from_check = models.BooleanField(default=False, verbose_name=_('Von Prüfung ausschließen'), exclude_from_check = models.BooleanField(default=False, verbose_name=_('Von Prüfung ausschließen'),
help_text=_("Organisation von der manuellen Überprüfung ausschließen, " help_text=_("Organisation von der manuellen Überprüfung ausschließen, "
"z.B. weil Tiere nicht online geführt werden")) "z.B. weil Tiere nicht online geführt werden"))
regular_check_status = models.CharField(max_length=30, choices=RegularCheckStatusChoices.choices,
default=RegularCheckStatusChoices.REGULAR_CHECK,
verbose_name=_('Status der regelmäßigen Prüfung'),
help_text=_(
"Organisationen können, durch ändern dieser Einstellung, von der "
"regelmäßigen Prüfung ausgeschlossen werden."))
ongoing_communication = models.BooleanField(default=False, verbose_name=_('In aktiver Kommunikation'),
help_text=_(
"Es findet gerade Kommunikation zwischen Notfellchen und der Organisation statt."))
parent_org = models.ForeignKey("RescueOrganization", on_delete=models.PROTECT, blank=True, null=True) parent_org = models.ForeignKey("RescueOrganization", on_delete=models.PROTECT, blank=True, null=True)
# allows to specify if a rescue organization has a specialization for dedicated species
specializations = models.ManyToManyField(Species, blank=True)
twenty_id = models.UUIDField(verbose_name=_("Twenty-ID"), null=True, blank=True,
help_text=_("ID der der Organisation in Twenty"))
class Meta: class Meta:
unique_together = ('external_object_identifier', 'external_source_identifier',) unique_together = ('external_object_identifier', 'external_source_identifier',)
ordering = ['name']
verbose_name = _("Tierschutzorganisation")
verbose_name_plural = _("Tierschutzorganisationen")
def __str__(self):
return f"{self.name}"
def clean(self): def clean(self):
super().clean() super().clean()
@@ -166,6 +209,29 @@ class RescueOrganization(models.Model):
def adoption_notices(self): def adoption_notices(self):
return AdoptionNotice.objects.filter(organization=self) return AdoptionNotice.objects.filter(organization=self)
@property
def adoption_notices_in_hierarchy(self):
"""
Shows all adoption notices of this rescue organization and all child organizations.
"""
adoption_notices_discovered = list(self.adoption_notices)
if self.child_organizations:
for child in self.child_organizations:
adoption_notices_discovered.extend(child.adoption_notices_in_hierarchy)
return adoption_notices_discovered
@property
def adoption_notices_in_hierarchy_divided_by_status(self):
"""Returns two lists of adoption notices, the first active, the other inactive."""
active_adoption_notices = []
inactive_adoption_notices = []
for an in self.adoption_notices_in_hierarchy:
if an.is_active:
active_adoption_notices.append(an)
else:
inactive_adoption_notices.append(an)
return active_adoption_notices, inactive_adoption_notices
@property @property
def position(self): def position(self):
if self.location: if self.location:
@@ -202,9 +268,17 @@ class RescueOrganization(models.Model):
""" """
return self.instagram or self.facebook or self.website or self.phone_number or self.email or self.fediverse_profile 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): @property
self.exclude_from_check = True def child_organizations(self):
self.save() return RescueOrganization.objects.filter(parent_org=self)
def in_distance(self, position, max_distance, unknown_true=True):
"""
Returns a boolean indicating if the Location of the adoption notice is within a given distance to the position
If the location is none, we by default return that the location is within the given distance
"""
return geo.object_in_distance(self, position, max_distance, unknown_true)
# Admins can perform all actions and have the highest trust associated with them # Admins can perform all actions and have the highest trust associated with them
@@ -234,8 +308,7 @@ class User(AbstractUser):
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
organization_affiliation = models.ForeignKey(RescueOrganization, on_delete=models.PROTECT, null=True, blank=True, organization_affiliation = models.ForeignKey(RescueOrganization, on_delete=models.PROTECT, null=True, blank=True,
verbose_name=_('Organisation')) verbose_name=_('Organisation'))
reason_for_signup = models.TextField(verbose_name=_("Grund für die Registrierung"), help_text=_( reason_for_signup = models.TextField(verbose_name=reason_for_signup_label, help_text=reason_for_signup_help_text)
"Wir würden gerne wissen warum du dich registriertst, ob du dich z.B. Tiere eines bestimmten Tierheim einstellen willst 'nur mal gucken' willst. Beides ist toll! Wenn du für ein Tierheim/eine Pflegestelle arbeitest kontaktieren wir dich ggf. um dir erweiterte Rechte zu geben."))
email_notifications = models.BooleanField(verbose_name=_("Benachrichtigung per E-Mail"), default=True) email_notifications = models.BooleanField(verbose_name=_("Benachrichtigung per E-Mail"), default=True)
REQUIRED_FIELDS = ["reason_for_signup", "email"] REQUIRED_FIELDS = ["reason_for_signup", "email"]
@@ -252,14 +325,17 @@ class User(AbstractUser):
def get_absolute_url(self): def get_absolute_url(self):
return reverse("user-detail", args=[str(self.pk)]) return reverse("user-detail", args=[str(self.pk)])
def get_full_url(self):
return f"{base_url}{self.get_absolute_url()}"
def get_notifications_url(self): def get_notifications_url(self):
return self.get_absolute_url() return self.get_absolute_url()
def get_unread_notifications(self): def get_unread_notifications(self):
return Notification.objects.filter(user=self, read=False) return Notification.objects.filter(user_to_notify=self, read=False)
def get_num_unread_notifications(self): def get_num_unread_notifications(self):
return Notification.objects.filter(user=self, read=False).count() return Notification.objects.filter(user_to_notify=self, read=False).count()
@property @property
def adoption_notices(self): def adoption_notices(self):
@@ -271,8 +347,9 @@ class User(AbstractUser):
class Image(models.Model): class Image(models.Model):
image = models.ImageField(upload_to='images') image = models.ImageField(upload_to='images', verbose_name=_("Bild"), help_text=_("Wähle ein Bild aus"))
alt_text = models.TextField(max_length=2000, verbose_name=_('Alternativtext')) alt_text = models.TextField(max_length=2000, verbose_name=_('Alternativtext'),
help_text=_("Beschreibe das Bild für blinde und sehbehinderte Menschen"))
owner = models.ForeignKey(User, on_delete=models.CASCADE) owner = models.ForeignKey(User, on_delete=models.CASCADE)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
@@ -280,25 +357,18 @@ class Image(models.Model):
def __str__(self): def __str__(self):
return self.alt_text return self.alt_text
class Meta:
verbose_name = _("Bild")
verbose_name_plural = _("Bilder")
@property @property
def as_html(self): def as_html(self):
return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">' return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">'
@property
class Species(models.Model): def as_base64(self):
"""Model representing a species of animal.""" encoded_string = base64.b64encode(self.image.file.read())
name = models.CharField(max_length=200, help_text=_('Name der Tierart'), return encoded_string.decode("utf-8")
verbose_name=_('Name'))
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
"""String for representing the Model object."""
return self.name
class Meta:
verbose_name = _('Tierart')
verbose_name_plural = _('Tierarten')
class AdoptionNotice(models.Model): class AdoptionNotice(models.Model):
@@ -306,11 +376,11 @@ class AdoptionNotice(models.Model):
permissions = [ permissions = [
("create_active_adoption_notice", "Can create an active adoption notice"), ("create_active_adoption_notice", "Can create an active adoption notice"),
] ]
verbose_name = _("Vermittlung")
verbose_name_plural = _("Vermittlungen")
def __str__(self): def __str__(self):
if not hasattr(self, 'adoptionnoticestatus'):
return self.name return self.name
return f"[{self.adoptionnoticestatus.as_string()}] {self.name}"
created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now) created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@@ -330,6 +400,11 @@ class AdoptionNotice(models.Model):
location_string = models.CharField(max_length=200, verbose_name=_("Ortsangabe")) location_string = models.CharField(max_length=200, verbose_name=_("Ortsangabe"))
location = models.ForeignKey(Location, blank=True, null=True, on_delete=models.SET_NULL, ) location = models.ForeignKey(Location, blank=True, null=True, on_delete=models.SET_NULL, )
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Creator')) owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Creator'))
adoption_notice_status = models.TextField(max_length=64, verbose_name=_('Status'),
choices=AdoptionNoticeStatusChoices.all_choices())
adoption_process = models.TextField(null=True, blank=True,
max_length=64, verbose_name=_('Adoptionsprozess'),
choices=AdoptionProcess)
@property @property
def animals(self): def animals(self):
@@ -344,11 +419,19 @@ class AdoptionNotice(models.Model):
@property @property
def num_per_sex(self): def num_per_sex(self):
print(f"{self.pk} x")
num_per_sex = dict() num_per_sex = dict()
for sex in SexChoices: for sex in SexChoices:
num_per_sex[sex] = self.animals.filter(sex=sex).count num_per_sex[sex] = len([animal for animal in self.animals if animal.sex == sex])
return num_per_sex return num_per_sex
@property
def species(self):
species = set()
for animal in self.animals:
species.add(animal.species)
return species
@property @property
def last_checked_hr(self): def last_checked_hr(self):
time_since_last_checked = timezone.now() - self.last_checked time_since_last_checked = timezone.now() - self.last_checked
@@ -378,12 +461,21 @@ class AdoptionNotice(models.Model):
else: else:
return self.location.latitude, self.location.longitude return self.location.latitude, self.location.longitude
@property def _get_short_description(self, length: int) -> str:
def description_short(self):
if self.description is None: if self.description is None:
return "" return ""
if len(self.description) > 200: elif len(self.description) > length:
return self.description[:200] + f" ... [weiterlesen]({self.get_absolute_url()})" return self.description[:length] + f" ... [weiterlesen]({self.get_absolute_url()})"
else:
return self.description
@property
def description_short(self):
return self._get_short_description(200)
@property
def description_100_short(self):
return self._get_short_description(90)
def get_absolute_url(self): def get_absolute_url(self):
"""Returns the url to access a detailed page for the adoption notice.""" """Returns the url to access a detailed page for the adoption notice."""
@@ -420,6 +512,7 @@ class AdoptionNotice(models.Model):
photos.extend(animal.photos.all()) photos.extend(animal.photos.all())
if len(photos) > 0: if len(photos) > 0:
return photos return photos
return None
def get_photo(self): def get_photo(self):
""" """
@@ -443,42 +536,36 @@ class AdoptionNotice(models.Model):
If the location is none, we by default return that the location is within the given distance If the location is none, we by default return that the location is within the given distance
""" """
if unknown_true and self.position is None: return geo.object_in_distance(self, position, max_distance, unknown_true)
return True
distance = geo.calculate_distance_between_coordinates(self.position, position) @staticmethod
return distance < max_distance def _values_of(list_of_enums):
return list(map(lambda x: x[0], list_of_enums))
@property @property
def is_active(self): def is_active(self):
if not hasattr(self, 'adoptionnoticestatus'): return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.Active.choices)
return False
return self.adoptionnoticestatus.is_active
@property @property
def is_disabled_unchecked(self): def is_disabled(self):
if not hasattr(self, 'adoptionnoticestatus'): return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.Disabled.choices)
return False
return self.adoptionnoticestatus.is_disabled_unchecked
def set_closed(self): @property
self.last_checked = timezone.now() def is_closed(self):
self.save() return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.Closed.choices)
self.adoptionnoticestatus.set_closed()
def set_active(self): @property
self.last_checked = timezone.now() def is_awaiting_action(self):
self.save() return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.AwaitingAction.choices)
if not hasattr(self, 'adoptionnoticestatus'):
AdoptionNoticeStatus.create_other(self) @property
self.adoptionnoticestatus.set_active() def status_description(self):
return AdoptionNoticeStatusChoicesDescriptions.mapping[self.adoption_notice_status]
def set_unchecked(self): def set_unchecked(self):
self.last_checked = timezone.now() self.last_checked = timezone.now()
self.adoption_notice_status = AdoptionNoticeStatusChoices.AwaitingAction.UNCHECKED
self.save() self.save()
if not hasattr(self, 'adoptionnoticestatus'):
AdoptionNoticeStatus.create_other(self)
self.adoptionnoticestatus.set_unchecked()
for subscription in self.get_subscriptions(): for subscription in self.get_subscriptions():
notification_title = _("Vermittlung deaktiviert:") + f" {self.name}" notification_title = _("Vermittlung deaktiviert:") + f" {self.name}"
@@ -489,100 +576,13 @@ class AdoptionNotice(models.Model):
text=text, text=text,
title=notification_title) title=notification_title)
def last_posted(self, platform=None):
class AdoptionNoticeStatus(models.Model): if platform is None:
""" last_post = SocialMediaPost.objects.filter(adoption_notice=self).order_by('-created_at').first()
The major status indicates a general state of an adoption notice else:
whereas the minor status is used for reporting last_post = SocialMediaPost.objects.filter(adoption_notice=self, platform=platform).order_by(
""" '-created_at').first()
return last_post.created_at
ACTIVE = "active"
AWAITING_ACTION = "awaiting_action"
CLOSED = "closed"
DISABLED = "disabled"
MAJOR_STATUS_CHOICES = {
ACTIVE: "active",
AWAITING_ACTION: "in review",
CLOSED: "closed",
DISABLED: "disabled",
}
MINOR_STATUS_CHOICES = {
ACTIVE: {
"searching": "searching",
"interested": "interested",
},
AWAITING_ACTION: {
"waiting_for_review": "waiting_for_review",
"needs_additional_info": "needs_additional_info",
},
CLOSED: {
"successful_with_notfellchen": "successful_with_notfellchen",
"successful_without_notfellchen": "successful_without_notfellchen",
"animal_died": "animal_died",
"closed_for_other_adoption_notice": "closed_for_other_adoption_notice",
"not_open_for_adoption_anymore": "not_open_for_adoption_anymore",
"other": "other"
},
DISABLED: {
"against_the_rules": "against_the_rules",
"missing_information": "missing_information",
"technical_error": "technical_error",
"unchecked": "unchecked",
"other": "other"
}
}
major_status = models.CharField(choices=MAJOR_STATUS_CHOICES, max_length=200)
minor_choices = {}
for key in MINOR_STATUS_CHOICES:
minor_choices.update(MINOR_STATUS_CHOICES[key])
minor_status = models.CharField(choices=minor_choices, max_length=200)
adoption_notice = models.OneToOneField(AdoptionNotice, on_delete=models.CASCADE)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.adoption_notice}: {self.major_status}, {self.minor_status}"
def as_string(self):
return f"{self.major_status}, {self.minor_status}"
@property
def is_active(self):
return self.major_status == self.ACTIVE
@property
def is_disabled_unchecked(self):
return self.major_status == self.DISABLED and self.minor_status == "unchecked"
@staticmethod
def get_minor_choices(major_status):
return AdoptionNoticeStatus.MINOR_STATUS_CHOICES[major_status]
@staticmethod
def create_other(an_instance):
# Used as empty status to be changed immediately
major_status = AdoptionNoticeStatus.DISABLED
minor_status = AdoptionNoticeStatus.MINOR_STATUS_CHOICES[AdoptionNoticeStatus.DISABLED]["other"]
AdoptionNoticeStatus.objects.create(major_status=major_status,
minor_status=minor_status,
adoption_notice=an_instance)
def set_closed(self):
self.major_status = self.MAJOR_STATUS_CHOICES[self.CLOSED]
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"]
self.save()
def set_unchecked(self):
self.major_status = self.MAJOR_STATUS_CHOICES[self.DISABLED]
self.minor_status = self.MINOR_STATUS_CHOICES[self.DISABLED]["unchecked"]
self.save()
def set_active(self):
self.major_status = self.MAJOR_STATUS_CHOICES[self.ACTIVE]
self.minor_status = self.MINOR_STATUS_CHOICES[self.ACTIVE]["searching"]
self.save()
class SexChoices(models.TextChoices): class SexChoices(models.TextChoices):
@@ -603,14 +603,19 @@ class SexChoicesWithAll(models.TextChoices):
class Animal(models.Model): class Animal(models.Model):
class Meta:
verbose_name = _('Tier')
verbose_name_plural = _('Tiere')
date_of_birth = models.DateField(verbose_name=_('Geburtsdatum')) date_of_birth = models.DateField(verbose_name=_('Geburtsdatum'))
name = models.CharField(max_length=200) name = models.CharField(max_length=200, verbose_name=_('Name'))
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung')) description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
species = models.ForeignKey(Species, on_delete=models.PROTECT, verbose_name=_("Tierart")) species = models.ForeignKey(Species, on_delete=models.PROTECT, verbose_name=_("Tierart"))
photos = models.ManyToManyField(Image, blank=True) photos = models.ManyToManyField(Image, blank=True, verbose_name=_("Fotos"))
sex = models.CharField( sex = models.CharField(
max_length=20, max_length=20,
choices=SexChoices.choices, choices=SexChoices.choices,
verbose_name=_("Geschlecht")
) )
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE) adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE)
owner = models.ForeignKey(User, on_delete=models.CASCADE) owner = models.ForeignKey(User, on_delete=models.CASCADE)
@@ -668,12 +673,17 @@ class SearchSubscription(models.Model):
- On new AdoptionNotice: Check all existing SearchSubscriptions for matches - On new AdoptionNotice: Check all existing SearchSubscriptions for matches
- For matches: Send notification to user of the SearchSubscription - For matches: Send notification to user of the SearchSubscription
""" """
class Meta:
verbose_name = _("Abonnierte Suche")
verbose_name_plural = _("Abonnierte Suchen")
owner = models.ForeignKey(User, on_delete=models.CASCADE) owner = models.ForeignKey(User, on_delete=models.CASCADE)
location = models.ForeignKey(Location, on_delete=models.PROTECT, null=True) location = models.ForeignKey(Location, on_delete=models.PROTECT, null=True)
sex = models.CharField(max_length=20, choices=SexChoicesWithAll.choices) sex = models.CharField(max_length=20, choices=SexChoicesWithAll.choices, verbose_name=_("Geschlecht"))
max_distance = models.IntegerField(choices=DistanceChoices.choices, null=True) max_distance = models.IntegerField(choices=DistanceChoices.choices, null=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Zuletzt geändert am"))
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am"))
def __str__(self): def __str__(self):
if self.location and self.max_distance: if self.location and self.max_distance:
@@ -686,15 +696,24 @@ class Rule(models.Model):
""" """
Class to store rules Class to store rules
""" """
class Meta:
verbose_name = _("Regel")
verbose_name_plural = _("Regeln")
title = models.CharField(max_length=200) title = models.CharField(max_length=200)
# Markdown is allowed in rule text # Markdown is allowed in rule text
rule_text = models.TextField() rule_text = models.TextField(verbose_name=_("Regeltext"))
language = models.ForeignKey(Language, on_delete=models.PROTECT) language = models.ForeignKey(Language, on_delete=models.PROTECT, verbose_name=_("Sprache"))
# Rule identifier allows to translate rules with the same identifier # Rule identifier allows to translate rules with the same identifier
rule_identifier = models.CharField(max_length=24) rule_identifier = models.CharField(max_length=24,
updated_at = models.DateTimeField(auto_now=True) verbose_name=_("Regel-ID"),
created_at = models.DateTimeField(auto_now_add=True) help_text=_("Ein eindeutiger Identifikator der Regel. Ein Regelobjekt "
"derselben Regel in einer anderen Sprache muss den gleichen "
"Identifikator haben"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Zuletzt geändert am"))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am"))
def __str__(self): def __str__(self):
return self.title return self.title
@@ -702,7 +721,8 @@ class Rule(models.Model):
class Report(models.Model): class Report(models.Model):
class Meta: class Meta:
permissions = [] verbose_name = _("Meldung")
verbose_name_plural = _("Meldungen")
ACTION_TAKEN = "action taken" ACTION_TAKEN = "action taken"
NO_ACTION_TAKEN = "no action taken" NO_ACTION_TAKEN = "no action taken"
@@ -717,8 +737,8 @@ class Report(models.Model):
status = models.CharField(max_length=30, choices=STATES) status = models.CharField(max_length=30, choices=STATES)
reported_broken_rules = models.ManyToManyField(Rule, verbose_name=_("Regeln gegen die verstoßen wurde")) reported_broken_rules = models.ManyToManyField(Rule, verbose_name=_("Regeln gegen die verstoßen wurde"))
user_comment = models.TextField(blank=True, verbose_name=_("Kommentar/Zusätzliche Information")) user_comment = models.TextField(blank=True, verbose_name=_("Kommentar/Zusätzliche Information"))
created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Zuletzt geändert am"))
updated_at = models.DateTimeField(auto_now=True) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Erstellt am"))
def __str__(self): def __str__(self):
return f"[{self.status}]: {self.user_comment:.20}" return f"[{self.status}]: {self.user_comment:.20}"
@@ -727,6 +747,9 @@ class Report(models.Model):
"""Returns the url to access a detailed page for the report.""" """Returns the url to access a detailed page for the report."""
return reverse('report-detail', args=[str(self.id)]) return reverse('report-detail', args=[str(self.id)])
def get_full_url(self):
return f"{base_url}{self.get_absolute_url()}"
def get_reported_rules(self): def get_reported_rules(self):
return self.reported_broken_rules.all() return self.reported_broken_rules.all()
@@ -779,6 +802,10 @@ class ReportComment(Report):
class ModerationAction(models.Model): class ModerationAction(models.Model):
class Meta:
verbose_name = _("Moderationsaktion")
verbose_name_plural = _("Moderationsaktionen")
BAN = "user_banned" BAN = "user_banned"
DELETE = "content_deleted" DELETE = "content_deleted"
COMMENT = "comment" COMMENT = "comment"
@@ -815,7 +842,7 @@ class Text(models.Model):
""" """
Base class to store markdown content Base class to store markdown content
""" """
title = models.CharField(max_length=100) title = models.CharField(max_length=100, verbose_name=_("Titel"))
content = models.TextField(verbose_name="Inhalt") content = models.TextField(verbose_name="Inhalt")
language = models.ForeignKey(Language, verbose_name="Sprache", on_delete=models.PROTECT) language = models.ForeignKey(Language, verbose_name="Sprache", on_delete=models.PROTECT)
text_code = models.CharField(max_length=24, verbose_name="Text code", blank=True) text_code = models.CharField(max_length=24, verbose_name="Text code", blank=True)
@@ -843,6 +870,11 @@ class Announcement(Text):
""" """
Class to store announcements that should be displayed for all users Class to store announcements that should be displayed for all users
""" """
class Meta:
verbose_name = _("Banner")
verbose_name_plural = _("Banner")
logged_in_only = models.BooleanField(default=False) logged_in_only = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@@ -892,10 +924,15 @@ class Comment(models.Model):
""" """
Class to store comments in markdown content Class to store comments in markdown content
""" """
class Meta:
verbose_name = _("Kommentar")
verbose_name_plural = _("Kommentare")
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in')) user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice')) adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
text = models.TextField(verbose_name="Inhalt") text = models.TextField(verbose_name="Inhalt")
reply_to = models.ForeignKey("self", verbose_name="Antwort auf", blank=True, null=True, on_delete=models.CASCADE) reply_to = models.ForeignKey("self", verbose_name="Antwort auf", blank=True, null=True, on_delete=models.CASCADE)
@@ -910,33 +947,28 @@ class Comment(models.Model):
return self.adoption_notice.get_absolute_url() return self.adoption_notice.get_absolute_url()
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): class Notification(models.Model):
class Meta:
verbose_name = _("Benachrichtigung")
verbose_name_plural = _("Benachrichtigungen")
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=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, notification_type = models.CharField(max_length=200,
choices=NotificationTypeChoices.choices, choices=NotificationTypeChoices.choices,
verbose_name=_('Benachrichtigungsgrund')) verbose_name=_('Benachrichtigungsgrund'))
title = models.CharField(max_length=100, verbose_name=_("Titel"))
text = models.TextField(verbose_name="Inhalt")
user_to_notify = models.ForeignKey(User, user_to_notify = models.ForeignKey(User,
on_delete=models.CASCADE, on_delete=models.CASCADE,
verbose_name=_('Nutzer*in'), verbose_name=_('Empfänger*in'),
help_text=_("Useraccount der Benachrichtigt wird"), help_text=_("Useraccount der benachrichtigt wird"),
related_name='user') related_name='user')
title = models.CharField(max_length=100, verbose_name=_("Titel"))
text = models.TextField(verbose_name="Inhalt")
read = models.BooleanField(default=False) read = models.BooleanField(default=False)
read_at = models.DateTimeField(blank=True, null=True, verbose_name=_("Gelesen am"))
comment = models.ForeignKey(Comment, blank=True, null=True, on_delete=models.CASCADE, verbose_name=_('Antwort')) 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')) adoption_notice = models.ForeignKey(AdoptionNotice, blank=True, null=True, on_delete=models.CASCADE,
verbose_name=_('Vermittlung'))
user_related = models.ForeignKey(User, user_related = models.ForeignKey(User,
blank=True, null=True, blank=True, null=True,
on_delete=models.CASCADE, verbose_name=_('Verwandter Useraccount'), on_delete=models.CASCADE, verbose_name=_('Verwandter Useraccount'),
@@ -958,10 +990,20 @@ class Notification(models.Model):
self.read_at = timezone.now() self.read_at = timezone.now()
self.save() self.save()
def get_body_part(self):
return NotificationDisplayMapping[self.notification_type].web_partial
class Subscriptions(models.Model): class Subscriptions(models.Model):
"""Subscription to a AdoptionNotice"""
class Meta:
verbose_name = _("Abonnement")
verbose_name_plural = _("Abonnements")
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in')) owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice')) adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'),
help_text=_("Vermittlung die abonniert wurde"))
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@@ -987,6 +1029,11 @@ class Timestamp(models.Model):
""" """
Class to store timestamps based on keys Class to store timestamps based on keys
""" """
class Meta:
verbose_name = _("Zeitstempel")
verbose_name_plural = _("Zeitstempel")
key = models.CharField(max_length=255, verbose_name=_("Schlüssel"), primary_key=True) key = models.CharField(max_length=255, verbose_name=_("Schlüssel"), primary_key=True)
timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("Zeitstempel")) timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("Zeitstempel"))
data = models.CharField(max_length=2000, blank=True, null=True) data = models.CharField(max_length=2000, blank=True, null=True)
@@ -999,19 +1046,33 @@ class SpeciesSpecificURL(models.Model):
""" """
Model that allows to specify a URL for a rescue organization where a certain species can be found Model that allows to specify a URL for a rescue organization where a certain species can be found
""" """
class Meta:
verbose_name = _("Tierartspezifische URL")
verbose_name_plural = _("Tierartspezifische URLs")
species = models.ForeignKey(Species, on_delete=models.CASCADE, verbose_name=_("Tierart")) species = models.ForeignKey(Species, on_delete=models.CASCADE, verbose_name=_("Tierart"))
rescue_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE, rescue_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE,
verbose_name=_("Tierschutzorganisation")) verbose_name=_("Tierschutzorganisation"))
url = models.URLField(verbose_name=_("Tierartspezifische URL")) url = models.URLField(verbose_name=_("Tierartspezifische URL"))
class SpeciesSpecialization(models.Model): class PlatformChoices(models.TextChoices):
""" FEDIVERSE = "fediverse", _("Fediverse")
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")) class SocialMediaPost(models.Model):
rescue_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE, created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
verbose_name=_("Tierschutzorganisation")) platform = models.CharField(max_length=255, verbose_name=_("Social Media Platform"),
choices=PlatformChoices.choices)
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
url = models.URLField(verbose_name=_("URL"))
@staticmethod
def get_an_to_post():
adoption_notices_without_post = AdoptionNotice.objects.filter(socialmediapost__isnull=True,
adoption_notice_status__in=AdoptionNoticeStatusChoices.Active.values)
return adoption_notices_without_post.first()
def __str__(self): def __str__(self):
return f"{_('Spezialisierung')} {self.species}" return f"{self.platform} - {self.adoption_notice}"

View File

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

View File

@@ -45,6 +45,7 @@ $confirm: hsl(133deg, 100%, calc(41% + 0%));
p > a { p > a {
text-decoration: underline; text-decoration: underline;
word-break: break-all;
} }
p > a.button { p > a.button {
@@ -234,7 +235,7 @@ IMAGES
.thumbnail img { .thumbnail img {
width: 100%; width: 100%;
height: 50px; height: 70px;
object-fit: cover; /* Crops the images */ object-fit: cover; /* Crops the images */
border-radius: 4px; border-radius: 4px;
} }
@@ -320,3 +321,43 @@ AN Cards
background-color: var(--bulma-success-on-scheme); background-color: var(--bulma-success-on-scheme);
} }
.notification-container {
display: inline-block;
position: relative;
padding: 0;
}
.notification-label {
padding: 2px 5px;
}
/* Make the badge float in the top right corner of the button */
.notification-badge {
background-color: #fa3e3e;
border-radius: 2px;
color: white;
padding: 1px 3px;
font-size: 8px;
position: absolute; /* Position the badge within the relatively positioned button */
top: 0;
right: 0;
}
/* Embedding Specifics */
.embed-main-content {
padding: 20px 10px 20px 10px;
}
// FLOATING BUTTON
.floating {
position: fixed;
border-radius: 0.3rem;
bottom: 4.5rem;
right: 1rem;
}

View File

@@ -1,423 +0,0 @@
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

@@ -0,0 +1,11 @@
/* mousetrap v1.6.5 craig.is/killing/mice */
(function(q,u,c){function v(a,b,g){a.addEventListener?a.addEventListener(b,g,!1):a.attachEvent("on"+b,g)}function z(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return n[a.which]?n[a.which]:r[a.which]?r[a.which]:String.fromCharCode(a.which).toLowerCase()}function F(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function w(a){return"shift"==a||"ctrl"==a||"alt"==a||
"meta"==a}function A(a,b){var g,d=[];var e=a;"+"===e?e=["+"]:(e=e.replace(/\+{2}/g,"+plus"),e=e.split("+"));for(g=0;g<e.length;++g){var m=e[g];B[m]&&(m=B[m]);b&&"keypress"!=b&&C[m]&&(m=C[m],d.push("shift"));w(m)&&d.push(m)}e=m;g=b;if(!g){if(!p){p={};for(var c in n)95<c&&112>c||n.hasOwnProperty(c)&&(p[n[c]]=c)}g=p[e]?"keydown":"keypress"}"keypress"==g&&d.length&&(g="keydown");return{key:m,modifiers:d,action:g}}function D(a,b){return null===a||a===u?!1:a===b?!0:D(a.parentNode,b)}function d(a){function b(a){a=
a||{};var b=!1,l;for(l in p)a[l]?b=!0:p[l]=0;b||(x=!1)}function g(a,b,t,f,g,d){var l,E=[],h=t.type;if(!k._callbacks[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(l=0;l<k._callbacks[a].length;++l){var c=k._callbacks[a][l];if((f||!c.seq||p[c.seq]==c.level)&&h==c.action){var e;(e="keypress"==h&&!t.metaKey&&!t.ctrlKey)||(e=c.modifiers,e=b.sort().join(",")===e.sort().join(","));e&&(e=f&&c.seq==f&&c.level==d,(!f&&c.combo==g||e)&&k._callbacks[a].splice(l,1),E.push(c))}}return E}function c(a,b,c,f){k.stopCallback(b,
b.target||b.srcElement,c,f)||!1!==a(b,c)||(b.preventDefault?b.preventDefault():b.returnValue=!1,b.stopPropagation?b.stopPropagation():b.cancelBubble=!0)}function e(a){"number"!==typeof a.which&&(a.which=a.keyCode);var b=z(a);b&&("keyup"==a.type&&y===b?y=!1:k.handleKey(b,F(a),a))}function m(a,g,t,f){function h(c){return function(){x=c;++p[a];clearTimeout(q);q=setTimeout(b,1E3)}}function l(g){c(t,g,a);"keyup"!==f&&(y=z(g));setTimeout(b,10)}for(var d=p[a]=0;d<g.length;++d){var e=d+1===g.length?l:h(f||
A(g[d+1]).action);n(g[d],e,f,a,d)}}function n(a,b,c,f,d){k._directMap[a+":"+c]=b;a=a.replace(/\s+/g," ");var e=a.split(" ");1<e.length?m(a,e,b,c):(c=A(a,c),k._callbacks[c.key]=k._callbacks[c.key]||[],g(c.key,c.modifiers,{type:c.action},f,a,d),k._callbacks[c.key][f?"unshift":"push"]({callback:b,modifiers:c.modifiers,action:c.action,seq:f,level:d,combo:a}))}var k=this;a=a||u;if(!(k instanceof d))return new d(a);k.target=a;k._callbacks={};k._directMap={};var p={},q,y=!1,r=!1,x=!1;k._handleKey=function(a,
d,e){var f=g(a,d,e),h;d={};var k=0,l=!1;for(h=0;h<f.length;++h)f[h].seq&&(k=Math.max(k,f[h].level));for(h=0;h<f.length;++h)f[h].seq?f[h].level==k&&(l=!0,d[f[h].seq]=1,c(f[h].callback,e,f[h].combo,f[h].seq)):l||c(f[h].callback,e,f[h].combo);f="keypress"==e.type&&r;e.type!=x||w(a)||f||b(d);r=l&&"keydown"==e.type};k._bindMultiple=function(a,b,c){for(var d=0;d<a.length;++d)n(a[d],b,c)};v(a,"keypress",e);v(a,"keydown",e);v(a,"keyup",e)}if(q){var n={8:"backspace",9:"tab",13:"enter",16:"shift",17:"ctrl",
18:"alt",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"ins",46:"del",91:"meta",93:"meta",224:"meta"},r={106:"*",107:"+",109:"-",110:".",111:"/",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},C={"~":"`","!":"1","@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0",_:"-","+":"=",":":";",'"':"'","<":",",">":".","?":"/","|":"\\"},B={option:"alt",command:"meta","return":"enter",
escape:"esc",plus:"+",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p;for(c=1;20>c;++c)n[111+c]="f"+c;for(c=0;9>=c;++c)n[c+96]=c.toString();d.prototype.bind=function(a,b,c){a=a instanceof Array?a:[a];this._bindMultiple.call(this,a,b,c);return this};d.prototype.unbind=function(a,b){return this.bind.call(this,a,function(){},b)};d.prototype.trigger=function(a,b){if(this._directMap[a+":"+b])this._directMap[a+":"+b]({},a);return this};d.prototype.reset=function(){this._callbacks={};
this._directMap={};return this};d.prototype.stopCallback=function(a,b){if(-1<(" "+b.className+" ").indexOf(" mousetrap ")||D(b,this.target))return!1;if("composedPath"in a&&"function"===typeof a.composedPath){var c=a.composedPath()[0];c!==a.target&&(b=c)}return"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable};d.prototype.handleKey=function(){return this._handleKey.apply(this,arguments)};d.addKeycodes=function(a){for(var b in a)a.hasOwnProperty(b)&&(n[b]=a[b]);p=null};
d.init=function(){var a=d(u),b;for(b in a)"_"!==b.charAt(0)&&(d[b]=function(b){return function(){return a[b].apply(a,arguments)}}(b))};d.init();q.Mousetrap=d;"undefined"!==typeof module&&module.exports&&(module.exports=d);"function"===typeof define&&define.amd&&define(function(){return d})}})("undefined"!==typeof window?window:null,"undefined"!==typeof window?document:null);

View File

@@ -0,0 +1,15 @@
function mark_checked(index) {
document.getElementById('mark_checked_'+index).submit();
}
function open_information(index) {
let link = document.getElementById('species_url_'+index+'_1');
if (!link) {
link = document.getElementById('rescue_org_website_'+index);
}
window.open(link.href);
}
Mousetrap.bind('c', function() { mark_checked(1); });
Mousetrap.bind('o', function() { open_information(1); });

View File

@@ -22,7 +22,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Looks for all notifications with a delete and allows closing them when pressing delete // Looks for all notifications with a delete and allows closing them when pressing delete
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => { (document.querySelectorAll('.notification .delete:not(.js-delete-excluded)') || []).forEach(($delete) => {
const $notification = $delete.parentNode; const $notification = $delete.parentNode;
$delete.addEventListener('click', () => { $delete.addEventListener('click', () => {
@@ -67,6 +67,51 @@ document.addEventListener('DOMContentLoaded', () => {
$el.classList.remove("is-active"); $el.classList.remove("is-active");
}); });
} }
// MODALS //
function openModal($el) {
$el.classList.add('is-active');
send("Modal.open", {
modal: $el.id
});
}
function closeModal($el) {
$el.classList.remove('is-active');
}
function closeAllModals() {
(document.querySelectorAll('.modal') || []).forEach(($modal) => {
closeModal($modal);
});
}
// Add a click event on buttons to open a specific modal
(document.querySelectorAll('.js-modal-trigger') || []).forEach(($trigger) => {
const modal = $trigger.dataset.target;
const $target = document.getElementById(modal);
$trigger.addEventListener('click', () => {
openModal($target);
});
});
// Add a click event on various child elements to close the parent modal
(document.querySelectorAll('.modal-background, .modal-close, .delete, .nf-modal-close') || []).forEach(($close) => {
const $target = $close.closest('.modal');
$close.addEventListener('click', () => {
closeModal($target);
});
});
// Add a keyboard event to close all modals
document.addEventListener('keydown', (event) => {
if (event.key === "Escape") {
closeAllModals();
}
});
}); });

View File

@@ -5,8 +5,9 @@ from django.utils import timezone
from notfellchen.celery import app as celery_app from notfellchen.celery import app as celery_app
from .mail import send_notification_email from .mail import send_notification_email
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices
from .tools.fedi import post_an_to_fedi
from .tools.misc import healthcheck_ok from .tools.misc import healthcheck_ok
from .models import Location, AdoptionNotice, Timestamp, RescueOrganization from .models import Location, AdoptionNotice, Timestamp, RescueOrganization, SocialMediaPost
from .tools.notifications import notify_of_AN_to_be_checked from .tools.notifications import notify_of_AN_to_be_checked
from .tools.search import notify_search_subscribers from .tools.search import notify_search_subscribers
@@ -38,6 +39,14 @@ def task_deactivate_unchecked():
set_timestamp("task_deactivate_404_adoption_notices") set_timestamp("task_deactivate_404_adoption_notices")
@celery_app.task(name="social_media.post_fedi")
def task_post_to_fedi():
adoption_notice = SocialMediaPost.get_an_to_post()
if adoption_notice is not None:
post_an_to_fedi(adoption_notice)
set_timestamp("task_social_media.post_fedi")
@celery_app.task(name="commit.post_an_save") @celery_app.task(name="commit.post_an_save")
def post_adoption_notice_save(pk): def post_adoption_notice_save(pk):
instance = AdoptionNotice.objects.get(pk=pk) instance = AdoptionNotice.objects.get(pk=pk)

View File

@@ -0,0 +1,12 @@
{% load allauth %}
{% setvar variant %}
{% if "primary" in attrs.tags %}
is-success
{% elif "secondary" in attrs.tags %}
is-success is-light
{% endif %}
{% endsetvar %}
<span class="tag{% if variant %} {{ variant }}{% endif %}" {% if attrs.title %}title="{{ attrs.title }}"{% endif %}>
{% slot %}
{% endslot %}
</span>

View File

@@ -0,0 +1,15 @@
{% load allauth %}
{% comment %} djlint:off {% endcomment %}
<div class="control">
<{% if attrs.href %}a href="{{ attrs.href }}"{% else %}button{% endif %}
class="button is-primary"
{% if attrs.form %}form="{{ attrs.form }}"{% endif %}
{% if attrs.id %}id="{{ attrs.id }}"{% endif %}
{% if attrs.name %}name="{{ attrs.name }}"{% endif %}
{% if attrs.value %}value="{{ attrs.value }}"{% endif %}
{% if attrs.type %}type="{{ attrs.type }}"{% endif %}
>
{% slot %}
{% endslot %}
</{% if attrs.href %}a{% else %}button{% endif %}>
</div>

View File

@@ -0,0 +1,5 @@
{% load allauth %}
<div class="field is-grouped">
{% slot %}
{% endslot %}
</div>

View File

@@ -0,0 +1,50 @@
{% load allauth %}
<div class="field">
{% if attrs.type == "textarea" %}
<label class="label" for="{{ attrs.id }}">
{% slot label %}
{% endslot %}
</label>
<textarea class="textarea"
{% if attrs.required %}required{% endif %}
{% if attrs.rows %}rows="{{ attrs.rows }}"{% endif %}
{% if attrs.disabled %}disabled{% endif %}
{% if attrs.readonly %}readonly{% endif %}
{% if attrs.checked %}checked{% endif %}
{% if attrs.name %}name="{{ attrs.name }}"{% endif %}
{% if attrs.id %}id="{{ attrs.id }}"{% endif %}
{% if attrs.placeholder %}placeholder="{{ attrs.placeholder }}"{% endif %}>{% slot value %}{% endslot %}</textarea>
{% else %}
{% if attrs.type != "checkbox" and attrs.type != "radio" %}
<label class="label" for="{{ attrs.id }}">
{% slot label %}
{% endslot %}
</label>
{% endif %}
<input {% if attrs.type != "checkbox" and attrs.type != "radio" %}class="input"{% endif %}
{% if attrs.required %}required{% endif %}
{% if attrs.disabled %}disabled{% endif %}
{% if attrs.readonly %}readonly{% endif %}
{% if attrs.checked %}checked{% endif %}
{% if attrs.name %}name="{{ attrs.name }}"{% endif %}
{% if attrs.id %}id="{{ attrs.id }}"{% endif %}
{% if attrs.placeholder %}placeholder="{{ attrs.placeholder }}"{% endif %}
{% if attrs.autocomplete %}autocomplete="{{ attrs.autocomplete }}"{% endif %}
{% if attrs.value is not None %}value="{{ attrs.value }}"{% endif %}
type="{{ attrs.type }}">
{% if attrs.type == "checkbox" or attrs.type == "radio" %}
<label for="{{ attrs.id }}">
{% slot label %}
{% endslot %}
</label>
{% endif %}
{% endif %}
{% if slots.help_text %}
<p class="help is-danger">
{% slot help_text %}
{% endslot %}
</p>
{% endif %}
<p class="help is-danger">{{ attrs.errors }}</p>
</div>

View File

@@ -0,0 +1 @@
{{ attrs.form }}

View File

@@ -0,0 +1,12 @@
{% load allauth %}
<div class="block">
<form method="{{ attrs.method }}"
{% if attrs.action %}action="{{ attrs.action }}"{% endif %}>
{% slot body %}
{% endslot %}
<div class="field is-grouped is-grouped-multiline">
{% slot actions %}
{% endslot %}
</div>
</form>
</div>

View File

@@ -0,0 +1 @@
{% comment %} djlint:off {% endcomment %}{% load allauth %}<h1 class="title is-1">{% slot %}{% endslot %}</h1>

View File

@@ -0,0 +1 @@
{% comment %} djlint:off {% endcomment %}{% load allauth %}<h2 class="title is-2">{% slot %}{% endslot %}</h2>

View File

@@ -0,0 +1 @@
{% comment %} djlint:off {% endcomment %}{% load allauth %}<p class="content">{% slot %}{% endslot %}</p>

View File

@@ -0,0 +1,18 @@
{% load allauth %}
<section class="block">
<h2 class="title is-2">
{% slot title %}
{% endslot %}
</h2>
{% slot body %}
{% endslot %}
{% if slots.actions %}
<div class="field is-grouped is-grouped-multiline">
{% for action in slots.actions %}
<div class="control">
{{ action }}
</div>
{% endfor %}
</div>
{% endif %}
</section>

View File

@@ -0,0 +1 @@
{% extends "fellchensammlung/base.html" %}

View File

@@ -30,7 +30,7 @@
<div class="card-header"> <div class="card-header">
<h2 class="card-header-title">{{ faq.title }}</h2> <h2 class="card-header-title">{{ faq.title }}</h2>
</div> </div>
<div class="card-content"> <div class="card-content content">
{{ faq.content | render_markdown }} {{ faq.content | render_markdown }}
</div> </div>
</div> </div>

View File

@@ -16,11 +16,27 @@
{% block canonical_url %}{% host %}{% url 'rescue-organizations' %}{% endblock %} {% block canonical_url %}{% host %}{% url 'rescue-organizations' %}{% endblock %}
{% block content %} {% block content %}
<div class="block"> <div class="columns block">
<div class=" column {% if show_search %}is-two-thirds {% endif %}">
<div style="height: 70vh"> <div style="height: 70vh">
{% include "fellchensammlung/partials/partial-map.html" %} {% include "fellchensammlung/partials/partial-map.html" %}
</div> </div>
</div> </div>
{% if show_search %}
<div class="column is-one-third">
<form method="GET" autocomplete="off">
<input type="hidden" name="longitude" maxlength="200" id="longitude">
<input type="hidden" name="latitude" maxlength="200" id="latitude">
<!--- https://docs.djangoproject.com/en/5.2/topics/forms/#reusable-form-templates -->
{{ search_form }}
<button class="button is-primary is-fullwidth" type="submit" value="search" name="action">
<i class="fas fa-search fa-fw"></i> {% trans 'Suchen' %}
</button>
</form>
</div>
{% endif %}
</div>
<div class="block"> <div class="block">
{% with rescue_organizations=rescue_organizations_to_list %} {% with rescue_organizations=rescue_organizations_to_list %}
{% include "fellchensammlung/lists/list-animal-shelters.html" %} {% include "fellchensammlung/lists/list-animal-shelters.html" %}
@@ -29,16 +45,17 @@
<nav class="pagination" role="navigation" aria-label="{% trans 'Paginierung' %}"> <nav class="pagination" role="navigation" aria-label="{% trans 'Paginierung' %}">
{% if rescue_organizations_to_list.has_previous %} {% if rescue_organizations_to_list.has_previous %}
<a class="pagination-previous" <a class="pagination-previous"
href="?page={{ rescue_organizations_to_list.previous_page_number }}">{% trans 'Vorherige' %}</a> href="?page={% url_replace request 'page' rescue_organizations_to_list.previous_page_number %}">{% trans 'Vorherige' %}</a>
{% endif %} {% endif %}
{% if rescue_organizations_to_list.has_next %} {% if rescue_organizations_to_list.has_next %}
<a class="pagination-next" href="?page={{ rescue_organizations_to_list.next_page_number }}">{% trans 'Nächste' %}</a> <a class="pagination-next"
href="?{% url_replace request 'page' rescue_organizations_to_list.next_page_number %}">{% trans 'Nächste' %}</a>
{% endif %} {% endif %}
<ul class="pagination-list"> <ul class="pagination-list">
{% for page in elided_page_range %} {% for page in elided_page_range %}
{% if page != "…" %} {% if page != "…" %}
<li> <li>
<a href="?page={{ page }}" <a href="?{% url_replace request 'page' page %}"
class="pagination-link {% if page == rescue_organizations_to_list.number %} is-current{% endif %}" class="pagination-link {% if page == rescue_organizations_to_list.number %} is-current{% endif %}"
aria-label="{% blocktranslate %}Gehe zu Seite {{ page }}.{% endblocktranslate %}"> aria-label="{% blocktranslate %}Gehe zu Seite {{ page }}.{% endblocktranslate %}">
{{ page }} {{ page }}

View File

@@ -37,6 +37,26 @@
{% block header %} {% block header %}
{% include "fellchensammlung/header.html" %} {% include "fellchensammlung/header.html" %}
{% endblock %} {% endblock %}
{% if profile %}
<div class="profile">
<table class="table is-bordered is-fullwidth is-hoverable is-striped">
<thead>
<tr>
<td>Timestamp</td>
<td>Status</td>
</tr>
</thead>
<tbody>
{% for status in profile %}
<tr>
<td>{{ status.0 }}</td>
<td>{{ status.1 }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
<div class="main-content"> <div class="main-content">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
@@ -45,5 +65,8 @@
{% block footer %} {% block footer %}
{% include "fellchensammlung/footer.html" %} {% include "fellchensammlung/footer.html" %}
{% endblock %} {% endblock %}
{% block extra_body %}
{% endblock extra_body %}
</body> </body>
</html> </html>

View File

@@ -1,5 +1,6 @@
{% extends "fellchensammlung/base.html" %} {% extends "fellchensammlung/base.html" %}
{% load custom_tags %} {% load custom_tags %}
{% load admin_urls %}
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
@@ -25,18 +26,14 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="columns"> {% include "fellchensammlung/partials/partial-adoption-notice-status.html" %}
<div class="column is-two-thirds">
<!--- Title level (including action dropdown) --> <!--- Title level (including action dropdown) -->
<div class="level"> <div class="block is-flex is-justify-content-space-between">
<div class="level-left"> <div class="">
<div class="level-item"> <h2 class="title is-3 is-size-4-mobile">{{ adoption_notice.name }}</h2>
<p class="title is-3 is-size-4-mobile">{{ adoption_notice.name }}</p>
</div>
</div> </div>
<div class="level-right"> <div class="">
<div class="level-item">
<div class="dropdown is-right"> <div class="dropdown is-right">
<div class="dropdown-trigger"> <div class="dropdown-trigger">
<button class="button" aria-haspopup="true" aria-controls="dropdown-menu4"> <button class="button" aria-haspopup="true" aria-controls="dropdown-menu4">
@@ -49,30 +46,58 @@
<!--- Action menu (dropdown) ---> <!--- Action menu (dropdown) --->
<div class="dropdown-menu" role="menu"> <div class="dropdown-menu" role="menu">
<div class="dropdown-content"> <div class="dropdown-content">
{% if is_subscribed %}
<form class="dropdown-item" method="POST">
{% csrf_token %}
<input type="hidden" name="action" value="unsubscribe">
<button type="submit" id="submit">
<i class="fas fa-bell-slash fa-fw"
aria-hidden="true"></i> {% trans 'Deabonnieren' %}
</button>
</form>
{% else %}
<form class="dropdown-item" method="POST">
{% csrf_token %}
<input type="hidden" name="action" value="subscribe">
<button type="submit" id="submit">
<i class="fas fa-bell fa-fw"
aria-hidden="true"></i> {% trans 'Abonnieren' %}
</button>
</form>
{% endif %}
<hr class="dropdown-divider">
{% if has_edit_permission %} {% if has_edit_permission %}
<form class="dropdown-item" method="POST">
<a class="dropdown-item"> {% csrf_token %}
<i class="fas fa-check" <input type="hidden" name="action" value="checked_active">
<button type="submit" id="submit">
<i class="fas fa-check fa-fw"
aria-hidden="true"></i> {% trans 'Als aktiv bestätigen' %} aria-hidden="true"></i> {% trans 'Als aktiv bestätigen' %}
</a> </button>
</form>
<a class="dropdown-item" <a class="dropdown-item"
href="{% url 'adoption-notice-edit' adoption_notice_id=adoption_notice.pk %}"> href="{% url 'adoption-notice-edit' adoption_notice_id=adoption_notice.pk %}">
<i class="fas fa-pencil" <i class="fas fa-pencil fa-fw"
aria-hidden="true"></i> {% translate 'Bearbeiten' %} aria-hidden="true"></i> {% translate 'Bearbeiten' %}
</a> </a>
<a class="dropdown-item" <a class="dropdown-item"
href="{% url 'adoption-notice-add-photo' adoption_notice.pk %}"> href="{% url 'adoption-notice-add-photo' adoption_notice.pk %}">
<i class="fas fa-image" <i class="fas fa-image fa-fw"
aria-hidden="true"></i> {% trans 'Bilder hinzufügen' %} aria-hidden="true"></i> {% trans 'Bilder hinzufügen' %}
</a> </a>
<a class="dropdown-item" <a class="dropdown-item"
href="{% url 'adoption-notice-add-animal' adoption_notice.pk %}"> href="{% url 'adoption-notice-add-animal' adoption_notice.pk %}">
<i class="fas fa-plus" <i class="fas fa-plus fa-fw"
aria-hidden="true"></i> {% trans 'Tier hinzufügen' %} aria-hidden="true"></i> {% trans 'Tier hinzufügen' %}
</a> </a>
<a class="dropdown-item"> <a class="dropdown-item"
<i class="fas fa-circle-xmark" href="{% url 'adoption-notice-close' adoption_notice_id=adoption_notice.pk %}">
<i class="fas fa-circle-xmark fa-fw"
aria-hidden="true"></i> {% trans 'Deaktivieren' %} aria-hidden="true"></i> {% trans 'Deaktivieren' %}
</a> </a>
<hr class="dropdown-divider"> <hr class="dropdown-divider">
@@ -81,12 +106,27 @@
<i class="fas fa-flag" <i class="fas fa-flag"
aria-hidden="true"></i> {% trans 'Melden' %} aria-hidden="true"></i> {% trans 'Melden' %}
</a> </a>
{% if request.user.is_superuser %}
{% if oxitraffic_base_url %}
<hr class="dropdown-divider">
<a class="dropdown-item"
href="{{ oxitraffic_base_url }}/stats?path={{ adoption_notice.get_absolute_url }}">
<i class="fas fa-chart-line fa-fw"></i> {% trans 'Aufrufe' %}
</a>
{% endif %}
<hr class="dropdown-divider">
<a class="dropdown-item is-warning"
href="{% url adoption_notice_meta|admin_urlname:'change' adoption_notice.pk %}">
<i class="fa-solid fa-tools fa-fw"></i> Admin interface
</a>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="columns">
<div class="column is-two-thirds">
<!--- General Information ---> <!--- General Information --->
<div class="grid"> <div class="grid">
<div class="cell"> <div class="cell">
@@ -119,7 +159,7 @@
</div> </div>
<!--- Images and Description ---> <!--- Images and Description --->
<div class="columns"> <div class="columns block">
<!--- Images ---> <!--- Images --->
{% if adoption_notice.get_photos %} {% if adoption_notice.get_photos %}
<div class="column block"> <div class="column block">
@@ -160,25 +200,36 @@
<div class="column block"> <div class="column block">
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h1 class="card-header-title title is-4">{% translate "Beschreibung" %}</h1> <h4 class="card-header-title title is-4">{% translate "Beschreibung" %}</h4>
</div> </div>
<div class="card-content"> <div class="card-content content">
<p class="expandable">{% if adoption_notice.description %} <p class="expandable">{% if adoption_notice.description %}
{{ adoption_notice.description | render_markdown }} {{ adoption_notice.description | render_markdown }}
{% else %} {% else %}
{% translate "Keine Beschreibung angegeben" %} {% translate "Keine Beschreibung angegeben" %}
{% endif %} {% endif %}
</p> </p>
<hr>
<p>
<strong>
{% translate 'Zuletzt auf Aktualität überprüft:' %}
</strong>
{{ adoption_notice.last_checked|time_since_hr }}
</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="block">
{% include 'fellchensammlung/partials/adoption_process/base.html' %}
</div>
<div class="block"> <div class="block">
{% if adoption_notice.further_information %} {% if adoption_notice.further_information %}
<form method="get" action="{% url 'external-site' %}"> <form method="get" action="{% url 'external-site' %}">
<input type="hidden" name="url" value="{{ adoption_notice.further_information }}"> <input type="hidden" name="url" value="{{ adoption_notice.further_information }}">
<button class="button is-primary is-fullwidth" type="submit" id="submit"> <button class="button is-warning is-fullwidth" type="submit" id="submit">
{{ adoption_notice.further_information | domain }} <i {% translate 'Weitere Informationen' %}: {{ adoption_notice.further_information | domain }}
<i
class="fa-solid fa-arrow-up-right-from-square fa-fw"></i> class="fa-solid fa-arrow-up-right-from-square fa-fw"></i>
</button> </button>
</form> </form>
@@ -193,7 +244,7 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<hr>
<div class="block"> <div class="block">

View File

@@ -19,30 +19,15 @@
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<div class="block"> <div class="block">
<div class="card"> {% include "fellchensammlung/partials/rescue_orgs/partial-basic-info-card.html" %}
<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>
<div class="block"> <div class="block">
{% include "fellchensammlung/partials/partial-rescue-organization-contact.html" %} {% include "fellchensammlung/partials/rescue_orgs/partial-rescue-organization-contact.html" %}
</div> </div>
<div class="block"> <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> <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> </div>
<div class="column"> <div class="column">
@@ -50,15 +35,43 @@
</div> </div>
</div> </div>
{% if org.child_organizations %}
<div class="block">
<h2 class="title is-2">{% translate 'Unterorganisationen' %}</h2>
{% with rescue_organizations=org.child_organizations %}
{% include "fellchensammlung/lists/list-animal-shelters.html" %}
{% endwith %}
</div>
{% endif %}
<h2 class="title is-2">{% translate 'Vermittlungen der Organisation' %}</h2> <h2 class="title is-2">{% translate 'Vermittlungen der Organisation' %}</h2>
{% with ans_by_status=org.adoption_notices_in_hierarchy_divided_by_status %}
{% with active_ans=ans_by_status.0 inactive_ans=ans_by_status.1 %}
<div class="block">
<h3 class="title is-3">{% translate 'Aktive Vermittlungen' %}</h3>
<div class="container-cards"> <div class="container-cards">
{% if org.adoption_notices %} {% if active_ans %}
{% for adoption_notice in org.adoption_notices %} {% for adoption_notice in active_ans %}
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %} {% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
{% endfor %} {% endfor %}
{% else %} {% else %}
<p>{% translate "Keine Vermittlungen gefunden." %}</p> <p>{% translate "Keine Vermittlungen gefunden." %}</p>
{% endif %} {% endif %}
</div> </div>
</div>
<div class="block">
<h3 class="title is-3">{% translate 'Inaktive Vermittlungen' %}</h3>
<div class="container-cards">
{% if inactive_ans %}
{% for adoption_notice in inactive_ans %}
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
{% endfor %}
{% else %}
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
{% endif %}
</div>
</div>
{% endwith %}
{% endwith %}
{% endblock %} {% endblock %}

View File

@@ -1,7 +1,8 @@
{% extends "fellchensammlung/base.html" %} {% extends "fellchensammlung/base.html" %}
{% load i18n %} {% load i18n %}
{% load account %}
{% block title %}<title>{{ user.get_full_name }}</title>{% endblock %} {% block title %}<title>{% user_display user %}</title>{% endblock %}
{% block content %} {% block content %}
@@ -13,7 +14,7 @@
</div> </div>
<div class="level-right"> <div class="level-right">
<div class="level-item"> <div class="level-item">
<form class="" action="{% url 'logout' %}" method="post"> <form class="" action="{% url 'account_logout' %}" method="post">
{% csrf_token %} {% csrf_token %}
<button class="button" type="submit"> <button class="button" type="submit">
<i aria-hidden="true" class="fas fa-sign-out fa-fw"></i> Logout <i aria-hidden="true" class="fas fa-sign-out fa-fw"></i> Logout
@@ -25,12 +26,30 @@
<div class="block"> <div class="block">
<h2 class="title is-2">{% trans 'Profil verwalten' %}</h2> <h2 class="title is-2">{% trans 'Profil verwalten' %}</h2>
<p><strong>{% translate "E-Mail" %}:</strong> {{ user.email }}</p> <div class="block"><strong>{% translate "E-Mail" %}:</strong> {{ user.email }}</div>
<div class=""> <div class="block">
<p> <div class="field is-grouped is-grouped-multiline">
<a class="button is-warning" href="{% url 'password_change' %}">{% translate "Change password" %}</a> <div class="control">
<a class="button is-info" href="{% url 'user-me-export' %}">{% translate "Daten exportieren" %}</a> <a class="button is-warning"
</p> href="{% url 'account_change_password' %}">{% translate "Passwort ändern" %}</a>
</div>
<div class="control">
<a class="button is-warning"
href="{% url 'account_email' %}">
{% translate "E-Mail Adresse ändern" %}
</a>
</div>
<div class="control">
<a class="button is-warning"
href="{% url 'mfa_index' %}">
{% translate "2-Faktor Authentifizierung" %}
</a>
</div>
<div class="control">
<a class="button is-info" href="{% url 'user-me-export' %}">
{% translate "Daten exportieren" %}
</a>
</div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,52 @@
{% load custom_tags %}
{% load i18n %}
{% load static %}
{% get_current_language as LANGUAGE_CODE %}
<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% 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/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">
{% if background_color %}
<style>
body {
background: #{{ background_color }};
}
</style>
{% endif %}
<script src="{% static 'fellchensammlung/js/custom.js' %}"></script>
<script src="{% static 'fellchensammlung/js/toggles.js' %}"></script>
<script src="{% static 'fellchensammlung/js/jquery.min.js' %}"></script>
<script type="module" src="{% static 'fellchensammlung/js/photoswipe.js' %}"></script>
{% block additional_scrips %}{% endblock %}
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'fellchensammlung/favicon/apple-touch-icon.png' %}">
<link rel="icon" type="image/png" sizes="32x32" href="{% static 'fellchensammlung/favicon/favicon-32x32.png' %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% static 'fellchensammlung/favicon/favicon-16x16.png' %}">
{% get_oxitraffic_script_if_enabled %}
<base target="_parent"/>
</head>
<body>
<div class="embed-main-content">
{% block content %}{% endblock %}
</div>
</body>
</html>

View File

@@ -0,0 +1,10 @@
{% extends "fellchensammlung/embeddables/embedding-base.html" %}
{% load custom_tags %}
{% load i18n %}
{% block content %}
<div class="block">
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
</div>
{% endblock %}

View File

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

View File

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

View File

@@ -34,6 +34,14 @@
{% translate 'Das Notfellchen Projekt' %} {% translate 'Das Notfellchen Projekt' %}
</a> </a>
<br/> <br/>
<a href="{% url "contact" %}">
{% translate 'Kontakt' %}
</a>
<br/>
<a href="{% url "buying" %}">
{% translate 'Ratten kaufen' %}
</a>
<br/>
<a href="{% url "terms-of-service" %}"> <a href="{% url "terms-of-service" %}">
{% translate 'Nutzungsbedingungen' %} {% translate 'Nutzungsbedingungen' %}
</a> </a>
@@ -88,6 +96,7 @@
{% translate 'Tierheime in der Nähe' %} {% translate 'Tierheime in der Nähe' %}
</a> </a>
<br/> <br/>
{% if request.user.is_authenticated %}
{% trust_level "MODERATOR" as coordinator_trust_level %} {% trust_level "MODERATOR" as coordinator_trust_level %}
{% if request.user.trust_level >= coordinator_trust_level %} {% if request.user.trust_level >= coordinator_trust_level %}
<a class="nav-link " href="{% url "modtools" %}"> <a class="nav-link " href="{% url "modtools" %}">
@@ -100,6 +109,7 @@
<i class="fa-solid fa-screwdriver-wrench fa-fw"></i>{% translate 'Admin-Bereich' %} <i class="fa-solid fa-screwdriver-wrench fa-fw"></i>{% translate 'Admin-Bereich' %}
</a> </a>
{% endif %} {% endif %}
{% endif %}
</div> </div>
</div> </div>
</footer> </footer>

View File

@@ -81,6 +81,20 @@
</div> </div>
</div> </div>
<div class="field">
<label class="label" for="an-organization">
{{ form.organization.label }}
{% if form.organization.field.required %}<span class="special_class">*</span>{% endif %}
</label>
<div class="select">
{{ form.organization|attr:"id:an-organization" }}
</div>
<div class="help">
{{ form.organization.help_text }}
</div>
</div>
<div class="notification is-info"> <div class="notification is-info">
<p> <p>

View File

@@ -1,19 +1,13 @@
{% load i18n %} {% load i18n %}
{% load widget_tweaks %}
<div class="card"> <form method="POST">
<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 %} {% csrf_token %}
<input type="hidden" name="action" value="comment"> <input type="hidden" name="action" value="comment">
{{ comment_form }} <div class="field">
<input type="submit" class="button is-primary" value="{% trans 'Kommentieren' %}"> {{ comment_form.text |add_class:"input textarea"|attr:"rows:3"|attr:"placeholder:Neuer Kommentar" }}
</form>
</div> </div>
</div> <div class="control">
<input type="submit" class="button is-primary" value="{% trans 'Kommentieren' %}">
</div>
</form>

View File

@@ -0,0 +1,35 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load widget_tweaks %}
{% load admin_urls %}
{% block title %}
<title>Organisation {{ org }} von regelmäßiger Prüfung ausschließen</title>
{% endblock %}
{% block content %}
<h1 class="title is-1">Organisation {{ org }} von regelmäßiger Prüfung ausschließen</h1>
<div class="columns block">
<div class="column">
{% include "fellchensammlung/partials/rescue_orgs/partial-basic-info-card.html" %}
</div>
<div class="column">
{% include "fellchensammlung/partials/rescue_orgs/partial-rescue-organization-contact.html" %}
</div>
</div>
<div class="block">
<form method="post">
{% csrf_token %}
{{ form }}
<input class="button is-primary" type="submit" name="delete"
value="{% translate "Aktualisieren" %}">
<a class="button" href="{% url 'organization-check' %}">{% translate "Zurück" %}</a>
</form>
</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>
{% endblock %}

View File

@@ -4,26 +4,84 @@
{% load widget_tweaks %} {% load widget_tweaks %}
{% block content %} {% block content %}
<div> <div class="block">
<p>
{% blocktranslate %} {% blocktranslate %}
Lade hier ein Foto hoch - wähle den Titel wie du willst und mach bitte eine Bildbeschreibung, Lade hier ein Foto hoch. Füge bitte eine Bildbeschreibung hinzu,
damit die Fotos auch für blinde und sehbehinderte Personen zugänglich sind. damit die Fotos auch für blinde und sehbehinderte Personen zugänglich sind.
{% endblocktranslate %} {% endblocktranslate %}
<p><a class="button" target="_blank"
href="https://www.dbsv.org/bildbeschreibung-4-regeln.html">{% translate 'Anleitung für Bildbeschreibungen' %}</a>
</p> </p>
</div> </div>
<div class="block">
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div class="field"> <div class="file has-name is-boxed is-primary is-">
<label class="label" for="image">{{ form.image.label }}</label> <label class="file-label" for="image">
{{ form.image|add_class:"input"|attr:"id:image" }} {{ form.image|add_class:"file-input"|attr:"id:image" }}
<span class="file-cta">
<span class="file-icon">
<i class="fas fa-upload"></i>
</span>
<span class="file-label">{{ form.image.help_text }}</span>
</span>
<span class="file-name" id="image-upload-filename">...</span>
</label>
</div> </div>
<!--- Image Preview: Only shown if image gets selected --->
<div class="field" id="image-preview-wrapper" style="display: none;">
<label class="label">{% translate "Vorschau" %}</label>
<div class="box has-text-centered">
<figure class="image is-256x256 is-inline-block">
<img id="image-preview" src="" alt="{% translate 'Bildvorschau' %}" class="is-rounded">
</figure>
</div>
</div>
<div class="field"> <div class="field">
<label class="label" for="alt-text">{{ form.alt_text.label }}</label> <label class="label" for="alt-text">{{ form.alt_text.label }}</label>
{{ form.alt_text|add_class:"textarea"|attr:"id:alt-text"|attr:"rows:3" }} {{ form.alt_text|add_class:"textarea"|attr:"id:alt-text"|attr:"rows:3" }}
<div class="help">
{{ form.alt_text.help_text }}
</div>
<div class="is-danger">{{ form.alt_text.errors }}</div> <div class="is-danger">{{ form.alt_text.errors }}</div>
</div> </div>
<input class="button is-primary" type="submit" value="{% translate "Speichern" %}"> <input class="button is-primary" type="submit" value="{% translate "Speichern" %}">
</form> </form>
</div>
<div class="block">
<a class="button is-info is-warning is-fullwidth" target="_blank"
href="https://www.dbsv.org/bildbeschreibung-4-regeln.html">
<i class="fa-solid fa-link fa-fw"></i> {% translate 'Anleitung für Bildbeschreibungen' %}
</a>
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const input = document.getElementById("image");
const previewWrapper = document.getElementById("image-preview-wrapper");
const preview = document.getElementById("image-preview");
const filename = document.getElementById("image-upload-filename");
input.addEventListener("change", function () {
const file = this.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function (e) {
preview.src = e.target.result;
previewWrapper.style.display = "block";
filename.innerText = file.name;
}
reader.readAsDataURL(file);
} else {
preview.src = "";
previewWrapper.style.display = "none";
}
});
});
</script>
{% endblock %} {% endblock %}

View File

@@ -1,6 +1,9 @@
{% extends "fellchensammlung/base.html" %} {% extends "fellchensammlung/base.html" %}
{% load i18n %} {% load i18n %}
{% block description %}
<meta name="description" content="{% trans 'Inhalt melden' %}">
{% endblock %}
{% block content %} {% block content %}
<h1 class="title is-1">{% translate "Melden" %}</h1> <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. 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.

View File

@@ -27,7 +27,7 @@
{{ field|add_class:"input" }} {{ field|add_class:"input" }}
{% endif %} {% endif %}
</div> </div>
<div class="help"> <div class="help content">
{{ field.help_text }} {{ field.help_text }}
</div> </div>
<div class="help is-danger"> <div class="help is-danger">

View File

@@ -9,7 +9,8 @@
<h1 class="title is-4">notfellchen.org</h1> <h1 class="title is-4">notfellchen.org</h1>
</a> </a>
<a role="button" class="navbar-burger" aria-label="{% trans 'Hauptmenü' %}" tabindex="0" aria-expanded="false" data-target="navbarBasicExample"> <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> <span aria-hidden="true"></span>
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
@@ -30,7 +31,16 @@
</div> </div>
<div class="navbar-end"> <div class="navbar-end">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<div class="navbar-item">
<div class="notification-container">
<a class="notification-label" href="{% url 'user-notifications' %}">
<i class="fas fa-bell fa-fw"></i>
</a>
{% if request.user.get_num_unread_notifications > 0 %}
<span class="notification-badge">{{ request.user.get_num_unread_notifications }}</span>
{% endif %}
</div>
</div>
<div class="navbar-item"> <div class="navbar-item">
<a href="{% url 'user-me' %}"> <a href="{% url 'user-me' %}">
<i class="fas fa-user fa-fw"></i> {{ user }} <i class="fas fa-user fa-fw"></i> {{ user }}
@@ -39,10 +49,10 @@
{% else %} {% else %}
<div class="navbar-item"> <div class="navbar-item">
<div class="buttons"> <div class="buttons">
<a class="button is-primary" href="{% url "django_registration_register" %}"> <a class="button is-primary" href="{% url "account_signup" %}">
<strong>{% translate "Registrieren" %}</strong> <strong>{% translate "Registrieren" %}</strong>
</a> </a>
<a class="button is-light" href="{% url "login" %}"> <a class="button is-light" href="{% url "account_login" %}">
<strong>{% translate "Login" %}</strong> <strong>{% translate "Login" %}</strong>
</a> </a>
</div> </div>

View File

@@ -0,0 +1,171 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="264.58334mm"
height="264.58334mm"
viewBox="0 0 264.58334 264.58334"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="drawing.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.51136354"
inkscape:cx="1504.8003"
inkscape:cy="472.26675"
inkscape:window-width="2048"
inkscape:window-height="1208"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:page
x="0"
y="0"
width="264.58334"
height="264.58334"
id="page1"
margin="0"
bleed="0"/>
<inkscape:page
x="274.58334"
y="0"
width="264.58334"
height="264.58334"
id="page2"
margin="0"
bleed="0"/>
<inkscape:page
x="549.16669"
y="0"
width="264.58334"
height="264.58334"
id="page4"
margin="0"
bleed="0"/>
</sodipodi:namedview>
<defs id="defs1"/>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#6cd4ff;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;fill-opacity:0.73333335"
id="rect1"
width="264.58337"
height="264.58337"
x="0"
y="0"/>
<rect
style="fill:#6cd4ff;fill-opacity:0.733333;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"
id="rect1-2"
width="264.58337"
height="264.58337"
x="274.58334"
y="0"/>
<rect
style="fill:#6cd4ff;fill-opacity:0.733333;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"
id="rect1-0"
width="264.58337"
height="264.58337"
x="549.16669"
y="0"/>
<text
xml:space="preserve"
style="font-size:19.7556px;line-height:1.3;font-family:'Latin Modern Mono';-inkscape-font-specification:'Latin Modern Mono, Normal';text-align:start;letter-spacing:0px;word-spacing:-0.574146px;writing-mode:lr-tb;direction:ltr;text-anchor:start;white-space:pre;inline-size:230.276;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"
x="16.116602"
y="81.350471"
id="text4"
transform="translate(-7.9696277,63.01184)"><tspan
x="16.116602"
y="81.350471"
id="tspan2">{{ adoption_notice.short_description }}</tspan></text>
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.17108;stroke-linecap:round;stroke-linejoin:round"
id="rect4"
width="192.30475"
height="93.450798"
x="42.798157"
y="14.846257"/>
<text
xml:space="preserve"
style="font-size:19.7556px;line-height:1.1;font-family:'Latin Modern Mono';-inkscape-font-specification:'Latin Modern Mono, Normal';text-align:start;letter-spacing:0px;word-spacing:-0.79286px;writing-mode:lr-tb;direction:ltr;text-anchor:start;white-space:pre;inline-size:192.796;fill:#000000;fill-opacity:0.733333;stroke:none;stroke-width:2.76188;stroke-linecap:round;stroke-linejoin:round"
x="98.301682"
y="49.274101"
id="text1"
transform="translate(-58.467048,-14.731528)"><tspan
x="98.301682"
y="49.274101"
id="tspan4"><tspan
dx="0 0.79285997 -0.79286003 0 0 0 0 0 0.79285902 -0.79285526 0 0.79285902"
style="text-align:center;text-anchor:middle"
id="tspan3">{{ adoption_notice.name }}</tspan>
</tspan>
</text>
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"
id="rect4-8"
width="175.74771"
height="40.675236"
x="309.5625"
y="17.4625"/>
<text
xml:space="preserve"
style="font-size:36.6612px;line-height:1.3;font-family:'Latin Modern Mono';-inkscape-font-specification:'Latin Modern Mono, Normal';text-align:start;letter-spacing:0px;word-spacing:-1.06546px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:0.733333;stroke:none;stroke-width:3.71147;stroke-linecap:round;stroke-linejoin:round"
x="328.02808"
y="47.813782"
id="text1-7">
<tspan
sodipodi:role="line"
id="tspan1-9"
style="fill:#000000;fill-opacity:0.733333;stroke-width:3.71147"
x="328.02808"
y="47.813782">
Ratte 1
</tspan>
</text>
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"
id="rect4-7"
width="175.74771"
height="40.675236"
x="597.69373"
y="18.520834"/>
<text
xml:space="preserve"
style="font-size:36.6612px;line-height:1.3;font-family:'Latin Modern Mono';-inkscape-font-specification:'Latin Modern Mono, Normal';text-align:start;letter-spacing:0px;word-spacing:-1.06546px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:0.733333;stroke:none;stroke-width:3.71147;stroke-linecap:round;stroke-linejoin:round"
x="616.1593"
y="48.872116"
id="text1-5">
<tspan
sodipodi:role="line"
id="tspan1-92"
style="fill:#000000;fill-opacity:0.733333;stroke-width:3.71147"
x="616.1593"
y="48.872116">Ratte 2</tspan>
</text>
<image
width="116.45744"
height="145.62663"
preserveAspectRatio="none"
xlink:href="data:image/jpeg;base64,{{ adoption_notice.get_photo.as_base64 }};"
id="image1"
x="339.55661"
y="87.612373"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -23,7 +23,9 @@
{% endfor %} {% endfor %}
{% if introduction %} {% if introduction %}
<h1>{{ introduction.title }}</h1> <h1>{{ introduction.title }}</h1>
<div class="content">
{{ introduction.content | render_markdown }} {{ introduction.content | render_markdown }}
</div>
{% endif %} {% endif %}
<h2 class="title is-2">{% translate "Aktuelle Vermittlungen" %}</h2> <h2 class="title is-2">{% translate "Aktuelle Vermittlungen" %}</h2>
@@ -44,7 +46,7 @@
<h2 class="title is-1">{{ how_to.title }}</h2> <h2 class="title is-1">{{ how_to.title }}</h2>
</div> </div>
</div> </div>
<div class="card-content"> <div class="card-content content">
{{ how_to.content | render_markdown }} {{ how_to.content | render_markdown }}
</div> </div>
</div> </div>

View File

@@ -3,10 +3,23 @@
<div class="grid is-col-min-10"> <div class="grid is-col-min-10">
{% for adoption_notice in adoption_notices %} {% for adoption_notice in adoption_notices %}
<div class="cell"> <div class="cell">
{% if expand %}
{% include "fellchensammlung/partials/partial-adoption-notice.html" %}
{% else %}
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %} {% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<article class="message is-warning">
<div class="message-header">
<p>{% translate "Keine Vermittlungen gefunden." %}</p> <p>{% translate "Keine Vermittlungen gefunden." %}</p>
</div>
<div class="message-body">
{% blocktranslate %}
Versuche es zu einem späteren Zeitpunkt erneut.
{% endblocktranslate %}
</div>
</article>
{% endif %} {% endif %}

View File

@@ -3,7 +3,7 @@
{% if rescue_organizations %} {% if rescue_organizations %}
{% for rescue_organization in rescue_organizations %} {% for rescue_organization in rescue_organizations %}
<div class="cell"> <div class="cell">
{% include "fellchensammlung/partials/partial-rescue-organization.html" %} {% include "fellchensammlung/partials/rescue_orgs/partial-rescue-organization.html" %}
</div> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}

View File

@@ -54,6 +54,11 @@
font-size: 14px; font-size: 14px;
} }
.setting-info {
font-size: 10px;
}
@media (max-width: 600px) { @media (max-width: 600px) {
.content, .header, .footer { .content, .header, .footer {
padding: 20px 15px; padding: 20px 15px;

View File

@@ -0,0 +1,4 @@
{% block content %}{% endblock %}
---
{% include "fellchensammlung/mail/footer.txt" %}

View File

@@ -1,3 +1,12 @@
{% load i18n %}
<div class="footer"> <div class="footer">
🐀 notfellchen.org | Für Menschen die Ratten aus dem Tierschutz ein liebendes Zuhause geben wollen. 🐀 notfellchen.org | Für Menschen die Ratten aus dem Tierschutz ein liebendes Zuhause geben wollen.
{% if notification %}
<div class="setting-info">
{% trans "Du bekommst diese Nachricht basierend auf deinen Benachrichtigungseinstellungen." %}<br>
<a href="{{ notification.user_to_notify.get_full_url }}">
{% trans "Einstellungen ändern" %}
</a>
</div>
{% endif %}
</div> </div>

View File

@@ -0,0 +1,7 @@
{% load i18n %}
🐀 notfellchen.org | Für Menschen die Ratten aus dem Tierschutz ein liebendes Zuhause geben wollen.
{% if notification %}
{% trans "Du bekommst diese Nachricht basierend auf deinen Benachrichtigungseinstellungen." %}
{% trans "Einstellungen ändern" %}: {{ notification.user_to_notify.get_full_url }}
{% endif %}

View File

@@ -0,0 +1,7 @@
{% extends "fellchensammlung/mail/base.txt" %}
{% load i18n %}
{% block content %}{% blocktranslate %}Moin,
die Vermittlung {{ notification.adoption_notice }} wurde deaktiviert.
{% endblocktranslate %}
{% translate 'Vermittlung anzeigen' %}: {{ notification.adoption_notice.get_full_url }}{% endblock %}

View File

@@ -0,0 +1,9 @@
{% extends "fellchensammlung/mail/base.txt" %}
{% load i18n %}
{% block content %}{% blocktranslate %}Moin,
es wurde eine neue Vermittlung gefunden, die deinen Kriterien entspricht: {{ notification.adoption_notice }}
Vermittlung anzeigen: {{ notification.adoption_notice.get_full_url }}
{% endblocktranslate %}{% endblock %}

View File

@@ -0,0 +1,8 @@
{% extends "fellchensammlung/mail/base.txt" %}
{% load i18n %}
{% block content %}{% blocktranslate %}Moin,
die Vermittlung {{ notification.adoption_notice }} muss überprüft werden.
Vermittlung anzeigen: {{ notification.adoption_notice.get_full_url }}
{% endblocktranslate %}{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends "fellchensammlung/mail/base.html" %}
{% load i18n %}
{% load custom_tags %}
{% block content %}{% blocktranslate %}Moin,
folgender Kommentar wurde zur Vermittlung {{ notification.adoption_notice }} hinzugefügt:
{{ notification.comment.text }}
Vermittlung anzeigen: {{ notification.adoption_notice.get_full_url }}
{% endblocktranslate %}{% endblock %}

View File

@@ -14,6 +14,6 @@
Details findest du hier Details findest du hier
</p> </p>
<p> <p>
<a href="{{ notification.user_related.get_absolute_url }}" class="cta-button">{% translate 'User anzeigen' %}</a> <a href="{{ notification.user_related.get_full_url }}" class="cta-button">{% translate 'User anzeigen' %}</a>
</p> </p>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,8 @@
{% extends "fellchensammlung/mail/base.txt" %}
{% load i18n %}
{% block content %}{% blocktranslate with new_user_url=notification.user_related.get_full_url %}Moin,
es wurde ein neuer Useraccount erstellt.
User anzeigen: {{ new_user_url }}
{% endblocktranslate %}{% endblock %}

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