Skip to content

Commit

Permalink
[DO NOT REVIEW] SIAE/Job search: simplify handling of departments
Browse files Browse the repository at this point in the history
The departments were computed by iterating over the list of objects,
forcing to fetch them all to memory. For Paris, with 100 kms radius,
that’s almost 2.5K job offers. That’s a lot of work, where a basic
adjacency list is likely sufficient for users.

Also, the departments field had to be reloaded when the search distance
changed. That would lead to a pretty annoying UI with the upcoming
changes to [Dropdown
filters](https://zeroheight.com/85c89893b/p/207551-buttons): when a
department is selected, the department field is reloaded (with HTMX),
closing the dropdown filter. Hence, a user wanting to select multiple
departments need to open the dropdown, click a department. The results
load, they can then open the dropdown, click a department, and so on.

Instead, use an adjacency list. Users can of course ask for an
impossible combination of filters, such as SIAE 5 kms from Limoges and
in Corrèze. They’ll just get no results, and pick more sensible filters.

The assertNumQueries were outdated, and were updated to better reflect
the actual queries.
  • Loading branch information
francoisfreitag committed Jun 20, 2024
1 parent f3ea743 commit f67056e
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 113 deletions.
100 changes: 100 additions & 0 deletions itou/common_apps/address/departments.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,106 @@ def get_department_to_region():
"75": {"city": "Paris", "max": 75020},
}

# https://gist.github.com/sunny/13803?permalink_comment_id=4216753#gistcomment-4216753
DEPARTMENTS_ADJACENCY = {
"01": ("38", "39", "69", "71", "73", "74"),
"02": ("08", "51", "59", "60", "77", "80"),
"03": ("18", "23", "42", "58", "63", "71"),
"04": ("05", "06", "13", "26", "83", "84"),
"05": ("04", "26", "38", "73"),
"06": ("04", "83"),
"07": ("26", "30", "38", "42", "43", "48", "84"),
"08": ("02", "51", "55"),
"09": ("11", "31", "66"),
"10": ("21", "51", "52", "77", "89"),
"11": ("09", "31", "34", "66", "81"),
"12": ("15", "30", "34", "46", "48", "81", "82"),
"13": ("04", "30", "83", "84"),
"14": ("27", "50", "61", "76"),
"15": ("12", "19", "43", "46", "48", "63"),
"16": ("17", "24", "79", "86", "87"),
"17": ("16", "24", "33", "79", "85"),
"18": ("03", "23", "36", "41", "45", "58"),
"19": ("15", "23", "24", "46", "63", "87"),
"2A": ("2B"),
"2B": ("2A"),
"21": ("10", "39", "52", "58", "70", "71", "89"),
"22": ("29", "35", "56"),
"23": ("03", "18", "19", "36", "63", "87"),
"24": ("16", "17", "19", "33", "46", "47", "87"),
"25": ("39", "70", "90"),
"26": ("04", "05", "07", "38", "84"),
"27": ("14", "28", "60", "61", "76", "78", "95"),
"28": ("27", "41", "45", "61", "72", "78", "91"),
"29": ("22", "56"),
"30": ("07", "12", "13", "34", "48", "84"),
"31": ("09", "11", "32", "65", "81", "82"),
"32": ("31", "40", "47", "64", "65", "82"),
"33": ("17", "24", "40", "47"),
"34": ("11", "12", "30", "81"),
"35": ("22", "44", "49", "50", "53", "56"),
"36": ("18", "23", "37", "41", "86", "87"),
"37": ("36", "41", "49", "72", "86"),
"38": ("01", "05", "07", "26", "42", "69", "73"),
"39": ("01", "21", "25", "70", "71"),
"40": ("32", "33", "47", "64"),
"41": ("18", "28", "36", "37", "45", "72"),
"42": ("03", "07", "38", "43", "63", "69", "71"),
"43": ("07", "15", "42", "48", "63"),
"44": ("35", "49", "56", "85"),
"45": ("18", "28", "41", "58", "77", "89", "91"),
"46": ("12", "15", "19", "24", "47", "82"),
"47": ("24", "32", "33", "40", "46", "82"),
"48": ("07", "12", "15", "30", "43"),
"49": ("35", "37", "44", "53", "72", "79", "85", "86"),
"50": ("14", "35", "53", "61"),
"51": ("02", "08", "10", "52", "55", "77"),
"52": ("10", "21", "51", "55", "70", "88"),
"53": ("35", "49", "50", "61", "72"),
"54": ("55", "57", "67", "88"),
"55": ("08", "51", "52", "54", "88"),
"56": ("22", "29", "35", "44"),
"57": ("54", "67"),
"58": ("03", "18", "21", "45", "71", "89"),
"59": ("02", "62", "80"),
"60": ("02", "27", "76", "77", "80", "95"),
"61": ("14", "27", "28", "50", "53", "72"),
"62": ("59", "80"),
"63": ("03", "15", "19", "23", "42", "43"),
"64": ("32", "40", "65"),
"65": ("31", "32", "64"),
"66": ("09", "11"),
"67": ("54", "57", "68", "88"),
"68": ("67", "88", "90"),
"69": ("01", "38", "42", "71"),
"70": ("21", "25", "39", "52", "88", "90"),
"71": ("01", "03", "21", "39", "42", "58", "69"),
"72": ("28", "37", "41", "49", "53", "61"),
"73": ("01", "05", "38", "74"),
"74": ("01", "73"),
"75": ("92", "93", "94"),
"76": ("14", "27", "60", "80"),
"77": ("02", "10", "45", "51", "60", "89", "91", "93", "94", "95"),
"78": ("27", "28", "91", "92", "95"),
"79": ("16", "17", "49", "85", "86"),
"80": ("02", "59", "60", "62", "76"),
"81": ("11", "12", "31", "34", "82"),
"82": ("12", "31", "32", "46", "47", "81"),
"83": ("04", "06", "13", "84"),
"84": ("04", "07", "13", "26", "30", "83"),
"85": ("17", "44", "49", "79"),
"86": ("16", "36", "37", "49", "79", "87"),
"87": ("16", "19", "23", "24", "36", "86"),
"88": ("52", "54", "55", "67", "68", "70", "90"),
"89": ("10", "21", "45", "58", "77"),
"90": ("25", "68", "70", "88"),
"91": ("28", "45", "77", "78", "92", "94"),
"92": ("75", "78", "91", "93", "94", "95"),
"93": ("75", "77", "92", "94", "95"),
"94": ("75", "77", "91", "92", "93"),
"95": ("27", "60", "77", "78", "92", "93"),
}


