diff --git a/src/fellchensammlung/admin.py b/src/fellchensammlung/admin.py index a1b4a9c..621d6a8 100644 --- a/src/fellchensammlung/admin.py +++ b/src/fellchensammlung/admin.py @@ -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 + SpeciesSpecificURL, ImportantLocation from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \ Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, BaseNotification @@ -94,12 +94,14 @@ 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",) @@ -122,14 +124,26 @@ 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 + + +@admin.register(Location) +class LocationAdmin(admin.ModelAdmin): + search_fields = ("name__icontains", "city__icontains") + 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) diff --git a/src/fellchensammlung/migrations/0045_importantlocation.py b/src/fellchensammlung/migrations/0045_importantlocation.py new file mode 100644 index 0000000..24c1254 --- /dev/null +++ b/src/fellchensammlung/migrations/0045_importantlocation.py @@ -0,0 +1,23 @@ +# 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')), + ], + ), + ] diff --git a/src/fellchensammlung/migrations/0046_alter_importantlocation_location.py b/src/fellchensammlung/migrations/0046_alter_importantlocation_location.py new file mode 100644 index 0000000..8b9f78b --- /dev/null +++ b/src/fellchensammlung/migrations/0046_alter_importantlocation_location.py @@ -0,0 +1,19 @@ +# 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'), + ), + ] diff --git a/src/fellchensammlung/models.py b/src/fellchensammlung/models.py index 94e1457..3cf2319 100644 --- a/src/fellchensammlung/models.py +++ b/src/fellchensammlung/models.py @@ -3,6 +3,7 @@ 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 @@ -99,6 +100,12 @@ 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") diff --git a/src/fellchensammlung/tools/search.py b/src/fellchensammlung/tools/search.py index d171a11..49237da 100644 --- a/src/fellchensammlung/tools/search.py +++ b/src/fellchensammlung/tools/search.py @@ -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,7 +47,6 @@ 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=}" @@ -93,7 +92,6 @@ class Search: return False return True - def get_adoption_notices(self): adoptions = AdoptionNotice.objects.order_by("-created_at") # Filter for active adoption notices @@ -118,13 +116,21 @@ 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): diff --git a/src/fellchensammlung/urls.py b/src/fellchensammlung/urls.py index 5dfb91d..8005610 100644 --- a/src/fellchensammlung/urls.py +++ b/src/fellchensammlung/urls.py @@ -45,6 +45,7 @@ urlpatterns = [ # ex: /search/ path("suchen/", views.search, name="search"), + path("suchen/", views.search_important_locations, name="search-by-location"), # ex: /map/ path("map/", views.map, name="map"), # ex: /vermitteln/ diff --git a/src/fellchensammlung/views.py b/src/fellchensammlung/views.py index f9ae554..80b85e1 100644 --- a/src/fellchensammlung/views.py +++ b/src/fellchensammlung/views.py @@ -18,7 +18,8 @@ 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 + Species, Log, Timestamp, TrustLevel, SexChoicesWithAll, SearchSubscription, AdoptionNoticeNotification, \ + ImportantLocation from .forms import AdoptionNoticeForm, AdoptionNoticeFormWithDateWidget, ImageForm, ReportAdoptionNoticeForm, \ CommentForm, ReportCommentForm, AnimalForm, \ AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal @@ -200,6 +201,26 @@ 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 = ImportantLocation.objects.get(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(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