territories-dashboard-lib 0.1.33b1__py3-none-any.whl → 1.1.1.dev7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. territories_dashboard_lib/geo_lib/admin.py +3 -1
  2. territories_dashboard_lib/geo_lib/migrations/0002_geoelement_linked_to_indicator.py +18 -0
  3. territories_dashboard_lib/geo_lib/migrations/0003_geofeature_color_column_geofeature_size_column.py +23 -0
  4. territories_dashboard_lib/geo_lib/models.py +17 -0
  5. territories_dashboard_lib/geo_lib/payloads.py +7 -21
  6. territories_dashboard_lib/geo_lib/views.py +58 -53
  7. territories_dashboard_lib/indicators_lib/enums.py +61 -37
  8. territories_dashboard_lib/indicators_lib/methodo_pdf.py +6 -1
  9. territories_dashboard_lib/indicators_lib/migrations/0004_alter_indicator_min_mesh.py +18 -0
  10. territories_dashboard_lib/indicators_lib/migrations/0005_auto_20251203_1621.py +124 -0
  11. territories_dashboard_lib/indicators_lib/models.py +7 -4
  12. territories_dashboard_lib/indicators_lib/payloads.py +14 -1
  13. territories_dashboard_lib/indicators_lib/query/commons.py +90 -104
  14. territories_dashboard_lib/indicators_lib/query/comparison.py +8 -3
  15. territories_dashboard_lib/indicators_lib/query/details.py +8 -13
  16. territories_dashboard_lib/indicators_lib/query/histogram.py +6 -1
  17. territories_dashboard_lib/indicators_lib/query/indicator_card.py +12 -7
  18. territories_dashboard_lib/indicators_lib/query/top_10.py +13 -12
  19. territories_dashboard_lib/indicators_lib/query/utils.py +9 -0
  20. territories_dashboard_lib/indicators_lib/table.py +15 -12
  21. territories_dashboard_lib/indicators_lib/views.py +49 -59
  22. territories_dashboard_lib/superset_lib/logic.py +24 -25
  23. territories_dashboard_lib/superset_lib/migrations/0002_alter_filter_mesh.py +18 -0
  24. territories_dashboard_lib/tracking_lib/enums.py +1 -0
  25. territories_dashboard_lib/tracking_lib/migrations/0005_alter_page_cmp_territory_mesh_alter_page_submesh_and_more.py +28 -0
  26. territories_dashboard_lib/tracking_lib/migrations/0006_alter_event_name.py +18 -0
  27. territories_dashboard_lib/tracking_lib/payloads.py +4 -2
  28. territories_dashboard_lib/tracking_lib/views.py +7 -6
  29. territories_dashboard_lib/website_lib/conf.py +28 -0
  30. territories_dashboard_lib/website_lib/context_processors.py +5 -6
  31. territories_dashboard_lib/website_lib/migrations/0005_mainconf_meshes.py +20 -0
  32. territories_dashboard_lib/website_lib/models.py +12 -0
  33. territories_dashboard_lib/website_lib/params.py +34 -22
  34. territories_dashboard_lib/website_lib/serializers.py +1 -0
  35. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/anchor.mjs +45 -0
  36. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/comparaison/page.mjs +5 -9
  37. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/details/histogram.mjs +4 -1
  38. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/details/page.mjs +2 -7
  39. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/details/proportions.mjs +3 -2
  40. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/details/top10.mjs +4 -2
  41. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/details/utils.mjs +14 -5
  42. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/dom.mjs +0 -15
  43. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/enums.mjs +13 -10
  44. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/side_panel.mjs +1 -15
  45. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/theme/page.mjs +5 -9
  46. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/react/indicatorMap.bundle.js +1 -1
  47. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/react/sankeyGraph.bundle.js +1 -1
  48. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/react/vendors.bundle.js +1 -1
  49. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/layout/base.css +12 -0
  50. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/layout/header.html +1 -1
  51. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/comparaison/[theme]/page.html +4 -3
  52. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/anchor.html +14 -0
  53. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/geo_params.html +3 -3
  54. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/indicator-card.html +14 -8
  55. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/select_territory.html +32 -0
  56. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/side_panel_geo.html +8 -35
  57. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/details/components/histogram.html +1 -0
  58. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/details/components/no-data.html +1 -0
  59. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/details/components/proportions.html +1 -0
  60. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/details/components/top10.html +1 -0
  61. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/details/map.css +3 -2
  62. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/details/page.css +8 -0
  63. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/details/page.html +9 -8
  64. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/methodo/methodo.js +28 -0
  65. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/methodo/page.html +40 -0
  66. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/themes/page.html +4 -3
  67. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/sitemap/page.html +9 -0
  68. territories_dashboard_lib/website_lib/templatetags/other_filters.py +6 -3
  69. territories_dashboard_lib/website_lib/views.py +100 -0
  70. {territories_dashboard_lib-0.1.33b1.dist-info → territories_dashboard_lib-1.1.1.dev7.dist-info}/METADATA +3 -3
  71. {territories_dashboard_lib-0.1.33b1.dist-info → territories_dashboard_lib-1.1.1.dev7.dist-info}/RECORD +74 -62
  72. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/side_panel_methodo.css +0 -29
  73. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/side_panel_methodo.html +0 -45
  74. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/side_panel_methodo.js +0 -19
  75. {territories_dashboard_lib-0.1.33b1.dist-info → territories_dashboard_lib-1.1.1.dev7.dist-info}/WHEEL +0 -0
  76. {territories_dashboard_lib-0.1.33b1.dist-info → territories_dashboard_lib-1.1.1.dev7.dist-info}/licenses/licence.md +0 -0
  77. {territories_dashboard_lib-0.1.33b1.dist-info → territories_dashboard_lib-1.1.1.dev7.dist-info}/top_level.txt +0 -0
