territories-dashboard-lib 0.1.38__py3-none-any.whl → 1.1.1.dev10__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 (63) hide show
  1. territories_dashboard_lib/geo_lib/admin.py +2 -0
  2. territories_dashboard_lib/geo_lib/migrations/0003_geofeature_color_column_geofeature_size_column.py +23 -0
  3. territories_dashboard_lib/geo_lib/models.py +12 -0
  4. territories_dashboard_lib/geo_lib/payloads.py +7 -21
  5. territories_dashboard_lib/geo_lib/views.py +58 -53
  6. territories_dashboard_lib/indicators_lib/enums.py +61 -37
  7. territories_dashboard_lib/indicators_lib/methodo_pdf.py +6 -1
  8. territories_dashboard_lib/indicators_lib/migrations/0004_alter_indicator_min_mesh.py +18 -0
  9. territories_dashboard_lib/indicators_lib/migrations/0005_auto_20251203_1621.py +124 -0
  10. territories_dashboard_lib/indicators_lib/models.py +7 -4
  11. territories_dashboard_lib/indicators_lib/payloads.py +14 -1
  12. territories_dashboard_lib/indicators_lib/query/commons.py +90 -104
  13. territories_dashboard_lib/indicators_lib/query/comparison.py +8 -3
  14. territories_dashboard_lib/indicators_lib/query/details.py +8 -13
  15. territories_dashboard_lib/indicators_lib/query/histogram.py +0 -1
  16. territories_dashboard_lib/indicators_lib/query/indicator_card.py +12 -7
  17. territories_dashboard_lib/indicators_lib/query/top_10.py +12 -12
  18. territories_dashboard_lib/indicators_lib/query/utils.py +9 -0
  19. territories_dashboard_lib/indicators_lib/table.py +15 -12
  20. territories_dashboard_lib/indicators_lib/views.py +49 -59
  21. territories_dashboard_lib/superset_lib/logic.py +24 -25
  22. territories_dashboard_lib/superset_lib/migrations/0002_alter_filter_mesh.py +18 -0
  23. territories_dashboard_lib/tracking_lib/enums.py +2 -0
  24. territories_dashboard_lib/tracking_lib/migrations/0005_alter_page_cmp_territory_mesh_alter_page_submesh_and_more.py +28 -0
  25. territories_dashboard_lib/tracking_lib/migrations/0006_alter_event_name.py +18 -0
  26. territories_dashboard_lib/tracking_lib/payloads.py +4 -2
  27. territories_dashboard_lib/tracking_lib/views.py +7 -6
  28. territories_dashboard_lib/website_lib/conf.py +28 -0
  29. territories_dashboard_lib/website_lib/context_processors.py +5 -6
  30. territories_dashboard_lib/website_lib/migrations/0005_mainconf_meshes.py +20 -0
  31. territories_dashboard_lib/website_lib/models.py +12 -0
  32. territories_dashboard_lib/website_lib/params.py +34 -22
  33. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/anchor.mjs +43 -0
  34. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/comparaison/page.mjs +7 -9
  35. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/details/page.mjs +2 -7
  36. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/dom.mjs +0 -15
  37. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/enums.mjs +13 -10
  38. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/side_panel.mjs +1 -15
  39. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/theme/page.mjs +7 -9
  40. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/js/pages/indicators/track-visible-indicators.mjs +121 -0
  41. territories_dashboard_lib/website_lib/static/territories_dashboard_lib/website/react/indicatorMap.bundle.js +1 -1
  42. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/layout/base.css +12 -0
  43. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/comparaison/[theme]/page.html +4 -3
  44. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/anchor.html +14 -0
  45. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/geo_params.html +3 -3
  46. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/indicator-card.html +14 -8
  47. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/select_territory.html +32 -0
  48. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/side_panel_geo.html +8 -35
  49. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/details/page.html +9 -8
  50. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/methodo/methodo.js +28 -0
  51. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/methodo/page.html +40 -0
  52. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/themes/page.html +4 -3
  53. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/sitemap/page.html +9 -0
  54. territories_dashboard_lib/website_lib/templatetags/other_filters.py +6 -3
  55. territories_dashboard_lib/website_lib/views.py +100 -0
  56. {territories_dashboard_lib-0.1.38.dist-info → territories_dashboard_lib-1.1.1.dev10.dist-info}/METADATA +2 -2
  57. {territories_dashboard_lib-0.1.38.dist-info → territories_dashboard_lib-1.1.1.dev10.dist-info}/RECORD +60 -49
  58. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/side_panel_methodo.css +0 -29
  59. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/side_panel_methodo.html +0 -45
  60. territories_dashboard_lib/website_lib/templates/territories_dashboard_lib/website/pages/indicators/components/side_panel_methodo.js +0 -19
  61. {territories_dashboard_lib-0.1.38.dist-info → territories_dashboard_lib-1.1.1.dev10.dist-info}/WHEEL +0 -0
  62. {territories_dashboard_lib-0.1.38.dist-info → territories_dashboard_lib-1.1.1.dev10.dist-info}/licenses/licence.md +0 -0
  63. {territories_dashboard_lib-0.1.38.dist-info → territories_dashboard_lib-1.1.1.dev10.dist-info}/top_level.txt +0 -0
