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" \
|
docker exec -it $(BACKEND_APP_NAME) $(SHELL) "-c" \
|
||||||
"python3 manage.py runscript create_categories"
|
"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:
|
build-dev:
|
||||||
DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 docker-compose -f docker-compose.yml up --build -d
|
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```
|
* ```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
|
## Notes aux développeurs
|
||||||
|
|
||||||
### Ajout d'une nouvelle source *custom*
|
### Ajout d'une nouvelle source *custom*
|
||||||
|
@ -8,7 +8,8 @@ from .models import (
|
|||||||
BatchImportation,
|
BatchImportation,
|
||||||
RecurrentImport,
|
RecurrentImport,
|
||||||
Place,
|
Place,
|
||||||
ContactMessage
|
ContactMessage,
|
||||||
|
ReferenceLocation
|
||||||
)
|
)
|
||||||
from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin
|
from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin
|
||||||
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
|
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
|
||||||
@ -22,6 +23,7 @@ admin.site.register(BatchImportation)
|
|||||||
admin.site.register(RecurrentImport)
|
admin.site.register(RecurrentImport)
|
||||||
admin.site.register(Place)
|
admin.site.register(Place)
|
||||||
admin.site.register(ContactMessage)
|
admin.site.register(ContactMessage)
|
||||||
|
admin.site.register(ReferenceLocation)
|
||||||
|
|
||||||
|
|
||||||
class URLWidget(DynamicArrayWidget):
|
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()
|
s.delete()
|
||||||
return nb
|
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):
|
class Place(models.Model):
|
||||||
name = models.CharField(verbose_name=_("Name"), help_text=_("Name of the place"))
|
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;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
#filters .categories {
|
#filters .categories {
|
||||||
width: 75%;
|
width: 50%;
|
||||||
float: left;
|
float: left;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
line-height: 2em;
|
line-height: 2em;
|
||||||
@ -152,6 +152,9 @@ details[role="list"] summary + ul li.selected>a:hover {
|
|||||||
padding: 0.2em 0.4em;
|
padding: 0.2em 0.4em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#filters details.active {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -855,6 +858,10 @@ form .buttons [role="button"] {
|
|||||||
.grid.two-columns.grid-reverse {
|
.grid.two-columns.grid-reverse {
|
||||||
grid-template-columns: 25% auto;
|
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 %}
|
{% show_legend filter=filter %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details>
|
<details {% if filter.is_active %}class="active"{% endif %}>
|
||||||
<summary role="button" class="outline secondary">
|
<summary role="button" class="outline secondary">
|
||||||
{% if filter.is_active %}
|
{% if filter.is_active %}
|
||||||
Autres filtres :
|
<strong>Filtres : </strong>
|
||||||
{% for t in filter.get_tags %}
|
{% for t in filter.get_tags %}
|
||||||
{{ t | tag_button }}
|
{{ t | tag_button }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -27,8 +27,9 @@
|
|||||||
{{ c }}
|
{{ c }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{{ filter.get_recurrence_filtering }}
|
{{ filter.get_recurrence_filtering }}
|
||||||
|
{{ filter.get_position_radius }}
|
||||||
{% else %}
|
{% else %}
|
||||||
Autres filtres
|
<strong>Filtrer</strong>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if filter.is_resetable %}
|
{% if filter.is_resetable %}
|
||||||
@ -37,7 +38,32 @@
|
|||||||
|
|
||||||
</summary>
|
</summary>
|
||||||
<form method="get" class="form django-form main-filter">
|
<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>
|
<button type="submit">Appliquer le filtre</button>
|
||||||
</form>
|
</form>
|
||||||
</details>
|
</details>
|
||||||
|
@ -11,6 +11,9 @@ from django import forms
|
|||||||
from django.contrib.postgres.search import SearchQuery, SearchHeadline
|
from django.contrib.postgres.search import SearchQuery, SearchHeadline
|
||||||
from django.utils.safestring import mark_safe
|
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.http import HttpResponseRedirect, HttpResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
@ -48,6 +51,7 @@ from .models import (
|
|||||||
ModerationQuestion,
|
ModerationQuestion,
|
||||||
ModerationAnswer,
|
ModerationAnswer,
|
||||||
Place,
|
Place,
|
||||||
|
ReferenceLocation
|
||||||
)
|
)
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
@ -170,6 +174,18 @@ class EventFilter(django_filters.FilterSet):
|
|||||||
("only_recurrent", "Montrer uniquement les événements récurrents"),
|
("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(
|
exclude_tags = django_filters.MultipleChoiceFilter(
|
||||||
label="Exclure les étiquettes",
|
label="Exclure les étiquettes",
|
||||||
choices=[(t, t) for t in Event.get_all_tags()],
|
choices=[(t, t) for t in Event.get_all_tags()],
|
||||||
@ -180,7 +196,7 @@ class EventFilter(django_filters.FilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
tags = django_filters.MultipleChoiceFilter(
|
tags = django_filters.MultipleChoiceFilter(
|
||||||
label="Filtrer par étiquettes",
|
label="Inclure les étiquettes",
|
||||||
choices=[(t, t) for t in Event.get_all_tags()],
|
choices=[(t, t) for t in Event.get_all_tags()],
|
||||||
lookup_expr="icontains",
|
lookup_expr="icontains",
|
||||||
field_name="tags",
|
field_name="tags",
|
||||||
@ -188,7 +204,7 @@ class EventFilter(django_filters.FilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
recurrences = django_filters.ChoiceFilter(
|
recurrences = django_filters.ChoiceFilter(
|
||||||
label="Filtrer par récurrence",
|
label="Inclure la récurrence",
|
||||||
choices=RECURRENT_CHOICES,
|
choices=RECURRENT_CHOICES,
|
||||||
method="filter_recurrences",
|
method="filter_recurrences",
|
||||||
)
|
)
|
||||||
@ -201,12 +217,6 @@ class EventFilter(django_filters.FilterSet):
|
|||||||
widget=MultipleHiddenInput,
|
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(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
label="Filtrer par status",
|
label="Filtrer par status",
|
||||||
@ -217,7 +227,7 @@ class EventFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Event
|
model = Event
|
||||||
fields = ["category", "city", "tags", "exclude_tags", "status", "recurrences"]
|
fields = ["category", "tags", "exclude_tags", "status", "recurrences"]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -229,6 +239,18 @@ class EventFilter(django_filters.FilterSet):
|
|||||||
lookup = "__".join([name, "isnull"])
|
lookup = "__".join([name, "isnull"])
|
||||||
return queryset.filter(**{lookup: value == "remove_recurrent"})
|
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):
|
def get_url(self):
|
||||||
if isinstance(self.form.data, QueryDict):
|
if isinstance(self.form.data, QueryDict):
|
||||||
return self.form.data.urlencode()
|
return self.form.data.urlencode()
|
||||||
@ -296,12 +318,15 @@ class EventFilter(django_filters.FilterSet):
|
|||||||
def get_status(self):
|
def get_status(self):
|
||||||
return self.get_cleaned_data("status")
|
return self.get_cleaned_data("status")
|
||||||
|
|
||||||
def get_cities(self):
|
def get_position(self):
|
||||||
return self.get_cleaned_data("city")
|
return self.get_cleaned_data("position")
|
||||||
|
|
||||||
|
def get_radius(self):
|
||||||
|
return self.get_cleaned_data("radius")
|
||||||
|
|
||||||
def to_str(self, prefix=''):
|
def to_str(self, prefix=''):
|
||||||
self.form.full_clean()
|
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:
|
if len(result) > 0:
|
||||||
result = prefix + result
|
result = prefix + result
|
||||||
return result
|
return result
|
||||||
@ -346,22 +371,19 @@ class EventFilter(django_filters.FilterSet):
|
|||||||
len(self.get_cleaned_data("tags")) != 0
|
len(self.get_cleaned_data("tags")) != 0
|
||||||
or len(self.get_cleaned_data("exclude_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("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):
|
def is_active(self, only_categories=False):
|
||||||
if only_categories:
|
if only_categories:
|
||||||
return len(self.get_cleaned_data("category")) != 0
|
return len(self.get_cleaned_data("category")) != 0
|
||||||
else:
|
else:
|
||||||
if (
|
|
||||||
len(self.get_cleaned_data("status")) != 0
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
return (
|
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("exclude_tags")) != 0
|
||||||
or len(self.get_cleaned_data("recurrences")) != 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):
|
def is_selected(self, cat):
|
||||||
@ -376,6 +398,12 @@ class EventFilter(django_filters.FilterSet):
|
|||||||
return request
|
return request
|
||||||
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):
|
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