Merge pull request 'PLZ overhaul' (#22) from dev into main
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #22
This commit is contained in:
Jannis Portmann 2023-10-15 00:35:48 +02:00
commit 71a8a66a10
9 changed files with 110 additions and 32 deletions

View file

@ -1,5 +1,5 @@
from django.contrib import admin from django.contrib import admin
from .models import Offer, Wish, PflaenzliUser from .models import Offer, Wish, PflaenzliUser, Plz
@admin.register(Offer) @admin.register(Offer)
@ -15,3 +15,8 @@ class WishAdmin(admin.ModelAdmin):
@admin.register(PflaenzliUser) @admin.register(PflaenzliUser)
class UserAdmin(admin.ModelAdmin): class UserAdmin(admin.ModelAdmin):
list_display = ['id', 'username', 'email', 'zipcode', 'language_code', 'date_joined'] list_display = ['id', 'username', 'email', 'zipcode', 'language_code', 'date_joined']
@admin.register(Plz)
class PlzAdmin(admin.ModelAdmin):
list_display = ['id', 'name', 'plz', 'lat', 'lon']

View file

@ -5,36 +5,52 @@ from urllib import request
import pandas as pd import pandas as pd
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from pflaenzli_django.settings import BASE_DIR from pflaenzli_django.settings import BASE_DIR
from pflaenzli.models import Plz
class Command(BaseCommand): class Command(BaseCommand):
help = 'Get the zip code index from post and compile it to a dataframe pickle' help = 'Get the zip code index from post and compile it to a dataframe pickle'
def handle(self, *args, **options): def add_arguments(self, parser):
self.parse_data(self.download_geojson(api='v2', data='v2')) parser.add_argument("--force", action="store_true", required=False)
self.stdout.write(self.style.SUCCESS('Done!'))
def download_geojson(self, api, data): def handle(self, *args, **options):
self.parse_data(*self.download_geojson(api='v2', data='v2', force=options["force"]), force=options["force"])
def download_geojson(self, api, data, force=False):
file = f'plz_verzeichnis_{data}.json' file = f'plz_verzeichnis_{data}.json'
if os.path.exists(file): if os.path.exists(file) and not force:
self.stdout.write('File already downloaded. Skipping...\n') self.stdout.write('File already downloaded.')
self.stdout.write(self.style.SUCCESS(
'Skipping...\n'))
exists = True
else: else:
self.stdout.write('Downloading geojson...') self.stdout.write('Downloading geojson...')
url = f'https://swisspost.opendatasoft.com/api/{api}/catalog/datasets/plz_verzeichnis_{data}/exports/geojson' url = f'https://swisspost.opendatasoft.com/api/{api}/catalog/datasets/plz_verzeichnis_{data}/exports/geojson'
request.urlretrieve(url, file) request.urlretrieve(url, file)
self.stdout.write(self.style.SUCCESS('Done!\n')) self.stdout.write(self.style.SUCCESS('Done!\n'))
exists = False
return file return file, exists
def parse_data(self, file, exists, force=False):
if exists and not force:
self.stdout.write(self.style.WARNING(
'Nothing was done, if you want to redownload the PLZ index, use the --force option.\n'))
return
def parse_data(self, file):
self.stdout.write('Opening file...') self.stdout.write('Opening file...')
# Load the GeoJSON data for the zip codes # Load the GeoJSON data for the zip codes
with open(file, encoding='UTF-8') as f: with open(file, encoding='UTF-8') as f:
full_data = json.load(f) full_data = json.load(f)
self.stdout.write(self.style.SUCCESS('Done!\n')) self.stdout.write(self.style.SUCCESS('Done!\n'))
self.stdout.write('Parsing file...') self.stdout.write('Deleting existing data...')
zip_dict = {} Plz.objects.all().delete()
self.stdout.write(self.style.SUCCESS('Done!\n'))
self.stdout.write('Parsing file and add new data...')
for plz_entry in full_data['features']: for plz_entry in full_data['features']:
plz_entry = plz_entry['properties'] plz_entry = plz_entry['properties']
try: try:
@ -51,14 +67,12 @@ class Command(BaseCommand):
if plz is None or lat is None or lon is None: if plz is None or lat is None or lon is None:
continue continue
zip_dict[int(plz)] = [lat, lon] try:
self.stdout.write(self.style.SUCCESS('Done!\n')) name = plz_entry['ortbez27']
except (KeyError, AttributeError, TypeError):
name = None
df = pd.DataFrame.from_dict(zip_dict, orient='index') plz, _ = Plz.objects.get_or_create(plz=int(plz), lat=lat, lon=lon, name=name)
df.columns = ['lat', 'lon']
self.stdout.write('Saving pickle...') self.stdout.write(self.style.SUCCESS('Wrote PLZ data to the databse successfully\n'))
destination = os.path.join(BASE_DIR, 'pflaenzli', 'utils', 'plz.pkl') self.stdout.write(self.style.SUCCESS('Done!'))
df.to_pickle(destination)
self.stdout.write(self.style.SUCCESS(f'Wrote pickle to {destination}\n'))

View file

@ -0,0 +1,31 @@
# Generated by Django 4.2.5 on 2023-09-13 15:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pflaenzli", "0005_pflaenzliuser_language_code_alter_offer_category"),
]
operations = [
migrations.CreateModel(
name="Plz",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("plz", models.IntegerField(verbose_name="PLZ")),
("lat", models.DecimalField(decimal_places=6, max_digits=8)),
("lon", models.DecimalField(decimal_places=6, max_digits=8)),
("name", models.CharField(max_length=27)),
],
),
]

