Ajout d'un filtre par proximité géographique

This commit is contained in:
Jean-Marie Favreau 2024-10-16 23:55:03 +02:00
parent fba52afbb0
commit 107c55863c
12 changed files with 3682 additions and 252 deletions

View File

@ -55,6 +55,10 @@ create-categories:
docker exec -it $(BACKEND_APP_NAME) $(SHELL) "-c" \
"python3 manage.py runscript create_categories"
create-reference-locations:
docker exec -it $(BACKEND_APP_NAME) $(SHELL) "-c" \
"python3 manage.py runscript create_reference_locations"
build-dev:
DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 docker-compose -f docker-compose.yml up --build -d

View File

@ -15,6 +15,12 @@ On peut aussi peupler les catégories avec un choix de catégories élémentaire
* ```make create-categories```
On peut aussi peupler les positions de référence qui serviront aux recherches géographiques avec la commande, après avoir éventuellement modifié le fichier [communes.json](./src/scripts/communes.json) qui contient pour l'exemple toutes les communes récupérées depuis [public.opendatasoft.com](https://public.opendatasoft.com/explore/dataset/georef-france-commune/export/?flg=fr-fr&disjunctive.reg_name&disjunctive.dep_name&disjunctive.arrdep_name&disjunctive.ze2020_name&disjunctive.bv2022_name&disjunctive.epci_name&disjunctive.ept_name&disjunctive.com_name&disjunctive.ze2010_name&disjunctive.com_is_mountain_area&sort=year&refine.dep_name=Puy-de-D%C3%B4me&location=9,45.51597,3.05969&basemap=jawg.light):
* ```make create-reference-locations```
## Notes aux développeurs
### Ajout d'une nouvelle source *custom*

View File

@ -8,7 +8,8 @@ from .models import (
BatchImportation,
RecurrentImport,
Place,
ContactMessage
ContactMessage,
ReferenceLocation
)
from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
@ -22,6 +23,7 @@ admin.site.register(BatchImportation)
admin.site.register(RecurrentImport)
admin.site.register(Place)
admin.site.register(ContactMessage)
admin.site.register(ReferenceLocation)
class URLWidget(DynamicArrayWidget):

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
# Generated by Django 4.2.9 on 2024-10-16 12:55
import django.contrib.gis.geos.point
from django.db import migrations, models
import location_field.models.spatial
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0086_alter_recurrentimport_processor'),
]
operations = [
migrations.CreateModel(
name='ReferenceLocation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Name of the location', verbose_name='Name')),
('location', location_field.models.spatial.LocationField(default=django.contrib.gis.geos.point.Point(3.08333, 45.783329), srid=4326)),
('main', models.BooleanField(default=False, help_text='This location is one of the main locations (shown first).', verbose_name='Main')),
],
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 4.2.9 on 2024-10-16 18:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0087_referencelocation'),
]
operations = [
migrations.AlterModelOptions(
name='referencelocation',
options={'verbose_name': 'Reference location', 'verbose_name_plural': 'Reference locations'},
),
migrations.AlterField(
model_name='referencelocation',
name='name',
field=models.CharField(help_text='Name of the location', unique=True, verbose_name='Name'),
),
]

View File

@ -236,6 +236,22 @@ class DuplicatedEvents(models.Model):
s.delete()
return nb
class ReferenceLocation(models.Model):
name = models.CharField(verbose_name=_("Name"), help_text=_("Name of the location"), unique=True, null=False)
location = LocationField(based_fields=["name"], zoom=12, default=Point(3.08333, 45.783329), srid=4326)
main = models.BooleanField(
verbose_name=_("Main"),
help_text=_("This location is one of the main locations (shown first)."),
default=False,
)
class Meta:
verbose_name = _("Reference location")
verbose_name_plural = _("Reference locations")
def __str__(self):
return self.name
class Place(models.Model):
name = models.CharField(verbose_name=_("Name"), help_text=_("Name of the place"))

View File

