Compare commits

63 Commits

Author SHA1 Message Date
e64cc4bd5f Merge branch 'develop'
# Conflicts:
#	src/locale/en/LC_MESSAGES/django.po
2025-11-09 17:55:11 +01:00
a498971d66 feat: create directory in one go, don't use cache dir and use more standard entrypoint.sh 2025-11-09 17:36:48 +01:00
527ab07b6f trans: fis various 2025-11-08 00:33:35 +01:00
e2e236d650 feat: add button to download all logs 2025-11-08 00:00:54 +01:00
c4100a9ade trans: fix various 2025-11-07 18:17:53 +01:00
9511b46af0 feat: deactivate cache in dev config 2025-11-07 18:13:30 +01:00
5b906a7708 feat: add clean method 2025-11-07 18:12:53 +01:00
d68e836b57 trans: add various english translations 2025-11-07 18:12:37 +01:00
fe77f1da8d trans: add many translations 2025-11-07 17:08:13 +01:00
78b71690c0 feat: Allow toggling ongoing communication status 2025-11-07 14:02:22 +01:00
3b9ee95abc feat: Add log export functionality 2025-11-07 14:01:51 +01:00
b4e50364de fix: dependency 2025-11-03 22:22:22 +01:00
b014b3b227 fix: quotes 2025-11-03 22:22:12 +01:00
99bfe460ee fix: remove debug statement 2025-11-03 18:24:45 +01:00
d4c7caa42d fix: allow empty internal IP 2025-11-03 18:24:20 +01:00
32c8fc88cf feat: add debug command that prints settings 2025-11-03 18:22:11 +01:00
151ce0d88e fix: massively reduce number of db queries by caching num_per_sex #27
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-11-03 17:00:16 +01:00
e07e633651 style: explicitly return none 2025-11-03 16:53:59 +01:00
dd3b1fde9d feat: Add logs for checking rescue orgs and remove deprecated exclusion 2025-11-03 16:15:11 +01:00
2ffc9b4ba1 feat: Ad django debug toolbar 2025-11-03 16:14:51 +01:00
22eebd4586 feat: Add simple profiler capability 2025-11-03 16:14:05 +01:00
e589a048d3 feat: Make logs in Admin more usable 2025-11-03 16:11:48 +01:00
392eb5a7a8 feat: Use unified explanation for reason for signup 2025-11-02 08:15:11 +01:00
44fa4d4880 feat: Remove requirement to retype password 2025-11-02 08:14:41 +01:00
9b97cc4cb1 fix: Ensure javascript for login is loaded 2025-11-02 08:14:03 +01:00
656a24ef02 feat: Make settings configurable
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-10-21 03:37:36 +02:00
74643db087 feat: Add nicer display of passkeys based on panels 2025-10-21 03:27:08 +02:00
3a6fd3cee1 feat: Add nicer badge 2025-10-21 03:24:19 +02:00
29e9d1bd8c feat: Don't make input for radio button 2025-10-21 03:24:07 +02:00
3c5ca9ae00 feat: Fix display of 2fa options 2025-10-21 02:12:34 +02:00
3d1ad6112d feat: Add link to 2fa options 2025-10-21 02:12:17 +02:00
b843e67e9b feat: put buttons in group 2025-10-21 01:47:17 +02:00
4cab71e8fb feat: Style allauth templates 2025-10-21 01:28:31 +02:00
969339a95f feat: Use allauth and add passkey support 2025-10-21 00:40:10 +02:00
e06efa1539 feat: limit to 10 2025-10-21 14:47:13 +02:00
2fb6d2782f fix: Align button description with function 2025-10-20 21:56:04 +02:00
f69eccd0e4 feat: add page for updating the exclusion reason where it's not set yet 2025-10-20 18:33:15 +02:00
e20e6d4b1d fix: typo 2025-10-20 18:31:10 +02:00
0352a60e28 feat: Add reason why rescue org was excluded from check
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-10-20 10:54:23 +02:00
abeb14601a docs: Add general explenation 2025-10-20 10:12:40 +02:00
f52225495d docs: Add number of rescueorgs in table 2025-10-20 10:09:58 +02:00
797b2c15f7 docs: Add adoption notice lifecycle
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-10-19 22:53:29 +02:00
e81618500b docs: Add getting started
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-10-19 21:02:51 +02:00
f7a5da306c docs: Add details to notifications 2025-10-19 21:02:33 +02:00
92a9b5c6c9 fix: typo 2025-10-19 17:46:47 +02:00
964aeb97a7 docs: documentation on checking rescue orgs
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
2025-10-19 09:41:19 +02:00
474e9eb0f8 feat: Add extension to sphinx that includes drawio diagrams 2025-10-19 09:40:51 +02:00
7acc2c6eec feat: Document shortcuts in org-check und documentation 2025-10-19 08:35:19 +02:00
5a02837d7f feat: Fix age display and test 2025-10-18 18:45:17 +02:00
7ff0a9b489 feat: Add stats 2025-10-03 18:37:54 +02:00
9af4b58a4f feat: Add stats 2025-10-03 09:39:48 +02:00
7a20890f17 feat: Add organization to form 2025-10-01 06:13:01 +02:00
f6c9e532f8 feat: Use status-specific description 2025-10-01 05:58:30 +02:00
f1698c4fd3 fix: use correct new status 2025-10-01 05:54:43 +02:00
cb82aeffde fix: use correct new status 2025-10-01 05:33:49 +02:00
e9c1ef2604 feat: typo 2025-09-29 17:34:46 +02:00
d8f0f2b3be feat: Use status description to more accuratly describe status 2025-09-29 17:34:38 +02:00
65f065f5ce feat: Move unchecked to awaiting action 2025-09-29 17:34:13 +02:00
5cba64e500 feat: allow links to break anywhere 2025-09-29 17:32:05 +02:00
064784a222 feat: Use contact data of parent org if org doesn't have specified 2025-09-27 15:35:39 +02:00
b890ef3563 fix: Use block only when description exists 2025-09-27 15:34:32 +02:00
962f2ae86c feat: Add contact site 2025-09-17 20:02:20 +02:00
c71a1940dd feat: Add url of adoption notice to API 2025-09-17 20:02:13 +02:00
79 changed files with 4495 additions and 1119 deletions

View File

@@ -9,15 +9,14 @@ RUN apt install gettext -y
RUN apt install libpq-dev gcc -y
COPY . /app
WORKDIR /app
RUN mkdir /app/data
RUN mkdir /app/data/static
RUN mkdir /app/data/static -p
RUN mkdir /app/data/media
RUN pip install -e . # Without the -e the library static folder will not be copied by collectstatic!
RUN pip install --no-cache-dir -e . # Without the -e the library static folder will not be copied by collectstatic!
RUN nf collectstatic --noinput
RUN nf compilemessages --ignore venv
COPY docker/notfellchen.bash /bin/notfellchen
COPY docker/entrypoint.sh /bin/notfellchen
EXPOSE 7345
CMD ["notfellchen"]

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

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

File diff suppressed because one or more lines are too long

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

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

View File

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

View File

@@ -38,7 +38,10 @@ dependencies = [
"celery[redis]",
"drf-spectacular[sidecar]",
"django-widget-tweaks",
"django-super-deduper"
"django-super-deduper",
"django-allauth[mfa]",
"django_debug_toolbar",
"django-admin-extra-buttons"
]
dynamic = ["version", "readme"]

View File

@@ -205,6 +205,8 @@ def main():
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
@@ -229,11 +231,13 @@ def main():
optional_data = ["email", "phone_number", "website", "description", "fediverse_profile", "facebook",
"instagram"]
# Check if rescue organization exits
# 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,
@@ -248,7 +252,9 @@ def main():
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"]}",
@@ -262,6 +268,7 @@ def main():
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__":

View File