@@ -11,7 +11,7 @@
11
11
  <div id="geo_side_panel" class="tdbmd-territory-tabs" style="margin-top: 32px;">
12
12
  <div class="fr-tabs fr-transition-none">
13
13
  <ul class="fr-tabs__list" role="tablist">
14
- {% for m in params.all_meshes %}
14
+ {% for m in params.ordered_meshes %}
15
15
  <li role="presentation">
16
16
  <button
17
17
  id="tabpanel-{{ m }}"
@@ -25,44 +25,17 @@
25
25
  {% endif %}
26
26
  role="tab"
27
27
  aria-controls="tabpanel-{{ m }}-panel">
28
- {{ params.meshes_titles|get_item:m }}
28
+ {{ params.meshes_short_titles|get_item:m }}
29
29
  </button>
30
30
  </li>
31
31
  {% endfor %}
32
32
  </ul>
33
- {% for m in params.all_meshes %}
34
- <div id="tabpanel-{{ m }}-panel" class="fr-tabs__panel tdbmbd-tabpanel-panel {% if m == params.territory_mesh %}fr-tabs__panel--selected{% endif %}" role="tabpanel" aria-labelledby="tabpanel-{{ m }}" tabindex="0" style="transition: none;">
35
- <h2>
36
- {{ params.meshes_titles|get_item:m }}
37
- </h2>
38
- <div class="fr-input-group autocomplete-input">
39
- <label id="territories-label-{{ m }}" class="fr-label" for="search-territories-{{ m }}">Sélectionner un territoire</label>
40
- <div class="fr-input-wrap fr-input-wrap--addon">
41
- <input
42
- data-mesh="{{ m }}"
43
- id="search-territories-{{ m }}" class="fr-input search-territory-input" aria-autocomplete="list" aria-controls="territories-menu-{{ m }}" aria-expanded="false" aria-labelledby="territories-label-{{ m }}" autocomplete="off" role="combobox" type="text"
44
- ></input>
45
- <button class="fr-btn fr-icon-arrow-down-s-line autocomplete-btn" aria-controls="territories-menu-{{ m }}" aria-expanded="false" tabindex="-1" title="Sélectionner un territoire">
46
- </button>
47
- </div>
48
- </div>
49
- <ul id="territories-menu-{{ m }}" data-mesh="{{ m }}" class="tdbmd-combobox-suggestions" role="listbox">
50
- <li data-type="load"><button onclick="loadMoreTerritories(this)" data-mesh="{{ m }}" class="fr-btn fr-btn--secondary fr-btn--sm">Charger plus de résultats</button></li>
51
- </ul>
52
- <input id="selected-territory-id-{{ m }}" type="hidden">
53
- <input id="selected-territory-mesh-{{ m }}" type="hidden">
54
- <input id="selected-territory-raw-name-{{ m }}" type="hidden">
55
- <button
56
- id="selected-territory-validate-{{ m }}"
57
- data-mesh="{{ m }}"
58
- disabled
59
- class="fr-btn validate-territory-btn"
60
- onclick="validateTerritory(this)"
61
- >
62
- Valider
63
- </button>
64
- <em id="selected-territory-name-{{ m }}"></em>
65
- </div>
33
+ {% for m in params.ordered_meshes %}
34
+ {% if m == params.territory_mesh %}
35
+ {% include "./select_territory.html" with selected=True key=m title=params.meshes_long_titles|get_item:m%}
36
+ {% else %}
37
+ {% include "./select_territory.html" with selected=False key=m title=params.meshes_long_titles|get_item:m%}
38
+ {% endif %}
66
39
  {% endfor %}
67
40
  </div>
68
41
  <div class="side-panel-last-close-button-container">
@@ -39,7 +39,6 @@
39
39
  {% include "territories_dashboard_lib/website/pages/indicators/components/themes-list.css" %}
40
40
  {% include "territories_dashboard_lib/website/pages/indicators/components/side_panel_geo.css" %}