@ -137,7 +137,7 @@ details[role="list"] summary + ul li.selected>a:hover {
margin-top: 0;
}
#filters .categories {
width: 75%;
width: 50%;
float: left;
text-align: left;
line-height: 2em;
@ -152,6 +152,9 @@ details[role="list"] summary + ul li.selected>a:hover {
padding: 0.2em 0.4em;
}
}
#filters details.active {
width: 50%;
}
}
@ -855,6 +858,10 @@ form .buttons [role="button"] {
.grid.two-columns.grid-reverse {
grid-template-columns: 25% auto;
}
.grid.two-columns-equal {
grid-column-gap: var(--nav-element-spacing-vertical);
grid-template-columns: 50% 50%;
}
}

View File

@ -10,10 +10,10 @@
{% show_legend filter=filter %}
</div>
<details>
<details {% if filter.is_active %}class="active"{% endif %}>
<summary role="button" class="outline secondary">
{% if filter.is_active %}
Autres filtres&nbsp;:
<strong>Filtres&nbsp;: </strong>
{% for t in filter.get_tags %}
{{ t | tag_button }}
{% endfor %}
@ -27,8 +27,9 @@
{{ c }}
{% endfor %}
{{ filter.get_recurrence_filtering }}
{{ filter.get_position_radius }}
{% else %}
Autres filtres
<strong>Filtrer</strong>
{% endif %}
{% if filter.is_resetable %}
@ -37,7 +38,32 @@
</summary>
<form method="get" class="form django-form main-filter">
{{ filter.form.as_p }}
<div class="grid two-columns-equal">
<article>
<header>Localisation</header>
{% for f in filter.form.visible_fields %}
{% if f.id_for_label and f.id_for_label in "id_position,id_radius" %}
<div>
{{ f.errors }}
{{ f.label_tag }} {{ f }}
<span class="helptext">{{ f.help_text }}</span>
</div>
{% endif %}
{% endfor %}
</article>
<article>
<header>Autres filtres</header>
{% for f in filter.form.visible_fields %}
{% if not f.id_for_label or not f.id_for_label in "id_position,id_radius" %}
<p>
{{ f.errors }}
{{ f.label_tag }} {{ f }}
<span class="helptext">{{ f.help_text }}</span>
</p>
{% endif %}
{% endfor %}
</article>
</div>
<button type="submit">Appliquer le filtre</button>
</form>
</details>

View File

