territories-dashboard-lib 0.1.4__py3-none-any.whl → 0.1.6__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 (27) hide show
  1. territories_dashboard_lib/geo_lib/views.py +13 -1
  2. territories_dashboard_lib/indicators_lib/export.py +3 -3
  3. territories_dashboard_lib/indicators_lib/views.py +10 -8
  4. territories_dashboard_lib/tracking_lib/enums.py +10 -0
  5. territories_dashboard_lib/tracking_lib/payloads.py +10 -0
  6. territories_dashboard_lib/tracking_lib/urls.py +9 -0
  7. territories_dashboard_lib/tracking_lib/views.py +43 -0
  8. territories_dashboard_lib/website_lib/migrations/0003_alter_mainconf_footer_navigation_and_more.py +23 -0
  9. territories_dashboard_lib/website_lib/models.py +2 -2
  10. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/css/website.css +54 -3
  11. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/comparaison/page.mjs +3 -1
  12. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/details/page.mjs +3 -1
  13. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/export-graph.mjs +10 -1
  14. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/theme/page.mjs +3 -1
  15. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/react/indicatorMap.bundle.js +1 -1
  16. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/react/sankeyGraph.bundle.js +1 -1
  17. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/comparaison/[theme]/components/histogram.html +1 -1
  18. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/comparaison/[theme]/components/indicateur-card.html +1 -1
  19. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/chart-buttons.html +1 -0
  20. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/details/components/histogram.html +1 -1
  21. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/details/components/proportions.html +1 -1
  22. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/details/components/top10.html +1 -1
  23. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/themes/components/indicateur-card.html +1 -1
  24. {territories_dashboard_lib-0.1.4.dist-info → territories_dashboard_lib-0.1.6.dist-info}/METADATA +1 -1
  25. {territories_dashboard_lib-0.1.4.dist-info → territories_dashboard_lib-0.1.6.dist-info}/RECORD +27 -23
  26. {territories_dashboard_lib-0.1.4.dist-info → territories_dashboard_lib-0.1.6.dist-info}/WHEEL +0 -0
  27. {territories_dashboard_lib-0.1.4.dist-info → territories_dashboard_lib-0.1.6.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,5 @@
1
1
  import base64
2
+ import datetime
2
3
  import gzip
3
4
  import json
4
5
  from typing import Optional
@@ -32,6 +33,17 @@ class GeoFeaturesParams(BaseModel):
32
33
  feature: str
33
34
 
34
35
 
36
+ class DateTimeEncoder(json.JSONEncoder):
37
+ def default(self, obj):
38
+ if isinstance(obj, (datetime.date, datetime.datetime)):
39
+ return (
40
+ obj.date().isoformat()
41
+ if isinstance(obj, datetime.datetime)
42
+ else obj.isoformat()
43
+ )
44
+ return super().default(obj)
45
+
46
+
35
47
  @require_GET
36
48
  @cache_control(max_age=3600)
37
49
  def geo_features_view(request):
@@ -96,7 +108,7 @@ def geo_features_view(request):
96
108
 
97
109
  last_queried_order_id = results[-1]["order_id"] if results else None
98
110
 
99
- compressed_data = gzip.compress(json.dumps(data).encode())
111
+ compressed_data = gzip.compress(json.dumps(data, cls=DateTimeEncoder).encode())
100
112
  compressed_base64 = base64.b64encode(compressed_data).decode()
101
113
 
102
114
  return JsonResponse({"data": compressed_base64, "last": last_queried_order_id})
@@ -2,14 +2,14 @@ import csv
2
2
 
3
3
  from django.http import HttpRequest, HttpResponse
4
4
 
5
- from territories_dashboard_lib.tracking_lib.enums import EventType
5
+ from territories_dashboard_lib.tracking_lib.enums import EventType, GraphType
6
6
  from territories_dashboard_lib.tracking_lib.logic import track_event
7
7
 
8
8
  from .models import Indicator
9
9
 
10
10
 
11
11
  def export_to_csv(
12
- request: HttpRequest, indicator: Indicator, graph_name: str, data: list[dict]
12
+ request: HttpRequest, indicator: Indicator, graph_name: GraphType, data: list[dict]
13
13
  ):
14
14
  response = HttpResponse(content_type="text/csv")
15
15
  response["Content-Disposition"] = (
@@ -24,6 +24,6 @@ def export_to_csv(
24
24
  request=request,
25
25
  response=response,
26
26
  event_name=EventType.download,
27
- data={"indicator": indicator.name, "objet": graph_name},
27
+ data={"indicator": indicator.name, "objet": graph_name, "type": "csv"},
28
28
  )
29
29
  return response
@@ -6,7 +6,7 @@ from django.views.decorators.cache import cache_control
6
6
  from django.views.decorators.http import require_GET
7
7
 
8
8
  from territories_dashboard_lib.commons.decorators import use_payload
9
- from territories_dashboard_lib.tracking_lib.enums import EventType
9
+ from territories_dashboard_lib.tracking_lib.enums import EventType, GraphType
10
10
  from territories_dashboard_lib.tracking_lib.logic import track_event
11
11
 
12
12
  from .enums import MESH_TITLES
@@ -54,7 +54,7 @@ def download_indicator_methodo_view(request, name):
54
54
  request=request,
55
55
  response=response,
56
56
  event_name=EventType.download,
57
- data={"indicator": indicator.name, "objet": "methodo"},
57
+ data={"indicator": indicator.name, "objet": "methodo", "type": "pdf"},
58
58
  )
59
59
  return response
60
60
 
@@ -129,14 +129,16 @@ def indicator_values_export_view(request, name, payload):
129
129
  "Année": value["annee"],
130
130
  f"Valeur {territory_name} ({indicator.unite})": value["valeur"],
131
131
  }