41
41
  {% include "territories_dashboard_lib/website/pages/indicators/components/side_panel.css" %}
42
- {% include "territories_dashboard_lib/website/pages/indicators/components/side_panel_methodo.css" %}
43
42
  {% include "territories_dashboard_lib/website/pages/indicators/details/components/filters-reminder.css" %}
44
43
  {% include "territories_dashboard_lib/website/pages/indicators/components/chart-buttons.css" %}
45
44
  {% include "territories_dashboard_lib/website/pages/indicators/details/components/table.css" %}
@@ -85,13 +84,16 @@
85
84
  {{indicator.source}}
86
85
  </p>
87
86
  {% endif %}
88
- <button
89
- class="fr-btn fr-btn--secondary fr-btn--sm fr-icon-file-line fr-btn--icon-right"
90
- aria-label="Afficher la fiche méthodologique dans la barre latérale"
91
- aria-controls="slide-panel-methodo"
87
+ <a
88
+ href="{% url 'website:indicator-methodo' indicator_name=indicator.name %}"
89
+ aria-label="Accéder à la fiche méthodologique - {{ indicator.title }} - nouvel onglet"
90
+ target="_blank"
91
+ class="no-link"
92
92
  >
93
- Fiche méthodologique
94
- </button>
93
+ <button class="fr-btn fr-btn--secondary fr-btn--sm fr-icon-file-line fr-btn--icon-right">
94
+ Fiche méthodologique
95
+ </button>
96
+ </a>
95
97
  </div>
96
98
  <div class="fr-px-md-4w fr-px-2w">
97
99
  <div id="filtres" class="fr-tag-list fr-mt-4w" role="group" aria-label="Filtres disponibles">
@@ -127,7 +129,6 @@
127
129
  {% include "./components/table-flows.html" %}
128
130
  </div>
129
131
  {% include "territories_dashboard_lib/website/pages/indicators/components/side_panel_geo.html" %}
130
- {% include "territories_dashboard_lib/website/pages/indicators/components/side_panel_methodo.html" %}
131
132
  </div>
132
133
  </div>
133
134
  </div>
@@ -0,0 +1,28 @@
1
+ function getIndicator() {
2
+ return JSON.parse(document.getElementById("indicator-js").textContent);
3
+ }
4
+
5
+ async function downloadMethodo() {
6
+ const indicator = getIndicator();
7
+ const response = await fetch(`/api/indicators/${indicator.name}/methodo/`);
8
+
9
+ if (!response.ok) {
10
+ throw new Error("Failed to fetch the PDF file.");
11
+ }
12
+
13
+ // Create a download link
14
+ const blob = await response.blob();
15
+ const url = window.URL.createObjectURL(blob);
16
+ const a = document.createElement("a");
17
+ a.href = url;
18
+ a.download = `${indicator.title}.pdf`;
19
+ a.click();
20
+ window.URL.revokeObjectURL(url);
21
+ }
22
+
23
+ document.addEventListener("DOMContentLoaded", () => {
24
+ const buttons = document.querySelectorAll(".methodo-export-button");
25
+ buttons.forEach((button) => {
26
+ button.addEventListener("click", downloadMethodo);
27
+ });
28
+ });
@@ -0,0 +1,40 @@
1
+ {% extends "territories_dashboard_lib/website/layout/base.html" %}
2
+
3
+ {% load static %}
4
+ {% load other_filters %}
5
+
6
+ {% block page_title %}Méthodologie : {{indicator.title}}{% endblock %}
7
+
8
+ {% block extra_head %}
9
+ {{ indicator|json_script:"indicator-js" }}
10
+ <script>{% include "./methodo.js" %}</script>
11
+ {% endblock %}
12
+
13
+ {% block main %}
14
+ <div class="fr-container fr-pt-5w fr-pb-10w">
15
+ <nav role="navigation" class="fr-breadcrumb" aria-label="vous êtes ici :">
16
+ <button class="fr-breadcrumb__button" aria-expanded="false" aria-controls="breadcrumb-1">Voir le fil d’Ariane</button>
17
+ <div class="fr-collapse" id="breadcrumb-1">
18
+ <ol class="fr-breadcrumb__list">
19
+ <li>
20
+ <a class="fr-breadcrumb__link" href="/">Accueil</a>
21
+ </li>
22
+ <li>
23
+ <a class="fr-breadcrumb__link" href="{% url 'website:theme' theme_name=theme.name %}">{{theme.title}}</a>
24
+ </li>
25
+ <li>
26
+ <a class="fr-breadcrumb__link" href="{% url 'website:indicator-details' indicator_name=indicator.name %}">{{indicator.title}}</a>
27
+ </li>
28
+ <li>
29
+ <a class="fr-breadcrumb__link" aria-current="page">Méthodologie</a>
30
+ </li>
31
+ </ol>
32
+ </div>
33
+ </nav>
34
+ <h1>Fiche méthodologique : {{ indicator.title }}</h1>
35
+ <p>Thématique : {{ indicator.theme }}</p>
36
+ <button class="methodo-export-button fr-btn fr-btn--secondary fr-btn--sm fr-icon-file-pdf-line fr-btn--icon-right fr-mb-4w">Exporter la fiche méthodologique</button>
37
+ <div class="tdbmd-markdown">{{ indicator.methodo_html | safe }}</div>
38
+ <button class="methodo-export-button fr-btn fr-btn--secondary fr-btn--sm fr-icon-file-pdf-line fr-btn--icon-right">Exporter la fiche méthodologique</button>
39
+ </div>
40
+ {% endblock %}
@@ -24,7 +24,6 @@
24
24
  {% include "territories_dashboard_lib/website/pages/indicators/components/indicator-card.css" %}
