territories-dashboard-lib 0.1.0__py3-none-any.whl → 0.1.2__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.

Potentially problematic release.


This version of territories-dashboard-lib might be problematic. Click here for more details.

Files changed (138) hide show
  1. territories_dashboard_lib/commons/__init__.py +0 -0
  2. territories_dashboard_lib/commons/decorators.py +36 -0
  3. territories_dashboard_lib/commons/models.py +9 -0
  4. territories_dashboard_lib/geo_lib/__init__.py +0 -0
  5. territories_dashboard_lib/geo_lib/admin.py +64 -0
  6. territories_dashboard_lib/geo_lib/enums.py +7 -0
  7. territories_dashboard_lib/geo_lib/migrations/0001_initial.py +51 -0
  8. territories_dashboard_lib/geo_lib/migrations/__init__.py +0 -0
  9. territories_dashboard_lib/geo_lib/models.py +58 -0
  10. territories_dashboard_lib/geo_lib/urls.py +27 -0
  11. territories_dashboard_lib/geo_lib/views.py +239 -0
  12. territories_dashboard_lib/indicators_lib/__init__.py +0 -0
  13. territories_dashboard_lib/indicators_lib/admin.py +140 -0
  14. territories_dashboard_lib/indicators_lib/enums.py +59 -0
  15. territories_dashboard_lib/indicators_lib/export.py +29 -0
  16. territories_dashboard_lib/indicators_lib/format.py +34 -0
  17. territories_dashboard_lib/indicators_lib/methodo_pdf.py +99 -0
  18. territories_dashboard_lib/indicators_lib/migrations/0001_initial.py +138 -0
  19. territories_dashboard_lib/indicators_lib/migrations/__init__.py +0 -0
  20. territories_dashboard_lib/indicators_lib/models.py +230 -0
  21. territories_dashboard_lib/indicators_lib/payloads.py +54 -0
  22. territories_dashboard_lib/indicators_lib/query/commons.py +223 -0
  23. territories_dashboard_lib/indicators_lib/query/comparison.py +70 -0
  24. territories_dashboard_lib/indicators_lib/query/details.py +64 -0
  25. territories_dashboard_lib/indicators_lib/query/histogram.py +82 -0
  26. territories_dashboard_lib/indicators_lib/query/indicator_card.py +102 -0
  27. territories_dashboard_lib/indicators_lib/query/top_10.py +100 -0
  28. territories_dashboard_lib/indicators_lib/query/utils.py +20 -0
  29. territories_dashboard_lib/indicators_lib/refresh_filters.py +17 -0
  30. territories_dashboard_lib/indicators_lib/table.py +154 -0
  31. territories_dashboard_lib/indicators_lib/urls.py +97 -0
  32. territories_dashboard_lib/indicators_lib/views.py +490 -0
  33. territories_dashboard_lib/superset_lib/__init__.py +0 -0
  34. territories_dashboard_lib/superset_lib/admin.py +22 -0
  35. territories_dashboard_lib/superset_lib/guest_token.py +64 -0
  36. territories_dashboard_lib/superset_lib/logic.py +67 -0
  37. territories_dashboard_lib/superset_lib/migrations/0001_initial.py +45 -0
  38. territories_dashboard_lib/superset_lib/migrations/__init__.py +0 -0
  39. territories_dashboard_lib/superset_lib/models.py +52 -0
  40. territories_dashboard_lib/superset_lib/serializers.py +10 -0
  41. territories_dashboard_lib/superset_lib/urls.py +10 -0
  42. territories_dashboard_lib/superset_lib/views.py +19 -0
  43. territories_dashboard_lib/tracking_lib/__init__.py +0 -0
  44. territories_dashboard_lib/tracking_lib/enums.py +7 -0
  45. territories_dashboard_lib/tracking_lib/logic.py +78 -0
  46. territories_dashboard_lib/tracking_lib/migrations/0001_initial.py +45 -0
  47. territories_dashboard_lib/tracking_lib/migrations/__init__.py +0 -0
  48. territories_dashboard_lib/tracking_lib/models.py +79 -0
  49. territories_dashboard_lib/website_lib/__init__.py +0 -0
  50. territories_dashboard_lib/website_lib/admin.py +40 -0
  51. territories_dashboard_lib/website_lib/context_processors.py +27 -0
  52. territories_dashboard_lib/website_lib/forms.py +47 -0
  53. territories_dashboard_lib/website_lib/migrations/0001_initial.py +91 -0
  54. territories_dashboard_lib/website_lib/migrations/__init__.py +0 -0
  55. territories_dashboard_lib/website_lib/models.py +148 -0
  56. territories_dashboard_lib/website_lib/navigation.py +124 -0
  57. territories_dashboard_lib/website_lib/params.py +268 -0
  58. territories_dashboard_lib/website_lib/serializers.py +105 -0
  59. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/css/website.css +956 -0
  60. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/chart.js +13 -0
  61. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/chartjs-plugin-datalabels.js +1 -0
  62. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/html2canvas.js +20 -0
  63. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/comparaison/data.mjs +62 -0
  64. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/comparaison/history.mjs +98 -0
  65. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/comparaison/main-values.mjs +30 -0
  66. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/comparaison/page.mjs +105 -0
  67. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/comparaison/territory-chart.mjs +141 -0
  68. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/details/data.mjs +59 -0
  69. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/details/histogram.mjs +130 -0
  70. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/details/map.mjs +25 -0
  71. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/details/page.mjs +96 -0
  72. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/details/proportions.mjs +77 -0
  73. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/details/sankey.mjs +27 -0
  74. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/details/table.mjs +229 -0
  75. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/details/top10.mjs +76 -0
  76. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/details/utils.mjs +8 -0
  77. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/dom.mjs +92 -0
  78. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/enums.mjs +104 -0
  79. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/export-graph.mjs +15 -0
  80. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/export.mjs +20 -0
  81. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/filters.mjs +159 -0
  82. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/format.mjs +54 -0
  83. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/side_panel.mjs +103 -0
  84. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/theme/history.mjs +89 -0
  85. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/theme/main-value.mjs +22 -0
  86. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/theme/page.mjs +162 -0
  87. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/theme/statistics.mjs +42 -0
  88. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/utils.mjs +93 -0
  89. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/static/page.mjs +35 -0
  90. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/patternomaly.js +1452 -0
  91. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/react18.js +31 -0
  92. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/reactdom18.js +267 -0
  93. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/supersetEmbed.js +355 -0
  94. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/react/indicatorMap.bundle.js +2 -0
  95. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/react/sankeyGraph.bundle.js +2 -0
  96. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/react/vendors-node_modules_mapbox-gl_dist_mapbox-gl_js.bundle.js +2 -0
  97. territories_dashboard_lib/website_lib/static_content.py +20 -0
  98. territories_dashboard_lib/website_lib/templates/admin/indicators_lib/indicator/change_form.html +8 -0
  99. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/404.html +10 -0
  100. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/500.html +10 -0
  101. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/layout/base.html +42 -0
  102. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/layout/base.js +47 -0
  103. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/layout/footer.html +96 -0
  104. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/layout/header.html +101 -0
  105. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/layout/header.js +102 -0
  106. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/comparaison/[theme]/components/indicateur-card.html +48 -0
  107. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/comparaison/[theme]/page.html +52 -0
  108. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/chart-buttons.html +29 -0
  109. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/geo_params.html +71 -0
  110. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/indicator-card.html +52 -0
  111. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/loader.html +28 -0
  112. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/side_panel.html +27 -0
  113. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/side_panel_geo.html +80 -0
  114. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/side_panel_geo.js +85 -0
  115. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/side_panel_methodo.html +45 -0
  116. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/side_panel_methodo.js +19 -0
  117. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/themes-list.html +9 -0
  118. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/themes-nav.html +33 -0
  119. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/title.html +28 -0
  120. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/details/components/filters-reminder.html +19 -0
  121. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/details/components/table.html +123 -0
  122. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/details/page.html +166 -0
  123. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/themes/components/extremum.html +15 -0
  124. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/themes/components/indicateur-card.html +76 -0
  125. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/themes/page.html +66 -0
  126. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/lexique/page.html +17 -0
  127. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/page.html +14 -0
  128. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/sitemap/page.html +71 -0
  129. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/static/page.html +16 -0
  130. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/superset/page.html +55 -0
  131. territories_dashboard_lib/website_lib/templatetags/htmlparams.py +75 -0
  132. territories_dashboard_lib/website_lib/templatetags/other_filters.py +30 -0
  133. territories_dashboard_lib/website_lib/views.py +212 -0
  134. {territories_dashboard_lib-0.1.0.dist-info → territories_dashboard_lib-0.1.2.dist-info}/METADATA +1 -1
  135. territories_dashboard_lib-0.1.2.dist-info/RECORD +138 -0
  136. territories_dashboard_lib-0.1.0.dist-info/RECORD +0 -5
  137. {territories_dashboard_lib-0.1.0.dist-info → territories_dashboard_lib-0.1.2.dist-info}/WHEEL +0 -0
  138. {territories_dashboard_lib-0.1.0.dist-info → territories_dashboard_lib-0.1.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,490 @@
1
+ import json
2
+
3
+ from django.http import Http404, HttpResponse, JsonResponse
4
+ from django.shortcuts import get_object_or_404, render
5
+ from django.views.decorators.cache import cache_control
6
+ from django.views.decorators.http import require_GET
7
+
8
+ from territories_dashboard_lib.commons.decorators import use_payload
9
+ from territories_dashboard_lib.tracking_lib.enums import EventType
10
+ from territories_dashboard_lib.tracking_lib.logic import track_event
11
+
12
+ from .enums import MESH_TITLES
13
+ from .export import export_to_csv
14
+ from .format import format_data, format_indicator_value
15
+ from .models import Indicator
16
+ from .payloads import (
17
+ BasePayload,
18
+ ComparisonQueryPayload,
19
+ FlowsPayload,
20
+ IndicatorTablePayload,
21
+ OptionalComparisonQueryPayload,
22
+ SubMeshPayload,
23
+ )
24
+ from .query.commons import (
25
+ get_sub_territories,
26
+ get_territory_name,
27
+ get_values_for_territory,
28
+ )
29
+ from .query.comparison import get_comparison_values_and_buckets
30
+ from .query.details import get_proportions_chart, get_values_for_submesh_territories
31
+ from .query.histogram import get_indicator_histogram_data
32
+ from .query.indicator_card import (
33
+ get_geography_statistics_values_for_indicator,
34
+ get_names_from_codes,
35
+ )
36
+ from .query.top_10 import get_indicator_top_10_data
37
+ from .query.utils import run_custom_query
38
+ from .table import (
39
+ get_count_and_data_for_indicator_table,
40
+ get_export_indicator_table_values,
41
+ )
42
+
43
+
44
+ def download_indicator_methodo_view(request, name):
45
+ """View to download the methodology file for an indicator."""
46
+ indicator = get_object_or_404(Indicator, name=name)
47
+
48
+ if not indicator.methodo_file:
49
+ raise Http404("No methodology file available for this indicator.")
50
+
51
+ response = HttpResponse(indicator.methodo_file, content_type="application/pdf")
52
+ response["Content-Disposition"] = "attachment; filename=methodo.pdf"
53
+ response = track_event(
54
+ request=request,
55
+ response=response,
56
+ event_name=EventType.download,
57
+ data={"indicator": indicator.name, "objet": "methodo"},
58
+ )
59
+ return response
60
+
61
+
62
+ @cache_control(max_age=3600)
63
+ @use_payload(SubMeshPayload)
64
+ def indicator_statistics_view(request, name, payload):
65
+ indicator = get_object_or_404(Indicator, name=name)
66
+ filters = get_filters(request, indicator)
67
+ query = get_geography_statistics_values_for_indicator(
68
+ indicator,
69
+ payload.territory,
70
+ payload.submesh,
71
+ filters,
72
+ )
73
+ rows = run_custom_query(query)
74
+ if not rows:
75
+ return HttpResponse("{}")
76
+ data = rows[0]
77
+ data = get_names_from_codes(data, payload.submesh)
78
+ return HttpResponse(json.dumps(data))
79
+
80
+
81
+ def get_values(indicator, payload, filters):
82
+ query = get_values_for_territory(
83
+ indicator,
84
+ payload.territory,
85
+ filters,
86
+ )
87
+ rows = run_custom_query(query)
88
+ results = {"values": rows}
89
+ if payload.cmp_territory:
90
+ query = get_values_for_territory(
91
+ indicator,
92
+ payload.cmp_territory,
93
+ filters,
94
+ )
95
+ cmp_rows = run_custom_query(query)
96
+ results["cmp_values"] = cmp_rows
97
+ return results
98
+
99
+
100
+ def get_filters(request, indicator):
101
+ filters = {}
102
+ for dimension in indicator.dimensions.all():
103
+ input_filters = request.GET.getlist(dimension.db_name)
104
+ possible_filters = [f.db_name for f in dimension.filters.all()]
105
+ filters[dimension.db_name] = [f for f in input_filters if f in possible_filters]
106
+ return filters
107
+
108
+
109
+ @require_GET
110
+ @cache_control(max_age=3600)
111
+ @use_payload(OptionalComparisonQueryPayload)
112
+ def indicator_values_view(request, name, payload):
113
+ indicator = get_object_or_404(Indicator, name=name)
114
+ filters = get_filters(request, indicator)
115
+ results = get_values(indicator, payload, filters)
116
+ return HttpResponse(json.dumps(results))
117
+
118
+
119
+ @require_GET
120
+ @use_payload(OptionalComparisonQueryPayload)
121
+ def indicator_values_export_view(request, name, payload):
122
+ indicator = get_object_or_404(Indicator, name=name)
123
+ filters = get_filters(request, indicator)
124
+ results = get_values(indicator, payload, filters)
125
+ export_values = {}
126
+ territory_name = get_territory_name(payload.territory)
127
+ for value in results["values"]:
128
+ export_values[value["annee"]] = {
129
+ "Année": value["annee"],
130
+ f"Valeur {territory_name} ({indicator.unite})": value["valeur"],
131
+ }
132
+ if results.get("cmp_values") is not None:
133
+ cmp_territory_name = get_territory_name(payload.cmp_territory)
134
+ for value in results["cmp_values"]:
135
+ export_values[value["annee"]][
136
+ f"Valeur {cmp_territory_name} ({indicator.unite})"
137
+ ] = value["valeur"]
138
+ return export_to_csv(
139
+ request, indicator, "comparaison-historique", list(export_values.values())
140
+ )
141
+
142
+
143
+ @require_GET
144
+ @cache_control(max_age=3600)
145
+ @use_payload(SubMeshPayload)
146
+ def indicator_submesh_territories_view(request, name, payload):
147
+ indicator = get_object_or_404(Indicator, name=name)
148
+ filters = get_filters(request, indicator)
149
+ data = get_values_for_submesh_territories(
150
+ indicator, payload.submesh, payload.territory, filters
151
+ )
152
+ return HttpResponse(json.dumps(data))
153
+
154
+
155
+ @cache_control(max_age=3600)
156
+ @use_payload(BasePayload)
157
+ def proportions_chart_view(request, name, payload):
158
+ indicator = get_object_or_404(Indicator, name=name)
159
+ if indicator.dimensions.count() == 0:
160
+ return HttpResponse(status=400)
161
+ filters = get_filters(request, indicator)
162
+ rows = get_proportions_chart(
163
+ indicator,
164
+ payload.territory,
165
+ filters,
166
+ )
167
+ return HttpResponse(json.dumps({"values": rows}))
168
+
169
+
170
+ @require_GET
171
+ @use_payload(BasePayload)
172
+ def indicator_proportions_export_view(request, name, payload):
173
+ indicator = get_object_or_404(Indicator, name=name)
174
+ if indicator.dimensions.count() == 0:
175
+ return HttpResponse(status=400)
176
+ filters = get_filters(request, indicator)
177
+ rows = get_proportions_chart(
178
+ indicator,
179
+ payload.territory,
180
+ filters,
181
+ )
182
+ rows = [
183
+ {"Dimension": r["label"], f"Valeur {indicator.unite}": r["data"][0]}
184
+ for r in rows
185
+ ]
186
+ return export_to_csv(request, indicator, "repartition-dimension", rows)
187
+
188
+
189
+ @cache_control(max_age=3600)
190
+ @use_payload(SubMeshPayload)
191
+ def indicator_histogram_view(request, name, payload):
192
+ indicator = get_object_or_404(Indicator, name=name)
193
+ filters = get_filters(request, indicator)
194
+ data = get_indicator_histogram_data(
195
+ indicator,
196
+ payload.territory,
197
+ payload.submesh,
198
+ filters,
199
+ )
200
+ return HttpResponse(json.dumps(data))
201
+
202
+
203
+ @cache_control(max_age=3600)
204
+ @use_payload(SubMeshPayload)
205
+ def indicator_histogram_export_view(request, name, payload):
206
+ indicator = get_object_or_404(Indicator, name=name)
207
+ filters = get_filters(request, indicator)
208
+ data = get_indicator_histogram_data(
209
+ indicator,
210
+ payload.territory,
211
+ payload.submesh,
212
+ filters,
213
+ )
214
+ rows = []
215
+ for index, decile in enumerate(data["deciles"]):
216
+ row = {}
217
+ next_decile = (
218
+ format_indicator_value(data["deciles"][index + 1])
219
+ if index < len(data["deciles"]) - 1
220
+ else "+"
221
+ )
222
+ row["Décile"] = f"{format_indicator_value(decile)} - {next_decile}"
223
+ row["Nombre de territoires"] = data["datasetsHistogramBarChart"]["data"][index][
224
+ "y"
225
+ ]
226
+ row["Commentaire"] = data["datasetsHistogramBarChart"]["comments"][
227
+ index
228
+ ].replace("\n", " | ")
229
+ rows.append(row)
230
+ return export_to_csv(request, indicator, "repartition-valeurs", rows)
231
+
232
+
233
+ @cache_control(max_age=3600)
234
+ @use_payload(SubMeshPayload)
235
+ def indicator_top_10_view(request, name, payload):
236
+ indicator = get_object_or_404(Indicator, name=name)
237
+ filters = get_filters(request, indicator)
238
+ data, _ = get_indicator_top_10_data(
239
+ indicator,
240
+ payload.territory,
241
+ payload.submesh,
242
+ filters,
243
+ )
244
+ return HttpResponse(json.dumps(data))
245
+
246
+
247
+ @require_GET
248
+ @use_payload(SubMeshPayload)
249
+ def indicator_top_10_export_view(request, name, payload):
250
+ indicator = get_object_or_404(Indicator, name=name)
251
+ filters = get_filters(request, indicator)
252
+ _, csv_data = get_indicator_top_10_data(
253
+ indicator,
254
+ payload.territory,
255
+ payload.submesh,
256
+ filters,
257
+ )
258
+ return export_to_csv(request, indicator, "top_10", csv_data)
259
+
260
+
261
+ @require_GET
262
+ @cache_control(max_age=3600)
263
+ @use_payload(FlowsPayload)
264
+ def flows_view(request, payload):
265
+ flows_table = f"{payload.prefix}_{payload.submesh}"
266
+
267
+ # Query to get the latest year
268
+ last_year_query = f"SELECT DISTINCT annee FROM {flows_table} ORDER BY annee DESC"
269
+ last_year = run_custom_query(last_year_query)[0]["annee"]
270
+
271
+ mapped_geo_level = (
272
+ "DEPCOM" if payload.territory.mesh == "com" else payload.territory.mesh.upper()
273
+ )
274
+ mapped_mesh_level = (
275
+ "DEPCOM" if payload.submesh == "com" else payload.submesh.upper()
276
+ )
277
+
278
+ codes = ", ".join(f"'{code.strip()}'" for code in payload.territory.id.split(","))
279
+
280
+ # Geo query
281
+ geo_query = f"""
282
+ SELECT DISTINCT
283
+ arbo."{mapped_mesh_level}" as territory_id,
284
+ arbo."NOM_{mapped_mesh_level}" as territory_name,
285
+ ST_ASGEOJSON(ST_CENTROID(contours.geometry)) as center
286
+ FROM arborescence_geo AS arbo
287
+ JOIN contours_simplified_{payload.submesh} as contours
288
+ ON arbo."{mapped_mesh_level}" = contours.code
289
+ WHERE arbo."{mapped_geo_level}" IN ({codes})
290
+ """
291
+ territories = run_custom_query(geo_query)
292
+ territories_ids = ", ".join(f"'{t['territory_id'].strip()}'" for t in territories)
293
+
294
+ dimension_value = (
295
+ f"{payload.dimension} as dimension"
296
+ if payload.dimension
297
+ else "'all' as dimension"
298
+ )
299
+
300
+ # Values query
301
+ values_query = f"""
302
+ SELECT
303
+ code_{payload.submesh}_1 as territory_1_id,
304
+ code_{payload.submesh}_2 as territory_2_id,
305
+ CAST(valeur AS int) as value,
306
+ {dimension_value}
307
+ FROM {flows_table} flows
308
+ WHERE annee = {last_year}
309
+ AND (
310
+ code_{payload.submesh}_1 IN ({territories_ids})
311
+ OR code_{payload.submesh}_2 IN ({territories_ids})
312
+ )
313
+ """
314
+ row_values = run_custom_query(values_query)
315
+ territories_dict = {
316
+ t["territory_id"]: {
317
+ "name": t["territory_name"],
318
+ "code": t["territory_id"],
319
+ "center": json.loads(t["center"]),
320
+ }
321
+ for t in territories
322
+ }
323
+
324
+ external_territories_ids = set()
325
+ for row in row_values:
326
+ if row["territory_1_id"] not in territories_dict:
327
+ external_territories_ids.add(row["territory_1_id"])
328
+ if row["territory_2_id"] not in territories_dict:
329
+ external_territories_ids.add(row["territory_2_id"])
330
+ external_territories_ids
331
+
332
+ codes = ", ".join([f"'{tid}'" for tid in external_territories_ids])
333
+ geo_query = f"""
334
+ SELECT DISTINCT
335
+ arbo."{mapped_mesh_level}" as territory_id,
336
+ arbo."NOM_{mapped_mesh_level}" as territory_name,
337
+ ST_ASGEOJSON(ST_CENTROID(contours.geometry)) as center
338
+ FROM arborescence_geo AS arbo
339
+ JOIN contours_simplified_{payload.submesh} as contours
340
+ ON arbo."{mapped_mesh_level}" = contours.code
341
+ WHERE arbo."{mapped_mesh_level}" IN ({codes})
342
+ """
343
+ external_territories = run_custom_query(geo_query)
344
+ for t in external_territories:
345
+ territories_dict[t["territory_id"]] = {
346
+ "name": f"{t['territory_name']} (externe)",
347
+ "code": t["territory_id"],
348
+ "center": json.loads(t["center"]),
349
+ }
350
+
351
+ return JsonResponse(
352
+ {"flows": row_values, "territories": territories_dict}, status=200
353
+ )
354
+
355
+
356
+ @require_GET
357
+ @cache_control(max_age=3600)
358
+ @use_payload(ComparisonQueryPayload)
359
+ def comparison_histogram_view(request, name, payload):
360
+ indicator = get_object_or_404(Indicator, name=name)
361
+ filters = get_filters(request, indicator)
362
+ territories = get_sub_territories(payload.territory, payload.submesh)
363
+ cmp_territories = get_sub_territories(payload.cmp_territory, payload.submesh)
364
+ values, cmp_values, buckets = get_comparison_values_and_buckets(
365
+ indicator, payload.submesh, territories, cmp_territories, filters
366
+ )
367
+ return JsonResponse(
368
+ {
369
+ "values": values,
370
+ "comparedValues": cmp_values,
371
+ "buckets": buckets,
372
+ }
373
+ )
374
+
375
+
376
+ @require_GET
377
+ @use_payload(ComparisonQueryPayload)
378
+ def comparison_histogram_export_view(request, name, payload):
379
+ indicator = get_object_or_404(Indicator, name=name)
380
+ filters = get_filters(request, indicator)
381
+ territories = get_sub_territories(payload.territory, payload.submesh)
382
+ cmp_territories = get_sub_territories(payload.cmp_territory, payload.submesh)
383
+ values, cmp_values, buckets = get_comparison_values_and_buckets(
384
+ indicator, payload.submesh, territories, cmp_territories, filters
385
+ )
386
+ rows = []
387
+ territory_name = get_territory_name(payload.territory)
388
+ cmp_territory_name = get_territory_name(payload.cmp_territory)
389
+ for index, bucket in enumerate(buckets):
390
+ row = {}
391
+ row["Décile"] = (
392
+ f"{format_indicator_value(bucket[0])} - {format_indicator_value(bucket[1])}"
393
+ )
394
+ row[f"{territory_name} - Nombre de {MESH_TITLES[payload.submesh]}s"] = len(
395
+ values[index + 1]
396
+ )
397
+ row[f"{cmp_territory_name} - Nombre de {MESH_TITLES[payload.submesh]}s"] = len(
398
+ cmp_values[index + 1]
399
+ )
400
+ row[f"{territory_name} - échantillon de dix territoires"] = " | ".join(
401
+ values[index + 1][:10]
402
+ )
403
+ row[f"{cmp_territory_name} - échantillon de dix territoires"] = " | ".join(
404
+ cmp_values[index + 1][:10]
405
+ )
406
+ rows.append(row)
407
+ return export_to_csv(request, indicator, "comparison-histogram", rows)
408
+
409
+
410
+ def get_label(props, indicator, key):
411
+ labels = {
412
+ "annee": "Année",
413
+ "lieu": "Lieu",
414
+ "territory_1": "Origine",
415
+ "territory_2": "Destination",
416
+ "valeur": "flux" if props.flows else indicator.unite,
417
+ "valeur_alternative": indicator.unite_alternative,
418
+ "dimension": "Dimension",
419
+ }
420
+ for dimension in indicator.dimensions.all():
421
+ labels[dimension.db_name] = dimension.title
422
+ return labels[key]
423
+
424
+
425
+ def get_pages(count, limit, current):
426
+ length = count // limit
427
+ if count % limit != 0 or count == 0:
428
+ length += 1
429
+ page_range = list(range(1, length + 1))
430
+ pages = {
431
+ "first": page_range[0] if page_range[0] != current else None,
432
+ "last": page_range[-1] if page_range[-1] != current else None,
433
+ "current": current,
434
+ "before": current - 1 if current - 1 in page_range else None,
435
+ "after": current + 1 if current + 1 in page_range else None,
436
+ "only_one_page": page_range == [1],
437
+ }
438
+ return pages
439
+
440
+
441
+ def get_line_focus(payload: IndicatorTablePayload):
442
+ if not payload.focus:
443
+ return None
444
+ limit = payload.limit
445
+ previous_limit = payload.previous_limit
446
+ if limit and previous_limit:
447
+ if limit <= previous_limit:
448
+ return 1
449
+ else:
450
+ return previous_limit + 1
451
+ return None
452
+
453
+
454
+ @require_GET
455
+ @use_payload(IndicatorTablePayload)
456
+ def indicator_details_table_view(request, name, payload):
457
+ indicator = get_object_or_404(Indicator, name=name)
458
+ filters = get_filters(request, indicator)
459
+ count, data = get_count_and_data_for_indicator_table(indicator, payload, filters)
460
+ formated_data = [format_data(element) for element in data]
461
+ keys = [
462
+ {"db": key, "label": get_label(payload, indicator, key)}
463
+ for key in (formated_data[0].keys() if formated_data else [])
464
+ ]
465
+ last_year = None
466
+ if data and "annee" in data[0].keys():
467
+ last_year = max([d["annee"] for d in data])
468
+ context = {
469
+ "rows": formated_data,
470
+ "keys": keys,
471
+ "result_count": count,
472
+ "pages": get_pages(count, payload.limit, payload.pagination),
473
+ "props": payload,
474
+ "last_year": last_year,
475
+ "line_focus": get_line_focus(payload),
476
+ }
477
+ return render(
478
+ request,
479
+ "territories_dashboard_lib/website/pages/indicators/details/components/table.html",
480
+ context,
481
+ )
482
+
483
+
484
+ @require_GET
485
+ @use_payload(IndicatorTablePayload)
486
+ def indicator_details_table_export_view(request, name, payload):
487
+ indicator = get_object_or_404(Indicator, name=name)
488
+ filters = get_filters(request, indicator)
489
+ table_values = get_export_indicator_table_values(indicator, payload, filters)
490
+ return export_to_csv(request, indicator, "table", table_values)
File without changes
@@ -0,0 +1,22 @@
1
+ from django.contrib import admin
2
+ from django.db import models
3
+ from django.forms import TextInput
4
+
5
+ from .models import Dashboard, Filter
6
+
7
+
8
+ class FilterInLine(admin.TabularInline):
9
+ model = Filter
10
+ extra = 0
11
+ formfield_overrides = {
12
+ models.TextField: {"widget": TextInput(attrs={"size": "32"})},
13
+ }
14
+
15
+
16
+ @admin.register(Dashboard)
17
+ class DashboardAdmin(admin.ModelAdmin):
18
+ formfield_overrides = {
19
+ models.TextField: {"widget": TextInput(attrs={"size": "32"})},
20
+ }
21
+ inlines = [FilterInLine]
22
+ list_display = ["short_name", "order"]
@@ -0,0 +1,64 @@
1
+ import http.client
2
+ import json
3
+
4
+ from django.conf import settings
5
+
6
+
7
+ def get_guest_token(dashboard_id):
8
+ """
9
+ Function copied from https://snum.gitlab-pages.din.developpement-durable.gouv.fr/ds/gd3ia/offre-dataviz-documentation/05-Documentation_SUPERSET_INTEGRATION/
10
+ """
11
+ conn = http.client.HTTPSConnection(settings.SUPERSET_DOMAIN)
12
+ params = json.dumps(
13
+ {
14
+ "provider": "db",
15
+ "refresh": "True",
16
+ "username": settings.SUPERSET_USERNAME,
17
+ "password": settings.SUPERSET_PASSWORD,
18
+ }
19
+ )
20
+ conn.request(
21
+ "POST", "/api/v1/security/login", params, {"Content-Type": "application/json"}
22
+ )
23
+ response = conn.getresponse()
24
+ content = json.loads(response.read())
25
+ access_token = content["access_token"]
26
+
27
+ # Recuperation du token CSRF et cookie de session associe
28
+ headers = {"Authorization": f"Bearer {access_token}"}
29
+ conn.request("GET", "/api/v1/security/csrf_token/", None, headers)
30
+ response = conn.getresponse()
31
+ content = json.loads(response.read())
32
+ csrf_token = content["result"]
33
+ cookie = response.headers["set-cookie"].split("; ")[0]
34
+
35
+ # Recuperation du guest_token pour l'affichage du diagramme
36
+ params = json.dumps(
37
+ {
38
+ "resources": [{"id": dashboard_id, "type": "dashboard"}],
39
+ "rls": [
40
+ {
41
+ # Clause SQL appliquée à la récupération des donnes du dashboard
42
+ # Elle peut être ajustée pour limiter les données affichées
43
+ # dans le dashboard en fonction du profil utilisateur
44
+ # La valeur "1=1" permet de n'appliquer aucun filtre
45
+ "clause": "1=1"
46
+ }
47
+ ],
48
+ "user": {
49
+ "first_name": "Prenom",
50
+ "last_name": "Nom",
51
+ "username": settings.SUPERSET_USERNAME,
52
+ },
53
+ }
54
+ )
55
+ headers = {
56
+ "Content-Type": "application/json",
57
+ "Authorization": f"Bearer {access_token}",
58
+ "X-Csrftoken": csrf_token,
59
+ "Cookie": cookie,
60
+ }
61
+ conn.request("POST", "/api/v1/security/guest_token/", params, headers)
62
+ response = conn.getresponse()
63
+ guest_token = json.loads(response.read())["token"]
64
+ return guest_token
@@ -0,0 +1,67 @@
1
+ from territories_dashboard_lib.indicators_lib.enums import MeshLevel
2
+ from territories_dashboard_lib.indicators_lib.query.utils import run_custom_query
3
+
4
+ from .models import Dashboard
5
+
6
+
7
+ def get_territory_meshes(territory_id: str, territory_mesh: MeshLevel):
8
+ current_reg = None
9
+ current_dep = None
10
+ current_epci = None
11
+ current_com = None
12
+ if territory_mesh != MeshLevel.National:
13
+ mapped_mesh = "DEPCOM" if territory_mesh == "com" else territory_mesh.upper()
14
+ query = f"""
15
+ SELECT
16
+ "NOM_REG" as reg_name,
17
+ "NOM_DEP" as dep_name,
18
+ "NOM_EPCI" as epci_name,
19
+ "NOM_DEPCOM" as com_name
20
+ FROM arborescence_geo
21
+ WHERE "{mapped_mesh}" = '{territory_id}'
22
+ LIMIT 1
23
+ """
24
+ row = run_custom_query(query)[0]
25
+ if territory_mesh in [
26
+ MeshLevel.Region,
27
+ MeshLevel.Department,
28
+ MeshLevel.Epci,
29
+ MeshLevel.Town,
30
+ ]:
31
+ current_reg = row["reg_name"]
32
+ if territory_mesh in [
33
+ MeshLevel.Department,
34
+ MeshLevel.Epci,
35
+ MeshLevel.Town,
36
+ ]:
37
+ current_dep = row["dep_name"]
38
+ if territory_mesh in [
39
+ MeshLevel.Epci,
40
+ MeshLevel.Town,
41
+ ]:
42
+ current_epci = row["epci_name"]
43
+ if territory_mesh in [
44
+ MeshLevel.Town,
45
+ ]:
46
+ current_com = row["com_name"]
47
+ return {
48
+ MeshLevel.Region: current_reg,
49
+ MeshLevel.Department: current_dep,
50
+ MeshLevel.Epci: current_epci,
51
+ MeshLevel.Town: current_com,
52
+ }
53
+
54
+
55
+ def make_filter(dashboard: Dashboard, territory_id: str, territory_mesh: MeshLevel):
56
+ if dashboard.filters.count() == 0:
57
+ return None
58
+ territory_meshes = get_territory_meshes(territory_id, territory_mesh)
59
+ filters = []
60
+ for f in dashboard.filters.all():
61
+ value = territory_meshes.get(f.mesh)
62
+ if value:
63
+ filter_string = f"""NATIVE_FILTER-{f.superset_id}:(__cache:(label:'{value}',validateStatus:!f,value:!('{value}')),extraFormData:(filters:!((col:{f.superset_col},op:IN,val:!('{value}')))),filterState:(label:'{value}',validateStatus:!f,value:!('{value}')),id:NATIVE_FILTER-{f.superset_id},ownState:())"""
64
+ filters.append(filter_string)
65
+ if not filters:
66
+ return None
67
+ return f"""({",".join(filters)})"""
@@ -0,0 +1,45 @@
1
+ # Generated by Django 5.2.3 on 2025-06-18 13:03
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ initial = True
10
+
11
+ dependencies = [
12
+ ]
13
+
14
+ operations = [
15
+ migrations.CreateModel(
16
+ name='Dashboard',
17
+ fields=[
18
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19
+ ('created_at', models.DateTimeField(auto_now_add=True)),
20
+ ('updated_at', models.DateTimeField(auto_now=True)),
21
+ ('superset_id', models.TextField(help_text="Un utilisateur administrateur de l'instance Superset doit accéder aux paramètres du dashboard et cliquer sur 'embed dashboard' pour récupérer cet ID nécessaire à la connexion avec l'instance Superset.", unique=True, verbose_name='Embed ID de Superset')),
22
+ ('short_name', models.TextField(help_text="Pour l'URL, ne mettre que des lettres minuscules sans accents et des tirets", unique=True, verbose_name='Nom court')),
23
+ ('label', models.TextField(help_text='Nom du dashboard à afficher dans la liste.', unique=True, verbose_name='Label')),
24
+ ('order', models.IntegerField(default=1, help_text="Numéro d'ordre dans la dropdown de sélection, les dashboards sont triés du plus petit numéro au plus grand.", verbose_name="Numéro d'ordre")),
25
+ ],
26
+ options={
27
+ 'abstract': False,
28
+ },
29
+ ),
30
+ migrations.CreateModel(
31
+ name='Filter',
32
+ fields=[
33
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
34
+ ('created_at', models.DateTimeField(auto_now_add=True)),
35
+ ('updated_at', models.DateTimeField(auto_now=True)),
36
+ ('superset_id', models.TextField(blank=True, help_text="ID du native filter du territoire, qui permettra d'initialiser les filtres du dasbhoard au territoire sélectionné dans l'application. Pour le récupérer c'est un petit parcours du combattant : aller sur le dashboard dans superset, cliquer sur modifier le dashboard, cliquer sur les trois petits points, puis sur modifier les propriétés. Cliquer sur avancé, copier le json et le coller dans un site comme : https://jsonformatter.curiousconcept.com/ pour mieux le voir. Chercher 'global_chart_configuration' puis 'native_filter_configuration'. Chercher le native filter lié au choix du territoire. Copier l'id qui dans son nom, le nom est de la forme NATIVE_FILTER-ID. Promis, c'est le plus simple que j'ai trouvé !", null=True)),
37
+ ('superset_col', models.TextField(blank=True, help_text="Nom de la colonne en base de données liée au filtre sur le territoire. Pour trouver le nom faire les mêmes étapes que pour geo_filter_id et chercher 'column' dans les paramètres json du native filter.", null=True)),
38
+ ('mesh', models.TextField(blank=True, choices=[('fr', 'National'), ('reg', 'Region'), ('dep', 'Department'), ('epci', 'Epci'), ('com', 'Town')], help_text="Maille du territoire sur lequel s'effectue le filtre.", null=True)),
39
+ ('dashboard', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='filters', to='superset_lib.dashboard')),
40
+ ],
41
+ options={
42
+ 'abstract': False,
43
+ },
44
+ ),
45
+ ]