@@ -7,6 +7,8 @@ from django.utils.html import format_html
from django.urls import reverse
from django.utils.http import urlencode
from admin_extra_buttons.api import ExtraButtonsMixin, button, link
from .models import Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
SpeciesSpecificURL, ImportantLocation, SocialMediaPost
@@ -17,6 +19,21 @@ from django.utils.translation import gettext_lazy as _
from .tools.model_helpers import AdoptionNoticeStatusChoices
def export_to_csv_generic(model, queryset):
meta = model._meta
field_names = [field.name for field in meta.fields]
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
writer = csv.writer(response)
writer.writerow(field_names)
for obj in queryset:
row = writer.writerow([getattr(obj, field) for field in field_names])
return response
@admin.register(AdoptionNotice)
class AdoptionNoticeAdmin(admin.ModelAdmin):
search_fields = ("name__icontains", "description__icontains")
@@ -49,17 +66,7 @@ class UserAdmin(admin.ModelAdmin):
return format_html('<a href="{}">{} Adoption Notices</a>', url, count)
def export_as_csv(self, request, queryset):
meta = self.model._meta
field_names = [field.name for field in meta.fields]
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename={}.csv'.format(meta)
writer = csv.writer(response)
writer.writerow(field_names)
for obj in queryset:
row = writer.writerow([getattr(obj, field) for field in field_names])
response = export_to_csv_generic(self.model, queryset)
return response
export_as_csv.short_description = _("Ausgewählte User exportieren")
@@ -164,6 +171,28 @@ class SocialMediaPostAdmin(admin.ModelAdmin):
list_filter = ("platform",)
@admin.register(Log)
class LogAdmin(ExtraButtonsMixin, admin.ModelAdmin):
ordering = ["-created_at"]
list_filter = ("action",)
list_display = ("action", "user", "created_at")
actions = ("export_as_csv",)
@admin.action(description=_("Ausgewählte Logs exportieren"))
def export_as_csv(self, request, queryset):
response = export_to_csv_generic(Log, queryset)
return response
@button()
def export_all_as_csv(self, request):
actual_queryset = Log.objects.all()
response = export_to_csv_generic(Log, actual_queryset)
return response
@link(href="https://www.google.com/", visible=lambda btn: True)
def invisible(self, button):
button.visible = False
admin.site.register(Animal)
admin.site.register(Species)
admin.site.register(Rule)
@@ -172,5 +201,4 @@ admin.site.register(ModerationAction)
admin.site.register(Language)
admin.site.register(Announcement)
admin.site.register(Subscriptions)
admin.site.register(Log)
admin.site.register(Timestamp)

View File

@@ -35,13 +35,18 @@ class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
required=False,
allow_null=True
)
url = serializers.SerializerMethodField()
photos = ImageSerializer(many=True, read_only=True)
def get_url(self, obj):
return obj.get_full_url()
class Meta:
model = AdoptionNotice
fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information",
"group_only", "location", "location_details", "organization", "photos", "adoption_notice_status"]
"group_only", "location", "location_details", "organization", "photos", "adoption_notice_status",
"url"]
class AdoptionNoticeGeoJSONSerializer(serializers.ModelSerializer):

View File

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

View File

@@ -0,0 +1,13 @@
from django.core.management import BaseCommand
from notfellchen import settings
class Command(BaseCommand):
help = 'Print the current settings'
def handle(self, *args, **options):
for key in settings.__dir__():
if key.startswith("_") or key == "SECRET_KEY":
continue
print(f"{key} = {getattr(settings, key)}")

View File

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

View File

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

View File

@@ -12,7 +12,9 @@ from .tools import misc, geo
from notfellchen.settings import MEDIA_URL, base_url
from .tools.geo import LocationProxy, Position
from .tools.misc import time_since_as_hr_string
from .tools.model_helpers import NotificationTypeChoices, AdoptionNoticeStatusChoices, AdoptionProcess
from .tools.model_helpers import NotificationTypeChoices, AdoptionNoticeStatusChoices, AdoptionProcess, \
AdoptionNoticeStatusChoicesDescriptions, RegularCheckStatusChoices, reason_for_signup_label, \
reason_for_signup_help_text
from .tools.model_helpers import ndm as NotificationDisplayMapping
@@ -171,6 +173,12 @@ class RescueOrganization(models.Model):
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."))
@@ -260,10 +268,6 @@ class RescueOrganization(models.Model):
"""
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)
@@ -304,8 +308,7 @@ class User(AbstractUser):
updated_at = models.DateTimeField(auto_now=True)
organization_affiliation = models.ForeignKey(RescueOrganization, on_delete=models.PROTECT, null=True, blank=True,
verbose_name=_('Organisation'))
reason_for_signup = models.TextField(verbose_name=_("Grund für die Registrierung"), help_text=_(
"Wir würden gerne wissen warum du dich registriertst, ob du dich z.B. Tiere eines bestimmten Tierheim einstellen willst 'nur mal gucken' willst. Beides ist toll! Wenn du für ein Tierheim/eine Pflegestelle arbeitest kontaktieren wir dich ggf. um dir erweiterte Rechte zu geben."))
reason_for_signup = models.TextField(verbose_name=reason_for_signup_label, help_text=reason_for_signup_help_text)
email_notifications = models.BooleanField(verbose_name=_("Benachrichtigung per E-Mail"), default=True)
REQUIRED_FIELDS = ["reason_for_signup", "email"]
@@ -418,7 +421,7 @@ class AdoptionNotice(models.Model):
def num_per_sex(self):
num_per_sex = dict()
for sex in SexChoices:
num_per_sex[sex] = self.animals.filter(sex=sex).count()
num_per_sex[sex] = len([animal for animal in self.animals if animal.sex == sex])
return num_per_sex
@property
@@ -508,6 +511,7 @@ class AdoptionNotice(models.Model):
photos.extend(animal.photos.all())
if len(photos) > 0:
return photos
return None
def get_photo(self):
"""
@@ -553,9 +557,13 @@ class AdoptionNotice(models.Model):
def is_awaiting_action(self):
return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.AwaitingAction.choices)
@property
def status_description(self):
return AdoptionNoticeStatusChoicesDescriptions.mapping[self.adoption_notice_status]
def set_unchecked(self):
self.last_checked = timezone.now()
self.adoption_notice_status = AdoptionNoticeStatusChoices.Disabled.UNCHECKED
self.adoption_notice_status = AdoptionNoticeStatusChoices.AwaitingAction.UNCHECKED
self.save()
for subscription in self.get_subscriptions():

View File

