arthexis 0.1.18__py3-none-any.whl → 0.1.20__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.
pages/tests.py CHANGED
@@ -3,12 +3,13 @@ import os
3
3
  os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
4
4
 
5
5
  import django
6
+ import pytest
6
7
 
7
8
  django.setup()
8
9
 
9
10
  from django.test import Client, RequestFactory, TestCase, SimpleTestCase, override_settings
10
11
  from django.test.utils import CaptureQueriesContext
11
- from django.urls import NoReverseMatch, reverse
12
+ from django.urls import reverse
12
13
  from django.templatetags.static import static
13
14
  from urllib.parse import quote
14
15
  from django.contrib.auth import get_user_model
@@ -33,6 +34,8 @@ from pages.models import (
33
34
  UserManual,
34
35
  UserStory,
35
36
  )
37
+ from django.http import FileResponse
38
+
36
39
  from pages.admin import (
37
40
  ApplicationAdmin,
38
41
  UserManualAdmin,
@@ -506,6 +509,7 @@ class InvitationTests(TestCase):
506
509
  lead = InviteLead.objects.get()
507
510
  self.assertEqual(lead.mac_address, "aa:bb:cc:dd:ee:ff")
508
511
 
512
+ @pytest.mark.feature("ap-router")
509
513
  @patch("pages.views.public_wifi.grant_public_access")
510
514
  @patch(
511
515
  "pages.views.public_wifi.resolve_mac_address",
@@ -690,23 +694,6 @@ class AdminDashboardAppListTests(TestCase):
690
694
  resp = self.client.get(reverse("admin:index"))
691
695
  self.assertContains(resp, "5. Horologia MODELS")
692
696
 
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
697
  class AdminSidebarTests(TestCase):
711
698
  def setUp(self):
712
699
  self.client = Client()
@@ -816,7 +803,8 @@ class ViewHistoryLoggingTests(TestCase):
816
803
  )
817
804
  landing = module.landings.get(path="/")
818
805
  landing.label = "Home Landing"
819
- landing.save(update_fields=["label"])
806
+ landing.track_leads = True
807
+ landing.save(update_fields=["label", "track_leads"])
820
808
 
821
809
  resp = self.client.get(
822
810
  reverse("pages:index"), HTTP_REFERER="https://example.com/ref"
@@ -841,7 +829,8 @@ class ViewHistoryLoggingTests(TestCase):
841
829
  )
842
830
  landing = module.landings.get(path="/")
843
831
  landing.label = "No Celery"
844
- landing.save(update_fields=["label"])
832
+ landing.track_leads = True
833
+ landing.save(update_fields=["label", "track_leads"])
845
834
 
846
835
  resp = self.client.get(reverse("pages:index"))
847
836
 
@@ -861,7 +850,8 @@ class ViewHistoryLoggingTests(TestCase):
861
850
  )
862
851
  landing = module.landings.get(path="/")
863
852
  landing.enabled = False
864
- landing.save(update_fields=["enabled"])
853
+ landing.track_leads = True
854
+ landing.save(update_fields=["enabled", "track_leads"])
865
855
 
866
856
  resp = self.client.get(reverse("pages:index"))
867
857
 
@@ -1130,6 +1120,50 @@ class LogViewerAdminTests(SimpleTestCase):
1130
1120
  self.assertEqual(context["selected_log"], "selected.log")
1131
1121
  self.assertIn("hello world", context["log_content"])
1132
1122
 
