Compare commits

...

10 Commits

10 changed files with 237 additions and 9 deletions

View File

@ -0,0 +1,20 @@
# Generated by Django 5.1.4 on 2025-03-09 08:31
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0037_alter_basenotification_title'),
]
operations = [
migrations.AddField(
model_name='rescueorganization',
name='last_checked',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Datum der letzten Prüfung'),
preserve_default=False,
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2025-03-09 16:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0038_rescueorganization_last_checked'),
]
operations = [
migrations.AlterField(
model_name='rescueorganization',
name='last_checked',
field=models.DateTimeField(auto_now_add=True, verbose_name='Datum der letzten Prüfung'),
),
]

View File

@ -122,6 +122,7 @@ class RescueOrganization(models.Model):
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)
last_checked = models.DateTimeField(auto_now_add=True, verbose_name=_('Datum der letzten Prüfung'))
internal_comment = models.TextField(verbose_name=_("Interner Kommentar"), null=True, blank=True, )
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung')) # Markdown allowed
external_object_identifier = models.CharField(max_length=200, null=True, blank=True,
@ -149,7 +150,20 @@ class RescueOrganization(models.Model):
if self.description is None:
return ""
if len(self.description) > 200:
return self.description[:200] + f" ... [weiterlesen]({self.get_absolute_url()})"
return self.description[:200] + _(f" ... [weiterlesen]({self.get_absolute_url()})")
def set_checked(self):
self.last_checked = timezone.now()
self.save()
@property
def last_checked_hr(self):
time_since_last_checked = timezone.now() - self.last_checked
return time_since_as_hr_string(time_since_last_checked)
@property
def species_urls(self):
return SpeciesSpecificURL.objects.filter(organization=self)
# Admins can perform all actions and have the highest trust associated with them

View File

@ -0,0 +1,21 @@
{% load i18n %}
{% load custom_tags %}
<div class="card">
<h1>
<a href="{{ rescue_org.get_absolute_url }}">{{ rescue_org.name }}</a>
</h1>
<i>{% translate 'Zuletzt geprüft:' %} {{ rescue_org.last_checked_hr }}</i>
{% if rescue_org.website %}
<p>{% translate "Website" %}: {{ rescue_org.website | safe }}</p>
{% endif %}
<div class="container-edit-buttons">
<form method="post">
{% csrf_token %}
<input type="hidden"
name="rescue_organization_id"
value="{{ rescue_org.pk }}">
<input type="hidden" name="action" value="checked">
<button class="btn" type="submit">{% translate "Organisation geprüft" %}</button>
</form>
</div>
</div>

View File

@ -0,0 +1,12 @@
{% extends "fellchensammlung/base_generic.html" %}
{% load i18n %}
{% block content %}
<h1>{% translate "Aktualitätscheck" %}</h1>
<p>{% translate "Überprüfe ob im Tierheim neue Vermittlungen ein Zuhause suchen" %}</p>
<div class="container-cards spaced">
<h1>{% translate 'Organisation zur Überprüfung' %}</h1>
{% for rescue_org in rescue_orgs %}
{% include "fellchensammlung/partials/partial-check-rescue-org.html" %}
{% endfor %}
</div>
{% endblock %}

View File

@ -31,9 +31,11 @@ urlpatterns = [
# ex: /adoption_notice/7/edit
path("vermittlung/<int:adoption_notice_id>/edit", views.adoption_notice_edit, name="adoption-notice-edit"),
# ex: /vermittlung/5/add-photo
path("vermittlung/<int:adoption_notice_id>/add-photo", views.add_photo_to_adoption_notice, name="adoption-notice-add-photo"),
path("vermittlung/<int:adoption_notice_id>/add-photo", views.add_photo_to_adoption_notice,
name="adoption-notice-add-photo"),
# ex: /adoption_notice/2/add-animal
path("vermittlung/<int:adoption_notice_id>/add-animal", views.adoption_notice_add_animal, name="adoption-notice-add-animal"),
path("vermittlung/<int:adoption_notice_id>/add-animal", views.adoption_notice_add_animal,
name="adoption-notice-add-animal"),
path("tierschutzorganisationen/", views.list_rescue_organizations, name="rescue-organizations"),
path("organisation/<int:rescue_organization_id>/", views.detail_view_rescue_organization,
@ -57,9 +59,11 @@ urlpatterns = [
path("meldung/<uuid:report_id>/", views.report_detail, name="report-detail"),
path("meldung/<uuid:report_id>/sucess", views.report_detail_success, name="report-detail-success"),
path("modqueue/", views.modqueue, name="modqueue"),
path("updatequeue/", views.updatequeue, name="updatequeue"),
path("organization-check/", views.rescue_organization_check, name="organization-check"),
###########
## USERS ##
###########

View File

@ -639,3 +639,18 @@ def styleguide(request):
context = {"geocoding_api_url": settings.GEOCODING_API_URL, }
return render(request, 'fellchensammlung/styleguide.html', context=context)
@login_required
def rescue_organization_check(request):
if request.method == "POST":
rescue_org = RescueOrganization.objects.get(id=request.POST.get("rescue_organization_id"))
edit_permission = user_is_trust_level_or_above(request.user, TrustLevel.MODERATOR)
if not edit_permission:
return render(request, "fellchensammlung/errors/403.html", status=403)
action = request.POST.get("action")
if action == "checked":
rescue_org.set_checked()
last_checked_rescue_orgs = RescueOrganization.objects.order_by("last_checked")
context = {"rescue_orgs": last_checked_rescue_orgs,}
return render(request, 'fellchensammlung/rescue-organization-check.html', context=context)

View File

@ -89,9 +89,9 @@ CELERY_RESULT_BACKEND = config.get("celery", "backend", fallback="redis://localh
HEALTHCHECKS_URL = config.get("monitoring", "healthchecks_url", fallback=None)
""" 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://photon.hyteck.de/api")
# GEOCODING_API_FORMAT is allowed to be one of ['nominatim', 'photon']
GEOCODING_API_FORMAT = config.get("geocoding", "api_format", fallback="nominatim")
GEOCODING_API_FORMAT = config.get("geocoding", "api_format", fallback="photon")
""" Tile Server """
MAP_TILE_SERVER = config.get("map", "tile_server", fallback="https://tiles.hyteck.de")

View File

@ -5,7 +5,8 @@ from django.urls import reverse
from model_bakery import baker
from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus, TrustLevel, \
Animal, Subscriptions, Comment, CommentNotification
Animal, Subscriptions, Comment, CommentNotification, SearchSubscription
from fellchensammlung.tools.geo import LocationProxy
from fellchensammlung.views import add_adoption_notice
@ -146,6 +147,35 @@ class SearchTest(TestCase):
self.assertContains(response, "TestAdoption3")
self.assertNotContains(response, "TestAdoption2")
def test_unauthenticated_subscribe(self):
response = self.client.post(reverse('search'), {"subscribe_to_search": ""})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/suchen/")
def test_unauthenticated_unsubscribe(self):
response = self.client.post(reverse('search'), {"unsubscribe_to_search": 1})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/suchen/")
def test_subscribe(self):
self.client.login(username='testuser0', password='12345')
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A",
"subscribe_to_search": ""})
self.assertEqual(response.status_code, 200)
self.assertTrue(SearchSubscription.objects.filter(owner=User.objects.get(username='testuser0'),
max_distance=50).exists())
def test_unsubscribe(self):
user0 = User.objects.get(username='testuser0')
self.client.login(username='testuser0', password='12345')
location = Location.get_location_from_string("München")
subscription = SearchSubscription.objects.create(owner=user0, max_distance=200, location=location, sex="A")
response = self.client.post(reverse('search'), {"max_distance": 200, "location_string": "München", "sex": "A",
"unsubscribe_to_search": subscription.pk})
self.assertEqual(response.status_code, 200)
self.assertFalse(SearchSubscription.objects.filter(owner=User.objects.get(username='testuser0'),
max_distance=200).exists())
def test_location_search(self):
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A"})
self.assertEqual(response.status_code, 200)
@ -357,7 +387,8 @@ class AdoptionEditTest(TestCase):
an = AdoptionNotice.objects.get(name="TestAdoption1")
assert self.client.login(username='testuser0', password='12345')
response = self.client.post(reverse("adoption-notice-edit", args=str(an.pk)), data=data, follow=True)
self.assertEqual(response.redirect_chain[0][1], 302) # See https://docs.djangoproject.com/en/5.1/topics/testing/tools/
self.assertEqual(response.redirect_chain[0][1],
302) # See https://docs.djangoproject.com/en/5.1/topics/testing/tools/
self.assertEqual(response.status_code, 200) # Redirects to AN page
self.assertContains(response, "Test3")
self.assertContains(response, "Mia")

View File

@ -1,7 +1,8 @@
from django.test import TestCase
from django.urls import reverse
from fellchensammlung.models import User, TrustLevel, AdoptionNotice, Species
from docs.conf import language
from fellchensammlung.models import User, TrustLevel, AdoptionNotice, Species, Rule, Language, Comment, ReportComment
from model_bakery import baker
@ -26,6 +27,19 @@ class BasicViewTest(TestCase):
for i in range(0, 4):
AdoptionNotice.objects.get(name=f"TestAdoption{i}").set_active()
rule1 = Rule.objects.create(title="Rule 1", rule_text="Description of r1", rule_identifier="rule1",
language=Language.objects.get(name="English"))
an1 = AdoptionNotice.objects.get(name="TestAdoption0")
comment1 = Comment.objects.create(adoption_notice=an1, text="Comment1", user=test_user1)
comment2 = Comment.objects.create(adoption_notice=an1, text="Comment2", user=test_user1)
comment3 = Comment.objects.create(adoption_notice=an1, text="Comment3", user=test_user1)
report_comment1 = ReportComment.objects.create(reported_comment=comment1,
user_comment="ReportComment1")
report_comment1.save()
report_comment1.reported_broken_rules.set({rule1,})
def test_index_logged_in(self):
self.client.login(username='testuser0', password='12345')
@ -41,3 +55,82 @@ class BasicViewTest(TestCase):
self.assertEqual(response.status_code, 200)
self.assertContains(response, "TestAdoption1")
self.assertNotContains(response, "TestAdoption4") # Should not be active, therefore not shown
def test_about_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(reverse('about'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Rule 1")
def test_about_anonymous(self):
response = self.client.get(reverse('about'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Rule 1")
def test_report_adoption_logged_in(self):
self.client.login(username='testuser0', password='12345')
an = AdoptionNotice.objects.get(name="TestAdoption0")
response = self.client.get(reverse('report-adoption-notice', args=str(an.pk)))
self.assertEqual(response.status_code, 200)
data = {"reported_broken_rules": 1, "user_comment": "animal cruelty"}
response = self.client.post(reverse('report-adoption-notice', args=str(an.pk)), data=data)
self.assertEqual(response.status_code, 302)
def test_report_adoption_anonymous(self):
an = AdoptionNotice.objects.get(name="TestAdoption0")
response = self.client.get(reverse('report-adoption-notice', args=str(an.pk)))
self.assertEqual(response.status_code, 200)
data = {"reported_broken_rules": 1, "user_comment": "animal cruelty"}
response = self.client.post(reverse('report-adoption-notice', args=str(an.pk)), data=data)
self.assertEqual(response.status_code, 302)
def test_report_comment_logged_in(self):
self.client.login(username='testuser0', password='12345')
c = Comment.objects.get(text="Comment1")
response = self.client.get(reverse('report-comment', args=str(c.pk)))
self.assertEqual(response.status_code, 200)
data = {"reported_broken_rules": 1, "user_comment": "animal cruelty"}
response = self.client.post(reverse('report-comment', args=str(c.pk)), data=data)
self.assertEqual(response.status_code, 302)
self.assertTrue(ReportComment.objects.filter(reported_comment=c.pk).exists())
def test_report_comment_anonymous(self):
c = Comment.objects.get(text="Comment2")
response = self.client.get(reverse('report-comment', args=str(c.pk)))
self.assertEqual(response.status_code, 200)
data = {"reported_broken_rules": 1, "user_comment": "animal cruelty"}
response = self.client.post(reverse('report-comment', args=str(c.pk)), data=data)
self.assertEqual(response.status_code, 302)
self.assertTrue(ReportComment.objects.filter(reported_comment=c.pk).exists())
def test_show_report_details_logged_in(self):
self.client.login(username='testuser0', password='12345')
report = ReportComment.objects.get(user_comment="ReportComment1")
response = self.client.get(reverse('report-detail', args=(report.pk,)))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Rule 1")
self.assertContains(response, "ReportComment1")
self.assertNotContains(response, '<form action="allow" class="">')
def test_show_report_details_anonymous(self):
report = ReportComment.objects.get(user_comment="ReportComment1")
response = self.client.get(reverse('report-detail', args=(report.pk,)))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Rule 1")
self.assertContains(response, "ReportComment1")
self.assertNotContains(response, '<form action="allow" class="">')
def test_show_report_details_admin(self):
self.client.login(username='testuser1', password='12345')
report = ReportComment.objects.get(user_comment="ReportComment1")
response = self.client.get(reverse('report-detail', args=(report.pk,)))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Rule 1")
self.assertContains(response, "ReportComment1")
self.assertContains(response, '<form action="allow" class="">')