Compare commits

...

22 Commits

Author SHA1 Message Date
e38234b736 docs: Add basics on Vermittlungen
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-10-10 17:10:17 +02:00
ce38002676 chore: bump version 2024-10-10 17:09:33 +02:00
314cdfdd7c fix: Add missing celery file 2024-10-10 17:07:58 +02:00
4504a18f60 feat: add celery task to deactivate unchecked 2024-10-10 17:07:11 +02:00
df41028e99 feat: Add unchecked AN cleanup to health check 2024-10-10 17:06:50 +02:00
f404cfa0a3 feat: Add unchecked as AN status 2024-10-10 17:06:29 +02:00
72dedb6b0c fix: Build psycopg2 from source, pin python minor version 2024-10-10 14:33:18 +02:00
17468097ec fix: Add tag 2024-10-10 07:39:53 +02:00
28331f105a feat: Use celery for location queries 2024-10-10 07:39:44 +02:00
39893c2185 feat: Add basic celery config 2024-10-09 21:54:31 +02:00
ab2b91735e feat: Set title per page 2024-10-09 20:46:50 +02:00
1b9574cca9 refactor: Reorder 2024-10-05 13:26:32 +02:00
0d52101f22 feat: Add basic redirect service 2024-10-05 11:22:10 +02:00
96c0c1218f refactor: identation 2024-10-05 11:05:50 +02:00
864c76bc21 feat: Add domain customtag 2024-10-05 11:05:33 +02:00
c3646e6334 fix: typo 2024-10-05 11:02:37 +02:00
83a219df0c refactor: Add staticmethod to get a number of texts 2024-10-05 11:02:26 +02:00
e6428965c4 fix: Use pre-built dockerfile
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-10-04 16:50:16 +02:00
7f31c58abf fix attempt
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-10-04 16:23:52 +02:00
1c3d2c7cf5 fix attempt
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-10-04 16:20:08 +02:00
90489e01b0 fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-10-04 16:13:19 +02:00
ed8ccd96f4 fix
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-10-04 16:10:04 +02:00
29 changed files with 470 additions and 190 deletions

View File

@ -1,17 +1,6 @@
--- ---
steps: steps:
create_docker_image:
image: woodpeckerci/plugin-docker-buildx
settings:
repo: moanos/sphinx-rtd
dockerfile: docs/Dockerfile
tag: latest
username:
from_secret: docker_username
password:
from_secret: docker_password
build: build:
image: moanos/sphinx-rtd image: moanos/sphinx-rtd
commands: commands:

View File

@ -1,10 +1,12 @@
FROM python:3-slim FROM python:3.11-slim
# Use 3.11 to avoid django.core.exceptions.ImproperlyConfigured: Error loading psycopg2 or psycopg module
MAINTAINER Julian-Samuel Gebühr MAINTAINER Julian-Samuel Gebühr
ENV DOCKER_BUILD=true ENV DOCKER_BUILD=true
RUN apt update RUN apt update
RUN apt install gettext -y RUN apt install gettext -y
RUN apt install libpq-dev gcc -y
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app
RUN mkdir /app/data RUN mkdir /app/data

View File

@ -106,3 +106,20 @@ Use a program like `gtranslator` or `poedit` to start translations
| Edit adoption notice | User that created, Moderator, Admin | | Edit adoption notice | User that created, Moderator, Admin |
| Edit animal | User that created, Moderator, Admin | | Edit animal | User that created, Moderator, Admin |
| Add animal/photo to adoption notice | User that created, Moderator, Admin | | Add animal/photo to adoption notice | User that created, Moderator, Admin |
# Celery and KeyDB
Start KeyDB docker container
```zsh
docker run -d --name keydb -p 6379:6379 eqalpha/keydb
```
Start worker
```zsh
celery -A notfellchen.celery worker
```
Start beat
```zsh
celery -A notfellchen.celery beat
```

View File

@ -1,5 +0,0 @@
FROM sphinxdoc/sphinx
WORKDIR .
ADD requirements.txt ./
RUN pip3 install -r requirements.txt

View File

@ -1,3 +1,18 @@
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.
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.
Nach 3 Wochen ohne Prüfung werden Anzeigen automatisch von der Seite entfernt und nur dann wieder freigeschaltet, wenn eine manuelle Prüfung erfolgt.
Darüber hinaus werden einmal täglich die verlinkten Seiten automatisiert geprüft. Wenn eine Vermittlungs-Seite bei einem Tierheim oder einer Pflegestelle entfernt wurde, wird die Anzeige ebenfalls deaktiviert.
Vermittlungen können von allen Menschen, auch ohne Account gemeldet werden. Grund dafür kann sein, dass Informationen veraltet sind oder Verdacht von Tierwohlgefärdung. Gemeldete Vermittlungen werden vom Moderationsteam geprüft und ggf. entfernt.
Die Kommentarfunktion von Vermittlungen ermöglicht es angemeldeten Nutzer*innen zusätzliche Informationen hinzuzufügen oder Fragen zu stellen.
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.

View File