def department_from_postcode(post_code):
"""
Expand Down
141 changes: 70 additions & 71 deletions itou/templates/search/includes/siaes_search_filters_departments.html
Original file line number Diff line number Diff line change
@@ -1,79 +1,78 @@
{% load django_bootstrap5 %}
<div id="department-fields"{% if request.htmx %} hx-swap-oob="true"{% endif %}>
{% if form.departments %}
<hr>
<fieldset>
<div class="form-group mb-0">
<legend class="has-collapse-caret mb-0"
data-bs-toggle="collapse"
href="#collapse_{{ form.departments.name }}"
role="button"
aria-expanded="{% if form.departments.value %}true{% else %}false{% endif %}"
aria-controls="collapse_{{ form.departments.name }}">
{{ form.departments.label | capfirst }}
</legend>
<div class="collapse mt-3{% if form.departments.value %} show{% endif %}" id="collapse_{{ form.departments.name }}">
{{ form.departments }}
</div>

{% if form.departments %}
<hr>
<fieldset>
<div class="form-group mb-0">
<legend class="has-collapse-caret mb-0"
data-bs-toggle="collapse"
href="#collapse_{{ form.departments.name }}"
role="button"
aria-expanded="{% if form.departments.value %}true{% else %}false{% endif %}"
aria-controls="collapse_{{ form.departments.name }}">
{{ form.departments.label | capfirst }}
</legend>
<div class="collapse mt-3{% if form.departments.value %} show{% endif %}" id="collapse_{{ form.departments.name }}">
{{ form.departments }}
</div>
</fieldset>
{% endif %}
</div>
</fieldset>
{% endif %}

{# getattr and list still painful in Django template #}
{% if form.districts_13 %}
<hr>
<fieldset>
<div class="form-group mb-0">
<legend class="has-collapse-caret mb-0"
data-bs-toggle="collapse"
href="#collapse_{{ form.districts_13.name }}"
role="button"
aria-expanded="{% if form.districts_13.value %}true{% else %}false{% endif %}"
aria-controls="collapse_{{ form.districts_13.name }}">
{{ form.districts_13.label | capfirst }}
</legend>
<div class="collapse mt-3{% if form.districts_13.value %} show{% endif %}" id="collapse_{{ form.districts_13.name }}">
{{ form.districts_13 }}
</div>
{# getattr and list still painful in Django template #}
{% if form.districts_13 %}
<hr>
<fieldset>
<div class="form-group mb-0">
<legend class="has-collapse-caret mb-0"
data-bs-toggle="collapse"
href="#collapse_{{ form.districts_13.name }}"
role="button"
aria-expanded="{% if form.districts_13.value %}true{% else %}false{% endif %}"
aria-controls="collapse_{{ form.districts_13.name }}">
{{ form.districts_13.label | capfirst }}
</legend>
<div class="collapse mt-3{% if form.districts_13.value %} show{% endif %}" id="collapse_{{ form.districts_13.name }}">
{{ form.districts_13 }}
</div>
</fieldset>
{% endif %}
</div>
</fieldset>
{% endif %}

{% if form.districts_69 %}
<hr>
<fieldset>
<div class="form-group mb-0">
<legend class="has-collapse-caret mb-0"
data-bs-toggle="collapse"
href="#collapse_{{ form.districts_69.name }}"
role="button"
aria-expanded="{% if form.districts_69.value %}true{% else %}false{% endif %}"
aria-controls="collapse_{{ form.districts_69.name }}">
{{ form.districts_69.label | capfirst }}
</legend>
<div class="collapse mt-3{% if form.districts_69.value %} show{% endif %}" id="collapse_{{ form.districts_69.name }}">
{{ form.districts_69 }}
</div>
{% if form.districts_69 %}
<hr>
<fieldset>
<div class="form-group mb-0">
<legend class="has-collapse-caret mb-0"
data-bs-toggle="collapse"
href="#collapse_{{ form.districts_69.name }}"
role="button"
aria-expanded="{% if form.districts_69.value %}true{% else %}false{% endif %}"
aria-controls="collapse_{{ form.districts_69.name }}">
{{ form.districts_69.label | capfirst }}
</legend>
<div class="collapse mt-3{% if form.districts_69.value %} show{% endif %}" id="collapse_{{ form.districts_69.name }}">
{{ form.districts_69 }}
</div>
</fieldset>
{% endif %}
</div>
</fieldset>
{% endif %}

{% if form.districts_75 %}
<hr>
<fieldset>
<div class="form-group mb-0">
<legend class="has-collapse-caret mb-0"
data-bs-toggle="collapse"
href="#collapse_{{ form.districts_75.name }}"
role="button"
aria-expanded="{% if form.districts_75.value %}true{% else %}false{% endif %}"
aria-controls="collapse_{{ form.districts_75.name }}">
{{ form.districts_75.label | capfirst }}
</legend>
<div class="collapse mt-3{% if form.districts_75.value %} show{% endif %}" id="collapse_{{ form.districts_75.name }}">
{{ form.districts_75 }}
</div>
{% if form.districts_75 %}
<hr>
<fieldset>
<div class="form-group mb-0">
<legend class="has-collapse-caret mb-0"
data-bs-toggle="collapse"
href="#collapse_{{ form.districts_75.name }}"
role="button"
aria-expanded="{% if form.districts_75.value %}true{% else %}false{% endif %}"
aria-controls="collapse_{{ form.districts_75.name }}">
{{ form.districts_75.label | capfirst }}
</legend>
<div class="collapse mt-3{% if form.districts_75.value %} show{% endif %}" id="collapse_{{ form.districts_75.name }}">
{{ form.districts_75 }}
</div>
</fieldset>
{% endif %}
</div>
</div>
</fieldset>
{% endif %}
1 change: 0 additions & 1 deletion itou/templates/search/includes/siaes_search_results.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
- Les emplois de l'inclusion
</title>
{% include "search/includes/siaes_search_filters_company.html" %}
{% include "search/includes/siaes_search_filters_departments.html" %}
{% include "search/includes/siaes_search_top.html" %}
{% include "search/includes/siaes_search_tabs.html" %}
{% endif %}
6 changes: 4 additions & 2 deletions itou/templates/search/siaes_search_results.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
<div class="s-tabs-01__container container">
<div class="s-tabs-01__row row">
<div class="s-tabs-01__col col-12">
{% include "search/includes/siaes_search_form.html" with form=form is_home=False only %}
<form>
{% include "search/includes/siaes_search_form.html" with form=form is_home=False only %}
</form>
{% include "search/includes/siaes_search_tabs.html" %}
<div class="tab-content">
{% include "search/includes/siaes_search_top.html" %}
<form class="d-block w-100"
hx-get="{% url request.resolver_match.view_name %}"
hx-trigger="change delay:.5s, change from:#id_city"
hx-trigger="change delay:.5s"
hx-indicator="#job-search-results"
hx-target="#job-search-results"
hx-swap="outerHTML"
Expand Down
11 changes: 8 additions & 3 deletions itou/www/search/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
from django_select2.forms import Select2Widget

from itou.cities.models import City
from itou.common_apps.address.departments import DEPARTMENTS, DEPARTMENTS_WITH_DISTRICTS, format_district
from itou.common_apps.address.departments import (
DEPARTMENTS,
DEPARTMENTS_ADJACENCY,
DEPARTMENTS_WITH_DISTRICTS,
format_district,
)
from itou.companies.enums import CompanyKind, ContractNature, ContractType
from itou.jobs.models import ROME_DOMAINS
from itou.utils.widgets import RemoteAutocompleteSelect2Widget
Expand Down Expand Up @@ -62,8 +67,8 @@ def clean_distance(self):
distance = self.fields["distance"].initial
return distance

def add_field_departements(self, departments):
# Build list of choices
def add_field_departements(self, city):
departments = sorted([city.department, *DEPARTMENTS_ADJACENCY[city.department]])
choices = ((department, DEPARTMENTS[department]) for department in departments)
self.fields["departments"] = forms.ChoiceField(
label="Départements",
Expand Down
53 changes: 19 additions & 34 deletions itou/www/search/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ def form_valid(self, form):
)
)

self.add_form_choices(form, siaes, job_descriptions)
form.add_field_departements(city)

self.add_form_choices(form, siaes)

if kinds:
siaes = siaes.filter(kind__in=kinds)
Expand Down Expand Up @@ -157,25 +159,19 @@ def form_valid(self, form):


class EmployerSearchView(EmployerSearchBaseView):
def add_form_choices(self, form, siaes, _job_descriptions):
def add_form_choices(self, form, siaes):
# Extract departments from results to inject them as filters
# The DB contains around 4k SIAE (always fast in Python and no need of iterator())
departments = set()
departments_districts = defaultdict(set)
company_choices = []
for siae in siaes:
company_choices.append((siae.pk, siae.display_name))
# Extract the department of SIAE
if siae.department:
departments.add(siae.department)
# Extract the post_code if it's a district to use it as criteria
if siae.department in DEPARTMENTS_WITH_DISTRICTS:
if int(siae.post_code) <= DEPARTMENTS_WITH_DISTRICTS[siae.department]["max"]:
departments_districts[siae.department].add(siae.post_code)

if departments:
departments = sorted(departments)
form.add_field_departements(departments)
# Extract the post_code if it's a district to use it as criteria
if (
siae.department in DEPARTMENTS_WITH_DISTRICTS
and int(siae.post_code) <= DEPARTMENTS_WITH_DISTRICTS[siae.department]["max"]
):
departments_districts[siae.department].add(siae.post_code)

city = form.cleaned_data["city"]
if departments_districts and city.code_insee in INSEE_CODES_WITH_DISTRICTS:
Expand Down Expand Up @@ -229,27 +225,16 @@ def get_results_page_and_counts(self, siaes, job_descriptions):
class JobDescriptionSearchView(EmployerSearchBaseView):
form_class = JobDescriptionSearchForm

def add_form_choices(self, form, _siaes, job_descriptions):
departments = set()
for job_description in job_descriptions:
department = None
if job_description.location:
department = job_description.location.department
# FIXME(vperron): on a un problème ici, c'est que les gens ne peuvent pas sélectionner
# un arrondissement au moment de la création d'une JobDescription: ils n'ont accès que aux "Cities"
# qui ne détaillent pas les arrondissements.
# Ce qui signifie que l'info est perdue dès le départ, à moins que l'on ne change le parcours
# de création des fiches de poste ou en enrichissant la table "Cities" de tous les arrondissements
# de Paris, Lyon et Marseille.
# En attendant on ne pourra pas trier par arrondissement pour ces offres.
elif job_description.company.department:
department = job_description.company.department
if department:
departments.add(department)
# FIXME(vperron): on a un problème ici, c'est que les gens ne peuvent pas sélectionner
# un arrondissement au moment de la création d'une JobDescription: ils n'ont accès que aux "Cities"
# qui ne détaillent pas les arrondissements.
# Ce qui signifie que l'info est perdue dès le départ, à moins que l'on ne change le parcours
# de création des fiches de poste ou en enrichissant la table "Cities" de tous les arrondissements
# de Paris, Lyon et Marseille.
# En attendant on ne pourra pas trier par arrondissement pour ces offres.

if departments:
departments = sorted(departments)
form.add_field_departements(departments)
def add_form_choices(self, form, siaes):
pass

def get_results_page_and_counts(self, siaes, job_descriptions):
job_descriptions = job_descriptions.order_by(
Expand Down
Loading

0 comments on commit f67056e

Please sign in to comment.