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
80 changed files with 1010 additions and 5987 deletions

2
.gitignore vendored
View File

@@ -4,7 +4,7 @@
notfellchen
# Media storage
/static
static
media

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,105 +0,0 @@
import argparse
import json
import os
import requests
from tqdm import tqdm
DEFAULT_OSM_DATA_FILE = "export.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 get_or_none(data, key):
if key in data["properties"].keys():
return data["properties"][key]
else:
return ""
def choose(keys, data, replace=False):
for key in keys:
if key in data.keys():
if replace:
return data[key].replace(" ", "").replace("-", "").replace("(", "").replace(")", "")
else:
return data[key]
return None
def add(value, platform):
if value != "":
if value.find(platform) == -1:
return f"https://www.{platform}.com/{value}"
else:
return value
else:
return None
def https(value):
if value is not None and value != "":
value = value.replace("http://", "")
if value.find("https") == -1:
return f"https://{value}"
else:
return value
else:
return None
def main():
api_token, instance, data_file = get_config()
# Set headers and endpoint
endpoint = f"{instance}/api/organizations/"
h = {'Authorization': f'Token {api_token}', "content-type": "application/json"}
with open(data_file, encoding="utf8") as f:
d = json.load(f)
for idx, tierheim in tqdm(enumerate(d["features"])):
if "name" not in tierheim["properties"].keys() or "addr:city" not in tierheim["properties"].keys():
continue
data = {"name": tierheim["properties"]["name"],
"location_string": f"{get_or_none(tierheim, "addr:street")} {get_or_none(tierheim, "addr:housenumber")}, {get_or_none(tierheim, "addr:postcode")} {tierheim["properties"]["addr:city"]}",
"phone_number": choose(("contact:phone", "phone"), tierheim["properties"], replace=True),
"fediverse_profile": get_or_none(tierheim, "contact:mastodon"),
"facebook": https(add(get_or_none(tierheim, "contact:facebook"), "facebook")),
"instagram": https(add(get_or_none(tierheim, "contact:instagram"), "instagram")),
"website": https(choose(("contact:website", "website"), tierheim["properties"])),
"email": choose(("contact:email", "email"), tierheim["properties"]),
"description": get_or_none(tierheim, "opening_hours"),
"external_object_identifier": f"{tierheim["id"]}",
"external_source_identifier": "OSM"
}
result = requests.post(endpoint, json=data, headers=h)
if result.status_code != 201:
print(f"{idx} {tierheim["properties"]["name"]}:{result.status_code} {result.json()}")
if __name__ == "__main__":
main()

View File

@@ -7,7 +7,7 @@ from django.urls import reverse
from django.utils.http import urlencode
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \
SpeciesSpecificURL, ImportantLocation
SpeciesSpecificURL
from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, BaseNotification
@@ -94,14 +94,12 @@ class ReportAdoptionNoticeAdmin(admin.ModelAdmin):
reported_content_link.short_description = "Reported Content"
class SpeciesSpecificURLInline(admin.StackedInline):
model = SpeciesSpecificURL
@admin.register(RescueOrganization)
class RescueOrganizationAdmin(admin.ModelAdmin):
search_fields = ("name", "description", "internal_comment", "location_string")
search_fields = ("name","description", "internal_comment", "location_string")
list_display = ("name", "trusted", "allows_using_materials", "website")
list_filter = ("allows_using_materials", "trusted",)
@@ -124,46 +122,14 @@ class CommentAdmin(admin.ModelAdmin):
class BaseNotificationAdmin(admin.ModelAdmin):
list_filter = ("user", "read")
@admin.register(SearchSubscription)
class SearchSubscriptionAdmin(admin.ModelAdmin):
list_filter = ("owner",)
class ImportantLocationInline(admin.StackedInline):
model = ImportantLocation
class IsImportantListFilter(admin.SimpleListFilter):
# See https://docs.djangoproject.com/en/5.1/ref/contrib/admin/filters/#modeladmin-list-filters
title = _('Is Important Location?')
parameter_name = 'important'
def lookups(self, request, model_admin):
return (
('is_important', _('Important Location')),
('is_normal', _('Normal Location')),
)
def queryset(self, request, queryset):
if self.value() == 'is_important':
return queryset.filter(importantlocation__isnull=False)
else:
return queryset.filter(importantlocation__isnull=True)
@admin.register(Location)
class LocationAdmin(admin.ModelAdmin):
search_fields = ("name__icontains", "city__icontains")
list_filter = [IsImportantListFilter]
inlines = [
ImportantLocationInline,
]
admin.site.register(Animal)
admin.site.register(Species)
admin.site.register(Location)
admin.site.register(Rule)
admin.site.register(Image)
admin.site.register(ModerationAction)

View File

@@ -1,35 +1,12 @@
from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image, Location
from ..models import Animal, RescueOrganization, AdoptionNotice, Species, Image
from rest_framework import serializers
class AdoptionNoticeSerializer(serializers.HyperlinkedModelSerializer):
location = serializers.PrimaryKeyRelatedField(
queryset=Location.objects.all(),
required=False,
allow_null=True
)
location_details = serializers.StringRelatedField(source='location', read_only=True)
organization = serializers.PrimaryKeyRelatedField(
queryset=RescueOrganization.objects.all(),
required=False,
allow_null=True
)
organization = serializers.PrimaryKeyRelatedField(
queryset=RescueOrganization.objects.all(),
required=False,
allow_null=True
)
photos = serializers.PrimaryKeyRelatedField(
queryset=Image.objects.all(),
many=True,
required=False
)
class Meta:
model = AdoptionNotice
fields = ['created_at', 'last_checked', "searching_since", "name", "description", "further_information",
"group_only", "location", "location_details", "organization", "photos"]
"group_only"]
class AnimalCreateSerializer(serializers.ModelSerializer):
@@ -37,14 +14,12 @@ class AnimalCreateSerializer(serializers.ModelSerializer):
model = Animal
fields = ["name", "date_of_birth", "description", "species", "sex", "adoption_notice"]
class RescueOrgSerializer(serializers.ModelSerializer):
class Meta:
model = RescueOrganization
fields = ["name", "location_string", "instagram", "facebook", "fediverse_profile", "email", "phone_number",
"website", "description", "external_object_identifier", "external_source_identifier"]
class AnimalGetSerializer(serializers.ModelSerializer):
class Meta:
model = Animal
@@ -76,9 +51,3 @@ class SpeciesSerializer(serializers.ModelSerializer):
class Meta:
model = Species
fields = "__all__"
class LocationSerializer(serializers.ModelSerializer):
class Meta:
model = Location
fields = "__all__"

View File

