feat: Add important locations to search around

This commit is contained in:
moanos [he/him] 2025-04-27 14:06:17 +02:00
parent f387930dee
commit bb14a346cb
7 changed files with 100 additions and 9 deletions

View File

@ -7,7 +7,7 @@ from django.urls import reverse
from django.utils.http import urlencode from django.utils.http import urlencode
from .models import User, Language, Text, ReportComment, ReportAdoptionNotice, Log, Timestamp, SearchSubscription, \ 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, \ from .models import Animal, Species, RescueOrganization, AdoptionNotice, Location, Rule, Image, ModerationAction, \
Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, BaseNotification Comment, Report, Announcement, AdoptionNoticeStatus, User, Subscriptions, BaseNotification
@ -94,12 +94,14 @@ class ReportAdoptionNoticeAdmin(admin.ModelAdmin):
reported_content_link.short_description = "Reported Content" reported_content_link.short_description = "Reported Content"
class SpeciesSpecificURLInline(admin.StackedInline): class SpeciesSpecificURLInline(admin.StackedInline):
model = SpeciesSpecificURL model = SpeciesSpecificURL
@admin.register(RescueOrganization) @admin.register(RescueOrganization)
class RescueOrganizationAdmin(admin.ModelAdmin): 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_display = ("name", "trusted", "allows_using_materials", "website")
list_filter = ("allows_using_materials", "trusted",) list_filter = ("allows_using_materials", "trusted",)
@ -122,14 +124,26 @@ class CommentAdmin(admin.ModelAdmin):
class BaseNotificationAdmin(admin.ModelAdmin): class BaseNotificationAdmin(admin.ModelAdmin):
list_filter = ("user", "read") list_filter = ("user", "read")
@admin.register(SearchSubscription) @admin.register(SearchSubscription)
class SearchSubscriptionAdmin(admin.ModelAdmin): class SearchSubscriptionAdmin(admin.ModelAdmin):
list_filter = ("owner",) 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(Animal)
admin.site.register(Species) admin.site.register(Species)
admin.site.register(Location)
admin.site.register(Rule) admin.site.register(Rule)
admin.site.register(Image) admin.site.register(Image)
admin.site.register(ModerationAction) admin.site.register(ModerationAction)

View File

@ -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')),
],
),
]

View File

@ -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'),
),
]

View File

@ -3,6 +3,7 @@ from random import choices
from tabnanny import verbose from tabnanny import verbose
from django.db import models from django.db import models
from django.template.defaultfilters import slugify
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone from django.utils import timezone
@ -99,6 +100,12 @@ class Location(models.Model):
instance.save() 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): class ExternalSourceChoices(models.TextChoices):
OSM = "OSM", _("Open Street Map") OSM = "OSM", _("Open Street Map")

View File

@ -6,7 +6,7 @@ from ..forms import AdoptionNoticeSearchForm
from ..models import SearchSubscription, AdoptionNotice, AdoptionNoticeNotification, SexChoicesWithAll, Location 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. 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. 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.sex = None
self.area_search = None self.area_search = None
self.max_distance = 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.place_not_found = False # Indicates that a location was given but could not be geocoded
self.search_form = None self.search_form = None
# Either place_id or location string must be set for area search # Either place_id or location string must be set for area search
@ -47,7 +47,6 @@ class Search:
elif search_subscription: elif search_subscription:
self.search_from_search_subscription(search_subscription) self.search_from_search_subscription(search_subscription)
def __str__(self): def __str__(self):
return f"Search: {self.sex=}, {self.location=}, {self.area_search=}, {self.max_distance=}" return f"Search: {self.sex=}, {self.location=}, {self.area_search=}, {self.max_distance=}"
@ -93,7 +92,6 @@ class Search:
return False return False
return True return True
def get_adoption_notices(self): def get_adoption_notices(self):
adoptions = AdoptionNotice.objects.order_by("-created_at") adoptions = AdoptionNotice.objects.order_by("-created_at")
# Filter for active adoption notices # Filter for active adoption notices
@ -118,13 +116,21 @@ class Search:
else: else:
self.search_form = AdoptionNoticeSearchForm() 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): def search_from_search_subscription(self, search_subscription: SearchSubscription):
self.sex = search_subscription.sex self.sex = search_subscription.sex
self.location = search_subscription.location self.location = search_subscription.location
self.area_search = True self.area_search = True
self.max_distance = search_subscription.max_distance self.max_distance = search_subscription.max_distance
def subscribe(self, user): def subscribe(self, user):
logging.info(f"{user} subscribed to search") logging.info(f"{user} subscribed to search")
if isinstance(self.location, LocationProxy): if isinstance(self.location, LocationProxy):

View File

@ -45,6 +45,7 @@ urlpatterns = [
# ex: /search/ # ex: /search/
path("suchen/", views.search, name="search"), path("suchen/", views.search, name="search"),
path("suchen/<slug:important_location_slug>", views.search_important_locations, name="search-by-location"),
# ex: /map/ # ex: /map/
path("map/", views.map, name="map"), path("map/", views.map, name="map"),
# ex: /vermitteln/ # ex: /vermitteln/

View File

@ -18,7 +18,8 @@ from notfellchen import settings
from fellchensammlung import logger from fellchensammlung import logger
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \ from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
User, Location, AdoptionNoticeStatus, Subscriptions, CommentNotification, BaseNotification, RescueOrganization, \ 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, \ from .forms import AdoptionNoticeForm, AdoptionNoticeFormWithDateWidget, ImageForm, ReportAdoptionNoticeForm, \
CommentForm, ReportCommentForm, AnimalForm, \ CommentForm, ReportCommentForm, AnimalForm, \
AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal
@ -200,6 +201,26 @@ def animal_detail(request, animal_id):
return render(request, 'fellchensammlung/details/detail_animal.html', context=context) 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): def search(request):
# A user just visiting the search site did not search, only upon completing the search form a user has really # 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. This will toggle the "subscribe" button