@ -11,6 +11,9 @@ from django import forms
from django.contrib.postgres.search import SearchQuery, SearchHeadline
from django.utils.safestring import mark_safe
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import D
from django.http import HttpResponseRedirect, HttpResponse
from django.urls import reverse
from collections import Counter
@ -48,6 +51,7 @@ from .models import (
ModerationQuestion,
ModerationAnswer,
Place,
ReferenceLocation
)
from django.utils import timezone
from django.utils.html import escape
@ -170,6 +174,18 @@ class EventFilter(django_filters.FilterSet):
("only_recurrent", "Montrer uniquement les événements récurrents"),
]
position = django_filters.ModelChoiceFilter(
label="À proximité de",
method="no_filter",
queryset=ReferenceLocation.objects.all().order_by("-main", "name__unaccent")
)
radius = django_filters.NumberFilter(
label="Distance maximum (km)",
method="no_filter",
widget=forms.NumberInput(attrs={"min": "1"})
)
exclude_tags = django_filters.MultipleChoiceFilter(
label="Exclure les étiquettes",
choices=[(t, t) for t in Event.get_all_tags()],
@ -180,7 +196,7 @@ class EventFilter(django_filters.FilterSet):
)
tags = django_filters.MultipleChoiceFilter(
label="Filtrer par étiquettes",
label="Inclure les étiquettes",
choices=[(t, t) for t in Event.get_all_tags()],
lookup_expr="icontains",
field_name="tags",
@ -188,7 +204,7 @@ class EventFilter(django_filters.FilterSet):
)
recurrences = django_filters.ChoiceFilter(
label="Filtrer par récurrence",
label="Inclure la récurrence",
choices=RECURRENT_CHOICES,
method="filter_recurrences",
)
@ -201,12 +217,6 @@ class EventFilter(django_filters.FilterSet):
widget=MultipleHiddenInput,
)
city = django_filters.MultipleChoiceFilter(
label="Filtrer par ville",
field_name="exact_location__city",
choices=[(c, c) for c in Place.get_all_cities()],
widget=forms.CheckboxSelectMultiple,
)
status = django_filters.MultipleChoiceFilter(
label="Filtrer par status",
@ -217,7 +227,7 @@ class EventFilter(django_filters.FilterSet):
class Meta:
model = Event
fields = ["category", "city", "tags", "exclude_tags", "status", "recurrences"]
fields = ["category", "tags", "exclude_tags", "status", "recurrences"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -229,6 +239,18 @@ class EventFilter(django_filters.FilterSet):
lookup = "__".join([name, "isnull"])
return queryset.filter(**{lookup: value == "remove_recurrent"})
def no_filter(self, queryset, name, value):
return queryset
@property
def qs(self):
parent = super().qs
if self.get_cleaned_data("position") is None or self.get_cleaned_data("radius") is None:
return parent
d = self.get_cleaned_data("radius")
p = self.get_cleaned_data("position").location
return parent.exclude(exact_location=False).filter(exact_location__location__distance_lt=(p, D(km=d)))
def get_url(self):
if isinstance(self.form.data, QueryDict):
return self.form.data.urlencode()
@ -296,12 +318,15 @@ class EventFilter(django_filters.FilterSet):
def get_status(self):
return self.get_cleaned_data("status")
def get_cities(self):
return self.get_cleaned_data("city")
def get_position(self):
return self.get_cleaned_data("position")
def get_radius(self):
return self.get_cleaned_data("radius")
def to_str(self, prefix=''):
self.form.full_clean()
result = ' '.join([c.name for c in self.get_categories()] + [t for t in self.get_tags()] + [c for c in self.get_cities()])
result = ' '.join([c.name for c in self.get_categories()] + [t for t in self.get_tags()] + [str(self.get_position()), str(self.get_radius())])
if len(result) > 0:
result = prefix + result
return result
@ -346,22 +371,19 @@ class EventFilter(django_filters.FilterSet):
len(self.get_cleaned_data("tags")) != 0
or len(self.get_cleaned_data("exclude_tags")) != 0
or len(self.get_cleaned_data("recurrences")) != 0
or len(self.get_cleaned_data("city")) != 0
or ((not self.get_cleaned_data("position") is None) and (not self.get_cleaned_data("radius") is None))
)
def is_active(self, only_categories=False):
if only_categories:
return len(self.get_cleaned_data("category")) != 0
else:
if (
len(self.get_cleaned_data("status")) != 0
):
return True
return (
len(self.get_cleaned_data("tags")) != 0
len(self.get_cleaned_data("status")) != 0
or len(self.get_cleaned_data("tags")) != 0
or len(self.get_cleaned_data("exclude_tags")) != 0
or len(self.get_cleaned_data("recurrences")) != 0
or len(self.get_cleaned_data("city")) != 0
or ((not self.get_cleaned_data("position") is None) and (not self.get_cleaned_data("radius") is None))
)
def is_selected(self, cat):
@ -376,6 +398,12 @@ class EventFilter(django_filters.FilterSet):
return request
return request
def get_position_radius(self):
if self.get_cleaned_data("position") is None or self.get_cleaned_data("radius") is None:
return ""
else:
return str(self.get_cleaned_data("position")) + ' (' + str(self.get_cleaned_data("radius")) + ' km)'
def mentions_legales(request):

3259
src/scripts/communes.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
import json, os
from django.contrib.gis.geos import Point
from agenda_culturel.models import ReferenceLocation
def run():
input_file = os.path.dirname(__file__) + os.path.sep + "communes.json"
data = []
with open(input_file, 'r') as file:
data = json.load(file)
objs = [ReferenceLocation(location=Point(c["geo_point_2d"]["lon"], c["geo_point_2d"]["lat"]), name=c["com_name"], main="main" in c) for c in data]
objs = ReferenceLocation.objects.bulk_create(objs, ignore_conflicts=True)