@@ -8,7 +8,7 @@ from territories_dashboard_lib.geo_lib.models import GeoElement, GeoFeature
8
8
 
9
9
  class GeoColumnInLine(admin.TabularInline):
10
10
  model = GeoElement
11
- fields = ["name", "label", "filterable"]
11
+ fields = ["name", "label", "filterable", "linked_to_indicator"]
12
12
  extra = 0
13
13
  formfield_overrides = {
14
14
  models.TextField: {"widget": forms.TextInput()},
@@ -30,6 +30,8 @@ class GeoFeatureForm(forms.ModelForm):
30
30
  "show_on_fr_level",
31
31
  "point_icon_svg",
32
32
  "svg_file",
33
+ "color_column",
34
+ "size_column",
33
35
  ]
34
36
 
35
37
  def clean(self):
@@ -0,0 +1,18 @@
1
+ # Generated by Django 5.2.7 on 2025-10-28 12:30
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('geo_lib', '0001_initial'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='geoelement',
15
+ name='linked_to_indicator',
16
+ field=models.BooleanField(default=False, help_text="Si coché, le filtre de l'indicateur avec le même nom sere relié au filtre de la carte.", verbose_name="Relié aux filtres de l'indicateur"),
17
+ ),
18
+ ]
@@ -0,0 +1,23 @@
1
+ # Generated by Django 5.2.9 on 2026-01-06 16:48
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('geo_lib', '0002_geoelement_linked_to_indicator'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='geofeature',
15
+ name='color_column',
16
+ field=models.TextField(blank=True, help_text="Optionnel. Nom de la colonne qui définit une couleur par point. Les valeurs de la colonne doivent être sous le format #ffffff (hexadecimale). Si renseigné, le champ 'color' ne sera pas utilisé. Seulement pour les points.", null=True, verbose_name='Colonne de la couleur'),
17
+ ),
18
+ migrations.AddField(
19
+ model_name='geofeature',
20
+ name='size_column',
21
+ field=models.TextField(blank=True, help_text="Optionnel. Nom de la colonne qui définit une taille par point. Les valeurs de la colonne doivent être 'sm' (small) ou 'md' (medium). Seulement pour les points.", null=True, verbose_name='Colonne de la taille'),
22
+ ),
23
+ ]
@@ -40,6 +40,18 @@ class GeoFeature(CommonModel):
40
40
  verbose_name="Afficher au niveau France entière",
41
41
  help_text="Décochez si la quantité de données est trop importante pour le niveau France entière.",
42
42
  )