@@ -45,6 +45,7 @@ $confirm: hsl(133deg, 100%, calc(41% + 0%));
p > a {
text-decoration: underline;
word-break: break-all;
}
p > a.button {
@@ -351,3 +352,12 @@ AN Cards
.embed-main-content {
padding: 20px 10px 20px 10px;
}
// FLOATING BUTTON
.floating {
position: fixed;
border-radius: 0.3rem;
bottom: 4.5rem;
right: 1rem;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,53 +19,10 @@
<div class="columns">
<div class="column">
<div class="block">
<div class="card">
<div class="card-header">
<h1 class="card-header-title">{{ org.name }}</h1>
</div>
<div class="card-content">
<div class="block">
<b><i class="fa-solid fa-location-dot"></i></b>
{% if org.location %}
{{ org.location }}
{% else %}
{{ org.location_string }}
{% endif %}
{% if org.description %}
<div class="block content">
<p>{{ org.description | render_markdown }}</p>
</div>
{% endif %}
</div>
{% if org.specializations %}
<div class="block">
<h3 class="title is-5">{% translate 'Spezialisierung' %}</h3>
<div class="content">
<ul>
{% for specialization in org.specializations.all %}
<li>{{ specialization }}</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% if org.parent_org %}
<div class="block">
<h3 class="title is-5">{% translate 'Übergeordnete Organisation' %}</h3>
<p>
<span>
<i class="fa-solid fa-building fa-fw"
aria-label="{% trans 'Tierschutzorganisation' %}"></i>
<a href="{{ org.parent_org.get_absolute_url }}"> {{ org.parent_org }}</a>
</span>
</p>
</div>
{% endif %}
</div>
</div>
{% include "fellchensammlung/partials/rescue_orgs/partial-basic-info-card.html" %}
</div>
<div class="block">
{% include "fellchensammlung/partials/partial-rescue-organization-contact.html" %}
{% include "fellchensammlung/partials/rescue_orgs/partial-rescue-organization-contact.html" %}
</div>
<div class="block">
<a class="button is-warning is-fullwidth" href="{% url org_meta|admin_urlname:'change' org.pk %}">

View File

@@ -1,7 +1,8 @@
{% extends "fellchensammlung/base.html" %}
{% load i18n %}
{% load account %}
{% block title %}<title>{{ user.get_full_name }}</title>{% endblock %}
{% block title %}<title>{% user_display user %}</title>{% endblock %}
{% block content %}
@@ -13,7 +14,7 @@
</div>
<div class="level-right">
<div class="level-item">
<form class="" action="{% url 'logout' %}" method="post">
<form class="" action="{% url 'account_logout' %}" method="post">
{% csrf_token %}
<button class="button" type="submit">
<i aria-hidden="true" class="fas fa-sign-out fa-fw"></i> Logout
@@ -25,69 +26,87 @@
<div class="block">
<h2 class="title is-2">{% trans 'Profil verwalten' %}</h2>
<p><strong>{% translate "E-Mail" %}:</strong> {{ user.email }}</p>
<div class="">
<p>
<a class="button is-warning" href="{% url 'password_change' %}">{% translate "Change password" %}</a>
<a class="button is-info" href="{% url 'user-me-export' %}">{% translate "Daten exportieren" %}</a>
</p>
</div>
</div>
{% if user.id is request.user.id %}
<div class="block"><strong>{% translate "E-Mail" %}:</strong> {{ user.email }}</div>
<div class="block">
<h2 class="title is-2">{% trans 'Einstellungen' %}</h2>
<form class="block" action="" method="POST">
{% csrf_token %}
{% if user.email_notifications %}
<label class="toggle">
<input type="submit" class="toggle-checkbox checked" name="toggle_email_notifications">
<div class="toggle-switch round "></div>
<span class="slider-label">
{% translate 'E-Mail Benachrichtigungen' %}
</span>
</label>
{% else %}
<label class="toggle">
<input type="submit" class="toggle-checkbox" name="toggle_email_notifications">
<div class="toggle-switch round"></div>
<span class="slider-label">
{% translate 'E-Mail Benachrichtigungen' %}
</span>
</label>
{% endif %}
</form>
<details>
<summary><strong>{% trans 'Erweiterte Einstellungen' %}</strong></summary>
<div class="block">
{% if token %}
<form action="" method="POST">
{% csrf_token %}
<p class="text-muted"><strong>{% translate "API token:" %}</strong> {{ token }}</p>
<input class="button is-danger" type="submit" name="delete_token"
value={% translate "Delete API token" %}>
</form>
{% else %}
<p>{% translate "Kein API-Token vorhanden." %}</p>
<form action="" method="POST">
{% csrf_token %}
<input class="button is-primary" type="submit" name="create_token"
value={% translate "Create API token" %}>
</form>
{% endif %}
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<a class="button is-warning"
href="{% url 'account_change_password' %}">{% translate "Passwort ändern" %}</a>
</div>
</details>
<div class="control">
<a class="button is-warning"
href="{% url 'account_email' %}">
{% translate "E-Mail Adresse ändern" %}
</a>
</div>
<div class="control">
<a class="button is-warning"
href="{% url 'mfa_index' %}">
{% translate "2-Faktor Authentifizierung" %}
</a>
</div>
<div class="control">
<a class="button is-info" href="{% url 'user-me-export' %}">
{% translate "Daten exportieren" %}
</a>
</div>
</div>
</div>
<h2 class="title is-2">{% translate 'Benachrichtigungen' %}</h2>
{% include "fellchensammlung/lists/list-notifications.html" %}
{% if user.id is request.user.id %}
<div class="block">
<h2 class="title is-2">{% trans 'Einstellungen' %}</h2>
<form class="block" action="" method="POST">
{% csrf_token %}
{% if user.email_notifications %}
<label class="toggle">
<input type="submit" class="toggle-checkbox checked" name="toggle_email_notifications">
<div class="toggle-switch round "></div>
<span class="slider-label">
{% translate 'E-Mail Benachrichtigungen' %}
</span>
</label>
{% else %}
<label class="toggle">
<input type="submit" class="toggle-checkbox" name="toggle_email_notifications">
<div class="toggle-switch round"></div>
<span class="slider-label">
{% translate 'E-Mail Benachrichtigungen' %}
</span>
</label>
{% endif %}
</form>
<details>
<summary><strong>{% trans 'Erweiterte Einstellungen' %}</strong></summary>
<div class="block">
{% if token %}
<form action="" method="POST">
{% csrf_token %}
<p class="text-muted"><strong>{% translate "API token:" %}</strong> {{ token }}</p>
<input class="button is-danger" type="submit" name="delete_token"
value={% translate "Delete API token" %}>
</form>
{% else %}
<p>{% translate "Kein API-Token vorhanden." %}</p>
<form action="" method="POST">
{% csrf_token %}
<input class="button is-primary" type="submit" name="create_token"
value={% translate "Create API token" %}>
</form>
{% endif %}
</div>
</details>
<h2 class="title is-2">{% translate 'Abonnierte Suchen' %}</h2>
{% include "fellchensammlung/lists/list-search-subscriptions.html" %}
</div>
<h2 class="title is-2">{% translate 'Meine Vermittlungen' %}</h2>
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
<h2 class="title is-2">{% translate 'Benachrichtigungen' %}</h2>
{% include "fellchensammlung/lists/list-notifications.html" %}
{% endif %}
<h2 class="title is-2">{% translate 'Abonnierte Suchen' %}</h2>
{% include "fellchensammlung/lists/list-search-subscriptions.html" %}
<h2 class="title is-2">{% translate 'Meine Vermittlungen' %}</h2>
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
{% endif %}
{% endblock %}

View File

@@ -2,7 +2,7 @@
{% load i18n %}
{% load custom_tags %}
{% block title %}<title>{% translate "403 Forbidden" %}</title>{% endblock %}
{% block title %}<title>{% translate "404 Forbidden" %}</title>{% endblock %}
{% block content %}
<h1 class="title is-1">404 Not Found</h1>

View File

@@ -34,6 +34,10 @@
{% translate 'Das Notfellchen Projekt' %}
</a>
<br/>
<a href="{% url "contact" %}">
{% translate 'Kontakt' %}
</a>
<br/>
<a href="{% url "buying" %}">
{% translate 'Ratten kaufen' %}
</a>

View File

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

View File

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

View File

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

View File

@@ -49,10 +49,10 @@
{% else %}
<div class="navbar-item">
<div class="buttons">
<a class="button is-primary" href="{% url "django_registration_register" %}">
<a class="button is-primary" href="{% url "account_signup" %}">
<strong>{% translate "Registrieren" %}</strong>
</a>
<a class="button is-light" href="{% url "login" %}">
<a class="button is-light" href="{% url "account_login" %}">
<strong>{% translate "Login" %}</strong>
</a>
</div>

View File

@@ -18,7 +18,7 @@
</div>
<div class="message-body">
{% blocktranslate %}
Versuche es zu einem späteren Zeitpunkt erneut
Versuche es zu einem späteren Zeitpunkt erneut.
{% endblocktranslate %}
</div>
</article>

View File

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

View File

@@ -0,0 +1,30 @@
{% load i18n %}
<div id="modal-shortcuts" class="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">{% trans 'Shortcuts' %}</p>
<button class="delete" aria-label="{% trans 'Schließen' %}"></button>
</header>
<section class="modal-card-body">
<div class="content">
<ul>
<li>{% trans 'Website öffnen' %}: <code>O</code></li>
<li>{% trans 'Website schließen' %}: <code>{% trans 'STRG' %} + W</code></li>
<li>{% trans 'Organisation als geprüft markieren' %}: <code>{% trans 'STRG' %} + W</code></li>
</ul>
</div>
</section>
<footer class="modal-card-foot">
<div class="buttons">
<button class="button is-success nf-modal-close">{% translate 'Verstanden' %}</button>
<button class="button">{% translate 'Details' %}</button>
</div>
</footer>
</div>
</div>
<button class="button is-info floating js-modal-trigger" data-target="modal-shortcuts">
<i class="fa-solid fa-keyboard fa-fw"></i> {% trans 'Shortcuts' %}
</button>

View File

@@ -4,14 +4,10 @@
{% if adoption_notice.is_closed %}
<article class="message is-warning">
<div class="message-header">
<p>{% translate 'Vermittlung deaktiviert' %}</p>
<p>{% translate 'Vermittlung geschlossen' %}</p>
</div>
<div class="message-body content">
{% blocktranslate %}
Diese Vermittlung wurde deaktiviert. Typischerweise passiert das, wenn die Tiere erfolgreich
vermittelt wurden.
In den Kommentaren findest du ggf. mehr Informationen.
{% endblocktranslate %}
{{ adoption_notice.status_description }}
</div>
</article>
{% elif adoption_notice.is_interested %}
@@ -29,14 +25,10 @@
{% elif adoption_notice.is_awaiting_action %}
<article class="message is-warning">
<div class="message-header">
<p>{% translate 'Warten auf Aktivierung' %}</p>
<p>{% translate 'Wartet auf Freigabe von Moderator*innen' %}</p>
</div>
<div class="message-body content">
{% blocktranslate %}
Diese Vermittlung muss noch durch Moderator*innen aktiviert werden und taucht daher nicht auf der
Startseite auf.
Ggf. fehlen noch relevante Informationen.
{% endblocktranslate %}
{{ adoption_notice.status_description }}
</div>
</article>
{% endif %}

View File

@@ -0,0 +1,55 @@
{% load i18n %}
{% if org.website %}
<a class="panel-block is-active" href="{{ org.website }}" target="_blank">
<span class="panel-icon">
<i class="fas fa-globe" aria-label="{% translate "Website" %}"></i>
</span>
{{ org.website }}
</a>
{% endif %}
{% if org.phone_number %}
<a class="panel-block is-active" href="tel:{{ org.phone_number }}">
<span class="panel-icon">
<i class="fas fa-phone" aria-label="{% translate "Telefonnummer" %}"></i>
</span>
{{ org.phone_number }}
</a>
{% endif %}
{% if org.email %}
<a class="panel-block is-active" href="mailto:{{ org.email }}">
<span class="panel-icon">
<i class="fas fa-envelope" aria-label="{% translate "E-Mail" %}"></i>
</span>
{{ org.email }}
</a>
{% endif %}
{% if org.fediverse_profile %}
<a class="panel-block is-active" href="{{ org.fediverse_profile }}" target="_blank">
<span class="panel-icon">
<i class="fas fa-hashtag" aria-label="{% translate "Fediverse Profil" %}"></i>
</span>
{{ org.fediverse_profile }}
</a>
{% endif %}
{% if org.instagram %}
<a class="panel-block is-active" href="{{ org.instagram }}" target="_blank">
<span class="panel-icon">
<i class="fa-brands fa-instagram" aria-label="{% translate "Instagram Profil" %}"></i>
</span>
{{ org.instagram }}
</a>
{% endif %}
{% if org.facebook %}
<a class="panel-block is-active" href="{{ org.facebook }}" target="_blank">
<span class="panel-icon">
<i class="fa-brands fa-facebook" aria-label="{% translate "Facebook Profil" %}"></i>
</span>
{{ org.facebook }}
</a>
{% endif %}

View File

@@ -1,65 +0,0 @@
{% load custom_tags %}
{% load i18n %}
<div class="panel block">
<p class="panel-heading">{% trans "Kontaktdaten" %}</p>
{% if org.has_contact_data %}
{% if org.website %}
<a class="panel-block is-active" href="{{ org.website }}" target="_blank">
<span class="panel-icon">
<i class="fas fa-globe" aria-label="{% translate "Website" %}"></i>
</span>
{{ org.website }}
</a>
{% endif %}
{% if org.phone_number %}
<a class="panel-block is-active" href="tel:{{ org.phone_number }}">
<span class="panel-icon">
<i class="fas fa-phone" aria-label="{% translate "Telefonnummer" %}"></i>
</span>
{{ org.phone_number }}
</a>
{% endif %}
{% if org.email %}
<a class="panel-block is-active" href="mailto:{{ org.email }}">
<span class="panel-icon">
<i class="fas fa-envelope" aria-label="{% translate "E-Mail" %}"></i>
</span>
{{ org.email }}
</a>
{% endif %}
{% if org.fediverse_profile %}
<a class="panel-block is-active" href="{{ org.fediverse_profile }}" target="_blank">
<span class="panel-icon">
<i class="fas fa-hashtag" aria-label="{% translate "Fediverse Profil" %}"></i>
</span>
{{ org.fediverse_profile }}
</a>
{% endif %}
{% if org.instagram %}
<a class="panel-block is-active" href="{{ org.instagram }}" target="_blank">
<span class="panel-icon">
<i class="fa-brands fa-instagram" aria-label="{% translate "Instagram Profil" %}"></i>
</span>
{{ org.instagram }}
</a>
{% endif %}
{% if org.facebook %}
<a class="panel-block is-active" href="{{ org.facebook }}" target="_blank">
<span class="panel-icon">
<i class="fa-brands fa-facebook" aria-label="{% translate "Facebook Profil" %}"></i>
</span>
{{ org.facebook }}
</a>
{% endif %}
{% else %}
<div class="panel-block is-active">
<span class="panel-icon">
<i class="fas fa-x" aria-label="{% translate "E-Mail" %}"></i>
</span>
{% translate 'Keine Kontaktdaten hinterlegt' %}
</div>
{% endif %}
</div>

View File

@@ -0,0 +1,46 @@
{% load i18n %}
{% load custom_tags %}
<div class="card">
<div class="card-header">
<h1 class="card-header-title">{{ org.name }}</h1>
</div>
<div class="card-content">
<div class="block">
<b><i class="fa-solid fa-location-dot"></i></b>
{% if org.location %}
{{ org.location }}
{% else %}
{{ org.location_string }}
{% endif %}
{% if org.description %}
<div class="block content">
<p>{{ org.description | render_markdown }}</p>
</div>
{% endif %}
</div>
{% if org.specializations %}
<div class="block">
<h3 class="title is-5">{% translate 'Spezialisierung' %}</h3>
<div class="content">
<ul>
{% for specialization in org.specializations.all %}
<li>{{ specialization }}</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% if org.parent_org %}
<div class="block">
<h3 class="title is-5">{% translate 'Übergeordnete Organisation' %}</h3>
<p>
<span>
<i class="fa-solid fa-building fa-fw"
aria-label="{% trans 'Tierschutzorganisation' %}"></i>
<a href="{{ org.parent_org.get_absolute_url }}"> {{ org.parent_org }}</a>
</span>
</p>
</div>
{% endif %}
</div>
</div>

