Ajout d'un filtre par proximité géographique
This commit is contained in:
parent
fba52afbb0
commit
107c55863c
4
Makefile
4
Makefile
@ -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
|
||||
|
||||
|
@ -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*
|
||||
|
@ -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
24
src/agenda_culturel/migrations/0087_referencelocation.py
Normal file
24
src/agenda_culturel/migrations/0087_referencelocation.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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"))
|
||||
|
@ -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%;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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 :
|
||||
<strong>Filtres : </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>
|
||||
|
@ -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
3259
src/scripts/communes.json
Normal file
File diff suppressed because it is too large
Load Diff
17
src/scripts/create_reference_locations.py
Normal file
17
src/scripts/create_reference_locations.py
Normal 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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user