132
+ tracking_objet = "historique"
132
133
  if results.get("cmp_values") is not None:
133
134
  cmp_territory_name = get_territory_name(payload.cmp_territory)
134
135
  for value in results["cmp_values"]:
135
136
  export_values[value["annee"]][
136
137
  f"Valeur {cmp_territory_name} ({indicator.unite})"
137
138
  ] = value["valeur"]
139
+ tracking_objet = "comparaison-" + tracking_objet
138
140
  return export_to_csv(
139
- request, indicator, "comparaison-historique", list(export_values.values())
141
+ request, indicator, tracking_objet, list(export_values.values())
140
142
  )
141
143
 
142
144
 
@@ -183,7 +185,7 @@ def indicator_proportions_export_view(request, name, payload):
183
185
  {"Dimension": r["label"], f"Valeur {indicator.unite}": r["data"][0]}
184
186
  for r in rows
185
187
  ]
186
- return export_to_csv(request, indicator, "repartition-dimension", rows)
188
+ return export_to_csv(request, indicator, GraphType.repartition_dimension, rows)
187
189
 
188
190
 
189
191
  @cache_control(max_age=3600)
@@ -227,7 +229,7 @@ def indicator_histogram_export_view(request, name, payload):
227
229
  index
228
230
  ].replace("\n", " | ")
229
231
  rows.append(row)
230
- return export_to_csv(request, indicator, "repartition-valeurs", rows)
232
+ return export_to_csv(request, indicator, GraphType.repartition_valeurs, rows)
231
233
 
232
234
 
233
235
  @cache_control(max_age=3600)
@@ -255,7 +257,7 @@ def indicator_top_10_export_view(request, name, payload):
255
257
  payload.submesh,
256
258
  filters,
257
259
  )
258
- return export_to_csv(request, indicator, "top_10", csv_data)
260
+ return export_to_csv(request, indicator, GraphType.top_10, csv_data)
259
261
 
260
262
 
261
263
  @require_GET
