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,59 @@
1
+ from django.db import models
2
+
3
+
4
+ class AggregationFunctions(models.TextChoices):
5
+ DISCRETE_COMPONENT_2 = "discrete"
6
+ REPEATED_COMPONENT_2 = "repeated"
7
+
8
+
9
+ class MeshLevel(models.TextChoices):
10
+ National = "fr"
11
+ Region = "reg"
12
+ Department = "dep"
13
+ Epci = "epci"
14
+ Town = "com"
15
+
16
+
17
+ class GeoLevel(models.TextChoices):
18
+ France = "fr"
19
+ Region = "reg"
20
+ Department = "dep"
21
+ Epci = "epci"
22
+ Town = "com"
23
+
24
+
25
+ class FranceGeoLevel(models.TextChoices):
26
+ All = "FR0,FR1,FR2"
27
+ METRO = "FR0,FR1"
28
+ METRO_HORS_IDF = "FR0"
29
+
30
+
31
+ FRANCE_GEOLEVEL_TITLES = {
32
+ FranceGeoLevel.All: "France entière",
33
+ FranceGeoLevel.METRO: "France métropolitaine",
34
+ FranceGeoLevel.METRO_HORS_IDF: "France métropolitaine hors IDF",
35
+ }
36
+
37
+ FRANCE_DB_VALUES = {
38
+ FranceGeoLevel.All: "FR_TOT",
39
+ FranceGeoLevel.METRO: "FR_METRO",
40
+ FranceGeoLevel.METRO_HORS_IDF: "FR_METRO_HORS_IDF",
41
+ }
42
+
43
+
44
+ DEFAULT_MESH = MeshLevel.Region
45
+
46
+ MESH_TITLES = {
47
+ MeshLevel.National: "France entière",
48
+ MeshLevel.Region: "Région",
49
+ MeshLevel.Department: "Département",
50
+ MeshLevel.Epci: "Intercommunalité",
51
+ MeshLevel.Town: "Commune",
52
+ }
53
+
54
+ MESH_DB = {
55
+ MeshLevel.Region: "REG",
56
+ MeshLevel.Department: "DEP",
57
+ MeshLevel.Epci: "EPCI",
58
+ MeshLevel.Town: "DEPCOM",
59
+ }
@@ -0,0 +1,29 @@
1
+ import csv
2
+
3
+ from django.http import HttpRequest, HttpResponse
4
+
5
+ from territories_dashboard_lib.tracking_lib.enums import EventType
6
+ from territories_dashboard_lib.tracking_lib.logic import track_event
7
+
8
+ from .models import Indicator
9
+
10
+
11
+ def export_to_csv(
12
+ request: HttpRequest, indicator: Indicator, graph_name: str, data: list[dict]
13
+ ):
14
+ response = HttpResponse(content_type="text/csv")
15
+ response["Content-Disposition"] = (
16
+ f'attachment; filename="{indicator.name}_{graph_name}.csv"'
17
+ )
18
+ writer = csv.writer(response)
19
+ if data:
20
+ writer.writerow(data[0].keys())
21
+ for row in data:
22
+ writer.writerow(row.values())
23
+ response = track_event(
24
+ request=request,
25
+ response=response,
26
+ event_name=EventType.download,
27
+ data={"indicator": indicator.name, "objet": graph_name},
28
+ )
29
+ return response
@@ -0,0 +1,34 @@
1
+ def _get_precision(value, force_integer):
2
+ return f"{value:.0f}" if force_integer else f"{value:.1f}"
3
+
4
+
5
+ def _remove_useless_0(value):
6
+ if "." in value:
7
+ value = value.rstrip("0").rstrip(".")
8
+ return value
9
+
10
+
11
+ def format_indicator_value(value, force_integer=False):
12
+ if value is None:
13
+ return "-"
14
+
15
+ abs_value = abs(value)
16
+
17
+ if abs_value > 999999:
18
+ nb = _remove_useless_0(_get_precision((value / 1_000_000), force_integer)) + "M"
19
+ elif abs_value > 999:
20
+ nb = _remove_useless_0(_get_precision((value / 1_000), force_integer)) + "k"
21
+
22
+ else:
23
+ nb = _remove_useless_0(_get_precision(value, force_integer))
24
+ return nb.replace(".", ",")
25
+
26
+
27
+ def _format_value(k, v):
28
+ if k.lower().startswith("valeur"):
29
+ v = format_indicator_value(v)
30
+ return v
31
+
32
+
33
+ def format_data(data):
34
+ return {k: _format_value(k, v) for k, v in data.items()}
@@ -0,0 +1,99 @@
1
+ import base64
2
+ import os
3
+ import subprocess
4
+ from tempfile import NamedTemporaryFile
5
+
6
+ from django.conf import settings
7
+ from markdown import markdown
8
+
9
+
10
+ def _html_to_pdf(html_content, output_pdf_path):
11
+ try:
12
+ process = subprocess.Popen(
13
+ [
14
+ "wkhtmltopdf",
15
+ "-",
16
+ output_pdf_path,
17
+ ], # '-' tells wkhtmltopdf to read from stdin
18
+ stdin=subprocess.PIPE,
19
+ stdout=subprocess.PIPE,
20
+ stderr=subprocess.PIPE,
21
+ )
22
+ stdout, stderr = process.communicate(input=html_content.encode("utf-8"))
23
+ if process.returncode != 0:
24
+ print("Error:", stderr.decode("utf-8"))
25
+ else:
26
+ print(f"PDF successfully created at: {output_pdf_path}")
27
+ except FileNotFoundError:
28
+ print("Error: wkhtmltopdf not found. Ensure it is installed and in your PATH.")
29
+ except Exception as e:
30
+ print(f"An error occurred: {e}")
31
+
32
+
33
+ def _generate_pdf_from_methodo(indicator, output_path):
34
+ html = markdown(indicator.methodo)
35
+ relative_logo_path = "whitenoise_root/ministere_logo.png"
36
+ logo_path = os.path.join(settings.BASE_DIR, relative_logo_path)
37
+ with open(logo_path, "rb") as fd:
38
+ encoded_logo = base64.b64encode(fd.read()).decode("utf-8")
39
+ html = (
40
+ """
41
+ <!DOCTYPE html>
42
+ <html lang="fr">
43
+ <head>
44
+ <meta charset="UTF-8">
45
+ <title>Accents Test</title>
46
+ <style>
47
+ body {
48
+ font-family: Tahoma, Arial, sans-serif;
49
+ }
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <header>
54
+ <table>
55
+ <tr>
56
+ <td>
57
+ """
58
+ + f'<img width="150px" src="data:image/png;base64,{encoded_logo}"/>'
59
+ + """
60
+ </td>
61
+ <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>
63
+ <div>Fiche méthodologique</div>
64
+ </td>
65
+ </tr>
66
+ </table>
67
+ </header>
68
+ <main>
69
+ """
70
+ + f"<h1>{indicator.title}</h1>"
71
+ + f"<p>Thématique : {indicator.sub_theme.theme.title} / {indicator.sub_theme.title}</p>"
72
+ + html
73
+ + """
74
+ </main>
75
+ </body>
76
+ </html>
77
+ """
78
+ )
79
+ _html_to_pdf(html, output_path)
80
+
81
+
82
+ def reset_methodo_file(indicator):
83
+ # Create a temporary file for the PDF
84
+ with NamedTemporaryFile(delete=False, suffix=".pdf") as temp_file:
85
+ pdf_path = temp_file.name
86
+
87
+ # Generate the PDF
88
+ _generate_pdf_from_methodo(indicator, pdf_path)
89
+
90
+ # Read the binary content of the generated PDF file
91
+ with open(pdf_path, "rb") as pdf_file:
92
+ pdf_content = pdf_file.read()
93
+
94
+ # Save the binary content to the BinaryField
95
+ indicator.methodo_file = pdf_content
96
+ indicator.save()
97
+
98
+ # Clean up temporary file
99
+ os.remove(pdf_path)
@@ -0,0 +1,138 @@
1
+ # Generated by Django 5.2.3 on 2025-06-19 07:13
2
+
3
+ import django.db.models.deletion
4
+ import martor.models
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ initial = True
11
+
12
+ dependencies = [
13
+ ]
14
+
15
+ operations = [
16
+ migrations.CreateModel(
17
+ name='SubTheme',
18
+ fields=[
19
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20
+ ('created_at', models.DateTimeField(auto_now_add=True)),
21
+ ('updated_at', models.DateTimeField(auto_now=True)),
22
+ ('name', models.CharField(max_length=64, unique=True)),
23
+ ('index_in_theme', models.IntegerField(default=0)),
24
+ ('title', models.CharField(max_length=128)),
25
+ ('description', models.TextField(blank=True, default='')),
26
+ ],
27
+ options={
28
+ 'verbose_name': '2 - Sous-thème',
29
+ 'ordering': ('theme', 'index_in_theme'),
30
+ },
31
+ ),
32
+ migrations.CreateModel(
33
+ name='Indicator',
34
+ fields=[
35
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
36
+ ('created_at', models.DateTimeField(auto_now_add=True)),
37
+ ('updated_at', models.DateTimeField(auto_now=True)),
38
+ ('is_active', models.BooleanField(default=True)),
39
+ ('index_in_theme', models.IntegerField(default=0)),
40
+ ('name', models.CharField(max_length=32, unique=True)),
41
+ ('title', models.CharField(max_length=128)),
42
+ ('db_table_prefix', models.CharField(max_length=128)),
43
+ ('is_composite', models.BooleanField(default=False)),
44
+ ('show_alternative', models.BooleanField(default=True, help_text='Pour certains indicateurs composites (par exemple les moyennes) on ne veut pas afficher la valeur alternative.', verbose_name='Afficher la valeur alternative')),
45
+ ('aggregation_constant', models.DecimalField(decimal_places=5, default=1, max_digits=10)),
46
+ ('aggregation_function', models.CharField(choices=[('discrete', 'Discrete Component 2'), ('repeated', 'Repeated Component 2')], default='repeated', max_length=8)),
47
+ ('unite', models.CharField(max_length=32)),
48
+ ('unite_nom_accessible', models.CharField(blank=True, default='', help_text="Nom accessible de l'unité qui sera lu par le lecteur d'écran.", max_length=64)),
49
+ ('unite_alternative', models.CharField(blank=True, default=None, max_length=32, null=True)),
50
+ ('unite_alternative_nom_accessible', models.CharField(blank=True, default='', help_text="Nom accessible de l'unité alternative qui sera lu par le lecteur d'écran.", max_length=64)),
51
+ ('flows_db_table_prefix', models.CharField(blank=True, help_text='Les données des flux seront affichés sur la carte et le graphique de Sankey.', max_length=128, null=True, verbose_name='Table des flux (prefix)')),
52
+ ('flows_dimension', models.CharField(blank=True, help_text='Nom de la colonne des dimensions de la table des flux', max_length=32, null=True, verbose_name='Dimension des flux')),
53
+ ('show_evolution', models.BooleanField(default=False)),
54
+ ('source', models.TextField(blank=True, default='')),
55
+ ('description', models.TextField(blank=True, default='')),
56
+ ('methodo', martor.models.MartorField(blank=True, default='')),
57
+ ('methodo_file', models.BinaryField(blank=True, null=True)),
58
+ ('secondary_indicator', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='indicators_lib.indicator', verbose_name='Second indicateur à afficher sur la carte')),
59
+ ('sub_theme', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='indicators', to='indicators_lib.subtheme')),
60
+ ],
61
+ options={
62
+ 'verbose_name': '3 - Indicateur',
63
+ 'ordering': ('sub_theme', 'index_in_theme'),
64
+ },
65
+ ),
66
+ migrations.CreateModel(
67
+ name='Dimension',
68
+ fields=[
69
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
70
+ ('created_at', models.DateTimeField(auto_now_add=True)),
71
+ ('updated_at', models.DateTimeField(auto_now=True)),
72
+ ('db_name', models.TextField()),
73
+ ('title', models.TextField()),
74
+ ('is_breakdown', models.BooleanField(default=False, help_text="Dans le cas de plusieurs dimensions pour un indicateur, l'une d'entre elles doit être la dimension de réparition pour les graphiques.", verbose_name='Répartir selon cette dimension')),
75
+ ('indicator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dimensions', to='indicators_lib.indicator')),
76
+ ],
77
+ options={
78
+ 'abstract': False,
79
+ },
80
+ ),
81
+ migrations.CreateModel(
82
+ name='Theme',
83
+ fields=[
84
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
85
+ ('created_at', models.DateTimeField(auto_now_add=True)),
86
+ ('updated_at', models.DateTimeField(auto_now=True)),
87
+ ('ordering', models.IntegerField(default=0)),
88
+ ('name', models.CharField(max_length=64, unique=True)),
89
+ ('title', models.CharField(max_length=128)),
90
+ ('objectif_theme', models.TextField(blank=True)),
91
+ ('action_theme', models.TextField(blank=True)),
92
+ ],
93
+ options={
94
+ 'verbose_name': '1 - Thème',
95
+ 'ordering': ('ordering',),
96
+ 'indexes': [models.Index(fields=['name'], name='indicators__name_fea408_idx')],
97
+ },
98
+ ),
99
+ migrations.AddField(
100
+ model_name='subtheme',
101
+ name='theme',
102
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sub_themes', to='indicators_lib.theme'),
103
+ ),
104
+ migrations.CreateModel(
105
+ name='Filter',
106
+ fields=[
107
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
108
+ ('created_at', models.DateTimeField(auto_now_add=True)),
109
+ ('updated_at', models.DateTimeField(auto_now=True)),
110
+ ('db_name', models.CharField(max_length=128)),
111
+ ('order', models.IntegerField(default=0)),
112
+ ('default', models.BooleanField(default=True)),
113
+ ('dimension', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='filters', to='indicators_lib.dimension')),
114
+ ],
115
+ options={
116
+ 'verbose_name': 'Filtre',
117
+ 'ordering': ('dimension', 'order', 'db_name'),
118
+ 'indexes': [models.Index(fields=['db_name'], name='indicators__db_name_d32459_idx'), models.Index(fields=['dimension', 'order'], name='indicators__dimensi_aa3fdf_idx')],
119
+ 'unique_together': {('dimension', 'db_name')},
120
+ },
121
+ ),
122
+ migrations.AddIndex(
123
+ model_name='indicator',
124
+ index=models.Index(fields=['sub_theme'], name='indicators__sub_the_b49caf_idx'),
125
+ ),
126
+ migrations.AddIndex(
127
+ model_name='indicator',
128
+ index=models.Index(fields=['name'], name='indicators__name_c5606a_idx'),
129
+ ),
130
+ migrations.AddIndex(
131
+ model_name='subtheme',
132
+ index=models.Index(fields=['theme', 'index_in_theme'], name='indicators__theme_i_0cbbc1_idx'),
133
+ ),
134
+ migrations.AddIndex(
135
+ model_name='subtheme',
136
+ index=models.Index(fields=['name'], name='indicators__name_79c542_idx'),
137
+ ),
138
+ ]
@@ -0,0 +1,230 @@
1
+ from django.db import models
2
+ from martor.models import MartorField
3
+
4
+ from territories_dashboard_lib.commons.models import CommonModel
5
+ from territories_dashboard_lib.indicators_lib.enums import AggregationFunctions
6
+ from territories_dashboard_lib.indicators_lib.refresh_filters import refresh_filters
7
+
8
+
9
+ class Theme(CommonModel):
10
+ # TODO: make it primary key
11
+ # django.db.utils.OperationalError: foreign key mismatch -
12
+ # "tdbmd_indicators_indicatorsubtheme" referencing "tdbmd_indicators_indicatortheme"
13
+ ordering = models.IntegerField(default=0)
14
+ ordering.verbose_name = "Ordre dans la sidebar"
15
+ name = models.CharField(max_length=64, unique=True)
16
+ name.verbose_name = "Nom (~id, URL)"
17
+ title = models.CharField(max_length=128)
18
+ title.verbose_name = "Titre (affiché)"
19
+ objectif_theme = models.TextField(blank=True)
20
+ objectif_theme.verbose_name = "Objectif"
21
+ action_theme = models.TextField(blank=True)
22
+ action_theme.verbose_name = "Actions"
23
+
24
+ def is_displayed_on_app(self):
25
+ return self.sub_themes.all().exists()
26
+
27
+ is_displayed_on_app.__name__ = "Visible ?"
28
+ is_displayed_on_app.boolean = True
29
+
30
+ def subthemes_count(self):
31
+ return self.sub_themes.count()
32
+
33
+ subthemes_count.__name__ = "Nb sous-thèmes"
34
+
35
+ def indicators_count(self):
36
+ return sum(
37
+ [sub_theme.indicators_count() for sub_theme in self.sub_themes.all()]
38
+ )
39
+
40
+ indicators_count.__name__ = "Total indicateurs"
41
+
42
+ def __str__(self):
43
+ return self.title
44
+
45
+ class Meta:
46
+ ordering = ("ordering",)
47
+ verbose_name = "1 - Thème"
48
+ indexes = [
49
+ models.Index(fields=["name"]),
50
+ ]
51
+
52
+
53
+ class SubTheme(CommonModel):
54
+ name = models.CharField(max_length=64, unique=True)
55
+ name.verbose_name = "Nom (~id, URL)"
56
+ theme = models.ForeignKey(
57
+ Theme, on_delete=models.CASCADE, related_name="sub_themes"
58
+ )
59
+ theme.verbose_name = "Thème"
60
+ index_in_theme = models.IntegerField(default=0)
61
+ index_in_theme.verbose_name = "Ordre thème"
62
+ title = models.CharField(max_length=128)
63
+ title.verbose_name = "Titre (affiché)"
64
+ description = models.TextField(default="", blank=True)
65
+
66
+ def __str__(self):
67
+ return f"{self.theme.title} > {self.title}"
68
+
69
+ def indicators_count(self):
70
+ return self.indicators.count()
71
+
72
+ def is_displayed_on_app(self):
73
+ return self.indicators.all().exists()
74
+
75
+ indicators_count.__name__ = "Nb indicateurs"
76
+ is_displayed_on_app.__name__ = "Visible ?"
77
+ is_displayed_on_app.boolean = True
78
+
79
+ class Meta:
80
+ ordering = ("theme", "index_in_theme")
81
+ verbose_name = "2 - Sous-thème"
82
+ indexes = [
83
+ models.Index(fields=["theme", "index_in_theme"]),
84
+ models.Index(fields=["name"]),
85
+ ]
86
+
87
+
88
+ class Indicator(CommonModel):
89
+ is_active = models.BooleanField(default=True)
90
+ is_active.verbose_name = "Actif ?"
91
+ sub_theme = models.ForeignKey(
92
+ SubTheme, on_delete=models.CASCADE, related_name="indicators"
93
+ )
94
+ sub_theme.verbose_name = "Sous-thème"
95
+ index_in_theme = models.IntegerField(default=0) # TODO: is it necessary?
96
+ index_in_theme.verbose_name = "Ordre sous-thème"
97
+ name = models.CharField(max_length=32, unique=True)
98
+ name.verbose_name = "Nom (~id, URL)"
99
+ title = models.CharField(max_length=128)
100
+ title.verbose_name = "Titre (affiché)"
101
+ # Indicator's DB attributes
102
+ db_table_prefix = models.CharField(max_length=128)
103
+ db_table_prefix.verbose_name = "Préfixe dans la DB"
104
+ is_composite = models.BooleanField(default=False)
105
+ is_composite.verbose_name = "Indicateur composite"
106
+ show_alternative = models.BooleanField(
107
+ default=True,
108
+ verbose_name="Afficher la valeur alternative",
109
+ help_text="Pour certains indicateurs composites (par exemple les moyennes) on ne veut pas afficher la valeur alternative.",
110
+ )
111
+ aggregation_constant = models.DecimalField(
112
+ default=1, decimal_places=5, max_digits=10
113
+ )
114
+ aggregation_constant.verbose_name = "Constante d'agrégation"
115
+ aggregation_function = models.CharField(
116
+ default=AggregationFunctions.REPEATED_COMPONENT_2,
117
+ max_length=8,
118
+ choices=AggregationFunctions.choices,
119
+ )
120
+ aggregation_function.verbose_name = "Fonction d'agrégation"
121
+ unite = models.CharField(max_length=32)
122
+ unite.verbose_name = "Unité (affichée)"
123
+ unite_nom_accessible = models.CharField(
124
+ max_length=64,
125
+ default="",
126
+ blank=True,
127
+ help_text="Nom accessible de l'unité qui sera lu par le lecteur d'écran.",
128
+ )
129
+ unite_alternative = models.CharField(
130
+ default=None, null=True, blank=True, max_length=32
131
+ )
132
+ unite_alternative.verbose_name = "Unité alternative (affichée)"
133
+ unite_alternative_nom_accessible = models.CharField(
134
+ max_length=64,
135
+ default="",
136
+ blank=True,
137
+ help_text="Nom accessible de l'unité alternative qui sera lu par le lecteur d'écran.",
138
+ )
139
+ secondary_indicator = models.ForeignKey(
140
+ "indicators_lib.Indicator",
141
+ null=True,
142
+ blank=True,
143
+ on_delete=models.SET_NULL,
144
+ verbose_name="Second indicateur à afficher sur la carte",
145
+ )
146
+ flows_db_table_prefix = models.CharField(
147
+ null=True,
148
+ blank=True,
149
+ max_length=128,
150
+ verbose_name="Table des flux (prefix)",
151
+ help_text="Les données des flux seront affichés sur la carte et le graphique de Sankey.",
152
+ )
153
+ flows_dimension = models.CharField(
154
+ null=True,
155
+ blank=True,
156
+ max_length=32,
157
+ verbose_name="Dimension des flux",
158
+ help_text="Nom de la colonne des dimensions de la table des flux",
159
+ )
160
+ # Descriptive attributes
161
+ show_evolution = models.BooleanField(default=False)
162
+ show_evolution.verbose_name = "Activer l'historique"
163
+ source = models.TextField(default="", blank=True)
164
+ description = models.TextField(default="", blank=True)
165
+ methodo = MartorField(default="", blank=True)
166
+ methodo.verbose_name = "Méthodologie (markdown)"
167
+ methodo_file = models.BinaryField(null=True, blank=True)
168
+
169
+ def __str__(self):
170
+ return self.title
171
+
172
+ def get_theme_title(self):
173
+ return self.sub_theme.theme.title
174
+
175
+ get_theme_title.short_description = "Thème"
176
+
177
+ class Meta:
178
+ ordering = ("sub_theme", "index_in_theme")
179
+ verbose_name = "3 - Indicateur"
180
+ indexes = [
181
+ models.Index(fields=["sub_theme"]),
182
+ models.Index(fields=["name"]),
183
+ ]
184
+
185
+
186
+ class Dimension(CommonModel):
187
+ indicator = models.ForeignKey(
188
+ Indicator, on_delete=models.CASCADE, related_name="dimensions"
189
+ )
190
+ db_name = models.TextField()
191
+ title = models.TextField()
192
+ is_breakdown = models.BooleanField(
193
+ default=False,
194
+ verbose_name="Répartir selon cette dimension",
195
+ help_text="Dans le cas de plusieurs dimensions pour un indicateur, l'une d'entre elles doit être la dimension de réparition pour les graphiques.",
196
+ )
197
+
198
+ def save(self, *args, **kwargs):
199
+ is_new = self.pk is None
200
+ result = super().save(*args, **kwargs)
201
+ if is_new:
202
+ try:
203
+ refresh_filters(self)
204
+ except Exception as e:
205
+ print(e)
206
+ return result
207
+
208
+
209
+ class Filter(CommonModel):
210
+ dimension = models.ForeignKey(
211
+ Dimension, related_name="filters", on_delete=models.CASCADE
212
+ )
213
+ db_name = models.CharField(max_length=128)
214
+ db_name.verbose_name = "Nom dans la BDD"
215
+ order = models.IntegerField(default=0)
216
+ order.verbose_name = "Ordre"
217
+ default = models.BooleanField(default=True)
218
+ default.verbose_name = "Sélectionné par défaut ?"
219
+
220
+ def __str__(self):
221
+ return self.db_name
222
+
223
+ class Meta:
224
+ ordering = ("dimension", "order", "db_name")
225
+ verbose_name = "Filtre"
226
+ indexes = [
227
+ models.Index(fields=["db_name"]),
228
+ models.Index(fields=["dimension", "order"]),
229
+ ]
230
+ unique_together = ("dimension", "db_name")
@@ -0,0 +1,54 @@
1
+ from typing import Annotated, Optional
2
+
3
+ from pydantic import BaseModel, BeforeValidator, Field
4
+
5
+ from .enums import MeshLevel
6
+
7
+
8
+ class Territory(BaseModel):
9
+ id: str
10
+ mesh: MeshLevel
11
+
12
+
13
+ def validate_territory(value):
14
+ if value and "-" in value:
15
+ return {"id": value.split("-")[0], "mesh": value.split("-")[1]}
16
+
17
+
18
+ class BasePayload(BaseModel):
19
+ territory: Annotated[Territory, BeforeValidator(validate_territory)]
20
+
21
+
22
+ class SubMeshPayload(BasePayload):
23
+ submesh: MeshLevel
24
+
25
+
26
+ class FlowsPayload(SubMeshPayload):
27
+ prefix: str
28
+ dimension: str | None = None
29
+
30
+
31
+ class ComparisonQueryPayload(SubMeshPayload):
32
+ cmp_territory: Annotated[
33
+ Territory, BeforeValidator(validate_territory), Field(alias="cmp-territory")
34
+ ]
35
+
36
+
37
+ class OptionalComparisonQueryPayload(SubMeshPayload):
38
+ cmp_territory: Annotated[
39
+ Optional[Territory],
40
+ BeforeValidator(validate_territory),
41
+ Field(default=None, alias="cmp-territory"),
42
+ ]
43
+
44
+
45
+ class IndicatorTablePayload(SubMeshPayload):
46
+ column_order: str | None = None
47
+ column_order_flow: str | None = None
48
+ pagination: int = 1
49
+ limit: int = 20
50
+ previous_limit: int | None = None
51
+ search: str | None = None
52
+ year: int | None = None
53
+ flows: bool | None = False
54
+ focus: bool | None = False