@@ -1,7 +1,7 @@
from django.urls import path
from .views import (
AdoptionNoticeApiView,
AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView, LocationApiView
AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView
)
urlpatterns = [
@@ -13,5 +13,4 @@ urlpatterns = [
path("organizations/<int:id>/", RescueOrganizationApiView.as_view(), name="api-organization-detail"),
path("images/", AddImageApiView.as_view(), name="api-add-image"),
path("species/", SpeciesApiView.as_view(), name="api-species-list"),
path("locations/", LocationApiView.as_view(), name="api-locations-list"),
]

View File

@@ -1,11 +1,8 @@
from django.db.models import Q
from fellchensammlung.api.serializers import LocationSerializer
from rest_framework.views import APIView
from rest_framework.response import Response
from django.db import transaction
from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel, Location
from fellchensammlung.tasks import post_adoption_notice_save, post_rescue_org_save
from fellchensammlung.models import AdoptionNotice, Animal, Log, TrustLevel
from fellchensammlung.tasks import post_adoption_notice_save
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from .serializers import (
@@ -19,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]
@@ -88,6 +84,7 @@ class AdoptionNoticeApiView(APIView):
)
class AnimalApiView(APIView):
permission_classes = [IsAuthenticated]
@@ -121,7 +118,6 @@ class AnimalApiView(APIView):
)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class RescueOrganizationApiView(APIView):
permission_classes = [IsAuthenticated]
@@ -133,44 +129,14 @@ class RescueOrganizationApiView(APIView):
'description': 'ID of the rescue organization to retrieve.',
'type': int
},
{
'name': 'trusted',
'required': False,
'description': 'Filter by trusted status (true/false).',
'type': bool
},
{
'name': 'external_object_identifier',
'required': False,
'description': 'Filter by external object identifier. Use "None" to filter for an empty field',
'type': str
},
{
'name': 'external_source_identifier',
'required': False,
'description': 'Filter by external source identifier. Use "None" to filter for an empty field',
'type': str
},
{
'name': 'search',
'required': False,
'description': 'Search by organization name or location name/city.',
'type': str
},
],
responses={200: RescueOrganizationSerializer(many=True)}
)
def get(self, request, *args, **kwargs):
"""
Get list of rescue organizations or a specific organization by ID or get a list with available filters for
- external_object_identifier
- external_source_identifier
Get list of rescue organizations or a specific organization by ID.
"""
org_id = request.query_params.get("id")
external_object_identifier = request.query_params.get("external_object_identifier")
external_source_identifier = request.query_params.get("external_source_identifier")
search_query = request.query_params.get("search")
org_id = kwargs.get("id")
if org_id:
try:
organization = RescueOrganization.objects.get(pk=org_id)
@@ -178,33 +144,14 @@ class RescueOrganizationApiView(APIView):
return Response(serializer.data, status=status.HTTP_200_OK)
except RescueOrganization.DoesNotExist:
return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
organizations = RescueOrganization.objects.all()
if external_object_identifier:
if external_object_identifier == "None":
external_object_identifier = None
organizations = organizations.filter(external_object_identifier=external_object_identifier)
if external_source_identifier:
if external_source_identifier == "None":
external_source_identifier = None
organizations = organizations.filter(external_source_identifier=external_source_identifier)
if search_query:
organizations = organizations.filter(
Q(name__icontains=search_query) |
Q(location_string__icontains=search_query) |
Q(location__name__icontains=search_query) |
Q(location__city__icontains=search_query)
)
serializer = RescueOrganizationSerializer(organizations, many=True, context={"request": request})
return Response(serializer.data, status=status.HTTP_200_OK)
@transaction.atomic
@extend_schema(
request=RescueOrgSerializer,
responses={201: 'Rescue organization created successfully!'}
request=RescueOrgSerializer, # Document the request body
responses={201: 'Rescue organization created/updated successfully!'}
)
def post(self, request, *args, **kwargs):
"""
@@ -212,39 +159,11 @@ class RescueOrganizationApiView(APIView):
"""
serializer = RescueOrgSerializer(data=request.data, context={"request": request})
if serializer.is_valid():
rescue_org = serializer.save()
# Add the location
post_rescue_org_save.delay_on_commit(rescue_org.pk)
rescue_org = serializer.save(owner=request.user)
return Response(
{"message": "Rescue organization created successfully!", "id": rescue_org.id},
{"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)
@transaction.atomic
@extend_schema(
request=RescueOrgSerializer,
responses={200: 'Rescue organization updated successfully!'}
)
def patch(self, request, *args, **kwargs):
"""
Partially update a rescue organization.
"""
org_id = kwargs.get("id")
if not org_id:
return Response({"error": "ID is required for updating."}, status=status.HTTP_400_BAD_REQUEST)
try:
organization = RescueOrganization.objects.get(pk=org_id)
except RescueOrganization.DoesNotExist:
return Response({"error": "Organization not found."}, status=status.HTTP_404_NOT_FOUND)
serializer = RescueOrgSerializer(organization, data=request.data, partial=True, context={"request": request})
if serializer.is_valid():
serializer.save()
return Response({"message": "Rescue organization updated successfully!"}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class AddImageApiView(APIView):
@@ -291,63 +210,3 @@ class SpeciesApiView(APIView):
species = Species.objects.all()
serializer = SpeciesSerializer(species, many=True, context={"request": request})
return Response(serializer.data, status=status.HTTP_200_OK)
class LocationApiView(APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
parameters=[
{
'name': 'id',
'required': False,
'description': 'ID of the location to retrieve.',
'type': int
},
],
responses={200: LocationSerializer(many=True)}
)
def get(self, request, *args, **kwargs):
"""
Retrieve a location
"""
location_id = kwargs.get("id")
if location_id:
try:
location = Location.objects.get(pk=location_id)
serializer = LocationSerializer(location, context={"request": request})
return Response(serializer.data, status=status.HTTP_200_OK)
except Location.DoesNotExist:
return Response({"error": "Location not found."}, status=status.HTTP_404_NOT_FOUND)
locations = Location.objects.all()
serializer = LocationSerializer(locations, many=True, context={"request": request})
return Response(serializer.data, status=status.HTTP_200_OK)
@transaction.atomic
@extend_schema(
request=LocationSerializer,
responses={201: 'Location created successfully!'}
)
def post(self, request, *args, **kwargs):
"""
API view to add a location
"""
serializer = LocationSerializer(data=request.data, context={'request': request})
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
location = serializer.save()
# Log the action
Log.objects.create(
user=request.user,
action="add_location",
text=f"{request.user} added adoption notice {location.pk} via API",
)
# Return success response with new adoption notice details
return Response(
{"message": "Location created successfully!", "id": location.pk},
status=status.HTTP_201_CREATED,
)

View File

@@ -22,15 +22,6 @@ class DateInput(forms.DateInput):
input_type = 'date'
class BulmaAdoptionNoticeForm(forms.ModelForm):
template_name = "fellchensammlung/forms/form_snippets.html"
class Meta:
model = AdoptionNotice
fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string",
"organization"]
class AdoptionNoticeForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
if 'in_adoption_notice_creation_flow' in kwargs:
@@ -136,9 +127,8 @@ class ImageForm(forms.ModelForm):
self.helper.form_method = 'post'
if in_flow:
submits = Div(Submit('submit', _('Speichern')),
Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')),
css_class="container-edit-buttons")
submits= Div(Submit('submit', _('Speichern')),
Submit('save-and-add-another', _('Speichern und weiteres Foto hinzufügen')), css_class="container-edit-buttons")
else:
submits = Fieldset(Submit('submit', _('Speichern')), css_class="container-edit-buttons")
self.helper.layout = Layout(
@@ -150,6 +140,7 @@ class ImageForm(forms.ModelForm):
submits
)
class Meta:
model = Image
fields = ('image', 'alt_text')
@@ -173,7 +164,7 @@ class CommentForm(forms.ModelForm):
self.helper = FormHelper()
self.helper.form_class = 'form-comments'
self.helper.add_input(Hidden('action', 'comment'))
self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="button is-primary"))
self.helper.add_input(Submit('submit', _('Kommentieren'), css_class="btn2"))
class Meta:
model = Comment
@@ -190,8 +181,7 @@ class CustomRegistrationForm(RegistrationForm):
class Meta(RegistrationForm.Meta):
model = User
captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_(
"Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um sicherzustellen, dass du kein Roboter bist."))
captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_("Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um sicherzustellen, dass du kein Roboter bist."))
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -203,10 +193,7 @@ class CustomRegistrationForm(RegistrationForm):
class AdoptionNoticeSearchForm(forms.Form):
template_name = "fellchensammlung/forms/form_snippets.html"
sex = forms.ChoiceField(choices=SexChoicesWithAll, label=_("Geschlecht"), required=False,
initial=SexChoicesWithAll.ALL)
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED,
label=_("Suchradius"))
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED, label=_("Suchradius"))
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)

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

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2025-03-20 23:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0039_alter_rescueorganization_last_checked'),
]
operations = [
migrations.AlterField(
model_name='rescueorganization',
name='allows_using_materials',
field=models.CharField(choices=[('allowed', 'Usage allowed'), ('requested', 'Usage requested'), ('denied', 'Usage denied'), ('other', "It's complicated"), ('not_asked', 'Not asked')], default='not_asked', max_length=200, verbose_name='Erlaubt Nutzung von Inhalten'),
),
]

View File

@@ -1,42 +0,0 @@
# Generated by Django 5.1.4 on 2025-04-06 06:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0040_alter_rescueorganization_allows_using_materials'),
]
operations = [
migrations.AddField(
model_name='location',
name='city',
field=models.CharField(blank=True, max_length=200, null=True),
),
migrations.AddField(
model_name='location',
name='country',
field=models.CharField(blank=True, help_text='Standardisierter Ländercode nach ISO 3166-1 ALPHA-2', max_length=2, null=True, verbose_name='Ländercode'),
),
migrations.AddField(
model_name='location',
name='housenumber',
field=models.CharField(blank=True, max_length=20, null=True),
),
migrations.AddField(
model_name='location',
name='postcode',
field=models.CharField(blank=True, max_length=20, null=True),
),
migrations.AddField(
model_name='location',
name='street',
field=models.CharField(blank=True, max_length=200, null=True),
),
migrations.AlterUniqueTogether(
name='rescueorganization',
unique_together={('external_object_identifier', 'external_source_identifier')},
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2025-04-24 17:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0041_location_city_location_country_location_housenumber_and_more'),
]
operations = [
migrations.AddField(
model_name='location',
name='county',
field=models.CharField(blank=True, max_length=200, null=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2025-04-24 17:41
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0042_location_county'),
]
operations = [
migrations.RenameField(
model_name='location',
old_name='country',
new_name='countrycode',
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.4 on 2025-04-26 22:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0043_rename_country_location_countrycode'),
]
operations = [
migrations.AlterField(
model_name='location',
name='place_id',
field=models.CharField(max_length=200),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 5.1.4 on 2025-04-27 11:31
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0044_alter_location_place_id'),
]
operations = [
migrations.CreateModel(
name='ImportantLocation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.SlugField(unique=True)),
('name', models.CharField(max_length=200)),
('location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.location')),
],
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 5.1.4 on 2025-04-27 11:51
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0045_importantlocation'),
]
operations = [
migrations.AlterField(
model_name='importantlocation',
name='location',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='fellchensammlung.location'),
),
]

View File

@@ -3,7 +3,6 @@ from random import choices
from tabnanny import verbose
from django.db import models
from django.template.defaultfilters import slugify
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
@@ -40,28 +39,15 @@ class Language(models.Model):
class Location(models.Model):
place_id = models.CharField(max_length=200) # OSM id
place_id = models.IntegerField() # OSM id
latitude = models.FloatField()
longitude = models.FloatField()
name = models.CharField(max_length=2000)
city = models.CharField(max_length=200, blank=True, null=True)
housenumber = models.CharField(max_length=20, blank=True, null=True)
postcode = models.CharField(max_length=20, blank=True, null=True)
street = models.CharField(max_length=200, blank=True, null=True)
county = models.CharField(max_length=200, blank=True, null=True)
# Country code as per ISO 3166-1 alpha-2
# https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes
countrycode = models.CharField(max_length=2, verbose_name=_("Ländercode"),
help_text=_("Standardisierter Ländercode nach ISO 3166-1 ALPHA-2"),
blank=True, null=True)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
if self.city and self.postcode:
return f"{self.city} ({self.postcode})"
else:
return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
@property
def position(self):
@@ -87,11 +73,6 @@ class Location(models.Model):
latitude=proxy.latitude,
longitude=proxy.longitude,
name=proxy.name,
postcode=proxy.postcode,
city=proxy.city,
street=proxy.street,
county=proxy.county,
countrycode=proxy.countrycode,
)
return location
@@ -103,33 +84,33 @@ class Location(models.Model):
instance.save()
class ImportantLocation(models.Model):
location = models.OneToOneField(Location, on_delete=models.CASCADE)
slug = models.SlugField(unique=True)
name = models.CharField(max_length=200)
class ExternalSourceChoices(models.TextChoices):
OSM = "OSM", _("Open Street Map")
class AllowUseOfMaterialsChices(models.TextChoices):
USE_MATERIALS_ALLOWED = "allowed", _("Usage allowed")
USE_MATERIALS_REQUESTED = "requested", _("Usage requested")
USE_MATERIALS_DENIED = "denied", _("Usage denied")
USE_MATERIALS_OTHER = "other", _("It's complicated")
USE_MATERIALS_NOT_ASKED = "not_asked", _("Not asked")
class RescueOrganization(models.Model):
def __str__(self):
return f"{self.name}"
USE_MATERIALS_ALLOWED = "allowed"
USE_MATERIALS_REQUESTED = "requested"
USE_MATERIALS_DENIED = "denied"
USE_MATERIALS_OTHER = "other"
USE_MATERIALS_NOT_ASKED = "not_asked"
ALLOW_USE_MATERIALS_CHOICE = {
USE_MATERIALS_ALLOWED: "Usage allowed",
USE_MATERIALS_REQUESTED: "Usage requested",
USE_MATERIALS_DENIED: "Usage denied",
USE_MATERIALS_OTHER: "It's complicated",
USE_MATERIALS_NOT_ASKED: "Not asked"
}
name = models.CharField(max_length=200)
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
allows_using_materials = models.CharField(max_length=200,
default=AllowUseOfMaterialsChices.USE_MATERIALS_NOT_ASKED,
choices=AllowUseOfMaterialsChices.choices,
default=ALLOW_USE_MATERIALS_CHOICE[USE_MATERIALS_NOT_ASKED],
choices=ALLOW_USE_MATERIALS_CHOICE,
verbose_name=_('Erlaubt Nutzung von Inhalten'))
location_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation"))
location = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True)
@@ -141,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,9 +130,6 @@ class RescueOrganization(models.Model):
choices=ExternalSourceChoices.choices,
verbose_name=_('External Source Identifier'))
class Meta:
unique_together = ('external_object_identifier', 'external_source_identifier',)
def get_absolute_url(self):
return reverse("rescue-organization-detail", args=[str(self.pk)])
@@ -172,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
@@ -297,13 +261,11 @@ class AdoptionNotice(models.Model):
updated_at = models.DateTimeField(auto_now=True)
last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=timezone.now)
searching_since = models.DateField(verbose_name=_('Sucht nach einem Zuhause seit'))
name = models.CharField(max_length=200, verbose_name=_('Titel der Vermittlung'))
name = models.CharField(max_length=200)
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
organization = models.ForeignKey(RescueOrganization, blank=True, null=True, on_delete=models.SET_NULL,
verbose_name=_('Organisation'))
further_information = models.URLField(null=True, blank=True,
verbose_name=_('Link zu mehr Informationen'),
help_text=_("Verlinke hier die Quelle der Vermittlung (z.B. die Website des Tierheims"))
further_information = models.URLField(null=True, blank=True, verbose_name=_('Link zu mehr Informationen'))
group_only = models.BooleanField(default=False, verbose_name=_('Ausschließlich Gruppenadoption'))
photos = models.ManyToManyField(Image, blank=True)
location_string = models.CharField(max_length=200, verbose_name=_("Ortsangabe"))
@@ -321,13 +283,6 @@ class AdoptionNotice(models.Model):
sexes.add(animal.sex)
return sexes
@property
def num_per_sex(self):
num_per_sex = dict()
for sex in SexChoices:
num_per_sex[sex] = self.animals.filter(sex=sex).count
return num_per_sex
@property
def last_checked_hr(self):
time_since_last_checked = timezone.now() - self.last_checked
@@ -368,10 +323,6 @@ class AdoptionNotice(models.Model):
"""Returns the url to access a detailed page for the adoption notice."""
return reverse('adoption-notice-detail', args=[str(self.id)])
def get_absolute_url_bulma(self):
"""Returns the url to access a detailed page for the adoption notice."""
return reverse('adoption-notice-detail-bulma', args=[str(self.id)])
def get_report_url(self):
"""Returns the url to report an adoption notice."""
return reverse('report-adoption-notice', args=[str(self.id)])
@@ -715,31 +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)
@@ -748,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,6 +1,6 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from fellchensammlung.models import BaseNotification, CommentNotification, User, TrustLevel, RescueOrganization
from fellchensammlung.models import BaseNotification, CommentNotification, User, TrustLevel
from .tasks import task_send_notification_email
from notfellchen.settings import host
from django.utils.translation import gettext_lazy as _
@@ -18,13 +18,6 @@ def base_notification_receiver(sender, instance: BaseNotification, created: bool
else:
task_send_notification_email.delay(instance.pk)
@receiver(post_save, sender=RescueOrganization)
def rescue_org_receiver(sender, instance: RescueOrganization, created: bool, **kwargs):
if instance.location:
return
else:
task_send_notification_email.delay(instance.pk)
@receiver(post_save, sender=User)
def notification_new_user(sender, instance: User, created: bool, **kwargs):

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,60 +0,0 @@
/***************/
/* MAIN COLORS */
/***************/
:root {
--primary-light-one: #5daa68;
--primary-light-two: #4a9455;
--primary-semidark-one: #356c3c;
--primary-dark-one: #17311b;
--secondary-light-one: #faf1cf;
--secondary-light-two: #e1d7b5;
--background-one: var(--primary-light-one);
--background-two: var(--primary-light-two);
--background-three: var(--secondary-light-one);
--background-four: var(--primary-dark-one);
--highlight-one: var(--primary-dark-one);
--highlight-one-text: var(--secondary-light-one);
--highlight-two: var(--primary-semidark-one);
--text-one: var(--secondary-light-one);
--shadow-one: var(--primary-dark-one);
--text-two: var(--primary-dark-one);
--text-three: var(--primary-light-one);
--shadow-three: var(--primary-dark-one);
}
.content {
padding: 10px;
}
/*******/
/* MAP */
/*******/
.map {
border-radius: 8px;
width:100%;
height:100%
}
.marker {
background-image: url('../img/logo_transparent.png');
background-size: cover;
width: 50px;
height: 50px;
cursor: pointer;
}
.animal-shelter-marker {
background-image: url('../img/animal_shelter.png');
!important;
}
.maplibregl-popup {
max-width: 600px !important;
}
.map-in-content #map {
max-height: 500px;
width: 90%;
}

