feat: Auto-add location to adoption notice

This commit is contained in:
moanos [he/him] 2024-05-31 10:58:57 +02:00
parent c8f65c9c54
commit 2a9dc337d2
6 changed files with 123 additions and 23 deletions

View File

@ -40,6 +40,7 @@ class AdoptionNoticeForm(forms.ModelForm):
'name', 'name',
'group_only', 'group_only',
'searching_since', 'searching_since',
'location_string',
'description', 'description',
'further_information', 'further_information',
), ),
@ -47,7 +48,7 @@ class AdoptionNoticeForm(forms.ModelForm):
class Meta: class Meta:
model = AdoptionNotice model = AdoptionNotice
fields = ['name', "group_only", "further_information", "description", "searching_since"] fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string"]
class AnimalForm(forms.ModelForm): class AnimalForm(forms.ModelForm):

View File

@ -0,0 +1,81 @@
# Generated by Django 5.0.6 on 2024-06-05 13:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("fellchensammlung", "0006_announcement_type"),
]
operations = [
migrations.RemoveField(
model_name="location",
name="country",
),
migrations.RemoveField(
model_name="location",
name="description",
),
migrations.RemoveField(
model_name="location",
name="postcode",
),
migrations.AddField(
model_name="adoptionnotice",
name="location",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="fellchensammlung.location",
),
),
migrations.AddField(
model_name="adoptionnotice",
name="location_string",
field=models.CharField(
default="72072", max_length=200, verbose_name="Ortsangabe"
),
preserve_default=False,
),
migrations.AddField(
model_name="location",
name="latitude",
field=models.FloatField(default=47),
preserve_default=False,
),
migrations.AddField(
model_name="location",
name="longitude",
field=models.FloatField(default=9),
preserve_default=False,
),
migrations.AddField(
model_name="location",
name="osm_id",
field=models.IntegerField(default=1),
preserve_default=False,
),
migrations.AddField(
model_name="location",
name="place_id",
field=models.IntegerField(default=1),
preserve_default=False,
),
migrations.AddField(
model_name="rescueorganization",
name="location_string",
field=models.CharField(
default="72072", max_length=200, verbose_name="Ort der Organisation"
),
preserve_default=False,
),
migrations.AlterField(
model_name="location",
name="name",
field=models.CharField(max_length=2000),
),
]

View File

@ -66,19 +66,11 @@ class Location(models.Model):
def __str__(self): def __str__(self):
return f"{self.name}" return f"{self.name}"
GERMANY = "DE" place_id = models.IntegerField()
AUSTRIA = "AT" osm_id = models.IntegerField()
SWITZERLAND = "CH" latitude = models.FloatField()
COUNTRIES_CHOICES = { longitude = models.FloatField()
GERMANY: "Germany", name = models.CharField(max_length=2000)
AUSTRIA: "Austria",
SWITZERLAND: "Switzerland"
}
name = models.CharField(max_length=200)
postcode = models.CharField(max_length=200)
country = models.CharField(max_length=20, choices=COUNTRIES_CHOICES)
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
class RescueOrganization(models.Model): class RescueOrganization(models.Model):
@ -87,6 +79,7 @@ class RescueOrganization(models.Model):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig')) trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
location_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation"))
location = models.ForeignKey(Location, on_delete=models.PROTECT) location = models.ForeignKey(Location, on_delete=models.PROTECT)
instagram = models.URLField(null=True, blank=True, verbose_name=_('Instagram Profil')) instagram = models.URLField(null=True, blank=True, verbose_name=_('Instagram Profil'))
facebook = models.URLField(null=True, blank=True, verbose_name=_('Facebook Profil')) facebook = models.URLField(null=True, blank=True, verbose_name=_('Facebook Profil'))
@ -112,6 +105,8 @@ class AdoptionNotice(models.Model):
further_information = models.URLField(null=True, blank=True, verbose_name=_('Link zu mehr Informationen')) further_information = models.URLField(null=True, blank=True, verbose_name=_('Link zu mehr Informationen'))
group_only = models.BooleanField(default=False, verbose_name=_('Ausschließlich Gruppenadoption')) group_only = models.BooleanField(default=False, verbose_name=_('Ausschließlich Gruppenadoption'))
photos = models.ManyToManyField(Image, blank=True) photos = models.ManyToManyField(Image, blank=True)
location_string = models.CharField(max_length=200, verbose_name=_("Ortsangabe"))
location = models.ForeignKey(Location, blank=True, null=True, on_delete=models.SET_NULL,)
@property @property
def animals(self): def animals(self):

View File

