Compare commits

28 Commits

Author SHA1 Message Date
9ae64e8cb1 feat: Auto add now the date oflast checked
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2025-03-09 17:46:13 +01:00
1b5a0c71e0 feat: Add rescue organization check 2025-03-09 09:46:56 +01:00
4d4f11c479 feat: Make string translatable 2025-03-09 09:16:37 +01:00
835c89d1d4 test: Add test for details of comment reports 2025-02-05 22:32:18 +01:00
46bf07dd8d test: Add test for reporting comments anon and logged in 2025-02-05 22:11:59 +01:00
f557672586 test: Add test for reporting rules anon 2025-02-05 20:17:36 +01:00
4e27e1be7f test: Add test for about page and for reporting rules logged in 2025-01-27 18:51:14 +01:00
6d390ad21e test: Add test for (un)subscribes of searches 2025-01-26 20:19:18 +01:00
2f2543160e test: Add test for unauthenticated (un) subscribes of searches 2025-01-26 19:01:39 +01:00
64a9db133e feat: default to photon for geocoding 2025-01-26 18:16:39 +01:00
712c3d32f3 feat: Add styleguide setup 2025-01-26 18:16:39 +01:00
8998bbdf6d Merge pull request #9 from moan0s/shelter-fixes
Add script to upload data of animal shelters
2025-01-26 18:04:48 +01:00
ff31caa139 refactor: Move script to dedicated folder 2025-01-26 18:01:38 +01:00
ad06829c31 feat: Add debug information 2025-01-26 18:00:23 +01:00
03a48da355 feat: Make instance and secrets CLI argument 2025-01-26 17:59:48 +01:00
885bed888d feat: Raise connection error upon unexpected error
This e.g. makes sure the API is not bombarded with unauthorized calls if the token is wrong
2025-01-26 17:07:50 +01:00
0051cb07c9 refactor: formatting 2025-01-26 17:06:41 +01:00
8858cff9cf fix: Construct necessary location string
I'd be better to directly create a location here but I for now want to make as little modifications as possible
2025-01-26 17:05:51 +01:00
70e2af6172 fix: fix key 2025-01-26 17:04:56 +01:00
461abd2e46 fix: don't try to save owner 2025-01-26 17:04:17 +01:00
Salil
d7269106db Added - animal_shelter Get Data
Import all German animal shelters
2025-01-24 12:22:28 +05:30
77fb99a527 Merge pull request #6 from Deadpool2000/patch-1
Create robots.txt
2025-01-22 20:28:39 +01:00
38a56daa24 feat: Add sitemap 2025-01-22 11:01:31 +01:00
Salil
ac0749797f Create robots.txt
- robots.txt file added in src/fellchensammlung/static/
2025-01-22 10:03:30 +05:30
f193f7d7ca test: Add tests for AN edit 2025-01-19 23:11:07 +01:00
43657e0862 test: Add tests for comment 2025-01-19 22:07:21 +01:00
68ad366f74 test: Add tests for comment 2025-01-19 09:00:53 +01:00
350d2c5da9 feat: Return HTTP response 2025-01-19 09:00:26 +01:00
15 changed files with 600 additions and 16 deletions

View File

@@ -0,0 +1,97 @@
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,6 +16,7 @@ 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]
@@ -84,7 +85,6 @@ class AdoptionNoticeApiView(APIView):
)
class AnimalApiView(APIView):
permission_classes = [IsAuthenticated]
@@ -118,6 +118,7 @@ class AnimalApiView(APIView):
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class RescueOrganizationApiView(APIView):
permission_classes = [IsAuthenticated]
@@ -159,13 +160,14 @@ class RescueOrganizationApiView(APIView):
"""
serializer = RescueOrgSerializer(data=request.data, context={"request": request})
if serializer.is_valid():
rescue_org = serializer.save(owner=request.user)
rescue_org = serializer.save()
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

@@ -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,47 @@
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

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

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

@@ -0,0 +1,102 @@
{% 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,6 +7,15 @@ 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"),
@@ -22,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,
@@ -48,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 ##
###########
@@ -95,4 +108,10 @@ 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,6 +2,7 @@ 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
@@ -133,7 +134,7 @@ def adoption_notice_detail(request, adoption_notice_id):
elif action == "subscribe":
return redirect_to_login(next=request.path)
else:
raise PermissionDenied
return HttpResponseForbidden()
else:
comment_form = CommentForm(instance=adoption_notice)
context = {"adoption_notice": adoption_notice, "comment_form": comment_form, "user": request.user,
@@ -632,3 +633,24 @@ 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://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")
@@ -168,6 +168,7 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
"django.contrib.sitemaps",
'fontawesomefree',
'crispy_forms',
"crispy_bootstrap4",

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
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)
@@ -233,10 +263,10 @@ class AdoptionDetailTest(TestCase):
password='12345')
test_user0.save()
test_user1 = User.objects.create_user(username='testuser1',
first_name="Max",
last_name="Müller",
password='12345')
cls.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()
@@ -256,6 +286,16 @@ 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')
@@ -264,7 +304,6 @@ 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")
@@ -284,3 +323,73 @@ 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,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="">')