File diff suppressed because one or more lines are too long

View File

@@ -1,420 +0,0 @@
/*! PhotoSwipe main CSS by Dmytro Semenov | photoswipe.com */
.pswp {
--pswp-bg: #000;
--pswp-placeholder-bg: #222;
--pswp-root-z-index: 100000;
--pswp-preloader-color: rgba(79, 79, 79, 0.4);
--pswp-preloader-color-secondary: rgba(255, 255, 255, 0.9);
/* defined via js:
--pswp-transition-duration: 333ms; */
--pswp-icon-color: #fff;
--pswp-icon-color-secondary: #4f4f4f;
--pswp-icon-stroke-color: #4f4f4f;
--pswp-icon-stroke-width: 2px;
--pswp-error-text-color: var(--pswp-icon-color);
}
/*
Styles for basic PhotoSwipe (pswp) functionality (sliding area, open/close transitions)
*/
.pswp {
position: fixed;
z-index: var(--pswp-root-z-index);
display: none;
touch-action: none;
outline: 0;
opacity: 0.003;
contain: layout style size;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* Prevents focus outline on the root element,
(it may be focused initially) */
.pswp:focus {
outline: 0;
}
.pswp * {
box-sizing: border-box;
}
.pswp img {
max-width: none;
}
.pswp--open {
display: block;
}
.pswp,
.pswp__bg {
transform: translateZ(0);
will-change: opacity;
}
.pswp__bg {
opacity: 0.005;
background: var(--pswp-bg);
}
.pswp,
.pswp__scroll-wrap {
overflow: hidden;
}
.pswp,
.pswp__scroll-wrap,
.pswp__bg,
.pswp__container,
.pswp__item,
.pswp__content,
.pswp__img,
.pswp__zoom-wrap {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.pswp {
position: fixed;
}
.pswp__img,
.pswp__zoom-wrap {
width: auto;
height: auto;
}
.pswp--click-to-zoom.pswp--zoom-allowed .pswp__img {
cursor: -webkit-zoom-in;
cursor: -moz-zoom-in;
cursor: zoom-in;
}
.pswp--click-to-zoom.pswp--zoomed-in .pswp__img {
cursor: move;
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.pswp--click-to-zoom.pswp--zoomed-in .pswp__img:active {
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* :active to override grabbing cursor */
.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img,
.pswp--no-mouse-drag.pswp--zoomed-in .pswp__img:active,
.pswp__img {
cursor: -webkit-zoom-out;
cursor: -moz-zoom-out;
cursor: zoom-out;
}
/* Prevent selection and tap highlights */
.pswp__container,
.pswp__img,
.pswp__button,
.pswp__counter {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.pswp__item {
/* z-index for fade transition */
z-index: 1;
overflow: hidden;
}
.pswp__hidden {
display: none !important;
}
/* Allow to click through pswp__content element, but not its children */
.pswp__content {
pointer-events: none;
}
.pswp__content > * {
pointer-events: auto;
}
/*
PhotoSwipe UI
*/
/*
Error message appears when image is not loaded
(JS option errorMsg controls markup)
*/
.pswp__error-msg-container {
display: grid;
}
.pswp__error-msg {
margin: auto;
font-size: 1em;
line-height: 1;
color: var(--pswp-error-text-color);
}
/*
class pswp__hide-on-close is applied to elements that
should hide (for example fade out) when PhotoSwipe is closed
and show (for example fade in) when PhotoSwipe is opened
*/
.pswp .pswp__hide-on-close {
opacity: 0.005;
will-change: opacity;
transition: opacity var(--pswp-transition-duration) cubic-bezier(0.4, 0, 0.22, 1);
z-index: 10; /* always overlap slide content */
pointer-events: none; /* hidden elements should not be clickable */
}
/* class pswp--ui-visible is added when opening or closing transition starts */
.pswp--ui-visible .pswp__hide-on-close {
opacity: 1;
pointer-events: auto;
}
/* <button> styles, including css reset */
.pswp__button {
position: relative;
display: block;
width: 50px;
height: 60px;
padding: 0;
margin: 0;
overflow: hidden;
cursor: pointer;
background: none;
border: 0;
box-shadow: none;
opacity: 0.85;
-webkit-appearance: none;
-webkit-touch-callout: none;
}
.pswp__button:hover,
.pswp__button:active,
.pswp__button:focus {
transition: none;
padding: 0;
background: none;
border: 0;
box-shadow: none;
opacity: 1;
}
.pswp__button:disabled {
opacity: 0.3;
cursor: auto;
}
.pswp__icn {
fill: var(--pswp-icon-color);
color: var(--pswp-icon-color-secondary);
}
.pswp__icn {
position: absolute;
top: 14px;
left: 9px;
width: 32px;
height: 32px;
overflow: hidden;
pointer-events: none;
}
.pswp__icn-shadow {
stroke: var(--pswp-icon-stroke-color);
stroke-width: var(--pswp-icon-stroke-width);
fill: none;
}
.pswp__icn:focus {
outline: 0;
}
/*
div element that matches size of large image,
large image loads on top of it,
used when msrc is not provided
*/
div.pswp__img--placeholder,
.pswp__img--with-bg {
background: var(--pswp-placeholder-bg);
}
.pswp__top-bar {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 60px;
display: flex;
flex-direction: row;
justify-content: flex-end;
z-index: 10;
/* allow events to pass through top bar itself */
pointer-events: none !important;
}
.pswp__top-bar > * {
pointer-events: auto;
/* this makes transition significantly more smooth,
even though inner elements are not animated */
will-change: opacity;
}
/*
Close button
*/
.pswp__button--close {
margin-right: 6px;
}
/*
Arrow buttons
*/
.pswp__button--arrow {
position: absolute;
top: 0;
width: 75px;
height: 100px;
top: 50%;
margin-top: -50px;
}
.pswp__button--arrow:disabled {
display: none;
cursor: default;
}
.pswp__button--arrow .pswp__icn {
top: 50%;
margin-top: -30px;
width: 60px;
height: 60px;
background: none;
border-radius: 0;
}
.pswp--one-slide .pswp__button--arrow {
display: none;
}
/* hide arrows on touch screens */
.pswp--touch .pswp__button--arrow {
visibility: hidden;
}
/* show arrows only after mouse was used */
.pswp--has_mouse .pswp__button--arrow {
visibility: visible;
}
.pswp__button--arrow--prev {
right: auto;
left: 0px;
}
.pswp__button--arrow--next {
right: 0px;
}
.pswp__button--arrow--next .pswp__icn {
left: auto;
right: 14px;
/* flip horizontally */
transform: scale(-1, 1);
}
/*
Zoom button
*/
.pswp__button--zoom {
display: none;
}
.pswp--zoom-allowed .pswp__button--zoom {
display: block;
}
/* "+" => "-" */
.pswp--zoomed-in .pswp__zoom-icn-bar-v {
display: none;
}
/*
Loading indicator
*/
.pswp__preloader {
position: relative;
overflow: hidden;
width: 50px;
height: 60px;
margin-right: auto;
}
.pswp__preloader .pswp__icn {
opacity: 0;
transition: opacity 0.2s linear;
animation: pswp-clockwise 600ms linear infinite;
}
.pswp__preloader--active .pswp__icn {
opacity: 0.85;
}
@keyframes pswp-clockwise {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/*
"1 of 10" counter
*/
.pswp__counter {
height: 30px;
margin: 15px 0 0 20px;
font-size: 14px;
line-height: 30px;
color: var(--pswp-icon-color);
text-shadow: 1px 1px 3px var(--pswp-icon-color-secondary);
opacity: 0.85;
}
.pswp--one-slide .pswp__counter {
display: none;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -1,11 +0,0 @@
import PhotoSwipeLightbox from 'https://unpkg.com/photoswipe/dist/photoswipe-lightbox.esm.js';
const lightbox = new PhotoSwipeLightbox({
gallery: '#my-gallery',
children: 'a',
pswpModule: () => import('https://unpkg.com/photoswipe'),
});
lightbox.init();

View File

@@ -1,32 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Add a click event on each of them
$navbarBurgers.forEach( el => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target;
const $target = document.getElementById(target);
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
});
});
});
// Looks for all notifications with a delete and allows closing them when pressing delete
document.addEventListener('DOMContentLoaded', () => {
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
const $notification = $delete.parentNode;
$delete.addEventListener('click', () => {
$notification.parentNode.removeChild($notification);
});
});
});

