arthexis 0.1.14__py3-none-any.whl → 0.1.16__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.

pages/tests.py CHANGED
@@ -7,6 +7,7 @@ import django
7
7
  django.setup()
8
8
 
9
9
  from django.test import Client, RequestFactory, TestCase, SimpleTestCase, override_settings
10
+ from django.test.utils import CaptureQueriesContext
10
11
  from django.urls import reverse
11
12
  from django.templatetags.static import static
12
13
  from urllib.parse import quote
@@ -15,7 +16,10 @@ from django.contrib.sites.models import Site
15
16
  from django.contrib import admin
16
17
  from django.contrib.messages.storage.fallback import FallbackStorage
17
18
  from django.core.exceptions import DisallowedHost
19
+ from django.core.cache import cache
20
+ from django.db import connection
18
21
  import socket
22
+ from django.db import connection
19
23
  from pages.models import (
20
24
  Application,
21
25
  Landing,
@@ -41,6 +45,7 @@ from pages.screenshot_specs import (
41
45
  ScreenshotUnavailable,
42
46
  registry,
43
47
  )
48
+ from pages.context_processors import nav_links
44
49
  from django.apps import apps as django_apps
45
50
  from core import mailer
46
51
  from core.admin import ProfileAdminMixin
@@ -62,7 +67,7 @@ import shutil
62
67
  from io import StringIO
63
68
  from django.conf import settings
64
69
  from pathlib import Path
65
- from unittest.mock import MagicMock, Mock, patch
70
+ from unittest.mock import MagicMock, Mock, call, patch
66
71
  from types import SimpleNamespace
67
72
  from django.core.management import call_command
68
73
  import re
@@ -92,7 +97,7 @@ from nodes.models import (
92
97
  NodeFeature,
93
98
  NodeFeatureAssignment,
94
99
  )
95
-
100
+ from django.contrib.auth.models import AnonymousUser
96
101
 
97
102
  class LoginViewTests(TestCase):
98
103
  def setUp(self):
@@ -706,6 +711,32 @@ class ViewHistoryLoggingTests(TestCase):
706
711
  def setUp(self):
707
712
  self.client = Client()
708
713
  Site.objects.update_or_create(id=1, defaults={"name": "Terminal"})
714
+ self.addCleanup(self._reset_purge_task)
715
+
716
+ def _reset_purge_task(self):
717
+ from django_celery_beat.models import PeriodicTask
718
+
719
+ PeriodicTask.objects.filter(name="pages_purge_landing_leads").delete()
720
+
721
+ def _create_local_node(self):
722
+ node, _ = Node.objects.update_or_create(
723
+ mac_address=Node.get_current_mac(),
724
+ defaults={
725
+ "hostname": socket.gethostname(),
726
+ "address": "127.0.0.1",
727
+ "base_path": settings.BASE_DIR,
728
+ "port": 8000,
729
+ },
730
+ )
731
+ return node
732
+
733
+ def _enable_celery_feature(self):
734
+ node = self._create_local_node()
735
+ feature, _ = NodeFeature.objects.get_or_create(
736
+ slug="celery-queue", defaults={"display": "Celery Queue"}
737
+ )
738
+ NodeFeatureAssignment.objects.get_or_create(node=node, feature=feature)
739
+ return node
709
740
 
710
741
  def test_successful_visit_creates_entry(self):
711
742
  resp = self.client.get(reverse("pages:index"))
@@ -750,6 +781,8 @@ class ViewHistoryLoggingTests(TestCase):
750
781
  self.assertEqual(user.last_visit_ip_address, "203.0.113.5")
751
782
 
752
783
  def test_landing_visit_records_lead(self):
784
+ self._enable_celery_feature()
785
+
753
786
  role = NodeRole.objects.create(name="landing-role")
754
787
  application = Application.objects.create(
755
788
  name="landing-tests-app", description=""
@@ -774,6 +807,26 @@ class ViewHistoryLoggingTests(TestCase):
774
807
  self.assertEqual(lead.path, "/")
775
808
  self.assertEqual(lead.referer, "https://example.com/ref")
776
809
 
810
+ def test_landing_visit_does_not_record_lead_without_celery(self):
811
+ role = NodeRole.objects.create(name="no-celery-role")
812
+ application = Application.objects.create(
813
+ name="no-celery-app", description=""
814
+ )
815
+ module = Module.objects.create(
816
+ node_role=role,
817
+ application=application,
818
+ path="/",
819
+ menu="Landing",
820
+ )
821
+ landing = module.landings.get(path="/")
822
+ landing.label = "No Celery"
823
+ landing.save(update_fields=["label"])
824
+
825
+ resp = self.client.get(reverse("pages:index"))
826
+
827
+ self.assertEqual(resp.status_code, 200)
828
+ self.assertFalse(LandingLead.objects.exists())
829
+
777
830
  def test_disabled_landing_does_not_record_lead(self):
778
831
  role = NodeRole.objects.create(name="landing-role-disabled")
779
832
  application = Application.objects.create(
@@ -894,6 +947,93 @@ class ViewHistoryAdminTests(TestCase):
894
947
  self.assertContains(resp, static("core/vendor/chart.umd.min.js"))
895
948
 
896
949
 
950
+ class LandingLeadAdminTests(TestCase):
951
+ def setUp(self):
952
+ self.client = Client()
953
+ User = get_user_model()
954
+ self.admin = User.objects.create_superuser(
955
+ username="lead_admin", password="pwd", email="lead@example.com"
956
+ )
957
+ self.client.force_login(self.admin)
958
+ Site.objects.update_or_create(
959
+ id=1, defaults={"name": "test", "domain": "testserver"}
960
+ )
961
+ self.node, _ = Node.objects.update_or_create(
962
+ mac_address=Node.get_current_mac(),
963
+ defaults={
964
+ "hostname": socket.gethostname(),
965
+ "address": "127.0.0.1",
966
+ "base_path": settings.BASE_DIR,
967
+ "port": 8000,
968
+ },
969
+ )
970
+ self.node.features.clear()
971
+ self.addCleanup(self._reset_purge_task)
972
+
973
+ def _reset_purge_task(self):
974
+ from django_celery_beat.models import PeriodicTask
975
+
976
+ PeriodicTask.objects.filter(name="pages_purge_landing_leads").delete()
977
+
978
+ def test_changelist_warns_without_celery(self):
979
+ url = reverse("admin:pages_landinglead_changelist")
980
+ response = self.client.get(url)
981
+ self.assertContains(
982
+ response,
983
+ "Landing leads are not being recorded because Celery is not running on this node.",
984
+ )
985
+
986
+ def test_changelist_no_warning_with_celery(self):
987
+ feature, _ = NodeFeature.objects.get_or_create(
988
+ slug="celery-queue", defaults={"display": "Celery Queue"}
989
+ )
990
+ NodeFeatureAssignment.objects.get_or_create(node=self.node, feature=feature)
991
+ url = reverse("admin:pages_landinglead_changelist")
992
+ response = self.client.get(url)
993
+ self.assertNotContains(
994
+ response,
995
+ "Landing leads are not being recorded because Celery is not running on this node.",
996
+ )
997
+
998
+
999
+ class LandingLeadTaskTests(TestCase):
1000
+ def setUp(self):
1001
+ self.role = NodeRole.objects.create(name="lead-task-role")
1002
+ self.application = Application.objects.create(
1003
+ name="lead-task-app", description=""
1004
+ )
1005
+ self.module = Module.objects.create(
1006
+ node_role=self.role,
1007
+ application=self.application,
1008
+ path="/tasks",
1009
+ menu="Landing",
1010
+ )
1011
+ self.landing = Landing.objects.create(
1012
+ module=self.module,
1013
+ path="/tasks/",
1014
+ label="Tasks Landing",
1015
+ enabled=True,
1016
+ )
1017
+
1018
+ def test_purge_expired_landing_leads_removes_old_records(self):
1019
+ from pages.tasks import purge_expired_landing_leads
1020
+
1021
+ stale = LandingLead.objects.create(landing=self.landing, path="/tasks/")
1022
+ recent = LandingLead.objects.create(landing=self.landing, path="/tasks/")
1023
+ LandingLead.objects.filter(pk=stale.pk).update(
1024
+ created_on=timezone.now() - timedelta(days=31)
1025
+ )
1026
+ LandingLead.objects.filter(pk=recent.pk).update(
1027
+ created_on=timezone.now() - timedelta(days=5)
1028
+ )
1029
+
1030
+ deleted = purge_expired_landing_leads()
1031
+
1032
+ self.assertEqual(deleted, 1)
1033
+ self.assertFalse(LandingLead.objects.filter(pk=stale.pk).exists())
1034
+ self.assertTrue(LandingLead.objects.filter(pk=recent.pk).exists())
1035
+
1036
+
897
1037
  class LogViewerAdminTests(SimpleTestCase):
898
1038
  def setUp(self):
899
1039
  self.factory = RequestFactory()
@@ -1092,6 +1232,45 @@ class SiteAdminScreenshotTests(TestCase):
1092
1232
  mock_capture.assert_called_once_with("http://testserver/")
1093
1233
 
1094
1234
 
1235
+ class SiteAdminReloadFixturesTests(TestCase):
1236
+ def setUp(self):
1237
+ self.client = Client()
1238
+ User = get_user_model()
1239
+ self.admin = User.objects.create_superuser(
1240
+ username="fixture-admin", password="pwd", email="admin@example.com"
1241
+ )
1242
+ self.client.force_login(self.admin)
1243
+ Site.objects.update_or_create(
1244
+ id=1, defaults={"name": "Terminal", "domain": "testserver"}
1245
+ )
1246
+
1247
+ @patch("pages.admin.call_command")
1248
+ def test_reload_site_fixtures_action(self, mock_call_command):
1249
+ response = self.client.post(
1250
+ reverse("admin:pages_siteproxy_changelist"),
1251
+ {"action": "reload_site_fixtures", "_selected_action": [1]},
1252
+ follow=True,
1253
+ )
1254
+ self.assertEqual(response.status_code, 200)
1255
+
1256
+ fixtures_dir = Path(settings.BASE_DIR) / "core" / "fixtures"
1257
+ expected = sorted(fixtures_dir.glob("references__00_site_*.json"))
1258
+ sigil_fixture = fixtures_dir / "sigil_roots__site.json"
1259
+ if sigil_fixture.exists():
1260
+ expected.append(sigil_fixture)
1261
+
1262
+ expected_calls = [
1263
+ call("loaddata", str(path), verbosity=0) for path in expected
1264
+ ]
1265
+ self.assertEqual(mock_call_command.call_args_list, expected_calls)
1266
+
1267
+ if expected_calls:
1268
+ self.assertContains(
1269
+ response,
1270
+ f"Reloaded {len(expected_calls)} site fixtures.",
1271
+ )
1272
+
1273
+
1095
1274
  class AdminBadgesWebsiteTests(TestCase):
1096
1275
  def setUp(self):
1097
1276
  self.client = Client()
@@ -1342,6 +1521,74 @@ class ConstellationNavTests(TestCase):
1342
1521
  resp = self.client.get(reverse("pages:index"))
1343
1522
  self.assertContains(resp, 'href="/ocpp/"')
1344
1523
 
1524
+
1525
+ class ReleaseModuleNavTests(TestCase):
1526
+ def setUp(self):
1527
+ self.client = Client()
1528
+ self.user_model = get_user_model()
1529
+ role, _ = NodeRole.objects.get_or_create(name="Terminal")
1530
+ Node.objects.update_or_create(
1531
+ mac_address=Node.get_current_mac(),
1532
+ defaults={
1533
+ "hostname": "localhost",
1534
+ "address": "127.0.0.1",
1535
+ "role": role,
1536
+ },
1537
+ )
1538
+ Site.objects.update_or_create(
1539
+ id=1, defaults={"domain": "testserver", "name": "Terminal"}
1540
+ )
1541
+ application, _ = Application.objects.get_or_create(name="core")
1542
+ module, _ = Module.objects.get_or_create(
1543
+ node_role=role,
1544
+ application=application,
1545
+ path="/release/",
1546
+ defaults={"menu": "Release", "is_default": False},
1547
+ )
1548
+ module_updates = []
1549
+ if module.menu != "Release":
1550
+ module.menu = "Release"
1551
+ module_updates.append("menu")
1552
+ if getattr(module, "is_deleted", False):
1553
+ module.is_deleted = False
1554
+ module_updates.append("is_deleted")
1555
+ if module_updates:
1556
+ module.save(update_fields=module_updates)
1557
+ Landing.objects.update_or_create(
1558
+ module=module,
1559
+ path="/release/",
1560
+ defaults={
1561
+ "label": "Package Releases",
1562
+ "enabled": True,
1563
+ "description": "",
1564
+ },
1565
+ )
1566
+ self.release_group, _ = SecurityGroup.objects.get_or_create(
1567
+ name="Release Managers"
1568
+ )
1569
+
1570
+ def test_release_module_hidden_for_anonymous(self):
1571
+ response = self.client.get(reverse("pages:index"))
1572
+ self.assertNotContains(response, 'badge rounded-pill text-bg-secondary">RELEASE')
1573
+
1574
+ def test_release_module_visible_to_release_manager(self):
1575
+ user = self.user_model.objects.create_user(
1576
+ "release-admin", password="test", is_staff=True
1577
+ )
1578
+ user.groups.add(self.release_group)
1579
+ self.client.force_login(user)
1580
+ response = self.client.get(reverse("pages:index"))
1581
+ self.assertContains(response, 'badge rounded-pill text-bg-secondary">RELEASE')
1582
+
1583
+ def test_release_module_hidden_for_non_member_staff(self):
1584
+ user = self.user_model.objects.create_user(
1585
+ "staff-user", password="test", is_staff=True
1586
+ )
1587
+ self.client.force_login(user)
1588
+ response = self.client.get(reverse("pages:index"))
1589
+ self.assertNotContains(response, 'badge rounded-pill text-bg-secondary">RELEASE')
1590
+
1591
+
1345
1592
  class ControlNavTests(TestCase):
1346
1593
  def setUp(self):
1347
1594
  self.client = Client()
@@ -1358,6 +1605,12 @@ class ControlNavTests(TestCase):
1358
1605
  id=1, defaults={"domain": "testserver", "name": ""}
1359
1606
  )
1360
1607
  fixtures = [
1608
+ Path(
1609
+ settings.BASE_DIR,
1610
+ "pages",
1611
+ "fixtures",
1612
+ "default__application_pages.json",
1613
+ ),
1361
1614
  Path(
1362
1615
  settings.BASE_DIR,
1363
1616
  "pages",
@@ -1388,6 +1641,18 @@ class ControlNavTests(TestCase):
1388
1641
  "fixtures",
1389
1642
  "control__landing_ocpp_rfid.json",
1390
1643
  ),
1644
+ Path(
1645
+ settings.BASE_DIR,
1646
+ "pages",
1647
+ "fixtures",
1648
+ "control__module_readme.json",
1649
+ ),
1650
+ Path(
1651
+ settings.BASE_DIR,
1652
+ "pages",
1653
+ "fixtures",
1654
+ "control__landing_readme.json",
1655
+ ),
1391
1656
  ]
1392
1657
  call_command("loaddata", *map(str, fixtures))
1393
1658
 
@@ -1428,6 +1693,84 @@ class ControlNavTests(TestCase):
1428
1693
  self.assertFalse(resp.context["header_references"])
1429
1694
  self.assertNotContains(resp, "https://example.com/hidden")
1430
1695
 
1696
+ def test_readme_pill_visible(self):
1697
+ resp = self.client.get(reverse("pages:readme"))
1698
+ self.assertContains(resp, 'href="/readme/"')
1699
+ self.assertContains(resp, 'badge rounded-pill text-bg-secondary">README')
1700
+
1701
+
1702
+ class SatelliteNavTests(TestCase):
1703
+ def setUp(self):
1704
+ self.client = Client()
1705
+ role, _ = NodeRole.objects.get_or_create(name="Satellite")
1706
+ Node.objects.update_or_create(
1707
+ mac_address=Node.get_current_mac(),
1708
+ defaults={
1709
+ "hostname": "localhost",
1710
+ "address": "127.0.0.1",
1711
+ "role": role,
1712
+ },
1713
+ )
1714
+ Site.objects.update_or_create(
1715
+ id=1, defaults={"domain": "testserver", "name": ""}
1716
+ )
1717
+ fixtures = [
1718
+ Path(
1719
+ settings.BASE_DIR,
1720
+ "pages",
1721
+ "fixtures",
1722
+ "default__application_pages.json",
1723
+ ),
1724
+ Path(
1725
+ settings.BASE_DIR,
1726
+ "pages",
1727
+ "fixtures",
1728
+ "satellite_box__application_ocpp.json",
1729
+ ),
1730
+ Path(
1731
+ settings.BASE_DIR,
1732
+ "pages",
1733
+ "fixtures",
1734
+ "satellite_box__module_ocpp.json",
1735
+ ),
1736
+ Path(
1737
+ settings.BASE_DIR,
1738
+ "pages",
1739
+ "fixtures",
1740
+ "satellite_box__landing_ocpp_dashboard.json",
1741
+ ),
1742
+ Path(
1743
+ settings.BASE_DIR,
1744
+ "pages",
1745
+ "fixtures",
1746
+ "satellite_box__landing_ocpp_cp_simulator.json",
1747
+ ),
1748
+ Path(
1749
+ settings.BASE_DIR,
1750
+ "pages",
1751
+ "fixtures",
1752
+ "satellite_box__landing_ocpp_rfid.json",
1753
+ ),
1754
+ Path(
1755
+ settings.BASE_DIR,
1756
+ "pages",
1757
+ "fixtures",
1758
+ "satellite_box__module_readme.json",
1759
+ ),
1760
+ Path(
1761
+ settings.BASE_DIR,
1762
+ "pages",
1763
+ "fixtures",
1764
+ "satellite_box__landing_readme.json",
1765
+ ),
1766
+ ]
1767
+ call_command("loaddata", *map(str, fixtures))
1768
+
1769
+ def test_readme_pill_visible(self):
1770
+ resp = self.client.get(reverse("pages:readme"))
1771
+ self.assertContains(resp, 'href="/readme/"')
1772
+ self.assertContains(resp, 'badge rounded-pill text-bg-secondary">README')
1773
+
1431
1774
 
1432
1775
  class PowerNavTests(TestCase):
1433
1776
  def setUp(self):
@@ -1519,6 +1862,83 @@ class StaffNavVisibilityTests(TestCase):
1519
1862
  self.assertContains(resp, 'href="/ocpp/"')
1520
1863
 
1521
1864
 
1865
+ class ModuleAdminReloadActionTests(TestCase):
1866
+ def setUp(self):
1867
+ self.client = Client()
1868
+ User = get_user_model()
1869
+ self.superuser = User.objects.create_superuser(
1870
+ username="admin",
1871
+ email="admin@example.com",
1872
+ password="pw",
1873
+ )
1874
+ self.client.force_login(self.superuser)
1875
+ self.role, _ = NodeRole.objects.get_or_create(name="Constellation")
1876
+ Application.objects.get_or_create(name="ocpp")
1877
+ Application.objects.get_or_create(name="awg")
1878
+ Site.objects.update_or_create(
1879
+ id=1, defaults={"domain": "testserver", "name": ""}
1880
+ )
1881
+
1882
+ def _post_reload(self):
1883
+ changelist_url = reverse("admin:pages_module_changelist")
1884
+ self.client.get(changelist_url)
1885
+ csrf_cookie = self.client.cookies.get("csrftoken")
1886
+ token = csrf_cookie.value if csrf_cookie else ""
1887
+ return self.client.post(
1888
+ reverse("admin:pages_module_reload_default_modules"),
1889
+ {"csrfmiddlewaretoken": token},
1890
+ follow=True,
1891
+ )
1892
+
1893
+ def test_reload_restores_missing_modules_and_landings(self):
1894
+ Module.objects.filter(node_role=self.role).delete()
1895
+ Landing.objects.filter(module__node_role=self.role).delete()
1896
+
1897
+ response = self._post_reload()
1898
+ self.assertEqual(response.status_code, 200)
1899
+
1900
+ chargers = Module.objects.get(node_role=self.role, path="/ocpp/")
1901
+ calculators = Module.objects.get(node_role=self.role, path="/awg/")
1902
+
1903
+ self.assertEqual(chargers.menu, "Chargers")
1904
+ self.assertEqual(calculators.menu, "")
1905
+ self.assertFalse(getattr(chargers, "is_deleted", False))
1906
+ self.assertFalse(getattr(calculators, "is_deleted", False))
1907
+
1908
+ charger_landings = set(
1909
+ Landing.objects.filter(module=chargers).values_list("path", flat=True)
1910
+ )
1911
+ self.assertSetEqual(
1912
+ charger_landings,
1913
+ {"/ocpp/", "/ocpp/simulator/", "/ocpp/rfid/"},
1914
+ )
1915
+
1916
+ calculator_landings = set(
1917
+ Landing.objects.filter(module=calculators).values_list(
1918
+ "path", flat=True
1919
+ )
1920
+ )
1921
+ self.assertSetEqual(
1922
+ calculator_landings,
1923
+ {"/awg/", "/awg/energy-tariff/"},
1924
+ )
1925
+
1926
+ def test_reload_is_idempotent(self):
1927
+ self._post_reload()
1928
+ module_count = Module.objects.filter(node_role=self.role).count()
1929
+ landing_count = Landing.objects.filter(module__node_role=self.role).count()
1930
+
1931
+ self._post_reload()
1932
+
1933
+ self.assertEqual(
1934
+ Module.objects.filter(node_role=self.role).count(), module_count
1935
+ )
1936
+ self.assertEqual(
1937
+ Landing.objects.filter(module__node_role=self.role).count(),
1938
+ landing_count,
1939
+ )
1940
+
1941
+
1522
1942
  class ApplicationModelTests(TestCase):
1523
1943
  def test_path_defaults_to_slugified_name(self):
1524
1944
  role, _ = NodeRole.objects.get_or_create(name="Terminal")
@@ -2069,6 +2489,40 @@ class FavoriteTests(TestCase):
2069
2489
  self.assertNotIn("Packages", labels)
2070
2490
  ContentType.objects.clear_cache()
2071
2491
 
2492
+ def test_future_action_items_limits_user_data_queries(self):
2493
+ from pages.templatetags import admin_extras
2494
+
2495
+ cache.delete(admin_extras.USER_DATA_MODELS_CACHE_KEY)
2496
+ self.addCleanup(cache.delete, admin_extras.USER_DATA_MODELS_CACHE_KEY)
2497
+
2498
+ for index in range(3):
2499
+ NodeRole.objects.create(name=f"CachedRole{index}", is_user_data=True)
2500
+ for index in range(2):
2501
+ NodeFeature.objects.create(
2502
+ slug=f"cached-feature-{index}",
2503
+ display=f"Feature {index}",
2504
+ is_user_data=True,
2505
+ )
2506
+
2507
+ Node.objects.create(
2508
+ hostname="cached-node",
2509
+ address="127.0.0.1",
2510
+ mac_address="AA:BB:CC:DD:EE:FF",
2511
+ port=8000,
2512
+ is_user_data=True,
2513
+ )
2514
+
2515
+ response = self.client.get(reverse("admin:index"))
2516
+ request = response.wsgi_request
2517
+
2518
+ admin_extras.future_action_items({"request": request})
2519
+ with CaptureQueriesContext(connection) as ctx:
2520
+ admin_extras.future_action_items({"request": request})
2521
+
2522
+ # History and favorites queries should remain bounded regardless of the
2523
+ # number of Entity subclasses with user data.
2524
+ self.assertLessEqual(len(ctx.captured_queries), 4)
2525
+
2072
2526
  def test_favorite_ct_id_recreates_missing_content_type(self):
2073
2527
  ct = ContentType.objects.get_by_natural_key("pages", "application")
2074
2528
  ct.delete()
@@ -2114,6 +2568,24 @@ class FavoriteTests(TestCase):
2114
2568
  resp, '<div class="todo-details">More info</div>', html=True
2115
2569
  )
2116
2570
 
2571
+ def test_dashboard_shows_todos_when_node_unknown(self):
2572
+ Todo.objects.create(request="Check fallback")
2573
+ from nodes.models import Node
2574
+
2575
+ Node.objects.all().delete()
2576
+
2577
+ resp = self.client.get(reverse("admin:index"))
2578
+ self.assertContains(resp, "Release manager tasks")
2579
+ self.assertContains(resp, "Check fallback")
2580
+
2581
+ def test_dashboard_shows_todos_without_release_manager_profile(self):
2582
+ Todo.objects.create(request="Unrestricted task")
2583
+ ReleaseManager.objects.filter(user=self.user).delete()
2584
+
2585
+ resp = self.client.get(reverse("admin:index"))
2586
+ self.assertContains(resp, "Release manager tasks")
2587
+ self.assertContains(resp, "Unrestricted task")
2588
+
2117
2589
  def test_dashboard_excludes_todo_changelist_link(self):
2118
2590
  ct = ContentType.objects.get_for_model(Todo)
2119
2591
  Favorite.objects.create(user=self.user, content_type=ct)
@@ -2127,7 +2599,7 @@ class FavoriteTests(TestCase):
2127
2599
  changelist = reverse("admin:core_todo_changelist")
2128
2600
  self.assertNotContains(resp, f'href="{changelist}"')
2129
2601
 
2130
- def test_dashboard_hides_todos_without_release_manager(self):
2602
+ def test_dashboard_shows_todos_for_admin_without_release_manager(self):
2131
2603
  todo = Todo.objects.create(request="Only Release Manager")
2132
2604
  User = get_user_model()
2133
2605
  other_user = User.objects.create_superuser(
@@ -2135,10 +2607,10 @@ class FavoriteTests(TestCase):
2135
2607
  )
2136
2608
  self.client.force_login(other_user)
2137
2609
  resp = self.client.get(reverse("admin:index"))
2138
- self.assertNotContains(resp, "Release manager tasks")
2139
- self.assertNotContains(resp, todo.request)
2610
+ self.assertContains(resp, "Release manager tasks")
2611
+ self.assertContains(resp, todo.request)
2140
2612
 
2141
- def test_dashboard_hides_todos_for_non_terminal_node(self):
2613
+ def test_dashboard_shows_todos_for_non_terminal_node(self):
2142
2614
  todo = Todo.objects.create(request="Terminal Tasks")
2143
2615
  from nodes.models import NodeRole
2144
2616
 
@@ -2146,8 +2618,8 @@ class FavoriteTests(TestCase):
2146
2618
  self.node.role = control_role
2147
2619
  self.node.save(update_fields=["role"])
2148
2620
  resp = self.client.get(reverse("admin:index"))
2149
- self.assertNotContains(resp, "Release manager tasks")
2150
- self.assertNotContains(resp, todo.request)
2621
+ self.assertContains(resp, "Release manager tasks")
2622
+ self.assertContains(resp, todo.request)
2151
2623
 
2152
2624
  def test_dashboard_shows_todos_for_delegate_release_manager(self):
2153
2625
  todo = Todo.objects.create(request="Delegate Task")
@@ -2172,6 +2644,62 @@ class FavoriteTests(TestCase):
2172
2644
  self.assertContains(resp, todo.request)
2173
2645
 
2174
2646
 
2647
+ class AdminIndexQueryRegressionTests(TestCase):
2648
+ def setUp(self):
2649
+ User = get_user_model()
2650
+ self.client = Client()
2651
+ self.user = User.objects.create_superuser(
2652
+ username="queryadmin", password="pwd", email="query@example.com"
2653
+ )
2654
+ self.client.force_login(self.user)
2655
+ Site.objects.update_or_create(
2656
+ id=1, defaults={"name": "test", "domain": "testserver"}
2657
+ )
2658
+ favorite_cts = [
2659
+ ContentType.objects.get_for_model(Application),
2660
+ ContentType.objects.get_for_model(Landing),
2661
+ ]
2662
+ for ct in favorite_cts:
2663
+ Favorite.objects.create(user=self.user, content_type=ct)
2664
+
2665
+ def _render_admin_and_count_queries(self):
2666
+ url = reverse("admin:index")
2667
+ with CaptureQueriesContext(connection) as ctx:
2668
+ response = self.client.get(url)
2669
+ self.assertEqual(response.status_code, 200)
2670
+ sql_statements = [query["sql"].lower() for query in ctx.captured_queries]
2671
+ ct_queries = [sql for sql in sql_statements if '"django_content_type"' in sql]
2672
+ favorite_queries = [sql for sql in sql_statements if '"pages_favorite"' in sql]
2673
+ return len(ct_queries), len(favorite_queries)
2674
+
2675
+ def test_admin_index_queries_constant_with_more_models(self):
2676
+ site = admin.site
2677
+ original_registry = site._registry.copy()
2678
+ registry_items = list(original_registry.items())
2679
+ if len(registry_items) < 2:
2680
+ self.skipTest("Not enough registered admin models for regression test")
2681
+
2682
+ for model in original_registry.keys():
2683
+ ContentType.objects.get_for_model(model)
2684
+
2685
+ try:
2686
+ site._registry = dict(registry_items[:1])
2687
+ baseline_ct_queries, baseline_favorite_queries = (
2688
+ self._render_admin_and_count_queries()
2689
+ )
2690
+
2691
+ expanded_limit = min(len(registry_items), 5)
2692
+ site._registry = dict(registry_items[:expanded_limit])
2693
+ expanded_ct_queries, expanded_favorite_queries = (
2694
+ self._render_admin_and_count_queries()
2695
+ )
2696
+ finally:
2697
+ site._registry = original_registry
2698
+
2699
+ self.assertEqual(expanded_ct_queries, baseline_ct_queries)
2700
+ self.assertEqual(expanded_favorite_queries, baseline_favorite_queries)
2701
+
2702
+
2175
2703
  class AdminActionListTests(TestCase):
2176
2704
  def setUp(self):
2177
2705
  User = get_user_model()
@@ -2352,6 +2880,7 @@ class DatasetteTests(TestCase):
2352
2880
 
2353
2881
  class UserStorySubmissionTests(TestCase):
2354
2882
  def setUp(self):
2883
+ cache.clear()
2355
2884
  self.client = Client()
2356
2885
  self.url = reverse("pages:user-story-submit")
2357
2886
  User = get_user_model()
@@ -2367,6 +2896,8 @@ class UserStorySubmissionTests(TestCase):
2367
2896
  "path": "/wizard/step-1/",
2368
2897
  "take_screenshot": "1",
2369
2898
  },