@ -1,6 +1,7 @@
import requests import requests
import json import json
from notfellchen import __version__ as nf_version from notfellchen import __version__ as nf_version
from fellchensammlung.models import Location
from math import radians, sqrt, sin, cos, atan2 from math import radians, sqrt, sin, cos, atan2
@ -21,13 +22,15 @@ def calculate_distance_between_coordinates(position1, position2):
distance_lat = radians(latitude2 - latitude1) distance_lat = radians(latitude2 - latitude1)
distance_long = radians(longitude2 - longitude1) distance_long = radians(longitude2 - longitude1)
a = pow(sin(distance_lat / 2), 2) + cos(radians(latitude1)) * cos(radians(latitude2)) * pow(sin(distance_long / 2), 2) a = pow(sin(distance_lat / 2), 2) + cos(radians(latitude1)) * cos(radians(latitude2)) * pow(sin(distance_long / 2),
2)
c = 2 * atan2(sqrt(a), sqrt(1 - a)) c = 2 * atan2(sqrt(a), sqrt(1 - a))
distance_in_km = earth_radius_km * c distance_in_km = earth_radius_km * c
return distance_in_km return distance_in_km
class ResponseMock: 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"]}]' 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 status_code = 200
@ -43,21 +46,33 @@ class RequestMock:
class GeoAPI: class GeoAPI:
api_url = "https://nominatim.openstreetmap.org/search"
headers = {
'User-Agent': f"Notfellchen {nf_version}",
'From': 'info@notfellchen.org' # This is another valid field
}
def __init__(self, debug=True): def __init__(self, debug=True):
self.api_url = "https://nominatim.openstreetmap.org/search"
if debug: if debug:
self.requests = RequestMock self.requests = RequestMock
else: else:
self.requests = requests self.requests = requests
def get_coordinates_from_postcode(self, postcode): def get_coordinates_from_postcode(self, postcode):
headers = { result = self.requests.get(self.api_url, {"q": postcode, "format": "jsonv2"}, headers=self.headers).json()[0]
'User-Agent': f"Notfellchen {nf_version}",
'From': 'info@notfellchen.org' # This is another valid field
}
result = self.requests.get(self.api_url, {"q": postcode, "format": "jsonv2"}, headers=headers).json()[0]
return result["lat"], result["lon"] return result["lat"], result["lon"]
def get_location_from_string(self, location_string):
result = self.requests.get(self.api_url, {"q": location_string, "format": "jsonv2"}, headers=self.headers).json()[0]
location = Location.objects.create(
place_id=result["place_id"],
osm_id=result["osm_id"],
latitude=result["lat"],
longitude=result["lon"],
name=result["name"],
)
return location
if __name__ == "__main__": if __name__ == "__main__":
geo = GeoAPI(debug=True) geo = GeoAPI(debug=True)

View File

@ -27,7 +27,7 @@ urlpatterns = [
# ex: /search/ # ex: /search/
path("suchen/", views.search, name="search"), path("suchen/", views.search, name="search"),
# ex: /vermitteln/ # ex: /vermitteln/
path("vermitteln/", views.add_adoption, name="add-adoption"), path("vermitteln/", views.add_adoption_notice, name="add-adoption"),
path("ueber-uns/", views.about, name="about"), path("ueber-uns/", views.about, name="about"),

View File

@ -15,6 +15,7 @@ from fellchensammlung.models import AdoptionNotice, Text, Animal, Rule, Image, R
Member Member
from .forms import AdoptionNoticeForm, ImageForm, ReportAdoptionNoticeForm, CommentForm, ReportCommentForm, AnimalForm from .forms import AdoptionNoticeForm, ImageForm, ReportAdoptionNoticeForm, CommentForm, ReportCommentForm, AnimalForm
from .models import Language, Announcement from .models import Language, Announcement
from .tools.geo import GeoAPI
def index(request): def index(request):
@ -93,12 +94,19 @@ def search(request):
@login_required @login_required
def add_adoption(request): def add_adoption_notice(request):
if request.method == 'POST': if request.method == 'POST':
form = AdoptionNoticeForm(request.POST, request.FILES, in_adoption_notice_creation_flow=True) form = AdoptionNoticeForm(request.POST, request.FILES, in_adoption_notice_creation_flow=True)
if form.is_valid(): if form.is_valid():
instance = form.save() instance = form.save()
"""Search the location given in the location string and add it to the adoption notice"""
geo_api = GeoAPI(debug=True)
location = geo_api.get_location_from_string(instance.location_string)
instance.location = location
instance.save()
return redirect(reverse("adoption-notice-add-animal", args=[instance.pk])) return redirect(reverse("adoption-notice-add-animal", args=[instance.pk]))
else: else:
form = AdoptionNoticeForm(in_adoption_notice_creation_flow=True) form = AdoptionNoticeForm(in_adoption_notice_creation_flow=True)