Compare commits

3 Commits

Author SHA1 Message Date
c9f46d7547 ci: fix?
All checks were successful
ci/woodpecker/push/docs Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
2025-01-19 07:12:09 +01:00
9f23f5768c ci: restructure
Some checks failed
ci/woodpecker/push/docs Pipeline failed
ci/woodpecker/push/test Pipeline failed
2025-01-19 07:07:53 +01:00
19210f90cd ci: try teests 2025-01-19 07:04:22 +01:00
18 changed files with 55 additions and 654 deletions

View File

@@ -6,6 +6,9 @@ steps:
commands:
- cd docs && make html
when:
event: [ tag, push ]
deploy:
image: appleboy/drone-scp
settings:
@@ -19,6 +22,8 @@ steps:
source: docs/_build/html/
key:
from_secret: ssh_key
when:
event: [ tag, push ]

14
.woodpecker/test.yml Normal file
View File

@@ -0,0 +1,14 @@
---
steps:
test:
image: python
commands:
- python -m pip install '.[develop]'
- coverage run --source='.' src/manage.py test src && coverage html
- coverage html
- cat htmlcov/index.html
when:
event: [tag, push]

View File

@@ -1,97 +0,0 @@
import argparse
import json
import os
import requests
DEFAULT_OSM_DATA_FILE = "osm_data.geojson"
def parse_args():
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(description="Upload animal shelter data to the Notfellchen API.")
parser.add_argument("--api-token", type=str, help="API token for authentication.")
parser.add_argument("--instance", type=str, help="API instance URL.")
parser.add_argument("--data-file", type=str, help="Path to the GeoJSON file containing (only) animal shelters.")
return parser.parse_args()
def get_config():
"""Get configuration from environment variables or command-line arguments."""
args = parse_args()
api_token = args.api_token or os.getenv("NOTFELLCHEN_API_TOKEN")
instance = args.instance or os.getenv("NOTFELLCHEN_INSTANCE")
data_file = args.data_file or os.getenv("NOTFELLCHEN_DATA_FILE", DEFAULT_OSM_DATA_FILE)
if not api_token or not instance:
raise ValueError("API token and instance URL must be provided via environment variables or CLI arguments.")
return api_token, instance, data_file
def load_osm_data(file_path):
"""Load OSM data from a GeoJSON file."""
with open(file_path, "r", encoding="utf-8") as file:
data = json.load(file)
return data
def load_osm_data(file_path):
#Load OSM data from a GeoJSON file.
with open(file_path, "r", encoding="utf-8") as file:
data = json.load(file)
return data
def transform_osm_data(feature):
#Transform a single OSM feature into the API payload format
prop = feature.get("properties", {})
geometry = feature.get("geometry", {})
return {
"name": prop.get("name", "Unnamed Shelter"),
"phone": prop.get("phone"),
"website": prop.get("website"),
"opening_hours": prop.get("opening_hours"),
"email": prop.get("email"),
"location_string": f'{prop.get("addr:street", "")} {prop.get("addr:housenumber", "")} {prop.get("addr:postcode", "")} {prop.get("addr:city", "")}',
"external_object_id": prop.get("@id"),
"external_source_id": "OSM"
}
def send_to_api(data, endpoint, headers):
# Send transformed data to the Notfellchen API.
response = requests.post(endpoint, headers=headers, json=data)
if response.status_code == 201:
print(f"Success: Shelter '{data['name']}' uploaded.")
elif response.status_code == 400:
print(f"Error: Shelter '{data['name']}' already exists or invalid data. {response.text}")
else:
print(f"Unexpected Error: {response.status_code} - {response.text}")
raise ConnectionError
def main():
# Get configuration
api_token, instance, data_file = get_config()
# Set headers and endpoint
endpoint = f"{instance}/api/organizations/"
headers = {
"Authorization": f"Token {api_token}",
"Content-Type": "application/json"
}
# Step 1: Load OSM data
osm_data = load_osm_data(data_file)
# Step 2: Process each shelter and send it to the API
for feature in osm_data.get("features", []):
shelter_data = transform_osm_data(feature)
send_to_api(shelter_data, endpoint, headers)
if __name__ == "__main__":
main()

View File