View File

@@ -71,19 +71,26 @@
value="{{ rescue_org.pk }}">
<input type="hidden" name="action" value="checked">
<button class="" type="submit">{% translate "Organisation geprüft" %}</button>
</form>
</div>
<div class="card-footer-item is-danger">
<div class="card-footer-item is-warning">
<form method="post">
{% csrf_token %}
<input type="hidden"
name="rescue_organization_id"
value="{{ rescue_org.pk }}">
<input type="hidden" name="action" value="exclude">
<button class="" type="submit">{% translate "Von Check exkludieren" %}</button>
<input type="hidden" name="action" value="toggle_active_communication">
<button class="" type="submit">
{% if rescue_org.ongoing_communication %}
{% translate "Aktive Kommunikation beendet" %}
{% else %}
{% translate "Aktive Kommunikation" %}
{% endif %}
</button>
</form>
</div>
<div class="card-footer-item is-danger">
<a href="{% url 'rescue-organization-exclude' rescue_organization_id=rescue_org.pk %}">{% translate "Von Check exkludieren" %}</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,20 @@
{% load custom_tags %}
{% load i18n %}
<div class="panel block">
<p class="panel-heading">{% trans "Kontaktdaten" %}</p>
{% if org.has_contact_data %}
{% include "fellchensammlung/partials/partial-in-panel-contact-data.html" %}
{% elif org.parent_org.has_contact_data %}
{% with org=org.parent_org %}
{% include "fellchensammlung/partials/partial-in-panel-contact-data.html" %}
{% endwith %}
{% else %}
<div class="panel-block is-active">
<span class="panel-icon">
<i class="fas fa-x" aria-label="{% translate "E-Mail" %}"></i>
</span>
{% translate 'Keine Kontaktdaten hinterlegt' %}
</div>
{% endif %}
</div>

View File

