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.

Files changed (67) 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_content.py +20 -0
  60. territories_dashboard_lib/website_lib/templatetags/htmlparams.py +75 -0
  61. territories_dashboard_lib/website_lib/templatetags/other_filters.py +30 -0
  62. territories_dashboard_lib/website_lib/views.py +212 -0
  63. {territories_dashboard_lib-0.1.0.dist-info → territories_dashboard_lib-0.1.1.dist-info}/METADATA +1 -1
  64. territories_dashboard_lib-0.1.1.dist-info/RECORD +67 -0
  65. territories_dashboard_lib-0.1.0.dist-info/RECORD +0 -5
  66. {territories_dashboard_lib-0.1.0.dist-info → territories_dashboard_lib-0.1.1.dist-info}/WHEEL +0 -0
  67. {territories_dashboard_lib-0.1.0.dist-info → territories_dashboard_lib-0.1.1.dist-info}/top_level.txt +0 -0
File without changes
@@ -0,0 +1,36 @@
1
+ import functools
2
+ from typing import get_origin
3
+
4
+ from django.http import HttpResponse
5
+ from pydantic import ValidationError
6
+
7
+
8
+ def use_payload(payload_class):
9
+ def decorator(func):
10
+ @functools.wraps(func)
11
+ def wrapper(request, *args, **kwargs):
12
+ if request.method != "GET":
13
+ raise NotImplementedError
14
+ data = {}
15
+ for field_name, field in payload_class.model_fields.items():
16
+ url_name = field.alias if field.alias else field_name
17
+ is_list_field = get_origin(field.annotation) is list
18
+ url_value = (
19
+ request.GET.getlist(url_name)
20
+ if is_list_field
21
+ else request.GET.get(url_name)
22
+ )
23
+ if url_value is not None:
24
+ data[url_name] = url_value
25
+ try:
26
+ payload = payload_class.model_validate(data)
27
+ except ValidationError as e:
28
+ return HttpResponse(
29
+ e.json(), headers={"content_type": "application/json"}, status=400
30
+ )
31
+
32
+ return func(request, *args, payload=payload, **kwargs)
33
+
34
+ return wrapper
35
+
36
+ return decorator
@@ -0,0 +1,9 @@
1
+ from django.db import models
2
+
3
+
4
+ class CommonModel(models.Model):
5
+ created_at = models.DateTimeField(auto_now_add=True)
6
+ updated_at = models.DateTimeField(auto_now=True)
7
+
8
+ class Meta:
9
+ abstract = True
File without changes
@@ -0,0 +1,64 @@
1
+ from django import forms
2
+ from django.contrib import admin
3
+ from django.db import models
4
+ from django.utils.safestring import mark_safe
5
+
6
+ from territories_dashboard_lib.geo_lib.models import GeoElement, GeoFeature
7
+
8
+
9
+ class GeoColumnInLine(admin.TabularInline):
10
+ model = GeoElement
11
+ fields = ["name", "label", "filterable"]
12
+ extra = 0
13
+ formfield_overrides = {
14
+ models.TextField: {"widget": forms.TextInput()},
15
+ }
16
+
17
+
18
+ class GeoFeatureForm(forms.ModelForm):
19
+ svg_file = forms.FileField(required=False, label="Charger un fichier SVG")
20
+
21
+ class Meta:
22
+ model = GeoFeature
23
+ fields = [
24
+ "indicator",
25
+ "name",
26
+ "title",
27
+ "unite",
28
+ "geo_type",
29
+ "color",
30
+ "show_on_fr_level",
31
+ "point_icon_svg",
32
+ "svg_file",
33
+ ]
34
+
35
+ def clean(self):
36
+ cleaned_data = super().clean()
37
+ svg_file = cleaned_data.get("svg_file")
38
+
39
+ if svg_file:
40
+ svg_content = svg_file.read().decode("utf-8")
41
+ cleaned_data["point_icon_svg"] = svg_content
42
+
43
+ return cleaned_data
44
+
45
+
46
+ class GeoTableAdmin(admin.ModelAdmin):
47
+ form = GeoFeatureForm
48
+ inlines = [GeoColumnInLine]
49
+ formfield_overrides = {
50
+ models.TextField: {"widget": forms.TextInput()},
51
+ }
52
+ list_display = ["__str__", "indicator"]
53
+ list_filter = ["indicator"]
54
+ readonly_fields = ["svg_preview"]
55
+
56
+ def svg_preview(self, obj):
57
+ if obj.point_icon_svg:
58
+ return mark_safe(obj.point_icon_svg)
59
+ return "-"
60
+
61
+ svg_preview.short_description = "Prévisualisation du SVG"
62
+
63
+
64
+ admin.site.register(GeoFeature, GeoTableAdmin)
@@ -0,0 +1,7 @@
1
+ from django.db import models
2
+
3
+
4
+ class GeoFeatureType(models.TextChoices):
5
+ point = "point"
6
+ line = "line"
7
+ polygon = "polygon"
@@ -0,0 +1,51 @@
1
+ # Generated by Django 5.2.3 on 2025-06-19 07:13
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ initial = True
10
+
11
+ dependencies = [
12
+ ('indicators_lib', '0001_initial'),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.CreateModel(
17
+ name='GeoFeature',
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.TextField(help_text='attention le nom doit être exactement le même que celui en base.', verbose_name='Nom de la table en base')),
23
+ ('title', models.TextField(default='à compléter', verbose_name='Titre à afficher')),
24
+ ('unite', models.TextField(verbose_name='Unité au pluriel')),
25
+ ('geo_type', models.TextField(choices=[('point', 'Point'), ('line', 'Line'), ('polygon', 'Polygon')], default='point', verbose_name='Type de géométrie')),
26
+ ('point_icon_svg', models.TextField(blank=True, help_text="Icone SVG a utilisé pour les points.<br/>Laisser vide pour les autres types de géométrie.<br/>Veuillez charger un fichier provenant des <a href='/'>Material Icons</a> de Google.", null=True, verbose_name='Icone des points')),
27
+ ('color', models.TextField(default='#000000', help_text="Couleur des données sur la date, à renseigner au format hexadécimal, par exemple: '#05B2F9'", verbose_name='Couleur')),
28
+ ('show_on_fr_level', models.BooleanField(default=True, help_text='Décochez si la quantité de données est trop importante pour le niveau France entière.', verbose_name='Afficher au niveau France entière')),
29
+ ('indicator', models.ForeignKey(help_text="Cette donnée géographique sera ajoutée à la carte de l'indicateur.", on_delete=django.db.models.deletion.CASCADE, related_name='geo_features', to='indicators_lib.indicator')),
30
+ ],
31
+ options={
32
+ 'verbose_name': 'Données de la carte',
33
+ 'ordering': ('indicator', 'name'),
34
+ },
35
+ ),
36
+ migrations.CreateModel(
37
+ name='GeoElement',
38
+ fields=[
39
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
40
+ ('created_at', models.DateTimeField(auto_now_add=True)),
41
+ ('updated_at', models.DateTimeField(auto_now=True)),
42
+ ('name', models.TextField(verbose_name='Nom de la colonne en DB')),
43
+ ('label', models.TextField()),
44
+ ('filterable', models.BooleanField(default=True, verbose_name='Filtrable')),
45
+ ('geo_feature', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='geo_lib.geofeature')),
46
+ ],
47
+ options={
48
+ 'abstract': False,
49
+ },
50
+ ),
51
+ ]
@@ -0,0 +1,58 @@
1
+ from django.db import models
2
+
3
+ from territories_dashboard_lib.commons.models import CommonModel
4
+ from territories_dashboard_lib.indicators_lib.models import Indicator
5
+
6
+ from .enums import GeoFeatureType
7
+
8
+
9
+ class GeoFeature(CommonModel):
10
+ name = models.TextField(
11
+ verbose_name="Nom de la table en base",
12
+ help_text="attention le nom doit être exactement le même que celui en base.",
13
+ )
14
+ title = models.TextField(verbose_name="Titre à afficher", default="à compléter")
15
+ unite = models.TextField(verbose_name="Unité au pluriel")
16
+ indicator = models.ForeignKey(
17
+ Indicator,
18
+ related_name="geo_features",
19
+ on_delete=models.CASCADE,
20
+ help_text="Cette donnée géographique sera ajoutée à la carte de l'indicateur.",
21
+ )
22
+ geo_type = models.TextField(
23
+ choices=GeoFeatureType.choices,
24
+ default=GeoFeatureType.point,
25
+ verbose_name="Type de géométrie",
26
+ )
27
+ point_icon_svg = models.TextField(
28
+ blank=True,
29
+ null=True,
30
+ verbose_name="Icone des points",
31
+ help_text="Icone SVG a utilisé pour les points.<br/>Laisser vide pour les autres types de géométrie.<br/>Veuillez charger un fichier provenant des <a href='/'>Material Icons</a> de Google.",
32
+ )
33
+ color = models.TextField(
34
+ default="#000000",
35
+ verbose_name="Couleur",
36
+ help_text="Couleur des données sur la date, à renseigner au format hexadécimal, par exemple: '#05B2F9'",
37
+ )
38
+ show_on_fr_level = models.BooleanField(
39
+ default=True,
40
+ verbose_name="Afficher au niveau France entière",
41
+ help_text="Décochez si la quantité de données est trop importante pour le niveau France entière.",
42
+ )
43
+
44
+ def __str__(self):
45
+ return self.name
46
+
47
+ class Meta:
48
+ ordering = ("indicator", "name")
49
+ verbose_name = "Données de la carte"
50
+
51
+
52
+ class GeoElement(CommonModel):
53
+ geo_feature = models.ForeignKey(
54
+ GeoFeature, related_name="items", on_delete=models.CASCADE
55
+ )
56
+ name = models.TextField(verbose_name="Nom de la colonne en DB")
57
+ label = models.TextField()
58
+ filterable = models.BooleanField(default=True, verbose_name="Filtrable")
@@ -0,0 +1,27 @@
1
+ from django.urls import path
2
+
3
+ from .views import (
4
+ geo_features_view,
5
+ main_territory_view,
6
+ precise_view,
7
+ search_territories_view,
8
+ territories_view,
9
+ )
10
+
11
+ app_name = "geo-api"
12
+
13
+ urlpatterns = [
14
+ path(
15
+ "geo-features/",
16
+ geo_features_view,
17
+ name="geo-features",
18
+ ),
19
+ path(
20
+ "main-territory/",
21
+ main_territory_view,
22
+ name="main-territory",
23
+ ),
24
+ path("precise/", precise_view, name="precise"),
25
+ path("territories/", territories_view, name="territories"),
26
+ path("search-territories/", search_territories_view, name="search"),
27
+ ]
@@ -0,0 +1,239 @@
1
+ import base64
2
+ import gzip
3
+ import json
4
+ from typing import Optional
5
+
6
+ from django.http import HttpResponse, JsonResponse
7
+ from django.shortcuts import get_object_or_404
8
+ from django.views.decorators.cache import cache_control
9
+ from django.views.decorators.http import require_GET
10
+ from psycopg2.sql import SQL, Identifier, Literal
11
+ from pydantic import BaseModel
12
+
13
+ from territories_dashboard_lib.indicators_lib.enums import (
14
+ FRANCE_GEOLEVEL_TITLES,
15
+ MESH_DB,
16
+ GeoLevel,
17
+ MeshLevel,
18
+ )
19
+ from territories_dashboard_lib.indicators_lib.query.commons import get_territories_ids
20
+ from territories_dashboard_lib.indicators_lib.query.utils import run_custom_query
21
+
22
+ from .enums import GeoFeatureType
23
+ from .models import GeoFeature
24
+
25
+
26
+ class GeoFeaturesParams(BaseModel):
27
+ mesh: MeshLevel
28
+ geo_level: GeoLevel
29
+ main_territories: str
30
+ last: Optional[str] = None
31
+ limit: Optional[str] = "1000"
32
+ feature: str
33
+
34
+
35
+ @require_GET
36
+ @cache_control(max_age=3600)
37
+ def geo_features_view(request):
38
+ params = GeoFeaturesParams(**request.GET.dict())
39
+
40
+ geo_feature = get_object_or_404(GeoFeature, id=params.feature)
41
+ geo_level = params.geo_level
42
+ mesh = params.mesh
43
+ main_territory_codes = params.main_territories.split(",")
44
+ last = params.last
45
+ limit = params.limit
46
+
47
+ last_year_query = (
48
+ f"SELECT DISTINCT annee FROM {geo_feature.name} ORDER BY annee DESC"
49
+ )
50
+ years = run_custom_query(last_year_query)
51
+ last_year = years[0].get("annee")
52
+
53
+ territories_ids = get_territories_ids(main_territory_codes, geo_level, mesh)
54
+
55
+ col_id_name = f"code_{mesh}"
56
+ columns = [el.name for el in geo_feature.items.all()]
57
+
58
+ coma = ", " if columns else ""
59
+ select_geo_code = (
60
+ f", geo.{col_id_name} AS geo_code"
61
+ if geo_feature.geo_type == GeoFeatureType.point
62
+ else ""
63
+ )
64
+
65
+ if geo_feature.geo_type == GeoFeatureType.point:
66
+ where_clause = f"WHERE geo.{col_id_name} IN ('{"', '".join(territories_ids)}')"
67
+ else:
68
+ where_clause = (
69
+ f"JOIN contours_simplified_{geo_level} contours ON ST_intersects(geo.geometry, contours.geometry)\n"
70
+ f"WHERE contours.code IN ('{"', '".join(main_territory_codes)}')"
71
+ )
72
+ where_clause += f" AND annee = {last_year}"
73
+
74
+ pagination = f"AND order_id > {last or 0} ORDER BY order_id ASC LIMIT {limit}"
75
+
76
+ query = (
77
+ f"SELECT ST_asgeojson(geo.geometry) AS geojson, order_id{coma + ', '.join(columns)}\n"
78
+ f"{select_geo_code}\n"
79
+ f"FROM {geo_feature.name} AS geo\n"
80
+ f"{where_clause} {pagination}"
81
+ )
82
+
83
+ # Execute the query
84
+ results = run_custom_query(query)
85
+ items = geo_feature.items.all()
86
+ # Transform results
87
+ data = [
88
+ {
89
+ "type": "Feature",
90
+ "id": r.get("geo_code"),
91
+ "properties": {el.label: r.get(el.name) for el in items},
92
+ "geometry": json.loads(r.get("geojson")),
93
+ }
94
+ for r in results
95
+ ]
96
+
97
+ last_queried_order_id = results[-1]["order_id"] if results else None
98
+
99
+ compressed_data = gzip.compress(json.dumps(data).encode())
100
+ compressed_base64 = base64.b64encode(compressed_data).decode()
101
+
102
+ return JsonResponse({"data": compressed_base64, "last": last_queried_order_id})
103
+
104
+
105
+ class MainTerritoryParams(BaseModel):
106
+ geo_level: GeoLevel
107
+ geo_id: str
108
+
109
+
110
+ @require_GET
111
+ @cache_control(max_age=3600)
112
+ def main_territory_view(request):
113
+ params = MainTerritoryParams(**request.GET.dict())
114
+
115
+ codes = ", ".join(f"'{code.strip()}'" for code in params.geo_id.split(","))
116
+
117
+ query = f"""
118
+ SELECT code as id, ST_asgeojson(ST_simplify(geometry, 0.01)) AS polygon
119
+ FROM contours_simplified_{params.geo_level}
120
+ WHERE code IN ({codes})
121
+ """
122
+
123
+ results = run_custom_query(query)
124
+
125
+ data = [
126
+ {
127
+ "id": r.get("id"),
128
+ "polygon": json.loads(r.get("polygon")),
129
+ }
130
+ for r in results
131
+ ]
132
+
133
+ return JsonResponse(data, safe=False)
134
+
135
+
136
+ @require_GET
137
+ @cache_control(max_age=3600)
138
+ def precise_view(request):
139
+ territories_ids = request.GET.get("territories", "").split(",")
140
+ mesh_level = request.GET.get("mesh")
141
+
142
+ if not mesh_level:
143
+ return JsonResponse({"error": "Missing 'mesh' parameter"}, status=400)
144
+
145
+ mapped_mesh_level = "DEPCOM" if mesh_level == "com" else mesh_level.upper()
146
+
147
+ query = f"""
148
+ SELECT "{mapped_mesh_level}" AS id, ST_asgeojson(geometry) AS polygon
149
+ FROM contours_geo_ign_{mesh_level}
150
+ WHERE "{mapped_mesh_level}" IN ('{"', '".join(territories_ids)}')
151
+ """
152
+
153
+ results = run_custom_query(query)
154
+
155
+ territories = {r.get("id"): json.loads(r.get("polygon")) for r in results}
156
+
157
+ return JsonResponse(territories)
158
+
159
+
160
+ class TerritoryFeatureParams(BaseModel):
161
+ mesh: MeshLevel
162
+ geo_level: GeoLevel | None = None
163
+ main_territories: str | None = None
164
+ codes: str | None = None
165
+
166
+
167
+ @require_GET
168
+ @cache_control(max_age=3600)
169
+ def territories_view(request):
170
+ params = TerritoryFeatureParams(**request.GET.dict())
171
+ geo_level = params.geo_level
172
+ mesh = params.mesh
173
+
174
+ if params.codes:
175
+ territories_ids = params.codes.split(",")
176
+ else:
177
+ main_territory_codes = params.main_territories.split(",")
178
+ territories_ids = get_territories_ids(main_territory_codes, geo_level, mesh)
179
+
180
+ query = f"""
181
+ SELECT code AS id, ST_asgeojson(geometry) AS polygon
182
+ FROM contours_simplified_{params.mesh}
183
+ WHERE code IN ('{"', '".join(territories_ids)}')
184
+ """
185
+
186
+ results = run_custom_query(query)
187
+
188
+ data = [
189
+ {"type": "Feature", "id": r.get("id"), "geometry": json.loads(r.get("polygon"))}
190
+ for r in results
191
+ ]
192
+ return JsonResponse(data, safe=False)
193
+
194
+
195
+ def _fill_territory_li(code, name, mesh):
196
+ label = name if mesh == MeshLevel.National else f"{name} - {code}"
197
+ return f"""<li data-code="{code}" data-name="{name}" onclick="chooseTerritory(this)"><button aria-label="Choisir {label}">{label}</button></li>"""
198
+
199
+
200
+ @require_GET
201
+ @cache_control(max_age=3600)
202
+ def search_territories_view(request):
203
+ request_mesh = request.GET.get("mesh")
204
+ if request_mesh in MeshLevel:
205
+ mesh = request_mesh
206
+ else:
207
+ raise ValueError("mesh")
208
+ if mesh == MeshLevel.National:
209
+ lis = []
210
+ for code, name in FRANCE_GEOLEVEL_TITLES.items():
211
+ li = _fill_territory_li(code, name, mesh)
212
+ lis.append(li)
213
+ return HttpResponse("\n".join(lis))
214
+ mesh_db = MESH_DB[mesh]
215
+ pagination = 20
216
+ search = request.GET.get("search", "")
217
+ offset = int(request.GET.get("offset", 0))
218
+ query = SQL("""
219
+ SELECT DISTINCT {code} as code, {name} as name FROM arborescence_geo
220
+ WHERE unaccent({name}) || {code} ILIKE unaccent(%s)
221
+ AND "FR" <> 'ETR'
222
+ ORDER BY {name}, {code}
223
+ LIMIT {limit} OFFSET {offset};
224
+ ;
225
+ """).format(
226
+ code=Identifier(mesh_db),
227
+ name=Identifier(f"NOM_{mesh_db}"),
228
+ offset=Literal(offset * pagination),
229
+ limit=Literal(pagination),
230
+ )
231
+ territories = run_custom_query(query, [f"%{search}%"])
232
+ lis = []
233
+ for territory in territories:
234
+ li = _fill_territory_li(territory["code"], territory["name"], mesh)
235
+ lis.append(li)
236
+ if len(territories) == pagination:
237
+ li = f"""<li data-type="load"><button onclick="loadMoreTerritories(this)" data-mesh="{mesh}" data-offset="{offset + 1}" class="fr-btn fr-btn--secondary fr-btn--sm">Charger plus de résultats</button></li>"""
238
+ lis.append(li)
239
+ return HttpResponse("\n".join(lis))
File without changes
@@ -0,0 +1,140 @@
1
+ import nested_admin
2
+ from django.contrib import admin
3
+ from django.db.models import TextField
4
+ from django.forms import TextInput
5
+ from django.shortcuts import redirect
6
+ from django.urls import path, reverse
7
+ from django.utils.html import format_html
8
+
9
+ from territories_dashboard_lib.indicators_lib.methodo_pdf import reset_methodo_file
10
+ from territories_dashboard_lib.indicators_lib.models import (
11
+ Dimension,
12
+ Filter,
13
+ Indicator,
14
+ SubTheme,
15
+ Theme,
16
+ )
17
+ from territories_dashboard_lib.indicators_lib.refresh_filters import refresh_filters
18
+
19
+
20
+ class FilterInline(nested_admin.NestedTabularInline): # type: ignore
21
+ model = Filter
22
+ extra = 0
23
+ formfield_overrides = {TextField: {"widget": TextInput(attrs={"size": "32"})}}
24
+
25
+
26
+ class DimensionInline(nested_admin.NestedTabularInline): # type: ignore
27
+ model = Dimension
28
+ extra = 0
29
+ inlines = [FilterInline]
30
+ formfield_overrides = {TextField: {"widget": TextInput(attrs={"size": "32"})}}
31
+
32
+
33
+ class IndicatorAdmin(nested_admin.NestedModelAdmin):
34
+ model = Indicator
35
+ inlines = [DimensionInline]
36
+ list_display = [
37
+ "name",
38
+ "title",
39
+ "sub_theme",
40
+ "index_in_theme",
41
+ "is_active",
42
+ ]
43
+
44
+ def save_model(self, request, indicator, form, change):
45
+ super().save_model(request, indicator, form, change)
46
+ if "methodo" in form.changed_data:
47
+ reset_methodo_file(indicator)
48
+
49
+ def change_view(self, request, object_id, form_url="", extra_context=None):
50
+ extra_context = extra_context or {}
51
+ extra_context["refresh_url"] = f"../../{object_id}/refresh_filters/"
52
+ return super().change_view(request, object_id, form_url, extra_context)
53
+
54
+ def get_urls(self):
55
+ urls = super().get_urls()
56
+ custom_urls = [
57
+ path(
58
+ "<path:object_id>/refresh_filters/",
59
+ self.admin_site.admin_view(self.refresh_filters_view),
60
+ name="refresh_filters",
61
+ ),
62
+ ]
63
+ return custom_urls + urls
64
+
65
+ def refresh_filters_view(self, request, object_id):
66
+ indicator = Indicator.objects.get(pk=object_id)
67
+ try:
68
+ for dimension in indicator.dimensions.all():
69
+ refresh_filters(dimension)
70
+ self.message_user(request, "Les filtres ont été actualisés.")
71
+ except Exception as e:
72
+ print(e)
73
+ self.message_user(
74
+ request,
75
+ f"Erreur lors de l'actualisation des filtres: {str(e)}",
76
+ level="error",
77
+ )
78
+
79
+ return redirect("../")
80
+
81
+
82
+ class IndicatorInline(admin.TabularInline):
83
+ model = Indicator
84
+ extra = 0
85
+ fields = ["name", "index_in_theme", "link"]
86
+ readonly_fields = ["link"]
87
+ ordering = ["index_in_theme"]
88
+
89
+ def link(self, obj):
90
+ url = reverse("admin:indicators_lib_indicator_change", args=[obj.id])
91
+ return format_html(f'<a href="{url}">Edit</a>')
92
+
93
+
94
+ class SubThemesInline(admin.TabularInline):
95
+ model = SubTheme
96
+ extra = 0
97
+ fields = [
98
+ "name",
99
+ "index_in_theme",
100
+ "indicators_count",
101
+ "is_displayed_on_app",
102
+ "link",
103
+ ]
104
+ ordering = ["index_in_theme"]
105
+ readonly_fields = ["indicators_count", "is_displayed_on_app", "link"]
106
+
107
+ def link(self, obj):
108
+ url = reverse("admin:indicators_lib_subtheme_change", args=[obj.id])
109
+ return format_html(f'<a href="{url}">Edit</a>')
110
+
111
+ link.short_description = "Link"
112
+
113
+
114
+ class ThemeAdmin(admin.ModelAdmin):
115
+ model = Theme
116
+ inlines = [SubThemesInline]
117
+ list_display = [
118
+ "title",
119
+ "ordering",
120
+ "subthemes_count",
121
+ "indicators_count",
122
+ "is_displayed_on_app",
123
+ ]
124
+
125
+
126
+ class SubThemeAdmin(admin.ModelAdmin):
127
+ model = SubTheme
128
+ inlines = [IndicatorInline]
129
+ list_display = [
130
+ "title",
131
+ "theme",
132
+ "index_in_theme",
133
+ "indicators_count",
134
+ "is_displayed_on_app",
135
+ ]
136
+
137
+
138
+ admin.site.register(Theme, ThemeAdmin)
139
+ admin.site.register(SubTheme, SubThemeAdmin)
140
+ admin.site.register(Indicator, IndicatorAdmin)