1123
+ def test_log_viewer_applies_line_limit(self):
1124
+ content = "\n".join(f"line {i}" for i in range(50))
1125
+ self._create_log("limited.log", content)
1126
+ response = self._render({"log": "limited.log", "limit": "20"})
1127
+ context = response.context_data
1128
+ self.assertEqual(context["log_limit_choice"], "20")
1129
+ self.assertIn("line 49", context["log_content"])
1130
+ self.assertIn("line 30", context["log_content"])
1131
+ self.assertNotIn("line 29", context["log_content"])
1132
+
1133
+ def test_log_viewer_all_limit_returns_full_log(self):
1134
+ content = "first\nsecond\nthird"
1135
+ self._create_log("all.log", content)
1136
+ response = self._render({"log": "all.log", "limit": "all"})
1137
+ context = response.context_data
1138
+ self.assertEqual(context["log_limit_choice"], "all")
1139
+ self.assertIn("first", context["log_content"])
1140
+ self.assertIn("second", context["log_content"])
1141
+
1142
+ def test_log_viewer_invalid_limit_defaults_to_20(self):
1143
+ content = "\n".join(f"item {i}" for i in range(5))
1144
+ self._create_log("invalid-limit.log", content)
1145
+ response = self._render({"log": "invalid-limit.log", "limit": "oops"})
1146
+ context = response.context_data
1147
+ self.assertEqual(context["log_limit_choice"], "20")
1148
+
1149
+ def test_log_viewer_downloads_selected_log(self):
1150
+ self._create_log("download.log", "downloadable content")
1151
+ request = self._build_request({"log": "download.log", "download": "1"})
1152
+ context = {
1153
+ "site_title": "Constellation",
1154
+ "site_header": "Constellation",
1155
+ "site_url": "/",
1156
+ "available_apps": [],
1157
+ }
1158
+ with patch("pages.admin.admin.site.each_context", return_value=context), patch(
1159
+ "pages.context_processors.get_site", return_value=None
1160
+ ):
1161
+ response = log_viewer(request)
1162
+ self.assertIsInstance(response, FileResponse)
1163
+ self.assertIn("attachment", response["Content-Disposition"])
1164
+ content = b"".join(response.streaming_content).decode()
1165
+ self.assertIn("downloadable content", content)
1166
+
1133
1167
  def test_log_viewer_reports_missing_log(self):
1134
1168
  response = self._render({"log": "missing.log"})
1135
1169
  self.assertIn("requested log could not be found", response.context_data["log_error"])
@@ -1328,6 +1362,7 @@ class SiteAdminRegisterCurrentTests(TestCase):
1328
1362
  self.assertEqual(site.name, "")
1329
1363
 
1330
1364
 
1365
+ @pytest.mark.feature("screenshot-poll")
1331
1366
  class SiteAdminScreenshotTests(TestCase):
1332
1367
  def setUp(self):
1333
1368
  self.client = Client()
@@ -1447,17 +1482,17 @@ class NavAppsTests(TestCase):
1447
1482
  )
1448
1483
  app = Application.objects.create(name="Readme")
1449
1484
  Module.objects.create(
1450
- node_role=role, application=app, path="/", is_default=True
1485
+ node_role=role, application=app, path="/", is_default=True, menu="Cookbooks"
1451
1486
  )
1452
1487
 
1453
1488
  def test_nav_pill_renders(self):
1454
1489
  resp = self.client.get(reverse("pages:index"))
1455
- self.assertContains(resp, "README")
1490
+ self.assertContains(resp, "COOKBOOKS")
1456
1491
  self.assertContains(resp, "badge rounded-pill")
1457
1492
 
1458
1493
  def test_nav_pill_renders_with_port(self):
1459
1494
  resp = self.client.get(reverse("pages:index"), HTTP_HOST="127.0.0.1:8000")
1460
- self.assertContains(resp, "README")
1495
+ self.assertContains(resp, "COOKBOOKS")
1461
1496
 
1462
1497
  def test_nav_pill_uses_menu_field(self):
1463
1498
  site_app = Module.objects.get()
@@ -1465,7 +1500,7 @@ class NavAppsTests(TestCase):
1465
1500
  site_app.save()
1466
1501
  resp = self.client.get(reverse("pages:index"))
1467
1502
  self.assertContains(resp, 'badge rounded-pill text-bg-secondary">DOCS')
1468
- self.assertNotContains(resp, 'badge rounded-pill text-bg-secondary">README')
1503
+ self.assertNotContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
1469
1504
 
1470
1505
  def test_app_without_root_url_excluded(self):
1471
1506
  role = NodeRole.objects.get(name="Terminal")
@@ -1530,20 +1565,22 @@ class RoleLandingRedirectTests(TestCase):
1530
1565
 
1531
1566
  def test_satellite_redirects_to_dashboard(self):