@@ -3,8 +3,10 @@
<div class="card">
<div class="card-header">
<h2 class="card-header-title"><a
href="{{ rescue_organization.get_absolute_url }}"> {{ rescue_organization.name }}</a></h2>
<h2 class="card-header-title">
<a href="{{ rescue_organization.get_absolute_url }}"> {{ rescue_organization.name }}
</a>
</h2>
</div>
<div class="card-content">
@@ -16,10 +18,10 @@
{{ rescue_organization.location_string }}
{% endif %}
</div>
<div class="block content">
{% if rescue_organization.description_short %}
{% if rescue_organization.description_short %}
<div class="block content">
{{ rescue_organization.description_short | render_markdown }}
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>

View File

@@ -1,19 +1,20 @@
{% load static %}
{% load i18n %}
<div class="grid is-col-min-2">
{% if adoption_notice.num_per_sex.F > 0 %}
<span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.F }} </span>
{% with num_per_sex=adoption_notice.num_per_sex %}
{% if num_per_sex.F > 0 %}
<span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ num_per_sex.F }}</span>
<span class="icon">
<img class="icon" src="{% static 'fellchensammlung/img/sexes/Female.png' %}"
alt="{% translate 'weibliche Tiere' %}">
</span>
</span>
{% endif %}
{% endif %}
{% if adoption_notice.num_per_sex.I > 0 %}
<span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.I }}</span>
{% if num_per_sex.I > 0 %}
<span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ num_per_sex.I }}</span>
<span class="icon">
<img class="icon"
@@ -21,24 +22,25 @@
alt="{% translate 'intersexuelle Tiere' %}">
</span>
</span>
{% endif %}
{% if adoption_notice.num_per_sex.M > 0 %}
<span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.M }}</span>
{% endif %}
{% if num_per_sex.M > 0 %}
<span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ num_per_sex.M }}</span>
<span class="icon">
<img class="icon" src="{% static 'fellchensammlung/img/sexes/Male.png' %}"
alt="{% translate 'männliche Tiere' %}">
</span>
</span>
{% endif %}
{% if adoption_notice.num_per_sex.M_N > 0 %}
<span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.M_N }}</span>
{% endif %}
{% if num_per_sex.M_N > 0 %}
<span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ num_per_sex.M_N }}</span>
<span class="icon">
<img class="icon"
src="{% static 'fellchensammlung/img/sexes/Male Neutered.png' %}"
alt="{% translate 'männlich, kastrierte Tiere' %}">
</span>
</span>
{% endif %}
{% endif %}
{% endwith %}
</div>

View File

@@ -23,9 +23,11 @@
</div>
<div class="column">
{% if dq %}
<a class="button is-info" href="{% url 'organization-check' %}">{% translate 'Datenergänzung deaktivieren' %}</a>
<a class="button is-info"
href="{% url 'organization-check' %}">{% translate 'Datenergänzung deaktivieren' %}</a>
{% else %}
<a class="button is-info is-light" href="{% url 'organization-check-dq' %}">{% translate 'Datenergänzung aktivieren' %}</a>
<a class="button is-info is-light"
href="{% url 'organization-check-dq' %}">{% translate 'Datenergänzung aktivieren' %}</a>
{% endif %}
</div>
</div>
@@ -36,7 +38,7 @@
<div class="grid is-col-min-15">
{% for rescue_org in rescue_orgs_to_check %}
<div class="cell">
{% include "fellchensammlung/partials/partial-check-rescue-org.html" %}
{% include "fellchensammlung/partials/rescue_orgs/partial-check-rescue-org.html" %}
</div>
{% endfor %}
</div>
@@ -48,7 +50,7 @@
<div class="grid is-col-min-15">
{% for rescue_org in rescue_orgs_with_ongoing_communication %}
<div class="cell">
{% include "fellchensammlung/partials/partial-check-rescue-org.html" %}
{% include "fellchensammlung/partials/rescue_orgs/partial-check-rescue-org.html" %}
</div>
{% endfor %}
</div>
@@ -60,9 +62,10 @@
<div class="grid is-col-min-15">
{% for rescue_org in rescue_orgs_last_checked %}
<div class="cell">
{% include "fellchensammlung/partials/partial-check-rescue-org.html" %}
{% include "fellchensammlung/partials/rescue_orgs/partial-check-rescue-org.html" %}
</div>
{% endfor %}
</div>
</div>
{% include "fellchensammlung/partials/modal-shortcuts.html" %}
{% endblock %}

View File

@@ -65,7 +65,7 @@
{% endif %}
</div>
{% if not subscribed_search %}
<div class="block">{% translate 'Wenn du die Suche abbonierst, wirst du für neue Vermittlungen, die den Kriterien entsprechen, benachrichtigt.' %}</div>
<div class="block">{% translate 'Wenn du die Suche abonnierst, wirst du für neue Vermittlungen, die den Kriterien entsprechen, benachrichtigt.' %}</div>
{% endif %}
{% else %}
<button class="button is-primary is-fullwidth" type="submit" value="search" name="search">

View File

@@ -0,0 +1,43 @@
{% extends "mfa/recovery_codes/base.html" %}
{% load i18n %}
{% load allauth %}
{% block content %}
{% element h1 %}
{% translate "Recovery Codes" %}
{% endelement %}
{% element p %}
{% blocktranslate count unused_count=unused_codes|length %}There is {{ unused_count }} out of {{ total_count }}
recovery codes available.{% plural %}There are {{ unused_count }} out of {{ total_count }} recovery codes
available.{% endblocktranslate %}
{% endelement %}
<div class="block">
{% element field id="recovery_codes" type="textarea" disabled=True rows=unused_codes|length readonly=True %}
{% slot label %}
{% translate "Unused codes" %}
{% endslot %}
{% comment %} djlint:off {% endcomment %}
{% slot value %}{% for code in unused_codes %}{% if forloop.counter0 %}
{% endif %}{{ code }}{% endfor %}{% endslot %}
{% comment %} djlint:on {% endcomment %}
{% endelement %}
</div>
<div class="block">
<div class="field is-grouped is-grouped-multiline">
{% if unused_codes %}
{% url 'mfa_download_recovery_codes' as download_url %}
<div class="control">
{% element button href=download_url %}
{% translate "Download codes" %}
{% endelement %}
</div>
{% endif %}
{% url 'mfa_generate_recovery_codes' as generate_url %}
<div class="control">
{% element button href=generate_url %}
{% translate "Generate new codes" %}
{% endelement %}
</div>
</div>
</div>
{% endblock content %}

View File

@@ -0,0 +1,78 @@
{% extends "mfa/webauthn/base.html" %}
{% load i18n %}
{% load static %}
{% load allauth %}
{% load humanize %}
{% block content %}
{% element h1 %}
{% trans "Security Keys" %}
{% endelement %}
{% if authenticators|length == 0 %}
{% element p %}
{% blocktranslate %}No security keys have been added.{% endblocktranslate %}
{% endelement %}
{% else %}
<article class="panel">
<p class="panel-heading">{% trans "Security Keys" %}</p>
{% for authenticator in authenticators %}
<div class="panel-block">
<div class="level" style="width: 100%;">
<div class="level-left">
<div class="level-item">
{% if authenticator.wrap.is_passwordless is True %}
{% element badge tags="mfa,key,primary" %}
{% translate "Passkey" %}
{% endelement %}
{% elif authenticator.wrap.is_passwordless is False %}
{% element badge tags="mfa,key,secondary" %}
{% translate "Security key" %}
{% endelement %}
{% else %}
{% element badge title=_("This key does not indicate whether it is a passkey.") tags="mfa,key,warning" %}
{% translate "Unspecified" %}
{% endelement %}
{% endif %}
</div>
<div class="level-item">
<strong>
{{ authenticator }}
</strong>
</div>
<div class="level-item">
{% blocktranslate with created_at=authenticator.created_at|date:"SHORT_DATE_FORMAT" %}
Added
on {{ created_at }}{% endblocktranslate %}.
</div>
<div class="level-item">
{% if authenticator.last_used_at %}
{% blocktranslate with last_used=authenticator.last_used_at|naturaltime %}Last used
{{ last_used }}{% endblocktranslate %}
{% else %}
Not used.
{% endif %}
</div>
</div>
<div class="level-right">
<div class="level-item">
{% url 'mfa_edit_webauthn' pk=authenticator.pk as edit_url %}
{% element button tags="mfa,authenticator,edit,tool" href=edit_url %}
{% translate "Edit" %}
{% endelement %}
</div>
<div class="level-item">
{% url 'mfa_remove_webauthn' pk=authenticator.pk as remove_url %}
{% element button tags="mfa,authenticator,danger,delete,tool" href=remove_url %}
{% translate "Remove" %}
{% endelement %}
</div>
</div>
</div>
</div>
{% endfor %}
</article>
{% endif %}
{% url 'mfa_add_webauthn' as add_url %}
{% element button href=add_url %}
{% translate "Add" %}
{% endelement %}
{% endblock %}

