Compare commits

..

18 Commits

Author SHA1 Message Date
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
30 changed files with 527 additions and 106 deletions

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

@@ -0,0 +1,67 @@
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
"""
has_content = False
required_arguments = 2 # html and png
def run(self) -> list[nodes.Node]:
html_path = self.arguments[0]
png_path = self.arguments[1]
env = self.state.document.settings.env
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()
container = nodes.container()
# HTML output -> raw HTML node
if self.builder.format == "html":
# Embed the HTML file contents directly
raw_html_node = nodes.raw(
"",
f'<div class="drawio-diagram">{open(html_path, encoding="utf-8").read()}</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]
@property
def builder(self):
# Helper to access the builder from the directive context
return self.state.document.settings.env.app.builder
def setup(app: Sphinx) -> ExtensionMetadata:
app.add_directive("drawio", DrawioDirective)
return {
"version": "0.1",
"parallel_read_safe": True,
"parallel_write_safe": True,
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View File

@@ -9,3 +9,4 @@ User Dokumentation
vermittlungen.rst vermittlungen.rst
moderationskonzept.rst moderationskonzept.rst
benachrichtigungen.rst benachrichtigungen.rst
organisationen-pruefen.rst

View File

@@ -0,0 +1,46 @@
Tiere in Vermittlung systematisch entdecken & eintragen
=======================================================
Notfellchen hat eine Liste der meisten deutschen Tierheime und anderer Tierschutzorganisationen (550, Stand Oktober 2025).
Die meisten dieser Organisationen (412, Stand Oktober 2025) 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.
.. 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` |
+------------------------------------------------------+---------------+

View File

@@ -1,7 +1,7 @@
Vermittlungen Vermittlungen
============= =============
Vermittlungen können von allen Nutzer*innen mit Account erstellt werden. Vermittlungen normaler Nutzer*innen kommen dann in eine Warteschlange und werden vom Admin & Modertionsteam geprüft und sichtbar geschaltet. Vermittlungen können von allen Nutzer\*innen mit Account erstellt werden. Vermittlungen normaler Nutzer*innen kommen dann in eine Warteschlange und werden vom Admin & Modertionsteam geprüft und sichtbar geschaltet.
Tierheime und Pflegestellen können auf Anfrage einen Koordinations-Status bekommen, wodurch sie Vermittlungsanzeigen erstellen können die direkt öffentlich sichtbar sind. Tierheime und Pflegestellen können auf Anfrage einen Koordinations-Status bekommen, wodurch sie Vermittlungsanzeigen erstellen können die direkt öffentlich sichtbar sind.
Jede Vermittlung hat ein "Zuletzt-geprüft" Datum, das anzeigt, wann ein Mensch zuletzt überprüft hat, ob die Anzeige noch aktuell ist. Jede Vermittlung hat ein "Zuletzt-geprüft" Datum, das anzeigt, wann ein Mensch zuletzt überprüft hat, ob die Anzeige noch aktuell ist.
@@ -15,3 +15,108 @@ 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.
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

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

View File

@@ -35,13 +35,18 @@ class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
required=False, required=False,
allow_null=True allow_null=True
) )
url = serializers.SerializerMethodField()
photos = ImageSerializer(many=True, read_only=True) 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", "location", "location_details", "organization", "photos", "adoption_notice_status",
"url"]
class AdoptionNoticeGeoJSONSerializer(serializers.ModelSerializer): class AdoptionNoticeGeoJSONSerializer(serializers.ModelSerializer):

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

@@ -12,7 +12,8 @@ from .tools import misc, geo
from notfellchen.settings import MEDIA_URL, base_url from notfellchen.settings import MEDIA_URL, base_url
from .tools.geo import LocationProxy, Position from .tools.geo import LocationProxy, Position
from .tools.misc import time_since_as_hr_string 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
from .tools.model_helpers import ndm as NotificationDisplayMapping from .tools.model_helpers import ndm as NotificationDisplayMapping
@@ -553,9 +554,13 @@ class AdoptionNotice(models.Model):
def is_awaiting_action(self): def is_awaiting_action(self):
return self.adoption_notice_status in self._values_of(AdoptionNoticeStatusChoices.AwaitingAction.choices) 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): def set_unchecked(self):
self.last_checked = timezone.now() self.last_checked = timezone.now()
self.adoption_notice_status = AdoptionNoticeStatusChoices.Disabled.UNCHECKED self.adoption_notice_status = AdoptionNoticeStatusChoices.AwaitingAction.UNCHECKED
self.save() self.save()
for subscription in self.get_subscriptions(): for subscription in self.get_subscriptions():

View File

@@ -45,6 +45,7 @@ $confirm: hsl(133deg, 100%, calc(41% + 0%));
p > a { p > a {
text-decoration: underline; text-decoration: underline;
word-break: break-all;
} }
p > a.button { p > a.button {
@@ -351,3 +352,12 @@ AN Cards
.embed-main-content { .embed-main-content {
padding: 20px 10px 20px 10px; 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"); $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

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

View File

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

View File

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

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

@@ -4,56 +4,11 @@
<div class="panel block"> <div class="panel block">
<p class="panel-heading">{% trans "Kontaktdaten" %}</p> <p class="panel-heading">{% trans "Kontaktdaten" %}</p>
{% if org.has_contact_data %} {% if org.has_contact_data %}
{% if org.website %} {% include "fellchensammlung/partials/partial-in-panel-contact-data.html" %}
<a class="panel-block is-active" href="{{ org.website }}" target="_blank"> {% elif org.parent_org.has_contact_data %}
<span class="panel-icon"> {% with org=org.parent_org %}
<i class="fas fa-globe" aria-label="{% translate "Website" %}"></i> {% include "fellchensammlung/partials/partial-in-panel-contact-data.html" %}
</span> {% endwith %}
{{ 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 %} {% else %}
<div class="panel-block is-active"> <div class="panel-block is-active">
<span class="panel-icon"> <span class="panel-icon">

View File

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

View File

@@ -23,9 +23,11 @@
</div> </div>
<div class="column"> <div class="column">
{% if dq %} {% 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 %} {% 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 %} {% endif %}
</div> </div>
</div> </div>
@@ -65,4 +67,5 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% include "fellchensammlung/partials/modal-shortcuts.html" %}
{% endblock %} {% 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(): def gather_metrics_data():
@@ -32,6 +50,10 @@ def gather_metrics_data():
active_animals_per_sex[sex] = number_of_animals active_animals_per_sex[sex] = number_of_animals
active_animals += 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 = { data = {
'users': num_user, 'users': num_user,
'staff': num_staff, 'staff': num_staff,
@@ -45,6 +67,12 @@ def gather_metrics_data():
}, },
'adoption_notices_without_location': adoption_notices_without_location, 'adoption_notices_without_location': adoption_notices_without_location,
'active_animals': active_animals, '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 return data

View File

@@ -17,10 +17,10 @@ def pluralize(number, letter="e"):
def age_as_hr_string(age: datetime.timedelta) -> str: def age_as_hr_string(age: datetime.timedelta) -> str:
days = age.days days = int(age.days)
weeks = age.days / 7 weeks = int(age.days / 7)
months = age.days / 30 months = int(age.days / 30)
years = age.days / 365 years = int(age.days / 365)
if years >= 1: if years >= 1:
months = months - 12 * years months = months - 12 * years
return f'{years:.0f} Jahr{pluralize(years)} und {months:.0f} Monat{pluralize(months)}' return f'{years:.0f} Jahr{pluralize(years)} und {months:.0f} Monat{pluralize(months)}'

View File

@@ -66,6 +66,7 @@ class AdoptionNoticeStatusChoices:
class AwaitingAction(TextChoices): class AwaitingAction(TextChoices):
WAITING_FOR_REVIEW = "awaiting_action_waiting_for_review", _("Waiting for review") WAITING_FOR_REVIEW = "awaiting_action_waiting_for_review", _("Waiting for review")
NEEDS_ADDITIONAL_INFO = "awaiting_action_needs_additional_info", _("Needs additional info") NEEDS_ADDITIONAL_INFO = "awaiting_action_needs_additional_info", _("Needs additional info")
UNCHECKED = "awaiting_action_unchecked", _("Unchecked")
class Closed(TextChoices): class Closed(TextChoices):
SUCCESSFUL_WITH_NOTFELLCHEN = "closed_successful_with_notfellchen", _("Successful (with Notfellchen)") SUCCESSFUL_WITH_NOTFELLCHEN = "closed_successful_with_notfellchen", _("Successful (with Notfellchen)")
@@ -79,7 +80,6 @@ class AdoptionNoticeStatusChoices:
class Disabled(TextChoices): class Disabled(TextChoices):
AGAINST_RULES = "disabled_against_the_rules", _("Against the rules") AGAINST_RULES = "disabled_against_the_rules", _("Against the rules")
UNCHECKED = "disabled_unchecked", _("Unchecked")
OTHER = "disabled_other", _("Other (disabled)") OTHER = "disabled_other", _("Other (disabled)")
@classmethod @classmethod
@@ -102,14 +102,17 @@ class AdoptionNoticeStatusChoicesDescriptions:
_ansc.Closed.ANIMAL_DIED: _("Die zu vermittelnden Tiere sind über die Regenbrücke gegangen."), _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.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.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.Closed.OTHER: _("Vermittlung geschlossen."),
_ansc.AwaitingAction.WAITING_FOR_REVIEW: _( _ansc.AwaitingAction.WAITING_FOR_REVIEW: _(
"Deaktiviert bis Moderator*innen die Vermittlung prüfen können."), "Deaktiviert bis Moderator*innen die Vermittlung prüfen können."),
_ansc.AwaitingAction.NEEDS_ADDITIONAL_INFO: _("Deaktiviert bis Informationen nachgetragen werden."), _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.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.") _ansc.Disabled.OTHER: _("Vermittlung deaktiviert.")
} }

View File

@@ -61,6 +61,7 @@ urlpatterns = [
path("ueber-uns/", views.about, name="about"), path("ueber-uns/", views.about, name="about"),
path("impressum/", views.imprint, name="imprint"), path("impressum/", views.imprint, name="imprint"),
path("terms-of-service/", views.terms_of_service, name="terms-of-service"), 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("datenschutz/", views.privacy, name="privacy"),
path("ratten-kaufen/", views.buying, name="buying"), path("ratten-kaufen/", views.buying, name="buying"),

View File

@@ -30,7 +30,7 @@ from .models import Language, Announcement
from .tools import i18n, img from .tools import i18n, img
from .tools.fedi import post_an_to_fedi from .tools.fedi import post_an_to_fedi
from .tools.geo import GeoAPI, zoom_level_for_radius 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, \ from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
deactivate_404_adoption_notices, send_test_email deactivate_404_adoption_notices, send_test_email
from .tasks import post_adoption_notice_save from .tasks import post_adoption_notice_save
@@ -509,6 +509,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): def report_adoption(request, adoption_notice_id):
""" """
Form to report adoption notices Form to report adoption notices
@@ -674,7 +679,7 @@ def updatequeue(request):
last_checked_adoption_list = AdoptionNotice.objects.filter(owner=request.user).order_by("last_checked") 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_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_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, context = {"adoption_notices_disabled": adoption_notices_disabled,
"adoption_notices_active": adoption_notices_active} "adoption_notices_active": adoption_notices_active}
return render(request, 'fellchensammlung/updatequeue.html', context=context) return render(request, 'fellchensammlung/updatequeue.html', context=context)
@@ -865,16 +870,7 @@ def rescue_organization_check(request, context=None):
org.id: RescueOrgInternalComment(instance=org) for org in rescue_orgs_to_comment org.id: RescueOrgInternalComment(instance=org) for org in rescue_orgs_to_comment
} }
timeframe = timezone.now().date() - timedelta(days=14) num_rescue_orgs_to_check, num_rescue_orgs_checked, percentage_checked = get_rescue_org_check_stats()
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
context["rescue_orgs_to_check"] = rescue_orgs_to_check context["rescue_orgs_to_check"] = rescue_orgs_to_check
context["rescue_orgs_last_checked"] = rescue_orgs_last_checked context["rescue_orgs_last_checked"] = rescue_orgs_last_checked

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