25
25
  {% include "territories_dashboard_lib/website/pages/indicators/components/side_panel_geo.css" %}
26
26
  {% include "territories_dashboard_lib/website/pages/indicators/components/side_panel.css" %}
27
- {% include "territories_dashboard_lib/website/pages/indicators/components/side_panel_methodo.css" %}
28
27
  {% endblock %}
29
28
 
30
29
 
@@ -61,7 +60,10 @@
61
60
  id="{{sub_theme.name}}"
62
61
  class="subtheme fr-pr-md-4w fr-pl-md-4w fr-pr-2w fr-pl-2w fr-pt-5w fr-pb-5w"
63
62
  >
64
- <h2>{{ sub_theme.title }}</h2>
63
+ <h2>
64
+ {{ sub_theme.title }}
65
+ {% include "territories_dashboard_lib/website/pages/indicators/components/anchor.html" with anchor=sub_theme.name only %}
66
+ </h2>
65
67
  {% for indicator in sub_theme.indicators %}
66
68
  {% with indicator.id|stringformat:"i"|add:"-api-urls" as script_name %}
67
69
  {{ indicator|indicator_api_urls|json_script:script_name }}
@@ -71,7 +73,6 @@
71
73
  </section>
72
74
  {% endfor %}
73
75
  {% include "territories_dashboard_lib/website/pages/indicators/components/side_panel_geo.html" %}
74
- {% include "territories_dashboard_lib/website/pages/indicators/components/side_panel_methodo.html" %}
75
76
  </div>
76
77
  </main>
77
78
  </div>
@@ -40,6 +40,15 @@
40
40
  {% endfor %}
41
41
  </ul>
42
42
  <br/>
43
+ <h3>Fiches méthodologiques</h3>
44
+ <ul>
45
+ {% for indicator in indicators %}
46
+ <li>
47
+ <a href="{% url 'website:indicator-methodo' indicator_name=indicator.name %}">Méthodologie : {{indicator.title}}</a>
48
+ </li>
49
+ {% endfor %}
50
+ </ul>
51
+ <br/>
43
52
  {% if ENABLE_SUPERSET and dashboards %}
44
53
  <h3>Portraits de territoires</h3>
45
54
  <ul>
@@ -1,6 +1,7 @@
1
1
  from django import template
2
2
  from django.urls import reverse
3
- from territories_dashboard_lib.indicators_lib.enums import get_all_meshes
3
+
4
+ from territories_dashboard_lib.website_lib.conf import get_meshes_for_current_project
4
5
 
5
6
  register = template.Library()
6
7
 
@@ -33,6 +34,8 @@ def indicator_api_urls(indicator_dict):
33
34
 
34
35
  @register.filter
35
36
  def should_mesh_analysis(params, indicator):
36
- meshes = get_all_meshes()
37
- above_min_mesh = meshes.index(params["mesh"]) <= meshes.index(indicator["min_mesh"])
37
+ all_meshes = get_meshes_for_current_project()
38
+ above_min_mesh = all_meshes.index(params["mesh"]) <= all_meshes.index(
39
+ indicator["min_mesh"]
40
+ )
38
41
  return above_min_mesh
@@ -1,3 +1,5 @@
1
+ from django.conf import settings
2
+ from django.http import HttpResponse
1
3
  from django.shortcuts import get_object_or_404, redirect, render
2
4
  from django.urls import reverse
3
5
  from django.views.decorators.gzip import gzip_page
