feat: Add search for rescue orgs
This commit is contained in:
@@ -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"))
|
||||
|
@@ -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):
|
||||
|
@@ -16,11 +16,26 @@
|
||||
{% block canonical_url %}{% host %}{% url 'rescue-organizations' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="block">
|
||||
<div style="height: 70vh">
|
||||
{% include "fellchensammlung/partials/partial-map.html" %}
|
||||
<div class="columns block">
|
||||
<div class=" column is-two-thirds">
|
||||
<div style="height: 70vh">
|
||||
{% include "fellchensammlung/partials/partial-map.html" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-one-third">
|
||||
<form method="post" autocomplete="off">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="longitude" maxlength="200" id="longitude">
|
||||
<input type="hidden" name="latitude" maxlength="200" id="latitude">
|
||||
<!--- https://docs.djangoproject.com/en/5.2/topics/forms/#reusable-form-templates -->
|
||||
{{ search_form }}
|
||||
<button class="button is-primary is-fullwidth" type="submit" value="search" name="search">
|
||||
<i class="fas fa-search fa-fw"></i> {% trans 'Suchen' %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
{% 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' %}</a>
|
||||
{% endif %}
|
||||
{% if rescue_organizations_to_list.has_next %}
|
||||
<a class="pagination-next" href="?page={{ rescue_organizations_to_list.next_page_number }}">{% trans 'Nächste' %}</a>
|
||||
<a class="pagination-next"
|
||||
href="?page={{ rescue_organizations_to_list.next_page_number }}">{% trans 'Nächste' %}</a>
|
||||
{% endif %}
|
||||
<ul class="pagination-list">
|
||||
{% for page in elided_page_range %}
|
||||
|
@@ -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
|
||||
|
@@ -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()
|
||||
|
@@ -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)
|
||||
|
||||
|
||||
|
@@ -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))
|
||||
|
||||
|
Reference in New Issue
Block a user