1532
1567
  target = self._configure_role_landing(
1533
- "Satellite", "/ocpp/", "CPMS Online Dashboard"
1568
+ "Satellite", "/ocpp/cpms/dashboard/", "CPMS Online Dashboard"
1534
1569
  )
1535
1570
  resp = self.client.get(reverse("pages:index"))
1536
1571
  self.assertRedirects(resp, target, fetch_redirect_response=False)
1537
1572
 
1538
1573
  def test_control_redirects_to_rfid(self):
1539
1574
  target = self._configure_role_landing(
1540
- "Control", "/ocpp/rfid/", "RFID Tag Validator"
1575
+ "Control", "/ocpp/rfid/validator/", "RFID Tag Validator"
1541
1576
  )
1542
1577
  resp = self.client.get(reverse("pages:index"))
1543
1578
  self.assertRedirects(resp, target, fetch_redirect_response=False)
1544
1579
 
1545
1580
  def test_security_group_redirect_takes_priority(self):
1546
- self._configure_role_landing("Control", "/ocpp/rfid/", "RFID Tag Validator")
1581
+ self._configure_role_landing(
1582
+ "Control", "/ocpp/rfid/validator/", "RFID Tag Validator"
1583
+ )
1547
1584
  role = self.node.role
1548
1585
  group = SecurityGroup.objects.create(name="Operators")
1549
1586
  group_landing = self._ensure_landing(role, "/ocpp/group/", "Group Landing")
@@ -1560,7 +1597,9 @@ class RoleLandingRedirectTests(TestCase):
1560
1597
  )
1561
1598
 
1562
1599
  def test_user_redirect_overrides_group_with_higher_priority(self):
1563
- self._configure_role_landing("Control", "/ocpp/rfid/", "RFID Tag Validator")
1600
+ self._configure_role_landing(
1601
+ "Control", "/ocpp/rfid/validator/", "RFID Tag Validator"
1602
+ )
1564
1603
  role = self.node.role
1565
1604
  group = SecurityGroup.objects.create(name="Operators")
1566
1605
  group_landing = self._ensure_landing(role, "/ocpp/group/", "Group Landing")
@@ -1582,10 +1621,10 @@ class RoleLandingRedirectTests(TestCase):
1582
1621
  )
1583
1622
 
1584
1623
 
1585
- class ConstellationNavTests(TestCase):
1624
+ class WatchtowerNavTests(TestCase):
1586
1625
  def setUp(self):
1587
1626
  self.client = Client()
1588
- role, _ = NodeRole.objects.get_or_create(name="Constellation")
1627
+ role, _ = NodeRole.objects.get_or_create(name="Watchtower")
1589
1628
  Node.objects.update_or_create(
1590
1629
  mac_address=Node.get_current_mac(),
1591
1630
  defaults={
@@ -1595,38 +1634,56 @@ class ConstellationNavTests(TestCase):
1595
1634
  },
1596
1635
  )
1597
1636
  Site.objects.update_or_create(
1598
- id=1, defaults={"domain": "testserver", "name": ""}
1637
+ id=1, defaults={"domain": "arthexis.com", "name": "Arthexis"}
1599
1638
  )
