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

View File

@@ -9,3 +9,4 @@ User Dokumentation
vermittlungen.rst
moderationskonzept.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 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,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.
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"}
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

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

@@ -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 .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
from .tools.model_helpers import ndm as NotificationDisplayMapping
@@ -553,9 +554,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

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

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

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

@@ -4,56 +4,11 @@
<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 %}
{% 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">

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 %}
<div class="block content">
{{ rescue_organization.description_short | render_markdown }}
</div>
{% endif %}
</div>
</div>
</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>
@@ -65,4 +67,5 @@
{% endfor %}
</div>
</div>
{% include "fellchensammlung/partials/modal-shortcuts.html" %}
{% 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

@@ -17,10 +17,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 +52,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:

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,17 @@ 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.")
}

View File

@@ -61,6 +61,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"),

View File

@@ -30,7 +30,7 @@ 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
@@ -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):
"""
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")
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)
@@ -865,16 +870,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

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