2899
+ HTTP_REFERER="https://example.test/wizard/step-1/",
2900
+ HTTP_USER_AGENT="FeedbackBot/1.0",
2370
2901
  )
2371
2902
  self.assertEqual(response.status_code, 200)
2372
2903
  self.assertEqual(response.json(), {"success": True})
@@ -2378,6 +2909,10 @@ class UserStorySubmissionTests(TestCase):
2378
2909
  self.assertEqual(story.owner, self.user)
2379
2910
  self.assertTrue(story.is_user_data)
2380
2911
  self.assertTrue(story.take_screenshot)
2912
+ self.assertEqual(story.status, UserStory.Status.OPEN)
2913
+ self.assertEqual(story.referer, "https://example.test/wizard/step-1/")
2914
+ self.assertEqual(story.user_agent, "FeedbackBot/1.0")
2915
+ self.assertEqual(story.ip_address, "127.0.0.1")
2381
2916
 
2382
2917
  def test_anonymous_submission_uses_provided_name(self):
2383
2918
  response = self.client.post(
@@ -2398,6 +2933,7 @@ class UserStorySubmissionTests(TestCase):
2398
2933
  self.assertIsNone(story.owner)
2399
2934
  self.assertEqual(story.comments, "It was fine.")
2400
2935
  self.assertTrue(story.take_screenshot)
2936
+ self.assertEqual(story.status, UserStory.Status.OPEN)
2401
2937
 
2402
2938
  def test_invalid_rating_returns_errors(self):
2403
2939
  response = self.client.post(
@@ -2430,6 +2966,7 @@ class UserStorySubmissionTests(TestCase):
2430
2966
  self.assertIsNone(story.user)
2431
2967
  self.assertIsNone(story.owner)
2432
2968
  self.assertTrue(story.take_screenshot)
2969
+ self.assertEqual(story.status, UserStory.Status.OPEN)
2433
2970
 
2434
2971
  def test_submission_without_screenshot_request(self):
2435
2972
  response = self.client.post(
@@ -2445,6 +2982,90 @@ class UserStorySubmissionTests(TestCase):
2445
2982
  self.assertFalse(story.take_screenshot)
2446
2983
  self.assertIsNone(story.owner)
2447
2984
 
2985
+ def test_rate_limit_blocks_repeated_submissions(self):
2986
+ payload = {
2987
+ "rating": 4,
2988
+ "comments": "Pretty good",
2989
+ "path": "/feedback/",
2990
+ "take_screenshot": "1",
2991
+ }
2992
+ first = self.client.post(self.url, payload)
2993
+ self.assertEqual(first.status_code, 200)
2994
+ second = self.client.post(self.url, payload)
2995
+ self.assertEqual(second.status_code, 429)
2996
+ data = second.json()
2997
+ self.assertFalse(data["success"])
2998
+ self.assertIn("__all__", data.get("errors", {}))
2999
+ self.assertIn("5", data["errors"]["__all__"][0])
3000
+
3001
+
3002
+ class UserStoryIssueAutomationTests(TestCase):
3003
+ def setUp(self):
3004
+ self.lock_dir = Path(settings.BASE_DIR) / "locks"
3005
+ self.lock_dir.mkdir(parents=True, exist_ok=True)
3006
+ self.lock_file = self.lock_dir / "celery.lck"
3007
+ User = get_user_model()
3008
+ self.user = User.objects.create_user(
3009
+ username="feedback_user", password="pwd"
3010
+ )
3011
+
3012
+ def tearDown(self):
3013
+ self.lock_file.unlink(missing_ok=True)
3014
+
3015
+ def test_low_rating_story_enqueues_issue_creation_when_celery_enabled(self):
3016
+ self.lock_file.write_text("")
3017
+
3018
+ with patch("pages.models.create_user_story_github_issue.delay") as mock_delay:
3019
+ story = UserStory.objects.create(
3020
+ path="/feedback/",
3021
+ rating=2,
3022
+ comments="Needs work",
3023
+ take_screenshot=False,
3024
+ user=self.user,
3025
+ )
3026
+
3027
+ mock_delay.assert_called_once_with(story.pk)
3028
+
3029
+ def test_low_rating_story_without_user_does_not_enqueue_issue(self):
3030
+ self.lock_file.write_text("")
3031
+
3032
+ with patch("pages.models.create_user_story_github_issue.delay") as mock_delay:
3033
+ UserStory.objects.create(
3034
+ path="/feedback/",
3035
+ rating=2,
3036
+ comments="Needs work",
3037
+ take_screenshot=False,
3038
+ )
3039
+
3040
+ mock_delay.assert_not_called()
3041
+
3042
+ def test_five_star_story_does_not_enqueue_issue(self):
3043
+ self.lock_file.write_text("")
3044
+
3045
+ with patch("pages.models.create_user_story_github_issue.delay") as mock_delay:
3046
+ UserStory.objects.create(
3047
+ path="/feedback/",
3048
+ rating=5,
3049
+ comments="Great!",
3050
+ take_screenshot=True,
3051
+ )
3052
+
3053
+ mock_delay.assert_not_called()
3054
+
3055
+ def test_low_rating_story_skips_when_celery_disabled(self):
3056
+ self.lock_file.unlink(missing_ok=True)
3057
+
3058
+ with patch("pages.models.create_user_story_github_issue.delay") as mock_delay:
3059
+ UserStory.objects.create(
3060
+ path="/feedback/",
3061
+ rating=1,
3062
+ comments="Not good",
3063
+ take_screenshot=False,
3064
+ user=self.user,
3065
+ )
3066
+
3067
+ mock_delay.assert_not_called()
3068
+
2448
3069
 
2449
3070
  class UserStoryAdminActionTests(TestCase):
2450
3071
  def setUp(self):