@@ -404,7 +406,7 @@ def comparison_histogram_export_view(request, name, payload):
404
406
  cmp_values[index + 1][:10]
405
407
  )
406
408
  rows.append(row)
407
- return export_to_csv(request, indicator, "comparison-histogram", rows)
409
+ return export_to_csv(request, indicator, GraphType.comparison_histogram, rows)
408
410
 
409
411
 
410
412
  def get_label(props, indicator, key):
@@ -487,4 +489,4 @@ def indicator_details_table_export_view(request, name, payload):
487
489
  indicator = get_object_or_404(Indicator, name=name)
488
490
  filters = get_filters(request, indicator)
489
491
  table_values = get_export_indicator_table_values(indicator, payload, filters)
490
- return export_to_csv(request, indicator, "table", table_values)
492
+ return export_to_csv(request, indicator, GraphType.table, table_values)
@@ -5,3 +5,13 @@ TRACKING_COOKIE_NAME = "omnibus"
5
5
 
6
6
  class EventType(models.TextChoices):
7
7
  download = "download"
8
+
9
+
10
+ class GraphType(models.TextChoices):
11
+ comparaison_historique = "comparaison-historique"
12
+ repartition_dimension = "repartition-dimension"
13
+ repartition_valeurs = "repartition-valeurs"
14
+ top_10 = "top_10"
15
+ historique = "historique"
16
+ comparison_histogram = "comparison-histogram"
17
+ table = "table"
@@ -0,0 +1,10 @@
1
+ from pydantic import BaseModel
2
+
3
+ from territories_dashboard_lib.tracking_lib.enums import EventType
4
+
5
+
6
+ class EventPayload(BaseModel):
7
+ indicator: str
8
+ event: EventType
9
+ objet: str
10
+ type: str
@@ -0,0 +1,9 @@
1
+ from django.urls import path
2
+
3
+ from territories_dashboard_lib.tracking_lib.views import track_event_view
4
+
5
+ app_name = "tracking-api"
6
+
7
+ urlpatterns = [
8
+ path("event/", track_event_view, name="event"),
9
+ ]
@@ -0,0 +1,43 @@
1
+ import json
2
+ from datetime import timedelta
3
+
4
+ from django.http import HttpResponse, JsonResponse
5
+ from django.utils import timezone
6
+ from django.views.decorators.csrf import csrf_exempt
7
+ from django.views.decorators.http import require_POST
8
+ from pydantic import ValidationError
9
+
10
+ from territories_dashboard_lib.tracking_lib.logic import (
11
+ track_event,
12
+ )
13
+ from territories_dashboard_lib.tracking_lib.models import Event
14
+ from territories_dashboard_lib.tracking_lib.payloads import EventPayload
15
+
16
+
17
+ @require_POST
18
+ @csrf_exempt
19
+ def track_event_view(request):
20
+ try:
21
+ data = json.loads(request.body)
22
+ payload = EventPayload(**data)
23
+ except json.JSONDecodeError:
24
+ return JsonResponse({"error": "Invalid JSON"}, status=400)
25
+ except ValidationError as e:
26
+ return JsonResponse({"error": e.errors()}, status=422)
27
+ if (
28
+ Event.objects.filter(created_at__gte=timezone.now() - timedelta(days=1)).count()
29
+ > 1000
30
+ ):
31
+ return HttpResponse(status=429)
32
+ response = HttpResponse()
33
+ track_event(
34
+ request=request,
35
+ response=response,
36
+ event_name=payload.event,
37
+ data={
38
+ "indicator": payload.indicator,
39
+ "objet": payload.objet,
40
+ "type": payload.type,
41
+ },
42
+ )
43
+ return HttpResponse()
@@ -0,0 +1,23 @@
1
+ # Generated by Django 5.1.6 on 2025-06-23 12:31
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('website_lib', '0002_mainconf_contact_email_mainconf_newsletter_link_and_more'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='mainconf',
15
+ name='footer_navigation',
16
+ field=models.TextField(default='', help_text='Navigation du footer, mettre les liens au format markdown, un par ligne. <br/><br/>\n Si le lien est interne, commencer et terminer le lien par un slash comme "/accueil/"<br/><br/>\n Par exemple : <br/><br/>\n [Plan du site](/plan-site/)<br/>\n [Accessibilité](/accessibilite/)<br/>\n [Mentions légales](/mentions-legales/) <br/>\n ', verbose_name='Navigation du footer'),
17
+ ),
18
+ migrations.AlterField(
19
+ model_name='mainconf',
20
+ name='header_navigation',
21
+ field=models.TextField(default='', help_text='Navigation du header, mettre les liens au format markdown avec un slash à la fin du lien, mettre une ligne vide entre chaque rubrique. <br/><br/>\n \n Une rubrique peut contenir un seul lien, ou bien plusiers, dans ce cas la rubrique sera sous forme de dropdown et il faut mettre un titre en premier. <br/><br/>Par exemple : <br/>\n <br/><br/>\n [Accueil](/accueil/)\n <br/><br/>\n [Indicateurs territoriaux](/indicateurs/)\n <br/><br/>\n À propos\n [Présentation](/presentation/)\n [Journal des versions](/journal/)\n ', verbose_name='Navigation du header'),
22
+ ),
23
+ ]
@@ -22,7 +22,7 @@ class MainConf(CommonModel):
22
22
  help_text="Renseigner le nom de l'entité au même format que le logo officiel en respectant les mises à la ligne.",