View file

@ -14,6 +14,13 @@ class PflaenzliUser(AbstractUser):
language_code = models.CharField(max_length=2, default='de') language_code = models.CharField(max_length=2, default='de')
class Plz(models.Model):
plz = models.IntegerField(verbose_name='PLZ')
lat = models.DecimalField(max_digits=8, decimal_places=6)
lon = models.DecimalField(max_digits=8, decimal_places=6)
name = models.CharField(max_length=27)
class Offer(models.Model): class Offer(models.Model):
CATEGORIES = [ CATEGORIES = [
('PLNT', _('Plant')), ('PLNT', _('Plant')),

View file

@ -140,7 +140,7 @@
</ul> </ul>
</li> </li>
<hr class="d-lg-none"> <hr class="d-lg-none">
<div class="d-flex flex-row"> <div class="d-flex flex-row align-items-center">
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<li class="nav-item dropdown me-2"> <li class="nav-item dropdown me-2">
<a class="nav-link dropdown-toggle" <a class="nav-link dropdown-toggle"

View file

@ -1,5 +1,6 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% load plz %}
{% block title %}Offer: {{ offer.title }}{% endblock %} {% block title %}Offer: {{ offer.title }}{% endblock %}
{% block meta %} {% block meta %}
<meta name="description" <meta name="description"
@ -30,7 +31,7 @@
{% endif %} {% endif %}
</p> </p>
<p class="mr-3"> <p class="mr-3">
<i class="fas fa-map-marker-alt"></i> {{ offer.zipcode }} {% with plz=offer|get_plz %}<i class="fas fa-map-marker-alt"></i> {{ plz.plz }} {{ plz.name }}{% endwith %}
</p> </p>
{% if dist %} {% if dist %}
<p class="pr-3"> <p class="pr-3">
@ -90,7 +91,7 @@
</div> </div>
<form method="post" action="{% url 'offer_trade' %}"> <form method="post" action="{% url 'offer_trade' %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="offer" value="{{ offer.id }}"/> <input type="hidden" name="offer" value="{{ offer.id }}" />
<button class="btn btn-pfl mb-3" data-umami-event="Trade offer">{% trans "Offer trade" %}</button> <button class="btn btn-pfl mb-3" data-umami-event="Trade offer">{% trans "Offer trade" %}</button>
</form> </form>
{% endif %} {% endif %}

View file

@ -0,0 +1,14 @@
from django import template
from pflaenzli.models import Plz
register = template.Library()
@register.filter
def get_plz(offer):
try:
return Plz.objects.get(plz=offer.zipcode)
except Plz.MultipleObjectsReturned:
return Plz.objects.filter(plz=offer.zipcode).first()
except Plz.DoesNotExist:
return None

View file

@ -2,28 +2,34 @@ from geopy.distance import distance
import os import os
from pandas import read_pickle from pandas import read_pickle
from django.db.models import F, Func from django.db.models import F, Func
from pflaenzli.models import Plz
path = os.path.dirname(os.path.abspath(__file__)) path = os.path.dirname(os.path.abspath(__file__))
df = read_pickle(os.path.join(path, 'plz.pkl')) df = read_pickle(os.path.join(path, 'plz.pkl'))
def calculate_distance(zip_1, zip_2): def calculate_distance(plz_1: Plz, plz_2: Plz):
if zip_1 == zip_2: if plz_1 == plz_2:
return 0 return 0
zip_1_coords = tuple(df[df.index == zip_1].values) dist = round(distance((plz_1.lat, plz_1.lon), (plz_2.lat, plz_2.lon)).kilometers)
zip_2_coords = tuple(df[df.index == zip_2].values)
dist = round(distance((zip_1_coords), (zip_2_coords)).kilometers)
return None if dist > 400 else dist return None if dist > 400 else dist
def filter_by_distance(qs, filter_zipcode, max_dist): def filter_by_distance(qs, filter_zipcode, max_dist):
filtered_offers = [] filtered_offers = []
try:
filter_plz = Plz.objects.get(plz=filter_zipcode)
except Plz.DoesNotExist:
return filtered_offers
for offer in qs: for offer in qs:
d = calculate_distance(int(offer.zipcode), int(filter_zipcode)) offer_plz = Plz.objects.get(plz=offer.zipcode)
d = calculate_distance(offer_plz, filter_plz)
if d is not None and d <= max_dist: if d is not None and d <= max_dist:
filtered_offers.append(offer) filtered_offers.append(offer)
return filtered_offers return filtered_offers

View file

@ -12,7 +12,7 @@ from django.views.decorators.http import require_POST
from .forms import CreateOfferForm, FilterForm, RegistrationForm, WishForm from .forms import CreateOfferForm, FilterForm, RegistrationForm, WishForm
from .mail import send_offer_email from .mail import send_offer_email
from .models import Offer, PflaenzliUser, Wish from .models import Offer, PflaenzliUser, Wish, Plz
from .upload import generate_unique_filename from .upload import generate_unique_filename
from .utils.compress_image import compress_image from .utils.compress_image import compress_image
from .utils.distance import calculate_distance, filter_by_distance from .utils.distance import calculate_distance, filter_by_distance
@ -59,7 +59,7 @@ def offer_detail(request, offer_id):
if offer.zipcode == request.user.zipcode: if offer.zipcode == request.user.zipcode:
dist = 0 dist = 0
else: else:
dist = calculate_distance(offer.zipcode, request.user.zipcode) dist = calculate_distance(Plz.objects.get(plz=offer.zipcode), Plz.objects.get(plz=request.user.zipcode))
else: else:
dist = None dist = None
else: else: