arthexis 0.1.18__py3-none-any.whl → 0.1.19__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 arthexis might be problematic. Click here for more details.
- {arthexis-0.1.18.dist-info → arthexis-0.1.19.dist-info}/METADATA +37 -10
- {arthexis-0.1.18.dist-info → arthexis-0.1.19.dist-info}/RECORD +26 -26
- config/settings.py +1 -5
- core/system.py +125 -0
- core/tasks.py +0 -22
- core/views.py +35 -4
- nodes/admin.py +1 -2
- nodes/models.py +18 -23
- nodes/tests.py +42 -34
- nodes/urls.py +0 -1
- nodes/views.py +2 -15
- ocpp/admin.py +23 -2
- ocpp/models.py +7 -0
- ocpp/store.py +6 -4
- ocpp/tests.py +14 -1
- ocpp/views.py +65 -12
- pages/admin.py +63 -10
- pages/context_processors.py +11 -0
- pages/middleware.py +3 -0
- pages/models.py +35 -0
- pages/tests.py +177 -34
- pages/urls.py +2 -1
- pages/views.py +70 -23
- {arthexis-0.1.18.dist-info → arthexis-0.1.19.dist-info}/WHEEL +0 -0
- {arthexis-0.1.18.dist-info → arthexis-0.1.19.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.18.dist-info → arthexis-0.1.19.dist-info}/top_level.txt +0 -0
pages/tests.py
CHANGED
|
@@ -8,7 +8,7 @@ django.setup()
|
|
|
8
8
|
|
|
9
9
|
from django.test import Client, RequestFactory, TestCase, SimpleTestCase, override_settings
|
|
10
10
|
from django.test.utils import CaptureQueriesContext
|
|
11
|
-
from django.urls import
|
|
11
|
+
from django.urls import reverse
|
|
12
12
|
from django.templatetags.static import static
|
|
13
13
|
from urllib.parse import quote
|
|
14
14
|
from django.contrib.auth import get_user_model
|
|
@@ -33,6 +33,8 @@ from pages.models import (
|
|
|
33
33
|
UserManual,
|
|
34
34
|
UserStory,
|
|
35
35
|
)
|
|
36
|
+
from django.http import FileResponse
|
|
37
|
+
|
|
36
38
|
from pages.admin import (
|
|
37
39
|
ApplicationAdmin,
|
|
38
40
|
UserManualAdmin,
|
|
@@ -690,23 +692,6 @@ class AdminDashboardAppListTests(TestCase):
|
|
|
690
692
|
resp = self.client.get(reverse("admin:index"))
|
|
691
693
|
self.assertContains(resp, "5. Horologia MODELS")
|
|
692
694
|
|
|
693
|
-
def test_dashboard_handles_missing_last_net_message_url(self):
|
|
694
|
-
from pages.templatetags import admin_extras
|
|
695
|
-
|
|
696
|
-
real_reverse = admin_extras.reverse
|
|
697
|
-
|
|
698
|
-
def fake_reverse(name, *args, **kwargs):
|
|
699
|
-
if name == "last-net-message":
|
|
700
|
-
raise NoReverseMatch("missing")
|
|
701
|
-
return real_reverse(name, *args, **kwargs)
|
|
702
|
-
|
|
703
|
-
with patch("pages.templatetags.admin_extras.reverse", side_effect=fake_reverse):
|
|
704
|
-
resp = self.client.get(reverse("admin:index"))
|
|
705
|
-
|
|
706
|
-
self.assertEqual(resp.status_code, 200)
|
|
707
|
-
self.assertNotIn(b"last-net-message", resp.content)
|
|
708
|
-
|
|
709
|
-
|
|
710
695
|
class AdminSidebarTests(TestCase):
|
|
711
696
|
def setUp(self):
|
|
712
697
|
self.client = Client()
|
|
@@ -816,7 +801,8 @@ class ViewHistoryLoggingTests(TestCase):
|
|
|
816
801
|
)
|
|
817
802
|
landing = module.landings.get(path="/")
|
|
818
803
|
landing.label = "Home Landing"
|
|
819
|
-
landing.
|
|
804
|
+
landing.track_leads = True
|
|
805
|
+
landing.save(update_fields=["label", "track_leads"])
|
|
820
806
|
|
|
821
807
|
resp = self.client.get(
|
|
822
808
|
reverse("pages:index"), HTTP_REFERER="https://example.com/ref"
|
|
@@ -841,7 +827,8 @@ class ViewHistoryLoggingTests(TestCase):
|
|
|
841
827
|
)
|
|
842
828
|
landing = module.landings.get(path="/")
|
|
843
829
|
landing.label = "No Celery"
|
|
844
|
-
landing.
|
|
830
|
+
landing.track_leads = True
|
|
831
|
+
landing.save(update_fields=["label", "track_leads"])
|
|
845
832
|
|
|
846
833
|
resp = self.client.get(reverse("pages:index"))
|
|
847
834
|
|
|
@@ -861,7 +848,8 @@ class ViewHistoryLoggingTests(TestCase):
|
|
|
861
848
|
)
|
|
862
849
|
landing = module.landings.get(path="/")
|
|
863
850
|
landing.enabled = False
|
|
864
|
-
landing.
|
|
851
|
+
landing.track_leads = True
|
|
852
|
+
landing.save(update_fields=["enabled", "track_leads"])
|
|
865
853
|
|
|
866
854
|
resp = self.client.get(reverse("pages:index"))
|
|
867
855
|
|
|
@@ -1130,6 +1118,50 @@ class LogViewerAdminTests(SimpleTestCase):
|
|
|
1130
1118
|
self.assertEqual(context["selected_log"], "selected.log")
|
|
1131
1119
|
self.assertIn("hello world", context["log_content"])
|
|
1132
1120
|
|
|
1121
|
+
def test_log_viewer_applies_line_limit(self):
|
|
1122
|
+
content = "\n".join(f"line {i}" for i in range(50))
|
|
1123
|
+
self._create_log("limited.log", content)
|
|
1124
|
+
response = self._render({"log": "limited.log", "limit": "20"})
|
|
1125
|
+
context = response.context_data
|
|
1126
|
+
self.assertEqual(context["log_limit_choice"], "20")
|
|
1127
|
+
self.assertIn("line 49", context["log_content"])
|
|
1128
|
+
self.assertIn("line 30", context["log_content"])
|
|
1129
|
+
self.assertNotIn("line 29", context["log_content"])
|
|
1130
|
+
|
|
1131
|
+
def test_log_viewer_all_limit_returns_full_log(self):
|
|
1132
|
+
content = "first\nsecond\nthird"
|
|
1133
|
+
self._create_log("all.log", content)
|
|
1134
|
+
response = self._render({"log": "all.log", "limit": "all"})
|
|
1135
|
+
context = response.context_data
|
|
1136
|
+
self.assertEqual(context["log_limit_choice"], "all")
|
|
1137
|
+
self.assertIn("first", context["log_content"])
|
|
1138
|
+
self.assertIn("second", context["log_content"])
|
|
1139
|
+
|
|
1140
|
+
def test_log_viewer_invalid_limit_defaults_to_20(self):
|
|
1141
|
+
content = "\n".join(f"item {i}" for i in range(5))
|
|
1142
|
+
self._create_log("invalid-limit.log", content)
|
|
1143
|
+
response = self._render({"log": "invalid-limit.log", "limit": "oops"})
|
|
1144
|
+
context = response.context_data
|
|
1145
|
+
self.assertEqual(context["log_limit_choice"], "20")
|
|
1146
|
+
|
|
1147
|
+
def test_log_viewer_downloads_selected_log(self):
|
|
1148
|
+
self._create_log("download.log", "downloadable content")
|
|
1149
|
+
request = self._build_request({"log": "download.log", "download": "1"})
|
|
1150
|
+
context = {
|
|
1151
|
+
"site_title": "Constellation",
|
|
1152
|
+
"site_header": "Constellation",
|
|
1153
|
+
"site_url": "/",
|
|
1154
|
+
"available_apps": [],
|
|
1155
|
+
}
|
|
1156
|
+
with patch("pages.admin.admin.site.each_context", return_value=context), patch(
|
|
1157
|
+
"pages.context_processors.get_site", return_value=None
|
|
1158
|
+
):
|
|
1159
|
+
response = log_viewer(request)
|
|
1160
|
+
self.assertIsInstance(response, FileResponse)
|
|
1161
|
+
self.assertIn("attachment", response["Content-Disposition"])
|
|
1162
|
+
content = b"".join(response.streaming_content).decode()
|
|
1163
|
+
self.assertIn("downloadable content", content)
|
|
1164
|
+
|
|
1133
1165
|
def test_log_viewer_reports_missing_log(self):
|
|
1134
1166
|
response = self._render({"log": "missing.log"})
|
|
1135
1167
|
self.assertIn("requested log could not be found", response.context_data["log_error"])
|
|
@@ -1447,17 +1479,17 @@ class NavAppsTests(TestCase):
|
|
|
1447
1479
|
)
|
|
1448
1480
|
app = Application.objects.create(name="Readme")
|
|
1449
1481
|
Module.objects.create(
|
|
1450
|
-
node_role=role, application=app, path="/", is_default=True
|
|
1482
|
+
node_role=role, application=app, path="/", is_default=True, menu="Cookbook"
|
|
1451
1483
|
)
|
|
1452
1484
|
|
|
1453
1485
|
def test_nav_pill_renders(self):
|
|
1454
1486
|
resp = self.client.get(reverse("pages:index"))
|
|
1455
|
-
self.assertContains(resp, "
|
|
1487
|
+
self.assertContains(resp, "COOKBOOK")
|
|
1456
1488
|
self.assertContains(resp, "badge rounded-pill")
|
|
1457
1489
|
|
|
1458
1490
|
def test_nav_pill_renders_with_port(self):
|
|
1459
1491
|
resp = self.client.get(reverse("pages:index"), HTTP_HOST="127.0.0.1:8000")
|
|
1460
|
-
self.assertContains(resp, "
|
|
1492
|
+
self.assertContains(resp, "COOKBOOK")
|
|
1461
1493
|
|
|
1462
1494
|
def test_nav_pill_uses_menu_field(self):
|
|
1463
1495
|
site_app = Module.objects.get()
|
|
@@ -1465,7 +1497,7 @@ class NavAppsTests(TestCase):
|
|
|
1465
1497
|
site_app.save()
|
|
1466
1498
|
resp = self.client.get(reverse("pages:index"))
|
|
1467
1499
|
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">DOCS')
|
|
1468
|
-
self.assertNotContains(resp, 'badge rounded-pill text-bg-secondary">
|
|
1500
|
+
self.assertNotContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOK')
|
|
1469
1501
|
|
|
1470
1502
|
def test_app_without_root_url_excluded(self):
|
|
1471
1503
|
role = NodeRole.objects.get(name="Terminal")
|
|
@@ -1835,8 +1867,57 @@ class ControlNavTests(TestCase):
|
|
|
1835
1867
|
|
|
1836
1868
|
def test_readme_pill_visible(self):
|
|
1837
1869
|
resp = self.client.get(reverse("pages:readme"))
|
|
1838
|
-
self.assertContains(resp, 'href="/
|
|
1839
|
-
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">
|
|
1870
|
+
self.assertContains(resp, 'href="/read/"')
|
|
1871
|
+
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOK')
|
|
1872
|
+
|
|
1873
|
+
def test_cookbook_pill_has_no_dropdown(self):
|
|
1874
|
+
module = Module.objects.get(node_role__name="Control", path="/read/")
|
|
1875
|
+
Landing.objects.create(
|
|
1876
|
+
module=module,
|
|
1877
|
+
path="/man/",
|
|
1878
|
+
label="Manuals",
|
|
1879
|
+
enabled=True,
|
|
1880
|
+
)
|
|
1881
|
+
|
|
1882
|
+
resp = self.client.get(reverse("pages:readme"))
|
|
1883
|
+
|
|
1884
|
+
self.assertContains(
|
|
1885
|
+
resp,
|
|
1886
|
+
'<a class="nav-link" href="/read/"><span class="badge rounded-pill text-bg-secondary">COOKBOOK</span></a>',
|
|
1887
|
+
html=True,
|
|
1888
|
+
)
|
|
1889
|
+
self.assertNotContains(resp, 'dropdown-item" href="/man/"')
|
|
1890
|
+
|
|
1891
|
+
def test_readme_page_includes_qr_share(self):
|
|
1892
|
+
resp = self.client.get(reverse("pages:readme"), {"section": "intro"})
|
|
1893
|
+
self.assertContains(resp, 'id="reader-qr"')
|
|
1894
|
+
self.assertContains(
|
|
1895
|
+
resp,
|
|
1896
|
+
'data-url="http://testserver/read/?section=intro"',
|
|
1897
|
+
)
|
|
1898
|
+
self.assertNotContains(resp, "Scan this page")
|
|
1899
|
+
self.assertNotContains(
|
|
1900
|
+
resp, 'class="small text-break text-muted mt-3 mb-0"'
|
|
1901
|
+
)
|
|
1902
|
+
|
|
1903
|
+
def test_readme_document_by_name(self):
|
|
1904
|
+
resp = self.client.get(reverse("pages:readme-document", args=["AGENTS.md"]))
|
|
1905
|
+
self.assertEqual(resp.status_code, 200)
|
|
1906
|
+
self.assertContains(resp, "Agent Guidelines")
|
|
1907
|
+
|
|
1908
|
+
def test_readme_document_by_relative_path(self):
|
|
1909
|
+
resp = self.client.get(
|
|
1910
|
+
reverse(
|
|
1911
|
+
"pages:readme-document",
|
|
1912
|
+
args=["docs/development/maintenance-roadmap.md"],
|
|
1913
|
+
)
|
|
1914
|
+
)
|
|
1915
|
+
self.assertEqual(resp.status_code, 200)
|
|
1916
|
+
self.assertContains(resp, "Maintenance Improvement Proposals")
|
|
1917
|
+
|
|
1918
|
+
def test_readme_document_rejects_traversal(self):
|
|
1919
|
+
resp = self.client.get("/read/../../SECRET.md")
|
|
1920
|
+
self.assertEqual(resp.status_code, 404)
|
|
1840
1921
|
|
|
1841
1922
|
|
|
1842
1923
|
class SatelliteNavTests(TestCase):
|
|
@@ -1908,8 +1989,8 @@ class SatelliteNavTests(TestCase):
|
|
|
1908
1989
|
|
|
1909
1990
|
def test_readme_pill_visible(self):
|
|
1910
1991
|
resp = self.client.get(reverse("pages:readme"))
|
|
1911
|
-
self.assertContains(resp, 'href="/
|
|
1912
|
-
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">
|
|
1992
|
+
self.assertContains(resp, 'href="/read/"')
|
|
1993
|
+
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOK')
|
|
1913
1994
|
|
|
1914
1995
|
|
|
1915
1996
|
class PowerNavTests(TestCase):
|
|
@@ -2188,6 +2269,47 @@ class UserManualAdminFormTests(TestCase):
|
|
|
2188
2269
|
self.assertEqual(form.cleaned_data["content_pdf"], self.manual.content_pdf)
|
|
2189
2270
|
|
|
2190
2271
|
|
|
2272
|
+
class UserManualModelTests(TestCase):
|
|
2273
|
+
def _build_manual(self, **overrides):
|
|
2274
|
+
defaults = {
|
|
2275
|
+
"slug": "manual-model-test",
|
|
2276
|
+
"title": "Manual Model",
|
|
2277
|
+
"description": "Manual description",
|
|
2278
|
+
"languages": "en",
|
|
2279
|
+
"content_html": "<p>Manual</p>",
|
|
2280
|
+
"content_pdf": base64.b64encode(b"initial").decode("ascii"),
|
|
2281
|
+
}
|
|
2282
|
+
defaults.update(overrides)
|
|
2283
|
+
return UserManual(**defaults)
|
|
2284
|
+
|
|
2285
|
+
def test_save_encodes_uploaded_file(self):
|
|
2286
|
+
upload = SimpleUploadedFile("manual.pdf", b"PDF data")
|
|
2287
|
+
manual = self._build_manual(slug="manual-upload", content_pdf=upload)
|
|
2288
|
+
manual.save()
|
|
2289
|
+
manual.refresh_from_db()
|
|
2290
|
+
self.assertEqual(
|
|
2291
|
+
manual.content_pdf,
|
|
2292
|
+
base64.b64encode(b"PDF data").decode("ascii"),
|
|
2293
|
+
)
|
|
2294
|
+
|
|
2295
|
+
def test_save_encodes_raw_bytes(self):
|
|
2296
|
+
manual = self._build_manual(slug="manual-bytes", content_pdf=b"PDF raw")
|
|
2297
|
+
manual.save()
|
|
2298
|
+
manual.refresh_from_db()
|
|
2299
|
+
self.assertEqual(
|
|
2300
|
+
manual.content_pdf,
|
|
2301
|
+
base64.b64encode(b"PDF raw").decode("ascii"),
|
|
2302
|
+
)
|
|
2303
|
+
|
|
2304
|
+
def test_save_strips_data_uri_prefix(self):
|
|
2305
|
+
encoded = base64.b64encode(b"PDF data").decode("ascii")
|
|
2306
|
+
data_uri = f"data:application/pdf;base64,{encoded}"
|
|
2307
|
+
manual = self._build_manual(slug="manual-data-uri", content_pdf=data_uri)
|
|
2308
|
+
manual.save()
|
|
2309
|
+
manual.refresh_from_db()
|
|
2310
|
+
self.assertEqual(manual.content_pdf, encoded)
|
|
2311
|
+
|
|
2312
|
+
|
|
2191
2313
|
class LandingCreationTests(TestCase):
|
|
2192
2314
|
def setUp(self):
|
|
2193
2315
|
role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
@@ -2547,20 +2669,41 @@ class FavoriteTests(TestCase):
|
|
|
2547
2669
|
self.assertContains(resp, f'aria-label="{badge_label}"')
|
|
2548
2670
|
|
|
2549
2671
|
def test_dashboard_shows_charge_point_availability_badge(self):
|
|
2550
|
-
Charger.objects.create(charger_id="CP-001", last_status="Available")
|
|
2551
2672
|
Charger.objects.create(
|
|
2552
2673
|
charger_id="CP-001", connector_id=1, last_status="Available"
|
|
2553
2674
|
)
|
|
2675
|
+
Charger.objects.create(charger_id="CP-002", last_status="Available")
|
|
2676
|
+
Charger.objects.create(
|
|
2677
|
+
charger_id="CP-003", connector_id=1, last_status="Unavailable"
|
|
2678
|
+
)
|
|
2679
|
+
|
|
2680
|
+
resp = self.client.get(reverse("admin:index"))
|
|
2681
|
+
|
|
2682
|
+
expected = "1 / 2"
|
|
2683
|
+
badge_label = gettext(
|
|
2684
|
+
"%(available)s chargers reporting Available status with a CP number, out of %(total)s total Available chargers. %(missing)s Available chargers are missing a connector number."
|
|
2685
|
+
) % {"available": 1, "total": 2, "missing": 1}
|
|
2686
|
+
|
|
2687
|
+
self.assertContains(resp, expected)
|
|
2688
|
+
self.assertContains(resp, 'class="charger-availability-badge"')
|
|
2689
|
+
self.assertContains(resp, f'title="{badge_label}"')
|
|
2690
|
+
self.assertContains(resp, f'aria-label="{badge_label}"')
|
|
2691
|
+
|
|
2692
|
+
def test_dashboard_charge_point_badge_ignores_aggregator(self):
|
|
2693
|
+
Charger.objects.create(charger_id="CP-AGG", last_status="Available")
|
|
2694
|
+
Charger.objects.create(
|
|
2695
|
+
charger_id="CP-AGG", connector_id=1, last_status="Available"
|
|
2696
|
+
)
|
|
2554
2697
|
Charger.objects.create(
|
|
2555
|
-
charger_id="CP-
|
|
2698
|
+
charger_id="CP-AGG", connector_id=2, last_status="Available"
|
|
2556
2699
|
)
|
|
2557
2700
|
|
|
2558
2701
|
resp = self.client.get(reverse("admin:index"))
|
|
2559
2702
|
|
|
2560
|
-
expected = "2 /
|
|
2703
|
+
expected = "2 / 2"
|
|
2561
2704
|
badge_label = gettext(
|
|
2562
|
-
"%(
|
|
2563
|
-
) % {"
|
|
2705
|
+
"%(available)s chargers reporting Available status with a CP number."
|
|
2706
|
+
) % {"available": 2}
|
|
2564
2707
|
|
|
2565
2708
|
self.assertContains(resp, expected)
|
|
2566
2709
|
self.assertContains(resp, 'class="charger-availability-badge"')
|
pages/urls.py
CHANGED
|
@@ -6,7 +6,8 @@ app_name = "pages"
|
|
|
6
6
|
|
|
7
7
|
urlpatterns = [
|
|
8
8
|
path("", views.index, name="index"),
|
|
9
|
-
path("
|
|
9
|
+
path("read/", views.readme, name="readme"),
|
|
10
|
+
path("read/<path:doc>", views.readme, name="readme-document"),
|
|
10
11
|
path("sitemap.xml", views.sitemap, name="pages-sitemap"),
|
|
11
12
|
path("release/", views.release_admin_redirect, name="release-admin"),
|
|
12
13
|
path("client-report/", views.client_report, name="client-report"),
|
pages/views.py
CHANGED
|
@@ -439,38 +439,80 @@ def admin_model_graph(request, app_label: str):
|
|
|
439
439
|
return response
|
|
440
440
|
|
|
441
441
|
|
|
442
|
-
def _render_readme(request, role):
|
|
442
|
+
def _render_readme(request, role, doc: str | None = None):
|
|
443
443
|
app = (
|
|
444
444
|
Module.objects.filter(node_role=role, is_default=True)
|
|
445
445
|
.select_related("application")
|
|
446
446
|
.first()
|
|
447
447
|
)
|
|
448
448
|
app_slug = app.path.strip("/") if app else ""
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
)
|
|
449
|
+
root_base = Path(settings.BASE_DIR).resolve()
|
|
450
|
+
readme_base = (root_base / app_slug).resolve() if app_slug else root_base
|
|
452
451
|
lang = getattr(request, "LANGUAGE_CODE", "")
|
|
453
452
|
lang = lang.replace("_", "-").lower()
|
|
454
|
-
root_base = Path(settings.BASE_DIR)
|
|
455
453
|
candidates = []
|
|
456
|
-
if
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
454
|
+
if doc:
|
|
455
|
+
normalized = doc.strip().replace("\\", "/")
|
|
456
|
+
while normalized.startswith("./"):
|
|
457
|
+
normalized = normalized[2:]
|
|
458
|
+
normalized = normalized.lstrip("/")
|
|
459
|
+
if not normalized:
|
|
460
|
+
raise Http404("Document not found")
|
|
461
|
+
doc_path = Path(normalized)
|
|
462
|
+
if doc_path.is_absolute() or any(part == ".." for part in doc_path.parts):
|
|
463
|
+
raise Http404("Document not found")
|
|
464
|
+
|
|
465
|
+
relative_candidates: list[Path] = []
|
|
466
|
+
|
|
467
|
+
def add_candidate(path: Path) -> None:
|
|
468
|
+
if path not in relative_candidates:
|
|
469
|
+
relative_candidates.append(path)
|
|
470
|
+
|
|
471
|
+
add_candidate(doc_path)
|
|
472
|
+
if doc_path.suffix.lower() != ".md" or doc_path.suffix != ".md":
|
|
473
|
+
add_candidate(doc_path.with_suffix(".md"))
|
|
474
|
+
if doc_path.suffix.lower() != ".md":
|
|
475
|
+
add_candidate(doc_path / "README.md")
|
|
476
|
+
|
|
477
|
+
search_roots = [readme_base]
|
|
478
|
+
if readme_base != root_base:
|
|
479
|
+
search_roots.append(root_base)
|
|
480
|
+
|
|
481
|
+
for relative in relative_candidates:
|
|
482
|
+
for base in search_roots:
|
|
483
|
+
base_resolved = base.resolve()
|
|
484
|
+
candidate = (base_resolved / relative).resolve(strict=False)
|
|
485
|
+
try:
|
|
486
|
+
candidate.relative_to(base_resolved)
|
|
487
|
+
except ValueError:
|
|
488
|
+
continue
|
|
489
|
+
candidates.append(candidate)
|
|
490
|
+
else:
|
|
463
491
|
if lang:
|
|
464
|
-
candidates.append(
|
|
492
|
+
candidates.append(readme_base / f"README.{lang}.md")
|
|
465
493
|
short = lang.split("-")[0]
|
|
466
494
|
if short != lang:
|
|
467
|
-
candidates.append(
|
|
468
|
-
candidates.append(
|
|
469
|
-
|
|
495
|
+
candidates.append(readme_base / f"README.{short}.md")
|
|
496
|
+
candidates.append(readme_base / "README.md")
|
|
497
|
+
if readme_base != root_base:
|
|
498
|
+
if lang:
|
|
499
|
+
candidates.append(root_base / f"README.{lang}.md")
|
|
500
|
+
short = lang.split("-")[0]
|
|
501
|
+
if short != lang:
|
|
502
|
+
candidates.append(root_base / f"README.{short}.md")
|
|
503
|
+
candidates.append(root_base / "README.md")
|
|
504
|
+
readme_file = next((p for p in candidates if p.exists()), None)
|
|
505
|
+
if readme_file is None:
|
|
506
|
+
raise Http404("Document not found")
|
|
470
507
|
text = readme_file.read_text(encoding="utf-8")
|
|
471
508
|
html, toc_html = _render_markdown_with_toc(text)
|
|
472
509
|
title = "README" if readme_file.name.startswith("README") else readme_file.stem
|
|
473
|
-
context = {
|
|
510
|
+
context = {
|
|
511
|
+
"content": html,
|
|
512
|
+
"title": title,
|
|
513
|
+
"toc": toc_html,
|
|
514
|
+
"page_url": request.build_absolute_uri(),
|
|
515
|
+
}
|
|
474
516
|
response = render(request, "pages/readme.html", context)
|
|
475
517
|
patch_vary_headers(response, ["Accept-Language", "Cookie"])
|
|
476
518
|
return response
|
|
@@ -525,10 +567,10 @@ def index(request):
|
|
|
525
567
|
|
|
526
568
|
|
|
527
569
|
@never_cache
|
|
528
|
-
def readme(request):
|
|
570
|
+
def readme(request, doc=None):
|
|
529
571
|
node = Node.get_local()
|
|
530
572
|
role = node.role if node else None
|
|
531
|
-
return _render_readme(request, role)
|
|
573
|
+
return _render_readme(request, role, doc)
|
|
532
574
|
|
|
533
575
|
|
|
534
576
|
def sitemap(request):
|
|
@@ -976,10 +1018,10 @@ class ClientReportForm(forms.Form):
|
|
|
976
1018
|
label=_("Email destinations"),
|
|
977
1019
|
required=False,
|
|
978
1020
|
widget=forms.Textarea(attrs={"rows": 2}),
|
|
979
|
-
help_text=_("Separate addresses with commas or new lines."),
|
|
1021
|
+
help_text=_("Separate addresses with commas, whitespace, or new lines."),
|
|
980
1022
|
)
|
|
981
1023
|
recurrence = forms.ChoiceField(
|
|
982
|
-
label=_("
|
|
1024
|
+
label=_("Recurrence"),
|
|
983
1025
|
choices=RECURRENCE_CHOICES,
|
|
984
1026
|
initial=ClientReportSchedule.PERIODICITY_NONE,
|
|
985
1027
|
help_text=_("Defines how often the report should be generated automatically."),
|
|
@@ -1006,8 +1048,13 @@ class ClientReportForm(forms.Form):
|
|
|
1006
1048
|
week_str = cleaned.get("week")
|
|
1007
1049
|
if not week_str:
|
|
1008
1050
|
raise forms.ValidationError(_("Please select a week."))
|
|
1009
|
-
|
|
1010
|
-
|
|
1051
|
+
try:
|
|
1052
|
+
year_str, week_num_str = week_str.split("-W", 1)
|
|
1053
|
+
start = datetime.date.fromisocalendar(
|
|
1054
|
+
int(year_str), int(week_num_str), 1
|
|
1055
|
+
)
|
|
1056
|
+
except (TypeError, ValueError):
|
|
1057
|
+
raise forms.ValidationError(_("Please select a week."))
|
|
1011
1058
|
cleaned["start"] = start
|
|
1012
1059
|
cleaned["end"] = start + datetime.timedelta(days=6)
|
|
1013
1060
|
elif period == "month":
|
|
File without changes
|
|
File without changes
|
|
File without changes
|