@@ -160,6 +162,28 @@ def indicator_details_view(request, *, indicator_name, context):
160
162
  return response
161
163
 
162
164
 
165
+ @gzip_page
166
+ def indicator_methodo_view(request, *, indicator_name):
167
+ indicator = get_object_or_404(Indicator, name=indicator_name)
168
+ theme = indicator.sub_theme.theme
169
+ context = {
170
+ "indicator": serialize_indicator(indicator),
171
+ "theme": theme,
172
+ }
173
+ response = render(
174
+ request,
175
+ "territories_dashboard_lib/website/pages/indicators/methodo/page.html",
176
+ context,
177
+ )
178
+ response = track_page(
179
+ request=request,
180
+ response=response,
181
+ indicator=indicator,
182
+ theme=theme,
183
+ )
184
+ return response
185
+
186
+
163
187
  @with_params
164
188
  def superset_view(request, dashboard_name, context):
165
189
  dashboard = get_object_or_404(Dashboard, short_name=dashboard_name)
@@ -210,3 +234,79 @@ def sitemap_view(request):
210
234
  )
211
235
  response = track_page(request=request, response=response)
212
236
  return response
237
+
238
+
239
+ def _get_base_url(request):
240
+ return (
241
+ settings.BASE_URL
242
+ if hasattr(settings, "BASE_URL")
243
+ else request.build_absolute_uri("/")[:-1]
244
+ )
245
+
246
+
247
+ def raw_sitemap_view(request):
248
+ """Generate XML sitemap for search engines."""
249
+ base_url = _get_base_url(request)
250
+
251
+ urls = []
252
+
253
+ # Landing page
254
+ urls.append(f"{base_url}{reverse('website:landing-page')}")
255
+
256
+ # Theme pages
257
+ for theme in Theme.objects.all().order_by("ordering"):
258
+ urls.append(
259
+ f"{base_url}{reverse('website:theme', kwargs={'theme_name': theme.name})}"
260
+ )
261
+
262
+ # Comparison pages
263
+ for theme in Theme.objects.all().order_by("ordering"):
264
+ urls.append(
265
+ f"{base_url}{reverse('website:comparison', kwargs={'theme_name': theme.name})}"
266
+ )
267
+
268
+ # Indicator detail pages
269
+ for indicator in Indicator.objects.all().order_by("index_in_theme"):
270
+ urls.append(
271
+ f"{base_url}{reverse('website:indicator-details', kwargs={'indicator_name': indicator.name})}"
272
+ )
273
+ # Indicator methodology pages
274
+ urls.append(
275
+ f"{base_url}{reverse('website:indicator-methodo', kwargs={'indicator_name': indicator.name})}"
276
+ )
277
+
278
+ # Static pages
279
+ for static_page in StaticPage.objects.all():
280
+ urls.append(
281
+ f"{base_url}{reverse('website:static-page', kwargs={'page_url': static_page.url})}"
282
+ )
283
+
284
+ # Lexique page (if glossary items exist)
285
+ if GlossaryItem.objects.exists():
286
+ urls.append(f"{base_url}{reverse('website:lexique')}")
287
+
288
+ # Sitemap page itself
289
+ urls.append(f"{base_url}{reverse('website:sitemap')}")
290
+
291
+ # Generate XML
292
+ xml_lines = [
293
+ '<?xml version="1.0" encoding="UTF-8"?>',
294
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
295
+ ]
296
+
297
+ for url in urls:
298
+ xml_lines.append(" <url>")
299
+ xml_lines.append(f" <loc>{url}</loc>")
300
+ xml_lines.append(" </url>")
301
+
302
+ xml_lines.append("</urlset>")
303
+
304
+ xml_content = "\n".join(xml_lines)
305
+
306
+ return HttpResponse(xml_content, content_type="application/xml")
307
+
308
+
309
+ def robots_txt_view(request):
310
+ base_url = _get_base_url(request)
311
+ text_content = f"User-agent: *\nSitemap: {base_url}/sitemap.xml"
312
+ return HttpResponse(text_content, content_type="text/plain")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: territories-dashboard-lib
3
- Version: 0.1.38
3
+ Version: 1.1.1.dev10
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
@@ -11,7 +11,7 @@ Classifier: Topic :: Software Development :: Libraries
11
11
  Requires-Python: <3.14,>=3.13
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: licence.md
14
- Requires-Dist: django>=5.1.6
14
+ Requires-Dist: django<6
15
15
  Requires-Dist: django-nested-admin>=4.1.1
16
16
  Requires-Dist: geoip2>=5.1.0
17
17
  Requires-Dist: gunicorn>=23.0.0