feat: Add basic name based search for rescue orgs

This commit is contained in:
2025-11-30 09:41:33 +01:00
parent f2bad5f171
commit 9a3cbffa42
8 changed files with 86 additions and 2 deletions

View File

@@ -145,10 +145,15 @@ class AnimalGetSerializer(serializers.ModelSerializer):
class RescueOrganizationSerializer(serializers.ModelSerializer): class RescueOrganizationSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField()
class Meta: class Meta:
model = RescueOrganization model = RescueOrganization
exclude = ["internal_comment", "allows_using_materials"] exclude = ["internal_comment", "allows_using_materials"]
def get_url(self, obj):
return obj.get_absolute_url()
class ImageCreateSerializer(serializers.ModelSerializer): class ImageCreateSerializer(serializers.ModelSerializer):
@staticmethod @staticmethod

View File

@@ -2,10 +2,11 @@ from django.urls import path
from .views import ( from .views import (
AdoptionNoticeApiView, AdoptionNoticeApiView,
AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView, LocationApiView, AnimalApiView, RescueOrganizationApiView, AddImageApiView, SpeciesApiView, LocationApiView,
AdoptionNoticeGeoJSONView, RescueOrgGeoJSONView, AdoptionNoticePerOrgApiView AdoptionNoticeGeoJSONView, RescueOrgGeoJSONView, AdoptionNoticePerOrgApiView, index
) )
urlpatterns = [ urlpatterns = [
path("", index, name="api-base-url"),
path("adoption_notice", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-list"), path("adoption_notice", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-list"),
path("adoption_notice.geojson", AdoptionNoticeGeoJSONView.as_view(), name="api-adoption-notice-list-geojson"), path("adoption_notice.geojson", AdoptionNoticeGeoJSONView.as_view(), name="api-adoption-notice-list-geojson"),
path("adoption_notice/<int:id>/", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-detail"), path("adoption_notice/<int:id>/", AdoptionNoticeApiView.as_view(), name="api-adoption-notice-detail"),

View File

@@ -1,4 +1,6 @@
from django.db.models import Q from django.db.models import Q
from django.shortcuts import redirect
from django.urls import reverse
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from rest_framework.generics import ListAPIView from rest_framework.generics import ListAPIView
@@ -450,3 +452,7 @@ class AdoptionNoticePerOrgApiView(APIView):
adoption_notices = temporary_an_storage adoption_notices = temporary_an_storage
serializer = AdoptionNoticeSerializer(adoption_notices, many=True, context={"request": request}) serializer = AdoptionNoticeSerializer(adoption_notices, many=True, context={"request": request})
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def index(request):
return redirect(reverse("swagger-ui"))

View File

@@ -186,6 +186,11 @@ class RescueOrgSearchForm(forms.Form):
label=_("Suchradius")) label=_("Suchradius"))
class RescueOrgSearchByNameForm(forms.Form):
template_name = "fellchensammlung/forms/form_snippets.html"
name = forms.CharField(max_length=100, label=_("Name der Organisation"), required=False)
class CloseAdoptionNoticeForm(forms.ModelForm): class CloseAdoptionNoticeForm(forms.ModelForm):
template_name = "fellchensammlung/forms/form_snippets.html" template_name = "fellchensammlung/forms/form_snippets.html"

View File

@@ -33,6 +33,11 @@
<i class="fas fa-search fa-fw"></i> {% trans 'Suchen' %} <i class="fas fa-search fa-fw"></i> {% trans 'Suchen' %}
</button> </button>
</form> </form>
<hr>
<form method="post" autocomplete="off">
{% csrf_token %}
{{ org_name_search_form }}
</form>
</div> </div>
{% endif %} {% endif %}
</div> </div>
@@ -69,4 +74,57 @@
{% endfor %} {% endfor %}
</ul> </ul>
</nav> </nav>
<script>
$(document).ready(function () {
const $nameInput = $("#id_name");
$nameInput.wrap("<div class='dropdown' id='location-result-list'></div>");
const dropdown = $("#location-result-list");
$nameInput.wrap("<div class='dropdown-trigger'></div>");
$("<div class='dropdown-content' id='results'></div>").insertAfter($nameInput);
const $resultsList = $("#results");
$resultsList.wrap("<div class='dropdown-menu'></div>");
$nameInput.on("input", function () {
const query = $.trim($nameInput.val());
if (query.length < 3) {
dropdown.removeClass("is-active");
return;
}
$.ajax({
url: "{% api_base_url %}organizations/",
data: {
search: query
},
method: "GET",
dataType: "json",
success: function (data) {
$resultsList.empty();
dropdown.addClass("is-active");
if (data) {
const orgs = data.slice(0, 5);
$.each(orgs, function (index, org) {
const $listItem = $("<a>")
.addClass("dropdown-item")
.addClass("result-item")
.attr('href', org.url)
.text(org.name);
$resultsList.append($listItem);
});
}
},
error: function () {
$resultsList.html('<li class="result-item">{% trans 'Error fetching data. Please try again.' %}</li>');
}
});
});
});
</script>
{% endblock %} {% endblock %}

View File

@@ -5,6 +5,7 @@ from django.template.defaultfilters import stringfilter
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from urllib.parse import urlparse from urllib.parse import urlparse
from django.utils import timezone from django.utils import timezone
from django.urls import reverse
from fellchensammlung.tools.misc import time_since_as_hr_string from fellchensammlung.tools.misc import time_since_as_hr_string
from notfellchen import settings from notfellchen import settings
@@ -54,6 +55,11 @@ def get_oxitraffic_script_if_enabled():
return "" return ""
@register.simple_tag
def api_base_url():
return reverse("api-base-url")
@register.filter @register.filter
@stringfilter @stringfilter
def pointdecimal(value): def pointdecimal(value):

View File

@@ -173,6 +173,7 @@ class AdoptionNoticeSearch:
class RescueOrgSearch: class RescueOrgSearch:
def __init__(self, request): def __init__(self, request):
self.name = 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
@@ -229,6 +230,7 @@ class RescueOrgSearch:
return fitting_rescue_orgs return fitting_rescue_orgs
def rescue_org_search_from_request(self, request): def rescue_org_search_from_request(self, request):
# Only search if request method is get with action search
if request.method == 'GET' and request.GET.get("action", False) == "search": if request.method == 'GET' and request.GET.get("action", False) == "search":
self.search_form = RescueOrgSearchForm(request.GET) self.search_form = RescueOrgSearchForm(request.GET)
self.search_form.is_valid() self.search_form.is_valid()

View File

@@ -25,7 +25,7 @@ from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, Moderatio
ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices, SocialMediaPost ImportantLocation, SpeciesSpecificURL, NotificationTypeChoices, SocialMediaPost
from .forms import AdoptionNoticeForm, ImageForm, ReportAdoptionNoticeForm, \ from .forms import AdoptionNoticeForm, ImageForm, ReportAdoptionNoticeForm, \
CommentForm, ReportCommentForm, AnimalForm, AdoptionNoticeFormAutoAnimal, SpeciesURLForm, RescueOrgInternalComment, \ CommentForm, ReportCommentForm, AnimalForm, AdoptionNoticeFormAutoAnimal, SpeciesURLForm, RescueOrgInternalComment, \
UpdateRescueOrgRegularCheckStatus, UserModCommentForm, CloseAdoptionNoticeForm UpdateRescueOrgRegularCheckStatus, UserModCommentForm, CloseAdoptionNoticeForm, RescueOrgSearchByNameForm
from .models import Language, Announcement from .models import Language, Announcement
from .tools import i18n, img from .tools import i18n, img
from .tools.fedi import post_an_to_fedi from .tools.fedi import post_an_to_fedi
@@ -851,6 +851,7 @@ def list_rescue_organizations(request, species=None, template='fellchensammlung/
context = {"rescue_organizations_to_list": rescue_organizations_to_list, context = {"rescue_organizations_to_list": rescue_organizations_to_list,
"show_rescue_orgs": True, "show_rescue_orgs": True,
"elided_page_range": paginator.get_elided_page_range(page_number, on_each_side=2, on_ends=1), "elided_page_range": paginator.get_elided_page_range(page_number, on_each_side=2, on_ends=1),
"org_name_search_form": RescueOrgSearchByNameForm(),
} }
if org_search: if org_search:
additional_context = { additional_context = {