Compare commits
3 Commits
712c3d32f3
...
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,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,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"),
|
||||
@@ -104,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,
|
||||
@@ -633,9 +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)
|
||||
|
@@ -168,7 +168,6 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
"django.contrib.sitemaps",
|
||||
'fontawesomefree',
|
||||
'crispy_forms',
|
||||
"crispy_bootstrap4",
|
||||
|
@@ -5,7 +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
|
||||
Animal, Subscriptions
|
||||
from fellchensammlung.views import add_adoption_notice
|
||||
|
||||
|
||||
@@ -233,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()
|
||||
|
||||
@@ -256,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')
|
||||
@@ -274,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")
|
||||
@@ -293,72 +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")
|
||||
|
Reference in New Issue
Block a user