1600
1639
  fixtures = [
1601
1640
  Path(
1602
1641
  settings.BASE_DIR,
1603
1642
  "pages",
1604
1643
  "fixtures",
1605
- "constellation__application_ocpp.json",
1644
+ "default__application_pages.json",
1645
+ ),
1646
+ Path(
1647
+ settings.BASE_DIR,
1648
+ "pages",
1649
+ "fixtures",
1650
+ "watchtower__application_ocpp.json",
1606
1651
  ),
1607
1652
  Path(
1608
1653
  settings.BASE_DIR,
1609
1654
  "pages",
1610
1655
  "fixtures",
1611
- "constellation__module_ocpp.json",
1656
+ "watchtower__module_ocpp.json",
1612
1657
  ),
1613
1658
  Path(
1614
1659
  settings.BASE_DIR,
1615
1660
  "pages",
1616
1661
  "fixtures",
1617
- "constellation__landing_ocpp_dashboard.json",
1662
+ "watchtower__landing_ocpp_dashboard.json",
1618
1663
  ),
1619
1664
  Path(
1620
1665
  settings.BASE_DIR,
1621
1666
  "pages",
1622
1667
  "fixtures",
1623
- "constellation__landing_ocpp_cp_simulator.json",
1668
+ "watchtower__landing_ocpp_cp_simulator.json",
1624
1669
  ),
1625
1670
  Path(
1626
1671
  settings.BASE_DIR,
1627
1672
  "pages",
1628
1673
  "fixtures",
1629
- "constellation__landing_ocpp_rfid.json",
1674
+ "watchtower__landing_ocpp_rfid.json",
1675
+ ),
1676
+ Path(
1677
+ settings.BASE_DIR,
1678
+ "pages",
1679
+ "fixtures",
1680
+ "watchtower__module_readme.json",
1681
+ ),
1682
+ Path(
1683
+ settings.BASE_DIR,
1684
+ "pages",
1685
+ "fixtures",
1686
+ "watchtower__landing_readme.json",
1630
1687
  ),
1631
1688
  ]
1632
1689
  call_command("loaddata", *map(str, fixtures))
@@ -1639,13 +1696,13 @@ class ConstellationNavTests(TestCase):
1639
1696
  self.assertNotIn("RFID", nav_labels)
1640
1697
  self.assertTrue(
1641
1698
  Module.objects.filter(
1642
- path="/ocpp/", node_role__name="Constellation"
1699
+ path="/ocpp/", node_role__name="Watchtower"
1643
1700
  ).exists()
1644
1701
  )
1645
1702
  self.assertFalse(
1646
1703
  Module.objects.filter(
1647
1704
  path="/ocpp/rfid/",
1648
- node_role__name="Constellation",
1705
+ node_role__name="Watchtower",
1649
1706
  is_deleted=False,
1650
1707
  ).exists()
1651
1708
  )
@@ -1657,9 +1714,16 @@ class ConstellationNavTests(TestCase):
1657
1714
  landing_labels = [landing.label for landing in ocpp_module.enabled_landings]
1658
1715
  self.assertIn("RFID Tag Validator", landing_labels)
1659
1716
 
1717
+ @override_settings(ALLOWED_HOSTS=["testserver", "arthexis.com"])
1718
+ def test_cookbooks_pill_visible_for_arthexis(self):
1719
+ resp = self.client.get(
1720
+ reverse("pages:index"), HTTP_HOST="arthexis.com"
1721
+ )
1722
+ self.assertContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
1723
+
1660
1724
  def test_ocpp_dashboard_visible(self):
1661
1725
  resp = self.client.get(reverse("pages:index"))
1662
- self.assertContains(resp, 'href="/ocpp/"')
1726
+ self.assertContains(resp, 'href="/ocpp/cpms/dashboard/"')
1663
1727
 
1664
1728
 
1665
1729
  class ReleaseModuleNavTests(TestCase):
@@ -1801,7 +1865,7 @@ class ControlNavTests(TestCase):
1801
1865
  self.client.force_login(user)
1802
1866
  resp = self.client.get(reverse("pages:index"))
1803
1867
  self.assertEqual(resp.status_code, 200)
1804
- self.assertContains(resp, 'href="/ocpp/"')
1868
+ self.assertContains(resp, 'href="/ocpp/cpms/dashboard/"')
1805
1869
  self.assertContains(
1806
1870
  resp, 'badge rounded-pill text-bg-secondary">CHARGERS'
1807
1871
  )
@@ -1833,10 +1897,76 @@ class ControlNavTests(TestCase):
1833
1897
  self.assertFalse(resp.context["header_references"])
1834
1898
  self.assertNotContains(resp, "https://example.com/hidden")
1835
1899
 
1900
+ def test_header_link_hidden_when_only_site_matches(self):
1901
+ terminal_role, _ = NodeRole.objects.get_or_create(name="Terminal")
1902
+ site = Site.objects.get(domain="testserver")
1903
+ reference = Reference.objects.create(
1904
+ alt_text="Restricted",
1905
+ value="https://example.com/restricted",
1906
+ show_in_header=True,
1907
+ )
1908
+ reference.roles.add(terminal_role)
1909
+ reference.sites.add(site)
1910
+
1911
+ resp = self.client.get(reverse("pages:index"))
1912
+
1913
+ self.assertIn("header_references", resp.context)
1914
+ self.assertFalse(resp.context["header_references"])
1915
+ self.assertNotContains(resp, "https://example.com/restricted")
1916
+
1836
1917
  def test_readme_pill_visible(self):
1837
1918
  resp = self.client.get(reverse("pages:readme"))
1838
- self.assertContains(resp, 'href="/readme/"')
1839
- self.assertContains(resp, 'badge rounded-pill text-bg-secondary">README')
1919
+ self.assertContains(resp, 'href="/read/"')
1920
+ self.assertContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
1921
+
1922
+ def test_cookbook_pill_has_no_dropdown(self):
1923
+ module = Module.objects.get(node_role__name="Control", path="/read/")
1924
+ Landing.objects.create(
1925
+ module=module,
1926
+ path="/man/",
1927
+ label="Manuals",
1928
+ enabled=True,
1929
+ )
1930
+
1931
+ resp = self.client.get(reverse("pages:readme"))
1932
+
1933
+ self.assertContains(
1934
+ resp,
1935
+ '<a class="nav-link" href="/read/"><span class="badge rounded-pill text-bg-secondary">COOKBOOKS</span></a>',
1936
+ html=True,
1937
+ )
1938
+ self.assertNotContains(resp, 'dropdown-item" href="/man/"')
1939
+
1940
+ def test_readme_page_includes_qr_share(self):
1941
+ resp = self.client.get(reverse("pages:readme"), {"section": "intro"})
1942
+ self.assertContains(resp, 'id="reader-qr"')
1943
+ self.assertContains(
1944
+ resp,
1945
+ 'data-url="http://testserver/read/?section=intro"',
1946
+ )
1947
+ self.assertNotContains(resp, "Scan this page")
1948
+ self.assertNotContains(
1949
+ resp, 'class="small text-break text-muted mt-3 mb-0"'
1950
+ )
1951
+
1952
+ def test_readme_document_by_name(self):
1953
+ resp = self.client.get(reverse("pages:readme-document", args=["AGENTS.md"]))
1954
+ self.assertEqual(resp.status_code, 200)
1955
+ self.assertContains(resp, "Agent Guidelines")
1956
+
1957
+ def test_readme_document_by_relative_path(self):
1958
+ resp = self.client.get(
1959
+ reverse(
1960
+ "pages:readme-document",
1961
+ args=["docs/development/maintenance-roadmap.md"],
1962
+ )
1963
+ )
1964
+ self.assertEqual(resp.status_code, 200)
1965
+ self.assertContains(resp, "Maintenance Improvement Proposals")
1966
+
1967
+ def test_readme_document_rejects_traversal(self):
1968
+ resp = self.client.get("/read/../../SECRET.md")
1969
+ self.assertEqual(resp.status_code, 404)
1840
1970
 
1841
1971
 
1842
1972
  class SatelliteNavTests(TestCase):
@@ -1908,8 +2038,8 @@ class SatelliteNavTests(TestCase):
1908
2038
 
1909
2039
  def test_readme_pill_visible(self):
1910
2040
  resp = self.client.get(reverse("pages:readme"))
1911
- self.assertContains(resp, 'href="/readme/"')
1912
- self.assertContains(resp, 'badge rounded-pill text-bg-secondary">README')
2041
+ self.assertContains(resp, 'href="/read/"')
2042
+ self.assertContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
1913
2043
 
1914
2044
 
1915
2045
  class PowerNavTests(TestCase):
@@ -1944,9 +2074,9 @@ class PowerNavTests(TestCase):
1944
2074
  power_module = module
1945
2075
  break
1946
2076
  self.assertIsNotNone(power_module)
1947
- self.assertEqual(power_module.menu_label.upper(), "CALCULATE")
2077
+ self.assertEqual(power_module.menu_label.upper(), "CALCULATORS")
1948
2078
  landing_labels = {landing.label for landing in power_module.enabled_landings}
1949
- self.assertIn("AWG Calculator", landing_labels)
2079
+ self.assertIn("AWG Cable Calculator", landing_labels)
1950
2080
 
1951
2081
  def test_manual_pill_label(self):
1952
2082
  resp = self.client.get(reverse("pages:index"))
@@ -1970,9 +2100,26 @@ class PowerNavTests(TestCase):
1970
2100
  break
1971
2101
  self.assertIsNotNone(power_module)
1972
2102
  landing_labels = {landing.label for landing in power_module.enabled_landings}
1973
- self.assertIn("AWG Calculator", landing_labels)
2103
+ self.assertIn("AWG Cable Calculator", landing_labels)
1974
2104
  self.assertIn("Energy Tariff Calculator", landing_labels)
1975
2105
 
2106
+ def test_locked_landing_shows_lock_icon(self):
2107
+ resp = self.client.get(reverse("pages:index"))
2108
+ html = resp.content.decode()
2109
+ energy_index = html.find("Energy Tariff Calculator")
2110
+ self.assertGreaterEqual(energy_index, 0)
2111
+ icon_index = html.find("dropdown-lock-icon", energy_index, energy_index + 300)
2112
+ self.assertGreaterEqual(icon_index, 0)
2113
+
2114
+ def test_lock_icon_disappears_after_login(self):
2115
+ self.client.force_login(self.user)
2116
+ resp = self.client.get(reverse("pages:index"))
2117
+ html = resp.content.decode()
2118
+ energy_index = html.find("Energy Tariff Calculator")
2119
+ self.assertGreaterEqual(energy_index, 0)
2120
+ icon_index = html.find("dropdown-lock-icon", energy_index, energy_index + 300)
2121
+ self.assertEqual(icon_index, -1)
2122
+
1976
2123
 
1977
2124
  class StaffNavVisibilityTests(TestCase):
1978
2125
  def setUp(self):
@@ -1994,12 +2141,12 @@ class StaffNavVisibilityTests(TestCase):
1994
2141
  def test_nonstaff_pill_hidden(self):
1995
2142
  self.client.login(username="user", password="pw")
1996
2143
  resp = self.client.get(reverse("pages:index"))
1997
- self.assertContains(resp, 'href="/ocpp/"')
2144
+ self.assertContains(resp, 'href="/ocpp/cpms/dashboard/"')
1998
2145
 
1999
2146
  def test_staff_sees_pill(self):
2000
2147
  self.client.login(username="staff", password="pw")
2001
2148
  resp = self.client.get(reverse("pages:index"))
2002
- self.assertContains(resp, 'href="/ocpp/"')
2149
+ self.assertContains(resp, 'href="/ocpp/cpms/dashboard/"')
2003
2150
 
2004
2151
 
2005
2152
  class ModuleAdminReloadActionTests(TestCase):
@@ -2012,7 +2159,7 @@ class ModuleAdminReloadActionTests(TestCase):
2012
2159
  password="pw",
2013
2160
  )
2014
2161
  self.client.force_login(self.superuser)
2015
- self.role, _ = NodeRole.objects.get_or_create(name="Constellation")
2162
+ self.role, _ = NodeRole.objects.get_or_create(name="Watchtower")
2016
2163
  Application.objects.get_or_create(name="ocpp")
2017
2164
  Application.objects.get_or_create(name="awg")
2018
2165
  Site.objects.update_or_create(
@@ -2050,7 +2197,11 @@ class ModuleAdminReloadActionTests(TestCase):
2050
2197
  )
2051
2198
  self.assertSetEqual(
2052
2199
  charger_landings,
2053
- {"/ocpp/", "/ocpp/simulator/", "/ocpp/rfid/"},
2200
+ {
2201
+ "/ocpp/cpms/dashboard/",
2202
+ "/ocpp/evcs/simulator/",
2203
+ "/ocpp/rfid/validator/",
2204
+ },
2054
2205
  )
2055
2206
 