@@ -16,7 +16,6 @@ from .serializers import (
from fellchensammlung.models import Animal, RescueOrganization, AdoptionNotice, Species, Image
from drf_spectacular.utils import extend_schema
class AdoptionNoticeApiView(APIView):
permission_classes = [IsAuthenticated]
@@ -85,6 +84,7 @@ class AdoptionNoticeApiView(APIView):
)
class AnimalApiView(APIView):
permission_classes = [IsAuthenticated]
@@ -118,7 +118,6 @@ class AnimalApiView(APIView):
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class RescueOrganizationApiView(APIView):
permission_classes = [IsAuthenticated]
@@ -160,14 +159,13 @@ class RescueOrganizationApiView(APIView):
"""
serializer = RescueOrgSerializer(data=request.data, context={"request": request})
if serializer.is_valid():
rescue_org = serializer.save()
rescue_org = serializer.save(owner=request.user)
return Response(
{"message": "Rescue organization created/updated successfully!", "id": rescue_org.id},
status=status.HTTP_201_CREATED,
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class AddImageApiView(APIView):
permission_classes = [IsAuthenticated]

View File

@@ -1,20 +0,0 @@
# 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

@@ -1,18 +0,0 @@
# 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,7 +122,6 @@ 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,
@@ -150,20 +149,7 @@ 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()})")
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)
return self.description[:200] + f" ... [weiterlesen]({self.get_absolute_url()})"
# Admins can perform all actions and have the highest trust associated with them
@@ -680,30 +666,6 @@ class Report(models.Model):
def get_moderation_actions(self):
return ModerationAction.objects.filter(report=self)
@property
def reported_content(self):
"""
Dynamically fetch the reported content based on subclass.
The alternative would be to use the ContentType framework:
https://docs.djangoproject.com/en/5.1/ref/contrib/contenttypes/
"""
if hasattr(self, "reportadoptionnotice"):
return self.reportadoptionnotice.adoption_notice
elif hasattr(self, "reportcomment"):
return self.reportcomment.reported_comment
return None
@property
def reported_content_url(self):
"""
Same as reported_content, just for url
"""
if hasattr(self, "reportadoptionnotice"):
print(self.reportadoptionnotice.adoption_notice.get_absolute_url)
return self.reportadoptionnotice.adoption_notice.get_absolute_url
elif hasattr(self, "reportcomment"):
return self.reportcomment.reported_comment.get_absolute_url
return None
class ReportAdoptionNotice(Report):
adoption_notice = models.ForeignKey("AdoptionNotice", on_delete=models.CASCADE)
@@ -712,9 +674,6 @@ class ReportAdoptionNotice(Report):
def reported_content(self):
return self.adoption_notice
def __str__(self):
return f"Report der Vermittlung {self.adoption_notice}"
class ReportComment(Report):
reported_comment = models.ForeignKey("Comment", on_delete=models.CASCADE)

View File

@@ -1,47 +0,0 @@
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
from .models import AdoptionNotice, RescueOrganization
class StaticViewSitemap(Sitemap):
priority = 0.8
changefreq = "weekly"
def items(self):
return ["index", "search", "map", "about", "rescue-organizations"]
def location(self, item):
return reverse(item)
class AdoptionNoticeSitemap(Sitemap):
priority = 0.5
changefreq = "daily"
def items(self):
return AdoptionNotice.get_active_ANs()
def lastmod(self, obj):
return obj.updated_at
class AnimalSitemap(Sitemap):
priority = 0.2
changefreq = "daily"
def items(self):
return AdoptionNotice.objects.all()
def lastmod(self, obj):
return obj.updated_at
class RescueOrganizationSitemap(Sitemap):
priority = 0.3
changefreq = "weekly"
def items(self):
return RescueOrganization.objects.all()
def lastmod(self, obj):
return obj.updated_at

View File

@@ -1,7 +0,0 @@
User-agent: *
Disallow: /admin/
User-agent: OpenAI
Disallow: /
Sitemap: https://notfellchen.org/sitemap.xml

View File

@@ -1,21 +0,0 @@
{% 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

@@ -1,7 +1,9 @@
{% load i18n %}
<div class="report card">
<h2>
{% translate 'Meldung von ' %} <a href="{{ report.reported_content_url }}"><i>{{ report.reported_content }}</i></a>
{% blocktranslate %}
Meldung von {{ report.reported_content }}
{% endblocktranslate %}
</h2>
{% if report.reported_broken_rules %}
{% translate "Regeln gegen die Verstoßen wurde" %}
@@ -11,25 +13,19 @@
{% endfor %}
</ul>
{% endif %}
<p>
{% if report.user_comment %}
<b>{% translate "Kommentar zur Meldung" %}:</b> {{ report.user_comment }}
{% else %}
<i>{% translate 'Es wurde kein Kommentar zur Meldung hinzugefügt.' %}</i>
{% endif %}
<p><b>{% translate "Kommentar zur Meldung" %}:</b>
{{ report.user_comment }}
</p>
{% if is_mod_or_above %}
<div class="container-edit-buttons">
<form action="allow" class="">
{% csrf_token %}
<input type="hidden" name="report_id" value="{{ report.pk }}">
<button class="btn allow" type="submit">{% translate "Inhalt genehmigen" %}</button>
</form>
<form action="disallow" class="">
{% csrf_token %}
<input type="hidden" name="report_id" value="{{ report.pk }}">
<button class="btn allow" type="submit">{% translate "Inhalt als gesperrt kennzeichnen" %}</button>
</form>
</div>
{% endif %}
<div class="container-edit-buttons">
<form action="allow" class="">
{% csrf_token %}
<input type="hidden" name="report_id" value="{{ report.pk }}">
<button class="btn allow" type="submit">{% translate "Inhalt genehmigen" %}</button>
</form>
<form action="disallow" class="">
{% csrf_token %}
<input type="hidden" name="report_id" value="{{ report.pk }}">
<button class="btn allow" type="submit">{% translate "Inhalt als gesperrt kennzeichnen" %}</button>
</form>
</div>
</div>

View File

@@ -1,12 +0,0 @@
{% 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

@@ -1,102 +0,0 @@
{% extends "fellchensammlung/base_generic.html" %}
{% load i18n %}
{% load static %}
{% block title %}<title>{% translate "Styleguide" %}</title>{% endblock %}
{% block content %}
<h1>This is a heading</h1>
<p>And this is a short paragraph below</p>
<div class="container-cards">
<h2>Card Containers</h2>
<div class="card">
<h3>I am a card</h3>
<p>Cards are responsive. Use them to display multiple items of the same category</p>
</div>
<div class="card">
<h3>Photos</h3>
<p>Cards are responsive. Use them to display multiple items of the same category</p>
<img src="{% static 'fellchensammlung/img/example_rat_single.png' %}" alt="A rat sitting on a wooden house">
</div>
</div>
<div class="container-cards">
<form class="form-search card half" method="post">
<label for="inputA">Input Alpha</label>
<input name="inputA" maxlength="200" id="inputA">
<label for="inputB">Beta</label>
<input name="inputB" maxlength="200" id="inputB">
<label for="id_location_string">Ort</label>
<input name="location_string" id="id_location_string">
<ul id="results"></ul>
<div class="container-edit-buttons">
<button class="btn" type="submit" value="search" name="search">
<i class="fas fa-search"></i> {% trans 'Suchen' %}
</button>
{% if searched %}
{% if subscribed_search %}
<button class="btn" type="submit" value="{{ subscribed_search.pk }}"
name="unsubscribe_to_search">
<i class="fas fa-bell-slash"></i> {% trans 'Suche nicht mehr abonnieren' %}
</button>
{% else %}
<button class="btn" type="submit" name="subscribe_to_search">
<i class="fas fa-bell"></i> {% trans 'Suche abonnieren' %}
</button>
{% endif %}
{% endif %}
</div>
{% if place_not_found %}
<p class="error">
{% trans 'Ort nicht gefunden' %}
</p>
{% endif %}
</form>
<div class="card half">
{% include "fellchensammlung/partials/partial-map.html" %}
</div>
</div>
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
<script>
const locationInput = document.getElementById('id_location_string');
const resultsList = document.getElementById('results');
const placeIdInput = document.getElementById('place_id');
locationInput.addEventListener('input', async function () {
const query = locationInput.value.trim();
if (query.length < 3) {
resultsList.innerHTML = ''; // Don't search for or show results if input is less than 3 characters
return;
}
try {
const response = await fetch(`{{ geocoding_api_url }}/?q=${encodeURIComponent(query)}&limit=5&lang=de`);
const data = await response.json();
if (data && data.features) {
resultsList.innerHTML = ''; // Clear previous results
const locations = data.features.slice(0, 5); // Show only the first 5 results
locations.forEach(location => {
const listItem = document.createElement('li');
listItem.classList.add('result-item');
listItem.textContent = geojson_to_summary(location);
// Add event when user clicks on a result location
listItem.addEventListener('click', () => {
locationInput.value = geojson_to_searchable_string(location); // Set input field to selected location
resultsList.innerHTML = ''; // Clear the results after selecting a location
});
resultsList.appendChild(listItem);
});
}
} catch (error) {
console.error('Error fetching location data:', error);
resultsList.innerHTML = '<li class="result-item">Error fetching data. Please try again.</li>';
}
});
</script>
{% endblock %}

View File

@@ -7,15 +7,6 @@ from .feeds import LatestAdoptionNoticesFeed
from . import views
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
from django.contrib.sitemaps.views import sitemap
from .sitemap import StaticViewSitemap, AdoptionNoticeSitemap, AnimalSitemap
sitemaps = {
"static": StaticViewSitemap,
"vermittlungen": AdoptionNoticeSitemap,
"tiere": AnimalSitemap,
}
urlpatterns = [
path("", views.index, name="index"),
path("rss/", LatestAdoptionNoticesFeed(), name="rss"),
@@ -31,11 +22,9 @@ 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,
@@ -59,11 +48,9 @@ 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 ##
###########
@@ -108,10 +95,4 @@ urlpatterns = [
###################
path('external-site/', views.external_site_warning, name="external-site"),
###############
## TECHNICAL ##
###############
path("sitemap.xml", sitemap, {"sitemaps": sitemaps}, name="django.contrib.sitemaps.views.sitemap"),
path("styleguide", views.styleguide, name="styleguide"),
]

View File

@@ -2,7 +2,6 @@ import logging
from django.contrib.auth.views import redirect_to_login
from django.http import HttpResponseRedirect, JsonResponse, HttpResponse
from django.http.response import HttpResponseForbidden
from django.shortcuts import render, redirect
from django.urls import reverse
from django.contrib.auth.decorators import login_required
@@ -134,7 +133,7 @@ def adoption_notice_detail(request, adoption_notice_id):
elif action == "subscribe":
return redirect_to_login(next=request.path)
else:
return HttpResponseForbidden()
raise PermissionDenied
else:
comment_form = CommentForm(instance=adoption_notice)
context = {"adoption_notice": adoption_notice, "comment_form": comment_form, "user": request.user,
@@ -429,13 +428,10 @@ def report_detail(request, report_id, form_complete=False):
"""
Detailed view of a report, including moderation actions
"""
# Prefetching reduces the number of queries to the database that are needed (see reported_content)
report = Report.objects.select_related("reportadoptionnotice", "reportcomment").get(pk=report_id)
report = Report.objects.get(pk=report_id)
moderation_actions = ModerationAction.objects.filter(report_id=report_id)
is_mod_or_above = user_is_trust_level_or_above(request.user, TrustLevel.MODERATOR)
context = {"report": report, "moderation_actions": moderation_actions,
"form_complete": form_complete, "is_mod_or_above": is_mod_or_above}
context = {"report": report, "moderation_actions": moderation_actions, "form_complete": form_complete}
return render(request, 'fellchensammlung/details/detail-report.html', context)
@@ -506,7 +502,7 @@ def my_profile(request):
@user_passes_test(user_is_trust_level_or_above)
def modqueue(request):
open_reports = Report.objects.select_related("reportadoptionnotice", "reportcomment").filter(status=Report.WAITING)
open_reports = Report.objects.filter(status=Report.WAITING)
context = {"reports": open_reports}
return render(request, 'fellchensammlung/modqueue.html', context=context)
@@ -636,24 +632,3 @@ def export_own_profile(request):
ANs_as_json = serialize('json', ANs)
full_json = f"{user_as_json}, {ANs_as_json}"
return HttpResponse(full_json, content_type="application/json")
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://photon.hyteck.de/api")
GEOCODING_API_URL = config.get("geocoding", "api_url", fallback="https://nominatim.hyteck.de/search")
# GEOCODING_API_FORMAT is allowed to be one of ['nominatim', 'photon']
GEOCODING_API_FORMAT = config.get("geocoding", "api_format", fallback="photon")
GEOCODING_API_FORMAT = config.get("geocoding", "api_format", fallback="nominatim")
""" Tile Server """
MAP_TILE_SERVER = config.get("map", "tile_server", fallback="https://tiles.hyteck.de")
@@ -168,7 +168,6 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
"django.contrib.sitemaps",
'fontawesomefree',
'crispy_forms',
"crispy_bootstrap4",

View File

@@ -5,8 +5,7 @@ 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, SearchSubscription
from fellchensammlung.tools.geo import LocationProxy
Animal, Subscriptions
from fellchensammlung.views import add_adoption_notice
@@ -147,35 +146,6 @@ 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)
@@ -263,10 +233,10 @@ class AdoptionDetailTest(TestCase):
password='12345')
test_user0.save()
cls.test_user1 = User.objects.create_user(username='testuser1',
first_name="Max",
last_name="Müller",
password='12345')
test_user1 = User.objects.create_user(username='testuser1',
first_name="Max",
last_name="Müller",
password='12345')
test_user0.trust_level = TrustLevel.ADMIN
test_user0.save()
@@ -286,16 +256,6 @@ class AdoptionDetailTest(TestCase):
adoption3.set_active()
adoption2.set_unchecked()
def test_basic_view(self):
response = self.client.get(
reverse('adoption-notice-detail', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)), )
self.assertEqual(response.status_code, 200)
def test_basic_view_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(
reverse('adoption-notice-detail', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)), )
self.assertEqual(response.status_code, 200)
def test_subscribe(self):
self.client.login(username='testuser0', password='12345')
@@ -304,6 +264,7 @@ class AdoptionDetailTest(TestCase):
data={"action": "subscribe"})
self.assertTrue(Subscriptions.objects.filter(owner__username="testuser0").exists())
def test_unsubscribe(self):
# Make sure subscription exists
an = AdoptionNotice.objects.get(name="TestAdoption1")
@@ -323,73 +284,3 @@ class AdoptionDetailTest(TestCase):
data={"action": "subscribe"})
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/accounts/login/?next=/vermittlung/1/")
def test_unauthenticated_comment(self):
response = self.client.post(
reverse('adoption-notice-detail', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)),
data={"action": "comment"})
self.assertEqual(response.status_code, 403)
def test_comment(self):
an1 = AdoptionNotice.objects.get(name="TestAdoption1")
# Set up subscription
Subscriptions.objects.create(owner=self.test_user1, adoption_notice=an1)
self.client.login(username='testuser0', password='12345')
response = self.client.post(
reverse('adoption-notice-detail', args=str(an1.pk)),
data={"action": "comment", "text": "Test"})
self.assertTrue(Comment.objects.filter(user__username="testuser0").exists())
self.assertFalse(CommentNotification.objects.filter(user__username="testuser0").exists())
self.assertTrue(CommentNotification.objects.filter(user__username="testuser1").exists())
class AdoptionEditTest(TestCase):
@classmethod
def setUpTestData(cls):
test_user0 = User.objects.create_user(username='testuser0',
first_name="Admin",
last_name="BOFH",
password='12345')
test_user0.save()
cls.test_user1 = User.objects.create_user(username='testuser1',
first_name="Max",
last_name="Müller",
password='12345')
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1", description="Test1", owner=test_user0)
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2", description="Test2")
def test_basic_view(self):
response = self.client.get(
reverse('adoption-notice-edit', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)), )
self.assertEqual(response.status_code, 302)
def test_basic_view_logged_in_unauthorized(self):
self.client.login(username='testuser1', password='12345')
response = self.client.get(
reverse('adoption-notice-edit', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)), )
self.assertEqual(response.status_code, 403)
def test_basic_view_logged_in(self):
self.client.login(username='testuser0', password='12345')
response = self.client.get(
reverse('adoption-notice-edit', args=str(AdoptionNotice.objects.get(name="TestAdoption1").pk)), )
self.assertEqual(response.status_code, 200)
def test_edit(self):
data = {"name": "Mia",
"searching_since": "01.01.2025",
"location_string": "Paderborn",
"organization": "",
"description": "Test3",
"further_information": ""}
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.status_code, 200) # Redirects to AN page
self.assertContains(response, "Test3")
self.assertContains(response, "Mia")
self.assertNotContains(response, "Test1")

View File

@@ -1,8 +1,7 @@
from django.test import TestCase
from django.urls import reverse
from docs.conf import language
from fellchensammlung.models import User, TrustLevel, AdoptionNotice, Species, Rule, Language, Comment, ReportComment
from fellchensammlung.models import User, TrustLevel, AdoptionNotice, Species
from model_bakery import baker
@@ -27,19 +26,6 @@ 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')
@@ -55,82 +41,3 @@ 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='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.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='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.assertContains(response, '<form action="allow" class="">')