diff --git a/src/fellchensammlung/forms.py b/src/fellchensammlung/forms.py index fd40827..aed247e 100644 --- a/src/fellchensammlung/forms.py +++ b/src/fellchensammlung/forms.py @@ -147,3 +147,11 @@ class AdoptionNoticeSearchForm(forms.Form): max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED, label=_("Suchradius")) location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False) + + +class RescueOrgSearchForm(forms.Form): + template_name = "fellchensammlung/forms/form_snippets.html" + + location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False) + max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED, + label=_("Suchradius")) diff --git a/src/fellchensammlung/models.py b/src/fellchensammlung/models.py index e222457..a437b04 100644 --- a/src/fellchensammlung/models.py +++ b/src/fellchensammlung/models.py @@ -272,6 +272,14 @@ class RescueOrganization(models.Model): def child_organizations(self): return RescueOrganization.objects.filter(parent_org=self) + def in_distance(self, position, max_distance, unknown_true=True): + """ + Returns a boolean indicating if the Location of the adoption notice is within a given distance to the position + + If the location is none, we by default return that the location is within the given distance + """ + return geo.object_in_distance(self, position, max_distance, unknown_true) + # Admins can perform all actions and have the highest trust associated with them # Moderators can make moderation decisions regarding the deletion of content @@ -500,11 +508,7 @@ class AdoptionNotice(models.Model): If the location is none, we by default return that the location is within the given distance """ - if unknown_true and self.position is None: - return True - - distance = geo.calculate_distance_between_coordinates(self.position, position) - return distance < max_distance + return geo.object_in_distance(self, position, max_distance, unknown_true) @property def is_active(self): diff --git a/src/fellchensammlung/templates/fellchensammlung/animal-shelters.html b/src/fellchensammlung/templates/fellchensammlung/animal-shelters.html index 2edf8d8..149cfe3 100644 --- a/src/fellchensammlung/templates/fellchensammlung/animal-shelters.html +++ b/src/fellchensammlung/templates/fellchensammlung/animal-shelters.html @@ -16,11 +16,26 @@ {% block canonical_url %}{% host %}{% url 'rescue-organizations' %}{% endblock %} {% block content %} -
-
- {% include "fellchensammlung/partials/partial-map.html" %} +
+
+
+ {% include "fellchensammlung/partials/partial-map.html" %} +
+
+
+
+ {% csrf_token %} + + + + {{ search_form }} + +
+
{% with rescue_organizations=rescue_organizations_to_list %} {% include "fellchensammlung/lists/list-animal-shelters.html" %} @@ -32,7 +47,8 @@ href="?page={{ rescue_organizations_to_list.previous_page_number }}">{% trans 'Vorherige' %} {% endif %} {% if rescue_organizations_to_list.has_next %} - {% trans 'Nächste' %} + {% trans 'Nächste' %} {% endif %}
    {% for page in elided_page_range %} diff --git a/src/fellchensammlung/tools/geo.py b/src/fellchensammlung/tools/geo.py index 4674f5e..cb86711 100644 --- a/src/fellchensammlung/tools/geo.py +++ b/src/fellchensammlung/tools/geo.py @@ -53,6 +53,19 @@ def calculate_distance_between_coordinates(position1, position2): return distance_in_km +def object_in_distance(obj, position, max_distance, unknown_true=True): + """ + Returns a boolean indicating if the Location of the object is within a given distance to the position + + If the location is none, we by default return that the location is within the given distance + """ + if unknown_true and obj.position is None: + return True + + distance = calculate_distance_between_coordinates(obj.position, position) + return distance < max_distance + + class ResponseMock: content = b'[{"place_id":138181499,"licence":"Data \xc2\xa9 OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"relation","osm_id":1247237,"lat":"48.4949904","lon":"9.040330235970146","category":"boundary","type":"postal_code","place_rank":21, "importance":0.12006895017929346,"addresstype":"postcode","name":"72072","display_name":"72072, Derendingen, T\xc3\xbcbingen, Landkreis T\xc3\xbcbingen, Baden-W\xc3\xbcrttemberg, Deutschland", "boundingbox":["48.4949404","48.4950404","9.0402802","9.0403802"]}]' status_code = 200 diff --git a/src/fellchensammlung/tools/search.py b/src/fellchensammlung/tools/search.py index ad36b4c..f97cd39 100644 --- a/src/fellchensammlung/tools/search.py +++ b/src/fellchensammlung/tools/search.py @@ -2,9 +2,9 @@ import logging from django.utils.translation import gettext_lazy as _ from .geo import LocationProxy, Position -from ..forms import AdoptionNoticeSearchForm +from ..forms import AdoptionNoticeSearchForm, RescueOrgSearchForm from ..models import SearchSubscription, AdoptionNotice, SexChoicesWithAll, Location, \ - Notification, NotificationTypeChoices + Notification, NotificationTypeChoices, RescueOrganization def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: bool = True): @@ -18,7 +18,7 @@ def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: b return for search_subscription in SearchSubscription.objects.all(): logging.debug(f"Search subscription {search_subscription} found.") - search = Search(search_subscription=search_subscription) + search = AdoptionNoticeSearch(search_subscription=search_subscription) if search.adoption_notice_fits_search(adoption_notice): notification_text = f"{_('Zu deiner Suche')} {search_subscription} wurde eine neue Vermittlung gefunden" Notification.objects.create(user_to_notify=search_subscription.owner, @@ -33,7 +33,7 @@ def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: b logging.info(f"Subscribers for AN {adoption_notice.pk} have been notified\n") -class Search: +class AdoptionNoticeSearch: def __init__(self, request=None, search_subscription=None): self.sex = None self.area_search = None @@ -45,7 +45,7 @@ class Search: self.location_string = None if request: - self.search_from_request(request) + self.adoption_notice_search_from_request(request) elif search_subscription: self.search_from_search_subscription(search_subscription) @@ -103,7 +103,7 @@ class Search: return adoptions - def search_from_request(self, request): + def adoption_notice_search_from_request(self, request): if request.method == 'POST': self.search_form = AdoptionNoticeSearchForm(request.POST) self.search_form.is_valid() @@ -157,3 +157,75 @@ class Search: return False else: return True + + +class RescueOrgSearch: + def __init__(self, request): + self.area_search = None + self.max_distance = None + 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 + self.location_string = None + + self.rescue_org_search_from_request(request) + + def __str__(self): + return f"{_('Suche')}: {self.location=}, {self.area_search=}, {self.max_distance=}" + + def __eq__(self, other): + """ + Custom equals that also supports SearchSubscriptions + + Only allowed to be called for located subscriptions + """ + # If both locations are empty check only the max distance + if self.location is None and other.location is None: + return self.max_distance == other.max_distance + # If one location is empty and the other is not, they are not equal + elif self.location is not None and other.location is None or self.location is None and other.location is not None: + return False + return self.location == other.location and self.max_distance == other.max_distance + + def _locate(self): + try: + self.location = LocationProxy(self.location_string) + except ValueError: + self.place_not_found = True + + @property + def position(self): + if self.area_search and not self.place_not_found: + return Position(latitude=self.location.latitude, longitude=self.location.longitude) + else: + return None + + def rescue_org_fits_search(self, rescue_org: RescueOrganization): + # make sure it's an area search and the place is found to check location + if self.area_search and not self.place_not_found: + # If adoption notice is in not in search distance, return false + if not rescue_org.in_distance(self.location.position, self.max_distance): + logging.debug("Area mismatch") + return False + return True + + def get_rescue_orgs(self): + rescue_orgs = RescueOrganization.objects.all() + fitting_rescue_orgs = [rescue_org for rescue_org in rescue_orgs if self.rescue_org_fits_search(rescue_org)] + + return fitting_rescue_orgs + + def rescue_org_search_from_request(self, request): + if request.method == 'POST': + self.search_form = RescueOrgSearchForm(request.POST) + self.search_form.is_valid() + + if self.search_form.cleaned_data["location_string"] != "" and self.search_form.cleaned_data[ + "max_distance"] != "": + self.area_search = True + self.location_string = self.search_form.cleaned_data["location_string"] + self.max_distance = int(self.search_form.cleaned_data["max_distance"]) + self._locate() + else: + self.search_form = RescueOrgSearchForm() diff --git a/src/fellchensammlung/views.py b/src/fellchensammlung/views.py index 19a8c88..20fa6a1 100644 --- a/src/fellchensammlung/views.py +++ b/src/fellchensammlung/views.py @@ -36,7 +36,7 @@ from .tools.admin import clean_locations, get_unchecked_adoption_notices, deacti from .tasks import post_adoption_notice_save from rest_framework.authtoken.models import Token -from .tools.search import Search +from .tools.search import AdoptionNoticeSearch, RescueOrgSearch def user_is_trust_level_or_above(user, trust_level=TrustLevel.MODERATOR): @@ -200,7 +200,7 @@ def adoption_notice_edit(request, adoption_notice_id): def search_important_locations(request, important_location_slug): i_location = get_object_or_404(ImportantLocation, slug=important_location_slug) - search = Search() + search = AdoptionNoticeSearch() search.search_from_predefined_i_location(i_location) site_title = _("Ratten in %(location_name)s") % {"location_name": i_location.name} @@ -231,8 +231,8 @@ def search(request, templatename="fellchensammlung/search.html"): # 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 - search = Search() - search.search_from_request(request) + search = AdoptionNoticeSearch() + search.adoption_notice_search_from_request(request) if request.method == 'POST': searched = True if "subscribe_to_search" in request.POST: @@ -749,7 +749,10 @@ def external_site_warning(request, template_name='fellchensammlung/external-site def list_rescue_organizations(request, species=None, template='fellchensammlung/animal-shelters.html'): if species is None: - rescue_organizations = RescueOrganization.objects.all() + # rescue_organizations = RescueOrganization.objects.all() + + org_search = RescueOrgSearch(request) + rescue_organizations = org_search.get_rescue_orgs() else: rescue_organizations = RescueOrganization.objects.filter(specializations=species) @@ -767,7 +770,8 @@ def list_rescue_organizations(request, species=None, template='fellchensammlung/ rescue_organizations_to_list = paginator.get_page(page_number) context = {"rescue_organizations_to_list": rescue_organizations_to_list, "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), + "search_form": org_search.search_form, } return render(request, template, context=context) diff --git a/src/tests/test_search.py b/src/tests/test_search.py index ce95116..73f6a2f 100644 --- a/src/tests/test_search.py +++ b/src/tests/test_search.py @@ -8,7 +8,7 @@ from model_bakery import baker from fellchensammlung.tools.geo import LocationProxy from fellchensammlung.tools.model_helpers import NotificationTypeChoices -from fellchensammlung.tools.search import Search, notify_search_subscribers +from fellchensammlung.tools.search import AdoptionNoticeSearch, notify_search_subscribers class TestSearch(TestCase): @@ -72,7 +72,7 @@ class TestSearch(TestCase): sex=SexChoicesWithAll.ALL, max_distance=100 ) - search1 = Search() + search1 = AdoptionNoticeSearch() search1.search_position = LocationProxy("Stuttgart").position search1.max_distance = 100 search1.area_search = True @@ -83,11 +83,11 @@ class TestSearch(TestCase): self.assertEqual(search_subscription1, search1) def test_adoption_notice_fits_search(self): - search1 = Search(search_subscription=self.subscription1) + search1 = AdoptionNoticeSearch(search_subscription=self.subscription1) self.assertTrue(search1.adoption_notice_fits_search(self.adoption1)) self.assertFalse(search1.adoption_notice_fits_search(self.adoption2)) - search2 = Search(search_subscription=self.subscription2) + search2 = AdoptionNoticeSearch(search_subscription=self.subscription2) self.assertFalse(search2.adoption_notice_fits_search(self.adoption1)) self.assertTrue(search2.adoption_notice_fits_search(self.adoption2))