@ -35,10 +35,11 @@ dependencies = [
"markdown", "markdown",
"Pillow", "Pillow",
"django-registration", "django-registration",
"psycopg2-binary", "psycopg2",
"django-crispy-forms", "django-crispy-forms",
"crispy-bootstrap4", "crispy-bootstrap4",
"djangorestframework" "djangorestframework",
"celery[redis]"
] ]
dynamic = ["version", "readme"] dynamic = ["version", "readme"]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.1.1 on 2024-10-10 14:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0007_alter_adoptionnotice_last_checked'),
]
operations = [
migrations.AlterField(
model_name='adoptionnoticestatus',
name='minor_status',
field=models.CharField(choices=[('searching', 'searching'), ('interested', 'interested'), ('waiting_for_review', 'waiting_for_review'), ('needs_additional_info', 'needs_additional_info'), ('successful_with_notfellchen', 'successful_with_notfellchen'), ('successful_without_notfellchen', 'successful_without_notfellchen'), ('animal_died', 'animal_died'), ('closed_for_other_adoption_notice', 'closed_for_other_adoption_notice'), ('not_open_for_adoption_anymore', 'not_open_for_adoption_anymore'), ('other', 'other'), ('against_the_rules', 'against_the_rules'), ('missing_information', 'missing_information'), ('technical_error', 'technical_error'), ('unchecked', 'unchecked')], max_length=200),
),
migrations.AlterField(
model_name='announcement',
name='publish_start_time',
field=models.DateTimeField(verbose_name='Veröffentlichungszeitpunkt'),
),
]

View File

@ -134,6 +134,12 @@ class Location(models.Model):
) )
return location return location
@staticmethod
def add_location_to_object(instance):
"""Search the location given in the location string and add it to the object"""
location = Location.get_location_from_string(instance.location_string)
instance.location = location
instance.save()
class RescueOrganization(models.Model): class RescueOrganization(models.Model):
def __str__(self): def __str__(self):
@ -324,6 +330,7 @@ class AdoptionNoticeStatus(models.Model):
"against_the_rules": "against_the_rules", "against_the_rules": "against_the_rules",
"missing_information": "missing_information", "missing_information": "missing_information",
"technical_error": "technical_error", "technical_error": "technical_error",
"unchecked": "unchecked",
"other": "other" "other": "other"
} }
} }
@ -351,6 +358,11 @@ class AdoptionNoticeStatus(models.Model):
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"] self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"]
self.save() self.save()
def deactivate_unchecked(self):
self.major_status = self.MAJOR_STATUS_CHOICES[self.DISABLED]
self.minor_status = self.MINOR_STATUS_CHOICES[self.DISABLED]["unchecked"]
self.save()
class Animal(models.Model): class Animal(models.Model):
MALE_NEUTERED = "M_N" MALE_NEUTERED = "M_N"
@ -516,6 +528,18 @@ class Text(models.Model):
def __str__(self): def __str__(self):
return f"{self.title} ({self.language})" return f"{self.title} ({self.language})"
@staticmethod
def get_texts(text_codes, language, expandable_dict=None):
if expandable_dict is None:
expandable_dict = {}
for text_code in text_codes:
try:
expandable_dict[text_code] = Text.objects.get(text_code=text_code, language=language, )
except Text.DoesNotExist:
expandable_dict[text_code] = None
return expandable_dict
class Announcement(Text): class Announcement(Text):
""" """
@ -523,7 +547,7 @@ class Announcement(Text):
""" """
logged_in_only = models.BooleanField(default=False) logged_in_only = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
publish_start_time = models.DateTimeField(verbose_name="Veröffentlichungszeitpunk") publish_start_time = models.DateTimeField(verbose_name="Veröffentlichungszeitpunkt")
publish_end_time = models.DateTimeField(verbose_name="Veröffentlichungsende") publish_end_time = models.DateTimeField(verbose_name="Veröffentlichungsende")
IMPORTANT = "important" IMPORTANT = "important"
WARNING = "warning" WARNING = "warning"

View File