View File

@@ -1,4 +1,22 @@
from fellchensammlung.models import User, AdoptionNotice, AdoptionNoticeStatusChoices
from datetime import timedelta
from django.utils import timezone
from fellchensammlung.models import User, AdoptionNotice, AdoptionNoticeStatusChoices, Animal, RescueOrganization
def get_rescue_org_check_stats():
timeframe = timezone.now().date() - timedelta(days=14)
num_rescue_orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=False).filter(
last_checked__lt=timeframe).count()
num_rescue_orgs_checked = RescueOrganization.objects.filter(exclude_from_check=False).filter(
last_checked__gte=timeframe).count()
try:
percentage_checked = 100 * num_rescue_orgs_checked / (num_rescue_orgs_to_check + num_rescue_orgs_checked)
except ZeroDivisionError:
percentage_checked = 100
return num_rescue_orgs_to_check, num_rescue_orgs_checked, percentage_checked
def gather_metrics_data():
@@ -32,6 +50,10 @@ def gather_metrics_data():
active_animals_per_sex[sex] = number_of_animals
active_animals += number_of_animals
num_animal_shelters = RescueOrganization.objects.all().count()
num_rescue_orgs_to_check, num_rescue_orgs_checked, percentage_checked = get_rescue_org_check_stats()
data = {
'users': num_user,
'staff': num_staff,
@@ -45,6 +67,12 @@ def gather_metrics_data():
},
'adoption_notices_without_location': adoption_notices_without_location,
'active_animals': active_animals,
'active_animals_per_sex': active_animals_per_sex
'active_animals_per_sex': active_animals_per_sex,
'rescue_organizations': num_animal_shelters,
'rescue_organization_check': {
'rescue_orgs_to_check': num_rescue_orgs_to_check,
'rescue_orgs_checked': num_rescue_orgs_checked,
'percentage_checked': percentage_checked,
}
}
return data

View File

@@ -1,5 +1,6 @@
import datetime as datetime
import logging
import time
from django.utils.translation import ngettext
from django.utils.translation import gettext as _
@@ -17,10 +18,10 @@ def pluralize(number, letter="e"):
def age_as_hr_string(age: datetime.timedelta) -> str:
days = age.days
weeks = age.days / 7
months = age.days / 30
years = age.days / 365
days = int(age.days)
weeks = int(age.days / 7)
months = int(age.days / 30)
years = int(age.days / 365)
if years >= 1:
months = months - 12 * years
return f'{years:.0f} Jahr{pluralize(years)} und {months:.0f} Monat{pluralize(months)}'
@@ -52,9 +53,9 @@ def time_since_as_hr_string(age: datetime.timedelta) -> str:
elif weeks >= 3:
text = _("vor %(weeks)d Wochen") % {"weeks": weeks}
elif days >= 1:
text = ngettext("vor einem Tag","vor %(count)d Tagen", days,) % {"count": days,}
text = ngettext("vor einem Tag", "vor %(count)d Tagen", days, ) % {"count": days, }
elif hours >= 1:
text = ngettext("vor einer Stunde", "vor %(count)d Stunden", hours,) % {"count": hours,}
text = ngettext("vor einer Stunde", "vor %(count)d Stunden", hours, ) % {"count": hours, }
elif minutes >= 1:
text = ngettext("vor einer Minute", "vor %(count)d Minuten", minutes, ) % {"count": minutes, }
else:
@@ -75,3 +76,23 @@ def is_404(url):
return result.status_code == 404
except requests.RequestException as e:
logging.warning(f"Request to {url} failed: {e}")
class RequestProfiler:
data = []
def clear(self):
self.data = []
def add_status(self, status):
self.data.append((time.time(), status))
@property
def as_relative(self):
first_ts = self.data[0][0]
return [(datum[0] - first_ts, datum[1]) for datum in self.data]
@property
def as_relative_with_ms(self):
first_ts = self.data[0][0]
return [(f"{(datum[0] - first_ts)*1000:.4}ms", datum[1]) for datum in self.data]

View File

@@ -66,6 +66,7 @@ class AdoptionNoticeStatusChoices:
class AwaitingAction(TextChoices):
WAITING_FOR_REVIEW = "awaiting_action_waiting_for_review", _("Waiting for review")
NEEDS_ADDITIONAL_INFO = "awaiting_action_needs_additional_info", _("Needs additional info")
UNCHECKED = "awaiting_action_unchecked", _("Unchecked")
class Closed(TextChoices):
SUCCESSFUL_WITH_NOTFELLCHEN = "closed_successful_with_notfellchen", _("Successful (with Notfellchen)")
@@ -79,7 +80,6 @@ class AdoptionNoticeStatusChoices:
class Disabled(TextChoices):
AGAINST_RULES = "disabled_against_the_rules", _("Against the rules")
UNCHECKED = "disabled_unchecked", _("Unchecked")
OTHER = "disabled_other", _("Other (disabled)")
@classmethod
@@ -102,14 +102,18 @@ class AdoptionNoticeStatusChoicesDescriptions:
_ansc.Closed.ANIMAL_DIED: _("Die zu vermittelnden Tiere sind über die Regenbrücke gegangen."),
_ansc.Closed.FOR_OTHER_ADOPTION_NOTICE: _("Vermittlung wurde zugunsten einer anderen geschlossen."),
_ansc.Closed.NOT_OPEN_ANYMORE: _("Tier(e) stehen nicht mehr zur Vermittlung bereit."),
_ansc.Closed.LINK_TO_MORE_INFO_NOT_REACHABLE: _(
"Der Link zu weiteren Informationen ist nicht mehr erreichbar,"
"die Vermittlung wurde daher automatisch deaktiviert"),
_ansc.Closed.OTHER: _("Vermittlung geschlossen."),
_ansc.AwaitingAction.WAITING_FOR_REVIEW: _(
"Deaktiviert bis Moderator*innen die Vermittlung prüfen können."),
_ansc.AwaitingAction.NEEDS_ADDITIONAL_INFO: _("Deaktiviert bis Informationen nachgetragen werden."),
_ansc.AwaitingAction.UNCHECKED: _(
"Vermittlung deaktiviert bis sie vom Team auf Aktualität geprüft wurde."),
_ansc.Disabled.AGAINST_RULES: _("Vermittlung deaktiviert da sie gegen die Regeln verstößt."),
_ansc.Disabled.UNCHECKED: _("Vermittlung deaktiviert bis sie vom Team auf Aktualität geprüft wurde."),
_ansc.Disabled.OTHER: _("Vermittlung deaktiviert.")
}
@@ -122,3 +126,22 @@ class AdoptionNoticeProcessTemplates:
_bp = "fellchensammlung/partials/adoption_process/" # Base path for ease
mapping = {AdoptionProcess.CONTACT_PERSON_IN_AN: f"{_bp}contact_person_in_an.html",
}
class RegularCheckStatusChoices(models.TextChoices):
REGULAR_CHECK = "regular_check", _("Wird regelmäßig geprüft")
EXCLUDED_NO_ONLINE_LISTING = "excluded_no_online_listing", _("Exkludiert: Tiere werden nicht online gelistet")
EXCLUDED_OTHER_ORG = "excluded_other_org", _("Exkludiert: Andere Organisation wird geprüft")
EXCLUDED_SCOPE = "excluded_scope", _("Exkludiert: Organisation hat nie Notfellchen-relevanten Vermittlungen")
EXCLUDED_OTHER = "excluded_other", _("Exkludiert: Anderer Grund")
##########
## USER ##
##########
reason_for_signup_label = _("Grund für die Registrierung")
reason_for_signup_help_text = _(
"Wir würden gerne wissen warum du dich registrierst, ob du dich z.B. Tiere eines bestimmten Tierheim einstellen "
"willst 'nur mal gucken' willst. Beides ist toll! Wenn du für ein Tierheim/eine Pflegestelle arbeitest "
"kontaktieren wir dich ggf. um dir erweiterte Rechte zu geben.")

