territories-dashboard-lib 0.1.0__py3-none-any.whl → 0.1.1__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.
- territories_dashboard_lib/commons/__init__.py +0 -0
- territories_dashboard_lib/commons/decorators.py +36 -0
- territories_dashboard_lib/commons/models.py +9 -0
- territories_dashboard_lib/geo_lib/__init__.py +0 -0
- territories_dashboard_lib/geo_lib/admin.py +64 -0
- territories_dashboard_lib/geo_lib/enums.py +7 -0
- territories_dashboard_lib/geo_lib/migrations/0001_initial.py +51 -0
- territories_dashboard_lib/geo_lib/migrations/__init__.py +0 -0
- territories_dashboard_lib/geo_lib/models.py +58 -0
- territories_dashboard_lib/geo_lib/urls.py +27 -0
- territories_dashboard_lib/geo_lib/views.py +239 -0
- territories_dashboard_lib/indicators_lib/__init__.py +0 -0
- territories_dashboard_lib/indicators_lib/admin.py +140 -0
- territories_dashboard_lib/indicators_lib/enums.py +59 -0
- territories_dashboard_lib/indicators_lib/export.py +29 -0
- territories_dashboard_lib/indicators_lib/format.py +34 -0
- territories_dashboard_lib/indicators_lib/methodo_pdf.py +99 -0
- territories_dashboard_lib/indicators_lib/migrations/0001_initial.py +138 -0
- territories_dashboard_lib/indicators_lib/migrations/__init__.py +0 -0
- territories_dashboard_lib/indicators_lib/models.py +230 -0
- territories_dashboard_lib/indicators_lib/payloads.py +54 -0
- territories_dashboard_lib/indicators_lib/query/commons.py +223 -0
- territories_dashboard_lib/indicators_lib/query/comparison.py +70 -0
- territories_dashboard_lib/indicators_lib/query/details.py +64 -0
- territories_dashboard_lib/indicators_lib/query/histogram.py +82 -0
- territories_dashboard_lib/indicators_lib/query/indicator_card.py +102 -0
- territories_dashboard_lib/indicators_lib/query/top_10.py +100 -0
- territories_dashboard_lib/indicators_lib/query/utils.py +20 -0
- territories_dashboard_lib/indicators_lib/refresh_filters.py +17 -0
- territories_dashboard_lib/indicators_lib/table.py +154 -0
- territories_dashboard_lib/indicators_lib/urls.py +97 -0
- territories_dashboard_lib/indicators_lib/views.py +490 -0
- territories_dashboard_lib/superset_lib/__init__.py +0 -0
- territories_dashboard_lib/superset_lib/admin.py +22 -0
- territories_dashboard_lib/superset_lib/guest_token.py +64 -0
- territories_dashboard_lib/superset_lib/logic.py +67 -0
- territories_dashboard_lib/superset_lib/migrations/0001_initial.py +45 -0
- territories_dashboard_lib/superset_lib/migrations/__init__.py +0 -0
- territories_dashboard_lib/superset_lib/models.py +52 -0
- territories_dashboard_lib/superset_lib/serializers.py +10 -0
- territories_dashboard_lib/superset_lib/urls.py +10 -0
- territories_dashboard_lib/superset_lib/views.py +19 -0
- territories_dashboard_lib/tracking_lib/__init__.py +0 -0
- territories_dashboard_lib/tracking_lib/enums.py +7 -0
- territories_dashboard_lib/tracking_lib/logic.py +78 -0
- territories_dashboard_lib/tracking_lib/migrations/0001_initial.py +45 -0
- territories_dashboard_lib/tracking_lib/migrations/__init__.py +0 -0
- territories_dashboard_lib/tracking_lib/models.py +79 -0
- territories_dashboard_lib/website_lib/__init__.py +0 -0
- territories_dashboard_lib/website_lib/admin.py +40 -0
- territories_dashboard_lib/website_lib/context_processors.py +27 -0
- territories_dashboard_lib/website_lib/forms.py +47 -0
- territories_dashboard_lib/website_lib/migrations/0001_initial.py +91 -0
- territories_dashboard_lib/website_lib/migrations/__init__.py +0 -0
- territories_dashboard_lib/website_lib/models.py +148 -0
- territories_dashboard_lib/website_lib/navigation.py +124 -0
- territories_dashboard_lib/website_lib/params.py +268 -0
- territories_dashboard_lib/website_lib/serializers.py +105 -0
- territories_dashboard_lib/website_lib/static_content.py +20 -0
- territories_dashboard_lib/website_lib/templatetags/htmlparams.py +75 -0
- territories_dashboard_lib/website_lib/templatetags/other_filters.py +30 -0
- territories_dashboard_lib/website_lib/views.py +212 -0
- {territories_dashboard_lib-0.1.0.dist-info → territories_dashboard_lib-0.1.1.dist-info}/METADATA +1 -1
- territories_dashboard_lib-0.1.1.dist-info/RECORD +67 -0
- territories_dashboard_lib-0.1.0.dist-info/RECORD +0 -5
- {territories_dashboard_lib-0.1.0.dist-info → territories_dashboard_lib-0.1.1.dist-info}/WHEEL +0 -0
- {territories_dashboard_lib-0.1.0.dist-info → territories_dashboard_lib-0.1.1.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
|
+
]
|
|
File without changes
|
|
@@ -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
|