View File

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

View File

@@ -6,7 +6,7 @@ from notfellchen.celery import app as celery_app
from .mail import send_notification_email
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices
from .tools.misc import healthcheck_ok
from .models import Location, AdoptionNotice, Timestamp, RescueOrganization
from .models import Location, AdoptionNotice, Timestamp
from .tools.notifications import notify_of_AN_to_be_checked
from .tools.search import notify_search_subscribers
@@ -57,10 +57,3 @@ def task_healthcheck():
@shared_task
def task_send_notification_email(notification_pk):
send_notification_email(notification_pk)
@celery_app.task(name="commit.post_rescue_org_save")
def post_rescue_org_save(pk):
instance = RescueOrganization.objects.get(pk=pk)
Location.add_location_to_object(instance)
set_timestamp("add_rescue_org_location")
logging.info(f"Location was added to Rescue Organization {pk}")

View File

@@ -1,43 +0,0 @@
{% load custom_tags %}
{% load i18n %}
{% load static %}
{% get_current_language as LANGUAGE_CODE%}
<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE }}">
<head>
{% block title %}{% endblock %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{% translate "Farbratten aus dem Tierschutz finden und adoptieren" %}">
<!-- Add additional CSS in static file -->
<link rel="stylesheet" href="{% static 'fellchensammlung/css/bulma-styles.css' %}">
<link rel="stylesheet" href="{% static 'fellchensammlung/css/bulma.min.css' %}">
<link rel="stylesheet" href="https://unpkg.com/photoswipe@5.2.2/dist/photoswipe.css">
<link href="{% static 'fontawesomefree/css/fontawesome.css' %}" rel="stylesheet" type="text/css">
<link href="{% static 'fontawesomefree/css/brands.css' %}" rel="stylesheet" type="text/css">
<link href="{% static 'fontawesomefree/css/solid.css' %}" rel="stylesheet" type="text/css">
<script src="{% static 'fellchensammlung/js/custom.js' %}"></script>
<script src="{% static 'fellchensammlung/js/toggles.js' %}"></script>
<script type="module" src="{% static 'fellchensammlung/js/photoswipe.js' %}"></script>
<link rel="apple-touch-icon" sizes="180x180" href="{% static 'fellchensammlung/favicon/apple-touch-icon.png' %}">
<link rel="icon" type="image/png" sizes="32x32" href="{% static 'fellchensammlung/favicon/favicon-32x32.png' %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% static 'fellchensammlung/favicon/favicon-16x16.png' %}">
{% get_oxitraffic_script_if_enabled %}
</head>
<body>
{% block header %}
{% include "fellchensammlung/bulma-header.html" %}
{% endblock %}
<div class="content">
{% block content %}{% endblock %}
</div>
{% block footer %}
{% include "fellchensammlung/bulma-footer.html" %}
{% endblock %}
</body>
</html>

View File

@@ -1,8 +1,7 @@
{% load custom_tags %}
{% load i18n %}
{% get_current_language as LANGUAGE_CODE%}
<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE }}">
<html lang="en">
<head>
{% block title %}{% endblock %}
<meta charset="utf-8">

View File

@@ -1,27 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% load custom_tags %}
{% block title %}<title>{% translate "Über uns" %}</title>{% endblock %}
{% block content %}
{% if about_us %}
<div class="block">
<h1 class="title is-1">{{ about_us.title }}</h1>
<div class="content">
{{ about_us.content | render_markdown }}
</div>
</div>
{% endif %}
{% if faq %}
<div class="card">
<div class="card-header">
<h2 class="card-header-title">{{ faq.title }}</h2>
</div>
<div class="card-content">
{{ faq.content | render_markdown }}
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -1,83 +0,0 @@
{% load static %}
{% load i18n %}
<footer class="footer">
<div class="columns">
<div class="column">
<div class="block">
<h3 class="bd-footer-title title is-3 has-text-left">
Notfellchen
</h3>
<!-- footer content -->
<p class="bd-footer-link
has-text-left">
Für Menschen die Ratten aus dem Tierschutz ein liebendes Zuhause geben wollen.
</p>
</div>
<div class="block">
<h3 class="bd-footer-title title is-5">
{% trans 'Sprache ändern' %}
</h3>
{% include "fellchensammlung/forms/change_language.html" %}
</div>
</div>
<div class="column">
<h4 class="bd-footer-title title is-4 has-text-justify">
{% translate 'Über uns' %}
</h4>
<a class="bd-footer-link" href="{% url "about-bulma" %}">
{% translate 'Das Notfellchen Projekt' %}
</a>
<br/>
<a class="bd-footer-link" href="{% url "terms-of-service" %}">
{% translate 'Nutzungsbedingungen' %}
</a>
<br/>
<a class="bd-footer-link" href="{% url "privacy" %}">
{% translate 'Datenschutz' %}
</a>
<br/>
<a class="bd-footer-link" href="{% url "imprint" %}">
{% translate 'Impressum' %}
</a>
<br/>
</div>
<div class="column">
<h4 class="bd-footer-title title is-4 has-text-justify">
Technisches
</h4>
<p class="bd-footer-link">
<a class="nav-link " href="{% url "rss" %}">
<i class="fa-solid fa-rss"></i> {% translate 'RSS' %}
</a>
<br/>
<a href="https://dokumentation.notfellchen.org/">
<span class="icon-text">
<span>{% translate 'Dokumentation' %}</span>
</span>
</a>
<br/>
<a href="mailto:info@notfellchen.org">
<span class="icon-text">
<span>{% translate 'Probleme melden' %}</span>
</span>
</a>
<br/>
<a href="https://codeberg.org/moanos/notfellchen">
<span class="icon-text">
<span>{% trans 'Code' %}</span>
</span>
</a>
</p>
</div>
</div>
</footer>

View File

@@ -1,43 +0,0 @@
{% load static %}
{% load i18n %}
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="{% url 'index-bulma' %}">
<img src="{% static 'fellchensammlung/img/logo_transparent.png' %}" alt="{% trans 'Notfellchen Logo' %}">
<h1 class="title is-4">notfellchen.org</h1>
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="{% url 'search-bulma' %}">
<i class="fas fa-search"></i> {% translate 'Suchen' %}
</a>
<a class="navbar-item" href="{% url "add-adoption-bulma" %}">
<i class="fas fa-feather"></i> {% translate 'Vermittlung hinzufügen' %}
</a>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<a class="button is-primary" href="{% url "django_registration_register" %}">
<strong>{% translate "Registrieren" %}</strong>
</a>
<a class="button is-light" href="{% url "login" %}">
<strong>{% translate "Login" %}</strong>
</a>
</div>
</div>
</div>
</div>
</div>
</nav>

View File

@@ -1,34 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% load custom_tags %}
{% block title %}<title>{% translate "Notfellchen - Farbratten aus dem Tierschutz adoptieren" %}</title>{% endblock %}
{% block content %}
{% for announcement in announcements %}
{% include "fellchensammlung/partials/bulma-partial-announcement.html" %}
{% endfor %}
{% if introduction %}
<h1>{{ introduction.title }}</h1>
{{ introduction.content | render_markdown }}
{% endif %}
<h2>{% translate "Aktuelle Vermittlungen" %}</h2>
<div class="block">
{% include "fellchensammlung/lists/bulma-list-adoption-notices.html" %}
<a class="button is-primary" href="{% url 'search' %}">{% translate "Mehr Vermittlungen" %}</a>
</div>
<div class="block" style="height: 50vh">
{% include "fellchensammlung/partials/bulma-partial-map.html" %}
</div>
{% if how_to %}
<div class="card">
<h1>{{ how_to.title }}</h1>
{{ how_to.content | render_markdown }}
</div>
{% endif %}
{% endblock %}

View File

@@ -1,9 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% block title %}<title>{% translate "Karte" %}</title>{% endblock %}
{% block content %}
<div style="height:70vh">
{% include "fellchensammlung/partials/bulma-partial-map.html" %}
</div>
{% endblock %}

View File

@@ -1,15 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% load custom_tags %}
{% block title %}<title>{{ text.title }}</title>{% endblock %}
{% block content %}
<div class="block">
<h1 class="title is-1">{{ text.title }}</h1>
<div class="content">
{{ text.content | render_markdown }}
</div>
</div>
{% endblock %}

View File

