territories-dashboard-lib 0.1.19__py3-none-any.whl → 0.1.21__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 (23) hide show
  1. territories_dashboard_lib/commons/types.py +20 -0
  2. territories_dashboard_lib/geo_lib/payloads.py +65 -0
  3. territories_dashboard_lib/geo_lib/views.py +19 -37
  4. territories_dashboard_lib/indicators_lib/payloads.py +7 -5
  5. territories_dashboard_lib/indicators_lib/query/commons.py +4 -38
  6. territories_dashboard_lib/indicators_lib/query/indicator_card.py +0 -11
  7. territories_dashboard_lib/indicators_lib/query/top_10.py +2 -0
  8. territories_dashboard_lib/indicators_lib/query/utils.py +25 -1
  9. territories_dashboard_lib/indicators_lib/table.py +9 -9
  10. territories_dashboard_lib/indicators_lib/urls.py +1 -1
  11. territories_dashboard_lib/indicators_lib/views.py +14 -12
  12. territories_dashboard_lib/superset_lib/payloads.py +7 -0
  13. territories_dashboard_lib/superset_lib/views.py +4 -2
  14. territories_dashboard_lib/website_lib/context_processors.py +1 -0
  15. territories_dashboard_lib/website_lib/migrations/0004_mainconf_description_mainconf_social_image_url.py +23 -0
  16. territories_dashboard_lib/website_lib/models.py +12 -0
  17. territories_dashboard_lib/website_lib/params.py +3 -5
  18. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/layout/base.html +11 -0
  19. {territories_dashboard_lib-0.1.19.dist-info → territories_dashboard_lib-0.1.21.dist-info}/METADATA +2 -2
  20. {territories_dashboard_lib-0.1.19.dist-info → territories_dashboard_lib-0.1.21.dist-info}/RECORD +23 -19
  21. {territories_dashboard_lib-0.1.19.dist-info → territories_dashboard_lib-0.1.21.dist-info}/WHEEL +0 -0
  22. {territories_dashboard_lib-0.1.19.dist-info → territories_dashboard_lib-0.1.21.dist-info}/licenses/licence.md +0 -0
  23. {territories_dashboard_lib-0.1.19.dist-info → territories_dashboard_lib-0.1.21.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,20 @@
1
+ from typing import Annotated
2
+
3
+ from pydantic import StringConstraints
4
+
5
+ TerritoryCode = Annotated[
6
+ str, StringConstraints(strip_whitespace=True, pattern=r"^[A-Za-z0-9]*$")
7
+ ]
8
+
9
+ # comme un TerritoryCode mais prend en compte l'exception pour la France entière : territory=FR0,FR1,FR2-fr
10
+ MutualisedTerritoryCode = Annotated[
11
+ str, StringConstraints(strip_whitespace=True, pattern=r"^[A-Za-z0-9,]*$")
12
+ ]
13
+
14
+ SQlName = Annotated[
15
+ str, StringConstraints(strip_whitespace=True, pattern=r"^[A-Za-z0-9_]*$")
16
+ ]
17
+
18
+ SimpleIDType = Annotated[
19
+ str, StringConstraints(strip_whitespace=True, pattern=r"^[A-Za-z0-9_\-]*$")
20
+ ]
@@ -0,0 +1,65 @@
1
+ from typing import List, Optional
2
+
3
+ from pydantic import BaseModel, field_validator
4
+
5
+ from territories_dashboard_lib.commons.types import TerritoryCode
6
+ from territories_dashboard_lib.indicators_lib.enums import (
7
+ GeoLevel,
8
+ MeshLevel,
9
+ )
10
+
11
+
12
+ class GeoFeaturesParams(BaseModel):
13
+ mesh: MeshLevel
14
+ geo_level: GeoLevel
15
+ main_territories: List[TerritoryCode]
16
+ last: Optional[int] = None
17
+ limit: Optional[int] = 1000
18
+ feature: int
19
+
20
+ @field_validator("main_territories", mode="before")
21
+ def split_main_territories(cls, v):
22
+ if isinstance(v, str):
23
+ return v.split(",")
24
+ return v
25
+
26
+
27
+ class MainTerritoryParams(BaseModel):
28
+ geo_level: GeoLevel
29
+ geo_id: List[TerritoryCode]
30
+
31
+ @field_validator("geo_id", mode="before")
32
+ def split_main_territories(cls, v):
33
+ if isinstance(v, str):
34
+ return v.split(",")
35
+ return v
36
+
37
+
38
+ class TerritoriesParams(BaseModel):
39
+ mesh: MeshLevel
40
+ territories: List[TerritoryCode]
41
+
42
+ @field_validator("territories", mode="before")
43
+ def split_main_territories(cls, v):
44
+ if isinstance(v, str):
45
+ return v.split(",")
46
+ return v
47
+
48
+
49
+ class TerritoryFeatureParams(BaseModel):
50
+ mesh: MeshLevel
51
+ geo_level: GeoLevel | None = None
52
+ main_territories: List[TerritoryCode] | None = None
53
+ codes: List[TerritoryCode] | None = None
54
+
55
+ @field_validator("main_territories", "codes", mode="before")
56
+ def split_main_territories(cls, v):
57
+ if isinstance(v, str):
58
+ return v.split(",")
59
+ return v
60
+
61
+
62
+ class SearchTerritoriesParams(BaseModel):
63
+ mesh: MeshLevel
64
+ search: str = ""
65
+ offset: int = 0
@@ -2,19 +2,23 @@ import base64
2
2
  import datetime
3
3
  import gzip
4
4
  import json
5
- from typing import Optional
6
5
 
7
6
  from django.http import HttpResponse, JsonResponse
8
7
  from django.shortcuts import get_object_or_404
9
8
  from django.views.decorators.cache import cache_control
10
9
  from django.views.decorators.http import require_GET
11
10
  from psycopg2.sql import SQL, Identifier, Literal
12
- from pydantic import BaseModel
13
11
 
12
+ from territories_dashboard_lib.geo_lib.payloads import (
13
+ GeoFeaturesParams,
14
+ MainTerritoryParams,
15
+ SearchTerritoriesParams,
16
+ TerritoriesParams,
17
+ TerritoryFeatureParams,
18
+ )
14
19
  from territories_dashboard_lib.indicators_lib.enums import (
15
20
  FRANCE_GEOLEVEL_TITLES,
16
21
  MESH_DB,
17
- GeoLevel,
18
22
  MeshLevel,
19
23
  )
20
24
  from territories_dashboard_lib.indicators_lib.query.commons import get_territories_ids
@@ -24,15 +28,6 @@ from .enums import GeoFeatureType
24
28
  from .models import GeoFeature
25
29
 
26
30
 
27
- class GeoFeaturesParams(BaseModel):
28
- mesh: MeshLevel
29
- geo_level: GeoLevel
30
- main_territories: str
31
- last: Optional[str] = None
32
- limit: Optional[str] = "1000"
33
- feature: str
34
-
35
-
36
31
  class DateTimeEncoder(json.JSONEncoder):
37
32
  def default(self, obj):
38
33
  if isinstance(obj, (datetime.date, datetime.datetime)):
@@ -52,7 +47,7 @@ def geo_features_view(request):
52
47
  geo_feature = get_object_or_404(GeoFeature, id=params.feature)
53
48
  geo_level = params.geo_level
54
49
  mesh = params.mesh
55
- main_territory_codes = params.main_territories.split(",")
50
+ main_territory_codes = params.main_territories
56
51
  last = params.last
57
52
  limit = params.limit
58
53
 
@@ -94,6 +89,7 @@ def geo_features_view(request):
94
89
 
95
90
  # Execute the query
96
91
  results = run_custom_query(query)
92
+
97
93
  items = geo_feature.items.all()
98
94
  # Transform results
99
95
  data = [
@@ -114,17 +110,12 @@ def geo_features_view(request):
114
110
  return JsonResponse({"data": compressed_base64, "last": last_queried_order_id})
115
111
 
116
112
 
117
- class MainTerritoryParams(BaseModel):
118
- geo_level: GeoLevel
119
- geo_id: str
120
-
121
-
122
113
  @require_GET
123
114
  @cache_control(max_age=3600)
124
115
  def main_territory_view(request):
125
116
  params = MainTerritoryParams(**request.GET.dict())
126
117
 
127
- codes = ", ".join(f"'{code.strip()}'" for code in params.geo_id.split(","))
118
+ codes = ", ".join(f"'{code.strip()}'" for code in params.geo_id)
128
119
 
129
120
  query = f"""
130
121
  SELECT code as id, ST_asgeojson(ST_simplify(geometry, 0.01)) AS polygon
@@ -148,8 +139,9 @@ def main_territory_view(request):
148
139
  @require_GET
149
140
  @cache_control(max_age=3600)
150
141
  def precise_view(request):
151
- territories_ids = request.GET.get("territories", "").split(",")
152
- mesh_level = request.GET.get("mesh")
142
+ params = TerritoriesParams(**request.GET.dict())
143
+ mesh_level = params.mesh
144
+ territories_ids = params.territories
153
145
 
154
146
  if not mesh_level:
155
147
  return JsonResponse({"error": "Missing 'mesh' parameter"}, status=400)
@@ -169,13 +161,6 @@ def precise_view(request):
169
161
  return JsonResponse(territories)
170
162
 
171
163
 
172
- class TerritoryFeatureParams(BaseModel):
173
- mesh: MeshLevel
174
- geo_level: GeoLevel | None = None
175
- main_territories: str | None = None
176
- codes: str | None = None
177
-
178
-
179
164
  @require_GET
180
165
  @cache_control(max_age=3600)
181
166
  def territories_view(request):
@@ -184,9 +169,9 @@ def territories_view(request):
184
169
  mesh = params.mesh
185
170
 
186
171
  if params.codes:
187
- territories_ids = params.codes.split(",")
172
+ territories_ids = params.codes
188
173
  else:
189
- main_territory_codes = params.main_territories.split(",")
174
+ main_territory_codes = params.main_territories
190
175
  territories_ids = get_territories_ids(main_territory_codes, geo_level, mesh)
191
176
 
192
177
  query = f"""
@@ -212,11 +197,10 @@ def _fill_territory_li(code, name, mesh):
212
197
  @require_GET
213
198
  @cache_control(max_age=3600)
214
199
  def search_territories_view(request):
215
- request_mesh = request.GET.get("mesh")
216
- if request_mesh in MeshLevel:
217
- mesh = request_mesh
218
- else:
219
- raise ValueError("mesh")
200
+ params = SearchTerritoriesParams(**request.GET.dict())
201
+ mesh = params.mesh
202
+ search = params.search
203
+ offset = params.offset
220
204
  if mesh == MeshLevel.National:
221
205
  lis = []
222
206
  for code, name in FRANCE_GEOLEVEL_TITLES.items():
@@ -225,8 +209,6 @@ def search_territories_view(request):
225
209
  return HttpResponse("\n".join(lis))
226
210
  mesh_db = MESH_DB[mesh]
227
211
  pagination = 20
228
- search = request.GET.get("search", "")
229
- offset = int(request.GET.get("offset", 0))
230
212
  query = SQL("""
231
213
  SELECT DISTINCT {code} as code, {name} as name FROM arborescence_geo
232
214
  WHERE unaccent({name}) || {code} ILIKE unaccent(%s)
@@ -2,11 +2,13 @@ from typing import Annotated, Optional
2
2
 
3
3
  from pydantic import BaseModel, BeforeValidator, Field
4
4
 
5
+ from territories_dashboard_lib.commons.types import MutualisedTerritoryCode, SQlName
6
+
5
7
  from .enums import MeshLevel
6
8
 
7
9
 
8
10
  class Territory(BaseModel):
9
- id: str
11
+ id: MutualisedTerritoryCode
10
12
  mesh: MeshLevel
11
13
 
12
14
 
@@ -24,8 +26,8 @@ class SubMeshPayload(BasePayload):
24
26
 
25
27
 
26
28
  class FlowsPayload(SubMeshPayload):
27
- prefix: str
28
- dimension: str | None = None
29
+ prefix: SQlName
30
+ dimension: SQlName | None = None
29
31
 
30
32
 
31
33
  class ComparisonQueryPayload(SubMeshPayload):
@@ -43,8 +45,8 @@ class OptionalComparisonQueryPayload(SubMeshPayload):
43
45
 
44
46
 
45
47
  class IndicatorTablePayload(SubMeshPayload):
46
- column_order: str | None = None
47
- column_order_flow: str | None = None
48
+ column_order: SQlName | None = None
49
+ column_order_flow: SQlName | None = None
48
50
  pagination: int = 1
49
51
  limit: int = 20
50
52
  previous_limit: int | None = None
@@ -1,9 +1,6 @@
1
1
  from ..enums import (
2
- DEFAULT_MESH,
3
2
  FRANCE_DB_VALUES,
4
- GeoLevel,
5
3
  MeshLevel,
6
- get_miminum_mesh,
7
4
  )
8
5
  from ..models import AggregationFunctions, Indicator
9
6
  from .utils import get_breakdown_dimension, run_custom_query
@@ -112,9 +109,13 @@ def get_table_data_for_geography(
112
109
 
113
110
 
114
111
  def calculate_aggregate_values(indicator, with_alternative=True):
112
+ # TODO coverage tester avec un indicateur de cette sorte
113
+ # Bastien doit exporter des nouvelles données pour faire le test
115
114
  if not indicator.is_composite:
116
115
  return "SUM(valeur) as valeur"
117
116
 
117
+ # TODO coverage tester avec un indicateur de cette sorte
118
+ # Bastien doit exporter des nouvelles données pour faire le test
118
119
  if indicator.aggregation_function == AggregationFunctions.DISCRETE_COMPONENT_2:
119
120
  sql = f"SUM(composante_1) / COALESCE(NULLIF(SUM(composante_2), 0), 1) * {indicator.aggregation_constant} as valeur"
120
121
  if with_alternative:
@@ -132,41 +133,6 @@ def calculate_aggregate_values(indicator, with_alternative=True):
132
133
  return sql
133
134
 
134
135
 
135
- def order_filters(filters, bo_ordered_filters):
136
- # Filters that exist in the predefined order
137
- ordered_filters = [filter for filter in bo_ordered_filters if filter in filters]
138
-
139
- # Filters that are not in the predefined order, sorted alphabetically
140
- not_in_bo = sorted(
141
- [item for item in filters if item not in bo_ordered_filters],
142
- key=lambda x: x.lower(), # Case-insensitive alphabetical sort
143
- )
144
-
145
- # Combine both lists
146
- final_array = ordered_filters + not_in_bo
147
- return final_array
148
-
149
-
150
- def get_mesh_level_for_geo_level(mesh, submesh):
151
- if mesh == GeoLevel.Region:
152
- return (
153
- MeshLevel.Department
154
- if submesh is None or submesh in [MeshLevel.National, MeshLevel.Region]
155
- else submesh
156
- )
157
- elif mesh == GeoLevel.Department:
158
- return (
159
- MeshLevel.Epci
160
- if submesh is None
161
- or submesh in [MeshLevel.National, MeshLevel.Region, MeshLevel.Department]
162
- else submesh
163
- )
164
- elif mesh in [GeoLevel.Epci, GeoLevel.Town]:
165
- return get_miminum_mesh()
166
- else:
167
- return submesh or DEFAULT_MESH
168
-
169
-
170
136
  def get_territories_ids(main_territory_codes, territory_mesh, submesh):
171
137
  mapped_territory_mesh = (
172
138
  "DEPCOM" if territory_mesh == "com" else territory_mesh.upper()
@@ -68,17 +68,6 @@ def get_geography_statistics_values_for_indicator(
68
68
  return query
69
69
 
70
70
 
71
- def get_indicator_filters(indicator):
72
- if not indicator.db_table_prefix or not indicator.dimension:
73
- return None
74
-
75
- # Get the possible indicator's dimension's values
76
- query = f"""
77
- SELECT DISTINCT({indicator.dimension}) as filter FROM "{indicator.db_table_prefix}_reg"
78
- """
79
- return query
80
-
81
-
82
71
  def get_names_from_codes(dict_result, submesh):
83
72
  codes_to_fetch = []
84
73
  for key, value in dict_result.items():
@@ -77,6 +77,8 @@ def get_indicator_top_10_data(indicator, territory, submesh, filters):
77
77
  }
78
78
  for f in breakdown_filters
79
79
  ]
80
+ # TODO coverage faire un test avec un indicateur qui n'a pas de dimensions
81
+ # necessite un export de données de Bastien
80
82
  else:
81
83
  datasets_top_bar_chart = [
82
84
  {
@@ -11,8 +11,32 @@ def get_breakdown_dimension(indicator):
11
11
  return breakdown_dimension
12
12
 
13
13
 
14
+ def get_connection():
15
+ """
16
+ The connection to the indicators database is defined in DATABASES
17
+ The key for the database in DATABASES is defined in settings.INDICATORS_DATABASE
18
+ During testing, the connection can also be defined in READONLY_DATABASES_HANDLER
19
+ which defines a readonly database which will not be recreated during tests
20
+ """
21
+ connection = None
22
+ indicators_database = getattr(settings, "INDICATORS_DATABASE", None)
23
+ if indicators_database:
24
+ connection = connections[indicators_database]
25
+ else:
26
+ testing = getattr(settings, "TESTING", False)
27
+ readonly_databases_handler = getattr(
28
+ settings, "READONLY_DATABASES_HANDLER", None
29
+ )
30
+ if testing and readonly_databases_handler:
31
+ connection = readonly_databases_handler["default"]
32
+ if connection is None:
33
+ raise ValueError("No connection was defined for the indicators database")
34
+ return connection
35
+
36
+
14
37
  def run_custom_query(query, params=None):
15
- with connections[settings.INDICATORS_DATABASE].cursor() as cursor:
38
+ connection = get_connection()
39
+ with connection.cursor() as cursor:
16
40
  cursor.execute(query, params)
17
41
  columns = [col[0] for col in cursor.description]
18
42
  rows = cursor.fetchall()
@@ -12,29 +12,29 @@ def _get_query(
12
12
  limitation = f"LIMIT {limit}" if limit is not None else ""
13
13
  offset = f"OFFSET {(props.pagination - 1) * limit}" if limit is not None else ""
14
14
  dimension_search_where = (
15
- f"OR (unaccent(LOWER({indicator.flows_dimension})) LIKE unaccent(LOWER('%' || '{props.search}' || '%')))"
15
+ f"OR (unaccent(LOWER({indicator.flows_dimension})) LIKE unaccent(LOWER(CONCAT('%%', %(search)s, '%%'))))"
16
16
  if props.flows
17
17
  else " ".join(
18
18
  [
19
- f"OR (unaccent(LOWER({dimension.db_name})) LIKE unaccent(LOWER('%' || '{props.search}' || '%')))"
19
+ f"OR (unaccent(LOWER({dimension.db_name})) LIKE unaccent(LOWER(CONCAT('%%', %(search)s, '%%'))))"
20
20
  for dimension in indicator.dimensions.all()
21
21
  ]
22
22
  )
23
23
  )
24
24
  territory_search_where = (
25
- f"OR (unaccent(LOWER(territory_1)) LIKE unaccent(LOWER('%' || '{props.search}' || '%'))) OR (unaccent(LOWER(territory_2)) LIKE unaccent(LOWER('%' || '{props.search}' || '%')))"
25
+ "OR (unaccent(LOWER(territory_1)) LIKE unaccent(LOWER(CONCAT('%%', %(search)s, '%%')))) OR (unaccent(LOWER(territory_2)) LIKE unaccent(LOWER(CONCAT('%%', %(search)s, '%%'))))"
26
26
  if props.flows
27
27
  else (
28
- f"OR (unaccent(LOWER(lieu)) LIKE unaccent(LOWER('%' || '{props.search}' || '%')))"
28
+ "OR (unaccent(LOWER(lieu)) LIKE unaccent(LOWER(CONCAT('%%', %(search)s, '%%'))))"
29
29
  )
30
30
  )
31
31
  where = (
32
32
  f"""
33
33
  AND (
34
- (LENGTH(TRIM('{props.search}')) = 0)
34
+ (LENGTH(TRIM(%(search)s)) = 0)
35
35
  {territory_search_where}
36
36
  {dimension_search_where}
37
- OR (CAST(annee AS TEXT) LIKE '%' || '{props.search}' || '%')
37
+ OR (CAST(annee AS TEXT) LIKE CONCAT('%%', %(search)s, '%%'))
38
38
  )
39
39
  """
40
40
  if props.search
@@ -128,7 +128,7 @@ def get_count_and_data_for_indicator_table(
128
128
  orders=[],
129
129
  filters=filters,
130
130
  )
131
- count = run_custom_query(count_query)
131
+ count = run_custom_query(count_query, {"search": props.search})
132
132
  columns = get_values_columns(indicator, props)
133
133
  orders = get_values_orders(indicator, props)
134
134
  data_query = _get_query(
@@ -139,7 +139,7 @@ def get_count_and_data_for_indicator_table(
139
139
  limit=props.limit,
140
140
  filters=filters,
141
141
  )
142
- data = run_custom_query(data_query)
142
+ data = run_custom_query(data_query, {"search": props.search})
143
143
  return count[0]["count"], data
144
144
 
145
145
 
@@ -151,4 +151,4 @@ def get_export_indicator_table_values(indicator, props: IndicatorTablePayload, f
151
151
  orders=get_values_orders(indicator, props),
152
152
  filters=filters,
153
153
  )
154
- return run_custom_query(query)
154
+ return run_custom_query(query, {"search": props.search})
@@ -36,7 +36,7 @@ urlpatterns = [
36
36
  path(
37
37
  "<str:name>/values/export/",
38
38
  indicator_values_export_view,
39
- name="values",
39
+ name="values-export",
40
40
  ),
41
41
  path(
42
42
  "<str:name>/values/",
@@ -335,18 +335,20 @@ def flows_view(request, payload):
335
335
  external_territories_ids.add(row["territory_2_id"])
336
336
  external_territories_ids
337
337
 
338
- codes = ", ".join([f"'{tid}'" for tid in external_territories_ids])
339
- geo_query = f"""
340
- SELECT DISTINCT
341
- arbo."{mapped_mesh_level}" as territory_id,
342
- arbo."NOM_{mapped_mesh_level}" as territory_name,
343
- ST_ASGEOJSON(ST_CENTROID(contours.geometry)) as center
344
- FROM arborescence_geo AS arbo
345
- JOIN contours_simplified_{payload.submesh} as contours
346
- ON arbo."{mapped_mesh_level}" = contours.code
347
- WHERE arbo."{mapped_mesh_level}" IN ({codes})
348
- """
349
- external_territories = run_custom_query(geo_query)
338
+ external_territories = []
339
+ if external_territories_ids:
340
+ codes = ", ".join([f"'{tid}'" for tid in external_territories_ids])
341
+ geo_query = f"""
342
+ SELECT DISTINCT
343
+ arbo."{mapped_mesh_level}" as territory_id,
344
+ arbo."NOM_{mapped_mesh_level}" as territory_name,
345
+ ST_ASGEOJSON(ST_CENTROID(contours.geometry)) as center
346
+ FROM arborescence_geo AS arbo
347
+ JOIN contours_simplified_{payload.submesh} as contours
348
+ ON arbo."{mapped_mesh_level}" = contours.code
349
+ WHERE arbo."{mapped_mesh_level}" IN ({codes})
350
+ """
351
+ external_territories = run_custom_query(geo_query)
350
352
  for t in external_territories:
351
353
  territories_dict[t["territory_id"]] = {
352
354
  "name": f"{t['territory_name']} (externe)",
@@ -0,0 +1,7 @@
1
+ from pydantic import BaseModel
2
+
3
+ from territories_dashboard_lib.commons.types import SimpleIDType
4
+
5
+
6
+ class GuestTokenPayload(BaseModel):
7
+ dashboard: SimpleIDType
@@ -1,14 +1,16 @@
1
1
  from django.http import HttpResponse, JsonResponse
2
2
  from django.views.decorators.http import require_GET
3
3
 
4
+ from territories_dashboard_lib.superset_lib.payloads import GuestTokenPayload
5
+
4
6
  from .guest_token import get_guest_token
5
7
  from .models import Dashboard
6
8
 
7
9
 
8
10
  @require_GET
9
11
  def guest_token_view(request):
10
- dashboard_id = request.GET.get("dashboard")
11
- guest_token = get_guest_token(dashboard_id)
12
+ payload = GuestTokenPayload(**request.GET.dict())
13
+ guest_token = get_guest_token(payload.dashboard)
12
14
  return HttpResponse(guest_token)
13
15
 
14
16
 
@@ -16,6 +16,7 @@ def default(request):
16
16
  "ANALYTICS_ID": settings.ANALYTICS_ID,
17
17
  "ENVIRONMENT": settings.ENVIRONMENT,
18
18
  "view_name": request.resolver_match.view_name,
19
+ "absolute_uri": request.build_absolute_uri(),
19
20
  "main_conf": main_conf,
20
21
  "notice": notice,
21
22
  "dashboards": [
@@ -0,0 +1,23 @@
1
+ # Generated by Django 5.2.6 on 2025-09-23 12:27
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('website_lib', '0003_alter_mainconf_footer_navigation_and_more'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='mainconf',
15
+ name='description',
16
+ field=models.TextField(blank=True, help_text='Utilisé pour la balise meta description du site et pour le relais sur les réseaux sociaux.', null=True, verbose_name='Description du site'),
17
+ ),
18
+ migrations.AddField(
19
+ model_name='mainconf',
20
+ name='social_image_url',
21
+ field=models.TextField(blank=True, help_text="Lien d'une image (idéalement 1200x630px). Elle sera utilisée lors du partage sur les réseaux sociaux.", null=True, verbose_name='Image de description'),
22
+ ),
23
+ ]
@@ -16,6 +16,12 @@ class MainConf(CommonModel):
16
16
  title = models.TextField(
17
17
  verbose_name="Titre principal du site", default="Tableau de bord"
18
18
  )
19
+ description = models.TextField(
20
+ verbose_name="Description du site",
21
+ null=True,
22
+ blank=True,
23
+ help_text="Utilisé pour la balise meta description du site et pour le relais sur les réseaux sociaux.",
24
+ )
19
25
  entity = models.TextField(
20
26
  verbose_name="Entité qui possède le site",
21
27
  default="",
@@ -62,6 +68,12 @@ class MainConf(CommonModel):
62
68
  verbose_name="Afficher une bannière de contact au-dessus du footer",
63
69
  help_text="Pour afficher la bannière, l'email de contact et le lien d'inscription à la newsletter doivent être renseignés.",
64
70
  )
71
+ social_image_url = models.TextField(
72
+ null=True,
73
+ blank=True,
74
+ verbose_name="Image de description",
75
+ help_text="Lien d'une image (idéalement 1200x630px). Elle sera utilisée lors du partage sur les réseaux sociaux.",
76
+ )
65
77
 
66
78
  @property
67
79
  def entity_breaklines(self):
@@ -32,6 +32,9 @@ class BadParam(Exception):
32
32
  pass
33
33
 
34
34
 
35
+ # TODO coverage
36
+ # appeler la page détail d'un indicateur (page html, pas api) avec plus de combinaisons de paramètres GET
37
+ # tester les valeurs du context du template
35
38
  class ParamsHandler:
36
39
  def __init__(self, request):
37
40
  self.request = request
@@ -201,13 +204,9 @@ class ParamsHandler:
201
204
  for m in get_all_meshes()
202
205
  if not (m == MeshLevel.Town and max_territory_mesh == MeshLevel.National)
203
206
  ]
204
- print("MESHES", meshes)
205
207
  min_mesh_index = meshes.index(max_territory_mesh)
206
- print("index", min_mesh_index)
207
208
  if get_allow_same_mesh() is False:
208
209
  min_mesh_index += 1
209
- print("AGAIN", min_mesh_index)
210
- print("Allow same mesh", get_allow_same_mesh())
211
210
  self.meshes = meshes[min(min_mesh_index, len(meshes) - 1) :]
212
211
 
213
212
  ######################## Commons
@@ -278,7 +277,6 @@ def with_params(view_func):
278
277
 
279
278
  response = view_func(request, *args, context=context, **kwargs)
280
279
  handler.set_cookie(response)
281
- print(context["params"])
282
280
  return response
283
281
 
284
282
  return wrapper
@@ -25,6 +25,17 @@
25
25
  {% block page_title %}{% endblock %}
26
26
  </title>
27
27
 
28
+ {% if main_conf.description %}
29
+ <meta name="description" content="{{ main_conf.description }}">
30
+ {% if main_conf.social_image_url %}
31
+ <meta property="og:url" content="{{ absolute_uri }}">
32
+ <meta property="og:type" content="website">
33
+ <meta property="og:title" content="{{ main_conf.title }}">
34
+ <meta property="og:description" content="{{ main_conf.description }}">
35
+ <meta property="og:image" content="{{ main_conf.social_image_url }}">
36
+ {% endif %}
37
+ {% endif %}
38
+
28
39
  {% if ENVIRONMENT != "prd" %}
29
40
  <meta name="robots" content="noindex, nofollow">
30
41
  {% endif %}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: territories-dashboard-lib
3
- Version: 0.1.19
3
+ Version: 0.1.21
4
4
  Summary: Librairie pour la visualisation d'indicateurs territoriaux.
5
5
  Author-email: Bastien <bastien@prune.sh>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -16,7 +16,7 @@ Requires-Dist: django-nested-admin>=4.1.1
16
16
  Requires-Dist: geoip2>=5.1.0
17
17
  Requires-Dist: gunicorn>=23.0.0
18
18
  Requires-Dist: ipython>=8.32.0
19
- Requires-Dist: martor>=1.6.45
19
+ Requires-Dist: martor>=1.7.15
20
20
  Requires-Dist: pre-commit>=4.2.0
21
21
  Requires-Dist: psycopg2-binary>=2.9.10
22
22
  Requires-Dist: pydantic>=2.10.6
@@ -2,12 +2,14 @@ territories_dashboard_lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
2
2
  territories_dashboard_lib/commons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  territories_dashboard_lib/commons/decorators.py,sha256=_ACVnbaiCPuplT_81oA0nf_d6l7z0wTCFXMYwXIabew,1225
4
4
  territories_dashboard_lib/commons/models.py,sha256=TQIvoXHSFrnIOQcVptHiMe4ZOdw5byse8sVLjnyeWTI,215
5
+ territories_dashboard_lib/commons/types.py,sha256=H9MuX0GQfdlqFzo67usQ_Gd7p9AaHEdBjfoch9wB4KI,614
5
6
  territories_dashboard_lib/geo_lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
7
  territories_dashboard_lib/geo_lib/admin.py,sha256=4Ek5WP-D3o1mBgPMqIBTPEsT7GeNo9REaw_FguIRw-M,1664
7
8
  territories_dashboard_lib/geo_lib/enums.py,sha256=AwMKwYQP5Z9Qm_5FlO7AtHlJUU92obHXpSpAO1vGuwY,135
8
9
  territories_dashboard_lib/geo_lib/models.py,sha256=r7lPzm0akEnbVTFSG3B_7dtDzFMSHzqpgt2MeNNhL8c,2191
10
+ territories_dashboard_lib/geo_lib/payloads.py,sha256=lwuZ4DpWIAWPyGHUgXOCevToxVimrgEOSHVKP6NpSVw,1646
9
11
  territories_dashboard_lib/geo_lib/urls.py,sha256=W0Zc2z7dn06p4pyFAf0EKATpXAbM0byxCpJK-LRYLak,602
10
- territories_dashboard_lib/geo_lib/views.py,sha256=qG6dZ32nOAONv6pL4UPwlb3pOdNZ7_2FYt6CKiW-ZDU,7885
12
+ territories_dashboard_lib/geo_lib/views.py,sha256=ElpoEGHWwynnq9BPGGadHzH1zyVII1Wq9mQ8zqggSH0,7428
11
13
  territories_dashboard_lib/geo_lib/migrations/0001_initial.py,sha256=-H_5uG0IaPesUfGDkYWdtqHqfWOIKvoQQuAZkfGAu-E,3092
12
14
  territories_dashboard_lib/geo_lib/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
15
  territories_dashboard_lib/indicators_lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -17,29 +19,30 @@ territories_dashboard_lib/indicators_lib/export.py,sha256=pfzGNR09093qG6t_rVzHNc
17
19
  territories_dashboard_lib/indicators_lib/format.py,sha256=W-ajQjrH6E2mseSq4Lozl0bhmEq481tGFIGsjYS8MMo,891
18
20
  territories_dashboard_lib/indicators_lib/methodo_pdf.py,sha256=LpDHMFSNavvfJOI7gDicdY7uF27JpUd-ceUMnOY_FNU,2920
19
21
  territories_dashboard_lib/indicators_lib/models.py,sha256=9f8XmM_WDuIjQTF9V3p3BAqRS0hlbLJjWTGOW5Kdp_4,8323
20
- territories_dashboard_lib/indicators_lib/payloads.py,sha256=I03bgoNoLTehd8lfby0xbSy2zUXKRILMUOqYGqvg-Ig,1288
22
+ territories_dashboard_lib/indicators_lib/payloads.py,sha256=gUM_ZpnnCsDiKAaVLgyrA88Kj9HoGMUSFRhEyEltNVk,1410
21
23
  territories_dashboard_lib/indicators_lib/refresh_filters.py,sha256=uPdKKcCtfwQL2QoGXtRcZcxagYpVv2yiiiTTsCYaXEI,652
22
- territories_dashboard_lib/indicators_lib/table.py,sha256=BsvimE68NxXdRI7xnUVYK1axZRZTywVpSUDEtAYWlMo,5332
23
- territories_dashboard_lib/indicators_lib/urls.py,sha256=BrizGcGJzvcijMvkImMzsi4zRzsgw4fl_cIScsfDYcU,2482
24
- territories_dashboard_lib/indicators_lib/views.py,sha256=EH0bMC_zCXXpou6hqohk2tni3oGLR5rZGVFHu1QH3a4,17138
24
+ territories_dashboard_lib/indicators_lib/table.py,sha256=yBbdW_W0nJAP3wN5kTXj6FylZ9s75GFh-j1tUGWXUNU,5402
25
+ territories_dashboard_lib/indicators_lib/urls.py,sha256=1Rr5mDOitIA40bZcO3lGlseQsvXIwyw1QM6PyUPi_Nw,2489
26
+ territories_dashboard_lib/indicators_lib/views.py,sha256=hxZdM-HpJYj9u3dfQ3ExE4CHnYxpup7otrDgIeaPHB0,17249
25
27
  territories_dashboard_lib/indicators_lib/migrations/0001_initial.py,sha256=3EyizMGkqve5Gtxrmf-mADzzmOhcAVWe-P0y1TIBKCs,8092
26
28
  territories_dashboard_lib/indicators_lib/migrations/0002_filter_color_indicator_min_mesh_and_more.py,sha256=Lz6_UVKLAmaRT-OskwbTMh3PjIh0k4BaGyIOHCC8zMI,884
27
29
  territories_dashboard_lib/indicators_lib/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- territories_dashboard_lib/indicators_lib/query/commons.py,sha256=ikZRa8-VvP5oyZT8Zv4ejvvlHGZjBwPgo9F8HxgMW8I,7885
30
+ territories_dashboard_lib/indicators_lib/query/commons.py,sha256=SI8h2Jdcp8AFzlent-J9MHXOcf1P9wv6r0pqtICNezA,6949
29
31
  territories_dashboard_lib/indicators_lib/query/comparison.py,sha256=ypT8jed99NubgBWqsjZ_4vs9xX2rArnxeGFQ2IY3cKM,2540
30
32
  territories_dashboard_lib/indicators_lib/query/details.py,sha256=si43KVB5QxgQTc9c6AJZ04EK8zPSag9NPfiril9Fv0k,2476
31
33
  territories_dashboard_lib/indicators_lib/query/histogram.py,sha256=NpQdAqVzm-bB5AuMZF3jOzEJKLjecwafhlorkYQY6IU,2964
32
- territories_dashboard_lib/indicators_lib/query/indicator_card.py,sha256=pu-oGTQ8-niIFtZP09HIgJJWkFfJb1CGX99EdsojJDw,3232
33
- territories_dashboard_lib/indicators_lib/query/top_10.py,sha256=3eDBd5w1ovfaMOyN3JQ5mYxBSUrcRJjR1i0v7HG04o4,3677
34
- territories_dashboard_lib/indicators_lib/query/utils.py,sha256=nCeQCLzfq35KZzun9hpM5U34TN7JowB4t3NshIzfLAo,649
34
+ territories_dashboard_lib/indicators_lib/query/indicator_card.py,sha256=ecgv-5gezo3iJ_vVGBHIn2i__Tf_fQqbhGBOffXg_gY,2926
35
+ territories_dashboard_lib/indicators_lib/query/top_10.py,sha256=n-VTwoXAL6L0jZEae3DRNGFsdzY4txgl77bGDCGqen4,3805
36
+ territories_dashboard_lib/indicators_lib/query/utils.py,sha256=o9tQTMSSlidH60JLo5RwVfWY3UXFCx0b44VeHiWC6cM,1610
35
37
  territories_dashboard_lib/superset_lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
38
  territories_dashboard_lib/superset_lib/admin.py,sha256=3Yj3PjCd-jC0g4uMNmah-OST-yoE7DDEzJkx2kWlrXg,563
37
39
  territories_dashboard_lib/superset_lib/guest_token.py,sha256=smsl11NS2Sgsuz5UJlvI_lDaBi6Fj9rdpSVmuUol8Fs,2300
38
40
  territories_dashboard_lib/superset_lib/logic.py,sha256=vVzSimSlrwefVVdEf2dMr83MW2c_yB4k2uw_Szu_nHg,2492
39
41
  territories_dashboard_lib/superset_lib/models.py,sha256=D_IKcYggr8OeH4Gc9KxmU0ArQ3lq8DFN-e0JDQs9Dng,2613
42
+ territories_dashboard_lib/superset_lib/payloads.py,sha256=IKXIuDIMFkSUZNZz4_9VGPHN41YXdRLRYyIIeu0vB1I,163
40
43
  territories_dashboard_lib/superset_lib/serializers.py,sha256=k5DcdwN-k5w9dPchiyxBZtG3BAULp8c00JTO3DR6rA4,250
41
44
  territories_dashboard_lib/superset_lib/urls.py,sha256=URXJLciWDTlzU7gIymlK0nWwAm3Iq8bMkhO0P7RwMuk,254
42
- territories_dashboard_lib/superset_lib/views.py,sha256=AdFNT81Z61zWsyXRqCzGsVXb--lZ2xRfpYVNDiOAyyw,586
45
+ territories_dashboard_lib/superset_lib/views.py,sha256=hsBkU8EshP4b3n_0hL7aFwGOZa2IRWwWwpGWkT21Li8,676
43
46
  territories_dashboard_lib/superset_lib/migrations/0001_initial.py,sha256=Y6iY33EVPMg0XIJu8EErKRJMguYqE7IaZhthZ69OxXA,3461
44
47
  territories_dashboard_lib/superset_lib/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
48
  territories_dashboard_lib/tracking_lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -59,17 +62,18 @@ territories_dashboard_lib/tracking_lib/migrations/0004_cookieinfo_ip_address.py,
59
62
  territories_dashboard_lib/tracking_lib/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
63
  territories_dashboard_lib/website_lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
64
  territories_dashboard_lib/website_lib/admin.py,sha256=jq0KgN8CPVqJQ1IMq8kaoWqqxzpPDyW0z7cTatmrmi0,952
62
- territories_dashboard_lib/website_lib/context_processors.py,sha256=Xq9W_w86Oh242VSGAjScHlgpnXuNcnNt3kSRIqK-ETc,863
65
+ territories_dashboard_lib/website_lib/context_processors.py,sha256=0lI2KvOBCGyvE52xnTfFyAVget25DLNtaIMgyT9NfAI,917
63
66
  territories_dashboard_lib/website_lib/forms.py,sha256=BNbHx6CgMlA33z6eD-1VaI7AkyO20ZsloJKsec4BcoU,1146
64
- territories_dashboard_lib/website_lib/models.py,sha256=y5L7MtONhA9oev5UI6rBejHexLo7r1v_XyxB8h2OEs4,5356
67
+ territories_dashboard_lib/website_lib/models.py,sha256=w5EtwvuzRc8Ei4jQBJOobcCJchokzTc5u1givt6URd0,5852
65
68
  territories_dashboard_lib/website_lib/navigation.py,sha256=D9YNfBVPpO9bRgNT02j99sp0A0_uqDaBgc25BbQOako,3994
66
- territories_dashboard_lib/website_lib/params.py,sha256=M7a3SsLpFiGsPlHy9hOzbwvnWRGyOHSCeDLym70gFTA,10067
69
+ territories_dashboard_lib/website_lib/params.py,sha256=SLPoaaKmkkis8c61d6DqIlZOpFaI1AKN9ZOJy1c06cg,10036
67
70
  territories_dashboard_lib/website_lib/serializers.py,sha256=PVcOC52zlNn2tp7IRwMuB4T7rOaLl3bLKiqqesWgRE4,3772
68
71
  territories_dashboard_lib/website_lib/static_content.py,sha256=08Fw3190RsTZ8wXwZkmskh-86widq7NRmTD9Ve-9PJY,589
69
72
  territories_dashboard_lib/website_lib/views.py,sha256=ZNz23Vrws-L49xeZzrhSJA8KFpqSnURn-9-b0RdR9JE,6760
70
73
  territories_dashboard_lib/website_lib/migrations/0001_initial.py,sha256=4g2YgUUBEdak_iW717cJojCv5kqnNV_46GVnw8u8eAw,5701
71
74
  territories_dashboard_lib/website_lib/migrations/0002_mainconf_contact_email_mainconf_newsletter_link_and_more.py,sha256=HJbz8w63SGhPwBNCqPJXFMuepkHfImgnmqRe3gQ3W8g,1052
72
75
  territories_dashboard_lib/website_lib/migrations/0003_alter_mainconf_footer_navigation_and_more.py,sha256=TK8o5EPq1Zcs737XLMDmFvbn3LqhjK3RRZBS9vcfD_I,1654
76
+ territories_dashboard_lib/website_lib/migrations/0004_mainconf_description_mainconf_social_image_url.py,sha256=FrCn4IwajxX7s80fnTK43NH8xUPpw1oUD13TOTz0jqA,894
73
77
  territories_dashboard_lib/website_lib/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
78
  territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/chart.js,sha256=qKg5SGiIDybjFd0WYySXtFROPHmdJVENEIatEXIQv34,205577
75
79
  territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/chartjs-plugin-datalabels.js,sha256=nhS5Le-PSsSKXcYJl2KaaI1NMFQlRIN2Oggha7ehtdc,12754
@@ -112,7 +116,7 @@ territories_dashboard_lib/website_lib/templates/admin/indicators_lib/indicator/c
112
116
  territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/404.html,sha256=IbXsHmSXPdhbhMFPYCFnuBCjSLTdFPNpeJbl-vQXX-o,278
113
117
  territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/500.html,sha256=zKtoQpzH_wOlNe_COwXANZR4lIV5DnZgcuymezmdk0g,278
114
118
  territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/layout/base.css,sha256=5Y_BiY7qqgR1XBjqz22tqbWpMqwyhydDNCIbim_W8jw,1385
115
- territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/layout/base.html,sha256=AR_v3FCtQYkuwxdGROTLw0CkeFncPjcqzZLDjCUWz-o,1801
119
+ territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/layout/base.html,sha256=T5a0xLiRBrZPsJ3SfIs5_S-glWUiAUxf312vmgMFsy8,2331
116
120
  territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/layout/base.js,sha256=IHW3YF3451k-G7GjZieQ7sBaRPezNK5TYt1WV_qF5OY,1624
117
121
  territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/layout/footer.css,sha256=IX2OUf1BCJEh3fMbpwvtQFtMVj24AY9GEf22DsHmEN4,233
118
122
  territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/layout/footer.html,sha256=I_JyfoVq1IZi9ESZA8C4nlYiJgIVYShy3L-rVioI5NA,6124
@@ -173,8 +177,8 @@ territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/websit
173
177
  territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/svg/delete-back.svg,sha256=fJPjkZT2jdTcW2nmwr7YtBayOmQKOxiGNyO8bM8jP2U,618
174
178
  territories_dashboard_lib/website_lib/templatetags/htmlparams.py,sha256=jWXlmT-nFh7YefVP_zJSXEA1YgT37wnWqelPhmj7UoM,2092
175
179
  territories_dashboard_lib/website_lib/templatetags/other_filters.py,sha256=EEczSQi8lJZ_rfceuXQz3iF1SIxh5d-3-liTY8lkKtU,944
176
- territories_dashboard_lib-0.1.19.dist-info/licenses/licence.md,sha256=9Tat8mM_Yyww-wmWps8avlOhzumqEUGYy8853tKy7aE,7804
177
- territories_dashboard_lib-0.1.19.dist-info/METADATA,sha256=bPm6D_xw0kDA27Xwz6BVUN5vnPre9jWiqXmz_7UJtNo,6191
178
- territories_dashboard_lib-0.1.19.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
179
- territories_dashboard_lib-0.1.19.dist-info/top_level.txt,sha256=Cih-lil8CSXTpDZV6fgHKIKIBtUjDaNpmkiuW2TSzsk,26
180
- territories_dashboard_lib-0.1.19.dist-info/RECORD,,
180
+ territories_dashboard_lib-0.1.21.dist-info/licenses/licence.md,sha256=9Tat8mM_Yyww-wmWps8avlOhzumqEUGYy8853tKy7aE,7804
181
+ territories_dashboard_lib-0.1.21.dist-info/METADATA,sha256=i12-Pzgaubzydg7rCXFfPMxfjgR5ikBXvjShSGZeARM,6191
182
+ territories_dashboard_lib-0.1.21.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
183
+ territories_dashboard_lib-0.1.21.dist-info/top_level.txt,sha256=Cih-lil8CSXTpDZV6fgHKIKIBtUjDaNpmkiuW2TSzsk,26
184
+ territories_dashboard_lib-0.1.21.dist-info/RECORD,,