43
+ color_column = models.TextField(
44
+ blank=True,
45
+ null=True,
46
+ verbose_name="Colonne de la couleur",
47
+ help_text="Optionnel. Nom de la colonne qui définit une couleur par point. Les valeurs de la colonne doivent être sous le format #ffffff (hexadecimale). Si renseigné, le champ 'color' ne sera pas utilisé. Seulement pour les points.",
48
+ )
49
+ size_column = models.TextField(
50
+ blank=True,
51
+ null=True,
52
+ verbose_name="Colonne de la taille",
53
+ help_text="Optionnel. Nom de la colonne qui définit une taille par point. Les valeurs de la colonne doivent être 'sm' (small) ou 'md' (medium). Seulement pour les points.",
54
+ )
43
55
 
44
56
  def __str__(self):
45
57
  return self.name
@@ -56,3 +68,8 @@ class GeoElement(CommonModel):
56
68
  name = models.TextField(verbose_name="Nom de la colonne en DB")
57
69
  label = models.TextField()
58
70
  filterable = models.BooleanField(default=True, verbose_name="Filtrable")
71
+ linked_to_indicator = models.BooleanField(
72
+ default=False,
73
+ verbose_name="Relié aux filtres de l'indicateur",
74
+ help_text="Si coché, le filtre de l'indicateur avec le même nom sere relié au filtre de la carte.",
75
+ )
@@ -3,29 +3,18 @@ from typing import List, Optional
3
3
  from pydantic import BaseModel, field_validator
4
4
 
5
5
  from territories_dashboard_lib.commons.types import TerritoryCode
6
- from territories_dashboard_lib.indicators_lib.enums import (
7
- GeoLevel,
8
- MeshLevel,
9
- )
6
+ from territories_dashboard_lib.indicators_lib.enums import MeshLevel
7
+ from territories_dashboard_lib.indicators_lib.payloads import SubMeshPayload
10
8
 
11
9
 
12
- class GeoFeaturesParams(BaseModel):
13
- mesh: MeshLevel
14
- geo_level: GeoLevel
15
- main_territories: List[TerritoryCode]
10
+ class GeoFeaturesPayload(SubMeshPayload):
16
11
  last: Optional[int] = None
17
12
  limit: Optional[int] = 1000
18
13
  feature: int
19
14
 
20
- @field_validator("main_territories", mode="before")
21
- def split_main_territories(cls, v):
22
- if isinstance(v, str):
23
- return v.split(",")
24
- return v
25
-
26
15
 
27
16
  class MainTerritoryParams(BaseModel):
28
- geo_level: GeoLevel
17
+ geo_level: MeshLevel
29
18
  geo_id: List[TerritoryCode]
30
19
 
31
20
  @field_validator("geo_id", mode="before")
@@ -46,14 +35,11 @@ class TerritoriesParams(BaseModel):
46
35
  return v
47
36
 
48
37
 
49
- class TerritoryFeatureParams(BaseModel):
50
- mesh: MeshLevel
51
- geo_level: GeoLevel | None = None
52
- main_territories: List[TerritoryCode] | None = None
38
+ class TerritoryFeaturePayload(SubMeshPayload):
53
39
  codes: List[TerritoryCode] | None = None
54
40
 
55
- @field_validator("main_territories", "codes", mode="before")
56
- def split_main_territories(cls, v):
41
+ @field_validator("codes", mode="before")
42
+ def split_codes(cls, v):
57
43
  if isinstance(v, str):
58
44
  return v.split(",")
59
45
  return v
@@ -7,22 +7,26 @@ from django.http import HttpResponse, JsonResponse
7
7
  from django.shortcuts import get_object_or_404
8
8
  from django.views.decorators.cache import cache_control
9
9
  from django.views.decorators.http import require_GET
10
- from psycopg2.sql import SQL, Identifier, Literal
10
+ from psycopg2.sql import SQL
11
11
 
12
12
  from territories_dashboard_lib.geo_lib.payloads import (
13
- GeoFeaturesParams,
13
+ GeoFeaturesPayload,
14
14
  MainTerritoryParams,
15
15
  SearchTerritoriesParams,
16
16
  TerritoriesParams,
17
- TerritoryFeatureParams,
17
+ TerritoryFeaturePayload,
18
18
  )
19
19
  from territories_dashboard_lib.indicators_lib.enums import (
20
20
  FRANCE_GEOLEVEL_TITLES,
21
- MESH_DB,
22
21
  MeshLevel,
23
22
  )
24
- from territories_dashboard_lib.indicators_lib.query.commons import get_territories_ids
25
- from territories_dashboard_lib.indicators_lib.query.utils import run_custom_query
23
+ from territories_dashboard_lib.indicators_lib.query.commons import (
24
+ get_sub_territories,
25
+ )
26
+ from territories_dashboard_lib.indicators_lib.query.utils import (
27
+ format_sql_codes,
28
+ run_custom_query,
29
+ )
26
30
 