View File

@@ -47,6 +47,10 @@ urlpatterns = [
path("tierschutzorganisationen/", views.list_rescue_organizations, name="rescue-organizations"),
path("tierschutzorganisationen/<int:rescue_organization_id>/", views.detail_view_rescue_organization,
name="rescue-organization-detail"),
path("tierschutzorganisationen/<int:rescue_organization_id>/exkludieren", views.exclude_from_regular_check,
name="rescue-organization-exclude"),
path("tierschutzorganisationen/add-exclusion-reason", views.update_exclusion_reason,
name="rescue-organization-add-exclusion-reason"),
path("tierschutzorganisationen/spezialisierung/<slug:species_slug>", views.specialized_rescues,
name="specialized-rescue-organizations"),
@@ -61,6 +65,7 @@ urlpatterns = [
path("ueber-uns/", views.about, name="about"),
path("impressum/", views.imprint, name="imprint"),
path("terms-of-service/", views.terms_of_service, name="terms-of-service"),
path("kontakt/", views.contact, name="contact"),
path("datenschutz/", views.privacy, name="privacy"),
path("ratten-kaufen/", views.buying, name="buying"),
@@ -89,15 +94,6 @@ urlpatterns = [
path("user/notifications/", views.my_notifications, name="user-notifications"),
path('user/me/export/', views.export_own_profile, name='user-me-export'),
path('accounts/register/',
registration_views.HTMLMailRegistrationView.as_view(
form_class=CustomRegistrationForm,
email_body_template="fellchensammlung/mail/activation_email.html",
),
name='django_registration_register',
),
path('accounts/', include('django_registration.backends.activation.urls')),
path('accounts/', include('django.contrib.auth.urls')),
path('change-language', views.change_language, name="change-language"),

View File

@@ -1,5 +1,4 @@
import logging
from datetime import timedelta
from django.contrib.auth.views import redirect_to_login
from django.core.paginator import Paginator
@@ -25,18 +24,20 @@ from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, Moderatio
Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, \
ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices, SocialMediaPost
from .forms import AdoptionNoticeForm, ImageForm, ReportAdoptionNoticeForm, \
CommentForm, ReportCommentForm, AnimalForm, AdoptionNoticeFormAutoAnimal, SpeciesURLForm, RescueOrgInternalComment
CommentForm, ReportCommentForm, AnimalForm, AdoptionNoticeFormAutoAnimal, SpeciesURLForm, RescueOrgInternalComment, \
UpdateRescueOrgRegularCheckStatus
from .models import Language, Announcement
from .tools import i18n, img
from .tools.fedi import post_an_to_fedi
from .tools.geo import GeoAPI, zoom_level_for_radius
from .tools.metrics import gather_metrics_data
from .tools.metrics import gather_metrics_data, get_rescue_org_check_stats
from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
deactivate_404_adoption_notices, send_test_email
from .tasks import post_adoption_notice_save
from rest_framework.authtoken.models import Token
from .tools.model_helpers import AdoptionNoticeStatusChoices, AdoptionNoticeProcessTemplates
from .tools.misc import RequestProfiler
from .tools.model_helpers import AdoptionNoticeStatusChoices, AdoptionNoticeProcessTemplates, RegularCheckStatusChoices
from .tools.search import AdoptionNoticeSearch, RescueOrgSearch
@@ -241,11 +242,17 @@ def search_important_locations(request, important_location_slug):
def search(request, templatename="fellchensammlung/search.html"):
search_profile = RequestProfiler()
search_profile.add_status("Start")
# A user just visiting the search site did not search, only upon completing the search form a user has really
# searched. This will toggle the "subscribe" button
searched = False
search_profile.add_status("Init Search")
search = AdoptionNoticeSearch()
search_profile.add_status("Search from request starting")
search.adoption_notice_search_from_request(request)
search_profile.add_status("Search from request finished")
if request.method == 'POST':
searched = True
if "subscribe_to_search" in request.POST:
@@ -265,10 +272,12 @@ def search(request, templatename="fellchensammlung/search.html"):
subscribed_search = search.get_subscription_or_none(request.user)
else:
subscribed_search = None
search_profile.add_status("End of POST")
site_title = _("Suche")
site_description = _("Ratten in Tierheimen und Rattenhilfen in der Nähe suchen.")
canonical_url = reverse("search")
search_profile.add_status("Start of context")
context = {"adoption_notices": search.get_adoption_notices(),
"search_form": search.search_form,
"place_not_found": search.place_not_found,
@@ -285,6 +294,10 @@ def search(request, templatename="fellchensammlung/search.html"):
"site_title": site_title,
"site_description": site_description,
"canonical_url": canonical_url}
search_profile.add_status("End of context")
if request.user.is_superuser:
context["profile"] = search_profile.as_relative_with_ms
search_profile.add_status("Finished - returing render")
return render(request, templatename, context=context)
@@ -509,6 +522,11 @@ def terms_of_service(request):
)
def contact(request):
text = i18n.get_text_by_language("contact")
return render_text(request, text)
def report_adoption(request, adoption_notice_id):
"""
Form to report adoption notices
@@ -572,12 +590,20 @@ def report_detail_success(request, report_id):
def user_detail(request, user, token=None):
user_detail_profile = RequestProfiler()
user_detail_profile.add_status("Start")
adoption_notices = AdoptionNotice.objects.filter(owner=user)
user_detail_profile.add_status("Finished fetching adoption notices")
context = {"user": user,
"adoption_notices": AdoptionNotice.objects.filter(owner=user),
"adoption_notices": adoption_notices,
"notifications": Notification.objects.filter(user_to_notify=user, read=False),
"search_subscriptions": SearchSubscription.objects.filter(owner=user), }
user_detail_profile.add_status("End of context")
if token is not None:
context["token"] = token
user_detail_profile.add_status("Finished - returning to renderer")
if request.user.is_superuser:
context["profile"] = user_detail_profile.as_relative_with_ms
return render(request, 'fellchensammlung/details/detail-user.html', context=context)
@@ -648,7 +674,7 @@ def my_notifications(request):
context = {"notifications_unread": Notification.objects.filter(user_to_notify=request.user, read=False).order_by(
"-created_at"),
"notifications_read_last": Notification.objects.filter(user_to_notify=request.user,
read=True).order_by("-read_at")}
read=True).order_by("-read_at")[:10]}
return render(request, 'fellchensammlung/notifications.html', context=context)
@@ -674,7 +700,7 @@ def updatequeue(request):
last_checked_adoption_list = AdoptionNotice.objects.filter(owner=request.user).order_by("last_checked")
adoption_notices_active = [adoption for adoption in last_checked_adoption_list if adoption.is_active]
adoption_notices_disabled = [adoption for adoption in last_checked_adoption_list if
adoption.adoption_notice_status == AdoptionNoticeStatusChoices.Disabled.UNCHECKED]
adoption.adoption_notice_status == AdoptionNoticeStatusChoices.AwaitingAction.UNCHECKED]
context = {"adoption_notices_disabled": adoption_notices_disabled,
"adoption_notices_active": adoption_notices_active}
return render(request, 'fellchensammlung/updatequeue.html', context=context)
@@ -840,8 +866,10 @@ def rescue_organization_check(request, context=None):
action = request.POST.get("action")
if action == "checked":
rescue_org.set_checked()
elif action == "exclude":
rescue_org.set_exclusion_from_checks()
Log.objects.create(user=request.user, action="rescue_organization_checked", )
elif action == "toggle_active_communication":
rescue_org.ongoing_communication = not rescue_org.ongoing_communication
rescue_org.save()
elif action == "set_species_url":
species_url_form = SpeciesURLForm(request.POST)
@@ -852,6 +880,7 @@ def rescue_organization_check(request, context=None):
elif action == "update_internal_comment":
comment_form = RescueOrgInternalComment(request.POST, instance=rescue_org)
if comment_form.is_valid():
Log.objects.create(user=request.user, action="rescue_organization_added_internal_comment", )
comment_form.save()
rescue_orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=False,
@@ -865,16 +894,7 @@ def rescue_organization_check(request, context=None):
org.id: RescueOrgInternalComment(instance=org) for org in rescue_orgs_to_comment
}
timeframe = timezone.now().date() - timedelta(days=14)
num_rescue_orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=False).filter(
last_checked__lt=timeframe).count()
num_rescue_orgs_checked = RescueOrganization.objects.filter(exclude_from_check=False).filter(
last_checked__gte=timeframe).count()
try:
percentage_checked = 100 * num_rescue_orgs_checked / (num_rescue_orgs_to_check + num_rescue_orgs_checked)
except ZeroDivisionError:
percentage_checked = 100
num_rescue_orgs_to_check, num_rescue_orgs_checked, percentage_checked = get_rescue_org_check_stats()
context["rescue_orgs_to_check"] = rescue_orgs_to_check
context["rescue_orgs_last_checked"] = rescue_orgs_last_checked
@@ -900,6 +920,42 @@ def rescue_organization_check_dq(request):
return rescue_organization_check(request, context)
def exclude_from_regular_check(request, rescue_organization_id, source="organization-check"):
rescue_org = get_object_or_404(RescueOrganization, pk=rescue_organization_id)
if request.method == "POST":
form = UpdateRescueOrgRegularCheckStatus(request.POST, instance=rescue_org)
if form.is_valid():
form.save()
to_be_excluded = form.cleaned_data["regular_check_status"] != RegularCheckStatusChoices.REGULAR_CHECK
rescue_org.exclude_from_check = to_be_excluded
rescue_org.save()
if to_be_excluded:
Log.objects.create(user=request.user,
action="rescue_organization_excluded_from_check",
text=f"New status: {form.cleaned_data['regular_check_status']}")
return redirect(reverse(source))
else:
form = UpdateRescueOrgRegularCheckStatus(instance=rescue_org)
org_meta = rescue_org._meta
context = {"form": form, "org": rescue_org, "org_meta": org_meta}
return render(request, 'fellchensammlung/forms/form-exclude-org-from-check.html', context=context)
@user_passes_test(user_is_trust_level_or_above)
def update_exclusion_reason(request):
"""
This view will redirect to update a rescue org that not yet has an exclusion reason but is excluded
"""
orgs_to_check = RescueOrganization.objects.filter(exclude_from_check=True,
regular_check_status=RegularCheckStatusChoices.REGULAR_CHECK)
if orgs_to_check.count() > 0:
return exclude_from_regular_check(request, orgs_to_check[0].pk,
source="rescue-organization-add-exclusion-reason")
else:
return render(request, "fellchensammlung/errors/404.html", status=404)
@user_passes_test(user_is_trust_level_or_above)
def moderation_tools_overview(request):
context = None

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ https://docs.djangoproject.com/en/3.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""
import json
from pathlib import Path
import os
import configparser
@@ -74,6 +74,20 @@ except configparser.NoSectionError:
raise BaseException("No config found or no Django Secret is configured!")
DEBUG = config.getboolean('django', 'debug', fallback=False)
# Internal IPs
internal_ip_raw_config_value = config.get("django", "internal_ips", fallback=None)
if internal_ip_raw_config_value:
INTERNAL_IPS = json.loads(internal_ip_raw_config_value)
# Cache
if config.getboolean('django', 'cache', fallback=False):
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "uniques-snowflake",
}
}
""" DATABASE """
DB_BACKEND = config.get("database", "backend", fallback="sqlite3")
DB_NAME = config.get("database", "name", fallback="notfellchen.sqlite3")
@@ -130,6 +144,37 @@ ACCOUNT_ACTIVATION_DAYS = 7 # One-week activation window
REGISTRATION_OPEN = True
REGISTRATION_SALT = "notfellchen"
AUTHENTICATION_BACKENDS = [
# Needed to login by username in Django admin, regardless of `allauth`
'django.contrib.auth.backends.ModelBackend',
# `allauth` specific authentication methods, such as login by email
'allauth.account.auth_backends.AuthenticationBackend',
]
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_EMAIL_VERIFICATION_BY_CODE_ENABLED = True
ACCOUNT_SIGNUP_FIELDS = ['username*', "email*", "password1*"]
ACCOUNT_SIGNUP_FORM_CLASS = 'fellchensammlung.forms.AddedRegistrationForm'
MFA_SUPPORTED_TYPES = ["totp",
"webauthn",
"recovery_codes"]
MFA_TOTP_TOLERANCE = 1
MFA_TOTP_ISSUER = config.get('security', 'totp_issuer', fallback="Notfellchen")
MFA_PASSKEY_LOGIN_ENABLED = True
MFA_PASSKEY_SIGNUP_ENABLED = True
# Optional -- use for local development only: the WebAuthn uses the
#``fido2`` package, and versions up to including version 1.1.3 do not
# regard localhost as a secure origin, which is problematic during
# local development and testing.
MFA_WEBAUTHN_ALLOW_INSECURE_ORIGIN = config.get('security', 'webauth_allow_insecure_origin', fallback=False)
""" SECURITY.TXT """
SEC_CONTACT = config.get("security", "Contact", fallback="julian-samuel@gebuehr.net")
SEC_EXPIRES = config.get("security", "Expires", fallback="2028-03-17T07:00:00.000Z")
@@ -182,7 +227,11 @@ INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
"django.contrib.humanize",
'django.contrib.messages',
'allauth',
'allauth.account',
'allauth.mfa',
'django.contrib.staticfiles',
"django.contrib.sitemaps",
'fontawesomefree',
@@ -193,21 +242,28 @@ INSTALLED_APPS = [
'rest_framework.authtoken',
'drf_spectacular',
'drf_spectacular_sidecar', # required for Django collectstatic discovery
'widget_tweaks'
'widget_tweaks',
"debug_toolbar",
'admin_extra_buttons',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
"debug_toolbar.middleware.DebugToolbarMiddleware",
# Static file serving & caching
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
# Needs to be after SessionMiddleware and before CommonMiddleware
'django.middleware.locale.LocaleMiddleware',
"django.middleware.cache.UpdateCacheMiddleware",
'django.middleware.common.CommonMiddleware',
"django.middleware.cache.FetchFromCacheMiddleware",
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# allauth middleware, needs to be after message middleware
"allauth.account.middleware.AccountMiddleware",
]
ROOT_URLCONF = 'notfellchen.urls'
@@ -222,6 +278,7 @@ TEMPLATES = [
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
# Needed for allauth
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.media',

View File

@@ -18,12 +18,14 @@ from django.conf.urls.i18n import i18n_patterns
from django.contrib import admin
from django.urls import include, path
from django.conf import settings
from django.conf.urls.static import static
from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [
path('admin/', admin.site.urls),
]
path('accounts/', include('allauth.urls')),
] + debug_toolbar_urls()
urlpatterns += i18n_patterns(
path("", include("fellchensammlung.urls")),

View File

@@ -47,7 +47,7 @@
<div class="block">
<a class="button is-warning" href="{% url 'password_reset' %}">{% translate "Passwort vergessen?" %}</a>
<a class="button is-link"
href="{% url 'django_registration_register' %}">{% translate "Registrieren" %}</a>
href="{% url 'account_signup' %}">{% translate "Registrieren" %}</a>
</div>
</div>
{% endif %}

15
src/tests/test_misc.py Normal file
View File

@@ -0,0 +1,15 @@
import datetime
from fellchensammlung.tools.misc import age_as_hr_string
from django.test import TestCase
class AgeTest(TestCase):
def test_age_as_hr_string(self):
self.assertEqual("7 Wochen", age_as_hr_string(datetime.timedelta(days=50)))
self.assertEqual("3 Monate", age_as_hr_string(datetime.timedelta(days=100)))
self.assertEqual("10 Monate", age_as_hr_string(datetime.timedelta(days=300)))
self.assertEqual("1 Jahr und 4 Monate", age_as_hr_string(datetime.timedelta(days=500)))
self.assertEqual("1 Jahr und 11 Monate", age_as_hr_string(datetime.timedelta(days=700)))
self.assertEqual("2 Jahre und 6 Monate", age_as_hr_string(datetime.timedelta(days=900)))