2056
2207
  calculator_landings = set(
@@ -2188,6 +2339,47 @@ class UserManualAdminFormTests(TestCase):
2188
2339
  self.assertEqual(form.cleaned_data["content_pdf"], self.manual.content_pdf)
2189
2340
 
2190
2341
 
2342
+ class UserManualModelTests(TestCase):
2343
+ def _build_manual(self, **overrides):
2344
+ defaults = {
2345
+ "slug": "manual-model-test",
2346
+ "title": "Manual Model",
2347
+ "description": "Manual description",
2348
+ "languages": "en",
2349
+ "content_html": "<p>Manual</p>",
2350
+ "content_pdf": base64.b64encode(b"initial").decode("ascii"),
2351
+ }
2352
+ defaults.update(overrides)
2353
+ return UserManual(**defaults)
2354
+
2355
+ def test_save_encodes_uploaded_file(self):
2356
+ upload = SimpleUploadedFile("manual.pdf", b"PDF data")
2357
+ manual = self._build_manual(slug="manual-upload", content_pdf=upload)
2358
+ manual.save()
2359
+ manual.refresh_from_db()
2360
+ self.assertEqual(
2361
+ manual.content_pdf,
2362
+ base64.b64encode(b"PDF data").decode("ascii"),
2363
+ )
2364
+
2365
+ def test_save_encodes_raw_bytes(self):
2366
+ manual = self._build_manual(slug="manual-bytes", content_pdf=b"PDF raw")
2367
+ manual.save()
2368
+ manual.refresh_from_db()
2369
+ self.assertEqual(
2370
+ manual.content_pdf,
2371
+ base64.b64encode(b"PDF raw").decode("ascii"),
2372
+ )
2373
+
2374
+ def test_save_strips_data_uri_prefix(self):
2375
+ encoded = base64.b64encode(b"PDF data").decode("ascii")
2376
+ data_uri = f"data:application/pdf;base64,{encoded}"
2377
+ manual = self._build_manual(slug="manual-data-uri", content_pdf=data_uri)
2378
+ manual.save()
2379
+ manual.refresh_from_db()
2380
+ self.assertEqual(manual.content_pdf, encoded)
2381
+
2382
+
2191
2383
  class LandingCreationTests(TestCase):
2192
2384
  def setUp(self):
2193
2385
  role, _ = NodeRole.objects.get_or_create(name="Terminal")
@@ -2209,12 +2401,12 @@ class LandingCreationTests(TestCase):
2209
2401
 
2210
2402
 
2211
2403
  class LandingFixtureTests(TestCase):
2212
- def test_constellation_fixture_loads_without_duplicates(self):
2404
+ def test_watchtower_fixture_loads_without_duplicates(self):
2213
2405
  from glob import glob
2214
2406
 
2215
- NodeRole.objects.get_or_create(name="Constellation")
2407
+ NodeRole.objects.get_or_create(name="Watchtower")
2216
2408
  fixtures = glob(
2217
- str(Path(settings.BASE_DIR, "pages", "fixtures", "constellation__*.json"))
2409
+ str(Path(settings.BASE_DIR, "pages", "fixtures", "watchtower__*.json"))
2218
2410
  )
2219
2411
  fixtures = sorted(
2220
2412
  fixtures,
@@ -2224,9 +2416,11 @@ class LandingFixtureTests(TestCase):
2224
2416
  )
2225
2417
  call_command("loaddata", *fixtures)
2226
2418
  call_command("loaddata", *fixtures)
2227
- module = Module.objects.get(path="/ocpp/", node_role__name="Constellation")
2419
+ module = Module.objects.get(path="/ocpp/", node_role__name="Watchtower")
2228
2420
  module.create_landings()
2229
- self.assertEqual(module.landings.filter(path="/ocpp/rfid/").count(), 1)
2421
+ self.assertEqual(
2422
+ module.landings.filter(path="/ocpp/rfid/validator/").count(), 1
2423
+ )
2230
2424
 
2231
2425
 
2232
2426
  class AllowedHostSubnetTests(TestCase):
@@ -2379,9 +2573,9 @@ class FaviconTests(TestCase):
2379
2573
  )
2380
2574
  self.assertContains(resp, b64)
2381
2575
 
