Compare commits
3 Commits
37ecf28f2f
...
ci-test-co
Author | SHA1 | Date | |
---|---|---|---|
c9f46d7547 | |||
9f23f5768c | |||
19210f90cd |
@@ -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
14
.woodpecker/test.yml
Normal 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]
|
||||
|
||||
|
@@ -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()
|
@@ -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]
|
||||
|
||||
|
@@ -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,
|
||||
),
|
||||
]
|
@@ -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'),
|
||||
),
|
||||
]
|
@@ -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)
|
||||
|
@@ -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
|
@@ -1,7 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow: /admin/
|
||||
|
||||
User-agent: OpenAI
|
||||
Disallow: /
|
||||
|
||||
Sitemap: https://notfellchen.org/sitemap.xml
|
@@ -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>
|
@@ -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>
|
@@ -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 %}
|
@@ -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 %}
|
@@ -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"),
|
||||
|
||||
]
|
||||
|
@@ -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)
|
||||
|
@@ -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",
|
||||
|
@@ -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")
|
||||
|
@@ -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="">')
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user