@@ -1,97 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% block title %}<title>{% translate "Suche" %}</title>{% endblock %}
{% block content %}
{% get_current_language as LANGUAGE_CODE_CURRENT %}
<div class="columns">
<div class="column is-two-thirds">
<div style="height: 50vh">
{% include "fellchensammlung/partials/bulma-partial-map.html" %}
</div>
</div>
<div class="column">
<form class="block" method="post">
{% csrf_token %}
<input type="hidden" name="longitude" maxlength="200" id="longitude">
<input type="hidden" name="latitude" maxlength="200" id="latitude">
<input type="hidden" id="place_id" name="place_id">
<!--- https://docs.djangoproject.com/en/5.2/topics/forms/#reusable-form-templates -->
{{ search_form }}
<ul id="results"></ul>
<button class="button is-primary" type="submit" value="search" name="search">
<i class="fas fa-search"></i> {% trans 'Suchen' %}
</button>
{% if searched %}
{% if subscribed_search %}
<button class="button" 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="button" type="submit" name="subscribe_to_search">
<i class="fas fa-bell"></i> {% trans 'Suche abonnieren' %}
</button>
{% endif %}
{% endif %}
</form>
<div class="block">
{% if place_not_found %}
<div class="block notification is-warning">
<p>
{% trans 'Ort nicht gefunden' %}
</p>
</div>
{% endif %}
</div>
</div>
</div>
<div class="">
{% include "fellchensammlung/lists/bulma-list-adoption-notices.html" %}
</div>
<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={{ LANGUAGE_CODE_CURRENT }}`);
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

@@ -1,120 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% load static %}
{% block title %}<title>{% translate "Styleguide für Bulma" %}</title>{% endblock %}
{% block content %}
<section class="section">
<div class="container">
<h1 class="title">
Hello World
</h1>
<p class="subtitle">
Notfellchen bald mit <strong>Bulma</strong>?
</p>
</div>
<div class="grid">
<div class="card">
<div class="card-image">
<figure class="image">
<img
src="{% static 'fellchensammlung/img/example_rat_single.png' %}"
alt="Placeholder image"
/>
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img
src="https://bulma.io/assets/images/placeholders/96x96.png"
alt="Placeholder image"
/>
</figure>
</div>
<div class="media-content">
<p class="title is-4">John Smith</p>
<p class="subtitle is-6">@johnsmith</p>
</div>
</div>
<div class="content">
Süße Ratte sucht Zuhause
<a href="#">#responsive</a>
<br/>
<time datetime="2016-1-1">11:09 PM - 1 Jan 2016</time>
</div>
</div>
</div>
<div class="card">
<div class="card-image">
<figure class="image">
<img
src="{% static 'fellchensammlung/img/example_rat_single.png' %}"
alt="Placeholder image"
/>
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img
src="https://bulma.io/assets/images/placeholders/96x96.png"
alt="Placeholder image"
/>
</figure>
</div>
<div class="media-content">
<p class="title is-4">John Smith</p>
<p class="subtitle is-6">@johnsmith</p>
</div>
</div>
<div class="content">
Süßeste Ratte sucht Zuhause
<a href="#">#responsive</a>
<br/>
<time datetime="2016-1-1">11:09 PM - 1 Jan 2016</time>
</div>
</div>
</div>
<div class="card">
<div class="card-image">
<figure class="image">
<img
src="{% static 'fellchensammlung/img/example_rat_single.png' %}"
alt="Placeholder image"
/>
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img
src="https://bulma.io/assets/images/placeholders/96x96.png"
alt="Placeholder image"
/>
</figure>
</div>
<div class="media-content">
<p class="title is-4">John Smith</p>
<p class="subtitle is-6">@johnsmith</p>
</div>
</div>
<div class="content">
Süßere Ratte sucht Zuhause
<a href="#">#responsive</a>
<br/>
<time datetime="2016-1-1">11:09 PM - 1 Jan 2016</time>
</div>
</div>
</div>
</div>
</section>
{% endblock %}

View File

@@ -1,17 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% load custom_tags %}
{% block title %}<title>{% translate "Über uns" %}</title>{% endblock %}
{% block content %}
<h2 class="title is-2">{% translate "Regeln" %}</h2>
{% include "fellchensammlung/lists/bulma-list-rules.html" %}
<div class="block">
<h1 class="title is-1">{{ text.title }}</h1>
<div class="content">
{{ text.content | render_markdown }}
</div>
</div>
{% endblock %}

View File

@@ -1,113 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load custom_tags %}
{% load i18n %}
{% load static %}
{% block title %}<title>{{ adoption_notice.name }}</title>{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h1 class="card-header-title title is-2">{{ adoption_notice.name }}</h1>
</div>
<div class="card-content">
<div class="grid">
<div class="cell">
<!--- General Information --->
<div class="grid">
<div class="cell">
<h2><strong>{% translate "Ort" %}</strong></h2>
<p>{% if adoption_notice.location %}
{{ adoption_notice.location }}
{% else %}
{{ adoption_notice.location_string }}
{% endif %}</p>
</div>
<div class="cell">
{% include "fellchensammlung/partials/bulma-sex-overview.html" %}
</div>
</div>
</div>
</div>
<div class="columns">
<!--- Images --->
<div class="column block">
<div class="card">
<div class="grid card-content">
<div class="cell" id="my-gallery">
{% for photo in adoption_notice.get_photos %}
<a href="{{ MEDIA_URL }}/{{ photo.image }}"
data-pswp-width="{{ photo.image.width }}"
data-pswp-height="{{ photo.image.height }}"
target="_blank">
<img style="height: 12rem" src="{{ MEDIA_URL }}/{{ photo.image }}"
alt="{ photo.alt_text }}"/>
</a>
{% endfor %}
</div>
</div>
</div>
</div>
<!--- Description --->
<div class="column block">
<div class="card">
<div class="card-header">
<h1 class="card-header-title title is-2">{% translate "Beschreibung" %}</h1>
</div>
<div class="card-content">
<p class="expandable">{% if adoption_notice.description %}
{{ adoption_notice.description | render_markdown }}
{% else %}
{% translate "Keine Beschreibung angegeben" %}
{% endif %}
</p>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer">
{% if has_edit_permission %}
<div class="card-footer-item">
<div class="column">
<a class="button is-primary is-light"
href="{% url 'adoption-notice-add-photo' adoption_notice_id=adoption_notice.pk %}">
{% translate 'Foto hinzufügen' %}
</a>
</div>
<div class="card-footer-item">
<a class="button is-primary"
href="{% url 'adoption-notice-edit' adoption_notice_id=adoption_notice.pk %}">
{% translate 'Bearbeiten' %}
</a>
</div>
</div>
{% endif %}
</div>
</div>
<div class="columns">
{% for animal in adoption_notice.animals %}
<div class="column">
{% include "fellchensammlung/partials/bulma-partial-animal-card.html" %}
</div>
{% endfor %}
</div>
<div class="block">
{% if adoption_notice.further_information %}
<form method="get" action="{% url 'external-site' %}">
<input type="hidden" name="url" value="{{ adoption_notice.further_information }}">
<button class="button is-primary is-fullwidth" type="submit" id="submit">
{{ adoption_notice.further_information | domain }} <i
class="fa-solid fa-arrow-up-right-from-square"></i>
</button>
</form>
{% endif %}
</div>
<div class="block">
{% include "fellchensammlung/partials/bulma-partial-comment-section.html" %}
</div>
{% endblock %}

View File

@@ -1,95 +0,0 @@
{% extends "fellchensammlung/base_bulma.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load widget_tweaks %}
{% block title %}<title>{% translate "Vermittlung hinzufügen" %}</title>{% endblock %}
{% block content %}
<h1>{% translate "Vermitteln" %}</h1>
<div class="notification">
<button class="delete"></button>
<p>
{% url 'terms-of-service' as rules_url %}
{% trans "Regeln" as rules_text %}
{% blocktranslate with rules_link='<a href="'|add:rules_url|add:'">'|add:rules_text|add:'</a>'|safe %}
Bitte mach dich zunächst mit unseren {{ rules_link }} vertraut. Dann trage hier die ersten Informationen
ein.
Fotos kannst du im nächsten Schritt hinzufügen.
{% endblocktranslate %}
</p>
</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="field">
<label class="label" for="an-name">{{ form.name.label }}
{% if form.name.field.required %}<span class="special_class">*</span>{% endif %}</label>
{{ form.name|add_class:"input"|attr:"id:an-name" }}
</div>
<div class="field">
<label class="label" for="an-description">{% translate 'Beschreibung' %}</label>
{{ form.description|add_class:"input textarea"|attr:"rows:3"|attr:"id:an-description" }}
</div>
<div class="field">
<label class="label" for="an-location">{{ form.location_string.label }}</label>
{{ form.location_string|add_class:"input"|attr:"id:an-location" }}
</div>
<div class="field">
<label class="checkbox" for="an-group-only">{{ form.group_only.label }}</label>
{{ form.group_only|add_class:"checkbox"|attr:"id:an-group-only" }}
</div>
<div class="field">
<label class="label" for="an-searching-since">{{ form.searching_since.label }}</label>
{{ form.searching_since|add_class:"input"|attr:"id:an-searching-since"|attr:"type:date" }}
</div>
<div class="notification">
<button class="delete"></button>
<p>
{% blocktranslate %}
Gibt hier schonmal erste Details zu den Tieren an.
Wenn du Details und Fotos zu den Tieren hinzufügen willst oder ihr Geschlecht und Geburtsdatum
anpassen
willst,
kannst du das im nächsten Schritt tun.
{% endblocktranslate %}
</p>
</div>
<div class="field">
<label class="label" for="an-species">{% translate 'Tierart' %}</label>
<div class="select">
{{ form.species|attr:"id:an-species" }}
</div>
</div>
<div class="field">
<label class="label" for="an-num-animals">{{ form.num_animals.label }}</label>
{{ form.num_animals|add_class:"input"|attr:"id:an-num-animals" }}
</div>
<div class="field">
<label class="label" for="an-sex">{% translate 'Geschlecht' %}</label>
<div class="select">
{{ form.sex|attr:"id:an-sex" }}
</div>
</div>
<div class="field">
<label class="label" for="an-date-of-birth">{{ form.date_of_birth.label }}</label>
{{ form.date_of_birth|add_class:"input"|attr:"id:an-date-of-birth"|attr:"type:date" }}
</div>
<input class="button is-primary" type="submit" value="{% translate "Speichern" %}">
</form>
{% endblock %}

View File

@@ -1,15 +0,0 @@
{% load i18n %}
{% load crispy_forms_tags %}
<div class="card">
<div class="card-header">
<div class="card-header-title">
{% blocktrans %}
Als {{ user }} kommentieren
{% endblocktrans %}
</div>
</div>
<div class="card-content">
{% crispy comment_form %}
</div>
</div>

View File

@@ -1,5 +1,5 @@
{% load i18n %}
<form class="btn2 select" action="{% url 'change-language' %}" method="post" onchange='this.form.submit()'>
<form class="btn2" action="{% url 'change-language' %}" method="post" onchange='this.form.submit()'>
{% csrf_token %}
<select name="language" onchange='this.form.submit()'>
{% get_current_language as LANGUAGE_CODE_CURRENT %}

View File

@@ -1,26 +0,0 @@
<!--- See https://docs.djangoproject.com/en/5.2/topics/forms/#reusable-form-templates -->
{% load custom_tags %}
{% for field in form %}
<div class="field">
<label class="label">
{{ field.label }}
</label>
<div class="control">
{% if field|widget_type == 'TextInput' %}
{{ field|add_class:"input" }}
{% elif field|widget_type == 'Select' %}
<div class="select">
{{ field }}
</div>
{% else %}
{{ field|add_class:"input" }}
{% endif %}
</div>
<div class="help is-danger">
{{ field.errors }}
</div>
</div>
{% endfor %}

View File

@@ -3,9 +3,7 @@
<section class="header">
<div>
<a href="{% url "index" %}" class="logo">
<img src="{% static 'fellchensammlung/img/logo_transparent.png' %}" alt="{% trans 'Notfellchen Logo' %}">
</a>
<a href="{% url "index" %}" class="logo"><img src={% static 'fellchensammlung/img/logo_transparent.png' %}></a>
</div>
<div class="profile-card">
@@ -29,7 +27,7 @@
</form>
{% else %}
<a class="btn2" href="{% url "django_registration_register" %}">{% translate "Registrieren" %}</a>
<a class="btn2" href="{% url "login" %}"><i class="fa fa-sign-in" aria-label="Login"></i></a>
<a class="btn2" href="{% url "login" %}"><i class="fa fa-sign-in" aria-hidden="true"></i></a>
{% endif %}
<input id="menu-toggle" type="checkbox"/>
<label class='menu-button-container' for="menu-toggle">

View File

@@ -1,12 +0,0 @@
{% load i18n %}
{% if adoption_notices %}
<div class="grid">
{% for adoption_notice in adoption_notices %}
<div class="cell">
{% include "fellchensammlung/partials/bulma-partial-adoption-notice-minimal.html" %}
</div>
{% endfor %}
</div>
{% else %}
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
{% endif %}

View File

@@ -1,5 +0,0 @@
<div class="container-cards">
{% for rule in rules %}
{% include "fellchensammlung/partials/bulma-partial-rule.html" %}
{% endfor %}
</div>

View File

@@ -1,6 +1,6 @@
{% extends "fellchensammlung/base_generic.html" %}
{% load i18n %}
{% block title %}<title>{% translate "Karte" %}</title>{% endblock %}
{% block title %}<title>{% translate "Karte" %}</title> %}
{% block content %}
<div class="card">

View File

@@ -1,40 +0,0 @@
{% load custom_tags %}
{% load i18n %}
<h2 class="heading-card-adoption-notice title is-4">
<a href="{{ adoption_notice.get_absolute_url_bulma }}"> {{ adoption_notice.name }}</a>
</h2>
<div class="grid mb-0">
<div class="cell">
<!--- General Information --->
<div class="grid">
<div class="cell">
<p>
<b><i class="fa-solid fa-location-dot"></i></b>
{% if adoption_notice.location %}
{{ adoption_notice.location }}
{% else %}
{{ adoption_notice.location_string }}
{% endif %}</p>
</div>
<div class="cell">
{% include "fellchensammlung/partials/bulma-sex-overview.html" %}
</div>
</div>
</div>
</div>
{% if adoption_notice.get_photo %}
<div class="adoption-notice-img img-small">
<img src="{{ MEDIA_URL }}/{{ adoption_notice.get_photo.image }}"
alt="{{ adoption_notice.get_photo.alt_text }}">
</div>
{% endif %}

View File

@@ -1,42 +0,0 @@
{% load custom_tags %}
{% load i18n %}
<div class="card">
<div class="header-card-adoption-notice">
<h2 class="heading-card-adoption-notice title is-4">
<a href="{{ adoption_notice.get_absolute_url_bulma }}"> {{ adoption_notice.name }}</a>
</h2>
</div>
<div class="grid">
<div class="cell">
<!--- General Information --->
<div class="grid">
<div class="cell">
<p>
<b><i class="fa-solid fa-location-dot"></i></b>
{% if adoption_notice.location %}
{{ adoption_notice.location }}
{% else %}
{{ adoption_notice.location_string }}
{% endif %}</p>
</div>
<div class="cell">
{% include "fellchensammlung/partials/bulma-sex-overview.html" %}
</div>
</div>
</div>
</div>
{% if adoption_notice.get_photo %}
<div class="adoption-notice-img img-small">
<img src="{{ MEDIA_URL }}/{{ adoption_notice.get_photo.image }}"
alt="{{ adoption_notice.get_photo.alt_text }}">
</div>
{% endif %}
</div>

View File

@@ -1,38 +0,0 @@
{% load i18n %}
{% load custom_tags %}
<div class="card">
<div class="card-header">
<h1 class="card-header-title">
<a href="{% url 'animal-detail' animal_id=animal.pk %}">{{ animal.name }}</a>
</h1>
<div class="tags">
<div class="tag species">{{ animal.species }}</div>
<div class="tag sex">{{ animal.get_sex_display }}</div>
</div>
</div>
<div class="card-content">
{% if animal.description %}
<p>{{ animal.description | render_markdown }}</p>
{% endif %}
<div class="cell" id="my-gallery">
{% for photo in animal.get_photos %}
<a href="{{ MEDIA_URL }}/{{ photo.image }}"
data-pswp-width="{{ photo.image.width }}"
data-pswp-height="{{ photo.image.height }}"
target="_blank">
<img src="{{ MEDIA_URL }}/{{ photo.image }}" alt="{{ photo.alt_text }}">
</a>
{% endfor %}
</div>
<!--- Assume a user does not have edit permissions on animal if they have no other edit permission --->
{% if has_edit_permission %}
<div class="card-footer">
<a class="card-footer-item button" href="{% url 'animal-edit' animal_id=animal.pk %}">{% translate 'Bearbeiten' %}</a>
<a class="card-footer-item button"
href="{% url 'animal-add-photo' animal_id=animal.pk %}">{% translate 'Foto hinzufügen' %}</a>
</div>
{% endif %}
</div>
</div>

View File

@@ -1,25 +0,0 @@
{% load i18n %}
<div class="card">
<div class="card-header">
<h2 class="card-header-title title is-2">{% translate 'Kommentare' %}</h2>
</div>
<div class="card-content">
{% if adoption_notice.comments %}
{% for comment in adoption_notice.comments %}
{% include "fellchensammlung/partials/bulma-partial-comment.html" %}
{% endfor %}
{% else %}
<p class="is-italic">{% translate 'Noch keine Kommentare' %}</p>
{% endif %}
</div>
<footer class="card-footer">
{% if user.is_authenticated %}
{% include "fellchensammlung/forms/bulma-form-comment.html" %}
{% else %}
<p class="card-footer-item">
{% translate 'Du musst dich einloggen um Kommentare zu hinterlassen' %}
</p>
{% endif %}
</footer>
</div>

View File

@@ -1,27 +0,0 @@
{% load i18n %}
{% load custom_tags %}
<div class="card">
<div class="card-header">
<div class="card-header-title content">
<b class="">{{ comment.user }}</b> <span class="tag"><time class="">{{ comment.created_at }}</time></span>
</div>
</div>
<div class="card-content">
<p class="content">
{{ comment.text | render_markdown }}
</p>
</div>
<div class="card-footer">
<a class="card-footer-item is-danger" href="{{ comment.get_report_url }}">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-flag"></i>
</span>
<span>{% trans 'Melden' %}</span>
</span>
</a>
</div>
</div>

View File

@@ -1,149 +0,0 @@
{% load static %}
{% load custom_tags %}
{% load i18n %}
<!-- add MapLibre JavaScript and CSS -->
<script src="{% settings_value "MAP_TILE_SERVER" %}/assets/lib/maplibre-gl/maplibre-gl.js"></script>
<link href="{% settings_value "MAP_TILE_SERVER" %}/assets/lib/maplibre-gl/maplibre-gl.css" rel="stylesheet"/>
<!-- add Turf see https://maplibre.org/maplibre-gl-js/docs/examples/draw-a-radius/ -->
<script src="{% static 'fellchensammlung/js/turf.min.js' %}"></script>
<!-- add container for the map -->
<div id="map" class="map"></div>
<!-- start map -->
<script>
{% if zoom_level %}
var zoom_level = {{ zoom_level }};
{% else %}
var zoom_level = 4;
{% endif %}
{% if map_center %}
var map_center = [{{ map_center.longitude | pointdecimal }}, {{ map_center.latitude | pointdecimal }}];
{% else %}
var map_center = [10.49, 50.68]; <!-- Point middle of Germany -->
zoom_level = 4; //Overwrite zoom level when no place is found
{% endif %}
let map = new maplibregl.Map({
container: 'map',
style: '{% static "fellchensammlung/map/styles/colorful/style.json" %}',
center: map_center,
zoom: zoom_level
});
map.addControl(new maplibregl.FullscreenControl());
map.addControl(new maplibregl.NavigationControl({showCompass: false}));
{% for adoption_notice in adoption_notices_map %}
{% if adoption_notice.location %}
// create the popup
const popup_{{ forloop.counter }} = new maplibregl.Popup({offset: 25}).setHTML(`{% include "fellchensammlung/partials/bulma-partial-adoption-notice-minimal-map.html" %}`);
// create DOM element for the marker
const el_{{ forloop.counter }} = document.createElement('div');
el_{{ forloop.counter }}.id = 'marker_{{ forloop.counter }}';
el_{{ forloop.counter }}.classList.add('marker');
const location_popup_{{ forloop.counter }} = [{{ adoption_notice.location.longitude | pointdecimal }}, {{ adoption_notice.location.latitude | pointdecimal }}];
// create the marker
new maplibregl.Marker({element: el_{{ forloop.counter }}})
.setLngLat(location_popup_{{ forloop.counter }})
.setPopup(popup_{{ forloop.counter }}) // sets a popup on this marker
.addTo(map);
{% endif %}
{% endfor %}
{% for rescue_organization in rescue_organizations %}
{% if rescue_organization.location %}
// create the popup
const popup_{{ forloop.counter }} = new maplibregl.Popup({offset: 25}).setHTML(`{% include "fellchensammlung/partials/partial-rescue-organization.html" %}`);
// create DOM element for the marker
const el_{{ forloop.counter }} = document.createElement('div');
el_{{ forloop.counter }}.id = 'marker_{{ forloop.counter }}';
el_{{ forloop.counter }}.classList.add('animal-shelter-marker', 'marker');
const location_popup_{{ forloop.counter }} = [{{ rescue_organization.location.longitude | pointdecimal }}, {{ rescue_organization.location.latitude | pointdecimal }}];
// create the marker
new maplibregl.Marker({element: el_{{ forloop.counter }}})
.setLngLat(location_popup_{{ forloop.counter }})
.setPopup(popup_{{ forloop.counter }}) // sets a popup on this marker
.addTo(map);
{% endif %}
{% endfor %}
map.on('load', async () => {
image = await map.loadImage('{% static "fellchensammlung/img/pin.png" %}');
map.addImage('pin', image.data);
{% for map_pin in map_pins %}
map.addSource('point_{{ forloop.counter }}', {
'type': 'geojson',
'data': {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [{{ map_pin.location.longitude | pointdecimal }}, {{ map_pin.location.latitude | pointdecimal }}]
}
}
]
}
});
map.addLayer({
'id': 'point_{{ forloop.counter }}',
'type': 'circle',
'source': 'point_{{ forloop.counter }}',
'paint': {
'circle-radius': 18,
'circle-color': '#ff878980'
}
});
{% endfor %}
});
{% if search_center %}
var search_center = [{{ search_center.longitude | pointdecimal }}, {{ search_center.latitude | pointdecimal }}];
map.on('load', () => {
const radius = {{ search_radius }}; // kilometer
const options = {
steps: 64,
units: 'kilometers'
};
const circle = turf.circle(search_center, radius, options);
// Add the circle as a GeoJSON source
map.addSource('location-radius', {
type: 'geojson',
data: circle
});
// Add a fill layer with some transparency
map.addLayer({
id: 'location-radius',
type: 'fill',
source: 'location-radius',
paint: {
'fill-color': 'rgba(140,207,255,0.3)',
'fill-opacity': 0.5
}
});
// Add a line layer to draw the circle outline
map.addLayer({
id: 'location-radius-outline',
type: 'line',
source: 'location-radius',
paint: {
'line-color': '#0094ff',
'line-width': 3
}
});
});
{% endif %}
</script>

View File

@@ -1,9 +0,0 @@
{% load custom_tags %}
<div class="card">
<div class="card-header">
<h2 class="card-header-title">{{ rule.title }}</h2>
</div>
<div class="card-content">
<p class="content">{{ rule.rule_text | render_markdown }}</p>
</div>
</div>

View File

@@ -1,44 +0,0 @@
{% load static %}
{% load i18n %}
<div class="grid">
{% if adoption_notice.num_per_sex.F > 0 %}
<span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.F }} x </span>
<span class="icon">
<img class="icon" src="{% static 'fellchensammlung/img/sexes/Female.png' %}"
alt="{% translate 'weibliche Tiere' %}">
</span>
</span>
{% endif %}
{% if adoption_notice.num_per_sex.I > 0 %}
<span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.I }}</span>
<span class="icon">
<img class="icon"
src="{% static 'fellchensammlung/img/sexes/Intersex.png' %}"
alt="{% translate 'intersexuelle Tiere' %}">
</span>
</span>
{% endif %}
{% if adoption_notice.num_per_sex.M > 0 %}
<span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.M }}</span>
<span class="icon">
<img class="icon" src="{% static 'fellchensammlung/img/sexes/Male.png' %}"
alt="{% translate 'männliche Tiere' %}">
</span>
</span>
{% endif %}
{% if adoption_notice.num_per_sex.M_N > 0 %}
<span class="cell icon-text tag is-medium">
<span class="has-text-weight-bold is-size-4">{{ adoption_notice.num_per_sex.M_N }}</span>
<span class="icon">
<img class="icon"
src="{% static 'fellchensammlung/img/sexes/Male Neutered.png' %}"
alt="{% translate 'männlich, kastrierte Tiere' %}">
</span>
</span>
{% endif %}
</div>

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" %}: <a href="{{ rescue_org.website }}">{{ rescue_org.website }}</a></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

@@ -3,8 +3,8 @@
{% load i18n %}
<!-- add MapLibre JavaScript and CSS -->
<script src="{% settings_value "MAP_TILE_SERVER" %}/assets/lib/maplibre-gl/maplibre-gl.js"></script>
<link href="{% settings_value "MAP_TILE_SERVER" %}/assets/lib/maplibre-gl/maplibre-gl.css" rel="stylesheet"/>
<script src="{% settings_value "MAP_TILE_SERVER" %}/assets/maplibre-gl/maplibre-gl.js"></script>
<link href="{% settings_value "MAP_TILE_SERVER" %}/assets/maplibre-gl/maplibre-gl.css" rel="stylesheet"/>
<!-- add Turf see https://maplibre.org/maplibre-gl-js/docs/examples/draw-a-radius/ -->
<script src="{% static 'fellchensammlung/js/turf.min.js' %}"></script>
@@ -29,7 +29,7 @@
let map = new maplibregl.Map({
container: 'map',
style: '{% static "fellchensammlung/map/styles/colorful/style.json" %}',
style: '{% static "fellchensammlung/map/styles/colorful.json" %}',
center: map_center,
zoom: zoom_level
}).addControl(new maplibregl.NavigationControl());
@@ -76,7 +76,7 @@
image = await map.loadImage('{% static "fellchensammlung/img/pin.png" %}');
map.addImage('pin', image.data);
{% for map_pin in map_pins %}
map.addSource('point_{{ forloop.counter }}', {
map.addSource('point', {
'type': 'geojson',
'data': {
'type': 'FeatureCollection',
@@ -91,16 +91,16 @@
]
}
});
map.addLayer({
'id': 'point_{{ forloop.counter }}',
'type': 'circle',
'source': 'point_{{ forloop.counter }}',
'paint': {
'circle-radius': 18,
'circle-color': '#ff878980'
}
});
{% endfor %}
map.addLayer({
'id': 'pints',
'type': 'symbol',
'source': 'point',
'layout': {
'icon-image': 'pin',
'icon-size': 0.1
}
});
});
{% if search_center %}

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

@@ -11,7 +11,7 @@
<input type="hidden" name="longitude" maxlength="200" id="longitude">
<input type="hidden" name="latitude" maxlength="200" id="latitude">
<input type="hidden" id="place_id" name="place_id">
{{ search_form }}
{{ search_form.as_p }}
<ul id="results"></ul>
<div class="container-edit-buttons">
<button class="btn" type="submit" value="search" name="search">

View File

@@ -1,35 +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">
</form>
<div class="card half">
{% include "fellchensammlung/partials/partial-map.html" %}
</div>
</div>
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
{% endblock %}

View File

@@ -49,7 +49,6 @@ def get_oxitraffic_script_if_enabled():
else:
return ""
@register.filter
@stringfilter
def pointdecimal(value):
@@ -58,7 +57,6 @@ def pointdecimal(value):
except ValueError:
return value
@register.filter
@stringfilter
def domain(url):
@@ -70,17 +68,6 @@ def domain(url):
except ValueError:
return url
@register.simple_tag
def settings_value(name):
return getattr(settings, name)
@register.filter(name='add_class')
def add_class(field, css_class):
return field.as_widget(attrs={"class": css_class})
@register.filter
def widget_type(field):
return field.field.widget.__class__.__name__

View File

@@ -74,21 +74,13 @@ class GeoFeature:
geofeatures = []
for feature in result["features"]:
geojson = {}
# Necessary features
geojson['place_id'] = feature["properties"]["osm_id"]
geojson['lat'] = feature["geometry"]["coordinates"][1]
geojson['lon'] = feature["geometry"]["coordinates"][0]
try:
geojson['name'] = feature["properties"]["name"]
except KeyError:
geojson['name'] = feature["properties"]["osm_id"]
optional_keys = ["housenumber", "street", "city", "postcode", "county", "countrycode"]
for key in optional_keys:
try:
geojson[key] = feature["properties"][key]
except KeyError:
pass
geojson['name'] = feature["properties"]["street"]
geojson['place_id'] = feature["properties"]["osm_id"]
geojson['lat'] = feature["geometry"]["coordinates"][1]
geojson['lon'] = feature["geometry"]["coordinates"][0]
geofeatures.append(geojson)
return geofeatures
@@ -169,7 +161,6 @@ class LocationProxy:
"""
self.geo_api = GeoAPI()
geofeatures = self.geo_api.get_geojson_for_query(location_string)
if geofeatures is None:
raise ValueError
result = geofeatures[0]
@@ -177,12 +168,6 @@ class LocationProxy:
self.place_id = result["place_id"]
self.latitude = result["lat"]
self.longitude = result["lon"]
optional_keys = ["housenumber", "street", "city", "postcode", "county", "countrycode"]
for key in optional_keys:
try:
self.__setattr__(key, result[key])
except KeyError:
self.__setattr__(key, None)
def __eq__(self, other):
return self.place_id == other.place_id

View File

@@ -1,23 +0,0 @@
from django.utils import translation
from fellchensammlung.models import Language, Text
def get_text_by_language(text_code, lang=None):
if lang is None:
language_code = translation.get_language()
lang = Language.objects.get(languagecode=language_code)
return Text.objects.get(text_code=text_code, language=lang, )
def get_texts_by_language(text_codes):
language_code = translation.get_language()
lang = Language.objects.get(languagecode=language_code)
texts = {}
for text_code in text_codes:
try:
texts[text_code] = get_text_by_language(text_code, lang)
except Text.DoesNotExist:
texts[text_code] = None
return texts

View File

@@ -6,7 +6,7 @@ from ..forms import AdoptionNoticeSearchForm
from ..models import SearchSubscription, AdoptionNotice, AdoptionNoticeNotification, SexChoicesWithAll, Location
def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: bool = True):
def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active : bool = True):
"""
This functions checks for all search subscriptions if the new adoption notice fits the search.
If the new adoption notice fits the search subscription, it sends a notification to the user that created the search.
@@ -36,7 +36,7 @@ class Search:
self.sex = None
self.area_search = None
self.max_distance = None
self.location = None # Can either be Location (DjangoModel) or LocationProxy
self.location = None # Can either be Location (DjangoModel) or LocationProxy
self.place_not_found = False # Indicates that a location was given but could not be geocoded
self.search_form = None
# Either place_id or location string must be set for area search
@@ -47,6 +47,7 @@ class Search:
elif search_subscription:
self.search_from_search_subscription(search_subscription)
def __str__(self):
return f"Search: {self.sex=}, {self.location=}, {self.area_search=}, {self.max_distance=}"
@@ -92,6 +93,7 @@ class Search:
return False
return True
def get_adoption_notices(self):
adoptions = AdoptionNotice.objects.order_by("-created_at")
# Filter for active adoption notices
@@ -116,21 +118,13 @@ class Search:
else:
self.search_form = AdoptionNoticeSearchForm()
def search_from_predefined_i_location(self, i_location, max_distance=100):
self.sex = SexChoicesWithAll.ALL
self.location = i_location.location
self.area_search = True
self.search_form = AdoptionNoticeSearchForm(initial={"location_string": self.location.name,
"max_distance": max_distance,
"sex": SexChoicesWithAll.ALL})
self.max_distance = max_distance
def search_from_search_subscription(self, search_subscription: SearchSubscription):
self.sex = search_subscription.sex
self.location = search_subscription.location
self.area_search = True
self.max_distance = search_subscription.max_distance
def subscribe(self, user):
logging.info(f"{user} subscribed to search")
if isinstance(self.location, LocationProxy):

View File

@@ -7,18 +7,8 @@ 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("bulma/", views.index_bulma, name="index-bulma"),
path("rss/", LatestAdoptionNoticesFeed(), name="rss"),
path("metrics/", views.metrics, name="metrics"),
# ex: /animal/5/
@@ -29,15 +19,12 @@ urlpatterns = [
path("tier/<int:animal_id>/add-photo", views.add_photo_to_animal, name="animal-add-photo"),
# ex: /adoption_notice/7/
path("vermittlung/<int:adoption_notice_id>/", views.adoption_notice_detail, name="adoption-notice-detail"),
path("bulma/vermittlung/<int:adoption_notice_id>/", views.adoption_notice_detail_bulma, name="adoption-notice-detail-bulma"),
# 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,
@@ -45,21 +32,12 @@ urlpatterns = [
# ex: /search/
path("suchen/", views.search, name="search"),
path("bulma/suchen/", views.search_bulma, name="search-bulma"),
path("suchen/<slug:important_location_slug>", views.search_important_locations, name="search-by-location"),
# ex: /map/
path("map/", views.map, name="map"),
# ex: /map/
path("bulma/map/", views.map_bulma, name="map-bulma"),
# ex: /vermitteln/
path("vermitteln/", views.add_adoption_notice, name="add-adoption"),
path("bulma/vermitteln/", views.add_adoption_notice_bulma, name="add-adoption-bulma"),
path("ueber-uns/", views.about, name="about"),
path("bulma/ueber-uns/", views.about_bulma, name="about-bulma"),
path("impressum/", views.imprint, name="imprint"),
path("terms-of-service/", views.terms_of_service, name="terms-of-service"),
path("datenschutz/", views.privacy, name="privacy"),
################
## Moderation ##
@@ -70,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 ##
###########
@@ -119,11 +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"),
path("styleguide-bulma", views.styleguide_bulma, name="styleguide-bulma"),
]

View File

@@ -2,8 +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, get_object_or_404
from django.shortcuts import render, redirect
from django.urls import reverse
from django.contrib.auth.decorators import login_required
from django.utils import translation
@@ -18,14 +17,11 @@ from notfellchen import settings
from fellchensammlung import logger
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
User, Location, AdoptionNoticeStatus, Subscriptions, CommentNotification, BaseNotification, RescueOrganization, \
Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, AdoptionNoticeNotification, \
ImportantLocation
Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, AdoptionNoticeNotification
from .forms import AdoptionNoticeForm, AdoptionNoticeFormWithDateWidget, ImageForm, ReportAdoptionNoticeForm, \
CommentForm, ReportCommentForm, AnimalForm, \
AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal, \
BulmaAdoptionNoticeForm
AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal
from .models import Language, Announcement
from .tools import i18n
from .tools.geo import GeoAPI, zoom_level_for_radius
from .tools.metrics import gather_metrics_data
from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
@@ -66,22 +62,6 @@ def index(request):
return render(request, 'fellchensammlung/index.html', context=context)
def index_bulma(request):
"""View function for home page of site."""
latest_adoption_list = AdoptionNotice.objects.filter(
adoptionnoticestatus__major_status=AdoptionNoticeStatus.ACTIVE).order_by("-created_at")
active_adoptions = [adoption for adoption in latest_adoption_list if adoption.is_active]
language_code = translation.get_language()
lang = Language.objects.get(languagecode=language_code)
active_announcements = Announcement.get_active_announcements(lang)
context = {"adoption_notices": active_adoptions[:5], "adoption_notices_map": active_adoptions,
"announcements": active_announcements}
Text.get_texts(["how_to", "introduction"], lang, context)
return render(request, 'fellchensammlung/bulma-index.html', context=context)
def change_language(request):
if request.method == 'POST':
language_code = request.POST.get('language')
@@ -99,11 +79,9 @@ def change_language(request):
return response
else:
return render(request, 'fellchensammlung/index.html')
else:
return render(request, 'fellchensammlung/index.html')
def adoption_notice_detail(request, adoption_notice_id, template=None):
def adoption_notice_detail(request, adoption_notice_id):
adoption_notice = AdoptionNotice.objects.get(id=adoption_notice_id)
if request.user.is_authenticated:
try:
@@ -155,20 +133,12 @@ def adoption_notice_detail(request, adoption_notice_id, template=None):
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,
"has_edit_permission": has_edit_permission, "is_subscribed": is_subscribed}
if template is not None:
return render(request, template, context=context)
else:
return render(request, 'fellchensammlung/details/detail_adoption_notice.html', context=context)
def adoption_notice_detail_bulma(request, adoption_notice_id):
return adoption_notice_detail(request, adoption_notice_id,
template='fellchensammlung/details/bulma-detail-adoption-notice.html')
return render(request, 'fellchensammlung/details/detail_adoption_notice.html', context=context)
@login_required()
@@ -203,31 +173,7 @@ def animal_detail(request, animal_id):
return render(request, 'fellchensammlung/details/detail_animal.html', context=context)
def search_important_locations(request, important_location_slug):
i_location = get_object_or_404(ImportantLocation, slug=important_location_slug)
search = Search()
search.search_from_predefined_i_location(i_location)
context = {"adoption_notices": search.get_adoption_notices(),
"search_form": search.search_form,
"place_not_found": search.place_not_found,
"subscribed_search": None,
"searched": False,
"adoption_notices_map": AdoptionNotice.get_active_ANs(),
"map_center": search.position,
"search_center": search.position,
"map_pins": [search],
"location": search.location,
"search_radius": search.max_distance,
"zoom_level": zoom_level_for_radius(search.max_distance),
"geocoding_api_url": settings.GEOCODING_API_URL, }
return render(request, 'fellchensammlung/search.html', context=context)
def search_bulma(request):
return search(request, "fellchensammlung/bulma-search.html")
def search(request, templatename="fellchensammlung/search.html"):
def search(request):
# A user just visiting the search site did not search, only upon completing the search form a user has really
# searched. This will toggle the "subscribe" button
searched = False
@@ -266,7 +212,7 @@ def search(request, templatename="fellchensammlung/search.html"):
"search_radius": search.max_distance,
"zoom_level": zoom_level_for_radius(search.max_distance),
"geocoding_api_url": settings.GEOCODING_API_URL, }
return render(request, templatename, context=context)
return render(request, 'fellchensammlung/search.html', context=context)
@login_required
@@ -311,51 +257,6 @@ def add_adoption_notice(request):
return render(request, 'fellchensammlung/forms/form_add_adoption.html', {'form': form})
@login_required
def add_adoption_notice_bulma(request):
if request.method == 'POST':
print("dada")
form = AdoptionNoticeFormWithDateWidgetAutoAnimal(request.POST)
if form.is_valid():
print("dodo")
an_instance = form.save(commit=False)
an_instance.owner = request.user
if request.user.trust_level >= TrustLevel.MODERATOR:
an_instance.set_active()
else:
an_instance.set_unchecked()
# Get the species and number of animals from the form
species = form.cleaned_data["species"]
sex = form.cleaned_data["sex"]
num_animals = form.cleaned_data["num_animals"]
date_of_birth = form.cleaned_data["date_of_birth"]
for i in range(0, num_animals):
Animal.objects.create(owner=request.user,
name=f"{species} {i + 1}", adoption_notice=an_instance, species=species, sex=sex,
date_of_birth=date_of_birth)
"""Log"""
Log.objects.create(user=request.user, action="add_adoption_notice",
text=f"{request.user} hat Vermittlung {an_instance.pk} hinzugefügt")
"""Spin up a task that adds the location and notifies search subscribers"""
post_adoption_notice_save.delay(an_instance.id)
"""Subscriptions"""
# Automatically subscribe user that created AN to AN
Subscriptions.objects.create(owner=request.user, adoption_notice=an_instance)
return redirect(reverse("adoption-notice-detail-bulma", args=[an_instance.pk]))
else:
print(form.errors)
else:
form = AdoptionNoticeFormWithDateWidgetAutoAnimal()
return render(request, 'fellchensammlung/forms/bulma-form-add-adoption.html', {'form': form})
@login_required
def adoption_notice_add_animal(request, adoption_notice_id):
# Only users that are mods or owners of the adoption notice are allowed to add to it
@@ -464,7 +365,15 @@ def animal_edit(request, animal_id):
def about(request):
rules = Rule.objects.all()
legal = i18n.get_texts_by_language(["terms_of_service", "privacy_statement", "imprint", "about_us", "faq"])
language_code = translation.get_language()
lang = Language.objects.get(languagecode=language_code)
legal = {}
for text_code in ["terms_of_service", "privacy_statement", "imprint", "about_us", "faq"]:
try:
legal[text_code] = Text.objects.get(text_code=text_code, language=lang, )
except Text.DoesNotExist:
legal[text_code] = None
context = {"rules": rules, }
context.update(legal)
@@ -475,47 +384,6 @@ def about(request):
)
def about_bulma(request):
context = i18n.get_texts_by_language(["about_us", "faq"])
return render(
request,
"fellchensammlung/bulma-about.html",
context=context
)
def render_text(request, text):
context = {"text": text}
return render(
request,
"fellchensammlung/bulma-one-text.html",
context=context
)
def imprint(request):
text = i18n.get_text_by_language("imprint")
return render_text(request, text)
def privacy(request):
text = i18n.get_text_by_language("privacy_statement")
return render_text(request, text)
def terms_of_service(request):
text = i18n.get_text_by_language("terms_of_service")
rules = Rule.objects.all()
context = {"rules": rules, "text": text}
return render(
request,
"fellchensammlung/bulma-terms-of-service.html",
context=context
)
def report_adoption(request, adoption_notice_id):
"""
Form to report adoption notices
@@ -560,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)
@@ -637,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)
@@ -667,14 +532,10 @@ def updatequeue(request):
return render(request, 'fellchensammlung/updatequeue.html', context=context)
def map(request, templatename='fellchensammlung/map.html'):
def map(request):
adoption_notices = AdoptionNotice.get_active_ANs()
context = {"adoption_notices_map": adoption_notices}
return render(request, templatename, context=context)
def map_bulma(request):
return map(request, templatename='fellchensammlung/bulma-map.html')
return render(request, 'fellchensammlung/map.html', context=context)
def metrics(request):
@@ -771,28 +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)
def styleguide_bulma(request):
return render(request, 'fellchensammlung/bulma-styleguide.html')
@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",
@@ -176,7 +175,6 @@ INSTALLED_APPS = [
'rest_framework.authtoken',
'drf_spectacular',
'drf_spectacular_sidecar', # required for Django collectstatic discovery
'widget_tweaks'
]
MIDDLEWARE = [

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="">')