2382
- def test_constellation_nodes_use_goldenrod_favicon(self):
2576
+ def test_watchtower_nodes_use_goldenrod_favicon(self):
2383
2577
  with override_settings(MEDIA_ROOT=self.tmpdir):
2384
- role, _ = NodeRole.objects.get_or_create(name="Constellation")
2578
+ role, _ = NodeRole.objects.get_or_create(name="Watchtower")
2385
2579
  Node.objects.update_or_create(
2386
2580
  mac_address=Node.get_current_mac(),
2387
2581
  defaults={
@@ -2396,7 +2590,7 @@ class FaviconTests(TestCase):
2396
2590
  resp = self.client.get(reverse("pages:index"))
2397
2591
  b64 = (
2398
2592
  Path(settings.BASE_DIR)
2399
- .joinpath("pages", "fixtures", "data", "favicon_constellation.txt")
2593
+ .joinpath("pages", "fixtures", "data", "favicon_watchtower.txt")
2400
2594
  .read_text()
2401
2595
  .strip()
2402
2596
  )
@@ -2547,20 +2741,41 @@ class FavoriteTests(TestCase):
2547
2741
  self.assertContains(resp, f'aria-label="{badge_label}"')
2548
2742
 
2549
2743
  def test_dashboard_shows_charge_point_availability_badge(self):
2550
- Charger.objects.create(charger_id="CP-001", last_status="Available")
2551
2744
  Charger.objects.create(
2552
2745
  charger_id="CP-001", connector_id=1, last_status="Available"
2553
2746
  )
2747
+ Charger.objects.create(charger_id="CP-002", last_status="Available")
2748
+ Charger.objects.create(
2749
+ charger_id="CP-003", connector_id=1, last_status="Unavailable"
2750
+ )
2751
+
2752
+ resp = self.client.get(reverse("admin:index"))
2753
+
2754
+ expected = "1 / 2"
2755
+ badge_label = gettext(
2756
+ "%(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."
2757
+ ) % {"available": 1, "total": 2, "missing": 1}
2758
+
2759
+ self.assertContains(resp, expected)
2760
+ self.assertContains(resp, 'class="charger-availability-badge"')
2761
+ self.assertContains(resp, f'title="{badge_label}"')
2762
+ self.assertContains(resp, f'aria-label="{badge_label}"')
2763
+
2764
+ def test_dashboard_charge_point_badge_ignores_aggregator(self):
2765
+ Charger.objects.create(charger_id="CP-AGG", last_status="Available")
2766
+ Charger.objects.create(
2767
+ charger_id="CP-AGG", connector_id=1, last_status="Available"
2768
+ )
2554
2769
  Charger.objects.create(
2555
- charger_id="CP-002", connector_id=1, last_status="Unavailable"
2770
+ charger_id="CP-AGG", connector_id=2, last_status="Available"
2556
2771
  )
2557
2772
 
2558
2773
  resp = self.client.get(reverse("admin:index"))
2559
2774
 
2560
- expected = "2 / 1"
2775
+ expected = "2 / 2"
2561
2776
  badge_label = gettext(
2562
- "%(total)s chargers reporting Available status, including %(with_cp)s with a CP number"
2563
- ) % {"total": 2, "with_cp": 1}
2777
+ "%(available)s chargers reporting Available status with a CP number."
2778
+ ) % {"available": 2}
2564
2779
 
2565
2780
  self.assertContains(resp, expected)
2566
2781
  self.assertContains(resp, 'class="charger-availability-badge"')
pages/urls.py CHANGED
@@ -6,7 +6,9 @@ app_name = "pages"
6
6
 
7
7
  urlpatterns = [
8
8
  path("", views.index, name="index"),
9
- path("readme/", views.readme, name="readme"),
9
+ path("read/<path:doc>/edit/", views.readme_edit, name="readme-edit"),
10
+ path("read/", views.readme, name="readme"),
11
+ path("read/<path:doc>", views.readme, name="readme-document"),
10
12
  path("sitemap.xml", views.sitemap, name="pages-sitemap"),
11
13
  path("release/", views.release_admin_redirect, name="release-admin"),
12
14
  path("client-report/", views.client_report, name="client-report"),