27
31
  from .enums import GeoFeatureType
28
32
  from .models import GeoFeature
@@ -42,14 +46,11 @@ class DateTimeEncoder(json.JSONEncoder):
42
46
  @require_GET
43
47
  @cache_control(max_age=3600)
44
48
  def geo_features_view(request):
45
- params = GeoFeaturesParams(**request.GET.dict())
49
+ payload = GeoFeaturesPayload(**request.GET.dict())
46
50
 
47
- geo_feature = get_object_or_404(GeoFeature, id=params.feature)
48
- geo_level = params.geo_level
49
- mesh = params.mesh
50
- main_territory_codes = params.main_territories
51
- last = params.last
52
- limit = params.limit
51
+ geo_feature = get_object_or_404(GeoFeature, id=payload.feature)
52
+ last = payload.last
53
+ limit = payload.limit
53
54
 
54
55
  last_year_query = (
55
56
  f"SELECT DISTINCT annee FROM {geo_feature.name} ORDER BY annee DESC"
@@ -57,10 +58,16 @@ def geo_features_view(request):
57
58
  years = run_custom_query(last_year_query)
58
59
  last_year = years[0].get("annee")
59
60
 
60
- territories_ids = get_territories_ids(main_territory_codes, geo_level, mesh)
61
-
62
- col_id_name = f"code_{mesh}"
61
+ territories = get_sub_territories(
62
+ submesh=payload.submesh, territory=payload.territory
63
+ )
64
+ territories_ids = [t["code"] for t in territories]
65
+ col_id_name = f"code_{payload.submesh}"
63
66
  columns = [el.name for el in geo_feature.items.all()]
67
+ if geo_feature.color_column:
68
+ columns.append(geo_feature.color_column)
69
+ if geo_feature.size_column:
70
+ columns.append(geo_feature.size_column)
64
71
 
65
72
  coma = ", " if columns else ""
66
73
  select_geo_code = (
@@ -73,8 +80,8 @@ def geo_features_view(request):
73
80
  where_clause = f"WHERE geo.{col_id_name} IN ('{"', '".join(territories_ids)}')"
74
81
  else:
75
82
  where_clause = (
76
- f"JOIN contours_simplified_{geo_level} contours ON ST_intersects(geo.geometry, contours.geometry)\n"
77
- f"WHERE contours.code IN ('{"', '".join(main_territory_codes)}')"
83
+ f"JOIN contours_simplified_{payload.territory.mesh} contours ON ST_intersects(geo.geometry, contours.geometry)\n"
84
+ f"WHERE contours.code IN {payload.territory.sql_codes}"
78
85
  )
79
86
  where_clause += f" AND annee = {last_year}"
80
87
 
@@ -91,16 +98,22 @@ def geo_features_view(request):
91
98
  results = run_custom_query(query)
92
99
 
93
100
  items = geo_feature.items.all()
94
- # Transform results
95
- data = [
96
- {
101
+ data = []
102
+ for r in results:
103
+ properties = {el.label: r.get(el.name) for el in items}
104
+ if geo_feature.color_column or geo_feature.size_column:
105
+ properties["display"] = {}
106
+ if geo_feature.color_column:
107
+ properties["display"]["color"] = r.get(geo_feature.color_column)
108
+ if geo_feature.size_column:
109
+ properties["display"]["size"] = r.get(geo_feature.size_column)
110
+ d = {
97
111
  "type": "Feature",
98
112
  "id": r.get("geo_code"),
99
- "properties": {el.label: r.get(el.name) for el in items},
113
+ "properties": properties,
100
114
  "geometry": json.loads(r.get("geojson")),
101
115
  }
102
- for r in results
103
- ]
116
+ data.append(d)
104
117
 
105
118
  last_queried_order_id = results[-1]["order_id"] if results else None
106
119
 
@@ -146,12 +159,10 @@ def precise_view(request):
146
159
  if not mesh_level:
147
160
  return JsonResponse({"error": "Missing 'mesh' parameter"}, status=400)
148
161
 
149
- mapped_mesh_level = "DEPCOM" if mesh_level == "com" else mesh_level.upper()
150
-
151
162
  query = f"""
152
- SELECT "{mapped_mesh_level}" AS id, ST_asgeojson(geometry) AS polygon
163
+ SELECT code AS id, ST_asgeojson(geometry) AS polygon
153
164
  FROM contours_geo_ign_{mesh_level}
154
- WHERE "{mapped_mesh_level}" IN ('{"', '".join(territories_ids)}')
165
+ WHERE code IN ('{"', '".join(territories_ids)}')
155
166
  """
156
167
 
157
168
  results = run_custom_query(query)
@@ -164,20 +175,20 @@ def precise_view(request):
164
175
  @require_GET
165
176
  @cache_control(max_age=3600)
166
177
  def territories_view(request):
167
- params = TerritoryFeatureParams(**request.GET.dict())
168
- geo_level = params.geo_level
169
- mesh = params.mesh
178
+ payload = TerritoryFeaturePayload(**request.GET.dict())
170
179
 
171
- if params.codes:
172
- territories_ids = params.codes
180
+ if payload.codes:
181
+ territories_ids = payload.codes
173
182
  else:
174
- main_territory_codes = params.main_territories
175
- territories_ids = get_territories_ids(main_territory_codes, geo_level, mesh)
183
+ territories = get_sub_territories(
184
+ submesh=payload.submesh, territory=payload.territory
185
+ )
186
+ territories_ids = [t["code"] for t in territories]
176
187
 
177
188
  query = f"""
178
189
  SELECT code AS id, ST_asgeojson(geometry) AS polygon
179
- FROM contours_simplified_{params.mesh}
180
- WHERE code IN ('{"', '".join(territories_ids)}')
190
+ FROM contours_simplified_{payload.submesh}
191
+ WHERE code IN {format_sql_codes(territories_ids)}
181
192
  """
182
193
 
183
194
  results = run_custom_query(query)
@@ -190,7 +201,7 @@ def territories_view(request):
190
201
 
191
202
 
192
203
  def _fill_territory_li(code, name, mesh):
193
- label = name if mesh == MeshLevel.National else f"{name} - {code}"
204
+ label = name if mesh == MeshLevel.fr else f"{name} - {code}"
194
205
  return f"""<li data-code="{code}" data-name="{name}" onclick="chooseTerritory(this)"><button aria-label="Choisir {label}">{label}</button></li>"""
195
206
 
196
207
 
@@ -201,27 +212,21 @@ def search_territories_view(request):
201
212
  mesh = params.mesh
202
213
  search = params.search
203
214
  offset = params.offset
204
- if mesh == MeshLevel.National:
215
+ if mesh == MeshLevel.fr:
205
216
  lis = []
206
217
  for code, name in FRANCE_GEOLEVEL_TITLES.items():
207
218
  li = _fill_territory_li(code, name, mesh)
208
219
  lis.append(li)
209
220
  return HttpResponse("\n".join(lis))
210
- mesh_db = MESH_DB[mesh]
221
+ table_name = f"arbo_{mesh}"
211
222
  pagination = 20
212
- query = SQL("""
213
- SELECT DISTINCT {code} as code, {name} as name FROM arborescence_geo
214
- WHERE unaccent({name}) || {code} ILIKE unaccent(%s)
215
- AND "FR" <> 'ETR'
216
- ORDER BY {name}, {code}
217
- LIMIT {limit} OFFSET {offset};
218
- ;
219
- """).format(
220
- code=Identifier(mesh_db),
221
- name=Identifier(f"NOM_{mesh_db}"),
222
- offset=Literal(offset * pagination),
223
- limit=Literal(pagination),
224
- )
223
+ query = SQL(f"""
224
+ SELECT DISTINCT code, name FROM {table_name}
225
+ WHERE unaccent(name) || code ILIKE unaccent(%s)
226
+ AND code !~ '^[A-Z]{{3}}$'
227
+ ORDER BY name, code
228
+ LIMIT {pagination} OFFSET {offset * pagination};
229
+ """)
225
230
  territories = run_custom_query(query, [f"%{search}%"])
226
231
  lis = []
227
232
  for territory in territories:
@@ -1,3 +1,5 @@
1
+ from typing import List
2
+
1
3
  from django.conf import settings
2
4
  from django.db import models
3
5
 
@@ -8,19 +10,48 @@ class AggregationFunctions(models.TextChoices):
8
10
 
9
11
 
10
12
  class MeshLevel(models.TextChoices):
11
- National = "fr"
12
- Region = "reg"
13
- Department = "dep"
14
- Epci = "epci"
15
- Town = "com"
16
-
17
-
18
- class GeoLevel(models.TextChoices):
19
- France = "fr"
20
- Region = "reg"
21
- Department = "dep"
22
- Epci = "epci"
23
- Town = "com"
13
+ fr = "fr"
14
+ reg = "reg"
15
+ dep = "dep"
16
+ epci = "epci"
17
+ com = "com"
18
+ aom = "aom"
19
+
20
+
21
+ STANDARD_MESHES = [
22
+ MeshLevel.fr,
23
+ MeshLevel.reg,
24
+ MeshLevel.dep,
25
+ MeshLevel.epci,
26
+ MeshLevel.com,
27
+ ]
28
+
29
+ ALL_MESHES_ABSOLUTE = [
30
+ MeshLevel.fr,
31
+ MeshLevel.reg,
32
+ MeshLevel.dep,
33
+ MeshLevel.aom,
34
+ MeshLevel.epci,
35
+ MeshLevel.com,
36
+ ]
37
+
38
+
39
+ MESHES_ORDERED_FOR_PRESENTATION = [
40
+ MeshLevel.fr,
41
+ MeshLevel.reg,
42
+ MeshLevel.dep,
43
+ MeshLevel.epci,
44
+ MeshLevel.com,
45
+ MeshLevel.aom,
46
+ ]
47
+
48
+
49
+ def order_meshes_for_presentation(meshes: List[MeshLevel]) -> List[MeshLevel]:
50
+ """
51
+ Orders a list of MeshLevel values according to MESHES_ORDERED_FOR_PRESENTATION.
52
+ """
53
+ order_index = {mesh: i for i, mesh in enumerate(MESHES_ORDERED_FOR_PRESENTATION)}
54
+ return sorted(meshes, key=lambda mesh: order_index[mesh])
24
55
 
25
56
 
26
57
  class FranceGeoLevel(models.TextChoices):
@@ -42,30 +73,33 @@ FRANCE_DB_VALUES = {
42
73
  }
43
74
 
44
75
 
45
- DEFAULT_MESH = MeshLevel.Region
76
+ DEFAULT_MESH = MeshLevel.reg
46
77
 
47
- MESH_TITLES = {
48
- MeshLevel.National: "France entière",
49
- MeshLevel.Region: "Région",
50
- MeshLevel.Department: "Département",
51
- MeshLevel.Epci: "Intercommunalité",
52
- MeshLevel.Town: "Commune",
78
+ MESHES_SHORT_TITLES = {
79
+ MeshLevel.fr: "France",
80
+ MeshLevel.reg: "Région",
81
+ MeshLevel.dep: "Département",
82
+ MeshLevel.epci: "Intercommunalité",
83
+ MeshLevel.com: "Commune",
84
+ MeshLevel.aom: "AOM",
53
85
  }
54
86
 
55
- MESH_DB = {
56
- MeshLevel.Region: "REG",
57
- MeshLevel.Department: "DEP",
58
- MeshLevel.Epci: "EPCI",
59
- MeshLevel.Town: "DEPCOM",
87
+ MESHES_LONG_TITLES = {
88
+ MeshLevel.fr: "France",
89
+ MeshLevel.reg: "Région",
90
+ MeshLevel.dep: "Département",
91
+ MeshLevel.epci: "Intercommunalité",
92
+ MeshLevel.com: "Commune",
93
+ MeshLevel.aom: "Autorité Organisatrice de la Mobilité",
60
94
  }
61
95
 
62
96
 
63
97
  def get_miminum_mesh():
64
98
  try:
65
99
  town_mesh_is_disabled = settings.DISABLE_TOWN_MESH
66
- return MeshLevel.Epci if town_mesh_is_disabled else MeshLevel.Town
100
+ return MeshLevel.epci if town_mesh_is_disabled else MeshLevel.com
67
101
  except AttributeError:
68
- return MeshLevel.Town
102
+ return MeshLevel.com
69
103
 
70
104
 
71
105
  def get_allow_same_mesh():
@@ -73,13 +107,3 @@ def get_allow_same_mesh():
73
107
  return bool(settings.ALLOW_SAME_MESH)
74
108
  except AttributeError:
75
109
  return False
76
-
77
-
78
- def get_all_meshes():
79
- min_mesh = get_miminum_mesh()
80
- meshes = []
81
- for m in MeshLevel:
82
- meshes.append(m)
83
- if m == min_mesh:
84
- break
85
- return meshes
@@ -6,6 +6,8 @@ from tempfile import NamedTemporaryFile
6
6
  from django.conf import settings
7
7
  from markdown import markdown
8
8
 
9
+ from territories_dashboard_lib.website_lib.conf import get_main_conf
10
+
9
11
 
10
12
  def _html_to_pdf(html_content, output_pdf_path):
11
13
  try:
@@ -36,6 +38,7 @@ def _generate_pdf_from_methodo(indicator, output_path):
36
38
  logo_path = os.path.join(settings.BASE_DIR, relative_logo_path)
37
39
  with open(logo_path, "rb") as fd:
38
40
  encoded_logo = base64.b64encode(fd.read()).decode("utf-8")
41
+ main_conf = get_main_conf()
39
42
  html = (
40
43
  """
41
44
  <!DOCTYPE html>
@@ -59,7 +62,9 @@ def _generate_pdf_from_methodo(indicator, output_path):
59
62
  + """
60
63
  </td>
61
64
  <td style="padding-left: 64px;">
62
- <div style="font-size: 30px; font-weight: 600; margin-bottom: 8px;">Tableau de bord des mobilités durables</div>
65
+ """
66
+ + f'<div style="font-size: 30px; font-weight: 600; margin-bottom: 8px;">{main_conf.title}</div>'
67
+ + """
63
68
  <div>Fiche méthodologique</div>
64
69
  </td>
65
70
  </tr>
@@ -0,0 +1,18 @@
1
+ # Generated by Django 5.2.8 on 2025-12-03 13:30
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('indicators_lib', '0003_indicator_short_title'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='indicator',
15
+ name='min_mesh',
16
+ field=models.TextField(choices=[('fr', 'Fr'), ('reg', 'Reg'), ('dep', 'Dep'), ('epci', 'Epci'), ('com', 'Com'), ('aom', 'Aom')], default='com'),
17
+ ),
18
+ ]
@@ -0,0 +1,124 @@
1
+ # Generated by Django 5.2.9 on 2025-12-03 15:21
2
+
3
+ import base64
4
+ import os
5
+ import subprocess
6
+ from tempfile import NamedTemporaryFile
7
+
8
+ from django.conf import settings
9
+ from django.db import migrations
10
+ from markdown import markdown
11
+
12
+
13
+ def _html_to_pdf(html_content, output_pdf_path):
14
+ try:
15
+ process = subprocess.Popen(
16
+ [
17
+ "wkhtmltopdf",
18
+ "-",
19
+ output_pdf_path,
20
+ ], # '-' tells wkhtmltopdf to read from stdin
21
+ stdin=subprocess.PIPE,
22
+ stdout=subprocess.PIPE,
23
+ stderr=subprocess.PIPE,
24
+ )
25
+ stdout, stderr = process.communicate(input=html_content.encode("utf-8"))
26
+ if process.returncode != 0:
27
+ print("Error:", stderr.decode("utf-8"))
28
+ else:
29
+ print(f"PDF successfully created at: {output_pdf_path}")
30
+ except FileNotFoundError:
31
+ print("Error: wkhtmltopdf not found. Ensure it is installed and in your PATH.")
32
+ except Exception as e:
33
+ print(f"An error occurred: {e}")
34
+
35
+
36
+ def _generate_pdf_from_methodo(main_conf, indicator, output_path):
37
+ html = markdown(indicator.methodo)
38
+ relative_logo_path = "whitenoise_root/ministere_logo.png"
39
+ logo_path = os.path.join(settings.BASE_DIR, relative_logo_path)
40
+ with open(logo_path, "rb") as fd:
41
+ encoded_logo = base64.b64encode(fd.read()).decode("utf-8")
42
+ html = (
43
+ """
44
+ <!DOCTYPE html>
45
+ <html lang="fr">
46
+ <head>
47
+ <meta charset="UTF-8">
48
+ <title>Accents Test</title>
49
+ <style>
50
+ body {
51
+ font-family: Tahoma, Arial, sans-serif;
52
+ }
53
+ </style>
54
+ </head>
55
+ <body>
56
+ <header>
57
+ <table>
58
+ <tr>
59
+ <td>
60
+ """
61
+ + f'<img width="150px" src="data:image/png;base64,{encoded_logo}"/>'
62
+ + """
63
+ </td>
64
+ <td style="padding-left: 64px;">
65
+ """
66
+ + f'<div style="font-size: 30px; font-weight: 600; margin-bottom: 8px;">{main_conf.title}</div>'
67
+ + """
68
+ <div>Fiche méthodologique</div>
69
+ </td>
70
+ </tr>
71
+ </table>
72
+ </header>
73
+ <main>
74
+ """
75
+ + f"<h1>{indicator.title}</h1>"
76
+ + f"<p>Thématique : {indicator.sub_theme.theme.title} / {indicator.sub_theme.title}</p>"
77
+ + html
78
+ + """
79
+ </main>
80
+ </body>
81
+ </html>
82
+ """
83
+ )
84
+ _html_to_pdf(html, output_path)
85
+
86
+
87
+ def reset_methodo_file(main_conf, indicator):
88
+ # Create a temporary file for the PDF
89
+ with NamedTemporaryFile(delete=False, suffix=".pdf") as temp_file:
90
+ pdf_path = temp_file.name
91
+
92
+ # Generate the PDF
93
+ _generate_pdf_from_methodo(main_conf, indicator, pdf_path)
94
+
95
+ # Read the binary content of the generated PDF file
96
+ with open(pdf_path, "rb") as pdf_file:
97
+ pdf_content = pdf_file.read()
98
+
99
+ # Save the binary content to the BinaryField
100
+ indicator.methodo_file = pdf_content
101
+ indicator.save()
102
+
103
+ # Clean up temporary file
104
+ os.remove(pdf_path)
105
+
106
+
107
+ def reset_indicators_methodo_pdf(apps, schema_editor):
108
+ MainConf = apps.get_model("website_lib", "MainConf")
109
+ Indicator = apps.get_model("indicators_lib", "Indicator")
110
+ main_conf = MainConf.objects.first()
111
+ if main_conf:
112
+ for indicator in Indicator.objects.all():
113
+ reset_methodo_file(main_conf, indicator)
114
+
115
+
116
+ class Migration(migrations.Migration):
117
+ dependencies = [
118
+ ("indicators_lib", "0004_alter_indicator_min_mesh"),
119
+ ("website_lib", "0005_mainconf_meshes"),
120
+ ]
121
+
122
+ operations = [
123
+ migrations.RunPython(reset_indicators_methodo_pdf, migrations.RunPython.noop),
124
+ ]
@@ -10,9 +10,6 @@ from territories_dashboard_lib.indicators_lib.refresh_filters import refresh_fil
10
10
 
11
11
 
12
12
  class Theme(CommonModel):
13
- # TODO: make it primary key
14
- # django.db.utils.OperationalError: foreign key mismatch -
15
- # "tdbmd_indicators_indicatorsubtheme" referencing "tdbmd_indicators_indicatortheme"
16
13
  ordering = models.IntegerField(default=0)
17
14
  ordering.verbose_name = "Ordre dans la sidebar"
18
15
  name = models.CharField(max_length=64, unique=True)
@@ -106,7 +103,7 @@ class Indicator(CommonModel):
106
103
  # Indicator's DB attributes
107
104
  db_table_prefix = models.CharField(max_length=128)
108
105
  db_table_prefix.verbose_name = "Préfixe dans la DB"
109
- min_mesh = models.TextField(choices=MeshLevel.choices, default=MeshLevel.Town)
106
+ min_mesh = models.TextField(choices=MeshLevel.choices, default=MeshLevel.com)
110
107
  is_composite = models.BooleanField(default=False)
111
108
  is_composite.verbose_name = "Indicateur composite"
112
109
  show_alternative = models.BooleanField(
@@ -180,6 +177,12 @@ class Indicator(CommonModel):
180
177
 
181
178
  get_theme_title.short_description = "Thème"
182
179
 
180
+ def table_name(indicator, mesh: MeshLevel, *, flows: bool = False):
181
+ table_prefix = (
182
+ indicator.flows_db_table_prefix if flows else indicator.db_table_prefix
183
+ )
184
+ return f"{table_prefix}_{mesh}"
185
+
183
186
  class Meta:
184
187
  ordering = ("sub_theme", "index_in_theme")
185
188
  verbose_name = "3 - Indicateur"