@ -1,3 +1,7 @@
/***************/
/* MAIN COLORS */
/***************/
:root { :root {
--primary-light-one: #5daa68; --primary-light-one: #5daa68;
--primary-light-two: #4a9455; --primary-light-two: #4a9455;
@ -18,7 +22,9 @@
--text-three: var(--primary-light-one); --text-three: var(--primary-light-one);
--shadow-three: var(--primary-dark-one); --shadow-three: var(--primary-dark-one);
} }
/**************************/
/* TAG SETTINGS (GENERAL) */
/**************************/
html, body { html, body {
margin: 0; margin: 0;
height: 100%; height: 100%;
@ -30,10 +36,6 @@ body {
} }
.content-box {
margin: 20px;
}
table { table {
width: 100%; width: 100%;
} }
@ -79,6 +81,127 @@ h1, h2 {
box-sizing: border-box; box-sizing: border-box;
} }
textarea {
border-radius: 10px;
width: 100%;
margin: 5px;
}
/**************/
/* CONTAINERS */
/**************/
.container-cards {
display: flex;
flex-wrap: wrap;
}
.card {
flex: 1 25%;
margin: 10px;
border-radius: 8px;
padding: 5px;
background: var(--background-three);
color: var(--text-two);
}
.container-edit-buttons {
display: flex;
flex-wrap: wrap;
.btn {
margin: 5px;
}
}
/*******************************/
/* PARTIAL SPECIFIC CONTAINERS */
/*******************************/
.detail-animal-header {
border-radius: 5px;
display: flex;
align-items: center;
justify-content: space-between;
}
@media screen and (max-width: 800px) {
.detail-animal-header {
display: block;
}
}
.profile-card {
display: flex;
border-radius: 0px 0px 8px 8px;
background-color: var(--highlight-two);
color: var(--highlight-one-text);
.btn2 {
height: 40px;
}
.button_darken:hover {
background-color: var(--highlight-one);
color: var(--highlight-one-text);
}
button {
background: inherit;
color: inherit;
}
}
.container-comment-form {
width: 80%;
color: var(--text-one);
b {
text-shadow: 2px 2px var(--shadow-one);
}
}
/***********/
/* BUTTONS */
/***********/
select, .button {
width: 100%;
border: none;
border-radius: 4px;
opacity: 1;
background-color: var(--secondary-light-one);
}
.btn {
background-color: var(--primary-light-one);
color: var(--secondary-light-one);
padding: 16px;
border-radius: 8px;
border: none;
font-weight: bold;
}
.btn2 {
background-color: var(--secondary-light-one);
color: var(--primary-dark-one);
padding: 8px;
border-radius: 4px;
border: none;
margin: 5px;
}
/*********************/
/* UNIQUE COMPONENTS */
/*********************/
.content-box {
margin: 20px;
}
.header { .header {
overflow: hidden; overflow: hidden;
background-color: var(--background-two); background-color: var(--background-two);
@ -110,14 +233,7 @@ h1, h2 {
color: white; color: white;
} }
select, .button {
width: 100%;
border: none;
border-radius: 4px;
opacity: 1;
background-color: var(--secondary-light-one);
}
.header-right select.option { .header-right select.option {
color: #000; color: #000;
@ -135,26 +251,6 @@ select, .button {
height: 67px; height: 67px;
} }
.profile-card {
display: flex;
border-radius: 0px 0px 8px 8px;
background-color: var(--highlight-two);
color: var(--highlight-one-text);
.btn2 {
height: 40px;
}
.button_darken:hover {
background-color: var(--highlight-one);
color: var(--highlight-one-text);
}
button {
background: inherit;
color: inherit;
}
}
@media screen and (max-width: 500px) { @media screen and (max-width: 500px) {
.header a { .header a {
@ -172,25 +268,6 @@ select, .button {
height: 40px; height: 40px;
} }
.btn {
background-color: var(--primary-light-one);
color: var(--secondary-light-one);
padding: 16px;
border-radius: 8px;
border: none;
font-weight: bold;
}
.btn2 {
background-color: var(--secondary-light-one);
color: var(--primary-dark-one);
padding: 8px;
border-radius: 4px;
border: none;
margin: 5px;
}
.form-button, .link-button a:link, .link-button a:visited { .form-button, .link-button a:link, .link-button a:visited {
background-color: #4ba3cd; background-color: #4ba3cd;
color: white; color: white;
@ -203,14 +280,6 @@ select, .button {
border: none; border: none;
} }
.container-edit-buttons {
display: flex;
flex-wrap: wrap;
.btn {
margin: 5px;
}
}
.form-button:hover, .link-button a:hover, .link-button a:active { .form-button:hover, .link-button a:hover, .link-button a:active {
background-color: #4090b6; background-color: #4090b6;
@ -326,18 +395,6 @@ select, .button {
} }
} }
.detail-animal-header {
border-radius: 5px;
display: flex;
align-items: center;
justify-content: space-between;
}
@media screen and (max-width: 800px) {
.detail-animal-header {
display: block;
}
}
.tag { .tag {
border: black 1px solid; border: black 1px solid;
@ -355,10 +412,7 @@ select, .button {
} }
.container-cards {
display: flex;
flex-wrap: wrap;
}
.photos { .photos {
@ -376,14 +430,6 @@ select, .button {
border-radius: 10%; border-radius: 10%;
} }
.card {
flex: 1 25%;
margin: 10px;
border-radius: 8px;
padding: 5px;
background: var(--background-three);
color: var(--text-two);
}
.card h1 { .card h1 {
color: var(--text-three); color: var(--text-three);
@ -514,20 +560,7 @@ select, .button {
color: var(--text-two); color: var(--text-two);
} }
.container-comment-form {
width: 80%;
color: var(--text-one);
b {
text-shadow: 2px 2px var(--shadow-one);
}
}
textarea {
border-radius: 10px;
width: 100%;
margin: 5px;
}
.form-comments { .form-comments {
.btn { .btn {
@ -552,6 +585,21 @@ textarea {
} }
} }
.form-search {
select, input {
background-color: var(--primary-light-one);
color: var(--text-one);
border-radius: 3px;
border: none;
}
}
/************************/
/* GENERAL HIGHLIGHTING */
/************************/
.important { .important {
border: #e01137 4px solid; border: #e01137 4px solid;
} }
@ -564,15 +612,10 @@ textarea {
border: rgba(17, 58, 224, 0.51) 4px solid; border: rgba(17, 58, 224, 0.51) 4px solid;
} }
.form-search {
select, input {
background-color: var(--primary-light-one);
color: var(--text-one);
border-radius: 3px;
border: none;
}
} /*******/
/* MAP */
/*******/
.marker { .marker {
background-image: url('../img/logo_transparent.png'); background-image: url('../img/logo_transparent.png');

View File

@ -0,0 +1,19 @@
from notfellchen.celery import app as celery_app
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices
from .models import Location, AdoptionNotice
@celery_app.task(name="admin.clean_locations")
def task_clean_locations():
clean_locations()
@celery_app.task(name="admin.deactivate_unchecked")
def task_deactivate_unchecked():
deactivate_unchecked_adoption_notices()
@celery_app.task(name="commit.add_location")
def add_adoption_notice_location(pk):
instance = AdoptionNotice.objects.get(pk=pk)
Location.add_location_to_object(instance)

View File

@ -2,6 +2,8 @@
{% load i18n %} {% load i18n %}
{% load custom_tags %} {% load custom_tags %}
{% block title %}<title>{% translate "Über uns und Regeln" %}</title> %}{% endblock %}
{% block content %} {% block content %}
<h1>{% translate "Regeln" %}</h1> <h1>{% translate "Regeln" %}</h1>
{% include "fellchensammlung/lists/list-rules.html" %} {% include "fellchensammlung/lists/list-rules.html" %}

View File

@ -1,10 +1,12 @@
{% load custom_tags %} {% load custom_tags %}
{% load i18n %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
{% block title %}<title>Notfellchen</title>{% endblock %} {% block title %}{% endblock %}
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{% translate "Farbratten aus dem Tierschutz finden und adoptieren" %}">
<!-- Add additional CSS in static file --> <!-- Add additional CSS in static file -->
{% load static %} {% load static %}
<link rel="stylesheet" href="{% static 'fellchensammlung/css/styles.css' %}"> <link rel="stylesheet" href="{% static 'fellchensammlung/css/styles.css' %}">

View File

@ -2,6 +2,8 @@
{% load custom_tags %} {% load custom_tags %}
{% load i18n %} {% load i18n %}
{% block title %}<title>{{adoption_notice.name }}</title>{% endblock %}
{% block content %} {% block content %}
<div class="detail-adoption-notice-header"> <div class="detail-adoption-notice-header">
<h1 class="detail-adoption-notice-header">{{ adoption_notice.name }} <h1 class="detail-adoption-notice-header">{{ adoption_notice.name }}
@ -48,7 +50,14 @@
<td>{{ adoption_notice.searching_since }}</td> <td>{{ adoption_notice.searching_since }}</td>
<td>{{ adoption_notice.last_checked | date:'d. F Y' }}</td> <td>{{ adoption_notice.last_checked | date:'d. F Y' }}</td>
{% if adoption_notice.further_information %} {% if adoption_notice.further_information %}
<td>{{ adoption_notice.link_to_more_information | safe }}</td> <td>
<form method="get" action="{% url 'external-site' %}">
<input type="hidden" name="url" value="{{ adoption_notice.further_information }}">
<button class="btn" type="submit" id="submit">
{{ adoption_notice.further_information | domain }} <i class="fa-solid fa-arrow-up-right-from-square"></i>
</button>
</form>
</td>
{% else %} {% else %}
<td>-</td> <td>-</td>
{% endif %} {% endif %}

View File

@ -2,6 +2,8 @@
{% load custom_tags %} {% load custom_tags %}
{% load i18n %} {% load i18n %}
{% block title %}<title>{{ animal.name }}</title> %}
{% block content %} {% block content %}
{% include "fellchensammlung/details/detail-animal-partial.html" %} {% include "fellchensammlung/details/detail-animal-partial.html" %}
{% endblock %} {% endblock %}

View File

@ -0,0 +1,10 @@
{% extends "fellchensammlung/base_generic.html" %}
{% load i18n %}
{% block content %}
<div class="card">
{% blocktranslate %}
<p>Achtung du verlässt notfellchen.org</p>
{% endblocktranslate %}
<a href="{{ url }}" class="btn" >{% translate "Weiter" %}</a>
</div>
{% endblock content %}

View File

@ -2,6 +2,8 @@
{% load i18n %} {% load i18n %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block title %}<title>{% translate "Vermittlung hinzufügen" %}</title> %}{% endblock %}
{% block content %} {% block content %}
<h1>{% translate "Vermitteln" %}</h1> <h1>{% translate "Vermitteln" %}</h1>
<p> <p>

View File

@ -2,6 +2,8 @@
{% load i18n %} {% load i18n %}
{% load custom_tags %} {% load custom_tags %}
{% block title %}<title>{% translate "Notfellchen - Farbratten aus dem Tierschutz adoptieren" %}</title>{% endblock %}
{% block content %} {% block content %}
{% for announcement in announcements %} {% for announcement in announcements %}
{% include "fellchensammlung/partials/partial-announcement.html" %} {% include "fellchensammlung/partials/partial-announcement.html" %}

View File

@ -1,5 +1,6 @@
{% extends "fellchensammlung/base_generic.html" %} {% extends "fellchensammlung/base_generic.html" %}
{% load i18n %} {% load i18n %}
{% block title %}<title>{% translate "Instanz-Check" %}</title> {% endblock %}
{% block content %} {% block content %}
<div class="card"> <div class="card">
<h1>{% translate "Instanz-Check" %}</h1> <h1>{% translate "Instanz-Check" %}</h1>
@ -55,6 +56,22 @@
<p>{{ number_not_geocoded_rescue_orgs }}/{{ number_of_rescue_orgs }}</p> <p>{{ number_not_geocoded_rescue_orgs }}/{{ number_of_rescue_orgs }}</p>
{% endif %} {% endif %}
<h2>{% translate "Nicht-geprüfte Vermittlungen" %}</h2>
{% if number_unchecked_ans > 0 %}
<details>
<summary>{{ number_unchecked_ans }}</summary>
<ul>
{% for unchecked_an in unchecked_ans %}
<li>
<a href="{{ unchecked_an.get_absolute_url }}">{{ unchecked_an.name }}</a>
</li>
{% endfor %}
</ul>
</details>
{% else %}
<p>{{ number_unchecked_ans }}</p>
{% endif %}
<form class="notification-card-mark-read" method="post"> <form class="notification-card-mark-read" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="action" value="clean_locations"> <input type="hidden" name="action" value="clean_locations">
@ -62,5 +79,13 @@
<i class="fa-solid fa-broom"></i> {% translate "Erneut lokalisieren" %} <i class="fa-solid fa-broom"></i> {% translate "Erneut lokalisieren" %}
</button> </button>
</form> </form>
<form class="notification-card-mark-read" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="deactivate_unchecked_adoption_notices">
<button class="btn" type="submit" id="submit">
<i class="fa-solid fa-broom"></i> {% translate "Deaktivire ungeprüfte Vermittlungen" %}
</button>
</form>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -1,5 +1,6 @@
{% extends "fellchensammlung/base_generic.html" %} {% extends "fellchensammlung/base_generic.html" %}
{% load i18n %} {% load i18n %}
{% block title %}<title>{% translate "Karte" %}</title> %}
{% block content %} {% block content %}
<div class="card"> <div class="card">

View File

@ -1,5 +1,6 @@
{% extends "fellchensammlung/base_generic.html" %} {% extends "fellchensammlung/base_generic.html" %}
{% load i18n %} {% load i18n %}
{% block title %}<title>{% translate "Modqueue" %}</title> %}{% endblock %}
{% block content %} {% block content %}
<h1>{% translate "Modqueue" %}</h1> <h1>{% translate "Modqueue" %}</h1>

View File

@ -1,5 +1,8 @@
{% extends "fellchensammlung/base_generic.html" %} {% extends "fellchensammlung/base_generic.html" %}
{% load i18n %} {% load i18n %}
{% block title %}<title>{% translate "Suche" %}</title> %}
{% block content %} {% block content %}
<form class="form-search card" method="post"> <form class="form-search card" method="post">
{% csrf_token %} {% csrf_token %}

View File

@ -4,6 +4,7 @@ from django import template
from django.template.defaultfilters import stringfilter from django.template.defaultfilters import stringfilter
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from notfellchen import settings from notfellchen import settings
from urllib.parse import urlparse
register = template.Library() register = template.Library()
@ -56,6 +57,17 @@ def pointdecimal(value):
except ValueError: except ValueError:
return value return value
@register.filter
@stringfilter
def domain(url):
try:
domain = urlparse(url).netloc
if domain.startswith("www."):
return domain[4:]
return domain
except ValueError:
return url
@register.simple_tag @register.simple_tag
def settings_value(name): def settings_value(name):
return getattr(settings, name) return getattr(settings, name)

View File

@ -1,4 +1,8 @@
from fellchensammlung.models import AdoptionNotice, Location, RescueOrganization from django.utils import timezone
from datetime import timedelta
from fellchensammlung.models import AdoptionNotice, Location, RescueOrganization, AdoptionNoticeStatus
def clean_locations(quiet=True): def clean_locations(quiet=True):
# ADOPTION NOTICES # ADOPTION NOTICES
@ -7,7 +11,7 @@ def clean_locations(quiet=True):
num_without_location = adoption_notices_without_location.count() num_without_location = adoption_notices_without_location.count()
if not quiet: if not quiet:
print(f"From {num_of_all} there are {num_without_location} adoption notices without location " print(f"From {num_of_all} there are {num_without_location} adoption notices without location "
f"({num_without_location/num_of_all*100:.2f}%)") f"({num_without_location / num_of_all * 100:.2f}%)")
for adoption_notice in adoption_notices_without_location: for adoption_notice in adoption_notices_without_location:
if not quiet: if not quiet:
print(f"Searching {adoption_notice.location_string} in Nominatim") print(f"Searching {adoption_notice.location_string} in Nominatim")
@ -28,7 +32,7 @@ def clean_locations(quiet=True):
num_without_location = rescue_orgs_without_location.count() num_without_location = rescue_orgs_without_location.count()
if not quiet: if not quiet:
print(f"From {num_of_all} there are {num_without_location} adoption notices without location " print(f"From {num_of_all} there are {num_without_location} adoption notices without location "
f"({num_without_location/num_of_all*100:.2f}%)") f"({num_without_location / num_of_all * 100:.2f}%)")
for rescue_org in rescue_orgs_without_location: for rescue_org in rescue_orgs_without_location:
if not quiet: if not quiet:
print(f"Searching {rescue_org.location_string} in Nominatim") print(f"Searching {rescue_org.location_string} in Nominatim")
@ -42,3 +46,20 @@ def clean_locations(quiet=True):
num_new = num_without_location - num_without_location_new num_new = num_without_location - num_without_location_new
if not quiet: if not quiet:
print(f"Added {num_new} new locations") print(f"Added {num_new} new locations")
def get_unchecked_adoption_notices(weeks=3):
now = timezone.now()
three_weeks_ago = now - timedelta(weeks=weeks)
# Query for active adoption notices that were checked in the last three weeks
unchecked_adoptions = AdoptionNotice.objects.filter(
last_checked__gte=three_weeks_ago
)
active_unchecked_adoptions = [adoption for adoption in unchecked_adoptions if adoption.is_active]
return active_unchecked_adoptions
def deactivate_unchecked_adoption_notices():
for adoption_notice in get_unchecked_adoption_notices(weeks=3):
AdoptionNoticeStatus.objects.get(adoption_notice=adoption_notice).deactivate_unchecked()

View File

@ -7,7 +7,6 @@ from notfellchen import __version__ as nf_version
from notfellchen import settings from notfellchen import settings
def calculate_distance_between_coordinates(position1, position2): def calculate_distance_between_coordinates(position1, position2):
""" """
Calculate the distance between two points identified by coordinates Calculate the distance between two points identified by coordinates
@ -65,7 +64,8 @@ class GeoAPI:
def get_coordinates_from_query(self, location_string): def get_coordinates_from_query(self, location_string):
try: try:
result = self.requests.get(self.api_url, {"q": location_string, "format": "jsonv2"}, headers=self.headers).json()[0] result = \
self.requests.get(self.api_url, {"q": location_string, "format": "jsonv2"}, headers=self.headers).json()[0]
except IndexError: except IndexError:
return None return None
return result["lat"], result["lon"] return result["lat"], result["lon"]

View File

@ -79,5 +79,9 @@ urlpatterns = [
######### #########
path('api/', include('fellchensammlung.api.urls')), path('api/', include('fellchensammlung.api.urls')),
###################
## External Site ##
###################
path('external-site/', views.external_site_warning, name="external-site"),
] ]

View File

@ -13,14 +13,16 @@ from notfellchen import settings
from fellchensammlung import logger from fellchensammlung import logger
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \ from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
User, Location, AdoptionNoticeStatus, Subscriptions, CommentNotification, BaseNotification, RescueOrganization, Species User, Location, AdoptionNoticeStatus, Subscriptions, CommentNotification, BaseNotification, RescueOrganization, \
Species
from .forms import AdoptionNoticeForm, AdoptionNoticeFormWithDateWidget, ImageForm, ReportAdoptionNoticeForm, \ from .forms import AdoptionNoticeForm, AdoptionNoticeFormWithDateWidget, ImageForm, ReportAdoptionNoticeForm, \
CommentForm, ReportCommentForm, AnimalForm, \ CommentForm, ReportCommentForm, AnimalForm, \
AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal
from .models import Language, Announcement from .models import Language, Announcement
from .tools.geo import GeoAPI from .tools.geo import GeoAPI
from .tools.metrics import gather_metrics_data from .tools.metrics import gather_metrics_data
from .tools.admin import clean_locations from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices
from .tasks import add_adoption_notice_location
def user_is_trust_level_or_above(user, trust_level=User.MODERATOR): def user_is_trust_level_or_above(user, trust_level=User.MODERATOR):
@ -46,12 +48,9 @@ def index(request):
lang = Language.objects.get(languagecode=language_code) lang = Language.objects.get(languagecode=language_code)
active_announcements = Announcement.get_active_announcements(lang) active_announcements = Announcement.get_active_announcements(lang)
context = {"adoption_notices": active_adoptions[:5], "adoption_notices_map": active_adoptions, "announcements": active_announcements} context = {"adoption_notices": active_adoptions[:5], "adoption_notices_map": active_adoptions,
for text_code in ["how_to", "introduction"]: "announcements": active_announcements}
try: Text.get_texts(["how_to", "introduction"], lang, context)
context[text_code] = Text.objects.get(text_code=text_code, language=lang, )
except Text.DoesNotExist:
context[text_code] = None
return render(request, 'fellchensammlung/index.html', context=context) return render(request, 'fellchensammlung/index.html', context=context)
@ -120,7 +119,7 @@ def adoption_notice_detail(request, adoption_notice_id):
raise PermissionDenied raise PermissionDenied
else: else:
comment_form = CommentForm(instance=adoption_notice) comment_form = CommentForm(instance=adoption_notice)
context = {"adoption_notice": adoption_notice,"comment_form": comment_form, "user": request.user, context = {"adoption_notice": adoption_notice, "comment_form": comment_form, "user": request.user,
"has_edit_permission": has_edit_permission, "is_subscribed": is_subscribed} "has_edit_permission": has_edit_permission, "is_subscribed": is_subscribed}
return render(request, 'fellchensammlung/details/detail_adoption_notice.html', context=context) return render(request, 'fellchensammlung/details/detail_adoption_notice.html', context=context)
@ -171,7 +170,8 @@ def search(request):
else: else:
adoption_notices_in_distance = [a for a in active_adoptions if a.in_distance(search_position, max_distance)] adoption_notices_in_distance = [a for a in active_adoptions if a.in_distance(search_position, max_distance)]
context = {"adoption_notices": adoption_notices_in_distance, "search_form": search_form, "place_not_found": place_not_found} context = {"adoption_notices": adoption_notices_in_distance, "search_form": search_form,
"place_not_found": place_not_found}
else: else:
latest_adoption_list = AdoptionNotice.objects.order_by("-created_at") latest_adoption_list = AdoptionNotice.objects.order_by("-created_at")
active_adoptions = [adoption for adoption in latest_adoption_list if adoption.is_active] active_adoptions = [adoption for adoption in latest_adoption_list if adoption.is_active]
@ -183,23 +183,25 @@ def search(request):
@login_required @login_required
def add_adoption_notice(request): def add_adoption_notice(request):
if request.method == 'POST': if request.method == 'POST':
form = AdoptionNoticeFormWithDateWidgetAutoAnimal(request.POST, request.FILES, in_adoption_notice_creation_flow=True) form = AdoptionNoticeFormWithDateWidgetAutoAnimal(request.POST, request.FILES,
in_adoption_notice_creation_flow=True)
if form.is_valid(): if form.is_valid():
instance = form.save(commit=False) instance = form.save(commit=False)
instance.owner = request.user instance.owner = request.user
"""Search the location given in the location string and add it to the adoption notice"""
location = Location.get_location_from_string(instance.location_string)
instance.location = location
instance.save() instance.save()
"""Spin up a task that adds the location"""
add_adoption_notice_location.delay_on_commit(instance.pk)
# Set correct status # Set correct status
if request.user.trust_level >= User.TRUST_LEVEL[User.COORDINATOR]: if request.user.trust_level >= User.TRUST_LEVEL[User.COORDINATOR]:
major_status = AdoptionNoticeStatus.ACTIVE major_status = AdoptionNoticeStatus.ACTIVE
minor_status = AdoptionNoticeStatus.MINOR_STATUS_CHOICES[AdoptionNoticeStatus.ACTIVE]["searching"] minor_status = AdoptionNoticeStatus.MINOR_STATUS_CHOICES[AdoptionNoticeStatus.ACTIVE]["searching"]
else: else:
major_status=AdoptionNoticeStatus.AWAITING_ACTION major_status = AdoptionNoticeStatus.AWAITING_ACTION
minor_status=AdoptionNoticeStatus.MINOR_STATUS_CHOICES[AdoptionNoticeStatus.AWAITING_ACTION]["waiting_for_review"] minor_status = AdoptionNoticeStatus.MINOR_STATUS_CHOICES[AdoptionNoticeStatus.AWAITING_ACTION][
"waiting_for_review"]
status = AdoptionNoticeStatus.objects.create(major_status=major_status, status = AdoptionNoticeStatus.objects.create(major_status=major_status,
minor_status=minor_status, minor_status=minor_status,
adoption_notice=instance) adoption_notice=instance)
@ -212,7 +214,8 @@ def add_adoption_notice(request):
date_of_birth = form.cleaned_data["date_of_birth"] date_of_birth = form.cleaned_data["date_of_birth"]
for i in range(0, num_animals): for i in range(0, num_animals):
Animal.objects.create(owner=request.user, Animal.objects.create(owner=request.user,
name=f"{species} {i+1}", adoption_notice=instance, species=species, sex=sex, date_of_birth=date_of_birth) name=f"{species} {i + 1}", adoption_notice=instance, species=species, sex=sex,
date_of_birth=date_of_birth)
return redirect(reverse("adoption-notice-detail", args=[instance.pk])) return redirect(reverse("adoption-notice-detail", args=[instance.pk]))
else: else:
form = AdoptionNoticeFormWithDateWidgetAutoAnimal(in_adoption_notice_creation_flow=True) form = AdoptionNoticeFormWithDateWidgetAutoAnimal(in_adoption_notice_creation_flow=True)
@ -423,6 +426,7 @@ def modqueue(request):
context = {"reports": open_reports} context = {"reports": open_reports}
return render(request, 'fellchensammlung/modqueue.html', context=context) return render(request, 'fellchensammlung/modqueue.html', context=context)
@login_required @login_required
def updatequeue(request): def updatequeue(request):
if request.method == "POST": if request.method == "POST":
@ -456,6 +460,7 @@ def metrics(request):
data = gather_metrics_data() data = gather_metrics_data()
return JsonResponse(data) return JsonResponse(data)
@login_required @login_required
def instance_health_check(request): def instance_health_check(request):
""" """
@ -465,16 +470,20 @@ def instance_health_check(request):
action = request.POST.get("action") action = request.POST.get("action")
if action == "clean_locations": if action == "clean_locations":
clean_locations(quiet=False) clean_locations(quiet=False)
elif action == "deactivate_unchecked_adoption_notices":
deactivate_unchecked_adoption_notices()
number_of_adoption_notices = AdoptionNotice.objects.all().count() number_of_adoption_notices = AdoptionNotice.objects.all().count()
none_geocoded_adoption_notices = AdoptionNotice.objects.filter(location__isnull=True) none_geocoded_adoption_notices = AdoptionNotice.objects.filter(location__isnull=True)
number_not_geocoded_adoption_notices = len(none_geocoded_adoption_notices) number_not_geocoded_adoption_notices = len(none_geocoded_adoption_notices)
number_of_rescue_orgs = RescueOrganization.objects.all().count() number_of_rescue_orgs = RescueOrganization.objects.all().count()
none_geocoded_rescue_orgs = RescueOrganization.objects.filter(location__isnull=True) none_geocoded_rescue_orgs = RescueOrganization.objects.filter(location__isnull=True)
number_not_geocoded_rescue_orgs = len(none_geocoded_rescue_orgs) number_not_geocoded_rescue_orgs = len(none_geocoded_rescue_orgs)
unchecked_ans = get_unchecked_adoption_notices()
number_unchecked_ans = len(unchecked_ans)
# CHECK FOR MISSING TEXTS # CHECK FOR MISSING TEXTS
languages = Language.objects.all() languages = Language.objects.all()
texts = Text.objects.all() texts = Text.objects.all()
@ -494,10 +503,19 @@ def instance_health_check(request):
"number_of_rescue_orgs": number_of_rescue_orgs, "number_of_rescue_orgs": number_of_rescue_orgs,
"number_not_geocoded_rescue_orgs": number_not_geocoded_rescue_orgs, "number_not_geocoded_rescue_orgs": number_not_geocoded_rescue_orgs,
"none_geocoded_rescue_orgs": none_geocoded_rescue_orgs, "none_geocoded_rescue_orgs": none_geocoded_rescue_orgs,
"missing_texts": missing_texts "missing_texts": missing_texts,
"number_unchecked_ans": number_unchecked_ans,
"unchecked_ans": unchecked_ans
} }
return render(request, 'fellchensammlung/instance-health-check.html', context=context) return render(request, 'fellchensammlung/instance-health-check.html', context=context)
def external_site_warning(request):
url = request.GET.get("url")
context = {"url": url}
language_code = translation.get_language()
lang = Language.objects.get(languagecode=language_code)
Text.get_texts(["external_site_warning", "good_adoption_practices"], language=lang)
return render(request, 'fellchensammlung/external_site_warning.html', context=context)

View File

@ -1 +1,8 @@
__version__ = "0.2.0" __version__ = "0.3.0"
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app
__all__ = ('celery_app',)

25
src/notfellchen/celery.py Normal file
View File

@ -0,0 +1,25 @@
# <your_project>/celery.py
import os
from celery import Celery
from celery.schedules import crontab
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'notfellchen.settings')
app = Celery('notfellchen')
# Load task modules from all registered Django app configs.
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
app.conf.beat_schedule = {
'daily-cleanup': {
'task': 'admin.clean_locations',
'schedule': crontab(hour=2),
},
'daily-deactivation': {
'task': 'admin.deactivate_unchecked',
'schedule': 30,
}
}

View File

@ -14,6 +14,7 @@ from pathlib import Path
import os import os
import configparser import configparser
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from celery import Celery
"""CONFIG PARSER """ """CONFIG PARSER """
config = configparser.RawConfigParser() config = configparser.RawConfigParser()
@ -79,6 +80,11 @@ DB_HOST = config.get("database", "host", fallback='')
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale')] LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale')]
""" CELERY + KEYDB """
CELERY_BROKER_URL = config.get("celery", "broker", fallback="redis://localhost:6379/0")
CELERY_RESULT_BACKEND = config.get("celery", "backend", fallback="redis://localhost:6379/0")
""" GEOCODING """ """ GEOCODING """
GEOCODING_API_URL = config.get("geocoding", "api_url", fallback="https://nominatim.hyteck.de/search") GEOCODING_API_URL = config.get("geocoding", "api_url", fallback="https://nominatim.hyteck.de/search")
""" Tile Server """ """ Tile Server """