Compare commits
	
		
			3 Commits
		
	
	
		
			main
			...
			ci-test-co
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| c9f46d7547 | |||
| 9f23f5768c | |||
| 19210f90cd | 
							
								
								
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -2,18 +2,11 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Database
 | 
					# Database
 | 
				
			||||||
notfellchen
 | 
					notfellchen
 | 
				
			||||||
*.sq3
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Geojson from imports
 | 
					 | 
				
			||||||
*.geojson
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Media storage
 | 
					# Media storage
 | 
				
			||||||
/static
 | 
					static
 | 
				
			||||||
media
 | 
					media
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Compiled CSS
 | 
					 | 
				
			||||||
/src/fellchensammlung/static/fellchensammlung/css/main.css
 | 
					 | 
				
			||||||
/src/fellchensammlung/static/fellchensammlung/css/main.css.map
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Byte-compiled / optimized / DLL files
 | 
					# Byte-compiled / optimized / DLL files
 | 
				
			||||||
__pycache__/
 | 
					__pycache__/
 | 
				
			||||||
@@ -168,4 +161,3 @@ dmypy.json
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Cython debug symbols
 | 
					# Cython debug symbols
 | 
				
			||||||
cython_debug/
 | 
					cython_debug/
 | 
				
			||||||
/node_modules/
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,9 @@ steps:
 | 
				
			|||||||
    commands:
 | 
					    commands:
 | 
				
			||||||
      - cd docs && make html
 | 
					      - cd docs && make html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    when:
 | 
				
			||||||
 | 
					      event: [ tag, push ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  deploy:
 | 
					  deploy:
 | 
				
			||||||
    image: appleboy/drone-scp
 | 
					    image: appleboy/drone-scp
 | 
				
			||||||
    settings:
 | 
					    settings:
 | 
				
			||||||
@@ -19,6 +22,8 @@ steps:
 | 
				
			|||||||
      source: docs/_build/html/
 | 
					      source: docs/_build/html/
 | 
				
			||||||
      key:
 | 
					      key:
 | 
				
			||||||
        from_secret: ssh_key
 | 
					        from_secret: ssh_key
 | 
				
			||||||
 | 
					    when:
 | 
				
			||||||
 | 
					      event: [ tag, push ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										14
									
								
								.woodpecker/test.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					steps:
 | 
				
			||||||
 | 
					  test:
 | 
				
			||||||
 | 
					    image: python
 | 
				
			||||||
 | 
					    commands:
 | 
				
			||||||
 | 
					      - python -m pip install '.[develop]'
 | 
				
			||||||
 | 
					      - coverage run --source='.' src/manage.py test src && coverage html
 | 
				
			||||||
 | 
					      - coverage html
 | 
				
			||||||
 | 
					      - cat htmlcov/index.html
 | 
				
			||||||
 | 
					    when:
 | 
				
			||||||
 | 
					      event: [tag, push]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
FROM python:3.11-slim
 | 
					FROM python:3.11-slim
 | 
				
			||||||
# Use 3.11 to avoid django.core.exceptions.ImproperlyConfigured: Error loading psycopg2 or psycopg module
 | 
					# Use 3.11 to avoid django.core.exceptions.ImproperlyConfigured: Error loading psycopg2 or psycopg module
 | 
				
			||||||
LABEL org.opencontainers.image.authors="Julian-Samuel Gebühr"
 | 
					MAINTAINER Julian-Samuel Gebühr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ENV DOCKER_BUILD=true
 | 
					ENV DOCKER_BUILD=true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										51
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -2,7 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[notfellchen.org](https://notfellchen.org) ist eine Sammelstelle für Tier-Vermittlungen. Die Idee entstand, da in der
 | 
					[notfellchen.org](https://notfellchen.org) ist eine Sammelstelle für Tier-Vermittlungen. Die Idee entstand, da in der
 | 
				
			||||||
deutschsprachigen Rattencommunity ein wilder Mix aus Websites, Foren und Facebookgruppen besteht die Ratten vermitteln.
 | 
					deutschsprachigen Rattencommunity ein wilder Mix aus Websites, Foren und Facebookgruppen besteht die Ratten vermitteln.
 | 
				
			||||||
Diese Website soll die bestehende Communitys NICHT ersetzten, jedoch ermöglichen, dass Menschen die Ratten aufnehmen
 | 
					Diese Website soll die bestehende Communities NICHT ersetzten, jedoch ermöglichen, dass Menschen die Ratten aufnehmen
 | 
				
			||||||
wollen Informationen einfach finden und nicht bereits in jeder Gruppe sein müssen.
 | 
					wollen Informationen einfach finden und nicht bereits in jeder Gruppe sein müssen.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Wir nehmen Angebote auf die
 | 
					Wir nehmen Angebote auf die
 | 
				
			||||||
@@ -57,50 +57,10 @@ Therefore, a solution is used where a number of predefined texts per site are su
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Developer Notes
 | 
					# Developer Notes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Getting started
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Clone the project
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
git clone https://codeberg.org/moanos/notfellchen.git
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Install dependencies
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
pip install -e '.[all]'
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Create the database
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
nf migrate
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Because of a wired bug the initial migrations must run two times as the first time the permissions
 | 
					Because of a wired bug the initial migrations must run two times as the first time the permissions
 | 
				
			||||||
for `create_active_adoption_notice` are created but can not yet be accessed and on the second time this permission will
 | 
					for `create_active_adoption_notice` are created but can not yet be accessed and on the second time this permission will
 | 
				
			||||||
be added to groups.
 | 
					be added to groups.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Start the server
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
nf runserver
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
### Build the docs
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
sphinx-autobuild ./docs ./docs/_build/html
 | 
					 | 
				
			||||||
```
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Styling
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Bulma is used for styling, including related SCSS. All styles should eventually be migrated to SCSS.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Use `npm run build-bulma` to generate the css file from SCSS.
 | 
					 | 
				
			||||||
You can use `npm start` during development so that the file is re-generated upon change.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
## Docker
 | 
					## Docker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Build latest image
 | 
					Build latest image
 | 
				
			||||||
@@ -117,7 +77,6 @@ docker push moanos/notfellchen:latest
 | 
				
			|||||||
docker run -p8000:7345 moanos/notfellchen:latest
 | 
					docker run -p8000:7345 moanos/notfellchen:latest
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
## Testing
 | 
					## Testing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Tests can be run with
 | 
					Tests can be run with
 | 
				
			||||||
@@ -185,17 +144,17 @@ Start beat
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Contributing
 | 
					# Contributing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This project is currently mainly developed by me, moanos. I'd like that to change and will be very happy for contributions
 | 
					This project is currently solely developed by me, moanos. I'd like that to change and will be very happy for contributions
 | 
				
			||||||
and shared responsibilities. Some ideas where you can look for contributing first
 | 
					and shared responsibilities. Some ideas where you can look for contributing first
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* UI improvements: Since a major redesign I'm much happier but the UI could use many, many little tweaks
 | 
					* CSS structure: It's a hot mess right now, and I'm happy it somehow works. As you might see, there is much room for improvement. Refactoring this and streamlining the look across the app would be amazing.
 | 
				
			||||||
* Docker: If you know how to build a docker container that is a) smaller or b) utilizes staged builds this would be amazing. Any improvement welcome
 | 
					* Docker: If you know how to build a docker container that is a) smaller or b) utilizes staged builds this would be amazing. Any improvement welcome
 | 
				
			||||||
* Testing: Writing tests is always welcome, and it's likely you discover a few bugs
 | 
					* Testing: Writing tests is always welcome, and it's likely you discover a few bugs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
I'm also very happy for all other contributions. Before you do large refactoring efforts or features, best write a short
 | 
					I'm also very happy for all other contributions. Before you do large refactoring efforts or features, best write a short
 | 
				
			||||||
issue for it before you spend a lot of work.
 | 
					issue for it before you spend a lot of work.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Send PRs either to [codeberg](https://codeberg.org/moanos/notfellchen) (preferred) or [GitHub](https://github.com/moan0s/notfellchen).
 | 
					Send PRs either to [codeberg](https://codeberg.org/moanos/notfellchen) (preferred) or [Github](https://github.com/moan0s/notfellchen).
 | 
				
			||||||
CI (currently only for documentation) runs via [git.hyteck.de](https://git.hyteck.de), you can also ask moanos for an account there.
 | 
					CI (currently only for dcumentation) runs via [git.hyteck.de](https://git.hyteck.de), you can also ask moanos for an account there.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Also welcome are new issues with suggestions or bugs and additions to the documentation.
 | 
					Also welcome are new issues with suggestions or bugs and additions to the documentation.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,8 +14,7 @@ Via browser
 | 
				
			|||||||
-----------
 | 
					-----------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
When a user is logged in, they can easily access the API in their browser, authenticated by their session.
 | 
					When a user is logged in, they can easily access the API in their browser, authenticated by their session.
 | 
				
			||||||
 | 
					The API endpoint can be found at http://notfellchen.org/api/adoption_notices
 | 
				
			||||||
For example: You can check all current adoption notices here: https://notfellchen.org/api/adoption_notice
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
Via token
 | 
					Via token
 | 
				
			||||||
---------
 | 
					---------
 | 
				
			||||||
@@ -29,9 +28,9 @@ An application can then send this token in the request header for authorization.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. warning::
 | 
					.. warning::
 | 
				
			||||||
    Usage or creation of content still has to follow the terms of notfellchen.org.
 | 
					    Usage or creation of content still has to follow the terms of Notfellchen.org
 | 
				
			||||||
    Copyright of content is often held by rescue organizations, so you are not allowed to simply mirror content.
 | 
					    Copyright of content is often held by rescue organizations, so you are not allowed to simply mirror content.
 | 
				
			||||||
    Talk to the notfellchen team if you want develop such things.
 | 
					    Talk to the Notfellchen-Team if you want develop such things.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Endpoints
 | 
					Endpoints
 | 
				
			||||||
@@ -46,8 +45,7 @@ Get Adoption Notices
 | 
				
			|||||||
++++++++++++++++++++
 | 
					++++++++++++++++++++
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. code-block::
 | 
					.. code-block::
 | 
				
			||||||
 | 
					    curl --request GET \
 | 
				
			||||||
  curl --request GET \
 | 
					 | 
				
			||||||
  --url https://notfellchen.org/api/adoption_notice \
 | 
					  --url https://notfellchen.org/api/adoption_notice \
 | 
				
			||||||
  --header 'Authorization: {{token}}'
 | 
					  --header 'Authorization: {{token}}'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -55,8 +53,7 @@ Create Adoption Notice
 | 
				
			|||||||
++++++++++++++++++++++
 | 
					++++++++++++++++++++++
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. code-block::
 | 
					.. code-block::
 | 
				
			||||||
 | 
					    curl --request POST \
 | 
				
			||||||
  curl --request POST \
 | 
					 | 
				
			||||||
  --url https://notfellchen.org/api/adoption_notice \
 | 
					  --url https://notfellchen.org/api/adoption_notice \
 | 
				
			||||||
  --header 'Authorization: {{token}}' \
 | 
					  --header 'Authorization: {{token}}' \
 | 
				
			||||||
  --header 'content-type: multipart/form-data' \
 | 
					  --header 'content-type: multipart/form-data' \
 | 
				
			||||||
@@ -71,7 +68,6 @@ Add Animal to Adoption Notice
 | 
				
			|||||||
+++++++++++++++++++++++++++++
 | 
					+++++++++++++++++++++++++++++
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. code-block::
 | 
					.. code-block::
 | 
				
			||||||
 | 
					 | 
				
			||||||
    curl --request POST \
 | 
					    curl --request POST \
 | 
				
			||||||
      --url https://notfellchen.org/api/animals/ \
 | 
					      --url https://notfellchen.org/api/animals/ \
 | 
				
			||||||
      --header 'Authorization: {{token}}' \
 | 
					      --header 'Authorization: {{token}}' \
 | 
				
			||||||
@@ -87,7 +83,6 @@ Add picture to Animal or Adoption Notice
 | 
				
			|||||||
++++++++++++++++++++++++++++++++++++++++
 | 
					++++++++++++++++++++++++++++++++++++++++
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. code-block::
 | 
					.. code-block::
 | 
				
			||||||
 | 
					 | 
				
			||||||
    curl -X POST https://notfellchen.org/api/images/ \
 | 
					    curl -X POST https://notfellchen.org/api/images/ \
 | 
				
			||||||
    -H "Authorization: Token {{token}}" \
 | 
					    -H "Authorization: Token {{token}}" \
 | 
				
			||||||
    -F "image=@256-256-crop.jpg" \
 | 
					    -F "image=@256-256-crop.jpg" \
 | 
				
			||||||
@@ -101,7 +96,6 @@ Species
 | 
				
			|||||||
Getting available species is mainly important when creating animals
 | 
					Getting available species is mainly important when creating animals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. code-block::
 | 
					.. code-block::
 | 
				
			||||||
 | 
					 | 
				
			||||||
    curl --request GET \
 | 
					    curl --request GET \
 | 
				
			||||||
      --url https://notfellchen.org/api/species \
 | 
					      --url https://notfellchen.org/api/species \
 | 
				
			||||||
      --header 'Authorization: {{token}}'
 | 
					      --header 'Authorization: {{token}}'
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,74 +0,0 @@
 | 
				
			|||||||
from __future__ import annotations
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from pathlib import Path
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from docutils import nodes
 | 
					 | 
				
			||||||
from sphinx.application import Sphinx
 | 
					 | 
				
			||||||
from sphinx.util.docutils import SphinxDirective
 | 
					 | 
				
			||||||
from sphinx.util.typing import ExtensionMetadata
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class DrawioDirective(SphinxDirective):
 | 
					 | 
				
			||||||
    """A directive to show a drawio diagram!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Usage:
 | 
					 | 
				
			||||||
    .. drawio::
 | 
					 | 
				
			||||||
       example-diagram.drawio.html
 | 
					 | 
				
			||||||
       example-diagram.drawio.png
 | 
					 | 
				
			||||||
       :alt: Example of a Draw.io diagram
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    has_content = False
 | 
					 | 
				
			||||||
    required_arguments = 2  # html and png
 | 
					 | 
				
			||||||
    optional_arguments = 1
 | 
					 | 
				
			||||||
    final_argument_whitespace = True  # indicating if the final argument may contain whitespace
 | 
					 | 
				
			||||||
    option_spec = {
 | 
					 | 
				
			||||||
        "alt": str,
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def run(self) -> list[nodes.Node]:
 | 
					 | 
				
			||||||
        env = self.state.document.settings.env
 | 
					 | 
				
			||||||
        builder = env.app.builder
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Resolve paths relative to the document
 | 
					 | 
				
			||||||
        docdir = Path(env.doc2path(env.docname)).parent
 | 
					 | 
				
			||||||
        html_rel = Path(self.arguments[0])
 | 
					 | 
				
			||||||
        png_rel = Path(self.arguments[1])
 | 
					 | 
				
			||||||
        html_path = (docdir / html_rel).resolve()
 | 
					 | 
				
			||||||
        png_path = (docdir / png_rel).resolve()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        alt_text = self.options.get("alt", "")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        container = nodes.container()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # HTML output -> raw HTML node
 | 
					 | 
				
			||||||
        if builder.format == "html":
 | 
					 | 
				
			||||||
            # Embed the HTML file contents directly
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                html_content = html_path.read_text(encoding="utf-8")
 | 
					 | 
				
			||||||
            except OSError as e:
 | 
					 | 
				
			||||||
                msg = self.state_machine.reporter.error(f"Cannot read HTML file: {e}")
 | 
					 | 
				
			||||||
                return [msg]
 | 
					 | 
				
			||||||
            aria_attribute = f' aria-label="{alt_text}"' if alt_text else ""
 | 
					 | 
				
			||||||
            raw_html_node = nodes.raw(
 | 
					 | 
				
			||||||
                "",
 | 
					 | 
				
			||||||
                f'<div class="drawio-diagram"{aria_attribute}>{html_content}</div>',
 | 
					 | 
				
			||||||
                format="html",
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            container += raw_html_node
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            # Other outputs -> PNG image node
 | 
					 | 
				
			||||||
            image_node = nodes.image(uri=png_path)
 | 
					 | 
				
			||||||
            container += image_node
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return [container]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def setup(app: Sphinx) -> ExtensionMetadata:
 | 
					 | 
				
			||||||
    app.add_directive("drawio", DrawioDirective)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
        "version": "0.2",
 | 
					 | 
				
			||||||
        "parallel_read_safe": True,
 | 
					 | 
				
			||||||
        "parallel_write_safe": True,
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
@@ -67,6 +67,5 @@ Healthchecks
 | 
				
			|||||||
You can configure notfellchen to give a hourly ping to a healthchecks server. If this ping is not received, you will get notified and cna check why the celery jobs are no running.
 | 
					You can configure notfellchen to give a hourly ping to a healthchecks server. If this ping is not received, you will get notified and cna check why the celery jobs are no running.
 | 
				
			||||||
Add the following to your `notfellchen.cfg` and adjust the URL to match your check.
 | 
					Add the following to your `notfellchen.cfg` and adjust the URL to match your check.
 | 
				
			||||||
.. code::
 | 
					.. code::
 | 
				
			||||||
 | 
					 | 
				
			||||||
  [monitoring]
 | 
					  [monitoring]
 | 
				
			||||||
  healthchecks_url=https://health.example.org/ping/5fa7c9b2-753a-4cb3-bcc9-f982f5bc68e8
 | 
					  healthchecks_url=https://health.example.org/ping/5fa7c9b2-753a-4cb3-bcc9-f982f5bc68e8
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								docs/conf.py
									
									
									
									
									
								
							
							
						
						@@ -16,10 +16,6 @@
 | 
				
			|||||||
# 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 -----------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -32,6 +28,7 @@ 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.
 | 
				
			||||||
@@ -43,7 +40,6 @@ 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.
 | 
				
			||||||
@@ -73,6 +69,7 @@ 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
 | 
				
			||||||
@@ -107,6 +104,7 @@ 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 = {
 | 
				
			||||||
@@ -135,6 +133,7 @@ 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
 | 
				
			||||||
@@ -144,6 +143,7 @@ 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,6 +155,7 @@ texinfo_documents = [
 | 
				
			|||||||
     'Miscellaneous'),
 | 
					     'Miscellaneous'),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# -- Options for Epub output -------------------------------------------------
 | 
					# -- Options for Epub output -------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Bibliographic Dublin Core info.
 | 
					# Bibliographic Dublin Core info.
 | 
				
			||||||
@@ -172,4 +173,5 @@ 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 -------------------------------------------------
 | 
				
			||||||
 
 | 
				
			|||||||
| 
		 Before Width: | Height: | Size: 53 KiB  | 
| 
		 Before Width: | Height: | Size: 18 KiB  | 
| 
		 Before Width: | Height: | Size: 120 KiB  | 
| 
		 Before Width: | Height: | Size: 150 KiB  | 
@@ -6,27 +6,14 @@ 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 Adresse an. An diese senden wir Benachrichtigungen, außer
 | 
					Mit während deiner :doc:`registrierung` gibst du eine E-Mail Addresse an.
 | 
				
			||||||
du deaktiviert dies wie oben beschrieben.
 | 
					
 | 
				
			||||||
 | 
					Benachrichtigungen senden wir per Mail - du kannst das jederzeit in den Einstellungen deaktivieren.
 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 46 KiB  | 
@@ -1,58 +0,0 @@
 | 
				
			|||||||
Erste Schritte
 | 
					 | 
				
			||||||
==============
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Tiere zum Adoptieren suchen
 | 
					 | 
				
			||||||
---------------------------
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Wenn du Tiere zum adoptieren suchst, brauchst du keinen Account. Du kannst bequem die `Suche <https://notfellchen.org/suchen/>`_ nutzen, um Tiere zur Adoption in deiner Nähe zu finden.
 | 
					 | 
				
			||||||
Wenn dich eine Vermittlung interessiert, kannst du folgendes tun
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
* die Vermittlung aufrufen um Details zu sehen
 | 
					 | 
				
			||||||
* den Link :guilabel:`Weitere Informationen` anklicken um auf der Tierheimwebsite mehr zu erfahren
 | 
					 | 
				
			||||||
* per Kommentar weitere Informationen erfragen oder hinzufügen
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Wenn du die Tiere tatsächlich informieren willst, folge der Anleitung unter :guilabel:`Adoptionsprozess`.
 | 
					 | 
				
			||||||
Dieser kann sich je nach Tierschutzorganisation unterscheiden.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.. image::
 | 
					 | 
				
			||||||
   screenshot-adoptionsprozess.png
 | 
					 | 
				
			||||||
   :alt: Screenshot der Sektion "Adoptionsprozess" einer Vermittlungsanzeige. Der Prozess ist folgendermaßen: 1. Link zu "Weiteren Informationen" prüfen, 2.  Organization kontaktieren, 3. Bei erfolgreicher Vermittlung: Vermittlung als geschlossen melden
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Suchen abonnieren
 | 
					 | 
				
			||||||
+++++++++++++++++
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Es kann sein, dass es in deiner Umgebung keine passenden Tiere für deine Suche gibt. Damit du nicht ständig wieder Suchen musst, gibt es die Funktion "Suche abonnieren".
 | 
					 | 
				
			||||||
Wenn du eine Suche abonnierst, wirst du für neue Vermittlungen, die den Kriterien der Suche entsprechen, benachrichtigt.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.. image::
 | 
					 | 
				
			||||||
   screenshot-suche-abonnieren.png
 | 
					 | 
				
			||||||
   :alt: Screenshot der Suchmaske auf Notfellchen.org . Ein roter Pfeil zeigt auf den Button "Suche abonnieren"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.. important::
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   Um Suchen zu abonnieren brauchst du einen Account. Wie du einen Account erstellst erfährst du hier: :doc:`registrierung`.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.. hint::
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   Mehr über Benachrichtigungen findest du hier: :doc:`benachrichtigungen`.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Vermittlungen hinzufügen
 | 
					 | 
				
			||||||
------------------------
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Gehe zu `Vermittlung hinzufügen <https://notfellchen.org/vermitteln/>`_ um eine neue Vermittlung einzustellen.
 | 
					 | 
				
			||||||
Füge alle Informationen die du hast hinzu.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.. important::
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   Um Vermittlungen hinzuzufügen brauchst du einen Account.
 | 
					 | 
				
			||||||
   Wie du einen Account erstellst erfährst du hier: :doc:`registrierung`.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.. important::
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
   Vermittlungen die du einstellst müssen erst durch Moderator\*innen freigeschaltet werden. Das passiert normalerweise
 | 
					 | 
				
			||||||
   innerhalb von 24 Stunden. Wenn deine Vermittlung dann noch nicht freigeschaltet ist, prüfe bitte dein E-Mail Postfach,
 | 
					 | 
				
			||||||
   es könnte sein, dass die Moderator\*innen Rückfragen haben. Melde dich gerne unter info@notfellchen.org, wenn deine
 | 
					 | 
				
			||||||
   Vermittlung nach 24 Stunden nicht freigeschaltet ist.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@@ -1,17 +1,11 @@
 | 
				
			|||||||
****************
 | 
					******************
 | 
				
			||||||
Benutzerhandbuch
 | 
					User Dokumentation
 | 
				
			||||||
****************
 | 
					******************
 | 
				
			||||||
 | 
					 | 
				
			||||||
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
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,55 +0,0 @@
 | 
				
			|||||||
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`      |
 | 
					 | 
				
			||||||
+------------------------------------------------------+---------------+
 | 
					 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 34 KiB  | 
| 
		 Before Width: | Height: | Size: 205 KiB  | 
| 
		 Before Width: | Height: | Size: 24 KiB  | 
| 
		 Before Width: | Height: | Size: 20 KiB  | 
@@ -1,7 +1,7 @@
 | 
				
			|||||||
Vermittlungen
 | 
					Vermittlungen
 | 
				
			||||||
=============
 | 
					=============
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Vermittlungen können von allen Nutzer\*innen mit Account erstellt werden. Vermittlungen normaler Nutzer*innen kommen dann in eine Warteschlange und werden vom Admin & Modertionsteam geprüft und sichtbar geschaltet.
 | 
					Vermittlungen können von allen Nutzer*innen mit Account erstellt werden. Vermittlungen normaler Nutzer*innen kommen dann in eine Warteschlange und werden vom Admin & Modertionsteam geprüft und sichtbar geschaltet.
 | 
				
			||||||
Tierheime und Pflegestellen können auf Anfrage einen Koordinations-Status bekommen, wodurch sie Vermittlungsanzeigen erstellen können die direkt öffentlich sichtbar sind.
 | 
					Tierheime und Pflegestellen können auf Anfrage einen Koordinations-Status bekommen, wodurch sie Vermittlungsanzeigen erstellen können die direkt öffentlich sichtbar sind.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Jede Vermittlung hat ein "Zuletzt-geprüft" Datum, das anzeigt, wann ein Mensch zuletzt überprüft hat, ob die Anzeige noch aktuell ist.
 | 
					Jede Vermittlung hat ein "Zuletzt-geprüft" Datum, das anzeigt, wann ein Mensch zuletzt überprüft hat, ob die Anzeige noch aktuell ist.
 | 
				
			||||||
@@ -15,114 +15,3 @@ 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.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,7 @@ media=./media
 | 
				
			|||||||
static=./static
 | 
					static=./static
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[mail]
 | 
					[mail]
 | 
				
			||||||
console_only=true
 | 
					console-only=true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[logging]
 | 
					[logging]
 | 
				
			||||||
app_log_level=INFO
 | 
					app_log_level=INFO
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										497
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						@@ -1,497 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "name": "notfellchen",
 | 
					 | 
				
			||||||
  "lockfileVersion": 3,
 | 
					 | 
				
			||||||
  "requires": true,
 | 
					 | 
				
			||||||
  "packages": {
 | 
					 | 
				
			||||||
    "": {
 | 
					 | 
				
			||||||
      "dependencies": {
 | 
					 | 
				
			||||||
        "bulma": "^1.0.4",
 | 
					 | 
				
			||||||
        "sass": "^1.89.2"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@parcel/watcher": {
 | 
					 | 
				
			||||||
      "version": "2.5.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
 | 
					 | 
				
			||||||
      "hasInstallScript": true,
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "dependencies": {
 | 
					 | 
				
			||||||
        "detect-libc": "^1.0.3",
 | 
					 | 
				
			||||||
        "is-glob": "^4.0.3",
 | 
					 | 
				
			||||||
        "micromatch": "^4.0.5",
 | 
					 | 
				
			||||||
        "node-addon-api": "^7.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">= 10.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "type": "opencollective",
 | 
					 | 
				
			||||||
        "url": "https://opencollective.com/parcel"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "optionalDependencies": {
 | 
					 | 
				
			||||||
        "@parcel/watcher-android-arm64": "2.5.1",
 | 
					 | 
				
			||||||
        "@parcel/watcher-darwin-arm64": "2.5.1",
 | 
					 | 
				
			||||||
        "@parcel/watcher-darwin-x64": "2.5.1",
 | 
					 | 
				
			||||||
        "@parcel/watcher-freebsd-x64": "2.5.1",
 | 
					 | 
				
			||||||
        "@parcel/watcher-linux-arm-glibc": "2.5.1",
 | 
					 | 
				
			||||||
        "@parcel/watcher-linux-arm-musl": "2.5.1",
 | 
					 | 
				
			||||||
        "@parcel/watcher-linux-arm64-glibc": "2.5.1",
 | 
					 | 
				
			||||||
        "@parcel/watcher-linux-arm64-musl": "2.5.1",
 | 
					 | 
				
			||||||
        "@parcel/watcher-linux-x64-glibc": "2.5.1",
 | 
					 | 
				
			||||||
        "@parcel/watcher-linux-x64-musl": "2.5.1",
 | 
					 | 
				
			||||||
        "@parcel/watcher-win32-arm64": "2.5.1",
 | 
					 | 
				
			||||||
        "@parcel/watcher-win32-ia32": "2.5.1",
 | 
					 | 
				
			||||||
        "@parcel/watcher-win32-x64": "2.5.1"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@parcel/watcher-android-arm64": {
 | 
					 | 
				
			||||||
      "version": "2.5.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
 | 
					 | 
				
			||||||
      "cpu": [
 | 
					 | 
				
			||||||
        "arm64"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "os": [
 | 
					 | 
				
			||||||
        "android"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">= 10.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "type": "opencollective",
 | 
					 | 
				
			||||||
        "url": "https://opencollective.com/parcel"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@parcel/watcher-darwin-arm64": {
 | 
					 | 
				
			||||||
      "version": "2.5.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
 | 
					 | 
				
			||||||
      "cpu": [
 | 
					 | 
				
			||||||
        "arm64"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "os": [
 | 
					 | 
				
			||||||
        "darwin"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">= 10.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "type": "opencollective",
 | 
					 | 
				
			||||||
        "url": "https://opencollective.com/parcel"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@parcel/watcher-darwin-x64": {
 | 
					 | 
				
			||||||
      "version": "2.5.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
 | 
					 | 
				
			||||||
      "cpu": [
 | 
					 | 
				
			||||||
        "x64"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "os": [
 | 
					 | 
				
			||||||
        "darwin"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">= 10.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "type": "opencollective",
 | 
					 | 
				
			||||||
        "url": "https://opencollective.com/parcel"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@parcel/watcher-freebsd-x64": {
 | 
					 | 
				
			||||||
      "version": "2.5.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
 | 
					 | 
				
			||||||
      "cpu": [
 | 
					 | 
				
			||||||
        "x64"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "os": [
 | 
					 | 
				
			||||||
        "freebsd"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">= 10.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "type": "opencollective",
 | 
					 | 
				
			||||||
        "url": "https://opencollective.com/parcel"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@parcel/watcher-linux-arm-glibc": {
 | 
					 | 
				
			||||||
      "version": "2.5.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
 | 
					 | 
				
			||||||
      "cpu": [
 | 
					 | 
				
			||||||
        "arm"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "os": [
 | 
					 | 
				
			||||||
        "linux"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">= 10.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "type": "opencollective",
 | 
					 | 
				
			||||||
        "url": "https://opencollective.com/parcel"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@parcel/watcher-linux-arm-musl": {
 | 
					 | 
				
			||||||
      "version": "2.5.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
 | 
					 | 
				
			||||||
      "cpu": [
 | 
					 | 
				
			||||||
        "arm"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "os": [
 | 
					 | 
				
			||||||
        "linux"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">= 10.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "type": "opencollective",
 | 
					 | 
				
			||||||
        "url": "https://opencollective.com/parcel"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@parcel/watcher-linux-arm64-glibc": {
 | 
					 | 
				
			||||||
      "version": "2.5.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
 | 
					 | 
				
			||||||
      "cpu": [
 | 
					 | 
				
			||||||
        "arm64"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "os": [
 | 
					 | 
				
			||||||
        "linux"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">= 10.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "type": "opencollective",
 | 
					 | 
				
			||||||
        "url": "https://opencollective.com/parcel"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@parcel/watcher-linux-arm64-musl": {
 | 
					 | 
				
			||||||
      "version": "2.5.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
 | 
					 | 
				
			||||||
      "cpu": [
 | 
					 | 
				
			||||||
        "arm64"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "os": [
 | 
					 | 
				
			||||||
        "linux"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">= 10.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "type": "opencollective",
 | 
					 | 
				
			||||||
        "url": "https://opencollective.com/parcel"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@parcel/watcher-linux-x64-glibc": {
 | 
					 | 
				
			||||||
      "version": "2.5.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
 | 
					 | 
				
			||||||
      "cpu": [
 | 
					 | 
				
			||||||
        "x64"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "os": [
 | 
					 | 
				
			||||||
        "linux"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">= 10.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "type": "opencollective",
 | 
					 | 
				
			||||||
        "url": "https://opencollective.com/parcel"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@parcel/watcher-linux-x64-musl": {
 | 
					 | 
				
			||||||
      "version": "2.5.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
 | 
					 | 
				
			||||||
      "cpu": [
 | 
					 | 
				
			||||||
        "x64"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "os": [
 | 
					 | 
				
			||||||
        "linux"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">= 10.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "type": "opencollective",
 | 
					 | 
				
			||||||
        "url": "https://opencollective.com/parcel"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@parcel/watcher-win32-arm64": {
 | 
					 | 
				
			||||||
      "version": "2.5.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
 | 
					 | 
				
			||||||
      "cpu": [
 | 
					 | 
				
			||||||
        "arm64"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "os": [
 | 
					 | 
				
			||||||
        "win32"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">= 10.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "type": "opencollective",
 | 
					 | 
				
			||||||
        "url": "https://opencollective.com/parcel"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@parcel/watcher-win32-ia32": {
 | 
					 | 
				
			||||||
      "version": "2.5.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
 | 
					 | 
				
			||||||
      "cpu": [
 | 
					 | 
				
			||||||
        "ia32"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "os": [
 | 
					 | 
				
			||||||
        "win32"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">= 10.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "type": "opencollective",
 | 
					 | 
				
			||||||
        "url": "https://opencollective.com/parcel"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@parcel/watcher-win32-x64": {
 | 
					 | 
				
			||||||
      "version": "2.5.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
 | 
					 | 
				
			||||||
      "cpu": [
 | 
					 | 
				
			||||||
        "x64"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "os": [
 | 
					 | 
				
			||||||
        "win32"
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">= 10.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "type": "opencollective",
 | 
					 | 
				
			||||||
        "url": "https://opencollective.com/parcel"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/braces": {
 | 
					 | 
				
			||||||
      "version": "3.0.3",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "dependencies": {
 | 
					 | 
				
			||||||
        "fill-range": "^7.1.1"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">=8"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/bulma": {
 | 
					 | 
				
			||||||
      "version": "1.0.4",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.4.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-Ffb6YGXDiZYX3cqvSbHWqQ8+LkX6tVoTcZuVB3lm93sbAVXlO0D6QlOTMnV6g18gILpAXqkG2z9hf9z4hCjz2g==",
 | 
					 | 
				
			||||||
      "license": "MIT"
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/chokidar": {
 | 
					 | 
				
			||||||
      "version": "4.0.3",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "dependencies": {
 | 
					 | 
				
			||||||
        "readdirp": "^4.0.1"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">= 14.16.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "url": "https://paulmillr.com/funding/"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/detect-libc": {
 | 
					 | 
				
			||||||
      "version": "1.0.3",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
 | 
					 | 
				
			||||||
      "license": "Apache-2.0",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "bin": {
 | 
					 | 
				
			||||||
        "detect-libc": "bin/detect-libc.js"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">=0.10"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/fill-range": {
 | 
					 | 
				
			||||||
      "version": "7.1.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "dependencies": {
 | 
					 | 
				
			||||||
        "to-regex-range": "^5.0.1"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">=8"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/immutable": {
 | 
					 | 
				
			||||||
      "version": "5.1.3",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
 | 
					 | 
				
			||||||
      "license": "MIT"
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/is-extglob": {
 | 
					 | 
				
			||||||
      "version": "2.1.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">=0.10.0"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/is-glob": {
 | 
					 | 
				
			||||||
      "version": "4.0.3",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "dependencies": {
 | 
					 | 
				
			||||||
        "is-extglob": "^2.1.1"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">=0.10.0"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/is-number": {
 | 
					 | 
				
			||||||
      "version": "7.0.0",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">=0.12.0"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/micromatch": {
 | 
					 | 
				
			||||||
      "version": "4.0.8",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "dependencies": {
 | 
					 | 
				
			||||||
        "braces": "^3.0.3",
 | 
					 | 
				
			||||||
        "picomatch": "^2.3.1"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">=8.6"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/node-addon-api": {
 | 
					 | 
				
			||||||
      "version": "7.1.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/picomatch": {
 | 
					 | 
				
			||||||
      "version": "2.3.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">=8.6"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "url": "https://github.com/sponsors/jonschlinkert"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/readdirp": {
 | 
					 | 
				
			||||||
      "version": "4.1.2",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">= 14.18.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "funding": {
 | 
					 | 
				
			||||||
        "type": "individual",
 | 
					 | 
				
			||||||
        "url": "https://paulmillr.com/funding/"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/sass": {
 | 
					 | 
				
			||||||
      "version": "1.89.2",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==",
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "dependencies": {
 | 
					 | 
				
			||||||
        "chokidar": "^4.0.0",
 | 
					 | 
				
			||||||
        "immutable": "^5.0.2",
 | 
					 | 
				
			||||||
        "source-map-js": ">=0.6.2 <2.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "bin": {
 | 
					 | 
				
			||||||
        "sass": "sass.js"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">=14.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "optionalDependencies": {
 | 
					 | 
				
			||||||
        "@parcel/watcher": "^2.4.1"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/source-map-js": {
 | 
					 | 
				
			||||||
      "version": "1.2.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
 | 
					 | 
				
			||||||
      "license": "BSD-3-Clause",
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">=0.10.0"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/to-regex-range": {
 | 
					 | 
				
			||||||
      "version": "5.0.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
 | 
					 | 
				
			||||||
      "license": "MIT",
 | 
					 | 
				
			||||||
      "optional": true,
 | 
					 | 
				
			||||||
      "dependencies": {
 | 
					 | 
				
			||||||
        "is-number": "^7.0.0"
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      "engines": {
 | 
					 | 
				
			||||||
        "node": ">=8.0"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@@ -1,10 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  "dependencies": {
 | 
					 | 
				
			||||||
    "bulma": "^1.0.4",
 | 
					 | 
				
			||||||
    "sass": "^1.89.2"
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "scripts": {
 | 
					 | 
				
			||||||
    "build-bulma": "sass --load-path=node_modules src/fellchensammlung/static/fellchensammlung/css/main.scss src/fellchensammlung/static/fellchensammlung/css/main.css --style compressed",
 | 
					 | 
				
			||||||
    "start": "npm run build-bulma -- --watch"
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -6,14 +6,14 @@ build-backend = "setuptools.build_meta"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[project]
 | 
					[project]
 | 
				
			||||||
name = "notfellchen"
 | 
					name = "notfellchen"
 | 
				
			||||||
description = "A website to help animals to find a loving home. It features organized input of adoption notices and related animals including automated lifecycle, location-based search, roles, and support for easy checking of rescue organizations."
 | 
					description = "A tool to help."
 | 
				
			||||||
authors = [
 | 
					authors = [
 | 
				
			||||||
    { name = "moanos", email = "julian-samuel@gebuehr.net" },
 | 
					    { name = "moanos", email = "julian-samuel@gebuehr.net" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
maintainers = [
 | 
					maintainers = [
 | 
				
			||||||
    { name = "moanos", email = "julian-samuel@gebuehr.net" },
 | 
					    { name = "moanos", email = "julian-samuel@gebuehr.net" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
keywords = ["animal", "adoption", "django", "rescue", "rats" ]
 | 
					keywords = ["animal", "adoption", "django", "rescue", ]
 | 
				
			||||||
license = { text = "AGPL-3.0-or-later" }
 | 
					license = { text = "AGPL-3.0-or-later" }
 | 
				
			||||||
classifiers = [
 | 
					classifiers = [
 | 
				
			||||||
    "Environment :: Web",
 | 
					    "Environment :: Web",
 | 
				
			||||||
@@ -25,6 +25,8 @@ classifiers = [
 | 
				
			|||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
    "Django",
 | 
					    "Django",
 | 
				
			||||||
    "codecov",
 | 
					    "codecov",
 | 
				
			||||||
 | 
					    "sphinx",
 | 
				
			||||||
 | 
					    "sphinx-rtd-theme",
 | 
				
			||||||
    "gunicorn",
 | 
					    "gunicorn",
 | 
				
			||||||
    "fontawesomefree",
 | 
					    "fontawesomefree",
 | 
				
			||||||
    "whitenoise",
 | 
					    "whitenoise",
 | 
				
			||||||
@@ -36,9 +38,7 @@ dependencies = [
 | 
				
			|||||||
    "crispy-bootstrap4",
 | 
					    "crispy-bootstrap4",
 | 
				
			||||||
    "djangorestframework",
 | 
					    "djangorestframework",
 | 
				
			||||||
    "celery[redis]",
 | 
					    "celery[redis]",
 | 
				
			||||||
    "drf-spectacular[sidecar]",
 | 
					    "drf-spectacular[sidecar]"
 | 
				
			||||||
    "django-widget-tweaks",
 | 
					 | 
				
			||||||
    "django-super-deduper"
 | 
					 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dynamic = ["version", "readme"]
 | 
					dynamic = ["version", "readme"]
 | 
				
			||||||
@@ -49,11 +49,6 @@ develop = [
 | 
				
			|||||||
    "coverage",
 | 
					    "coverage",
 | 
				
			||||||
    "model_bakery",
 | 
					    "model_bakery",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
docs = [
 | 
					 | 
				
			||||||
    "sphinx",
 | 
					 | 
				
			||||||
    "sphinx-rtd-theme",
 | 
					 | 
				
			||||||
    "sphinx-autobuild"
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
[project.urls]
 | 
					[project.urls]
 | 
				
			||||||
homepage = "https://notfellchen.org"
 | 
					homepage = "https://notfellchen.org"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,275 +0,0 @@
 | 
				
			|||||||
import argparse
 | 
					 | 
				
			||||||
import json
 | 
					 | 
				
			||||||
import logging
 | 
					 | 
				
			||||||
import os
 | 
					 | 
				
			||||||
from types import SimpleNamespace
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import requests
 | 
					 | 
				
			||||||
# TODO: consider using OSMPythonTools instead of requests or overpass library
 | 
					 | 
				
			||||||
from osmtogeojson import osmtogeojson
 | 
					 | 
				
			||||||
from tqdm import tqdm
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
DEFAULT_OSM_DATA_FILE = "export.geojson"
 | 
					 | 
				
			||||||
# Search area must be the official name, e.g. "Germany" is not a valid area name in Overpass API
 | 
					 | 
				
			||||||
# Consider instead finding & using the code within the query itself, e.g. "ISO3166-1"="DE"
 | 
					 | 
				
			||||||
DEFAULT_OVERPASS_SEARCH_AREA = "Deutschland"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def parse_args():
 | 
					 | 
				
			||||||
    """Parse command-line arguments."""
 | 
					 | 
				
			||||||
    parser = argparse.ArgumentParser(
 | 
					 | 
				
			||||||
        description="Download animal shelter data from the Overpass API to the Notfellchen API.")
 | 
					 | 
				
			||||||
    parser.add_argument("--api-token", type=str, help="API token for authentication.")
 | 
					 | 
				
			||||||
    parser.add_argument("--area", type=str, help="Area to search for animal shelters (default: Deutschland).")
 | 
					 | 
				
			||||||
    parser.add_argument("--instance", type=str, help="API instance URL.")
 | 
					 | 
				
			||||||
    parser.add_argument("--data-file", type=str, help="Path to the GeoJSON file containing (only) animal shelters.")
 | 
					 | 
				
			||||||
    parser.add_argument("--use-cached", action='store_true', help="Use the stored GeoJSON file")
 | 
					 | 
				
			||||||
    return parser.parse_args()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def get_config():
 | 
					 | 
				
			||||||
    """Get configuration from environment variables or command-line arguments."""
 | 
					 | 
				
			||||||
    args = parse_args()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    api_token = args.api_token or os.getenv("NOTFELLCHEN_API_TOKEN")
 | 
					 | 
				
			||||||
    # TODO: document new environment variable NOTFELLCHEN_AREA
 | 
					 | 
				
			||||||
    area = args.area or os.getenv("NOTFELLCHEN_AREA", DEFAULT_OVERPASS_SEARCH_AREA)
 | 
					 | 
				
			||||||
    instance = args.instance or os.getenv("NOTFELLCHEN_INSTANCE")
 | 
					 | 
				
			||||||
    data_file = args.data_file or os.getenv("NOTFELLCHEN_DATA_FILE", DEFAULT_OSM_DATA_FILE)
 | 
					 | 
				
			||||||
    use_cached = args.use_cached or os.getenv("NOTFELLCHEN_USE_CACHED", False)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if not api_token or not instance:
 | 
					 | 
				
			||||||
        raise ValueError("API token and instance URL must be provided via environment variables or CLI arguments.")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return api_token, area, instance, data_file, use_cached
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def get_or_none(data, key):
 | 
					 | 
				
			||||||
    if key in data["properties"].keys():
 | 
					 | 
				
			||||||
        return data["properties"][key]
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def get_or_empty(data, key):
 | 
					 | 
				
			||||||
    if key in data["properties"].keys():
 | 
					 | 
				
			||||||
        return data["properties"][key]
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        return ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def choose(keys, data, replace=False):
 | 
					 | 
				
			||||||
    for key in keys:
 | 
					 | 
				
			||||||
        if key in data.keys():
 | 
					 | 
				
			||||||
            if replace:
 | 
					 | 
				
			||||||
                return data[key].replace(" ", "").replace("-", "").replace("(", "").replace(")", "")
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                return data[key]
 | 
					 | 
				
			||||||
    return None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def add(value, platform):
 | 
					 | 
				
			||||||
    if value != "":
 | 
					 | 
				
			||||||
        if value.find(platform) == -1:
 | 
					 | 
				
			||||||
            return f"https://www.{platform}.com/{value}"
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            return value
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def https(value):
 | 
					 | 
				
			||||||
    if value is not None and value != "":
 | 
					 | 
				
			||||||
        value = value.replace("http://", "")
 | 
					 | 
				
			||||||
        if value.find("https") == -1:
 | 
					 | 
				
			||||||
            return f"https://{value}"
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            return value
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def calc_coordinate_center(coordinates):
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    Calculates the center as the arithmetic mean of the list of coordinates.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Not perfect because earth is a sphere (citation needed) but good enough.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    if not coordinates:
 | 
					 | 
				
			||||||
        return None, None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    lon_sum = 0.0
 | 
					 | 
				
			||||||
    lat_sum = 0.0
 | 
					 | 
				
			||||||
    count = 0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for lon, lat in coordinates:
 | 
					 | 
				
			||||||
        lon_sum += lon
 | 
					 | 
				
			||||||
        lat_sum += lat
 | 
					 | 
				
			||||||
        count += 1
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return lon_sum / count, lat_sum / count
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def get_center_coordinates(geometry):
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    Given a GeoJSON geometry dict, return (longitude, latitude)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    If a shape, calculate the center, else reurn the point
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    geom_type = geometry["type"]
 | 
					 | 
				
			||||||
    coordinates = geometry["coordinates"]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if geom_type == "Point":
 | 
					 | 
				
			||||||
        return coordinates[0], coordinates[1]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    elif geom_type == "LineString":
 | 
					 | 
				
			||||||
        return calc_coordinate_center(coordinates)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    elif geom_type == "Polygon":
 | 
					 | 
				
			||||||
        outer_ring = coordinates[0]
 | 
					 | 
				
			||||||
        return calc_coordinate_center(outer_ring)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        raise ValueError(f"Unsupported geometry type: {geom_type}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# TODO: take note of new get_overpass_result function which does the bulk of the new overpass query work
 | 
					 | 
				
			||||||
def get_overpass_result(area, data_file):
 | 
					 | 
				
			||||||
    """Build the Overpass query for fetching animal shelters in the specified area."""
 | 
					 | 
				
			||||||
    overpass_endpoint = "https://overpass-api.de/api/interpreter"
 | 
					 | 
				
			||||||
    overpass_query = f"""
 | 
					 | 
				
			||||||
        [out:json][timeout:25];
 | 
					 | 
				
			||||||
        area[name="{area}"]->.searchArea;
 | 
					 | 
				
			||||||
        nwr["amenity"="animal_shelter"](area.searchArea);
 | 
					 | 
				
			||||||
        out body;
 | 
					 | 
				
			||||||
        >;
 | 
					 | 
				
			||||||
        out skel qt;
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
    r = requests.get(overpass_endpoint, params={'data': overpass_query})
 | 
					 | 
				
			||||||
    if r.status_code == 200:
 | 
					 | 
				
			||||||
        rjson = r.json()
 | 
					 | 
				
			||||||
        result = osmtogeojson.process_osm_json(rjson)
 | 
					 | 
				
			||||||
        with open(data_file, 'w', encoding='utf-8') as f:
 | 
					 | 
				
			||||||
            json.dump(result, f, ensure_ascii=False)
 | 
					 | 
				
			||||||
        return result
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def add_if_available(base_data, keys, result):
 | 
					 | 
				
			||||||
    # Loads the data into the org if available
 | 
					 | 
				
			||||||
    for key in keys:
 | 
					 | 
				
			||||||
        if getattr(base_data, key) is not None:
 | 
					 | 
				
			||||||
            result[key] = getattr(base_data, key)
 | 
					 | 
				
			||||||
    return result
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def create_location(tierheim, instance, headers):
 | 
					 | 
				
			||||||
    location_data = {
 | 
					 | 
				
			||||||
        "place_id": tierheim["id"],
 | 
					 | 
				
			||||||
        "longitude": get_center_coordinates(tierheim["geometry"])[0],
 | 
					 | 
				
			||||||
        "latitude": get_center_coordinates(tierheim["geometry"])[1],
 | 
					 | 
				
			||||||
        "name": tierheim["properties"]["name"],
 | 
					 | 
				
			||||||
        "city": tierheim["properties"]["addr:city"],
 | 
					 | 
				
			||||||
        "housenumber": get_or_empty(tierheim, "addr:housenumber"),
 | 
					 | 
				
			||||||
        "postcode": get_or_empty(tierheim, "addr:postcode"),
 | 
					 | 
				
			||||||
        "street": get_or_empty(tierheim, "addr:street"),
 | 
					 | 
				
			||||||
        "countrycode": get_or_empty(tierheim, "addr:country"),
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    location_result = requests.post(f"{instance}/api/locations/", json=location_data, headers=headers)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if location_result.status_code != 201:
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            print(
 | 
					 | 
				
			||||||
                f"Location for {tierheim["properties"]["name"]}:{location_result.status_code} {location_result.json()} not created")
 | 
					 | 
				
			||||||
        except requests.exceptions.JSONDecodeError:
 | 
					 | 
				
			||||||
            print(f"Location for {tierheim["properties"]["name"]} could not be created")
 | 
					 | 
				
			||||||
            exit()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return location_result.json()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def main():
 | 
					 | 
				
			||||||
    api_token, area, instance, data_file, use_cached = get_config()
 | 
					 | 
				
			||||||
    if not use_cached:
 | 
					 | 
				
			||||||
        # Query shelters
 | 
					 | 
				
			||||||
        overpass_result = get_overpass_result(area, data_file)
 | 
					 | 
				
			||||||
        if overpass_result is None:
 | 
					 | 
				
			||||||
            print("Error: get_overpass_result returned None")
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        with open(data_file, 'r', encoding='utf-8') as f:
 | 
					 | 
				
			||||||
            overpass_result = json.load(f)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Set headers and endpoint
 | 
					 | 
				
			||||||
    endpoint = f"{instance}/api/organizations/"
 | 
					 | 
				
			||||||
    h = {'Authorization': f'Token {api_token}', "content-type": "application/json"}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    tierheime = overpass_result["features"]
 | 
					 | 
				
			||||||
    stats = {"num_updated_orgs": 0,
 | 
					 | 
				
			||||||
             "num_inserted_orgs": 0}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for idx, tierheim in enumerate(tqdm(tierheime)):
 | 
					 | 
				
			||||||
        # Check if data is low quality
 | 
					 | 
				
			||||||
        if "name" not in tierheim["properties"].keys() or "addr:city" not in tierheim["properties"].keys():
 | 
					 | 
				
			||||||
            continue
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Load TH data in for easier accessing
 | 
					 | 
				
			||||||
        th_data = SimpleNamespace(
 | 
					 | 
				
			||||||
            name=tierheim["properties"]["name"],
 | 
					 | 
				
			||||||
            email=choose(("contact:email", "email"), tierheim["properties"]),
 | 
					 | 
				
			||||||
            phone_number=choose(("contact:phone", "phone"), tierheim["properties"], replace=True),
 | 
					 | 
				
			||||||
            fediverse_profile=get_or_none(tierheim, "contact:mastodon"),
 | 
					 | 
				
			||||||
            facebook=https(add(get_or_empty(tierheim, "contact:facebook"), "facebook")),
 | 
					 | 
				
			||||||
            instagram=https(add(get_or_empty(tierheim, "contact:instagram"), "instagram")),
 | 
					 | 
				
			||||||
            website=https(choose(("contact:website", "website"), tierheim["properties"])),
 | 
					 | 
				
			||||||
            description=get_or_none(tierheim, "opening_hours"),
 | 
					 | 
				
			||||||
            external_object_identifier=tierheim["id"],
 | 
					 | 
				
			||||||
            EXTERNAL_SOURCE_IDENTIFIER="OSM",
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Define here for later
 | 
					 | 
				
			||||||
        optional_data = ["email", "phone_number", "website", "description", "fediverse_profile", "facebook",
 | 
					 | 
				
			||||||
                         "instagram"]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Check if rescue organization exists
 | 
					 | 
				
			||||||
        search_data = {"external_source_identifier": "OSM",
 | 
					 | 
				
			||||||
                       "external_object_identifier": f"{tierheim["id"]}"}
 | 
					 | 
				
			||||||
        search_result = requests.get(f"{instance}/api/organizations", params=search_data, headers=h)
 | 
					 | 
				
			||||||
        # Rescue organization exits
 | 
					 | 
				
			||||||
        if search_result.status_code == 200:
 | 
					 | 
				
			||||||
            stats["num_updated_orgs"] += 1
 | 
					 | 
				
			||||||
            org_id = search_result.json()[0]["id"]
 | 
					 | 
				
			||||||
            logging.debug(f"{th_data.name} already exists as ID {org_id}.")
 | 
					 | 
				
			||||||
            org_patch_data = {"id": org_id,
 | 
					 | 
				
			||||||
                              "name": th_data.name}
 | 
					 | 
				
			||||||
            if search_result.json()[0]["location"] is None:
 | 
					 | 
				
			||||||
                location = create_location(tierheim, instance, h)
 | 
					 | 
				
			||||||
                org_patch_data["location"] = location["id"]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            org_patch_data = add_if_available(th_data, optional_data, org_patch_data)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            result = requests.patch(endpoint, json=org_patch_data, headers=h)
 | 
					 | 
				
			||||||
            if result.status_code != 200:
 | 
					 | 
				
			||||||
                logging.warning(f"Updating {tierheim['properties']['name']} failed:{result.status_code} {result.json()}")
 | 
					 | 
				
			||||||
            continue
 | 
					 | 
				
			||||||
        # Rescue organization does not exist
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            stats["num_inserted_orgs"] += 1
 | 
					 | 
				
			||||||
            location = create_location(tierheim, instance, h)
 | 
					 | 
				
			||||||
            org_data = {"name": tierheim["properties"]["name"],
 | 
					 | 
				
			||||||
                        "external_object_identifier": f"{tierheim["id"]}",
 | 
					 | 
				
			||||||
                        "external_source_identifier": "OSM",
 | 
					 | 
				
			||||||
                        "location": location["id"]
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            org_data = add_if_available(th_data, optional_data, org_data)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            result = requests.post(endpoint, json=org_data, headers=h)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if result.status_code != 201:
 | 
					 | 
				
			||||||
                print(f"{idx} {tierheim["properties"]["name"]}:{result.status_code} {result.json()}")
 | 
					 | 
				
			||||||
    print(f"Upload finished. Inserted {stats['num_inserted_orgs']} new orgs and updated {stats['num_updated_orgs']} orgs.")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
if __name__ == "__main__":
 | 
					 | 
				
			||||||
    main()
 | 
					 | 
				
			||||||
@@ -1,32 +1,35 @@
 | 
				
			|||||||
import csv
 | 
					import csv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.contrib import admin
 | 
					from django.contrib import admin
 | 
				
			||||||
from django.contrib.admin import EmptyFieldListFilter
 | 
					 | 
				
			||||||
from django.http import HttpResponse
 | 
					from django.http import HttpResponse
 | 
				
			||||||
from django.utils.html import format_html
 | 
					from django.utils.html import format_html
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
from django.utils.http import urlencode
 | 
					from django.utils.http import urlencode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
 | 
					from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
 | 
				
			||||||
    SpeciesSpecificURL, ImportantLocation, SocialMediaPost
 | 
					    SpeciesSpecificURL
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
 | 
					from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
 | 
				
			||||||
    Comment, Announcement, User, Subscriptions, Notification
 | 
					    Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, BaseNotification
 | 
				
			||||||
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.adoption_notice_status = AdoptionNoticeStatusChoices.Active.SEARCHING
 | 
					            obj.set_active()
 | 
				
			||||||
            obj.save()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    activate.short_description = _("Ausgewählte Vermittlungen aktivieren")
 | 
					    activate.short_description = _("Ausgewählte Vermittlungen aktivieren")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -91,16 +94,14 @@ class ReportAdoptionNoticeAdmin(admin.ModelAdmin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    reported_content_link.short_description = "Reported Content"
 | 
					    reported_content_link.short_description = "Reported Content"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
class SpeciesSpecificURLInline(admin.StackedInline):
 | 
					class SpeciesSpecificURLInline(admin.StackedInline):
 | 
				
			||||||
    model = SpeciesSpecificURL
 | 
					    model = SpeciesSpecificURL
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
@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")
 | 
				
			||||||
    list_display = ("name", "trusted", "allows_using_materials", "website")
 | 
					    list_display = ("name", "trusted", "allows_using_materials", "website")
 | 
				
			||||||
    list_filter = ("allows_using_materials", "trusted", ("external_source_identifier", EmptyFieldListFilter))
 | 
					    list_filter = ("allows_using_materials", "trusted",)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    inlines = [
 | 
					    inlines = [
 | 
				
			||||||
        SpeciesSpecificURLInline,
 | 
					        SpeciesSpecificURLInline,
 | 
				
			||||||
@@ -117,60 +118,24 @@ class CommentAdmin(admin.ModelAdmin):
 | 
				
			|||||||
    list_filter = ("user",)
 | 
					    list_filter = ("user",)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@admin.register(Notification)
 | 
					@admin.register(BaseNotification)
 | 
				
			||||||
class BaseNotificationAdmin(admin.ModelAdmin):
 | 
					class BaseNotificationAdmin(admin.ModelAdmin):
 | 
				
			||||||
    list_filter = ("user_to_notify", "read")
 | 
					    list_filter = ("user", "read")
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@admin.register(SearchSubscription)
 | 
					@admin.register(SearchSubscription)
 | 
				
			||||||
class SearchSubscriptionAdmin(admin.ModelAdmin):
 | 
					class SearchSubscriptionAdmin(admin.ModelAdmin):
 | 
				
			||||||
    list_filter = ("owner",)
 | 
					    list_filter = ("owner",)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ImportantLocationInline(admin.StackedInline):
 | 
					 | 
				
			||||||
    model = ImportantLocation
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class IsImportantListFilter(admin.SimpleListFilter):
 | 
					 | 
				
			||||||
    # See https://docs.djangoproject.com/en/5.1/ref/contrib/admin/filters/#modeladmin-list-filters
 | 
					 | 
				
			||||||
    title = _('Is Important Location?')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    parameter_name = 'important'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def lookups(self, request, model_admin):
 | 
					 | 
				
			||||||
        return (
 | 
					 | 
				
			||||||
            ('is_important', _('Important Location')),
 | 
					 | 
				
			||||||
            ('is_normal', _('Normal Location')),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def queryset(self, request, queryset):
 | 
					 | 
				
			||||||
        if self.value() == 'is_important':
 | 
					 | 
				
			||||||
            return queryset.filter(importantlocation__isnull=False)
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            return queryset.filter(importantlocation__isnull=True)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@admin.register(Location)
 | 
					 | 
				
			||||||
class LocationAdmin(admin.ModelAdmin):
 | 
					 | 
				
			||||||
    search_fields = ("name__icontains", "city__icontains")
 | 
					 | 
				
			||||||
    list_filter = [IsImportantListFilter]
 | 
					 | 
				
			||||||
    inlines = [
 | 
					 | 
				
			||||||
        ImportantLocationInline,
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@admin.register(SocialMediaPost)
 | 
					 | 
				
			||||||
class SocialMediaPostAdmin(admin.ModelAdmin):
 | 
					 | 
				
			||||||
    list_filter = ("platform",)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
admin.site.register(Animal)
 | 
					admin.site.register(Animal)
 | 
				
			||||||
admin.site.register(Species)
 | 
					admin.site.register(Species)
 | 
				
			||||||
 | 
					admin.site.register(Location)
 | 
				
			||||||
admin.site.register(Rule)
 | 
					admin.site.register(Rule)
 | 
				
			||||||
admin.site.register(Image)
 | 
					admin.site.register(Image)
 | 
				
			||||||
admin.site.register(ModerationAction)
 | 
					admin.site.register(ModerationAction)
 | 
				
			||||||
admin.site.register(Language)
 | 
					admin.site.register(Language)
 | 
				
			||||||
admin.site.register(Announcement)
 | 
					admin.site.register(Announcement)
 | 
				
			||||||
 | 
					admin.site.register(AdoptionNoticeStatus)
 | 
				
			||||||
admin.site.register(Subscriptions)
 | 
					admin.site.register(Subscriptions)
 | 
				
			||||||
admin.site.register(Log)
 | 
					admin.site.register(Log)
 | 
				
			||||||
admin.site.register(Timestamp)
 | 
					admin.site.register(Timestamp)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,33 +0,0 @@
 | 
				
			|||||||
from rest_framework.renderers import BaseRenderer
 | 
					 | 
				
			||||||
import json
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class GeoJSONRenderer(BaseRenderer):
 | 
					 | 
				
			||||||
    media_type = 'application/json'
 | 
					 | 
				
			||||||
    format = 'geojson'
 | 
					 | 
				
			||||||
    charset = 'utf-8'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def render(self, data, accepted_media_type=None, renderer_context=None):
 | 
					 | 
				
			||||||
        features = []
 | 
					 | 
				
			||||||
        for item in data:
 | 
					 | 
				
			||||||
            coords = item["coordinates"]
 | 
					 | 
				
			||||||
            if coords:
 | 
					 | 
				
			||||||
                feature = {
 | 
					 | 
				
			||||||
                    "type": "Feature",
 | 
					 | 
				
			||||||
                    "geometry": {
 | 
					 | 
				
			||||||
                        "type": "Point",
 | 
					 | 
				
			||||||
                        "coordinates": coords
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    "properties": {
 | 
					 | 
				
			||||||
                        k: v for k, v in item.items()
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    "id": f"{item['id']}"
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                features.append(feature)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        geojson = {
 | 
					 | 
				
			||||||
            "type": "FeatureCollection",
 | 
					 | 
				
			||||||
            "generator": "notfellchen",
 | 
					 | 
				
			||||||
            "features": features
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return json.dumps(geojson)
 | 
					 | 
				
			||||||
@@ -1,135 +1,12 @@
 | 
				
			|||||||
from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image, Location
 | 
					from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image
 | 
				
			||||||
from rest_framework import serializers
 | 
					from rest_framework import serializers
 | 
				
			||||||
import math
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ImageSerializer(serializers.ModelSerializer):
 | 
					 | 
				
			||||||
    width = serializers.SerializerMethodField()
 | 
					 | 
				
			||||||
    height = serializers.SerializerMethodField()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    class Meta:
 | 
					 | 
				
			||||||
        model = Image
 | 
					 | 
				
			||||||
        fields = ['id', 'image', 'alt_text', 'width', 'height']
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_width(self, obj):
 | 
					 | 
				
			||||||
        return obj.image.width
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_height(self, obj):
 | 
					 | 
				
			||||||
        return obj.image.height
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
 | 
					class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
 | 
				
			||||||
    location = serializers.PrimaryKeyRelatedField(
 | 
					 | 
				
			||||||
        queryset=Location.objects.all(),
 | 
					 | 
				
			||||||
        required=False,
 | 
					 | 
				
			||||||
        allow_null=True
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    location_details = serializers.StringRelatedField(source='location', read_only=True)
 | 
					 | 
				
			||||||
    organization = serializers.PrimaryKeyRelatedField(
 | 
					 | 
				
			||||||
        queryset=RescueOrganization.objects.all(),
 | 
					 | 
				
			||||||
        required=False,
 | 
					 | 
				
			||||||
        allow_null=True
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    organization = serializers.PrimaryKeyRelatedField(
 | 
					 | 
				
			||||||
        queryset=RescueOrganization.objects.all(),
 | 
					 | 
				
			||||||
        required=False,
 | 
					 | 
				
			||||||
        allow_null=True
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    url = serializers.SerializerMethodField()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    photos = ImageSerializer(many=True, read_only=True)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_url(self, obj):
 | 
					 | 
				
			||||||
        return obj.get_full_url()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = AdoptionNotice
 | 
					        model = AdoptionNotice
 | 
				
			||||||
        fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information",
 | 
					        fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information",
 | 
				
			||||||
                  "group_only", "location", "location_details", "organization", "photos", "adoption_notice_status",
 | 
					                  "group_only"]
 | 
				
			||||||
                  "url"]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class AdoptionNoticeGeoJSONSerializer(serializers.ModelSerializer):
 | 
					 | 
				
			||||||
    species = serializers.SerializerMethodField()
 | 
					 | 
				
			||||||
    title = serializers.CharField(source='name')
 | 
					 | 
				
			||||||
    url = serializers.SerializerMethodField()
 | 
					 | 
				
			||||||
    location_hr = serializers.SerializerMethodField()
 | 
					 | 
				
			||||||
    coordinates = serializers.SerializerMethodField()
 | 
					 | 
				
			||||||
    image_url = serializers.SerializerMethodField()
 | 
					 | 
				
			||||||
    image_alt = serializers.SerializerMethodField()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    class Meta:
 | 
					 | 
				
			||||||
        model = AdoptionNotice
 | 
					 | 
				
			||||||
        fields = ('id', 'species', 'title', 'description', 'url', 'location_hr', 'coordinates', 'image_url',
 | 
					 | 
				
			||||||
                  'image_alt')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_species(self, obj):
 | 
					 | 
				
			||||||
        return "rat"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_url(self, obj):
 | 
					 | 
				
			||||||
        return obj.get_absolute_url()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_image_url(self, obj):
 | 
					 | 
				
			||||||
        photo = obj.get_photo()
 | 
					 | 
				
			||||||
        if photo is not None:
 | 
					 | 
				
			||||||
            return obj.get_photo().image.url
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_image_alt(self, obj):
 | 
					 | 
				
			||||||
        photo = obj.get_photo()
 | 
					 | 
				
			||||||
        if photo is not None:
 | 
					 | 
				
			||||||
            return obj.get_photo().alt_text
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_coordinates(self, obj):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Coordinates are randomly moved around real location, roughly in a circle. The object id is used as angle so that
 | 
					 | 
				
			||||||
        points are always displayed at the same location (as if they were a seed for a random function).
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        It's not exactly a circle, because the earth is round.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if obj.location:
 | 
					 | 
				
			||||||
            longitude_addition = math.sin(obj.id) / 2000
 | 
					 | 
				
			||||||
            latitude_addition = math.cos(obj.id) / 2000
 | 
					 | 
				
			||||||
            return [obj.location.longitude + longitude_addition, obj.location.latitude + latitude_addition]
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_location_hr(self, obj):
 | 
					 | 
				
			||||||
        if obj.location:
 | 
					 | 
				
			||||||
            return f"{obj.location}"
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class RescueOrgeGeoJSONSerializer(serializers.ModelSerializer):
 | 
					 | 
				
			||||||
    name = serializers.CharField()
 | 
					 | 
				
			||||||
    url = serializers.SerializerMethodField()
 | 
					 | 
				
			||||||
    location_hr = serializers.SerializerMethodField()
 | 
					 | 
				
			||||||
    coordinates = serializers.SerializerMethodField()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    class Meta:
 | 
					 | 
				
			||||||
        model = AdoptionNotice
 | 
					 | 
				
			||||||
        fields = ('id', 'name', 'description', 'url', 'location_hr', 'coordinates')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_url(self, obj):
 | 
					 | 
				
			||||||
        return obj.get_absolute_url()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_coordinates(self, obj):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Coordinates are randomly moved around real location, roughly in a circle. The object id is used as angle so that
 | 
					 | 
				
			||||||
        points are always displayed at the same location (as if they were a seed for a random function).
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        It's not exactly a circle, because the earth is round.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if obj.location:
 | 
					 | 
				
			||||||
            return [obj.location.longitude, obj.location.latitude]
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_location_hr(self, obj):
 | 
					 | 
				
			||||||
        if obj.location.city:
 | 
					 | 
				
			||||||
            return f"{obj.location.city}"
 | 
					 | 
				
			||||||
        elif obj.location:
 | 
					 | 
				
			||||||
            return f"{obj.location}"
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AnimalCreateSerializer(serializers.ModelSerializer):
 | 
					class AnimalCreateSerializer(serializers.ModelSerializer):
 | 
				
			||||||
@@ -137,6 +14,11 @@ class AnimalCreateSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
        model = Animal
 | 
					        model = Animal
 | 
				
			||||||
        fields = ["name", "date_of_birth", "description", "species", "sex", "adoption_notice"]
 | 
					        fields = ["name", "date_of_birth", "description", "species", "sex", "adoption_notice"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RescueOrgSerializer(serializers.ModelSerializer):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = RescueOrganization
 | 
				
			||||||
 | 
					        fields = ["name", "location_string", "instagram", "facebook", "fediverse_profile", "email", "phone_number",
 | 
				
			||||||
 | 
					                  "website", "description", "external_object_identifier", "external_source_identifier"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AnimalGetSerializer(serializers.ModelSerializer):
 | 
					class AnimalGetSerializer(serializers.ModelSerializer):
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
@@ -169,9 +51,3 @@ class SpeciesSerializer(serializers.ModelSerializer):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Species
 | 
					        model = Species
 | 
				
			||||||
        fields = "__all__"
 | 
					        fields = "__all__"
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class LocationSerializer(serializers.ModelSerializer):
 | 
					 | 
				
			||||||
    class Meta:
 | 
					 | 
				
			||||||
        model = Location
 | 
					 | 
				
			||||||
        fields = "__all__"
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,21 +1,16 @@
 | 
				
			|||||||
from django.urls import path
 | 
					from django.urls import path
 | 
				
			||||||
from .views import (
 | 
					from .views import (
 | 
				
			||||||
    AdoptionNoticeApiView,
 | 
					    AdoptionNoticeApiView,
 | 
				
			||||||
    AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView, LocationApiView,
 | 
					    AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView
 | 
				
			||||||
    AdoptionNoticeGeoJSONView, RescueOrgGeoJSONView, AdoptionNoticePerOrgApiView
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
urlpatterns = [
 | 
					urlpatterns = [
 | 
				
			||||||
    path("adoption_notice", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-list"),
 | 
					    path("adoption_notice", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-list"),
 | 
				
			||||||
    path("adoption_notice.geojson", AdoptionNoticeGeoJSONView.as_view(), name="api-adoption-notice-list-geojson"),
 | 
					 | 
				
			||||||
    path("adoption_notice/<int:id>/", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-detail"),
 | 
					    path("adoption_notice/<int:id>/", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-detail"),
 | 
				
			||||||
    path("animals/", AnimalApiView.as_view(), name="api-animal-list"),
 | 
					    path("animals/", AnimalApiView.as_view(), name="api-animal-list"),
 | 
				
			||||||
    path("animals/<int:id>/", AnimalApiView.as_view(), name="api-animal-detail"),
 | 
					    path("animals/<int:id>/", AnimalApiView.as_view(), name="api-animal-detail"),
 | 
				
			||||||
    path("organizations/", RescueOrganizationApiView.as_view(), name="api-organization-list"),
 | 
					    path("organizations/", RescueOrganizationApiView.as_view(), name="api-organization-list"),
 | 
				
			||||||
    path("organizations.geojson", RescueOrgGeoJSONView.as_view(), name="api-organization-list-geojson"),
 | 
					 | 
				
			||||||
    path("organizations/<int:id>/", RescueOrganizationApiView.as_view(), name="api-organization-detail"),
 | 
					    path("organizations/<int:id>/", RescueOrganizationApiView.as_view(), name="api-organization-detail"),
 | 
				
			||||||
    path("organizations/<int:id>/adoption-notices", AdoptionNoticePerOrgApiView.as_view(), name="api-organization-adoption-notices"),
 | 
					 | 
				
			||||||
    path("images/", AddImageApiView.as_view(), name="api-add-image"),
 | 
					    path("images/", AddImageApiView.as_view(), name="api-add-image"),
 | 
				
			||||||
    path("species/", SpeciesApiView.as_view(), name="api-species-list"),
 | 
					    path("species/", SpeciesApiView.as_view(), name="api-species-list"),
 | 
				
			||||||
    path("locations/", LocationApiView.as_view(), name="api-locations-list"),
 | 
					 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,28 +1,20 @@
 | 
				
			|||||||
from django.db.models import Q
 | 
					 | 
				
			||||||
from drf_spectacular.types import OpenApiTypes
 | 
					 | 
				
			||||||
from rest_framework.generics import ListAPIView
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from fellchensammlung.api.serializers import LocationSerializer, AdoptionNoticeGeoJSONSerializer
 | 
					 | 
				
			||||||
from rest_framework.views import APIView
 | 
					from rest_framework.views import APIView
 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
from django.db import transaction
 | 
					from django.db import transaction
 | 
				
			||||||
from fellchensammlung.models import Log, TrustLevel, Location, AdoptionNoticeStatusChoices
 | 
					from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel
 | 
				
			||||||
from fellchensammlung.tasks import post_adoption_notice_save, post_rescue_org_save
 | 
					from fellchensammlung.tasks import post_adoption_notice_save
 | 
				
			||||||
from rest_framework import status, serializers
 | 
					from rest_framework import status
 | 
				
			||||||
from rest_framework.permissions import IsAuthenticated
 | 
					from rest_framework.permissions import IsAuthenticated
 | 
				
			||||||
 | 
					 | 
				
			||||||
from .renderers import GeoJSONRenderer
 | 
					 | 
				
			||||||
from .serializers import (
 | 
					from .serializers import (
 | 
				
			||||||
    AnimalGetSerializer,
 | 
					    AnimalGetSerializer,
 | 
				
			||||||
    AnimalCreateSerializer,
 | 
					    AnimalCreateSerializer,
 | 
				
			||||||
    RescueOrgeGeoJSONSerializer,
 | 
					    RescueOrganizationSerializer,
 | 
				
			||||||
    AdoptionNoticeSerializer,
 | 
					    AdoptionNoticeSerializer,
 | 
				
			||||||
    ImageCreateSerializer,
 | 
					    ImageCreateSerializer,
 | 
				
			||||||
    SpeciesSerializer, RescueOrganizationSerializer,
 | 
					    SpeciesSerializer, RescueOrgSerializer,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image
 | 
					from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image
 | 
				
			||||||
from drf_spectacular.utils import extend_schema, inline_serializer, OpenApiParameter
 | 
					from drf_spectacular.utils import extend_schema
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AdoptionNoticeApiView(APIView):
 | 
					class AdoptionNoticeApiView(APIView):
 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
@@ -67,22 +59,22 @@ class AdoptionNoticeApiView(APIView):
 | 
				
			|||||||
        if not serializer.is_valid():
 | 
					        if not serializer.is_valid():
 | 
				
			||||||
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 | 
					            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        adoption_notice = serializer.save(owner=request.user_to_notify)
 | 
					        adoption_notice = serializer.save(owner=request.user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Add the location
 | 
					        # Add the location
 | 
				
			||||||
        post_adoption_notice_save.delay_on_commit(adoption_notice.pk)
 | 
					        post_adoption_notice_save.delay_on_commit(adoption_notice.pk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Only set active when user has trust level moderator or higher
 | 
					        # Only set active when user has trust level moderator or higher
 | 
				
			||||||
        if request.user_to_notify.trust_level >= TrustLevel.MODERATOR:
 | 
					        if request.user.trust_level >= TrustLevel.MODERATOR:
 | 
				
			||||||
            adoption_notice.adoption_notice_status = AdoptionNoticeStatusChoices.Active.SEARCHING
 | 
					            adoption_notice.set_active()
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            adoption_notice.adoption_notice_status = AdoptionNoticeStatusChoices.AwaitingAction.WAITING_FOR_REVIEW
 | 
					            adoption_notice.set_unchecked()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Log the action
 | 
					        # Log the action
 | 
				
			||||||
        Log.objects.create(
 | 
					        Log.objects.create(
 | 
				
			||||||
            user=request.user_to_notify,
 | 
					            user=request.user,
 | 
				
			||||||
            action="add_adoption_notice",
 | 
					            action="add_adoption_notice",
 | 
				
			||||||
            text=f"{request.user_to_notify} added adoption notice {adoption_notice.pk} via API",
 | 
					            text=f"{request.user} added adoption notice {adoption_notice.pk} via API",
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Return success response with new adoption notice details
 | 
					        # Return success response with new adoption notice details
 | 
				
			||||||
@@ -92,12 +84,10 @@ class AdoptionNoticeApiView(APIView):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AnimalApiView(APIView):
 | 
					class AnimalApiView(APIView):
 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @extend_schema(
 | 
					 | 
				
			||||||
        responses=AnimalGetSerializer
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    def get(self, request, *args, **kwargs):
 | 
					    def get(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Get list of animals or a specific animal by ID.
 | 
					        Get list of animals or a specific animal by ID.
 | 
				
			||||||
@@ -114,16 +104,6 @@ class AnimalApiView(APIView):
 | 
				
			|||||||
        serializer = AnimalGetSerializer(animals, many=True, context={"request": request})
 | 
					        serializer = AnimalGetSerializer(animals, many=True, context={"request": request})
 | 
				
			||||||
        return Response(serializer.data, status=status.HTTP_200_OK)
 | 
					        return Response(serializer.data, status=status.HTTP_200_OK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @transaction.atomic
 | 
					 | 
				
			||||||
    @extend_schema(
 | 
					 | 
				
			||||||
        request=AnimalCreateSerializer,
 | 
					 | 
				
			||||||
        responses={201: inline_serializer(
 | 
					 | 
				
			||||||
            name='Animal',
 | 
					 | 
				
			||||||
            fields={
 | 
					 | 
				
			||||||
                'id': serializers.IntegerField(),
 | 
					 | 
				
			||||||
                "message": serializers.Field()}),
 | 
					 | 
				
			||||||
            400: "json"}
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    @transaction.atomic
 | 
					    @transaction.atomic
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					    def post(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -131,14 +111,13 @@ class AnimalApiView(APIView):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        serializer = AnimalCreateSerializer(data=request.data, context={"request": request})
 | 
					        serializer = AnimalCreateSerializer(data=request.data, context={"request": request})
 | 
				
			||||||
        if serializer.is_valid():
 | 
					        if serializer.is_valid():
 | 
				
			||||||
            animal = serializer.save(owner=request.user_to_notify)
 | 
					            animal = serializer.save(owner=request.user)
 | 
				
			||||||
            return Response(
 | 
					            return Response(
 | 
				
			||||||
                {"message": "Animal created successfully!", "id": animal.id},
 | 
					                {"message": "Animal created successfully!", "id": animal.id},
 | 
				
			||||||
                status=status.HTTP_201_CREATED,
 | 
					                status=status.HTTP_201_CREATED,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 | 
					        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
class RescueOrganizationApiView(APIView):
 | 
					class RescueOrganizationApiView(APIView):
 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -150,44 +129,14 @@ class RescueOrganizationApiView(APIView):
 | 
				
			|||||||
                'description': 'ID of the rescue organization to retrieve.',
 | 
					                'description': 'ID of the rescue organization to retrieve.',
 | 
				
			||||||
                'type': int
 | 
					                'type': int
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                'name': 'trusted',
 | 
					 | 
				
			||||||
                'required': False,
 | 
					 | 
				
			||||||
                'description': 'Filter by trusted status (true/false).',
 | 
					 | 
				
			||||||
                'type': bool
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                'name': 'external_object_identifier',
 | 
					 | 
				
			||||||
                'required': False,
 | 
					 | 
				
			||||||
                'description': 'Filter by external object identifier. Use "None" to filter for an empty field',
 | 
					 | 
				
			||||||
                'type': str
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                'name': 'external_source_identifier',
 | 
					 | 
				
			||||||
                'required': False,
 | 
					 | 
				
			||||||
                'description': 'Filter by external source identifier. Use "None" to filter for an empty field',
 | 
					 | 
				
			||||||
                'type': str
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                'name': 'search',
 | 
					 | 
				
			||||||
                'required': False,
 | 
					 | 
				
			||||||
                'description': 'Search by organization name or location name/city.',
 | 
					 | 
				
			||||||
                'type': str
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        responses={200: RescueOrganizationSerializer(many=True)}
 | 
					        responses={200: RescueOrganizationSerializer(many=True)}
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    def get(self, request, *args, **kwargs):
 | 
					    def get(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Get list of rescue organizations or a specific organization by ID or get a list with available filters for
 | 
					        Get list of rescue organizations or a specific organization by ID.
 | 
				
			||||||
        - external_object_identifier
 | 
					 | 
				
			||||||
        - external_source_identifier
 | 
					 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        org_id = request.query_params.get("id")
 | 
					        org_id = kwargs.get("id")
 | 
				
			||||||
        external_object_identifier = request.query_params.get("external_object_identifier")
 | 
					 | 
				
			||||||
        external_source_identifier = request.query_params.get("external_source_identifier")
 | 
					 | 
				
			||||||
        search_query = request.query_params.get("search")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if org_id:
 | 
					        if org_id:
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                organization = RescueOrganization.objects.get(pk=org_id)
 | 
					                organization = RescueOrganization.objects.get(pk=org_id)
 | 
				
			||||||
@@ -195,79 +144,28 @@ class RescueOrganizationApiView(APIView):
 | 
				
			|||||||
                return Response(serializer.data, status=status.HTTP_200_OK)
 | 
					                return Response(serializer.data, status=status.HTTP_200_OK)
 | 
				
			||||||
            except RescueOrganization.DoesNotExist:
 | 
					            except RescueOrganization.DoesNotExist:
 | 
				
			||||||
                return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
 | 
					                return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        organizations = RescueOrganization.objects.all()
 | 
					        organizations = RescueOrganization.objects.all()
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if external_object_identifier:
 | 
					 | 
				
			||||||
            if external_object_identifier == "None":
 | 
					 | 
				
			||||||
                external_object_identifier = None
 | 
					 | 
				
			||||||
            organizations = organizations.filter(external_object_identifier=external_object_identifier)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if external_source_identifier:
 | 
					 | 
				
			||||||
            if external_source_identifier == "None":
 | 
					 | 
				
			||||||
                external_source_identifier = None
 | 
					 | 
				
			||||||
            organizations = organizations.filter(external_source_identifier=external_source_identifier)
 | 
					 | 
				
			||||||
        if search_query:
 | 
					 | 
				
			||||||
            organizations = organizations.filter(
 | 
					 | 
				
			||||||
                Q(name__icontains=search_query) |
 | 
					 | 
				
			||||||
                Q(location_string__icontains=search_query) |
 | 
					 | 
				
			||||||
                Q(location__name__icontains=search_query) |
 | 
					 | 
				
			||||||
                Q(location__city__icontains=search_query)
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        if organizations.count() == 0:
 | 
					 | 
				
			||||||
            return Response({"error": "No organizations found."}, status=status.HTTP_404_NOT_FOUND)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        serializer = RescueOrganizationSerializer(organizations, many=True, context={"request": request})
 | 
					        serializer = RescueOrganizationSerializer(organizations, many=True, context={"request": request})
 | 
				
			||||||
        return Response(serializer.data, status=status.HTTP_200_OK)
 | 
					        return Response(serializer.data, status=status.HTTP_200_OK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @transaction.atomic
 | 
					    @transaction.atomic
 | 
				
			||||||
    @extend_schema(
 | 
					    @extend_schema(
 | 
				
			||||||
        request=RescueOrganizationSerializer,
 | 
					        request=RescueOrgSerializer,  # Document the request body
 | 
				
			||||||
        responses={201: 'Rescue organization created successfully!'}
 | 
					        responses={201: 'Rescue organization created/updated successfully!'}
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					    def post(self, request, *args, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Create or update a rescue organization.
 | 
					        Create or update a rescue organization.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        serializer = RescueOrganizationSerializer(data=request.data, context={"request": request})
 | 
					        serializer = RescueOrgSerializer(data=request.data, context={"request": request})
 | 
				
			||||||
        if serializer.is_valid():
 | 
					        if serializer.is_valid():
 | 
				
			||||||
            rescue_org = serializer.save()
 | 
					            rescue_org = serializer.save(owner=request.user)
 | 
				
			||||||
            if rescue_org.location is None:
 | 
					 | 
				
			||||||
                # Add the location
 | 
					 | 
				
			||||||
                post_rescue_org_save.delay_on_commit(rescue_org.pk)
 | 
					 | 
				
			||||||
            return Response(
 | 
					            return Response(
 | 
				
			||||||
                {"message": "Rescue organization created successfully!", "id": rescue_org.id},
 | 
					                {"message": "Rescue organization created/updated successfully!", "id": rescue_org.id},
 | 
				
			||||||
                status=status.HTTP_201_CREATED,
 | 
					                status=status.HTTP_201_CREATED,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					 | 
				
			||||||
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 | 
					        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @transaction.atomic
 | 
					 | 
				
			||||||
    @extend_schema(
 | 
					 | 
				
			||||||
        request=RescueOrganizationSerializer,
 | 
					 | 
				
			||||||
        responses={200: 'Rescue organization updated successfully!'}
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    def patch(self, request, *args, **kwargs):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Partially update a rescue organization.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        org_id = request.data.get("id")
 | 
					 | 
				
			||||||
        if not org_id:
 | 
					 | 
				
			||||||
            return Response({"error": "ID is required for updating."}, status=status.HTTP_400_BAD_REQUEST)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            organization = RescueOrganization.objects.get(pk=org_id)
 | 
					 | 
				
			||||||
        except RescueOrganization.DoesNotExist:
 | 
					 | 
				
			||||||
            return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        serializer = RescueOrganizationSerializer(organization, data=request.data, partial=True)
 | 
					 | 
				
			||||||
        if serializer.is_valid():
 | 
					 | 
				
			||||||
            serializer.save()
 | 
					 | 
				
			||||||
            return Response({"message": "Rescue organization updated successfully!"}, status=status.HTTP_200_OK)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class AddImageApiView(APIView):
 | 
					class AddImageApiView(APIView):
 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					    permission_classes = [IsAuthenticated]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -290,7 +188,7 @@ class AddImageApiView(APIView):
 | 
				
			|||||||
                raise ValueError("Unknown attach_to_type given, should not happen. Check serializer")
 | 
					                raise ValueError("Unknown attach_to_type given, should not happen. Check serializer")
 | 
				
			||||||
            serializer.validated_data.pop('attach_to_type', None)
 | 
					            serializer.validated_data.pop('attach_to_type', None)
 | 
				
			||||||
            serializer.validated_data.pop('attach_to', None)
 | 
					            serializer.validated_data.pop('attach_to', None)
 | 
				
			||||||
            image = serializer.save(owner=request.user_to_notify)
 | 
					            image = serializer.save(owner=request.user)
 | 
				
			||||||
            object_to_attach_to.photos.add(image)
 | 
					            object_to_attach_to.photos.add(image)
 | 
				
			||||||
            return Response(
 | 
					            return Response(
 | 
				
			||||||
                {"message": "Image added successfully!", "id": image.id},
 | 
					                {"message": "Image added successfully!", "id": image.id},
 | 
				
			||||||
@@ -312,141 +210,3 @@ class SpeciesApiView(APIView):
 | 
				
			|||||||
        species = Species.objects.all()
 | 
					        species = Species.objects.all()
 | 
				
			||||||
        serializer = SpeciesSerializer(species, many=True, context={"request": request})
 | 
					        serializer = SpeciesSerializer(species, many=True, context={"request": request})
 | 
				
			||||||
        return Response(serializer.data, status=status.HTTP_200_OK)
 | 
					        return Response(serializer.data, status=status.HTTP_200_OK)
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class LocationApiView(APIView):
 | 
					 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @extend_schema(
 | 
					 | 
				
			||||||
        parameters=[
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                'name': 'id',
 | 
					 | 
				
			||||||
                'required': False,
 | 
					 | 
				
			||||||
                'description': 'ID of the location to retrieve.',
 | 
					 | 
				
			||||||
                'type': int
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
        responses={200: LocationSerializer(many=True)}
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    def get(self, request, *args, **kwargs):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Retrieve a location
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        location_id = kwargs.get("id")
 | 
					 | 
				
			||||||
        if location_id:
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                location = Location.objects.get(pk=location_id)
 | 
					 | 
				
			||||||
                serializer = LocationSerializer(location, context={"request": request})
 | 
					 | 
				
			||||||
                return Response(serializer.data, status=status.HTTP_200_OK)
 | 
					 | 
				
			||||||
            except Location.DoesNotExist:
 | 
					 | 
				
			||||||
                return Response({"error": "Location not found."}, status=status.HTTP_404_NOT_FOUND)
 | 
					 | 
				
			||||||
        locations = Location.objects.all()
 | 
					 | 
				
			||||||
        serializer = LocationSerializer(locations, many=True, context={"request": request})
 | 
					 | 
				
			||||||
        return Response(serializer.data, status=status.HTTP_200_OK)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @transaction.atomic
 | 
					 | 
				
			||||||
    @extend_schema(
 | 
					 | 
				
			||||||
        request=LocationSerializer,
 | 
					 | 
				
			||||||
        responses={201: 'Location created successfully!'}
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        API view to add a location
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        serializer = LocationSerializer(data=request.data, context={'request': request})
 | 
					 | 
				
			||||||
        if not serializer.is_valid():
 | 
					 | 
				
			||||||
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        location = serializer.save()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Log the action
 | 
					 | 
				
			||||||
        Log.objects.create(
 | 
					 | 
				
			||||||
            user=request.user,
 | 
					 | 
				
			||||||
            action="add_location",
 | 
					 | 
				
			||||||
            text=f"{request.user} added adoption notice {location.pk} via API",
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Return success response with new adoption notice details
 | 
					 | 
				
			||||||
        return Response(
 | 
					 | 
				
			||||||
            {"message": "Location created successfully!", "id": location.pk},
 | 
					 | 
				
			||||||
            status=status.HTTP_201_CREATED,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class AdoptionNoticeGeoJSONView(ListAPIView):
 | 
					 | 
				
			||||||
    queryset = AdoptionNotice.objects.select_related('location').filter(location__isnull=False).filter(
 | 
					 | 
				
			||||||
        adoption_notice_status__in=AdoptionNoticeStatusChoices.Active.values)
 | 
					 | 
				
			||||||
    serializer_class = AdoptionNoticeGeoJSONSerializer
 | 
					 | 
				
			||||||
    renderer_classes = [GeoJSONRenderer]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class RescueOrgGeoJSONView(ListAPIView):
 | 
					 | 
				
			||||||
    queryset = RescueOrganization.objects.select_related('location').filter(location__isnull=False)
 | 
					 | 
				
			||||||
    serializer_class = RescueOrgeGeoJSONSerializer
 | 
					 | 
				
			||||||
    renderer_classes = [GeoJSONRenderer]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class AdoptionNoticePerOrgApiView(APIView):
 | 
					 | 
				
			||||||
    permission_classes = [IsAuthenticated]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @extend_schema(
 | 
					 | 
				
			||||||
        parameters=[
 | 
					 | 
				
			||||||
            OpenApiParameter(
 | 
					 | 
				
			||||||
                name='id',
 | 
					 | 
				
			||||||
                required=False,
 | 
					 | 
				
			||||||
                description='ID of the rescue organization from which to retrieve adoption notices.',
 | 
					 | 
				
			||||||
                type=OpenApiTypes.INT
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            OpenApiParameter(
 | 
					 | 
				
			||||||
                name='in_hierarchy',
 | 
					 | 
				
			||||||
                type=OpenApiTypes.BOOL,
 | 
					 | 
				
			||||||
                required=False,
 | 
					 | 
				
			||||||
                description='Show all Adoption Notices in hierarchy.',
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            OpenApiParameter(
 | 
					 | 
				
			||||||
                name='status',
 | 
					 | 
				
			||||||
                type=OpenApiTypes.STR,
 | 
					 | 
				
			||||||
                required=False,
 | 
					 | 
				
			||||||
                description='Show all Adoption Notices in a certain status. Comma separated list of values e.g. '
 | 
					 | 
				
			||||||
                            '"active,closed"',
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
        responses={200: AdoptionNoticeSerializer(many=True)}
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    def get(self, request, *args, **kwargs):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Retrieve adoption notices with their related animals and images.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        org_id = kwargs.get("id")
 | 
					 | 
				
			||||||
        in_hierarchy = request.query_params.get("in_hierarchy")
 | 
					 | 
				
			||||||
        an_status = request.query_params.get("status")
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            org = RescueOrganization.objects.get(id=org_id)
 | 
					 | 
				
			||||||
        except RescueOrganization.DoesNotExist:
 | 
					 | 
				
			||||||
            return Response({"error": "Rescue Organization notice not found."}, status=status.HTTP_404_NOT_FOUND)
 | 
					 | 
				
			||||||
        if in_hierarchy:
 | 
					 | 
				
			||||||
            adoption_notices = org.adoption_notices_in_hierarchy
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            adoption_notices = AdoptionNotice.objects.filter(organization=org)
 | 
					 | 
				
			||||||
        if an_status:
 | 
					 | 
				
			||||||
            status_list = an_status.lower().strip().split(",")
 | 
					 | 
				
			||||||
            temporary_an_storage = []
 | 
					 | 
				
			||||||
            if "active" in status_list:
 | 
					 | 
				
			||||||
                active_ans = [adoption_notice for adoption_notice in adoption_notices if
 | 
					 | 
				
			||||||
                              adoption_notice.adoption_notice_status in AdoptionNoticeStatusChoices.Active.values]
 | 
					 | 
				
			||||||
                temporary_an_storage.extend(active_ans)
 | 
					 | 
				
			||||||
            if "closed" in status_list:
 | 
					 | 
				
			||||||
                closed_ans = [adoption_notice for adoption_notice in adoption_notices if
 | 
					 | 
				
			||||||
                              adoption_notice.adoption_notice_status in AdoptionNoticeStatusChoices.Closed.values]
 | 
					 | 
				
			||||||
                temporary_an_storage.extend(closed_ans)
 | 
					 | 
				
			||||||
            if "disabled" in status_list:
 | 
					 | 
				
			||||||
                disabled_ans = [adoption_notice for adoption_notice in adoption_notices if
 | 
					 | 
				
			||||||
                                adoption_notice.adoption_notice_status in AdoptionNoticeStatusChoices.Disabled.values]
 | 
					 | 
				
			||||||
                temporary_an_storage.extend(disabled_ans)
 | 
					 | 
				
			||||||
            if "awaiting_action" in status_list:
 | 
					 | 
				
			||||||
                awaiting_action_ans = [adoption_notice for adoption_notice in adoption_notices if
 | 
					 | 
				
			||||||
                                       adoption_notice.adoption_notice_status in AdoptionNoticeStatusChoices.AwaitingAction.values]
 | 
					 | 
				
			||||||
                temporary_an_storage.extend(awaiting_action_ans)
 | 
					 | 
				
			||||||
            adoption_notices = temporary_an_storage
 | 
					 | 
				
			||||||
        serializer = AdoptionNoticeSerializer(adoption_notices, many=True, context={"request": request})
 | 
					 | 
				
			||||||
        return Response(serializer.data, status=status.HTTP_200_OK)
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,37 +0,0 @@
 | 
				
			|||||||
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})
 | 
					 | 
				
			||||||
@@ -1,23 +0,0 @@
 | 
				
			|||||||
 | 
					 | 
				
			||||||
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
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@@ -1,12 +0,0 @@
 | 
				
			|||||||
from django.urls import path
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from . import embeddables
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
urlpatterns = [
 | 
					 | 
				
			||||||
    path("tierschutzorganisationen/<int:rescue_organization_id>/vermittlungen/",
 | 
					 | 
				
			||||||
         embeddables.list_ans_per_rescue_organization,
 | 
					 | 
				
			||||||
         name="list-adoption-notices-for-rescue-organization"),
 | 
					 | 
				
			||||||
    path("tierschutzorganisationen/<int:rescue_organization_id>/vermittlungen/<slug:species_slug>/",
 | 
					 | 
				
			||||||
         embeddables.list_ans_per_rescue_organization,
 | 
					 | 
				
			||||||
         name="list-adoption-notices-for-rescue-organization-species"),
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
from django import forms
 | 
					from django import forms
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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
 | 
				
			||||||
from django_registration.forms import RegistrationForm
 | 
					from django_registration.forms import RegistrationForm
 | 
				
			||||||
from crispy_forms.helper import FormHelper
 | 
					from crispy_forms.helper import FormHelper
 | 
				
			||||||
from crispy_forms.layout import Submit, Layout, Fieldset, HTML, Row, Column, Field, Hidden
 | 
					from crispy_forms.layout import Submit, Layout, Fieldset, HTML, Row, Column, Field, Hidden
 | 
				
			||||||
@@ -23,46 +23,95 @@ class DateInput(forms.DateInput):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AdoptionNoticeForm(forms.ModelForm):
 | 
					class AdoptionNoticeForm(forms.ModelForm):
 | 
				
			||||||
    template_name = "fellchensammlung/forms/form_snippets.html"
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        if 'in_adoption_notice_creation_flow' in kwargs:
 | 
				
			||||||
 | 
					            in_flow = kwargs.pop('in_adoption_notice_creation_flow')
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            in_flow = False
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        self.helper = FormHelper()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.helper.form_id = 'form-adoption-notice'
 | 
				
			||||||
 | 
					        self.helper.form_class = 'card'
 | 
				
			||||||
 | 
					        self.helper.form_method = 'post'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if in_flow:
 | 
				
			||||||
 | 
					            submit = Submit('save-and-add-another-animal', _('Speichern'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            submit = Submit('submit', _('Speichern'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.helper.layout = Layout(
 | 
				
			||||||
 | 
					            Fieldset(
 | 
				
			||||||
 | 
					                _('Vermittlungsdetails'),
 | 
				
			||||||
 | 
					                'name',
 | 
				
			||||||
 | 
					                'species',
 | 
				
			||||||
 | 
					                'num_animals',
 | 
				
			||||||
 | 
					                'date_of_birth',
 | 
				
			||||||
 | 
					                'sex',
 | 
				
			||||||
 | 
					                'group_only',
 | 
				
			||||||
 | 
					                'searching_since',
 | 
				
			||||||
 | 
					                'location_string',
 | 
				
			||||||
 | 
					                'organization',
 | 
				
			||||||
 | 
					                'description',
 | 
				
			||||||
 | 
					                'further_information',
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            submit)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = AdoptionNotice
 | 
				
			||||||
 | 
					        fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string",
 | 
				
			||||||
 | 
					                  "organization"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AdoptionNoticeFormWithDateWidget(AdoptionNoticeForm):
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = AdoptionNotice
 | 
					        model = AdoptionNotice
 | 
				
			||||||
        fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string",
 | 
					        fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string",
 | 
				
			||||||
                  "organization"]
 | 
					                  "organization"]
 | 
				
			||||||
        widgets = {
 | 
					        widgets = {
 | 
				
			||||||
            'searching_since': DateInput(format=('%Y-%m-%d')),
 | 
					            'searching_since': DateInput(),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AdoptionNoticeFormAutoAnimal(AdoptionNoticeForm):
 | 
					 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					 | 
				
			||||||
        super(AdoptionNoticeFormAutoAnimal, self).__init__(*args, **kwargs)
 | 
					 | 
				
			||||||
        self.fields["num_animals"] = forms.fields.IntegerField(min_value=1, max_value=30, label=_("Zahl der Tiere"))
 | 
					 | 
				
			||||||
        animal_form = AnimalForm()
 | 
					 | 
				
			||||||
        self.fields["species"] = animal_form.fields["species"]
 | 
					 | 
				
			||||||
        self.fields["sex"] = animal_form.fields["sex"]
 | 
					 | 
				
			||||||
        self.fields["date_of_birth"] = animal_form.fields["date_of_birth"]
 | 
					 | 
				
			||||||
        self.fields["date_of_birth"].widget = DateInput(format=('%Y-%m-%d'))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class AnimalForm(forms.ModelForm):
 | 
					class AnimalForm(forms.ModelForm):
 | 
				
			||||||
    template_name = "fellchensammlung/forms/form_snippets.html"
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        if 'in_adoption_notice_creation_flow' in kwargs:
 | 
				
			||||||
 | 
					            adding = kwargs.pop('in_adoption_notice_creation_flow')
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            adding = False
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        self.helper = FormHelper()
 | 
				
			||||||
 | 
					        self.helper.form_class = 'form-animal card'
 | 
				
			||||||
 | 
					        if adding:
 | 
				
			||||||
 | 
					            self.helper.add_input(Submit('save-and-add-another-animal', _('Speichern und weiteres Tier hinzufügen')))
 | 
				
			||||||
 | 
					            self.helper.add_input(Submit('save-and-finish', _('Speichern und beenden')))
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            self.helper.add_input(Submit('submit', _('Speichern'), css_class="btn"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Animal
 | 
					        model = Animal
 | 
				
			||||||
        fields = ["name", "date_of_birth", "species", "sex", "description"]
 | 
					        fields = ["name", "date_of_birth", "species", "sex", "description"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AnimalFormWithDateWidget(AnimalForm):
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        model = Animal
 | 
				
			||||||
 | 
					        fields = ["name", "date_of_birth", "species", "sex", "description"]
 | 
				
			||||||
        widgets = {
 | 
					        widgets = {
 | 
				
			||||||
            'date_of_birth': DateInput(format=('%Y-%m-%d'))
 | 
					            'date_of_birth': DateInput(),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UpdateRescueOrgRegularCheckStatus(forms.ModelForm):
 | 
					class AdoptionNoticeFormWithDateWidgetAutoAnimal(AdoptionNoticeFormWithDateWidget):
 | 
				
			||||||
    template_name = "fellchensammlung/forms/form_snippets.html"
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        super(AdoptionNoticeFormWithDateWidgetAutoAnimal, self).__init__(*args, **kwargs)
 | 
				
			||||||
    class Meta:
 | 
					        self.fields["num_animals"] = forms.fields.IntegerField(min_value=1, max_value=30, label=_("Zahl der Tiere"))
 | 
				
			||||||
        model = RescueOrganization
 | 
					        animal_form = AnimalForm()
 | 
				
			||||||
        fields = ["regular_check_status"]
 | 
					        self.fields["species"] = animal_form.fields["species"]
 | 
				
			||||||
 | 
					        self.fields["sex"] = animal_form.fields["sex"]
 | 
				
			||||||
 | 
					        self.fields["date_of_birth"] = animal_form.fields["date_of_birth"]
 | 
				
			||||||
 | 
					        self.fields["date_of_birth"].widget = DateInput()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ImageForm(forms.ModelForm):
 | 
					class ImageForm(forms.ModelForm):
 | 
				
			||||||
@@ -78,9 +127,8 @@ class ImageForm(forms.ModelForm):
 | 
				
			|||||||
        self.helper.form_method = 'post'
 | 
					        self.helper.form_method = 'post'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if in_flow:
 | 
					        if in_flow:
 | 
				
			||||||
            submits = Div(Submit('submit', _('Speichern')),
 | 
					           submits= Div(Submit('submit', _('Speichern')),
 | 
				
			||||||
                          Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')),
 | 
					                      Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')), css_class="container-edit-buttons")
 | 
				
			||||||
                          css_class="container-edit-buttons")
 | 
					 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            submits = Fieldset(Submit('submit', _('Speichern')), css_class="container-edit-buttons")
 | 
					            submits = Fieldset(Submit('submit', _('Speichern')), css_class="container-edit-buttons")
 | 
				
			||||||
        self.helper.layout = Layout(
 | 
					        self.helper.layout = Layout(
 | 
				
			||||||
@@ -92,45 +140,37 @@ class ImageForm(forms.ModelForm):
 | 
				
			|||||||
            submits
 | 
					            submits
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Image
 | 
					        model = Image
 | 
				
			||||||
        fields = ('image', 'alt_text')
 | 
					        fields = ('image', 'alt_text')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReportAdoptionNoticeForm(forms.ModelForm):
 | 
					class ReportAdoptionNoticeForm(forms.ModelForm):
 | 
				
			||||||
    template_name = "fellchensammlung/forms/form_snippets.html"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = ReportAdoptionNotice
 | 
					        model = ReportAdoptionNotice
 | 
				
			||||||
        fields = ('reported_broken_rules', 'user_comment')
 | 
					        fields = ('reported_broken_rules', 'user_comment')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReportCommentForm(forms.ModelForm):
 | 
					class ReportCommentForm(forms.ModelForm):
 | 
				
			||||||
    template_name = "fellchensammlung/forms/form_snippets.html"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = ReportComment
 | 
					        model = ReportComment
 | 
				
			||||||
        fields = ('reported_broken_rules', 'user_comment')
 | 
					        fields = ('reported_broken_rules', 'user_comment')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CommentForm(forms.ModelForm):
 | 
					class CommentForm(forms.ModelForm):
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        self.helper = FormHelper()
 | 
				
			||||||
 | 
					        self.helper.form_class = 'form-comments'
 | 
				
			||||||
 | 
					        self.helper.add_input(Hidden('action', 'comment'))
 | 
				
			||||||
 | 
					        self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="btn2"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = Comment
 | 
					        model = Comment
 | 
				
			||||||
        fields = ('text',)
 | 
					        fields = ('text',)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SpeciesURLForm(forms.ModelForm):
 | 
					 | 
				
			||||||
    class Meta:
 | 
					 | 
				
			||||||
        model = SpeciesSpecificURL
 | 
					 | 
				
			||||||
        fields = ('species', 'url')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class RescueOrgInternalComment(forms.ModelForm):
 | 
					 | 
				
			||||||
    class Meta:
 | 
					 | 
				
			||||||
        model = RescueOrganization
 | 
					 | 
				
			||||||
        fields = ('internal_comment',)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ModerationActionForm(forms.ModelForm):
 | 
					class ModerationActionForm(forms.ModelForm):
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = ModerationAction
 | 
					        model = ModerationAction
 | 
				
			||||||
@@ -141,25 +181,19 @@ class CustomRegistrationForm(RegistrationForm):
 | 
				
			|||||||
    class Meta(RegistrationForm.Meta):
 | 
					    class Meta(RegistrationForm.Meta):
 | 
				
			||||||
        model = User
 | 
					        model = User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    template_name = "fellchensammlung/forms/form_snippets.html"
 | 
					    captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_("Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um sicherzustellen, dass du kein Roboter bist."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_(
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
        "Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um sicherzustellen, dass du kein Roboter bist."))
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        self.helper = FormHelper()
 | 
				
			||||||
 | 
					        self.helper.form_id = 'form-registration'
 | 
				
			||||||
 | 
					        self.helper.form_class = 'card'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.helper.add_input(Submit('submit', _('Registrieren'), css_class="btn"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AdoptionNoticeSearchForm(forms.Form):
 | 
					class AdoptionNoticeSearchForm(forms.Form):
 | 
				
			||||||
    template_name = "fellchensammlung/forms/form_snippets.html"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False,
 | 
					    sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False,
 | 
				
			||||||
                            initial=SexChoicesWithAll.ALL)
 | 
					                            initial=SexChoicesWithAll.ALL)
 | 
				
			||||||
    max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED,
 | 
					    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"))
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,43 +1,43 @@
 | 
				
			|||||||
from django.db.models.signals import post_save
 | 
					from django.db.models.signals import post_save
 | 
				
			||||||
from django.dispatch import receiver
 | 
					from django.dispatch import receiver
 | 
				
			||||||
from django.template.loader import render_to_string
 | 
					 | 
				
			||||||
from django.utils.html import strip_tags
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.core import mail
 | 
					from django.core import mail
 | 
				
			||||||
from fellchensammlung.models import User, Notification, TrustLevel, NotificationTypeChoices
 | 
					from fellchensammlung.models import User, CommentNotification, BaseNotification, TrustLevel
 | 
				
			||||||
from fellchensammlung.tools.model_helpers import ndm
 | 
					from notfellchen.settings import host
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					NEWLINE = "\r\n"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def notify_mods_new_report(report, notification_type):
 | 
					def mail_admins_new_report(report):
 | 
				
			||||||
    """
 | 
					    subject = _("Neue Meldung")
 | 
				
			||||||
    Sends an e-mail to all users that should handle the report.
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
 | 
					    for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
 | 
				
			||||||
        if notification_type == NotificationTypeChoices.NEW_REPORT_AN:
 | 
					        greeting = _("Moin,") + "{NEWLINE}"
 | 
				
			||||||
            title = _("Vermittlung gemeldet")
 | 
					        new_report_text = _("es wurde ein Regelverstoß gemeldet.") + "{NEWLINE}"
 | 
				
			||||||
        elif notification_type == NotificationTypeChoices.NEW_REPORT_COMMENT:
 | 
					        if len(report.reported_broken_rules.all()) > 0:
 | 
				
			||||||
            title = _("Kommentar gemeldet")
 | 
					            reported_rules_text = (f"Ein Verstoß gegen die folgenden Regeln wurde gemeldet:{NEWLINE}"
 | 
				
			||||||
 | 
					                                   f"- {f'{NEWLINE} - '.join([str(r) for r in report.reported_broken_rules.all()])}{NEWLINE}")
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            raise NotImplementedError
 | 
					            reported_rules_text = f"Es wurden keine Regeln angegeben gegen die Verstoßen wurde.{NEWLINE}"
 | 
				
			||||||
        notification = Notification.objects.create(
 | 
					        if report.user_comment:
 | 
				
			||||||
            notification_type=notification_type,
 | 
					            comment_text = f'Kommentar zum Report: "{report.user_comment}"{NEWLINE}'
 | 
				
			||||||
            user_to_notify=moderator,
 | 
					        else:
 | 
				
			||||||
            report=report,
 | 
					            comment_text = f"Es wurde kein Kommentar hinzugefügt.{NEWLINE}"
 | 
				
			||||||
            title=title,
 | 
					
 | 
				
			||||||
        )
 | 
					        report_url = "https://" + host + report.get_absolute_url()
 | 
				
			||||||
        notification.save()
 | 
					        link_text = f"Um alle Details zu sehen, geh bitte auf: {report_url}"
 | 
				
			||||||
 | 
					        body_text = greeting + new_report_text + reported_rules_text + comment_text + link_text
 | 
				
			||||||
 | 
					        message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [moderator.email])
 | 
				
			||||||
 | 
					        message.send()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def send_notification_email(notification_pk):
 | 
					def send_notification_email(notification_pk):
 | 
				
			||||||
    notification = Notification.objects.get(pk=notification_pk)
 | 
					    try:
 | 
				
			||||||
 | 
					        notification = CommentNotification.objects.get(pk=notification_pk)
 | 
				
			||||||
    subject = f"{notification.title}"
 | 
					    except CommentNotification.DoesNotExist:
 | 
				
			||||||
    context = {"notification": notification, }
 | 
					        notification = BaseNotification.objects.get(pk=notification_pk)
 | 
				
			||||||
    html_message = render_to_string(ndm[notification.notification_type].email_html_template, context)
 | 
					    subject = f"🔔 {notification.title}"
 | 
				
			||||||
    plain_message = render_to_string(ndm[notification.notification_type].email_plain_template, context)
 | 
					    body_text = notification.text
 | 
				
			||||||
 | 
					    message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [notification.user.email])
 | 
				
			||||||
    mail.send_mail(subject, plain_message, settings.DEFAULT_FROM_EMAIL,
 | 
					    message.send()
 | 
				
			||||||
                   [notification.user_to_notify.email],
 | 
					 | 
				
			||||||
                   html_message=html_message)
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +0,0 @@
 | 
				
			|||||||
from django.core.management import BaseCommand
 | 
					 | 
				
			||||||
from fellchensammlung.tools.admin import dedup_locations
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Command(BaseCommand):
 | 
					 | 
				
			||||||
    help = 'Deduplicate locations based on place_id'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def handle(self, *args, **options):
 | 
					 | 
				
			||||||
        dedup_locations()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@@ -1,11 +0,0 @@
 | 
				
			|||||||
from django.core.management import BaseCommand
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from fellchensammlung.tools.admin import export_orgs_as_vcf
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Command(BaseCommand):
 | 
					 | 
				
			||||||
    help = 'Export organizations with phone number as contacts in vcf format'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def handle(self, *args, **options):
 | 
					 | 
				
			||||||
        export_orgs_as_vcf()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@@ -1,13 +0,0 @@
 | 
				
			|||||||
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)
 | 
					 | 
				
			||||||
@@ -1,19 +0,0 @@
 | 
				
			|||||||
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)
 | 
					 | 
				
			||||||
@@ -1,20 +0,0 @@
 | 
				
			|||||||
# Generated by Django 5.1.4 on 2025-03-09 08:31
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import django.utils.timezone
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('fellchensammlung', '0037_alter_basenotification_title'),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='rescueorganization',
 | 
					 | 
				
			||||||
            name='last_checked',
 | 
					 | 
				
			||||||
            field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Datum der letzten Prüfung'),
 | 
					 | 
				
			||||||
            preserve_default=False,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,18 +0,0 @@
 | 
				
			|||||||
# Generated by Django 5.1.4 on 2025-03-09 16:44
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('fellchensammlung', '0038_rescueorganization_last_checked'),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.AlterField(
 | 
					 | 
				
			||||||
            model_name='rescueorganization',
 | 
					 | 
				
			||||||
            name='last_checked',
 | 
					 | 
				
			||||||
            field=models.DateTimeField(auto_now_add=True, verbose_name='Datum der letzten Prüfung'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,18 +0,0 @@
 | 
				
			|||||||
# Generated by Django 5.1.4 on 2025-03-20 23:27
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('fellchensammlung', '0039_alter_rescueorganization_last_checked'),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.AlterField(
 | 
					 | 
				
			||||||
            model_name='rescueorganization',
 | 
					 | 
				
			||||||
            name='allows_using_materials',
 | 
					 | 
				
			||||||
            field=models.CharField(choices=[('allowed', 'Usage allowed'), ('requested', 'Usage requested'), ('denied', 'Usage denied'), ('other', "It's complicated"), ('not_asked', 'Not asked')], default='not_asked', max_length=200, verbose_name='Erlaubt Nutzung von Inhalten'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,42 +0,0 @@
 | 
				
			|||||||
# Generated by Django 5.1.4 on 2025-04-06 06:37
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('fellchensammlung', '0040_alter_rescueorganization_allows_using_materials'),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='location',
 | 
					 | 
				
			||||||
            name='city',
 | 
					 | 
				
			||||||
            field=models.CharField(blank=True, max_length=200, null=True),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='location',
 | 
					 | 
				
			||||||
            name='country',
 | 
					 | 
				
			||||||
            field=models.CharField(blank=True, help_text='Standardisierter Ländercode nach ISO 3166-1 ALPHA-2', max_length=2, null=True, verbose_name='Ländercode'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='location',
 | 
					 | 
				
			||||||
            name='housenumber',
 | 
					 | 
				
			||||||
            field=models.CharField(blank=True, max_length=20, null=True),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='location',
 | 
					 | 
				
			||||||
            name='postcode',
 | 
					 | 
				
			||||||
            field=models.CharField(blank=True, max_length=20, null=True),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='location',
 | 
					 | 
				
			||||||
            name='street',
 | 
					 | 
				
			||||||
            field=models.CharField(blank=True, max_length=200, null=True),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AlterUniqueTogether(
 | 
					 | 
				
			||||||
            name='rescueorganization',
 | 
					 | 
				
			||||||
            unique_together={('external_object_identifier', 'external_source_identifier')},
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,18 +0,0 @@
 | 
				
			|||||||
# Generated by Django 5.1.4 on 2025-04-24 17:13
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('fellchensammlung', '0041_location_city_location_country_location_housenumber_and_more'),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='location',
 | 
					 | 
				
			||||||
            name='county',
 | 
					 | 
				
			||||||
            field=models.CharField(blank=True, max_length=200, null=True),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,18 +0,0 @@
 | 
				
			|||||||
# Generated by Django 5.1.4 on 2025-04-24 17:41
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.db import migrations
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('fellchensammlung', '0042_location_county'),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.RenameField(
 | 
					 | 
				
			||||||
            model_name='location',
 | 
					 | 
				
			||||||
            old_name='country',
 | 
					 | 
				
			||||||
            new_name='countrycode',
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,18 +0,0 @@
 | 
				
			|||||||
# Generated by Django 5.1.4 on 2025-04-26 22:21
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('fellchensammlung', '0043_rename_country_location_countrycode'),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.AlterField(
 | 
					 | 
				
			||||||
            model_name='location',
 | 
					 | 
				
			||||||
            name='place_id',
 | 
					 | 
				
			||||||
            field=models.CharField(max_length=200),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,23 +0,0 @@
 | 
				
			|||||||
# Generated by Django 5.1.4 on 2025-04-27 11:31
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import django.db.models.deletion
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('fellchensammlung', '0044_alter_location_place_id'),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.CreateModel(
 | 
					 | 
				
			||||||
            name='ImportantLocation',
 | 
					 | 
				
			||||||
            fields=[
 | 
					 | 
				
			||||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
					 | 
				
			||||||
                ('slug', models.SlugField(unique=True)),
 | 
					 | 
				
			||||||
                ('name', models.CharField(max_length=200)),
 | 
					 | 
				
			||||||
                ('location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.location')),
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,19 +0,0 @@
 | 
				
			|||||||
# Generated by Django 5.1.4 on 2025-04-27 11:51
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import django.db.models.deletion
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('fellchensammlung', '0045_importantlocation'),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.AlterField(
 | 
					 | 
				
			||||||
            model_name='importantlocation',
 | 
					 | 
				
			||||||
            name='location',
 | 
					 | 
				
			||||||
            field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.location'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,28 +0,0 @@
 | 
				
			|||||||
# Generated by Django 5.2.1 on 2025-05-23 16:07
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('fellchensammlung', '0046_alter_importantlocation_location'),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.AlterField(
 | 
					 | 
				
			||||||
            model_name='adoptionnotice',
 | 
					 | 
				
			||||||
            name='further_information',
 | 
					 | 
				
			||||||
            field=models.URLField(blank=True, help_text='Verlinke hier die Quelle der Vermittlung (z.B. die Website des Tierheims', null=True, verbose_name='Link zu mehr Informationen'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AlterField(
 | 
					 | 
				
			||||||
            model_name='adoptionnotice',
 | 
					 | 
				
			||||||
            name='name',
 | 
					 | 
				
			||||||
            field=models.CharField(max_length=200, verbose_name='Titel der Vermittlung'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AlterField(
 | 
					 | 
				
			||||||
            model_name='rescueorganization',
 | 
					 | 
				
			||||||
            name='location_string',
 | 
					 | 
				
			||||||
            field=models.CharField(blank=True, max_length=200, null=True, verbose_name='Ort der Organisation'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,18 +0,0 @@
 | 
				
			|||||||
# Generated by Django 5.2.1 on 2025-06-19 15:51
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('fellchensammlung', '0047_alter_adoptionnotice_further_information_and_more'),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.AlterField(
 | 
					 | 
				
			||||||
            model_name='adoptionnotice',
 | 
					 | 
				
			||||||
            name='further_information',
 | 
					 | 
				
			||||||
            field=models.URLField(blank=True, help_text='Verlinke hier die Quelle der Vermittlung (z.B. die Website des Tierheims)', null=True, verbose_name='Link zu mehr Informationen'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,18 +0,0 @@
 | 
				
			|||||||
# Generated by Django 5.2.1 on 2025-06-19 21:26
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('fellchensammlung', '0048_alter_adoptionnotice_further_information'),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='rescueorganization',
 | 
					 | 
				
			||||||
            name='exclude_from_check',
 | 
					 | 
				
			||||||
            field=models.BooleanField(default=False, help_text='Organisation von der manuellen Überprüfung ausschließen, z.B. weil Tiere nicht online geführt werden', verbose_name='Von Prüfung ausschließen'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,18 +0,0 @@
 | 
				
			|||||||
# Generated by Django 5.2.1 on 2025-06-20 16:52
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.db import migrations
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('fellchensammlung', '0049_rescueorganization_exclude_from_check'),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.RenameField(
 | 
					 | 
				
			||||||
            model_name='speciesspecificurl',
 | 
					 | 
				
			||||||
            old_name='rescues_organization',
 | 
					 | 
				
			||||||
            new_name='rescue_organization',
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,27 +0,0 @@
 | 
				
			|||||||
# Generated by Django 5.2.1 on 2025-07-03 09:28
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import django.db.models.deletion
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('fellchensammlung', '0050_rename_rescues_organization_speciesspecificurl_rescue_organization'),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='rescueorganization',
 | 
					 | 
				
			||||||
            name='parent_org',
 | 
					 | 
				
			||||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.rescueorganization'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.CreateModel(
 | 
					 | 
				
			||||||
            name='SpeciesSpecialization',
 | 
					 | 
				
			||||||
            fields=[
 | 
					 | 
				
			||||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
					 | 
				
			||||||
                ('rescue_organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.rescueorganization', verbose_name='Tierschutzorganisation')),
 | 
					 | 
				
			||||||
                ('species', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.species', verbose_name='Tierart')),
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,54 +0,0 @@
 | 
				
			|||||||
# Generated by Django 5.2.1 on 2025-07-11 09:01
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import django.db.models.deletion
 | 
					 | 
				
			||||||
from django.conf import settings
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('fellchensammlung', '0051_rescueorganization_parent_org_speciesspecialization'),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.RemoveField(
 | 
					 | 
				
			||||||
            model_name='basenotification',
 | 
					 | 
				
			||||||
            name='user',
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.RemoveField(
 | 
					 | 
				
			||||||
            model_name='commentnotification',
 | 
					 | 
				
			||||||
            name='basenotification_ptr',
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.RemoveField(
 | 
					 | 
				
			||||||
            model_name='commentnotification',
 | 
					 | 
				
			||||||
            name='comment',
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.CreateModel(
 | 
					 | 
				
			||||||
            name='Notification',
 | 
					 | 
				
			||||||
            fields=[
 | 
					 | 
				
			||||||
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
 | 
					 | 
				
			||||||
                ('created_at', models.DateTimeField(auto_now_add=True)),
 | 
					 | 
				
			||||||
                ('updated_at', models.DateTimeField(auto_now=True)),
 | 
					 | 
				
			||||||
                ('read_at', models.DateTimeField(blank=True, null=True, verbose_name='Gelesen am')),
 | 
					 | 
				
			||||||
                ('notification_type', models.CharField(choices=[('new_user', 'Useraccount wurde erstellt'), ('new_report_an', 'Vermittlung wurde gemeldet'), ('new_report_comment', 'Kommentar wurde gemeldet'), ('an_is_to_be_checked', 'Vermittlung muss überprüft werden'), ('an_was_deactivated', 'Vermittlung wurde deaktiviert'), ('an_for_search_found', 'Vermittlung für Suche gefunden')], max_length=200, verbose_name='Benachrichtigungsgrund')),
 | 
					 | 
				
			||||||
                ('title', models.CharField(max_length=100, verbose_name='Titel')),
 | 
					 | 
				
			||||||
                ('text', models.TextField(verbose_name='Inhalt')),
 | 
					 | 
				
			||||||
                ('read', models.BooleanField(default=False)),
 | 
					 | 
				
			||||||
                ('adoption_notice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung')),
 | 
					 | 
				
			||||||
                ('comment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.comment', verbose_name='Antwort')),
 | 
					 | 
				
			||||||
                ('report', models.ForeignKey(help_text='Report auf den sich die Benachrichtigung bezieht.', on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.report', verbose_name='Report')),
 | 
					 | 
				
			||||||
                ('user_related', models.ForeignKey(help_text='Useraccount auf den sich die Benachrichtigung bezieht.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Verwandter Useraccount')),
 | 
					 | 
				
			||||||
                ('user_to_notify', models.ForeignKey(help_text='Useraccount der Benachrichtigt wird', on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in')),
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.DeleteModel(
 | 
					 | 
				
			||||||
            name='AdoptionNoticeNotification',
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.DeleteModel(
 | 
					 | 
				
			||||||
            name='BaseNotification',
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.DeleteModel(
 | 
					 | 
				
			||||||
            name='CommentNotification',
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,40 +0,0 @@
 | 
				
			|||||||
# Generated by Django 5.2.1 on 2025-07-11 09:10
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import django.db.models.deletion
 | 
					 | 
				
			||||||
from django.conf import settings
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('fellchensammlung', '0052_remove_basenotification_user_and_more'),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.AlterField(
 | 
					 | 
				
			||||||
            model_name='notification',
 | 
					 | 
				
			||||||
            name='adoption_notice',
 | 
					 | 
				
			||||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.adoptionnotice', verbose_name='Vermittlung'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AlterField(
 | 
					 | 
				
			||||||
            model_name='notification',
 | 
					 | 
				
			||||||
            name='notification_type',
 | 
					 | 
				
			||||||
            field=models.CharField(choices=[('new_user', 'Useraccount wurde erstellt'), ('new_report_an', 'Vermittlung wurde gemeldet'), ('new_report_comment', 'Kommentar wurde gemeldet'), ('an_is_to_be_checked', 'Vermittlung muss überprüft werden'), ('an_was_deactivated', 'Vermittlung wurde deaktiviert'), ('an_for_search_found', 'Vermittlung für Suche gefunden'), ('new_comment', 'Neuer Kommentar')], max_length=200, verbose_name='Benachrichtigungsgrund'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AlterField(
 | 
					 | 
				
			||||||
            model_name='notification',
 | 
					 | 
				
			||||||
            name='report',
 | 
					 | 
				
			||||||
            field=models.ForeignKey(blank=True, help_text='Report auf den sich die Benachrichtigung bezieht.', null=True, on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.report', verbose_name='Report'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AlterField(
 | 
					 | 
				
			||||||
            model_name='notification',
 | 
					 | 
				
			||||||
            name='user_related',
 | 
					 | 
				
			||||||
            field=models.ForeignKey(blank=True, help_text='Useraccount auf den sich die Benachrichtigung bezieht.', null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Verwandter Useraccount'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        migrations.AlterField(
 | 
					 | 
				
			||||||
            model_name='notification',
 | 
					 | 
				
			||||||
            name='user_to_notify',
 | 
					 | 
				
			||||||
            field=models.ForeignKey(help_text='Useraccount der Benachrichtigt wird', on_delete=django.db.models.deletion.CASCADE, related_name='user', to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,19 +0,0 @@
 | 
				
			|||||||
# Generated by Django 5.2.1 on 2025-07-11 11:47
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import django.db.models.deletion
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('fellchensammlung', '0053_alter_notification_adoption_notice_and_more'),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.AlterField(
 | 
					 | 
				
			||||||
            model_name='notification',
 | 
					 | 
				
			||||||
            name='comment',
 | 
					 | 
				
			||||||
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.comment', verbose_name='Antwort'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,25 +0,0 @@
 | 
				
			|||||||
# 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'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,22 +0,0 @@
 | 
				
			|||||||
# 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'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,16 +0,0 @@
 | 
				
			|||||||
# 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',
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,25 +0,0 @@
 | 
				
			|||||||
# 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')),
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,23 +0,0 @@
 | 
				
			|||||||
# 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'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,87 +0,0 @@
 | 
				
			|||||||
# 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,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,63 +0,0 @@
 | 
				
			|||||||
# 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),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,21 +0,0 @@
 | 
				
			|||||||
# 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',
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,18 +0,0 @@
 | 
				
			|||||||
# 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'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,140 +0,0 @@
 | 
				
			|||||||
# 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'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,18 +0,0 @@
 | 
				
			|||||||
# 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'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,20 +0,0 @@
 | 
				
			|||||||
# 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),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,18 +0,0 @@
 | 
				
			|||||||
# 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'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,23 +0,0 @@
 | 
				
			|||||||
# 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'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,18 +0,0 @@
 | 
				
			|||||||
# Generated by Django 5.2.1 on 2025-10-20 08:43
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ('fellchensammlung', '0068_alter_adoptionnotice_adoption_notice_status_and_more'),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name='rescueorganization',
 | 
					 | 
				
			||||||
            name='regular_check_status',
 | 
					 | 
				
			||||||
            field=models.CharField(choices=[('regular_check', 'Wird regelmäßig geprüft'), ('excluded_no_online_listing', 'Exkludiert: Tiere werden nicht online gelistet'), ('excluded_other_org', 'Exkludiert: Andere Organisation wird geprüft'), ('excluded_scope', 'Exkludiert: Organisation hat nie Notfellchen-relevanten Vermittlungen'), ('excluded_other', 'Exkludiert: Anderer Grund')], default='regular_check', help_text='Organisationen können, durch ändern dieser Einstellung, von der regelmäßigen Prüfung ausgeschlossen werden.', max_length=30, verbose_name='Status der regelmäßigen Prüfung'),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@@ -1,20 +1,20 @@
 | 
				
			|||||||
import uuid
 | 
					import uuid
 | 
				
			||||||
 | 
					from random import choices
 | 
				
			||||||
 | 
					from tabnanny import verbose
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
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
 | 
					 | 
				
			||||||
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
 | 
				
			||||||
from .tools.geo import LocationProxy, Position
 | 
					from .tools.geo import LocationProxy, Position
 | 
				
			||||||
from .tools.misc import time_since_as_hr_string
 | 
					from .tools.misc import age_as_hr_string, time_since_as_hr_string
 | 
				
			||||||
from .tools.model_helpers import NotificationTypeChoices, AdoptionNoticeStatusChoices, AdoptionProcess, \
 | 
					 | 
				
			||||||
    AdoptionNoticeStatusChoicesDescriptions, RegularCheckStatusChoices
 | 
					 | 
				
			||||||
from .tools.model_helpers import ndm as NotificationDisplayMapping
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Language(models.Model):
 | 
					class Language(models.Model):
 | 
				
			||||||
@@ -39,37 +39,24 @@ class Language(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Location(models.Model):
 | 
					class Location(models.Model):
 | 
				
			||||||
    place_id = models.CharField(max_length=200)  # OSM id
 | 
					    place_id = models.IntegerField()  # OSM id
 | 
				
			||||||
    latitude = models.FloatField(verbose_name=_("Breitengrad"))
 | 
					    latitude = models.FloatField()
 | 
				
			||||||
    longitude = models.FloatField(verbose_name=_("Längengrad"))
 | 
					    longitude = models.FloatField()
 | 
				
			||||||
    name = models.CharField(max_length=2000)
 | 
					    name = models.CharField(max_length=2000)
 | 
				
			||||||
    city = models.CharField(max_length=200, blank=True, null=True, verbose_name=_('Stadt'))
 | 
					 | 
				
			||||||
    housenumber = models.CharField(max_length=20, blank=True, null=True, verbose_name=_("Hausnummer"))
 | 
					 | 
				
			||||||
    postcode = models.CharField(max_length=20, blank=True, null=True, verbose_name=_("Postleitzahl"))
 | 
					 | 
				
			||||||
    street = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Straße"))
 | 
					 | 
				
			||||||
    county = models.CharField(max_length=200, blank=True, null=True, verbose_name=_("Landkreis"))
 | 
					 | 
				
			||||||
    # Country code as per ISO 3166-1 alpha-2
 | 
					 | 
				
			||||||
    # https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes
 | 
					 | 
				
			||||||
    countrycode = models.CharField(max_length=2, verbose_name=_("Ländercode"),
 | 
					 | 
				
			||||||
                                   help_text=_("Standardisierter Ländercode nach ISO 3166-1 ALPHA-2"),
 | 
					 | 
				
			||||||
                                   blank=True, null=True)
 | 
					 | 
				
			||||||
    updated_at = models.DateTimeField(auto_now=True)
 | 
					    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:
 | 
					        return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
 | 
				
			||||||
            return f"{self.city} ({self.postcode})"
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            return f"{self.name}"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def position(self):
 | 
					    def position(self):
 | 
				
			||||||
        return (self.latitude, self.longitude)
 | 
					        return (self.latitude, self.longitude)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def str_hr(self):
 | 
				
			||||||
 | 
					        return f"{self.name.split(',')[0]}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def get_location_from_string(location_string):
 | 
					    def get_location_from_string(location_string):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
@@ -86,11 +73,6 @@ class Location(models.Model):
 | 
				
			|||||||
            latitude=proxy.latitude,
 | 
					            latitude=proxy.latitude,
 | 
				
			||||||
            longitude=proxy.longitude,
 | 
					            longitude=proxy.longitude,
 | 
				
			||||||
            name=proxy.name,
 | 
					            name=proxy.name,
 | 
				
			||||||
            postcode=proxy.postcode,
 | 
					 | 
				
			||||||
            city=proxy.city,
 | 
					 | 
				
			||||||
            street=proxy.street,
 | 
					 | 
				
			||||||
            county=proxy.county,
 | 
					 | 
				
			||||||
            countrycode=proxy.countrycode,
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        return location
 | 
					        return location
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -102,56 +84,35 @@ class Location(models.Model):
 | 
				
			|||||||
        instance.save()
 | 
					        instance.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ImportantLocation(models.Model):
 | 
					 | 
				
			||||||
    class Meta:
 | 
					 | 
				
			||||||
        verbose_name = _("Wichtiger Standort")
 | 
					 | 
				
			||||||
        verbose_name_plural = _("Wichtige Standorte")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    location = models.OneToOneField(Location, on_delete=models.CASCADE)
 | 
					 | 
				
			||||||
    slug = models.SlugField(unique=True)
 | 
					 | 
				
			||||||
    name = models.CharField(max_length=200)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_absolute_url(self):
 | 
					 | 
				
			||||||
        return reverse('search-by-location', kwargs={'important_location_slug': self.slug})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ExternalSourceChoices(models.TextChoices):
 | 
					class ExternalSourceChoices(models.TextChoices):
 | 
				
			||||||
    OSM = "OSM", _("Open Street Map")
 | 
					    OSM = "OSM", _("Open Street Map")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AllowUseOfMaterialsChices(models.TextChoices):
 | 
					 | 
				
			||||||
    USE_MATERIALS_ALLOWED = "allowed", _("Usage allowed")
 | 
					 | 
				
			||||||
    USE_MATERIALS_REQUESTED = "requested", _("Usage requested")
 | 
					 | 
				
			||||||
    USE_MATERIALS_DENIED = "denied", _("Usage denied")
 | 
					 | 
				
			||||||
    USE_MATERIALS_OTHER = "other", _("It's complicated")
 | 
					 | 
				
			||||||
    USE_MATERIALS_NOT_ASKED = "not_asked", _("Not asked")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Species(models.Model):
 | 
					 | 
				
			||||||
    """Model representing a species of animal."""
 | 
					 | 
				
			||||||
    name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
 | 
					 | 
				
			||||||
                            verbose_name=_('Name'))
 | 
					 | 
				
			||||||
    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):
 | 
					class RescueOrganization(models.Model):
 | 
				
			||||||
 | 
					    def __str__(self):
 | 
				
			||||||
 | 
					        return f"{self.name}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    USE_MATERIALS_ALLOWED = "allowed"
 | 
				
			||||||
 | 
					    USE_MATERIALS_REQUESTED = "requested"
 | 
				
			||||||
 | 
					    USE_MATERIALS_DENIED = "denied"
 | 
				
			||||||
 | 
					    USE_MATERIALS_OTHER = "other"
 | 
				
			||||||
 | 
					    USE_MATERIALS_NOT_ASKED = "not_asked"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ALLOW_USE_MATERIALS_CHOICE = {
 | 
				
			||||||
 | 
					        USE_MATERIALS_ALLOWED: "Usage allowed",
 | 
				
			||||||
 | 
					        USE_MATERIALS_REQUESTED: "Usage requested",
 | 
				
			||||||
 | 
					        USE_MATERIALS_DENIED: "Usage denied",
 | 
				
			||||||
 | 
					        USE_MATERIALS_OTHER: "It's complicated",
 | 
				
			||||||
 | 
					        USE_MATERIALS_NOT_ASKED: "Not asked"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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,
 | 
				
			||||||
                                              default=AllowUseOfMaterialsChices.USE_MATERIALS_NOT_ASKED,
 | 
					                                              default=ALLOW_USE_MATERIALS_CHOICE[USE_MATERIALS_NOT_ASKED],
 | 
				
			||||||
                                              choices=AllowUseOfMaterialsChices.choices,
 | 
					                                              choices=ALLOW_USE_MATERIALS_CHOICE,
 | 
				
			||||||
                                              verbose_name=_('Erlaubt Nutzung von Inhalten'))
 | 
					                                              verbose_name=_('Erlaubt Nutzung von Inhalten'))
 | 
				
			||||||
    location_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation"), null=True, blank=True, )
 | 
					    location_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation"))
 | 
				
			||||||
    location = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True)
 | 
					    location = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True)
 | 
				
			||||||
    instagram = models.URLField(null=True, blank=True, verbose_name=_('Instagram Profil'))
 | 
					    instagram = models.URLField(null=True, blank=True, verbose_name=_('Instagram Profil'))
 | 
				
			||||||
    facebook = models.URLField(null=True, blank=True, verbose_name=_('Facebook Profil'))
 | 
					    facebook = models.URLField(null=True, blank=True, verbose_name=_('Facebook Profil'))
 | 
				
			||||||
@@ -161,7 +122,6 @@ class RescueOrganization(models.Model):
 | 
				
			|||||||
    website = models.URLField(null=True, blank=True, verbose_name=_('Website'))
 | 
					    website = models.URLField(null=True, blank=True, verbose_name=_('Website'))
 | 
				
			||||||
    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)
 | 
				
			||||||
    last_checked = models.DateTimeField(auto_now_add=True, verbose_name=_('Datum der letzten Prüfung'))
 | 
					 | 
				
			||||||
    internal_comment = models.TextField(verbose_name=_("Interner Kommentar"), null=True, blank=True, )
 | 
					    internal_comment = models.TextField(verbose_name=_("Interner Kommentar"), null=True, blank=True, )
 | 
				
			||||||
    description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))  # Markdown allowed
 | 
					    description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))  # Markdown allowed
 | 
				
			||||||
    external_object_identifier = models.CharField(max_length=200, null=True, blank=True,
 | 
					    external_object_identifier = models.CharField(max_length=200, null=True, blank=True,
 | 
				
			||||||
@@ -169,37 +129,6 @@ class RescueOrganization(models.Model):
 | 
				
			|||||||
    external_source_identifier = models.CharField(max_length=200, null=True, blank=True,
 | 
					    external_source_identifier = models.CharField(max_length=200, null=True, blank=True,
 | 
				
			||||||
                                                  choices=ExternalSourceChoices.choices,
 | 
					                                                  choices=ExternalSourceChoices.choices,
 | 
				
			||||||
                                                  verbose_name=_('External Source Identifier'))
 | 
					                                                  verbose_name=_('External Source Identifier'))
 | 
				
			||||||
    exclude_from_check = models.BooleanField(default=False, verbose_name=_('Von Prüfung ausschließen'),
 | 
					 | 
				
			||||||
                                             help_text=_("Organisation von der manuellen Überprüfung ausschließen, "
 | 
					 | 
				
			||||||
                                                         "z.B. weil Tiere nicht online geführt werden"))
 | 
					 | 
				
			||||||
    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)
 | 
					 | 
				
			||||||
    # 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:
 | 
					 | 
				
			||||||
        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):
 | 
					 | 
				
			||||||
        super().clean()
 | 
					 | 
				
			||||||
        if self.location is None and self.location_string is None:
 | 
					 | 
				
			||||||
            raise ValidationError(_('Location or Location String must be set'))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_absolute_url(self):
 | 
					    def get_absolute_url(self):
 | 
				
			||||||
        return reverse("rescue-organization-detail", args=[str(self.pk)])
 | 
					        return reverse("rescue-organization-detail", args=[str(self.pk)])
 | 
				
			||||||
@@ -208,29 +137,6 @@ 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:
 | 
				
			||||||
@@ -243,45 +149,7 @@ class RescueOrganization(models.Model):
 | 
				
			|||||||
        if self.description is None:
 | 
					        if self.description is None:
 | 
				
			||||||
            return ""
 | 
					            return ""
 | 
				
			||||||
        if len(self.description) > 200:
 | 
					        if len(self.description) > 200:
 | 
				
			||||||
            return self.description[:200] + _(f" ... [weiterlesen]({self.get_absolute_url()})")
 | 
					            return self.description[:200] + f" ... [weiterlesen]({self.get_absolute_url()})"
 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            return self.description
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def set_checked(self):
 | 
					 | 
				
			||||||
        self.last_checked = timezone.now()
 | 
					 | 
				
			||||||
        self.save()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def last_checked_hr(self):
 | 
					 | 
				
			||||||
        time_since_last_checked = timezone.now() - self.last_checked
 | 
					 | 
				
			||||||
        return time_since_as_hr_string(time_since_last_checked)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def species_urls(self):
 | 
					 | 
				
			||||||
        return SpeciesSpecificURL.objects.filter(rescue_organization=self)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def has_contact_data(self):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Returns true if at least one type of contact data is available.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        return self.instagram or self.facebook or self.website or self.phone_number or self.email or self.fediverse_profile
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def set_exclusion_from_checks(self):
 | 
					 | 
				
			||||||
        self.exclude_from_check = True
 | 
					 | 
				
			||||||
        self.save()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def child_organizations(self):
 | 
					 | 
				
			||||||
        return RescueOrganization.objects.filter(parent_org=self)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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
 | 
				
			||||||
@@ -329,17 +197,14 @@ 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_to_notify=self, read=False)
 | 
					        return BaseNotification.objects.filter(user=self, read=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_num_unread_notifications(self):
 | 
					    def get_num_unread_notifications(self):
 | 
				
			||||||
        return Notification.objects.filter(user_to_notify=self, read=False).count()
 | 
					        return BaseNotification.objects.filter(user=self, read=False).count()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def adoption_notices(self):
 | 
					    def adoption_notices(self):
 | 
				
			||||||
@@ -351,9 +216,8 @@ class User(AbstractUser):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Image(models.Model):
 | 
					class Image(models.Model):
 | 
				
			||||||
    image = models.ImageField(upload_to='images', verbose_name=_("Bild"), help_text=_("Wähle ein Bild aus"))
 | 
					    image = models.ImageField(upload_to='images')
 | 
				
			||||||
    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)
 | 
				
			||||||
@@ -361,18 +225,25 @@ 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
 | 
					
 | 
				
			||||||
    def as_base64(self):
 | 
					class Species(models.Model):
 | 
				
			||||||
        encoded_string = base64.b64encode(self.image.file.read())
 | 
					    """Model representing a species of animal."""
 | 
				
			||||||
        return encoded_string.decode("utf-8")
 | 
					    name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
 | 
				
			||||||
 | 
					                            verbose_name=_('Name'))
 | 
				
			||||||
 | 
					    updated_at = models.DateTimeField(auto_now=True)
 | 
				
			||||||
 | 
					    created_at = models.DateTimeField(auto_now_add=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __str__(self):
 | 
				
			||||||
 | 
					        """String for representing the Model object."""
 | 
				
			||||||
 | 
					        return self.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        verbose_name = _('Tierart')
 | 
				
			||||||
 | 
					        verbose_name_plural = _('Tierarten')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AdoptionNotice(models.Model):
 | 
					class AdoptionNotice(models.Model):
 | 
				
			||||||
@@ -380,35 +251,26 @@ 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):
 | 
				
			||||||
        return self.name
 | 
					        if not hasattr(self, 'adoptionnoticestatus'):
 | 
				
			||||||
 | 
					            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)
 | 
				
			||||||
    last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=timezone.now)
 | 
					    last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=timezone.now)
 | 
				
			||||||
    searching_since = models.DateField(verbose_name=_('Sucht nach einem Zuhause seit'))
 | 
					    searching_since = models.DateField(verbose_name=_('Sucht nach einem Zuhause seit'))
 | 
				
			||||||
    name = models.CharField(max_length=200, verbose_name=_('Titel der Vermittlung'))
 | 
					    name = models.CharField(max_length=200)
 | 
				
			||||||
    description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
 | 
					    description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
 | 
				
			||||||
    organization = models.ForeignKey(RescueOrganization, blank=True, null=True, on_delete=models.SET_NULL,
 | 
					    organization = models.ForeignKey(RescueOrganization, blank=True, null=True, on_delete=models.SET_NULL,
 | 
				
			||||||
                                     verbose_name=_('Organisation'))
 | 
					                                     verbose_name=_('Organisation'))
 | 
				
			||||||
    further_information = models.URLField(null=True, blank=True,
 | 
					    further_information = models.URLField(null=True, blank=True, verbose_name=_('Link zu mehr Informationen'))
 | 
				
			||||||
                                          verbose_name=_('Link zu mehr Informationen'),
 | 
					 | 
				
			||||||
                                          help_text=_(
 | 
					 | 
				
			||||||
                                              "Verlinke hier die Quelle der Vermittlung (z.B. die Website des "
 | 
					 | 
				
			||||||
                                              "Tierheims)"))
 | 
					 | 
				
			||||||
    group_only = models.BooleanField(default=False, verbose_name=_('Ausschließlich Gruppenadoption'))
 | 
					    group_only = models.BooleanField(default=False, verbose_name=_('Ausschließlich Gruppenadoption'))
 | 
				
			||||||
    photos = models.ManyToManyField(Image, blank=True)
 | 
					    photos = models.ManyToManyField(Image, blank=True)
 | 
				
			||||||
    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):
 | 
				
			||||||
@@ -421,20 +283,6 @@ class AdoptionNotice(models.Model):
 | 
				
			|||||||
            sexes.add(animal.sex)
 | 
					            sexes.add(animal.sex)
 | 
				
			||||||
        return sexes
 | 
					        return sexes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def num_per_sex(self):
 | 
					 | 
				
			||||||
        num_per_sex = dict()
 | 
					 | 
				
			||||||
        for sex in SexChoices:
 | 
					 | 
				
			||||||
            num_per_sex[sex] = self.animals.filter(sex=sex).count()
 | 
					 | 
				
			||||||
        return num_per_sex
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def 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
 | 
				
			||||||
@@ -464,30 +312,17 @@ class AdoptionNotice(models.Model):
 | 
				
			|||||||
        else:
 | 
					        else:
 | 
				
			||||||
            return self.location.latitude, self.location.longitude
 | 
					            return self.location.latitude, self.location.longitude
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _get_short_description(self, length: int) -> str:
 | 
					 | 
				
			||||||
        if self.description is None:
 | 
					 | 
				
			||||||
            return ""
 | 
					 | 
				
			||||||
        elif len(self.description) > length:
 | 
					 | 
				
			||||||
            return self.description[:length] + f" ... [weiterlesen]({self.get_absolute_url()})"
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            return self.description
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def description_short(self):
 | 
					    def description_short(self):
 | 
				
			||||||
        return self._get_short_description(200)
 | 
					        if self.description is None:
 | 
				
			||||||
 | 
					            return ""
 | 
				
			||||||
    @property
 | 
					        if len(self.description) > 200:
 | 
				
			||||||
    def description_100_short(self):
 | 
					            return self.description[:200] + f" ... [weiterlesen]({self.get_absolute_url()})"
 | 
				
			||||||
        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."""
 | 
				
			||||||
        return reverse('adoption-notice-detail', args=[str(self.id)])
 | 
					        return reverse('adoption-notice-detail', args=[str(self.id)])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_full_url(self):
 | 
					 | 
				
			||||||
        """Returns the url including protocol and domain"""
 | 
					 | 
				
			||||||
        return f"{base_url}{self.get_absolute_url()}"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_report_url(self):
 | 
					    def get_report_url(self):
 | 
				
			||||||
        """Returns the url to report an adoption notice."""
 | 
					        """Returns the url to report an adoption notice."""
 | 
				
			||||||
        return reverse('report-adoption-notice', args=[str(self.id)])
 | 
					        return reverse('report-adoption-notice', args=[str(self.id)])
 | 
				
			||||||
@@ -538,53 +373,149 @@ 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
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return geo.object_in_distance(self, position, max_distance, unknown_true)
 | 
					        if unknown_true and self.position is None:
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					        distance = geo.calculate_distance_between_coordinates(self.position, position)
 | 
				
			||||||
    def _values_of(list_of_enums):
 | 
					        return distance < max_distance
 | 
				
			||||||
        return list(map(lambda x: x[0], list_of_enums))
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def link_to_more_information(self):
 | 
				
			||||||
 | 
					        from urllib.parse import urlparse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        domain = urlparse(self.further_information).netloc
 | 
				
			||||||
 | 
					        return f"<a href='{self.further_information}'>{domain}</a>"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def is_active(self):
 | 
					    def is_active(self):
 | 
				
			||||||
        return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.Active.choices)
 | 
					        if not hasattr(self, 'adoptionnoticestatus'):
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        return self.adoptionnoticestatus.is_active
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def is_disabled(self):
 | 
					    def is_disabled_unchecked(self):
 | 
				
			||||||
        return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.Disabled.choices)
 | 
					        if not hasattr(self, 'adoptionnoticestatus'):
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        return self.adoptionnoticestatus.is_disabled_unchecked
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    def set_closed(self):
 | 
				
			||||||
    def is_closed(self):
 | 
					        self.last_checked = timezone.now()
 | 
				
			||||||
        return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.Closed.choices)
 | 
					        self.save()
 | 
				
			||||||
 | 
					        self.adoptionnoticestatus.set_closed()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    def set_active(self):
 | 
				
			||||||
    def is_awaiting_action(self):
 | 
					        self.last_checked = timezone.now()
 | 
				
			||||||
        return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.AwaitingAction.choices)
 | 
					        self.save()
 | 
				
			||||||
 | 
					        if not hasattr(self, 'adoptionnoticestatus'):
 | 
				
			||||||
    @property
 | 
					            AdoptionNoticeStatus.create_other(self)
 | 
				
			||||||
    def status_description(self):
 | 
					        self.adoptionnoticestatus.set_active()
 | 
				
			||||||
        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}"
 | 
				
			||||||
            text = _("Die folgende Vermittlung wurde deaktiviert: ") + f"[{self.name}]({self.get_absolute_url()})"
 | 
					            text = _("Die folgende Vermittlung wurde deaktiviert: ") + f"[{self.name}]({self.get_absolute_url()})"
 | 
				
			||||||
            Notification.objects.create(user_to_notify=subscription.owner,
 | 
					            BaseNotification.objects.create(user=subscription.owner, text=text, title=notification_title)
 | 
				
			||||||
                                        notification_type=NotificationTypeChoices.AN_WAS_DEACTIVATED,
 | 
					 | 
				
			||||||
                                        adoption_notice=self,
 | 
					 | 
				
			||||||
                                        text=text,
 | 
					 | 
				
			||||||
                                        title=notification_title)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def last_posted(self, platform=None):
 | 
					
 | 
				
			||||||
        if platform is None:
 | 
					class AdoptionNoticeStatus(models.Model):
 | 
				
			||||||
            last_post = SocialMediaPost.objects.filter(adoption_notice=self).order_by('-created_at').first()
 | 
					    """
 | 
				
			||||||
        else:
 | 
					    The major status indicates a general state of an adoption notice
 | 
				
			||||||
            last_post = SocialMediaPost.objects.filter(adoption_notice=self, platform=platform).order_by(
 | 
					    whereas the minor status is used for reporting
 | 
				
			||||||
                '-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):
 | 
				
			||||||
@@ -605,19 +536,14 @@ 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, verbose_name=_('Name'))
 | 
					    name = models.CharField(max_length=200)
 | 
				
			||||||
    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, verbose_name=_("Fotos"))
 | 
					    photos = models.ManyToManyField(Image, blank=True)
 | 
				
			||||||
    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)
 | 
				
			||||||
@@ -675,17 +601,12 @@ 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, verbose_name=_("Geschlecht"))
 | 
					    sex = models.CharField(max_length=20, choices=SexChoicesWithAll.choices)
 | 
				
			||||||
    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, 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"))
 | 
					    created_at = models.DateTimeField(auto_now_add=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        if self.location and self.max_distance:
 | 
					        if self.location and self.max_distance:
 | 
				
			||||||
@@ -698,24 +619,15 @@ 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(verbose_name=_("Regeltext"))
 | 
					    rule_text = models.TextField()
 | 
				
			||||||
    language = models.ForeignKey(Language, on_delete=models.PROTECT, verbose_name=_("Sprache"))
 | 
					    language = models.ForeignKey(Language, on_delete=models.PROTECT)
 | 
				
			||||||
    # 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)
 | 
				
			||||||
                                       verbose_name=_("Regel-ID"),
 | 
					    updated_at = models.DateTimeField(auto_now=True)
 | 
				
			||||||
                                       help_text=_("Ein eindeutiger Identifikator der Regel. Ein Regelobjekt "
 | 
					    created_at = models.DateTimeField(auto_now_add=True)
 | 
				
			||||||
                                                   "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
 | 
				
			||||||
@@ -723,8 +635,7 @@ class Rule(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class Report(models.Model):
 | 
					class Report(models.Model):
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _("Meldung")
 | 
					        permissions = []
 | 
				
			||||||
        verbose_name_plural = _("Meldungen")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ACTION_TAKEN = "action taken"
 | 
					    ACTION_TAKEN = "action taken"
 | 
				
			||||||
    NO_ACTION_TAKEN = "no action taken"
 | 
					    NO_ACTION_TAKEN = "no action taken"
 | 
				
			||||||
@@ -739,8 +650,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"))
 | 
				
			||||||
    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"))
 | 
					    updated_at = models.DateTimeField(auto_now=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return f"[{self.status}]: {self.user_comment:.20}"
 | 
					        return f"[{self.status}]: {self.user_comment:.20}"
 | 
				
			||||||
@@ -749,40 +660,12 @@ 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()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_moderation_actions(self):
 | 
					    def get_moderation_actions(self):
 | 
				
			||||||
        return ModerationAction.objects.filter(report=self)
 | 
					        return ModerationAction.objects.filter(report=self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def reported_content(self):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Dynamically fetch the reported content based on subclass.
 | 
					 | 
				
			||||||
        The alternative would be to use the ContentType framework:
 | 
					 | 
				
			||||||
        https://docs.djangoproject.com/en/5.1/ref/contrib/contenttypes/
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if hasattr(self, "reportadoptionnotice"):
 | 
					 | 
				
			||||||
            return self.reportadoptionnotice.adoption_notice
 | 
					 | 
				
			||||||
        elif hasattr(self, "reportcomment"):
 | 
					 | 
				
			||||||
            return self.reportcomment.reported_comment
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def reported_content_url(self):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Same as reported_content, just for url
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if hasattr(self, "reportadoptionnotice"):
 | 
					 | 
				
			||||||
            print(self.reportadoptionnotice.adoption_notice.get_absolute_url)
 | 
					 | 
				
			||||||
            return self.reportadoptionnotice.adoption_notice.get_absolute_url
 | 
					 | 
				
			||||||
        elif hasattr(self, "reportcomment"):
 | 
					 | 
				
			||||||
            return self.reportcomment.reported_comment.get_absolute_url
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReportAdoptionNotice(Report):
 | 
					class ReportAdoptionNotice(Report):
 | 
				
			||||||
    adoption_notice = models.ForeignKey("AdoptionNotice", on_delete=models.CASCADE)
 | 
					    adoption_notice = models.ForeignKey("AdoptionNotice", on_delete=models.CASCADE)
 | 
				
			||||||
@@ -791,9 +674,6 @@ class ReportAdoptionNotice(Report):
 | 
				
			|||||||
    def reported_content(self):
 | 
					    def reported_content(self):
 | 
				
			||||||
        return self.adoption_notice
 | 
					        return self.adoption_notice
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					 | 
				
			||||||
        return f"Report der Vermittlung {self.adoption_notice}"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReportComment(Report):
 | 
					class ReportComment(Report):
 | 
				
			||||||
    reported_comment = models.ForeignKey("Comment", on_delete=models.CASCADE)
 | 
					    reported_comment = models.ForeignKey("Comment", on_delete=models.CASCADE)
 | 
				
			||||||
@@ -804,10 +684,6 @@ 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"
 | 
				
			||||||
@@ -844,7 +720,7 @@ class Text(models.Model):
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    Base class to store markdown content
 | 
					    Base class to store markdown content
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    title = models.CharField(max_length=100, verbose_name=_("Titel"))
 | 
					    title = models.CharField(max_length=100)
 | 
				
			||||||
    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)
 | 
				
			||||||
@@ -872,11 +748,6 @@ 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)
 | 
				
			||||||
@@ -926,15 +797,10 @@ 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=_('Vermittlung'))
 | 
					    adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
 | 
				
			||||||
    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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -949,63 +815,46 @@ class Comment(models.Model):
 | 
				
			|||||||
        return self.adoption_notice.get_absolute_url()
 | 
					        return self.adoption_notice.get_absolute_url()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Notification(models.Model):
 | 
					class BaseNotification(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)
 | 
				
			||||||
    notification_type = models.CharField(max_length=200,
 | 
					    read_at = models.DateTimeField(blank=True, null=True, verbose_name=_("Gelesen am"))
 | 
				
			||||||
                                         choices=NotificationTypeChoices.choices,
 | 
					 | 
				
			||||||
                                         verbose_name=_('Benachrichtigungsgrund'))
 | 
					 | 
				
			||||||
    user_to_notify = models.ForeignKey(User,
 | 
					 | 
				
			||||||
                                       on_delete=models.CASCADE,
 | 
					 | 
				
			||||||
                                       verbose_name=_('Empfänger*in'),
 | 
					 | 
				
			||||||
                                       help_text=_("Useraccount der benachrichtigt wird"),
 | 
					 | 
				
			||||||
                                       related_name='user')
 | 
					 | 
				
			||||||
    title = models.CharField(max_length=100, verbose_name=_("Titel"))
 | 
					    title = models.CharField(max_length=100, verbose_name=_("Titel"))
 | 
				
			||||||
    text = models.TextField(verbose_name="Inhalt")
 | 
					    text = models.TextField(verbose_name="Inhalt")
 | 
				
			||||||
 | 
					    user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
 | 
				
			||||||
    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'))
 | 
					 | 
				
			||||||
    adoption_notice = models.ForeignKey(AdoptionNotice, blank=True, null=True, on_delete=models.CASCADE,
 | 
					 | 
				
			||||||
                                        verbose_name=_('Vermittlung'))
 | 
					 | 
				
			||||||
    user_related = models.ForeignKey(User,
 | 
					 | 
				
			||||||
                                     blank=True, null=True,
 | 
					 | 
				
			||||||
                                     on_delete=models.CASCADE, verbose_name=_('Verwandter Useraccount'),
 | 
					 | 
				
			||||||
                                     help_text=_("Useraccount auf den sich die Benachrichtigung bezieht."))
 | 
					 | 
				
			||||||
    report = models.ForeignKey(Report,
 | 
					 | 
				
			||||||
                               blank=True, null=True,
 | 
					 | 
				
			||||||
                               on_delete=models.CASCADE,
 | 
					 | 
				
			||||||
                               verbose_name=_('Report'),
 | 
					 | 
				
			||||||
                               help_text=_("Report auf den sich die Benachrichtigung bezieht."))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return f"[{self.user_to_notify}] {self.title} ({self.created_at})"
 | 
					        return f"[{self.user}] {self.title} ({self.created_at})"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_absolute_url(self):
 | 
					    def get_absolute_url(self):
 | 
				
			||||||
        self.user_to_notify.get_notifications_url()
 | 
					        self.user.get_notifications_url()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def mark_read(self):
 | 
					    def mark_read(self):
 | 
				
			||||||
        self.read = True
 | 
					        self.read = True
 | 
				
			||||||
        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 CommentNotification(BaseNotification):
 | 
				
			||||||
 | 
					    comment = models.ForeignKey(Comment, on_delete=models.CASCADE, verbose_name=_('Antwort'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def url(self):
 | 
				
			||||||
 | 
					        return self.comment.get_absolute_url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AdoptionNoticeNotification(BaseNotification):
 | 
				
			||||||
 | 
					    adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def url(self):
 | 
				
			||||||
 | 
					        return self.adoption_notice.get_absolute_url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Subscriptions(models.Model):
 | 
					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=_('Vermittlung'),
 | 
					    adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
 | 
				
			||||||
                                        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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1031,16 +880,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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def ___str__(self):
 | 
				
			||||||
        return f"[{self.key}] - {self.timestamp.strftime('%H:%M:%S %d-%m-%Y ')} - {self.data}"
 | 
					        return f"[{self.key}] - {self.timestamp.strftime('%H:%M:%S %d-%m-%Y ')} - {self.data}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1048,33 +892,7 @@ 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,
 | 
					    rescues_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 PlatformChoices(models.TextChoices):
 | 
					 | 
				
			||||||
    FEDIVERSE = "fediverse", _("Fediverse")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class SocialMediaPost(models.Model):
 | 
					 | 
				
			||||||
    created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
 | 
					 | 
				
			||||||
    platform = models.CharField(max_length=255, verbose_name=_("Social Media Platform"),
 | 
					 | 
				
			||||||
                                choices=PlatformChoices.choices)
 | 
					 | 
				
			||||||
    adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('Vermittlung'))
 | 
					 | 
				
			||||||
    url = models.URLField(verbose_name=_("URL"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @staticmethod
 | 
					 | 
				
			||||||
    def get_an_to_post():
 | 
					 | 
				
			||||||
        adoption_notices_without_post = AdoptionNotice.objects.filter(socialmediapost__isnull=True,
 | 
					 | 
				
			||||||
                                                                      adoption_notice_status__in=AdoptionNoticeStatusChoices.Active.values)
 | 
					 | 
				
			||||||
        return adoption_notices_without_post.first()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __str__(self):
 | 
					 | 
				
			||||||
        return f"{self.platform} - {self.adoption_notice}"
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,23 +1,19 @@
 | 
				
			|||||||
from django.db.models.signals import post_save
 | 
					from django.db.models.signals import post_save
 | 
				
			||||||
from django.dispatch import receiver
 | 
					from django.dispatch import receiver
 | 
				
			||||||
from fellchensammlung.models import Notification, User, TrustLevel, RescueOrganization, \
 | 
					from fellchensammlung.models import BaseNotification, CommentNotification, User, TrustLevel
 | 
				
			||||||
    NotificationTypeChoices
 | 
					 | 
				
			||||||
from .tasks import task_send_notification_email
 | 
					from .tasks import task_send_notification_email
 | 
				
			||||||
from notfellchen.settings import host
 | 
					from notfellchen.settings import host
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@receiver(post_save, sender=Notification)
 | 
					@receiver(post_save, sender=CommentNotification)
 | 
				
			||||||
def base_notification_receiver(sender, instance: Notification, created: bool, **kwargs):
 | 
					def comment_notification_receiver(sender, instance: BaseNotification, created: bool, **kwargs):
 | 
				
			||||||
    if not created or not instance.user_to_notify.email_notifications:
 | 
					    base_notification_receiver(sender, instance, created, **kwargs)
 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        task_send_notification_email.delay(instance.pk)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@receiver(post_save, sender=RescueOrganization)
 | 
					@receiver(post_save, sender=BaseNotification)
 | 
				
			||||||
def rescue_org_receiver(sender, instance: RescueOrganization, created: bool, **kwargs):
 | 
					def base_notification_receiver(sender, instance: BaseNotification, created: bool, **kwargs):
 | 
				
			||||||
    if instance.location:
 | 
					    if not created or not instance.user.email_notifications:
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        task_send_notification_email.delay(instance.pk)
 | 
					        task_send_notification_email.delay(instance.pk)
 | 
				
			||||||
@@ -37,9 +33,5 @@ def notification_new_user(sender, instance: User, created: bool, **kwargs):
 | 
				
			|||||||
    link_text = f"Um alle Details zu sehen, geh bitte auf: {user_url}"
 | 
					    link_text = f"Um alle Details zu sehen, geh bitte auf: {user_url}"
 | 
				
			||||||
    body_text = new_user_text + user_detail_text + link_text
 | 
					    body_text = new_user_text + user_detail_text + link_text
 | 
				
			||||||
    for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
 | 
					    for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
 | 
				
			||||||
        notification = Notification.objects.create(title=subject,
 | 
					        notification = BaseNotification.objects.create(title=subject, text=body_text, user=moderator)
 | 
				
			||||||
                                                   text=body_text,
 | 
					 | 
				
			||||||
                                                   notification_type=NotificationTypeChoices.NEW_USER,
 | 
					 | 
				
			||||||
                                                   user_to_notify=moderator,
 | 
					 | 
				
			||||||
                                                   user_related=instance)
 | 
					 | 
				
			||||||
        notification.save()
 | 
					        notification.save()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,29 +0,0 @@
 | 
				
			|||||||
from django.utils.html import strip_tags
 | 
					 | 
				
			||||||
from django_registration.backends.activation.views import RegistrationView
 | 
					 | 
				
			||||||
from django.core.mail import EmailMultiAlternatives
 | 
					 | 
				
			||||||
from django.template.loader import render_to_string
 | 
					 | 
				
			||||||
from django.conf import settings
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class HTMLMailRegistrationView(RegistrationView):
 | 
					 | 
				
			||||||
    def send_activation_email(self, user):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        overwrites the function in django registration
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        activation_key = self.get_activation_key(user)
 | 
					 | 
				
			||||||
        context = self.get_email_context(activation_key)
 | 
					 | 
				
			||||||
        context["user"] = user
 | 
					 | 
				
			||||||
        subject = render_to_string(
 | 
					 | 
				
			||||||
            template_name=self.email_subject_template,
 | 
					 | 
				
			||||||
            context=context,
 | 
					 | 
				
			||||||
            request=self.request,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        # Force subject to a single line to avoid header-injection issues.
 | 
					 | 
				
			||||||
        subject = "".join(subject.splitlines())
 | 
					 | 
				
			||||||
        message = render_to_string(
 | 
					 | 
				
			||||||
            template_name=self.email_body_template,
 | 
					 | 
				
			||||||
            context=context,
 | 
					 | 
				
			||||||
            request=self.request,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        plain_message = strip_tags(message)
 | 
					 | 
				
			||||||
        user.email_user(subject, plain_message, settings.DEFAULT_FROM_EMAIL, html_message=message)
 | 
					 | 
				
			||||||
@@ -1,45 +0,0 @@
 | 
				
			|||||||
from django.contrib.sitemaps import Sitemap
 | 
					 | 
				
			||||||
from django.urls import reverse
 | 
					 | 
				
			||||||
from .models import AdoptionNotice, RescueOrganization, ImportantLocation, Animal
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class StaticViewSitemap(Sitemap):
 | 
					 | 
				
			||||||
    priority = 0.8
 | 
					 | 
				
			||||||
    changefreq = "weekly"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def items(self):
 | 
					 | 
				
			||||||
        return ["index", "search", "map", "about", "rescue-organizations", "buying", "imprint", "terms-of-service",
 | 
					 | 
				
			||||||
                "privacy"]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def location(self, item):
 | 
					 | 
				
			||||||
        return reverse(item)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class AdoptionNoticeSitemap(Sitemap):
 | 
					 | 
				
			||||||
    priority = 0.5
 | 
					 | 
				
			||||||
    changefreq = "daily"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def items(self):
 | 
					 | 
				
			||||||
        return AdoptionNotice.get_active_ANs()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def lastmod(self, obj):
 | 
					 | 
				
			||||||
        return obj.updated_at
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class RescueOrganizationSitemap(Sitemap):
 | 
					 | 
				
			||||||
    priority = 0.3
 | 
					 | 
				
			||||||
    changefreq = "weekly"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def items(self):
 | 
					 | 
				
			||||||
        return RescueOrganization.objects.all()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def lastmod(self, obj):
 | 
					 | 
				
			||||||
        return obj.updated_at
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class SearchSitemap(Sitemap):
 | 
					 | 
				
			||||||
    priority = 0.5
 | 
					 | 
				
			||||||
    chanfreq = "daily"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def items(self):
 | 
					 | 
				
			||||||
        return ImportantLocation.objects.all()
 | 
					 | 
				
			||||||
@@ -1,363 +0,0 @@
 | 
				
			|||||||
$primary: #6CD4FF;
 | 
					 | 
				
			||||||
$link: #292a2c;
 | 
					 | 
				
			||||||
$grey-light: #c4c6ce;
 | 
					 | 
				
			||||||
$grey-dark: #262728;
 | 
					 | 
				
			||||||
$confirm: hsl(133deg, 100%, calc(41% + 0%));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Path to Bulma's sass folder
 | 
					 | 
				
			||||||
@use "bulma/sass" with (
 | 
					 | 
				
			||||||
  $family-primary: '"Nunito", sans-serif',
 | 
					 | 
				
			||||||
  $grey-dark: $grey-dark,
 | 
					 | 
				
			||||||
  $grey-light: $grey-light,
 | 
					 | 
				
			||||||
  $primary: $primary,
 | 
					 | 
				
			||||||
  $link: $link,
 | 
					 | 
				
			||||||
  $control-border-width: 2px,
 | 
					 | 
				
			||||||
  $input-shadow: none
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@use "bulma/sass/utilities/css-variables" as cv;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@include cv.system-theme($name: "dark") {
 | 
					 | 
				
			||||||
  .navbar-item > img {
 | 
					 | 
				
			||||||
    background-color: $grey-light !important;
 | 
					 | 
				
			||||||
    border-radius: 5px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  .card-header {
 | 
					 | 
				
			||||||
    background-color: $grey-dark;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  a.card-footer-item.is-danger {
 | 
					 | 
				
			||||||
    color: black;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  .tag {
 | 
					 | 
				
			||||||
    color: $grey-dark;
 | 
					 | 
				
			||||||
    background-color: $grey-light;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// General Styles
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.main-content {
 | 
					 | 
				
			||||||
  margin: auto;
 | 
					 | 
				
			||||||
  max-width: 80em;
 | 
					 | 
				
			||||||
  padding: 10px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
p > a {
 | 
					 | 
				
			||||||
  text-decoration: underline;
 | 
					 | 
				
			||||||
  word-break: break-all;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
p > a.button {
 | 
					 | 
				
			||||||
  text-decoration: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Cards
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.card-header {
 | 
					 | 
				
			||||||
  background-color: $primary;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Search form suggestion dropdown
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#location-result-list {
 | 
					 | 
				
			||||||
  display: inline; //ensures that the dropdown is not restricted in width WTF
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.result-item {
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.result-item:hover {
 | 
					 | 
				
			||||||
  background-color: #b2aaaa;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Toggle switch
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.toggle-switch {
 | 
					 | 
				
			||||||
  display: inline-block;
 | 
					 | 
				
			||||||
  background: #ccc;
 | 
					 | 
				
			||||||
  border-radius: 16px;
 | 
					 | 
				
			||||||
  width: 58px;
 | 
					 | 
				
			||||||
  height: 32px;
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
  vertical-align: middle;
 | 
					 | 
				
			||||||
  transition: background 0.25s;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.toggle-switch:before, .toggle-switch:after {
 | 
					 | 
				
			||||||
  content: "";
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.toggle-switch:before {
 | 
					 | 
				
			||||||
  display: block;
 | 
					 | 
				
			||||||
  background: linear-gradient(to bottom, #fff 0%, #eee 100%);
 | 
					 | 
				
			||||||
  border-radius: 50%;
 | 
					 | 
				
			||||||
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
 | 
					 | 
				
			||||||
  width: 24px;
 | 
					 | 
				
			||||||
  height: 24px;
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  top: 4px;
 | 
					 | 
				
			||||||
  left: 4px;
 | 
					 | 
				
			||||||
  transition: left 0.25s;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.toggle:hover .toggle-switch:before {
 | 
					 | 
				
			||||||
  background: linear-gradient(to bottom, #fff 0%, #fff 100%);
 | 
					 | 
				
			||||||
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.checked + .toggle-switch {
 | 
					 | 
				
			||||||
  background: #56c080;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.checked + .toggle-switch:before {
 | 
					 | 
				
			||||||
  left: 30px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.toggle-checkbox {
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  visibility: hidden;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.slider-label {
 | 
					 | 
				
			||||||
  margin-left: 5px;
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
  top: 2px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Button in card footer
 | 
					 | 
				
			||||||
.card-footer {
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.card-footer .card-footer-item.is-confirm {
 | 
					 | 
				
			||||||
  background-color: $confirm;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.card-footer .card-footer-item.is-confirm:hover {
 | 
					 | 
				
			||||||
  filter: brightness(0.9);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.card-footer .card-footer-item.is-danger {
 | 
					 | 
				
			||||||
  background-color: sass.$danger;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.card-footer .card-footer-item.is-danger:hover {
 | 
					 | 
				
			||||||
  filter: brightness(0.9);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.card-footer .card-footer-item.is-warning {
 | 
					 | 
				
			||||||
  background-color: sass.$warning;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.card-footer .card-footer-item.is-warning:hover {
 | 
					 | 
				
			||||||
  filter: brightness(0.9);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*******/
 | 
					 | 
				
			||||||
/* MAP */
 | 
					 | 
				
			||||||
/*******/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.map {
 | 
					 | 
				
			||||||
  border-radius: 8px;
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  height: 100%
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.marker {
 | 
					 | 
				
			||||||
  background-image: url('../img/logo_transparent.png');
 | 
					 | 
				
			||||||
  background-size: cover;
 | 
					 | 
				
			||||||
  width: 50px;
 | 
					 | 
				
			||||||
  height: 50px;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.animal-shelter-marker {
 | 
					 | 
				
			||||||
  background-image: url('../img/animal_shelter.png');
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.map-in-content #map {
 | 
					 | 
				
			||||||
  max-height: 500px;
 | 
					 | 
				
			||||||
  width: 90%;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@media only screen and (min-width: 768px) {
 | 
					 | 
				
			||||||
  .maplibregl-popup {
 | 
					 | 
				
			||||||
    max-width: 280px !important;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@media only screen and (max-width: 768px) {
 | 
					 | 
				
			||||||
  .maplibregl-popup {
 | 
					 | 
				
			||||||
    max-width: 150px !important;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.maplibregl-popup-close-button {
 | 
					 | 
				
			||||||
  all: unset; /* Remove all inherited styles */
 | 
					 | 
				
			||||||
  font-size: 1.2rem;
 | 
					 | 
				
			||||||
  background: none;
 | 
					 | 
				
			||||||
  border: none;
 | 
					 | 
				
			||||||
  color: black;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  top: 0.25rem;
 | 
					 | 
				
			||||||
  right: 0.5rem;
 | 
					 | 
				
			||||||
  padding: 0.25rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.popup-content {
 | 
					 | 
				
			||||||
  line-height: 1.4;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*****
 | 
					 | 
				
			||||||
IMAGES
 | 
					 | 
				
			||||||
 *****/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.gallery .main-photo img {
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  height: 150px;
 | 
					 | 
				
			||||||
  object-fit: cover; /* Crops the images */
 | 
					 | 
				
			||||||
  border-radius: 6px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.thumbnail-row {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  gap: 10px;
 | 
					 | 
				
			||||||
  margin-top: 10px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.thumbnail img {
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  height: 70px;
 | 
					 | 
				
			||||||
  object-fit: cover; /* Crops the images */
 | 
					 | 
				
			||||||
  border-radius: 4px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* Ensure each thumbnail takes equal width */
 | 
					 | 
				
			||||||
.thumbnail {
 | 
					 | 
				
			||||||
  flex: 1;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
AN Cards
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.an-card {
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  height: 100%;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Fonts
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@font-face {
 | 
					 | 
				
			||||||
  font-family: 'Nunito';
 | 
					 | 
				
			||||||
  font-style: normal;
 | 
					 | 
				
			||||||
  font-weight: 400;
 | 
					 | 
				
			||||||
  src: url(../fonts/nunito.woff2) format('woff2');
 | 
					 | 
				
			||||||
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.new-animal-ad fieldset {
 | 
					 | 
				
			||||||
  border-top: 4px solid var(--bulma-text-weak);
 | 
					 | 
				
			||||||
  margin-top: 2em;
 | 
					 | 
				
			||||||
  padding-top: 1em;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.new-animal-ad * {
 | 
					 | 
				
			||||||
  transition: all ease 0.5s;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.new-animal-ad .open {
 | 
					 | 
				
			||||||
  display: block;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.new-animal-ad .closed {
 | 
					 | 
				
			||||||
  display: none;
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.new-animal-ad legend {
 | 
					 | 
				
			||||||
  font-weight: bold;
 | 
					 | 
				
			||||||
  padding-right: 0.2em;
 | 
					 | 
				
			||||||
  color: var(--bulma-label-color);
 | 
					 | 
				
			||||||
  font-size: 130%;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.feedback-backdrop {
 | 
					 | 
				
			||||||
  position: fixed;
 | 
					 | 
				
			||||||
  top: 0;
 | 
					 | 
				
			||||||
  bottom: 0;
 | 
					 | 
				
			||||||
  left: 0;
 | 
					 | 
				
			||||||
  right: 0;
 | 
					 | 
				
			||||||
  background-color: rgba(0, 0, 0, 0.4);
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  justify-content: center;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.feedback-add-new {
 | 
					 | 
				
			||||||
  width: 40ch;
 | 
					 | 
				
			||||||
  min-height: 40ch;
 | 
					 | 
				
			||||||
  padding: 1.5em;
 | 
					 | 
				
			||||||
  background-color: var(--bulma-info-on-scheme);
 | 
					 | 
				
			||||||
  color: black;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.feedback-add-new.error {
 | 
					 | 
				
			||||||
  background-color: var(--bulma-danger-on-scheme);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.feedback-add-new.success {
 | 
					 | 
				
			||||||
  background-color: var(--bulma-success-on-scheme);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.notification-container {
 | 
					 | 
				
			||||||
    display: inline-block;
 | 
					 | 
				
			||||||
    position: relative;
 | 
					 | 
				
			||||||
    padding: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.notification-label {
 | 
					 | 
				
			||||||
  padding: 2px 5px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* Make the badge float in the top right corner of the button */
 | 
					 | 
				
			||||||
.notification-badge {
 | 
					 | 
				
			||||||
    background-color: #fa3e3e;
 | 
					 | 
				
			||||||
    border-radius: 2px;
 | 
					 | 
				
			||||||
    color: white;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    padding: 1px 3px;
 | 
					 | 
				
			||||||
    font-size: 8px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    position: absolute; /* Position the badge within the relatively positioned button */
 | 
					 | 
				
			||||||
    top: 0;
 | 
					 | 
				
			||||||
    right: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* Embedding Specifics */
 | 
					 | 
				
			||||||
.embed-main-content {
 | 
					 | 
				
			||||||
  padding: 20px 10px 20px 10px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// FLOATING BUTTON
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.floating {
 | 
					 | 
				
			||||||
  position: fixed;
 | 
					 | 
				
			||||||
  border-radius: 0.3rem;
 | 
					 | 
				
			||||||
  bottom: 4.5rem;
 | 
					 | 
				
			||||||
  right: 1rem;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,420 +0,0 @@
 | 
				
			|||||||
/*! PhotoSwipe main CSS by Dmytro Semenov | photoswipe.com */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp {
 | 
					 | 
				
			||||||
  --pswp-bg: #000;
 | 
					 | 
				
			||||||
  --pswp-placeholder-bg: #222;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  --pswp-root-z-index: 100000;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  --pswp-preloader-color: rgba(79, 79, 79, 0.4);
 | 
					 | 
				
			||||||
  --pswp-preloader-color-secondary: rgba(255, 255, 255, 0.9);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /* defined via js:
 | 
					 | 
				
			||||||
  --pswp-transition-duration: 333ms; */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  --pswp-icon-color: #fff;
 | 
					 | 
				
			||||||
  --pswp-icon-color-secondary: #4f4f4f;
 | 
					 | 
				
			||||||
  --pswp-icon-stroke-color: #4f4f4f;
 | 
					 | 
				
			||||||
  --pswp-icon-stroke-width: 2px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  --pswp-error-text-color: var(--pswp-icon-color);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
	Styles for basic PhotoSwipe (pswp) functionality (sliding area, open/close transitions)
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp {
 | 
					 | 
				
			||||||
	position: fixed;
 | 
					 | 
				
			||||||
	z-index: var(--pswp-root-z-index);
 | 
					 | 
				
			||||||
	display: none;
 | 
					 | 
				
			||||||
	touch-action: none;
 | 
					 | 
				
			||||||
	outline: 0;
 | 
					 | 
				
			||||||
	opacity: 0.003;
 | 
					 | 
				
			||||||
	contain: layout style size;
 | 
					 | 
				
			||||||
	-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* Prevents focus outline on the root element,
 | 
					 | 
				
			||||||
  (it may be focused initially) */
 | 
					 | 
				
			||||||
.pswp:focus {
 | 
					 | 
				
			||||||
  outline: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp * {
 | 
					 | 
				
			||||||
  box-sizing: border-box;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp img {
 | 
					 | 
				
			||||||
  max-width: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp--open {
 | 
					 | 
				
			||||||
	display: block;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp,
 | 
					 | 
				
			||||||
.pswp__bg {
 | 
					 | 
				
			||||||
	transform: translateZ(0);
 | 
					 | 
				
			||||||
	will-change: opacity;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp__bg {
 | 
					 | 
				
			||||||
  opacity: 0.005;
 | 
					 | 
				
			||||||
	background: var(--pswp-bg);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp,
 | 
					 | 
				
			||||||
.pswp__scroll-wrap {
 | 
					 | 
				
			||||||
	overflow: hidden;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp,
 | 
					 | 
				
			||||||
.pswp__scroll-wrap,
 | 
					 | 
				
			||||||
.pswp__bg,
 | 
					 | 
				
			||||||
.pswp__container,
 | 
					 | 
				
			||||||
.pswp__item,
 | 
					 | 
				
			||||||
.pswp__content,
 | 
					 | 
				
			||||||
.pswp__img,
 | 
					 | 
				
			||||||
.pswp__zoom-wrap {
 | 
					 | 
				
			||||||
	position: absolute;
 | 
					 | 
				
			||||||
	top: 0;
 | 
					 | 
				
			||||||
	left: 0;
 | 
					 | 
				
			||||||
	width: 100%;
 | 
					 | 
				
			||||||
	height: 100%;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp {
 | 
					 | 
				
			||||||
	position: fixed;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp__img,
 | 
					 | 
				
			||||||
.pswp__zoom-wrap {
 | 
					 | 
				
			||||||
	width: auto;
 | 
					 | 
				
			||||||
	height: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp--click-to-zoom.pswp--zoom-allowed .pswp__img {
 | 
					 | 
				
			||||||
	cursor: -webkit-zoom-in;
 | 
					 | 
				
			||||||
	cursor: -moz-zoom-in;
 | 
					 | 
				
			||||||
	cursor: zoom-in;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp--click-to-zoom.pswp--zoomed-in .pswp__img {
 | 
					 | 
				
			||||||
	cursor: move;
 | 
					 | 
				
			||||||
	cursor: -webkit-grab;
 | 
					 | 
				
			||||||
	cursor: -moz-grab;
 | 
					 | 
				
			||||||
	cursor: grab;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp--click-to-zoom.pswp--zoomed-in .pswp__img:active {
 | 
					 | 
				
			||||||
  cursor: -webkit-grabbing;
 | 
					 | 
				
			||||||
  cursor: -moz-grabbing;
 | 
					 | 
				
			||||||
  cursor: grabbing;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* :active to override grabbing cursor */
 | 
					 | 
				
			||||||
.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img,
 | 
					 | 
				
			||||||
.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img:active,
 | 
					 | 
				
			||||||
.pswp__img {
 | 
					 | 
				
			||||||
	cursor: -webkit-zoom-out;
 | 
					 | 
				
			||||||
	cursor: -moz-zoom-out;
 | 
					 | 
				
			||||||
	cursor: zoom-out;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* Prevent selection and tap highlights */
 | 
					 | 
				
			||||||
.pswp__container,
 | 
					 | 
				
			||||||
.pswp__img,
 | 
					 | 
				
			||||||
.pswp__button,
 | 
					 | 
				
			||||||
.pswp__counter {
 | 
					 | 
				
			||||||
	-webkit-user-select: none;
 | 
					 | 
				
			||||||
	-moz-user-select: none;
 | 
					 | 
				
			||||||
	-ms-user-select: none;
 | 
					 | 
				
			||||||
	user-select: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp__item {
 | 
					 | 
				
			||||||
	/* z-index for fade transition */
 | 
					 | 
				
			||||||
	z-index: 1;
 | 
					 | 
				
			||||||
	overflow: hidden;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp__hidden {
 | 
					 | 
				
			||||||
	display: none !important;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* Allow to click through pswp__content element, but not its children */
 | 
					 | 
				
			||||||
.pswp__content {
 | 
					 | 
				
			||||||
  pointer-events: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.pswp__content > * {
 | 
					 | 
				
			||||||
  pointer-events: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  PhotoSwipe UI
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
	Error message appears when image is not loaded
 | 
					 | 
				
			||||||
	(JS option errorMsg controls markup)
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
.pswp__error-msg-container {
 | 
					 | 
				
			||||||
  display: grid;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.pswp__error-msg {
 | 
					 | 
				
			||||||
	margin: auto;
 | 
					 | 
				
			||||||
	font-size: 1em;
 | 
					 | 
				
			||||||
	line-height: 1;
 | 
					 | 
				
			||||||
	color: var(--pswp-error-text-color);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
class pswp__hide-on-close is applied to elements that
 | 
					 | 
				
			||||||
should hide (for example fade out) when PhotoSwipe is closed
 | 
					 | 
				
			||||||
and show (for example fade in) when PhotoSwipe is opened
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
.pswp .pswp__hide-on-close {
 | 
					 | 
				
			||||||
	opacity: 0.005;
 | 
					 | 
				
			||||||
	will-change: opacity;
 | 
					 | 
				
			||||||
	transition: opacity var(--pswp-transition-duration) cubic-bezier(0.4, 0, 0.22, 1);
 | 
					 | 
				
			||||||
	z-index: 10; /* always overlap slide content */
 | 
					 | 
				
			||||||
	pointer-events: none; /* hidden elements should not be clickable */
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* class pswp--ui-visible is added when opening or closing transition starts */
 | 
					 | 
				
			||||||
.pswp--ui-visible .pswp__hide-on-close {
 | 
					 | 
				
			||||||
	opacity: 1;
 | 
					 | 
				
			||||||
	pointer-events: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* <button> styles, including css reset */
 | 
					 | 
				
			||||||
.pswp__button {
 | 
					 | 
				
			||||||
	position: relative;
 | 
					 | 
				
			||||||
	display: block;
 | 
					 | 
				
			||||||
	width: 50px;
 | 
					 | 
				
			||||||
	height: 60px;
 | 
					 | 
				
			||||||
	padding: 0;
 | 
					 | 
				
			||||||
	margin: 0;
 | 
					 | 
				
			||||||
	overflow: hidden;
 | 
					 | 
				
			||||||
	cursor: pointer;
 | 
					 | 
				
			||||||
	background: none;
 | 
					 | 
				
			||||||
	border: 0;
 | 
					 | 
				
			||||||
	box-shadow: none;
 | 
					 | 
				
			||||||
	opacity: 0.85;
 | 
					 | 
				
			||||||
	-webkit-appearance: none;
 | 
					 | 
				
			||||||
	-webkit-touch-callout: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp__button:hover,
 | 
					 | 
				
			||||||
.pswp__button:active,
 | 
					 | 
				
			||||||
.pswp__button:focus {
 | 
					 | 
				
			||||||
  transition: none;
 | 
					 | 
				
			||||||
  padding: 0;
 | 
					 | 
				
			||||||
  background: none;
 | 
					 | 
				
			||||||
  border: 0;
 | 
					 | 
				
			||||||
  box-shadow: none;
 | 
					 | 
				
			||||||
  opacity: 1;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp__button:disabled {
 | 
					 | 
				
			||||||
  opacity: 0.3;
 | 
					 | 
				
			||||||
  cursor: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp__icn {
 | 
					 | 
				
			||||||
  fill: var(--pswp-icon-color);
 | 
					 | 
				
			||||||
  color: var(--pswp-icon-color-secondary);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp__icn {
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  top: 14px;
 | 
					 | 
				
			||||||
  left: 9px;
 | 
					 | 
				
			||||||
  width: 32px;
 | 
					 | 
				
			||||||
  height: 32px;
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
  pointer-events: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp__icn-shadow {
 | 
					 | 
				
			||||||
  stroke: var(--pswp-icon-stroke-color);
 | 
					 | 
				
			||||||
  stroke-width: var(--pswp-icon-stroke-width);
 | 
					 | 
				
			||||||
  fill: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp__icn:focus {
 | 
					 | 
				
			||||||
	outline: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
	div element that matches size of large image,
 | 
					 | 
				
			||||||
	large image loads on top of it,
 | 
					 | 
				
			||||||
	used when msrc is not provided
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
div.pswp__img--placeholder,
 | 
					 | 
				
			||||||
.pswp__img--with-bg {
 | 
					 | 
				
			||||||
	background: var(--pswp-placeholder-bg);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp__top-bar {
 | 
					 | 
				
			||||||
	position: absolute;
 | 
					 | 
				
			||||||
	left: 0;
 | 
					 | 
				
			||||||
	top: 0;
 | 
					 | 
				
			||||||
	width: 100%;
 | 
					 | 
				
			||||||
	height: 60px;
 | 
					 | 
				
			||||||
	display: flex;
 | 
					 | 
				
			||||||
  flex-direction: row;
 | 
					 | 
				
			||||||
  justify-content: flex-end;
 | 
					 | 
				
			||||||
	z-index: 10;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/* allow events to pass through top bar itself */
 | 
					 | 
				
			||||||
	pointer-events: none !important;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.pswp__top-bar > * {
 | 
					 | 
				
			||||||
  pointer-events: auto;
 | 
					 | 
				
			||||||
  /* this makes transition significantly more smooth,
 | 
					 | 
				
			||||||
     even though inner elements are not animated */
 | 
					 | 
				
			||||||
  will-change: opacity;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Close button
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
.pswp__button--close {
 | 
					 | 
				
			||||||
  margin-right: 6px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Arrow buttons
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
.pswp__button--arrow {
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  top: 0;
 | 
					 | 
				
			||||||
  width: 75px;
 | 
					 | 
				
			||||||
  height: 100px;
 | 
					 | 
				
			||||||
  top: 50%;
 | 
					 | 
				
			||||||
  margin-top: -50px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp__button--arrow:disabled {
 | 
					 | 
				
			||||||
  display: none;
 | 
					 | 
				
			||||||
  cursor: default;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp__button--arrow .pswp__icn {
 | 
					 | 
				
			||||||
  top: 50%;
 | 
					 | 
				
			||||||
  margin-top: -30px;
 | 
					 | 
				
			||||||
  width: 60px;
 | 
					 | 
				
			||||||
  height: 60px;
 | 
					 | 
				
			||||||
  background: none;
 | 
					 | 
				
			||||||
  border-radius: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp--one-slide .pswp__button--arrow {
 | 
					 | 
				
			||||||
  display: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* hide arrows on touch screens */
 | 
					 | 
				
			||||||
.pswp--touch .pswp__button--arrow {
 | 
					 | 
				
			||||||
  visibility: hidden;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* show arrows only after mouse was used */
 | 
					 | 
				
			||||||
.pswp--has_mouse .pswp__button--arrow {
 | 
					 | 
				
			||||||
  visibility: visible;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp__button--arrow--prev {
 | 
					 | 
				
			||||||
  right: auto;
 | 
					 | 
				
			||||||
  left: 0px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp__button--arrow--next {
 | 
					 | 
				
			||||||
  right: 0px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.pswp__button--arrow--next .pswp__icn {
 | 
					 | 
				
			||||||
  left: auto;
 | 
					 | 
				
			||||||
  right: 14px;
 | 
					 | 
				
			||||||
  /* flip horizontally */
 | 
					 | 
				
			||||||
  transform: scale(-1, 1);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Zoom button
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
.pswp__button--zoom {
 | 
					 | 
				
			||||||
  display: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp--zoom-allowed .pswp__button--zoom {
 | 
					 | 
				
			||||||
  display: block;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* "+" => "-" */
 | 
					 | 
				
			||||||
.pswp--zoomed-in .pswp__zoom-icn-bar-v {
 | 
					 | 
				
			||||||
  display: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Loading indicator
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
.pswp__preloader {
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
  width: 50px;
 | 
					 | 
				
			||||||
  height: 60px;
 | 
					 | 
				
			||||||
  margin-right: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp__preloader .pswp__icn {
 | 
					 | 
				
			||||||
  opacity: 0;
 | 
					 | 
				
			||||||
  transition: opacity 0.2s linear;
 | 
					 | 
				
			||||||
  animation: pswp-clockwise 600ms linear infinite;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp__preloader--active .pswp__icn {
 | 
					 | 
				
			||||||
  opacity: 0.85;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@keyframes pswp-clockwise {
 | 
					 | 
				
			||||||
  0% { transform: rotate(0deg); }
 | 
					 | 
				
			||||||
  100% { transform: rotate(360deg); }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  "1 of 10" counter
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
.pswp__counter {
 | 
					 | 
				
			||||||
  height: 30px;
 | 
					 | 
				
			||||||
  margin: 15px 0 0 20px;
 | 
					 | 
				
			||||||
  font-size: 14px;
 | 
					 | 
				
			||||||
  line-height: 30px;
 | 
					 | 
				
			||||||
  color: var(--pswp-icon-color);
 | 
					 | 
				
			||||||
  text-shadow: 1px 1px 3px var(--pswp-icon-color-secondary);
 | 
					 | 
				
			||||||
  opacity: 0.85;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.pswp--one-slide .pswp__counter {
 | 
					 | 
				
			||||||
  display: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,15 +1,27 @@
 | 
				
			|||||||
:root {
 | 
					:root {
 | 
				
			||||||
    --primary: #6CD4FF;
 | 
					    --primary-light-one: #5daa68;
 | 
				
			||||||
    --link: #292a2c;
 | 
					    --primary-light-two: #4a9455;
 | 
				
			||||||
    --grey-light: #c4c6ce;
 | 
					    --primary-dark-one: #17311b;
 | 
				
			||||||
    --grey-dark: #262728;
 | 
					    --secondary-light-one: #faf1cf;
 | 
				
			||||||
 | 
					    --secondary-light-two: #e1d7b5;
 | 
				
			||||||
 | 
					    --background-one: var(--primary-light-one);
 | 
				
			||||||
 | 
					    --background-two: var(--primary-light-two);
 | 
				
			||||||
 | 
					    --background-three: var(--secondary-light-one);
 | 
				
			||||||
 | 
					    --background-four: var(--primary-dark-one);
 | 
				
			||||||
 | 
					    --highlight-one: var(--primary-dark-one);
 | 
				
			||||||
 | 
					    --highlight-one-text: var(--secondary-light-one);
 | 
				
			||||||
 | 
					    --text-one: var(--secondary-light-one);
 | 
				
			||||||
 | 
					    --shadow-one: var(--primary-dark-one);
 | 
				
			||||||
 | 
					    --text-two: var(--primary-dark-one);
 | 
				
			||||||
 | 
					    --text-three: var(--primary-light-one);
 | 
				
			||||||
 | 
					    --shadow-three: var(--primary-dark-one);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
body {
 | 
					body {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    flex-direction: column;
 | 
					    flex-direction: column;
 | 
				
			||||||
    background-color: hsl(221, 14%, 100%)r;
 | 
					    background-color: var(--background-one);
 | 
				
			||||||
    color: #000000;
 | 
					    color: var(--text-one);
 | 
				
			||||||
    margin: 0;
 | 
					    margin: 0;
 | 
				
			||||||
    height: 100%;
 | 
					    height: 100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -20,22 +32,24 @@ body {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
alert-box {
 | 
					alert-box {
 | 
				
			||||||
 | 
					    color: var(--highlight-one);
 | 
				
			||||||
    display: block;
 | 
					    display: block;
 | 
				
			||||||
    margin: 3rem 0;
 | 
					    margin: 3rem 0;
 | 
				
			||||||
    padding: 2rem 3rem;
 | 
					    padding: 2rem 3rem;
 | 
				
			||||||
    border: 1px solid var(--highlight-one);
 | 
					    border: 1px solid var(--highlight-one);
 | 
				
			||||||
    border-left-width: .5rem;
 | 
					    border-left-width: .5rem;
 | 
				
			||||||
    border-radius: .4rem;
 | 
					    border-radius: .4rem;
 | 
				
			||||||
    background-color: var(--primary);
 | 
					    background-color: var(--background-three);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    a {
 | 
					    a {
 | 
				
			||||||
        text-decoration: underline;
 | 
					        color: var(--text-three);
 | 
				
			||||||
 | 
					        text-decoration: none;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
a {
 | 
					a {
 | 
				
			||||||
 | 
					    color: inherit;
 | 
				
			||||||
    text-decoration: none;
 | 
					    text-decoration: none;
 | 
				
			||||||
    color: var(--link);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -51,7 +65,7 @@ a {
 | 
				
			|||||||
    margin: 1rem;
 | 
					    margin: 1rem;
 | 
				
			||||||
    padding: 5px;
 | 
					    padding: 5px;
 | 
				
			||||||
    border-radius: .4rem;
 | 
					    border-radius: .4rem;
 | 
				
			||||||
    border: 3px solid var(--primary);
 | 
					    background-color: var(--background-one);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.post-summary h1 {
 | 
					.post-summary h1 {
 | 
				
			||||||
@@ -65,7 +79,8 @@ a {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.navigation-sticky {
 | 
					.navigation-sticky {
 | 
				
			||||||
    background-color: var(--primary);
 | 
					    background-color: var(--secondary-light-one);
 | 
				
			||||||
 | 
					    color: var(--primary-light-one);
 | 
				
			||||||
    padding: 16px;
 | 
					    padding: 16px;
 | 
				
			||||||
    margin: 0;
 | 
					    margin: 0;
 | 
				
			||||||
    border-bottom-right-radius: 8px;
 | 
					    border-bottom-right-radius: 8px;
 | 
				
			||||||
 
 | 
				
			|||||||
| 
		 Before Width: | Height: | Size: 6.5 KiB  | 
| 
		 Before Width: | Height: | Size: 1.0 MiB  | 
| 
		 Before Width: | Height: | Size: 103 KiB  | 
| 
		 Before Width: | Height: | Size: 546 B  | 
| 
		 Before Width: | Height: | Size: 5.0 KiB  | 
| 
		 Before Width: | Height: | Size: 4.5 KiB  | 
| 
		 Before Width: | Height: | Size: 4.6 KiB  | 
| 
		 Before Width: | Height: | Size: 4.8 KiB  | 
| 
		 Before Width: | Height: | Size: 4.8 KiB  | 
@@ -19,6 +19,3 @@ function geojson_to_searchable_string(location) {
 | 
				
			|||||||
    return ifdef(location.properties.name, "", ", ") + ifdef(location.properties.street, "", ifdef(location.properties.housenumber, " ",", ")) + ifdef(location.properties.city, "", ", ") + ifdef(location.properties.country, "", "")
 | 
					    return ifdef(location.properties.name, "", ", ") + ifdef(location.properties.street, "", ifdef(location.properties.housenumber, " ",", ")) + ifdef(location.properties.city, "", ", ") + ifdef(location.properties.country, "", "")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function truncate(str, n, url){
 | 
					 | 
				
			||||||
  return (str.length > n) ? str.slice(0, n-1) + '<a href="' + url + '">…</a>' : str;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +0,0 @@
 | 
				
			|||||||
/* 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);
 | 
					 | 
				
			||||||
@@ -1,11 +0,0 @@
 | 
				
			|||||||
import PhotoSwipeLightbox from './photoswipe-lightbox.esm.js';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const lightbox = new PhotoSwipeLightbox({
 | 
					 | 
				
			||||||
  gallery: '.gallery',
 | 
					 | 
				
			||||||
  children: 'a',
 | 
					 | 
				
			||||||
  pswpModule: () => import('https://unpkg.com/photoswipe'),
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
lightbox.init();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||