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:
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:
image: moanos/sphinx-rtd
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
ENV DOCKER_BUILD=true
RUN apt update
RUN apt install gettext -y
RUN apt install libpq-dev gcc -y
COPY . /app
WORKDIR /app
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 animal | 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 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",
"Pillow",
"django-registration",
"psycopg2-binary",
"psycopg2",
"django-crispy-forms",
"crispy-bootstrap4",
"djangorestframework"
"djangorestframework",
"celery[redis]"
]
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
@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):
def __str__(self):
@ -324,6 +330,7 @@ class AdoptionNoticeStatus(models.Model):
"against_the_rules": "against_the_rules",
"missing_information": "missing_information",
"technical_error": "technical_error",
"unchecked": "unchecked",
"other": "other"
}
}
@ -351,6 +358,11 @@ class AdoptionNoticeStatus(models.Model):
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"]
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):
MALE_NEUTERED = "M_N"
@ -516,6 +528,18 @@ class Text(models.Model):
def __str__(self):
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):
"""
@ -523,7 +547,7 @@ class Announcement(Text):
"""
logged_in_only = models.BooleanField(default=False)
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")
IMPORTANT = "important"
WARNING = "warning"

View File

@ -1,3 +1,7 @@
/***************/
/* MAIN COLORS */
/***************/
:root {
--primary-light-one: #5daa68;
--primary-light-two: #4a9455;
@ -18,7 +22,9 @@
--text-three: var(--primary-light-one);
--shadow-three: var(--primary-dark-one);
}
/**************************/
/* TAG SETTINGS (GENERAL) */
/**************************/
html, body {
margin: 0;
height: 100%;
@ -30,10 +36,6 @@ body {
}
.content-box {
margin: 20px;
}
table {
width: 100%;
}
@ -79,6 +81,127 @@ h1, h2 {
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 {
overflow: hidden;
background-color: var(--background-two);
@ -110,14 +233,7 @@ h1, h2 {
color: white;
}
select, .button {
width: 100%;
border: none;
border-radius: 4px;
opacity: 1;
background-color: var(--secondary-light-one);
}
.header-right select.option {
color: #000;
@ -135,26 +251,6 @@ select, .button {
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) {
.header a {
@ -172,25 +268,6 @@ select, .button {
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 {
background-color: #4ba3cd;
color: white;
@ -203,14 +280,6 @@ select, .button {
border: none;
}
.container-edit-buttons {
display: flex;
flex-wrap: wrap;
.btn {
margin: 5px;
}
}
.form-button:hover, .link-button a:hover, .link-button a:active {
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 {
border: black 1px solid;
@ -355,10 +412,7 @@ select, .button {
}
.container-cards {
display: flex;
flex-wrap: wrap;
}
.photos {
@ -376,14 +430,6 @@ select, .button {
border-radius: 10%;
}
.card {
flex: 1 25%;
margin: 10px;
border-radius: 8px;
padding: 5px;
background: var(--background-three);
color: var(--text-two);
}
.card h1 {
color: var(--text-three);
@ -514,20 +560,7 @@ select, .button {
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 {
.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 {
border: #e01137 4px solid;
}
@ -564,15 +612,10 @@ textarea {
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 {
background-image: url('../img/logo_transparent.png');
@ -594,4 +637,4 @@ textarea {
.map-in-content #map {
height: 500px;
width: 90%;
}
}

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 custom_tags %}
{% block title %}<title>{% translate "Über uns und Regeln" %}</title> %}{% endblock %}
{% block content %}
<h1>{% translate "Regeln" %}</h1>
{% include "fellchensammlung/lists/list-rules.html" %}

View File

@ -1,10 +1,12 @@
{% load custom_tags %}
{% load i18n %}
<!DOCTYPE html>
<html lang="en">
<head>
{% block title %}<title>Notfellchen</title>{% endblock %}
{% block title %}{% endblock %}
<meta charset="utf-8">
<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 -->
{% load static %}
<link rel="stylesheet" href="{% static 'fellchensammlung/css/styles.css' %}">

View File

@ -2,23 +2,25 @@
{% load custom_tags %}
{% load i18n %}
{% block title %}<title>{{adoption_notice.name }}</title>{% endblock %}
{% block content %}
<div class="detail-adoption-notice-header">
<h1 class="detail-adoption-notice-header">{{ adoption_notice.name }}
{% if not is_subscribed %}
<form class="notification-card-mark-read" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="subscribe">
<input type="hidden" name="adoption_notice_id" value="{{ adoption_notice.pk }}">
<button class="btn2" type="submit" id="submit"><i class="fa-solid fa-bell"></i></button>
</form>
<form class="notification-card-mark-read" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="subscribe">
<input type="hidden" name="adoption_notice_id" value="{{ adoption_notice.pk }}">
<button class="btn2" type="submit" id="submit"><i class="fa-solid fa-bell"></i></button>
</form>
{% else %}
<form class="notification-card-mark-read" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="unsubscribe">
<input type="hidden" name="adoption_notice_id" value="{{ adoption_notice.pk }}">
<button class="btn2" type="submit" id="submit"><i class="fa-solid fa-bell-slash"></i></button>
</form>
<form class="notification-card-mark-read" method="post">
{% csrf_token %}
<input type="hidden" name="action" value="unsubscribe">
<input type="hidden" name="adoption_notice_id" value="{{ adoption_notice.pk }}">
<button class="btn2" type="submit" id="submit"><i class="fa-solid fa-bell-slash"></i></button>
</form>
{% endif %}
</h1>
{% if has_edit_permission %}
@ -48,7 +50,14 @@
<td>{{ adoption_notice.searching_since }}</td>
<td>{{ adoption_notice.last_checked | date:'d. F Y' }}</td>
{% 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 %}
<td>-</td>
{% endif %}

View File

@ -2,6 +2,8 @@
{% load custom_tags %}
{% load i18n %}
{% block title %}<title>{{ animal.name }}</title> %}
{% block content %}
{% include "fellchensammlung/details/detail-animal-partial.html" %}
{% 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 crispy_forms_tags %}
{% block title %}<title>{% translate "Vermittlung hinzufügen" %}</title> %}{% endblock %}
{% block content %}
<h1>{% translate "Vermitteln" %}</h1>
<p>

View File

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

View File

@ -1,23 +1,24 @@
{% extends "fellchensammlung/base_generic.html" %}
{% load i18n %}
{% block title %}<title>{% translate "Instanz-Check" %}</title> {% endblock %}
{% block content %}
<div class="card">
<h1>{% translate "Instanz-Check" %}</h1>
{% if missing_texts|length > 0 %}
<h2>{% trans "Fehlende Texte" %}</h2>
<p>
<table>
<table>
<tr>
<th>{% translate "Text Code" %}</th>
<th>{% translate "Sprache" %}</th>
</tr>
{% for missing_text in missing_texts %}
<tr>
<th>{% translate "Text Code" %}</th>
<th>{% translate "Sprache" %}</th>
<td>{{ missing_text.0 }}</td>
<td>{{ missing_text.1 }}</td>
</tr>
{% for missing_text in missing_texts %}
<tr>
<td>{{ missing_text.0 }}</td>
<td>{{ missing_text.1 }}</td>
</tr>
{% endfor %}
</table>
{% endfor %}
</table>
</p>
{% else %}
<p>{% translate "Texte scheinen vollständig" %}</p>
@ -55,6 +56,22 @@
<p>{{ number_not_geocoded_rescue_orgs }}/{{ number_of_rescue_orgs }}</p>
{% 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">
{% csrf_token %}
<input type="hidden" name="action" value="clean_locations">
@ -62,5 +79,13 @@
<i class="fa-solid fa-broom"></i> {% translate "Erneut lokalisieren" %}
</button>
</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>
{% endblock content %}

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ from django import template
from django.template.defaultfilters import stringfilter
from django.utils.safestring import mark_safe
from notfellchen import settings
from urllib.parse import urlparse
register = template.Library()
@ -56,6 +57,17 @@ def pointdecimal(value):
except ValueError:
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
def settings_value(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):
# ADOPTION NOTICES
@ -7,7 +11,7 @@ def clean_locations(quiet=True):
num_without_location = adoption_notices_without_location.count()
if not quiet:
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:
if not quiet:
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()
if not quiet:
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:
if not quiet:
print(f"Searching {rescue_org.location_string} in Nominatim")
@ -41,4 +45,21 @@ def clean_locations(quiet=True):
num_without_location_new = rescue_orgs_without_location_new.count()
num_new = num_without_location - num_without_location_new
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
def calculate_distance_between_coordinates(position1, position2):
"""
Calculate the distance between two points identified by coordinates
@ -65,7 +64,8 @@ class GeoAPI:
def get_coordinates_from_query(self, location_string):
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:
return None
return result["lat"], result["lon"]
@ -77,9 +77,9 @@ class GeoAPI:
def get_geojson_for_query(self, location_string):
try:
result = self.requests.get(self.api_url,
{"q": location_string,
"format": "jsonv2"},
headers=self.headers).json()
{"q": location_string,
"format": "jsonv2"},
headers=self.headers).json()
except Exception as e:
logging.warning(f"Exception {e} when querying Nominatim")
return None

View File

@ -79,5 +79,9 @@ urlpatterns = [
#########
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 .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, \
CommentForm, ReportCommentForm, AnimalForm, \
AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal
from .models import Language, Announcement
from .tools.geo import GeoAPI
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):
@ -46,12 +48,9 @@ def index(request):
lang = Language.objects.get(languagecode=language_code)
active_announcements = Announcement.get_active_announcements(lang)
context = {"adoption_notices": active_adoptions[:5], "adoption_notices_map": active_adoptions, "announcements": active_announcements}
for text_code in ["how_to", "introduction"]:
try:
context[text_code] = Text.objects.get(text_code=text_code, language=lang, )
except Text.DoesNotExist:
context[text_code] = None
context = {"adoption_notices": active_adoptions[:5], "adoption_notices_map": active_adoptions,
"announcements": active_announcements}
Text.get_texts(["how_to", "introduction"], lang, context)
return render(request, 'fellchensammlung/index.html', context=context)
@ -76,7 +75,7 @@ def change_language(request):
def adoption_notice_detail(request, adoption_notice_id):
adoption_notice = AdoptionNotice.objects.get(id=adoption_notice_id)
if request.user.is_authenticated:
try:
try:
subscription = Subscriptions.objects.get(owner=request.user, adoption_notice=adoption_notice)
is_subscribed = True
except Subscriptions.DoesNotExist:
@ -104,9 +103,9 @@ def adoption_notice_detail(request, adoption_notice_id):
# Create a notification but only if the user is not the one that posted the comment
if subscription.owner != request.user:
notification = CommentNotification(user=subscription.owner,
title=f"{adoption_notice.name} - Neuer Kommentar",
text=f"{request.user}: {comment_instance.text}",
comment=comment_instance)
title=f"{adoption_notice.name} - Neuer Kommentar",
text=f"{request.user}: {comment_instance.text}",
comment=comment_instance)
notification.save()
else:
comment_form = CommentForm(instance=adoption_notice)
@ -114,13 +113,13 @@ def adoption_notice_detail(request, adoption_notice_id):
Subscriptions.objects.create(owner=request.user, adoption_notice=adoption_notice)
is_subscribed = True
if action == "unsubscribe":
subscription.delete()
is_subscribed = False
subscription.delete()
is_subscribed = False
else:
raise PermissionDenied
else:
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}
return render(request, 'fellchensammlung/details/detail_adoption_notice.html', context=context)
@ -170,8 +169,9 @@ def search(request):
adoption_notices_in_distance = active_adoptions
else:
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:
latest_adoption_list = AdoptionNotice.objects.order_by("-created_at")
active_adoptions = [adoption for adoption in latest_adoption_list if adoption.is_active]
@ -183,36 +183,39 @@ def search(request):
@login_required
def add_adoption_notice(request):
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():
instance = form.save(commit=False)
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()
"""Spin up a task that adds the location"""
add_adoption_notice_location.delay_on_commit(instance.pk)
# Set correct status
if request.user.trust_level >= User.TRUST_LEVEL[User.COORDINATOR]:
major_status = AdoptionNoticeStatus.ACTIVE
minor_status = AdoptionNoticeStatus.MINOR_STATUS_CHOICES[AdoptionNoticeStatus.ACTIVE]["searching"]
else:
major_status=AdoptionNoticeStatus.AWAITING_ACTION
minor_status=AdoptionNoticeStatus.MINOR_STATUS_CHOICES[AdoptionNoticeStatus.AWAITING_ACTION]["waiting_for_review"]
major_status = AdoptionNoticeStatus.AWAITING_ACTION
minor_status = AdoptionNoticeStatus.MINOR_STATUS_CHOICES[AdoptionNoticeStatus.AWAITING_ACTION][
"waiting_for_review"]
status = AdoptionNoticeStatus.objects.create(major_status=major_status,
minor_status=minor_status,
adoption_notice=instance)
minor_status=minor_status,
adoption_notice=instance)
status.save()
# Get the species and number of animals from the form
# Get the species and number of animals from the form
species = form.cleaned_data["species"]
sex = form.cleaned_data["sex"]
num_animals = form.cleaned_data["num_animals"]
date_of_birth = form.cleaned_data["date_of_birth"]
for i in range(0, num_animals):
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]))
else:
form = AdoptionNoticeFormWithDateWidgetAutoAnimal(in_adoption_notice_creation_flow=True)
@ -423,6 +426,7 @@ def modqueue(request):
context = {"reports": open_reports}
return render(request, 'fellchensammlung/modqueue.html', context=context)
@login_required
def updatequeue(request):
if request.method == "POST":
@ -447,7 +451,7 @@ def updatequeue(request):
def map(request):
adoption_notices = AdoptionNotice.objects.all() #TODO: Filter to active
adoption_notices = AdoptionNotice.objects.all() #TODO: Filter to active
context = {"adoption_notices_map": adoption_notices}
return render(request, 'fellchensammlung/map.html', context=context)
@ -456,6 +460,7 @@ def metrics(request):
data = gather_metrics_data()
return JsonResponse(data)
@login_required
def instance_health_check(request):
"""
@ -465,16 +470,20 @@ def instance_health_check(request):
action = request.POST.get("action")
if action == "clean_locations":
clean_locations(quiet=False)
elif action == "deactivate_unchecked_adoption_notices":
deactivate_unchecked_adoption_notices()
number_of_adoption_notices = AdoptionNotice.objects.all().count()
none_geocoded_adoption_notices = AdoptionNotice.objects.filter(location__isnull=True)
number_not_geocoded_adoption_notices = len(none_geocoded_adoption_notices)
number_of_rescue_orgs = RescueOrganization.objects.all().count()
none_geocoded_rescue_orgs = RescueOrganization.objects.filter(location__isnull=True)
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
languages = Language.objects.all()
texts = Text.objects.all()
@ -494,10 +503,19 @@ def instance_health_check(request):
"number_of_rescue_orgs": number_of_rescue_orgs,
"number_not_geocoded_rescue_orgs": number_not_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)
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 configparser
from django.utils.translation import gettext_lazy as _
from celery import Celery
"""CONFIG PARSER """
config = configparser.RawConfigParser()
@ -79,6 +80,11 @@ DB_HOST = config.get("database", "host", fallback='')
BASE_DIR = Path(__file__).resolve().parent.parent
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_API_URL = config.get("geocoding", "api_url", fallback="https://nominatim.hyteck.de/search")
""" Tile Server """