Compare commits
39 Commits
6702211c05
...
ca8996fff6
Author | SHA1 | Date | |
---|---|---|---|
ca8996fff6 | |||
eb734d2716 | |||
655e304c6c | |||
8e34ed440e | |||
0c7080f005 | |||
0b93b5eccb | |||
f1d9f7ad22 | |||
4e71ac7866 | |||
1d0a42a7e1 | |||
d384e75746 | |||
70154abd37 | |||
ab3437e61d | |||
0ccbb18411 | |||
e6f12ce5b1 | |||
6325de17d9 | |||
b9d6293546 | |||
dbe52e4884 | |||
3c286d84d8 | |||
227fa4d5a8 | |||
d47f181e1d | |||
272046142e | |||
5c18832961 | |||
d59cc0034a | |||
64024be833 | |||
5ef20bdce0 | |||
7ddd7b0c0c | |||
cbd8700917 | |||
6eb2f5000f | |||
1cd70228b9 | |||
23d8e85031 | |||
4fb92d8215 | |||
6dfc92bf15 | |||
2015f8b332 | |||
66a0b42718 | |||
efecfc910d | |||
96bc44c508 | |||
a2c8f469a7 | |||
a98b428614 | |||
dfede77e98 |
@ -51,6 +51,7 @@ Therefore, a solution is used where a number of predefined texts per site are su
|
||||
| `privacy_statement` | About |
|
||||
| `terms_of_service` | About |
|
||||
| `imprint` | About |
|
||||
| `about_us` | About |
|
||||
| Any rule | About |
|
||||
|
||||
# Developer Notes
|
||||
|
@ -1,6 +1,7 @@
|
||||
Benachrichtigungen
|
||||
==================
|
||||
|
||||
Ersteller*innen von Vermittlungen werden über neue Kommentare per Mail benachrichtigt, ebenso alle die die Vermittlung abonniert haben.
|
||||
Jede Vermittlung kann abonniert werden. Dafür klickst du auf die Glocke neben dem Titel der Vermittlung.
|
||||
|
||||
.. image:: abonnieren.png
|
||||
|
@ -6,5 +6,6 @@ Users guide
|
||||
:caption: Contents:
|
||||
|
||||
registrierung.rst
|
||||
benachrichtigungen.rst
|
||||
vermittlungen.rst
|
||||
moderationskonzept.rt
|
||||
benachrichtigungen.rst
|
||||
|
29
docs/user/moderationskonzept.rst
Normal file
29
docs/user/moderationskonzept.rst
Normal file
@ -0,0 +1,29 @@
|
||||
Moderationskonzept
|
||||
==================
|
||||
|
||||
Vertrauen in notfellchen.org ist uns wichtig. Unser Kernziel ist es Tierschutz und Tierwohl zu fördern. Dafür sind drei
|
||||
Grundkonzepte wichtig
|
||||
|
||||
* Aktualität: Informationen auf notfellchen.org müssen aktuell&richtig sein
|
||||
* Tierschutz: Ausschließlich Ratten aus dem Tierschutz werden vermittelt
|
||||
* Moderation: Vermittlungen und Kommentare können gemeldet werden und werden vom Team zügig moderiert.
|
||||
|
||||
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 Vermittlung auf der Website eines Tierheims oder einer Pflegestelle entfernt wird, wird die Anzeige sofort deaktiviert.
|
||||
|
||||
Vermittlungen können von allen Menschen, auch ohne Account gemeldet werden. Grund für eine Meldung kann sein, dass Informationen veraltet sind oder ein Verdacht von Tierwohlgefärdung. Gemeldete Vermittlungen werden vom Moderationsteam geprüft und ggf. entfernt.
|
||||
|
||||
Kommentare
|
||||
++++++++++
|
||||
|
||||
Die Kommentarfunktion von Vermittlungen ermöglicht es angemeldeten Nutzer*innen zusätzliche Informationen hinzuzufügen oder Fragen zu stellen.
|
||||
|
||||
Kommentare können, wie Vermittlungen, gemeldet werden wenn sie nicht den Regeln entsprechen.
|
@ -41,8 +41,14 @@ dependencies = [
|
||||
"djangorestframework",
|
||||
"celery[redis]"
|
||||
]
|
||||
|
||||
dynamic = ["version", "readme"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
develop = [
|
||||
"pytest",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://notfellchen.org"
|
||||
repository = "https://codeberg.org/moanos/notfellchen/"
|
||||
|
@ -1,5 +1,3 @@
|
||||
import datetime
|
||||
|
||||
from django import forms
|
||||
|
||||
from .models import AdoptionNotice, Animal, Image, ReportAdoptionNotice, ReportComment, ModerationAction, User, Species, \
|
||||
@ -175,5 +173,5 @@ def _get_distances():
|
||||
|
||||
|
||||
class AdoptionNoticeSearchForm(forms.Form):
|
||||
postcode = forms.CharField(max_length=20, label=_("Postleitzahl"))
|
||||
location = forms.CharField(max_length=20, label=_("Stadt"))
|
||||
max_distance = forms.ChoiceField(choices=_get_distances, label=_("Max. Distanz"))
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django.core.management import BaseCommand
|
||||
from fellchensammlung.models import AdoptionNotice, Location
|
||||
from fellchensammlung.tools.geo import clean_locations
|
||||
from fellchensammlung.tools.admin import clean_locations
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@ -15,4 +15,4 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
clean_locations(quiet=False)
|
||||
clean_locations(quiet=False)
|
||||
|
@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.1.1 on 2024-10-29 10:44
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0010_timestamp'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adoptionnotice',
|
||||
name='created_at',
|
||||
field=models.DateField(default=django.utils.timezone.now, verbose_name='Erstellt am'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adoptionnotice',
|
||||
name='last_checked',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Zuletzt überprüft am'),
|
||||
),
|
||||
]
|
@ -0,0 +1,136 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-03 20:07
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0011_alter_adoptionnotice_created_at_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='adoptionnotice',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='adoptionnoticestatus',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='adoptionnoticestatus',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='animal',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='animal',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='announcement',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='basenotification',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='comment',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='image',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='image',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='location',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='location',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='log',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moderationaction',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='report',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rescueorganization',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rule',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='rule',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subscriptions',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
20
src/fellchensammlung/migrations/0013_alter_log_user.py
Normal file
20
src/fellchensammlung/migrations/0013_alter_log_user.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.1.1 on 2024-11-06 07:02
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('fellchensammlung', '0012_adoptionnotice_updated_at_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='log',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Nutzer*in'),
|
||||
),
|
||||
]
|
@ -3,7 +3,6 @@ import uuid
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from datetime import datetime
|
||||
from django.utils import timezone
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import post_save
|
||||
@ -60,6 +59,7 @@ class User(AbstractUser):
|
||||
preferred_language = models.ForeignKey(Language, on_delete=models.PROTECT, null=True, blank=True,
|
||||
verbose_name=_('Bevorzugte Sprache'))
|
||||
trust_level = models.IntegerField(choices=TRUST_LEVEL, default=TRUST_LEVEL[MEMBER])
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Nutzer*in')
|
||||
@ -83,6 +83,8 @@ class Image(models.Model):
|
||||
image = models.ImageField(upload_to='images')
|
||||
alt_text = models.TextField(max_length=2000)
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.alt_text
|
||||
@ -96,6 +98,8 @@ class Species(models.Model):
|
||||
"""Model representing a species of animal."""
|
||||
name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
|
||||
verbose_name=_('Name'))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
"""String for representing the Model object."""
|
||||
@ -111,6 +115,8 @@ class Location(models.Model):
|
||||
latitude = models.FloatField()
|
||||
longitude = models.FloatField()
|
||||
name = models.CharField(max_length=2000)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
|
||||
@ -172,6 +178,8 @@ class RescueOrganization(models.Model):
|
||||
facebook = models.URLField(null=True, blank=True, verbose_name=_('Facebook Profil'))
|
||||
fediverse_profile = models.URLField(null=True, blank=True, verbose_name=_('Fediverse Profil'))
|
||||
website = models.URLField(null=True, blank=True, verbose_name=_('Website'))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class AdoptionNotice(models.Model):
|
||||
@ -181,10 +189,13 @@ class AdoptionNotice(models.Model):
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}"
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
return self.name
|
||||
return f"[{self.adoptionnoticestatus.as_string()}] {self.name}"
|
||||
|
||||
created_at = models.DateField(verbose_name=_('Erstellt am'), default=datetime.now)
|
||||
last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=datetime.now)
|
||||
created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=timezone.now)
|
||||
searching_since = models.DateField(verbose_name=_('Sucht nach einem Zuhause seit'))
|
||||
name = models.CharField(max_length=200)
|
||||
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
|
||||
@ -288,19 +299,27 @@ class AdoptionNotice(models.Model):
|
||||
return self.adoptionnoticestatus.is_active
|
||||
|
||||
@property
|
||||
def is_to_be_checked(self, include_active=False):
|
||||
def is_disabled_unchecked(self):
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
return False
|
||||
return self.adoptionnoticestatus.is_to_be_checked or (include_active and self.adoptionnoticestatus.is_active)
|
||||
|
||||
def set_checked(self):
|
||||
self.last_checked = datetime.now()
|
||||
self.save()
|
||||
return self.adoptionnoticestatus.is_disabled_unchecked
|
||||
|
||||
def set_closed(self):
|
||||
self.last_checked = datetime.now()
|
||||
self.last_checked = timezone.now()
|
||||
self.adoptionnoticestatus.set_closed()
|
||||
|
||||
def set_active(self):
|
||||
self.last_checked = timezone.now()
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
AdoptionNoticeStatus.create_other(self)
|
||||
self.adoptionnoticestatus.set_active()
|
||||
|
||||
def set_unchecked(self):
|
||||
self.last_checked = timezone.now()
|
||||
if not hasattr(self, 'adoptionnoticestatus'):
|
||||
AdoptionNoticeStatus.create_other(self)
|
||||
self.adoptionnoticestatus.set_unchecked()
|
||||
|
||||
|
||||
class AdoptionNoticeStatus(models.Model):
|
||||
"""
|
||||
@ -351,32 +370,52 @@ class AdoptionNoticeStatus(models.Model):
|
||||
minor_choices.update(MINOR_STATUS_CHOICES[key])
|
||||
minor_status = models.CharField(choices=minor_choices, max_length=200)
|
||||
adoption_notice = models.OneToOneField(AdoptionNotice, on_delete=models.CASCADE)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.adoption_notice}: {self.major_status}, {self.minor_status}"
|
||||
|
||||
def as_string(self):
|
||||
return f"{self.major_status}, {self.minor_status}"
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return self.major_status == self.ACTIVE
|
||||
|
||||
@property
|
||||
def is_to_be_checked(self):
|
||||
def is_disabled_unchecked(self):
|
||||
return self.major_status == self.DISABLED and self.minor_status == "unchecked"
|
||||
|
||||
@staticmethod
|
||||
def get_minor_choices(major_status):
|
||||
return AdoptionNoticeStatus.MINOR_STATUS_CHOICES[major_status]
|
||||
|
||||
@staticmethod
|
||||
def create_other(an_instance):
|
||||
# Used as empty status to be changed immediately
|
||||
major_status = AdoptionNoticeStatus.DISABLED
|
||||
minor_status = AdoptionNoticeStatus.MINOR_STATUS_CHOICES[AdoptionNoticeStatus.DISABLED]["other"]
|
||||
AdoptionNoticeStatus.objects.create(major_status=major_status,
|
||||
minor_status=minor_status,
|
||||
adoption_notice=an_instance)
|
||||
|
||||
def set_closed(self):
|
||||
self.major_status = self.MAJOR_STATUS_CHOICES[self.CLOSED]
|
||||
self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"]
|
||||
self.save()
|
||||
|
||||
def deactivate_unchecked(self):
|
||||
def set_unchecked(self):
|
||||
self.major_status = self.MAJOR_STATUS_CHOICES[self.DISABLED]
|
||||
self.minor_status = self.MINOR_STATUS_CHOICES[self.DISABLED]["unchecked"]
|
||||
self.save()
|
||||
|
||||
def set_active(self):
|
||||
self.major_status = self.MAJOR_STATUS_CHOICES[self.ACTIVE]
|
||||
self.minor_status = self.MINOR_STATUS_CHOICES[self.ACTIVE]["searching"]
|
||||
self.save()
|
||||
|
||||
|
||||
|
||||
class Animal(models.Model):
|
||||
MALE_NEUTERED = "M_N"
|
||||
@ -398,13 +437,15 @@ class Animal(models.Model):
|
||||
sex = models.CharField(max_length=20, choices=SEX_CHOICES, )
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE)
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}"
|
||||
|
||||
@property
|
||||
def age(self):
|
||||
return datetime.today().date() - self.date_of_birth
|
||||
return timezone.now().today().date() - self.date_of_birth
|
||||
|
||||
@property
|
||||
def hr_age(self):
|
||||
@ -441,6 +482,8 @@ class Rule(models.Model):
|
||||
language = models.ForeignKey(Language, on_delete=models.PROTECT)
|
||||
# Rule identifier allows to translate rules with the same identifier
|
||||
rule_identifier = models.CharField(max_length=24)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
@ -464,6 +507,7 @@ class Report(models.Model):
|
||||
reported_broken_rules = models.ManyToManyField(Rule)
|
||||
user_comment = models.TextField(blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.status}]: {self.user_comment:.20}"
|
||||
@ -510,12 +554,12 @@ class ModerationAction(models.Model):
|
||||
}
|
||||
action = models.CharField(max_length=30, choices=ACTIONS.items())
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
public_comment = models.TextField(blank=True)
|
||||
# Only visible to moderator
|
||||
private_comment = models.TextField(blank=True)
|
||||
report = models.ForeignKey(Report, on_delete=models.CASCADE)
|
||||
|
||||
# TODO: Needs field for moderator that performed the action
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.action}]: {self.public_comment}"
|
||||
@ -560,6 +604,7 @@ class Announcement(Text):
|
||||
"""
|
||||
logged_in_only = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
publish_start_time = models.DateTimeField(verbose_name="Veröffentlichungszeitpunkt")
|
||||
publish_end_time = models.DateTimeField(verbose_name="Veröffentlichungsende")
|
||||
IMPORTANT = "important"
|
||||
@ -608,6 +653,7 @@ class Comment(models.Model):
|
||||
"""
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
|
||||
text = models.TextField(verbose_name="Inhalt")
|
||||
reply_to = models.ForeignKey("self", verbose_name="Antwort auf", blank=True, null=True, on_delete=models.CASCADE)
|
||||
@ -625,6 +671,7 @@ class Comment(models.Model):
|
||||
|
||||
class BaseNotification(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
title = models.CharField(max_length=100)
|
||||
text = models.TextField(verbose_name="Inhalt")
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||
@ -650,6 +697,7 @@ class Subscriptions(models.Model):
|
||||
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.owner} - {self.adoption_notice}"
|
||||
@ -659,10 +707,11 @@ class Log(models.Model):
|
||||
"""
|
||||
Basic class that allows logging random entries for later inspection
|
||||
"""
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_("Nutzer*in"))
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_("Nutzer*in"), blank=True, null=True)
|
||||
action = models.CharField(max_length=255, verbose_name=_("Aktion"))
|
||||
text = models.CharField(max_length=1000, verbose_name=_("Log text"))
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.action}] - {self.user} - {self.created_at.strftime('%H:%M:%S %d-%m-%Y ')}"
|
||||
|
@ -115,6 +115,10 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
.spaced {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
/*******************************/
|
||||
/* PARTIAL SPECIFIC CONTAINERS */
|
||||
/*******************************/
|
||||
@ -163,6 +167,16 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
/*************/
|
||||
/* Modifiers */
|
||||
/*************/
|
||||
|
||||
/* Used to enlargen cards */
|
||||
.full-width {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/***********/
|
||||
/* BUTTONS */
|
||||
/***********/
|
||||
|
@ -1,6 +1,6 @@
|
||||
from datetime import datetime
|
||||
from django.utils import timezone
|
||||
from notfellchen.celery import app as celery_app
|
||||
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices
|
||||
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices
|
||||
from .tools.misc import healthcheck_ok
|
||||
from .models import Location, AdoptionNotice, Timestamp
|
||||
|
||||
@ -8,9 +8,9 @@ from .models import Location, AdoptionNotice, Timestamp
|
||||
def set_timestamp(key: str):
|
||||
try:
|
||||
ts = Timestamp.objects.get(key=key)
|
||||
ts.timestamp = datetime.now()
|
||||
ts.timestamp = timezone.now()
|
||||
except Timestamp.DoesNotExist:
|
||||
Timestamp.objects.create(key=key, timestamp=datetime.now())
|
||||
Timestamp.objects.create(key=key, timestamp=timezone.now())
|
||||
|
||||
|
||||
@celery_app.task(name="admin.clean_locations")
|
||||
@ -19,10 +19,16 @@ def task_clean_locations():
|
||||
set_timestamp("task_clean_locations")
|
||||
|
||||
|
||||
@celery_app.task(name="admin.deactivate_unchecked")
|
||||
@celery_app.task(name="admin.daily_unchecked_deactivation")
|
||||
def task_deactivate_unchecked():
|
||||
deactivate_unchecked_adoption_notices()
|
||||
set_timestamp("task_deactivate_unchecked")
|
||||
set_timestamp("task_daily_unchecked_deactivation")
|
||||
|
||||
|
||||
@celery_app.task(name="admin.deactivate_404_adoption_notices")
|
||||
def task_deactivate_unchecked():
|
||||
deactivate_404_adoption_notices()
|
||||
set_timestamp("task_deactivate_404_adoption_notices")
|
||||
|
||||
|
||||
@celery_app.task(name="commit.add_location")
|
||||
|
@ -2,9 +2,14 @@
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
|
||||
{% block title %}<title>{% translate "Über uns und Regeln" %}</title> %}{% endblock %}
|
||||
{% block title %}<title>{% translate "Über uns und Regeln" %}</title>{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if about_us %}
|
||||
<h1>{{ about_us.title }}</h1>
|
||||
{{ about_us.content | render_markdown }}
|
||||
{% endif %}
|
||||
|
||||
<h1>{% translate "Regeln" %}</h1>
|
||||
{% include "fellchensammlung/lists/list-rules.html" %}
|
||||
|
||||
|
@ -0,0 +1,28 @@
|
||||
{% load i18n %}
|
||||
{% load custom_tags %}
|
||||
<div class="card">
|
||||
<h1>
|
||||
<a href="{{ adoption_notice.get_absolute_url }}">{{ adoption_notice.name }}</a>
|
||||
</h1>
|
||||
{% if adoption_notice.further_information %}
|
||||
<p>{% translate "Externe Quelle" %}: {{ adoption_notice.link_to_more_information | safe }}</p>
|
||||
{% endif %}
|
||||
<div>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden"
|
||||
name="adoption_notice_id"
|
||||
value="{{ adoption_notice.pk }}">
|
||||
<input type="hidden" name="action" value="checked_active">
|
||||
<button class="btn" type="submit">{% translate "Vermittlung noch aktuell" %}</button>
|
||||
</form>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden"
|
||||
name="adoption_notice_id"
|
||||
value="{{ adoption_notice.pk }}">
|
||||
<input type="hidden" name="action" value="checked_inactive">
|
||||
<button class="btn" type="submit">{% translate "Vermittlung inaktiv" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -3,32 +3,16 @@
|
||||
{% block content %}
|
||||
<h1>{% translate "Aktualitätscheck" %}</h1>
|
||||
<p>{% translate "Überprüfe ob Vermittlungen noch aktuell sind" %}</p>
|
||||
{% for adoption_notice in adoption_notices %}
|
||||
<div class="card">
|
||||
<h1>
|
||||
<a href="{{ adoption_notice.get_absolute_url }}">{{ adoption_notice.name }}</a>
|
||||
</h1>
|
||||
{% if adoption_notice.further_information %}
|
||||
<p>{% translate "Externe Quelle" %}: {{ adoption_notice.link_to_more_information | safe }}</p>
|
||||
{% endif %}
|
||||
<div>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden"
|
||||
name="adoption_notice_id"
|
||||
value="{{ adoption_notice.pk }}">
|
||||
<input type="hidden" name="action" value="checked_active">
|
||||
<button class="btn" type="submit">{% translate "Vermittlung noch aktuell" %}</button>
|
||||
</form>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden"
|
||||
name="adoption_notice_id"
|
||||
value="{{ adoption_notice.pk }}">
|
||||
<input type="hidden" name="action" value="checked_inactive">
|
||||
<button class="btn" type="submit">{% translate "Vermittlung inaktiv" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="container-cards spaced">
|
||||
<h1>{% translate 'Deaktivierte Vermittlungen zur Überprüfung' %}</h1>
|
||||
{% for adoption_notice in adoption_notices_disabled %}
|
||||
{% include "fellchensammlung/partials/partial-check-adoption-notice.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="container-cards spaced">
|
||||
<h1>{% translate 'Aktive Vermittlungen zur Überprüfung' %}</h1>
|
||||
{% for adoption_notice in adoption_notices_active %}
|
||||
{% include "fellchensammlung/partials/partial-check-adoption-notice.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
@ -1,7 +1,10 @@
|
||||
import logging
|
||||
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
from fellchensammlung.models import AdoptionNotice, Location, RescueOrganization, AdoptionNoticeStatus
|
||||
from fellchensammlung.models import AdoptionNotice, Location, RescueOrganization, AdoptionNoticeStatus, Log
|
||||
from fellchensammlung.tools.misc import is_404
|
||||
|
||||
|
||||
def clean_locations(quiet=True):
|
||||
@ -54,12 +57,28 @@ def get_unchecked_adoption_notices(weeks=3):
|
||||
|
||||
# Query for active adoption notices that were checked in the last three weeks
|
||||
unchecked_adoptions = AdoptionNotice.objects.filter(
|
||||
last_checked__gte=three_weeks_ago
|
||||
last_checked__lte=three_weeks_ago
|
||||
)
|
||||
active_unchecked_adoptions = [adoption for adoption in unchecked_adoptions if adoption.is_active]
|
||||
return active_unchecked_adoptions
|
||||
|
||||
|
||||
def get_active_adoption_notices():
|
||||
ans = AdoptionNotice.objects.all()
|
||||
active_adoptions = [adoption for adoption in ans if adoption.is_active]
|
||||
return active_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()
|
||||
AdoptionNoticeStatus.objects.get(adoption_notice=adoption_notice).set_unchecked()
|
||||
|
||||
|
||||
def deactivate_404_adoption_notices():
|
||||
for adoption_notice in get_active_adoption_notices():
|
||||
if adoption_notice.further_information and adoption_notice.further_information != "":
|
||||
if is_404(adoption_notice.further_information):
|
||||
adoption_notice.set_closed()
|
||||
logging_msg = f"Automatically set Adoption Notice {adoption_notice.id} closed as link to more information returened 404"
|
||||
logging.info(logging_msg)
|
||||
Log.objects.create(action="automated", text=logging_msg)
|
||||
|
@ -34,3 +34,11 @@ def healthcheck_ok():
|
||||
requests.get(settings.HEALTHCHECKS_URL, timeout=10)
|
||||
except requests.RequestException as e:
|
||||
logging.error("Ping to healthcheck-server failed: %s" % e)
|
||||
|
||||
|
||||
def is_404(url):
|
||||
try:
|
||||
result = requests.get(url, timeout=10)
|
||||
return result.status_code == 404
|
||||
except requests.RequestException as e:
|
||||
logging.warning(f"Request to {url} failed: {e}")
|
||||
|
@ -172,7 +172,7 @@ def search(request):
|
||||
if max_distance == "":
|
||||
max_distance = None
|
||||
geo_api = GeoAPI()
|
||||
search_position = geo_api.get_coordinates_from_query(request.POST['postcode'])
|
||||
search_position = geo_api.get_coordinates_from_query(request.POST['location'])
|
||||
if search_position is None:
|
||||
place_not_found = True
|
||||
adoption_notices_in_distance = active_adoptions
|
||||
@ -205,16 +205,9 @@ def add_adoption_notice(request):
|
||||
|
||||
# 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"]
|
||||
instance.set_active()
|
||||
else:
|
||||
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)
|
||||
status.save()
|
||||
instance.set_unchecked()
|
||||
|
||||
# Get the species and number of animals from the form
|
||||
species = form.cleaned_data["species"]
|
||||
@ -347,7 +340,7 @@ def about(request):
|
||||
lang = Language.objects.get(languagecode=language_code)
|
||||
|
||||
legal = {}
|
||||
for text_code in ["terms_of_service", "privacy_statement", "imprint"]:
|
||||
for text_code in ["terms_of_service", "privacy_statement", "imprint", "about_us"]:
|
||||
try:
|
||||
legal[text_code] = Text.objects.get(text_code=text_code, language=lang, )
|
||||
except Text.DoesNotExist:
|
||||
@ -456,23 +449,21 @@ def modqueue(request):
|
||||
def updatequeue(request):
|
||||
#TODO: Make sure update can only be done for instances with permission
|
||||
if request.method == "POST":
|
||||
print(request.POST.get("adoption_notice_id"))
|
||||
adoption_notice = AdoptionNotice.objects.get(id=request.POST.get("adoption_notice_id"))
|
||||
action = request.POST.get("action")
|
||||
print(f"Action: {action}")
|
||||
if action == "checked_inactive":
|
||||
adoption_notice.set_closed()
|
||||
elif action == "checked_active":
|
||||
print("set checked")
|
||||
adoption_notice.set_checked()
|
||||
if action == "checked_active":
|
||||
adoption_notice.set_active()
|
||||
|
||||
if user_is_trust_level_or_above(request.user, User.MODERATOR):
|
||||
last_checked_adoption_list = AdoptionNotice.objects.order_by("last_checked")
|
||||
else:
|
||||
last_checked_adoption_list = AdoptionNotice.objects.filter(owner=request.user).order_by("last_checked")
|
||||
adoption_notices = [adoption for adoption in last_checked_adoption_list if adoption.is_active or adoption.is_to_be_checked]
|
||||
|
||||
context = {"adoption_notices": adoption_notices}
|
||||
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.is_disabled_unchecked]
|
||||
context = {"adoption_notices_disabled": adoption_notices_disabled,
|
||||
"adoption_notices_active": adoption_notices_active}
|
||||
return render(request, 'fellchensammlung/updatequeue.html', context=context)
|
||||
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
__version__ = "0.3.0"
|
||||
__version__ = "0.3.1"
|
||||
|
||||
# This will make sure the app is always imported when
|
||||
# Django starts so that shared_task will use this app.
|
||||
|
@ -16,10 +16,14 @@ app.conf.beat_schedule = {
|
||||
'task': 'admin.clean_locations',
|
||||
'schedule': crontab(hour=2),
|
||||
},
|
||||
'daily-deactivation': {
|
||||
'task': 'admin.deactivate_unchecked',
|
||||
'daily-unchecked-deactivation': {
|
||||
'task': 'admin.daily_unchecked_deactivation',
|
||||
'schedule': crontab(hour=1),
|
||||
},
|
||||
'daily-404-deactivation': {
|
||||
'task': 'admin.deactivate_404_adoption_notices',
|
||||
'schedule': crontab(hour=3),
|
||||
},
|
||||
}
|
||||
|
||||
if settings.HEALTHCHECKS_URL is not None and settings.HEALTHCHECKS_URL != "":
|
||||
|
@ -76,6 +76,7 @@ DB_NAME = config.get("database", "name", fallback="notfellchen.sqlite3")
|
||||
DB_USER = config.get("database", "user", fallback='')
|
||||
DB_PASSWORD = config.get("database", "password", fallback='')
|
||||
DB_HOST = config.get("database", "host", fallback='')
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale')]
|
||||
@ -92,7 +93,6 @@ GEOCODING_API_URL = config.get("geocoding", "api_url", fallback="https://nominat
|
||||
""" Tile Server """
|
||||
MAP_TILE_SERVER = config.get("map", "tile_server", fallback="https://tiles.hyteck.de")
|
||||
|
||||
|
||||
""" OxiTraffic"""
|
||||
OXITRAFFIC_ENABLED = config.get("tracking", "oxitraffic_enabled", fallback=False)
|
||||
OXITRAFFIC_BASE_URL = config.get("tracking", "oxitraffic_base_url", fallback="")
|
||||
@ -187,6 +187,8 @@ MIDDLEWARE = [
|
||||
|
||||
ROOT_URLCONF = 'notfellchen.urls'
|
||||
|
||||
SETTINGS_PATH = os.path.dirname(os.path.dirname(__file__))
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
|
97
src/tests/test_admin_tasks.py
Normal file
97
src/tests/test_admin_tasks.py
Normal file
@ -0,0 +1,97 @@
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
from fellchensammlung.tools.admin import get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
|
||||
deactivate_404_adoption_notices
|
||||
from fellchensammlung.tools.misc import is_404
|
||||
from django.test import TestCase
|
||||
|
||||
from model_bakery import baker
|
||||
from fellchensammlung.models import AdoptionNotice
|
||||
|
||||
|
||||
class DeactiviationTest(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
now = timezone.now()
|
||||
more_than_three_weeks_ago = now - timedelta(weeks=3, days=2)
|
||||
less_than_three_weeks_ago = now - timedelta(weeks=1, days=2)
|
||||
|
||||
cls.adoption1 = baker.make(AdoptionNotice,
|
||||
name="TestAdoption1",
|
||||
created_at=more_than_three_weeks_ago,
|
||||
last_checked=more_than_three_weeks_ago)
|
||||
cls.adoption2 = baker.make(AdoptionNotice, name="TestAdoption2")
|
||||
cls.adoption3 = baker.make(AdoptionNotice,
|
||||
name="TestAdoption3",
|
||||
created_at=less_than_three_weeks_ago,
|
||||
last_checked=less_than_three_weeks_ago)
|
||||
|
||||
cls.adoption1.set_active()
|
||||
cls.adoption3.set_active()
|
||||
|
||||
def test_get_unchecked_adoption_notices(self):
|
||||
result = get_unchecked_adoption_notices()
|
||||
|
||||
self.assertIn(self.adoption1, result)
|
||||
self.assertNotIn(self.adoption2, result)
|
||||
self.assertNotIn(self.adoption3, result)
|
||||
|
||||
def test_deactivate_unchecked_adoption_notices(self):
|
||||
self.assertTrue(self.adoption1.is_active)
|
||||
self.assertFalse(self.adoption2.is_active)
|
||||
self.assertTrue(self.adoption3.is_active)
|
||||
|
||||
deactivate_unchecked_adoption_notices()
|
||||
|
||||
self.adoption1.refresh_from_db()
|
||||
self.adoption2.refresh_from_db()
|
||||
self.adoption3.refresh_from_db()
|
||||
|
||||
self.assertFalse(self.adoption1.is_active)
|
||||
self.assertFalse(self.adoption2.is_active)
|
||||
self.assertTrue(self.adoption3.is_active)
|
||||
|
||||
|
||||
class PingTest(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
link_active = "https://hyteck.de/"
|
||||
link_inactive = "https://hyteck.de/maxwell"
|
||||
now = timezone.now()
|
||||
less_than_three_weeks_ago = now - timedelta(weeks=1, days=2)
|
||||
|
||||
cls.adoption1 = baker.make(AdoptionNotice,
|
||||
name="TestAdoption1",
|
||||
created_at=less_than_three_weeks_ago,
|
||||
last_checked=less_than_three_weeks_ago,
|
||||
further_information=link_active)
|
||||
cls.adoption2 = baker.make(AdoptionNotice,
|
||||
name="TestAdoption2",
|
||||
created_at=less_than_three_weeks_ago,
|
||||
last_checked=less_than_three_weeks_ago,
|
||||
further_information=link_inactive)
|
||||
cls.adoption3 = baker.make(AdoptionNotice,
|
||||
name="TestAdoption3",
|
||||
created_at=less_than_three_weeks_ago,
|
||||
last_checked=less_than_three_weeks_ago,
|
||||
further_information=None)
|
||||
cls.adoption1.set_active()
|
||||
cls.adoption2.set_active()
|
||||
cls.adoption3.set_active()
|
||||
|
||||
def test_is_404(self):
|
||||
urls = [("https://hyteck.de/maxwell", True),
|
||||
("https://hyteck.de", False)]
|
||||
for url, expected_result in urls:
|
||||
self.assertEqual(is_404(url), expected_result)
|
||||
|
||||
def test_deactivate_404_adoption_notices(self):
|
||||
self.assertTrue(self.adoption1.is_active)
|
||||
self.assertTrue(self.adoption2.is_active)
|
||||
deactivate_404_adoption_notices()
|
||||
self.adoption1.refresh_from_db()
|
||||
self.adoption2.refresh_from_db()
|
||||
self.assertTrue(self.adoption1.is_active)
|
||||
self.assertFalse(self.adoption2.is_active)
|
||||
|
24
src/tests/test_geo.py
Normal file
24
src/tests/test_geo.py
Normal file
@ -0,0 +1,24 @@
|
||||
from fellchensammlung.tools.geo import calculate_distance_between_coordinates
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class DistanceTest(TestCase):
|
||||
accuracy = 1.05 # 5% off is ok
|
||||
|
||||
def test_calculate_distance_between_coordinates(self):
|
||||
coordinates_berlin = (52.50327,13.41238)
|
||||
coordinates_stuttgart = (48.77753579028781, 9.185250111016634)
|
||||
coordinates_weil_im_dorf = (48.813691653929276, 9.112217733791029)
|
||||
coordinates_with_distance = {"berlin_stuttgart": (coordinates_berlin, coordinates_stuttgart, 510),
|
||||
"stuttgart_berlin": (coordinates_stuttgart, coordinates_berlin, 510),
|
||||
"stuttgart_weil": (coordinates_stuttgart, coordinates_weil_im_dorf, 6.7),
|
||||
}
|
||||
for key in coordinates_with_distance:
|
||||
(a, b, distance) = coordinates_with_distance[key]
|
||||
result = calculate_distance_between_coordinates(a, b)
|
||||
try:
|
||||
self.assertLess(result, distance * self.accuracy)
|
||||
self.assertGreater(result, distance / self.accuracy)
|
||||
except AssertionError as e:
|
||||
print(f"Distance calculation failed. Expected {distance}, got {result}")
|
||||
raise e
|
@ -4,7 +4,9 @@ from django.urls import reverse
|
||||
|
||||
from model_bakery import baker
|
||||
|
||||
from fellchensammlung.models import Animal, Species, AdoptionNotice, User
|
||||
from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus
|
||||
from fellchensammlung.views import add_adoption_notice
|
||||
|
||||
|
||||
class AnimalAndAdoptionTest(TestCase):
|
||||
@classmethod
|
||||
@ -47,3 +49,123 @@ class AnimalAndAdoptionTest(TestCase):
|
||||
self.assertEqual(str(response.context['user']), 'testuser0')
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertContains(response, "Rat1")
|
||||
|
||||
|
||||
class SearchTest(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
test_user0 = User.objects.create_user(username='testuser0',
|
||||
first_name="Admin",
|
||||
last_name="BOFH",
|
||||
password='12345')
|
||||
test_user0.save()
|
||||
|
||||
# Location of Berlin: lat 52.5170365 lon 13.3888599 PLZ 10115 (Mitte)
|
||||
|
||||
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
|
||||
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2")
|
||||
adoption3 = baker.make(AdoptionNotice, name="TestAdoption3")
|
||||
|
||||
berlin = Location.get_location_from_string("Berlin")
|
||||
adoption1.location = berlin
|
||||
adoption1.save()
|
||||
stuttgart = Location.get_location_from_string("Tübingen")
|
||||
adoption3.location = stuttgart
|
||||
adoption3.save()
|
||||
|
||||
adoption1.set_active()
|
||||
adoption3.set_active()
|
||||
adoption2.set_unchecked()
|
||||
|
||||
def test_basic_view(self):
|
||||
response = self.client.get(reverse('search'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertNotContains(response, "TestAdoption2")
|
||||
self.assertContains(response, "TestAdoption3")
|
||||
|
||||
def test_basic_view_logged_in(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
response = self.client.get(reverse('search'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Check our user is logged in
|
||||
self.assertEqual(str(response.context['user']), 'testuser0')
|
||||
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertContains(response, "TestAdoption3")
|
||||
self.assertNotContains(response, "TestAdoption2")
|
||||
|
||||
def test_plz_search(self):
|
||||
response = self.client.post(reverse('search'), {"max_distance": 100, "location": "Berlin"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertNotContains(response, "TestAdoption3")
|
||||
|
||||
|
||||
class UpdateQueueTest(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
test_user0 = User.objects.create_user(username='testuser0',
|
||||
first_name="Admin",
|
||||
last_name="BOFH",
|
||||
password='12345',
|
||||
trust_level=User.TRUST_LEVEL[User.MODERATOR])
|
||||
test_user0.is_superuser = True
|
||||
test_user0.save()
|
||||
|
||||
# Location of Berlin: lat 52.5170365 lon 13.3888599 PLZ 10115 (Mitte)
|
||||
|
||||
cls.adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
|
||||
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2")
|
||||
cls.adoption3 = baker.make(AdoptionNotice, name="TestAdoption3")
|
||||
|
||||
cls.adoption1.set_unchecked()
|
||||
cls.adoption3.set_unchecked()
|
||||
|
||||
def test_login_required(self):
|
||||
response = self.client.get(reverse('updatequeue'))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEquals(response.url, "/accounts/login/?next=/updatequeue/")
|
||||
|
||||
def test_set_updated(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
|
||||
# First get the list
|
||||
response = self.client.get(reverse('updatequeue'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Make sure Adoption1 is in response
|
||||
self.assertContains(response, "TestAdoption1")
|
||||
self.assertNotContains(response, "TestAdoption2")
|
||||
|
||||
self.assertFalse(self.adoption1.is_active)
|
||||
|
||||
# Mark as checked
|
||||
response = self.client.post(reverse('updatequeue'), {"adoption_notice_id": self.adoption1.pk,
|
||||
"action": "checked_active"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.adoption1.refresh_from_db()
|
||||
self.assertTrue(self.adoption1.is_active)
|
||||
|
||||
def test_set_checked_inactive(self):
|
||||
self.client.login(username='testuser0', password='12345')
|
||||
# First get the list
|
||||
response = self.client.get(reverse('updatequeue'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Make sure Adoption3 is in response
|
||||
self.assertContains(response, "TestAdoption3")
|
||||
self.assertNotContains(response, "TestAdoption2")
|
||||
|
||||
self.assertFalse(self.adoption3.is_active)
|
||||
|
||||
# Mark as checked
|
||||
response = self.client.post(reverse('updatequeue'),
|
||||
{"adoption_notice_id": self.adoption3.id, "action": "checked_inactive"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.adoption3.refresh_from_db()
|
||||
|
||||
# Make sure correct status is set and AN is not shown anymore
|
||||
self.assertNotContains(response, "TestAdoption3")
|
||||
self.assertFalse(self.adoption3.is_active)
|
||||
self.assertEqual(self.adoption3.adoptionnoticestatus.major_status, AdoptionNoticeStatus.CLOSED)
|
||||
|
Loading…
Reference in New Issue
Block a user