23
23
  )
24
24
  header_navigation = models.TextField(
25
- verbose_name="Navigation",
25
+ verbose_name="Navigation du header",
26
26
  help_text="""Navigation du header, mettre les liens au format markdown avec un slash à la fin du lien, mettre une ligne vide entre chaque rubrique. <br/><br/>
27
27
 
28
28
  Une rubrique peut contenir un seul lien, ou bien plusiers, dans ce cas la rubrique sera sous forme de dropdown et il faut mettre un titre en premier. <br/><br/>Par exemple : <br/>
@@ -38,7 +38,7 @@ class MainConf(CommonModel):
38
38
  default="",
39
39
  )
40
40
  footer_navigation = models.TextField(
41
- verbose_name="Navigation",
41
+ verbose_name="Navigation du footer",
42
42
  help_text="""Navigation du footer, mettre les liens au format markdown, un par ligne. <br/><br/>
43
43
  Si le lien est interne, commencer et terminer le lien par un slash comme "/accueil/"<br/><br/>
44
44
  Par exemple : <br/><br/>
@@ -875,13 +875,17 @@ footer .fr-follow {
875
875
  display: flex;
876
876
  flex-direction: column;
877
877
  gap: 8px;
878
- padding-bottom: 32px;
878
+ padding-bottom: 64px;
879
879
  }
880
880
 
881
881
  .table-geo-filters summary {
882
882
  font-size: 0.9rem;
883
883
  }
884
884
 
885
+ .table-geo-filters:has(details[open]) {
886
+ padding-bottom: 300px;
887
+ }
888
+
885
889
  .table-geo-filters input,
886
890
  .table-geo-filters select {
887
891
  padding: 4px 8px;
@@ -890,10 +894,11 @@ footer .fr-follow {
890
894
  }
891
895
 
892
896
  .table-geo-filters input[type="number"] {
893
- max-width: 150px;
897
+ max-width: 70px;
894
898
  }
895
899
 
896
- .table-geo-filters label {
900
+ .table-geo-filters label,
901
+ .table-geo-filters i {
897
902
  font-size: 0.8rem;
898
903
  }
899
904
 
@@ -908,6 +913,12 @@ footer .fr-follow {
908
913
  gap: 8px;
909
914
  }
910
915
 
916
+ .geo-filters-numbers-row {
917
+ display: flex;
918
+ flex-direction: row;
919
+ gap: 32px;
920
+ }
921
+
911
922
  .staticPage .code {
912
923
  background-color: #ececfe;
913
924
  font-family: inherit;
@@ -954,3 +965,43 @@ footer .fr-follow {
954
965
  .glossary .definitions dd {
955
966
  width: 100%;
956
967
  }
968
+
969
+ @media (min-width: 48em) {
970
+ .fr-footer__bottom-item::before {
971
+ margin-right: 0.5rem;
972
+ }
973
+ }
974
+
975
+ .fr-footer__bottom-link {
976
+ white-space: nowrap;
977
+ }
978
+
979
+ .react-datepicker__input-container input {
980
+ border-color: hsl(0, 0%, 80%);
981
+ border-radius: 4px;
982
+ border-style: solid;
983
+ border-width: 1px;
984
+ width: 95px;
985
+ }
986
+
987
+ .date-pickers-range {
988
+ display: flex;
989
+ flex-direction: row;
990
+ gap: 32px;
991
+ }
992
+
993
+ .react-datepicker__header {
994
+ background-color: #ececfe !important;
995
+ }
996
+
997
+ .react-datepicker__header h2 {
998
+ padding: 0px !important;
999
+ }
1000
+
1001
+ .react-datepicker__header__dropdown {
1002
+ padding-bottom: 16px !important;
1003
+ }
1004
+
1005
+ .react-datepicker__close-icon:hover {
1006
+ background-color: inherit !important;
1007
+ }
@@ -74,7 +74,9 @@ document
74
74
  await new Promise((resolve) => setTimeout(resolve, 50));
75
75
  await exportImageAsync(
76
76
  button.parentElement.previousElementSibling,
77
- `${indicator.name} - ${button.dataset.title}`
77
+ `${indicator.name} - ${button.dataset.title}`,
78
+ indicator,
79
+ button.dataset["trackingobjet"]
78
80
  );
79
81
  button.removeAttribute("disabled");
80
82
  });
@@ -59,7 +59,9 @@ document
59
59
  await new Promise((resolve) => setTimeout(resolve, 50));
60
60
  await exportImageAsync(
61
61
  button.parentElement.previousElementSibling,
62
- `${indicator.name} - ${button.dataset.title}`
62
+ `${indicator.name} - ${button.dataset.title}`,
63
+ indicator,
64
+ button.dataset["trackingobjet"]
63
65
  );
64
66
  button.removeAttribute("disabled");
65
67
  });
@@ -1,6 +1,6 @@
1
1
  /* globals html2canvas */
2
2
 
3
- async function exportImageAsync(element, fileName) {
3
+ async function exportImageAsync(element, fileName, indicator, graphName) {
4
4
  const canvas = await html2canvas(element);
5
5
  const image = canvas.toDataURL("image/png", 1.0);
6
6
  const tempLink = document.createElement("a");
@@ -10,6 +10,15 @@ async function exportImageAsync(element, fileName) {
10
10
  tempLink.click();
11
11
  document.body.removeChild(tempLink);
12
12
  tempLink.remove();
13
+ fetch("/api/tracking/event/", {
14
+ method: "POST",
15
+ body: JSON.stringify({
16
+ indicator: indicator.name,
17
+ event: "download",
18
+ objet: graphName,
19
+ type: "image",
20
+ }),
21
+ });
13
22
  }
14
23
 
15
24
  export { exportImageAsync };
@@ -155,7 +155,9 @@ document
155
155
  await new Promise((resolve) => setTimeout(resolve, 50));
156
156
  await exportImageAsync(
157
157
  button.parentElement.previousElementSibling,
158
- `${indicator.name} - ${button.dataset.title}`
158
+ `${indicator.name} - ${button.dataset.title}`,
159
+ indicator,
160
+ button.dataset["trackingobjet"]
159
161
  );
160
162
  button.removeAttribute("disabled");
161
163
  });