arthexis 0.1.16__py3-none-any.whl → 0.1.28__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.16.dist-info → arthexis-0.1.28.dist-info}/METADATA +95 -41
- arthexis-0.1.28.dist-info/RECORD +112 -0
- config/asgi.py +1 -15
- config/middleware.py +47 -1
- config/settings.py +21 -30
- config/settings_helpers.py +176 -1
- config/urls.py +69 -1
- core/admin.py +805 -473
- core/apps.py +6 -8
- core/auto_upgrade.py +19 -4
- core/backends.py +13 -3
- core/celery_utils.py +73 -0
- core/changelog.py +66 -5
- core/environment.py +4 -5
- core/models.py +1825 -218
- core/notifications.py +1 -1
- core/reference_utils.py +10 -11
- core/release.py +55 -7
- core/sigil_builder.py +2 -2
- core/sigil_resolver.py +1 -66
- core/system.py +285 -4
- core/tasks.py +439 -138
- core/test_system_info.py +43 -5
- core/tests.py +516 -18
- core/user_data.py +94 -21
- core/views.py +348 -186
- nodes/admin.py +904 -67
- nodes/apps.py +12 -1
- nodes/feature_checks.py +30 -0
- nodes/models.py +800 -127
- nodes/rfid_sync.py +1 -1
- nodes/tasks.py +98 -3
- nodes/tests.py +1381 -152
- nodes/urls.py +15 -1
- nodes/utils.py +51 -3
- nodes/views.py +1382 -152
- ocpp/admin.py +1970 -152
- ocpp/consumers.py +839 -34
- ocpp/models.py +968 -17
- ocpp/network.py +398 -0
- ocpp/store.py +411 -43
- ocpp/tasks.py +261 -3
- ocpp/test_export_import.py +1 -0
- ocpp/test_rfid.py +194 -6
- ocpp/tests.py +1918 -87
- ocpp/transactions_io.py +9 -1
- ocpp/urls.py +8 -3
- ocpp/views.py +700 -53
- pages/admin.py +262 -30
- pages/apps.py +35 -0
- pages/context_processors.py +28 -21
- pages/defaults.py +1 -1
- pages/forms.py +31 -8
- pages/middleware.py +6 -2
- pages/models.py +86 -2
- pages/module_defaults.py +5 -5
- pages/site_config.py +137 -0
- pages/tests.py +1050 -126
- pages/urls.py +14 -2
- pages/utils.py +70 -0
- pages/views.py +622 -56
- arthexis-0.1.16.dist-info/RECORD +0 -111
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/WHEEL +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/top_level.txt +0 -0
pages/tests.py
CHANGED
|
@@ -3,23 +3,26 @@ 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
12
|
from django.urls import reverse
|
|
13
|
+
from django.shortcuts import resolve_url
|
|
12
14
|
from django.templatetags.static import static
|
|
13
15
|
from urllib.parse import quote
|
|
14
16
|
from django.contrib.auth import get_user_model
|
|
15
17
|
from django.contrib.sites.models import Site
|
|
16
|
-
from django.contrib import admin
|
|
18
|
+
from django.contrib import admin, messages
|
|
17
19
|
from django.contrib.messages.storage.fallback import FallbackStorage
|
|
18
20
|
from django.core.exceptions import DisallowedHost
|
|
19
21
|
from django.core.cache import cache
|
|
20
22
|
from django.db import connection
|
|
21
23
|
import socket
|
|
22
24
|
from django.db import connection
|
|
25
|
+
from pages import site_config
|
|
23
26
|
from pages.models import (
|
|
24
27
|
Application,
|
|
25
28
|
Landing,
|
|
@@ -32,6 +35,8 @@ from pages.models import (
|
|
|
32
35
|
UserManual,
|
|
33
36
|
UserStory,
|
|
34
37
|
)
|
|
38
|
+
from django.http import FileResponse
|
|
39
|
+
|
|
35
40
|
from pages.admin import (
|
|
36
41
|
ApplicationAdmin,
|
|
37
42
|
UserManualAdmin,
|
|
@@ -47,31 +52,40 @@ from pages.screenshot_specs import (
|
|
|
47
52
|
)
|
|
48
53
|
from pages.context_processors import nav_links
|
|
49
54
|
from django.apps import apps as django_apps
|
|
55
|
+
from config.middleware import SiteHttpsRedirectMiddleware
|
|
50
56
|
from core import mailer
|
|
51
57
|
from core.admin import ProfileAdminMixin
|
|
52
58
|
from core.models import (
|
|
53
59
|
AdminHistory,
|
|
60
|
+
ClientReport,
|
|
54
61
|
InviteLead,
|
|
55
62
|
Package,
|
|
56
63
|
Reference,
|
|
57
64
|
RFID,
|
|
58
65
|
ReleaseManager,
|
|
59
66
|
SecurityGroup,
|
|
67
|
+
GoogleCalendarProfile,
|
|
60
68
|
Todo,
|
|
61
69
|
TOTPDeviceSettings,
|
|
62
70
|
)
|
|
71
|
+
from ocpp.models import Charger
|
|
63
72
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
64
73
|
import base64
|
|
74
|
+
import json
|
|
65
75
|
import tempfile
|
|
66
76
|
import shutil
|
|
77
|
+
from datetime import timedelta
|
|
67
78
|
from io import StringIO
|
|
68
79
|
from django.conf import settings
|
|
80
|
+
from django.utils import timezone
|
|
81
|
+
from django.utils.html import escape
|
|
69
82
|
from pathlib import Path
|
|
70
83
|
from unittest.mock import MagicMock, Mock, call, patch
|
|
71
84
|
from types import SimpleNamespace
|
|
72
85
|
from django.core.management import call_command
|
|
73
86
|
import re
|
|
74
87
|
from django.contrib.contenttypes.models import ContentType
|
|
88
|
+
from django.http import HttpResponse
|
|
75
89
|
from datetime import (
|
|
76
90
|
date,
|
|
77
91
|
datetime,
|
|
@@ -82,6 +96,7 @@ from datetime import (
|
|
|
82
96
|
from django.core import mail
|
|
83
97
|
from django.utils import timezone
|
|
84
98
|
from django.utils.text import slugify
|
|
99
|
+
from django.utils import translation
|
|
85
100
|
from django.utils.translation import gettext
|
|
86
101
|
from django_otp import DEVICE_ID_SESSION_KEY
|
|
87
102
|
from django_otp.oath import TOTP
|
|
@@ -96,6 +111,7 @@ from nodes.models import (
|
|
|
96
111
|
NodeRole,
|
|
97
112
|
NodeFeature,
|
|
98
113
|
NodeFeatureAssignment,
|
|
114
|
+
NetMessage,
|
|
99
115
|
)
|
|
100
116
|
from django.contrib.auth.models import AnonymousUser
|
|
101
117
|
|
|
@@ -109,9 +125,26 @@ class LoginViewTests(TestCase):
|
|
|
109
125
|
self.user = User.objects.create_user(username="user", password="pwd")
|
|
110
126
|
Site.objects.update_or_create(id=1, defaults={"name": "Terminal"})
|
|
111
127
|
|
|
128
|
+
def _enable_rfid_scanner(self):
|
|
129
|
+
node, _ = Node.objects.get_or_create(
|
|
130
|
+
mac_address=Node.get_current_mac(),
|
|
131
|
+
defaults={"hostname": "local-node"},
|
|
132
|
+
)
|
|
133
|
+
feature, _ = NodeFeature.objects.get_or_create(
|
|
134
|
+
slug="rfid-scanner", defaults={"display": "RFID Scanner"}
|
|
135
|
+
)
|
|
136
|
+
NodeFeatureAssignment.objects.get_or_create(node=node, feature=feature)
|
|
137
|
+
return node
|
|
138
|
+
|
|
112
139
|
def test_login_link_in_navbar(self):
|
|
113
140
|
resp = self.client.get(reverse("pages:index"))
|
|
114
|
-
|
|
141
|
+
login_url = resolve_url(settings.LOGIN_URL)
|
|
142
|
+
self.assertContains(resp, f'href="{login_url}"')
|
|
143
|
+
|
|
144
|
+
@override_settings(LOGIN_URL="/staff/login/")
|
|
145
|
+
def test_login_link_uses_configured_login_url(self):
|
|
146
|
+
resp = self.client.get(reverse("pages:index"))
|
|
147
|
+
self.assertContains(resp, 'href="/staff/login/"')
|
|
115
148
|
|
|
116
149
|
def test_login_page_shows_authenticator_toggle(self):
|
|
117
150
|
resp = self.client.get(reverse("pages:login"))
|
|
@@ -187,6 +220,83 @@ class LoginViewTests(TestCase):
|
|
|
187
220
|
)
|
|
188
221
|
self.assertRedirects(resp, "/nodes/list/")
|
|
189
222
|
|
|
223
|
+
def test_login_page_shows_rfid_link_when_feature_enabled(self):
|
|
224
|
+
self._enable_rfid_scanner()
|
|
225
|
+
resp = self.client.get(reverse("pages:login"))
|
|
226
|
+
self.assertContains(resp, reverse("pages:rfid-login"))
|
|
227
|
+
|
|
228
|
+
def test_login_page_detects_rfid_lock_without_mac_address(self):
|
|
229
|
+
Node.objects.all().delete()
|
|
230
|
+
NodeFeature.objects.get_or_create(
|
|
231
|
+
slug="rfid-scanner", defaults={"display": "RFID Scanner"}
|
|
232
|
+
)
|
|
233
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
234
|
+
locks_dir = Path(tempdir) / "locks"
|
|
235
|
+
locks_dir.mkdir()
|
|
236
|
+
(locks_dir / "rfid.lck").touch()
|
|
237
|
+
Node.objects.create(
|
|
238
|
+
hostname="local-node",
|
|
239
|
+
base_path=tempdir,
|
|
240
|
+
current_relation=Node.Relation.SELF,
|
|
241
|
+
mac_address=None,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
resp = self.client.get(reverse("pages:login"))
|
|
245
|
+
|
|
246
|
+
self.assertContains(resp, reverse("pages:rfid-login"))
|
|
247
|
+
|
|
248
|
+
def test_rfid_login_page_requires_feature(self):
|
|
249
|
+
resp = self.client.get(reverse("pages:rfid-login"))
|
|
250
|
+
self.assertEqual(resp.status_code, 404)
|
|
251
|
+
|
|
252
|
+
def test_rfid_login_page_redirects_authenticated_user(self):
|
|
253
|
+
self._enable_rfid_scanner()
|
|
254
|
+
self.client.force_login(self.user)
|
|
255
|
+
resp = self.client.get(reverse("pages:rfid-login"))
|
|
256
|
+
self.assertRedirects(resp, "/")
|
|
257
|
+
|
|
258
|
+
def test_rfid_login_page_includes_scan_url(self):
|
|
259
|
+
self._enable_rfid_scanner()
|
|
260
|
+
resp = self.client.get(reverse("pages:rfid-login"))
|
|
261
|
+
self.assertEqual(resp.status_code, 200)
|
|
262
|
+
self.assertEqual(resp.context["login_api_url"], reverse("rfid-login"))
|
|
263
|
+
self.assertEqual(resp.context["scan_api_url"], reverse("rfid-scan-next"))
|
|
264
|
+
|
|
265
|
+
def test_homepage_excludes_version_banner_for_anonymous(self):
|
|
266
|
+
response = self.client.get(reverse("pages:index"))
|
|
267
|
+
|
|
268
|
+
self.assertEqual(response.status_code, 200)
|
|
269
|
+
self.assertNotContains(response, "__versionCheckInitialized")
|
|
270
|
+
|
|
271
|
+
def test_homepage_includes_version_banner_for_staff(self):
|
|
272
|
+
self.client.force_login(self.staff)
|
|
273
|
+
response = self.client.get(reverse("pages:index"))
|
|
274
|
+
|
|
275
|
+
self.assertEqual(response.status_code, 200)
|
|
276
|
+
self.assertContains(response, "__versionCheckInitialized")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class AdminTemplateVersionBannerTests(TestCase):
|
|
280
|
+
def setUp(self):
|
|
281
|
+
self.client = Client()
|
|
282
|
+
User = get_user_model()
|
|
283
|
+
self.staff = User.objects.create_user(
|
|
284
|
+
username="admin-staff", password="pwd", is_staff=True
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def test_admin_login_excludes_version_banner_for_anonymous(self):
|
|
288
|
+
response = self.client.get(reverse("admin:login"))
|
|
289
|
+
|
|
290
|
+
self.assertEqual(response.status_code, 200)
|
|
291
|
+
self.assertNotContains(response, "__versionCheckInitialized")
|
|
292
|
+
|
|
293
|
+
def test_admin_dashboard_includes_version_banner_for_staff(self):
|
|
294
|
+
self.client.force_login(self.staff)
|
|
295
|
+
response = self.client.get(reverse("admin:index"))
|
|
296
|
+
|
|
297
|
+
self.assertEqual(response.status_code, 200)
|
|
298
|
+
self.assertContains(response, "__versionCheckInitialized")
|
|
299
|
+
|
|
190
300
|
def test_staff_redirects_next_when_specified(self):
|
|
191
301
|
resp = self.client.post(
|
|
192
302
|
reverse("pages:login") + "?next=/nodes/list/",
|
|
@@ -466,6 +576,23 @@ class InvitationTests(TestCase):
|
|
|
466
576
|
self.assertEqual(lead.mac_address, "")
|
|
467
577
|
self.assertEqual(len(mail.outbox), 0)
|
|
468
578
|
|
|
579
|
+
def test_request_invite_uses_original_referer(self):
|
|
580
|
+
InviteLead.objects.all().delete()
|
|
581
|
+
self.client.get(
|
|
582
|
+
reverse("pages:index"),
|
|
583
|
+
HTTP_REFERER="https://campaign.example/landing",
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
resp = self.client.post(
|
|
587
|
+
reverse("pages:request-invite"),
|
|
588
|
+
{"email": "origin@example.com"},
|
|
589
|
+
HTTP_REFERER="http://testserver/pages/request-invite/",
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
self.assertEqual(resp.status_code, 200)
|
|
593
|
+
lead = InviteLead.objects.get()
|
|
594
|
+
self.assertEqual(lead.referer, "https://campaign.example/landing")
|
|
595
|
+
|
|
469
596
|
def test_request_invite_falls_back_to_send_mail(self):
|
|
470
597
|
node = Node.objects.create(
|
|
471
598
|
hostname="local", address="127.0.0.1", mac_address="00:11:22:33:44:55"
|
|
@@ -501,6 +628,7 @@ class InvitationTests(TestCase):
|
|
|
501
628
|
lead = InviteLead.objects.get()
|
|
502
629
|
self.assertEqual(lead.mac_address, "aa:bb:cc:dd:ee:ff")
|
|
503
630
|
|
|
631
|
+
@pytest.mark.feature("ap-router")
|
|
504
632
|
@patch("pages.views.public_wifi.grant_public_access")
|
|
505
633
|
@patch(
|
|
506
634
|
"pages.views.public_wifi.resolve_mac_address",
|
|
@@ -659,7 +787,7 @@ class AdminDashboardAppListTests(TestCase):
|
|
|
659
787
|
"hostname": socket.gethostname(),
|
|
660
788
|
"address": socket.gethostbyname(socket.gethostname()),
|
|
661
789
|
"base_path": settings.BASE_DIR,
|
|
662
|
-
"port":
|
|
790
|
+
"port": 8888,
|
|
663
791
|
},
|
|
664
792
|
)
|
|
665
793
|
self.node.features.clear()
|
|
@@ -672,19 +800,36 @@ class AdminDashboardAppListTests(TestCase):
|
|
|
672
800
|
|
|
673
801
|
def test_horologia_hidden_without_celery_feature(self):
|
|
674
802
|
resp = self.client.get(reverse("admin:index"))
|
|
675
|
-
self.assertNotContains(resp, "5. Horologia
|
|
803
|
+
self.assertNotContains(resp, "5. Horologia</a>")
|
|
676
804
|
|
|
677
805
|
def test_horologia_visible_with_celery_feature(self):
|
|
678
806
|
feature = NodeFeature.objects.create(slug="celery-queue", display="Celery Queue")
|
|
679
807
|
NodeFeatureAssignment.objects.create(node=self.node, feature=feature)
|
|
680
808
|
resp = self.client.get(reverse("admin:index"))
|
|
681
|
-
self.assertContains(resp, "5. Horologia
|
|
809
|
+
self.assertContains(resp, "5. Horologia</a>")
|
|
682
810
|
|
|
683
811
|
def test_horologia_visible_with_celery_lock(self):
|
|
684
812
|
self.celery_lock.write_text("")
|
|
685
813
|
resp = self.client.get(reverse("admin:index"))
|
|
686
|
-
self.assertContains(resp, "5. Horologia
|
|
814
|
+
self.assertContains(resp, "5. Horologia</a>")
|
|
815
|
+
|
|
816
|
+
def test_dashboard_shows_last_net_message(self):
|
|
817
|
+
NetMessage.objects.all().delete()
|
|
818
|
+
NetMessage.objects.create(subject="Older", body="First body")
|
|
819
|
+
NetMessage.objects.create(subject="Latest", body="Signal ready")
|
|
820
|
+
|
|
821
|
+
resp = self.client.get(reverse("admin:index"))
|
|
822
|
+
|
|
823
|
+
self.assertContains(resp, gettext("Net message"))
|
|
824
|
+
self.assertContains(resp, "Latest — Signal ready")
|
|
825
|
+
self.assertNotContains(resp, gettext("No net messages available"))
|
|
826
|
+
|
|
827
|
+
def test_dashboard_shows_placeholder_without_net_message(self):
|
|
828
|
+
NetMessage.objects.all().delete()
|
|
829
|
+
|
|
830
|
+
resp = self.client.get(reverse("admin:index"))
|
|
687
831
|
|
|
832
|
+
self.assertContains(resp, gettext("No net messages available"))
|
|
688
833
|
|
|
689
834
|
class AdminSidebarTests(TestCase):
|
|
690
835
|
def setUp(self):
|
|
@@ -707,6 +852,66 @@ class AdminSidebarTests(TestCase):
|
|
|
707
852
|
self.assertContains(resp, 'id="admin-collapsible-apps"')
|
|
708
853
|
|
|
709
854
|
|
|
855
|
+
class AdminGoogleCalendarSidebarTests(TestCase):
|
|
856
|
+
def setUp(self):
|
|
857
|
+
self.client = Client()
|
|
858
|
+
User = get_user_model()
|
|
859
|
+
self.admin = User.objects.create_superuser(
|
|
860
|
+
username="calendar_admin", password="pwd", email="admin@example.com"
|
|
861
|
+
)
|
|
862
|
+
self.client.force_login(self.admin)
|
|
863
|
+
Site.objects.update_or_create(
|
|
864
|
+
id=1, defaults={"name": "test", "domain": "testserver"}
|
|
865
|
+
)
|
|
866
|
+
Node.objects.create(hostname="testserver", address="127.0.0.1")
|
|
867
|
+
|
|
868
|
+
def test_calendar_module_hidden_without_profile(self):
|
|
869
|
+
resp = self.client.get(reverse("admin:index"))
|
|
870
|
+
self.assertNotContains(resp, 'id="google-calendar-module"', html=False)
|
|
871
|
+
|
|
872
|
+
@patch("core.models.GoogleCalendarProfile.fetch_events")
|
|
873
|
+
def test_calendar_module_shows_events_for_user(self, fetch_events):
|
|
874
|
+
fetch_events.return_value = [
|
|
875
|
+
{
|
|
876
|
+
"summary": "Standup",
|
|
877
|
+
"start": timezone.now(),
|
|
878
|
+
"end": None,
|
|
879
|
+
"all_day": False,
|
|
880
|
+
"html_link": "https://calendar.google.com/event",
|
|
881
|
+
"location": "HQ",
|
|
882
|
+
}
|
|
883
|
+
]
|
|
884
|
+
GoogleCalendarProfile.objects.create(
|
|
885
|
+
user=self.admin,
|
|
886
|
+
calendar_id="example@group.calendar.google.com",
|
|
887
|
+
api_key="secret",
|
|
888
|
+
display_name="Team Calendar",
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
resp = self.client.get(reverse("admin:index"))
|
|
892
|
+
|
|
893
|
+
self.assertContains(resp, 'id="google-calendar-module"', html=False)
|
|
894
|
+
self.assertContains(resp, "Standup")
|
|
895
|
+
self.assertContains(resp, "Open full calendar")
|
|
896
|
+
fetch_events.assert_called_once()
|
|
897
|
+
|
|
898
|
+
@patch("core.models.GoogleCalendarProfile.fetch_events")
|
|
899
|
+
def test_calendar_module_uses_group_profile(self, fetch_events):
|
|
900
|
+
fetch_events.return_value = []
|
|
901
|
+
group = SecurityGroup.objects.create(name="Calendar Group")
|
|
902
|
+
self.admin.groups.add(group)
|
|
903
|
+
GoogleCalendarProfile.objects.create(
|
|
904
|
+
group=group,
|
|
905
|
+
calendar_id="group@calendar.google.com",
|
|
906
|
+
api_key="secret",
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
resp = self.client.get(reverse("admin:index"))
|
|
910
|
+
|
|
911
|
+
self.assertContains(resp, 'id="google-calendar-module"', html=False)
|
|
912
|
+
fetch_events.assert_called_once()
|
|
913
|
+
|
|
914
|
+
|
|
710
915
|
class ViewHistoryLoggingTests(TestCase):
|
|
711
916
|
def setUp(self):
|
|
712
917
|
self.client = Client()
|
|
@@ -715,8 +920,11 @@ class ViewHistoryLoggingTests(TestCase):
|
|
|
715
920
|
|
|
716
921
|
def _reset_purge_task(self):
|
|
717
922
|
from django_celery_beat.models import PeriodicTask
|
|
923
|
+
from core.celery_utils import periodic_task_name_variants
|
|
718
924
|
|
|
719
|
-
PeriodicTask.objects.filter(
|
|
925
|
+
PeriodicTask.objects.filter(
|
|
926
|
+
name__in=periodic_task_name_variants("pages_purge_landing_leads")
|
|
927
|
+
).delete()
|
|
720
928
|
|
|
721
929
|
def _create_local_node(self):
|
|
722
930
|
node, _ = Node.objects.update_or_create(
|
|
@@ -725,7 +933,7 @@ class ViewHistoryLoggingTests(TestCase):
|
|
|
725
933
|
"hostname": socket.gethostname(),
|
|
726
934
|
"address": "127.0.0.1",
|
|
727
935
|
"base_path": settings.BASE_DIR,
|
|
728
|
-
"port":
|
|
936
|
+
"port": 8888,
|
|
729
937
|
},
|
|
730
938
|
)
|
|
731
939
|
return node
|
|
@@ -795,7 +1003,8 @@ class ViewHistoryLoggingTests(TestCase):
|
|
|
795
1003
|
)
|
|
796
1004
|
landing = module.landings.get(path="/")
|
|
797
1005
|
landing.label = "Home Landing"
|
|
798
|
-
landing.
|
|
1006
|
+
landing.track_leads = True
|
|
1007
|
+
landing.save(update_fields=["label", "track_leads"])
|
|
799
1008
|
|
|
800
1009
|
resp = self.client.get(
|
|
801
1010
|
reverse("pages:index"), HTTP_REFERER="https://example.com/ref"
|
|
@@ -807,6 +1016,35 @@ class ViewHistoryLoggingTests(TestCase):
|
|
|
807
1016
|
self.assertEqual(lead.path, "/")
|
|
808
1017
|
self.assertEqual(lead.referer, "https://example.com/ref")
|
|
809
1018
|
|
|
1019
|
+
def test_pages_config_purges_old_view_history(self):
|
|
1020
|
+
ViewHistory.objects.all().delete()
|
|
1021
|
+
|
|
1022
|
+
old_entry = ViewHistory.objects.create(
|
|
1023
|
+
path="/old/",
|
|
1024
|
+
method="GET",
|
|
1025
|
+
status_code=200,
|
|
1026
|
+
status_text="OK",
|
|
1027
|
+
)
|
|
1028
|
+
new_entry = ViewHistory.objects.create(
|
|
1029
|
+
path="/recent/",
|
|
1030
|
+
method="GET",
|
|
1031
|
+
status_code=200,
|
|
1032
|
+
status_text="OK",
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
ViewHistory.objects.filter(pk=old_entry.pk).update(
|
|
1036
|
+
visited_at=timezone.now() - timedelta(days=20)
|
|
1037
|
+
)
|
|
1038
|
+
ViewHistory.objects.filter(pk=new_entry.pk).update(
|
|
1039
|
+
visited_at=timezone.now() - timedelta(days=10)
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
config = django_apps.get_app_config("pages")
|
|
1043
|
+
config._purge_view_history()
|
|
1044
|
+
|
|
1045
|
+
self.assertFalse(ViewHistory.objects.filter(pk=old_entry.pk).exists())
|
|
1046
|
+
self.assertTrue(ViewHistory.objects.filter(pk=new_entry.pk).exists())
|
|
1047
|
+
|
|
810
1048
|
def test_landing_visit_does_not_record_lead_without_celery(self):
|
|
811
1049
|
role = NodeRole.objects.create(name="no-celery-role")
|
|
812
1050
|
application = Application.objects.create(
|
|
@@ -820,7 +1058,8 @@ class ViewHistoryLoggingTests(TestCase):
|
|
|
820
1058
|
)
|
|
821
1059
|
landing = module.landings.get(path="/")
|
|
822
1060
|
landing.label = "No Celery"
|
|
823
|
-
landing.
|
|
1061
|
+
landing.track_leads = True
|
|
1062
|
+
landing.save(update_fields=["label", "track_leads"])
|
|
824
1063
|
|
|
825
1064
|
resp = self.client.get(reverse("pages:index"))
|
|
826
1065
|
|
|
@@ -840,7 +1079,8 @@ class ViewHistoryLoggingTests(TestCase):
|
|
|
840
1079
|
)
|
|
841
1080
|
landing = module.landings.get(path="/")
|
|
842
1081
|
landing.enabled = False
|
|
843
|
-
landing.
|
|
1082
|
+
landing.track_leads = True
|
|
1083
|
+
landing.save(update_fields=["enabled", "track_leads"])
|
|
844
1084
|
|
|
845
1085
|
resp = self.client.get(reverse("pages:index"))
|
|
846
1086
|
|
|
@@ -902,6 +1142,44 @@ class ViewHistoryAdminTests(TestCase):
|
|
|
902
1142
|
self.assertEqual(totals.get("/"), 2)
|
|
903
1143
|
self.assertEqual(totals.get("/about/"), 1)
|
|
904
1144
|
|
|
1145
|
+
def test_graph_data_endpoint_respects_days_parameter(self):
|
|
1146
|
+
ViewHistory.all_objects.all().delete()
|
|
1147
|
+
reference_date = date(2025, 5, 1)
|
|
1148
|
+
tz = timezone.get_current_timezone()
|
|
1149
|
+
path = "/range/"
|
|
1150
|
+
|
|
1151
|
+
for offset in range(10):
|
|
1152
|
+
entry = ViewHistory.objects.create(
|
|
1153
|
+
path=path,
|
|
1154
|
+
method="GET",
|
|
1155
|
+
status_code=200,
|
|
1156
|
+
status_text="OK",
|
|
1157
|
+
error_message="",
|
|
1158
|
+
view_name="pages:index",
|
|
1159
|
+
)
|
|
1160
|
+
visited_date = reference_date - timedelta(days=offset)
|
|
1161
|
+
visited_at = timezone.make_aware(
|
|
1162
|
+
datetime.combine(visited_date, datetime_time(12, 0)), tz
|
|
1163
|
+
)
|
|
1164
|
+
entry.visited_at = visited_at
|
|
1165
|
+
entry.save(update_fields=["visited_at"])
|
|
1166
|
+
|
|
1167
|
+
url = reverse("admin:pages_viewhistory_traffic_data")
|
|
1168
|
+
with patch("pages.admin.timezone.localdate", return_value=reference_date):
|
|
1169
|
+
resp = self.client.get(url, {"days": 7})
|
|
1170
|
+
|
|
1171
|
+
self.assertEqual(resp.status_code, 200)
|
|
1172
|
+
data = resp.json()
|
|
1173
|
+
|
|
1174
|
+
self.assertEqual(len(data.get("labels", [])), 7)
|
|
1175
|
+
self.assertEqual(data.get("meta", {}).get("start"), (reference_date - timedelta(days=6)).isoformat())
|
|
1176
|
+
self.assertEqual(data.get("meta", {}).get("end"), reference_date.isoformat())
|
|
1177
|
+
|
|
1178
|
+
totals = {
|
|
1179
|
+
dataset["label"]: sum(dataset["data"]) for dataset in data.get("datasets", [])
|
|
1180
|
+
}
|
|
1181
|
+
self.assertEqual(totals.get(path), 7)
|
|
1182
|
+
|
|
905
1183
|
def test_graph_data_includes_late_evening_visits(self):
|
|
906
1184
|
target_date = date(2025, 9, 27)
|
|
907
1185
|
entry = ViewHistory.objects.create(
|
|
@@ -964,7 +1242,7 @@ class LandingLeadAdminTests(TestCase):
|
|
|
964
1242
|
"hostname": socket.gethostname(),
|
|
965
1243
|
"address": "127.0.0.1",
|
|
966
1244
|
"base_path": settings.BASE_DIR,
|
|
967
|
-
"port":
|
|
1245
|
+
"port": 8888,
|
|
968
1246
|
},
|
|
969
1247
|
)
|
|
970
1248
|
self.node.features.clear()
|
|
@@ -972,8 +1250,11 @@ class LandingLeadAdminTests(TestCase):
|
|
|
972
1250
|
|
|
973
1251
|
def _reset_purge_task(self):
|
|
974
1252
|
from django_celery_beat.models import PeriodicTask
|
|
1253
|
+
from core.celery_utils import periodic_task_name_variants
|
|
975
1254
|
|
|
976
|
-
PeriodicTask.objects.filter(
|
|
1255
|
+
PeriodicTask.objects.filter(
|
|
1256
|
+
name__in=periodic_task_name_variants("pages_purge_landing_leads")
|
|
1257
|
+
).delete()
|
|
977
1258
|
|
|
978
1259
|
def test_changelist_warns_without_celery(self):
|
|
979
1260
|
url = reverse("admin:pages_landinglead_changelist")
|
|
@@ -1109,6 +1390,50 @@ class LogViewerAdminTests(SimpleTestCase):
|
|
|
1109
1390
|
self.assertEqual(context["selected_log"], "selected.log")
|
|
1110
1391
|
self.assertIn("hello world", context["log_content"])
|
|
1111
1392
|
|
|
1393
|
+
def test_log_viewer_applies_line_limit(self):
|
|
1394
|
+
content = "\n".join(f"line {i}" for i in range(50))
|
|
1395
|
+
self._create_log("limited.log", content)
|
|
1396
|
+
response = self._render({"log": "limited.log", "limit": "20"})
|
|
1397
|
+
context = response.context_data
|
|
1398
|
+
self.assertEqual(context["log_limit_choice"], "20")
|
|
1399
|
+
self.assertIn("line 49", context["log_content"])
|
|
1400
|
+
self.assertIn("line 30", context["log_content"])
|
|
1401
|
+
self.assertNotIn("line 29", context["log_content"])
|
|
1402
|
+
|
|
1403
|
+
def test_log_viewer_all_limit_returns_full_log(self):
|
|
1404
|
+
content = "first\nsecond\nthird"
|
|
1405
|
+
self._create_log("all.log", content)
|
|
1406
|
+
response = self._render({"log": "all.log", "limit": "all"})
|
|
1407
|
+
context = response.context_data
|
|
1408
|
+
self.assertEqual(context["log_limit_choice"], "all")
|
|
1409
|
+
self.assertIn("first", context["log_content"])
|
|
1410
|
+
self.assertIn("second", context["log_content"])
|
|
1411
|
+
|
|
1412
|
+
def test_log_viewer_invalid_limit_defaults_to_20(self):
|
|
1413
|
+
content = "\n".join(f"item {i}" for i in range(5))
|
|
1414
|
+
self._create_log("invalid-limit.log", content)
|
|
1415
|
+
response = self._render({"log": "invalid-limit.log", "limit": "oops"})
|
|
1416
|
+
context = response.context_data
|
|
1417
|
+
self.assertEqual(context["log_limit_choice"], "20")
|
|
1418
|
+
|
|
1419
|
+
def test_log_viewer_downloads_selected_log(self):
|
|
1420
|
+
self._create_log("download.log", "downloadable content")
|
|
1421
|
+
request = self._build_request({"log": "download.log", "download": "1"})
|
|
1422
|
+
context = {
|
|
1423
|
+
"site_title": "Constellation",
|
|
1424
|
+
"site_header": "Constellation",
|
|
1425
|
+
"site_url": "/",
|
|
1426
|
+
"available_apps": [],
|
|
1427
|
+
}
|
|
1428
|
+
with patch("pages.admin.admin.site.each_context", return_value=context), patch(
|
|
1429
|
+
"pages.context_processors.get_site", return_value=None
|
|
1430
|
+
):
|
|
1431
|
+
response = log_viewer(request)
|
|
1432
|
+
self.assertIsInstance(response, FileResponse)
|
|
1433
|
+
self.assertIn("attachment", response["Content-Disposition"])
|
|
1434
|
+
content = b"".join(response.streaming_content).decode()
|
|
1435
|
+
self.assertIn("downloadable content", content)
|
|
1436
|
+
|
|
1112
1437
|
def test_log_viewer_reports_missing_log(self):
|
|
1113
1438
|
response = self._render({"log": "missing.log"})
|
|
1114
1439
|
self.assertIn("requested log could not be found", response.context_data["log_error"])
|
|
@@ -1145,15 +1470,131 @@ class AdminModelStatusTests(TestCase):
|
|
|
1145
1470
|
|
|
1146
1471
|
Node.objects.create(hostname="testserver", address="127.0.0.1")
|
|
1147
1472
|
|
|
1148
|
-
|
|
1149
|
-
def test_status_dots_render(self, mock_tables):
|
|
1150
|
-
from django.db import connection
|
|
1151
|
-
|
|
1152
|
-
tables = type(connection.introspection).table_names(connection.introspection)
|
|
1153
|
-
mock_tables.return_value = [t for t in tables if t != "pages_module"]
|
|
1473
|
+
def test_status_indicator_removed(self):
|
|
1154
1474
|
resp = self.client.get(reverse("admin:index"))
|
|
1155
|
-
self.
|
|
1156
|
-
|
|
1475
|
+
self.assertNotContains(resp, "class=\"model-status")
|
|
1476
|
+
|
|
1477
|
+
changelist = self.client.get(reverse("admin:pages_application_changelist"))
|
|
1478
|
+
self.assertNotContains(changelist, "class=\"model-status")
|
|
1479
|
+
|
|
1480
|
+
|
|
1481
|
+
class _FakeQuerySet(list):
|
|
1482
|
+
def only(self, *args, **kwargs):
|
|
1483
|
+
return self
|
|
1484
|
+
|
|
1485
|
+
def order_by(self, *args, **kwargs):
|
|
1486
|
+
return self
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
class SiteConfigurationStagingTests(SimpleTestCase):
|
|
1490
|
+
def setUp(self):
|
|
1491
|
+
self.tmpdir = tempfile.mkdtemp()
|
|
1492
|
+
self.addCleanup(shutil.rmtree, self.tmpdir)
|
|
1493
|
+
self.config_path = Path(self.tmpdir) / "nginx-sites.json"
|
|
1494
|
+
self._path_patcher = patch(
|
|
1495
|
+
"pages.site_config._sites_config_path", side_effect=lambda: self.config_path
|
|
1496
|
+
)
|
|
1497
|
+
self._path_patcher.start()
|
|
1498
|
+
self.addCleanup(self._path_patcher.stop)
|
|
1499
|
+
self._model_patcher = patch("pages.site_config.apps.get_model")
|
|
1500
|
+
self.mock_get_model = self._model_patcher.start()
|
|
1501
|
+
self.addCleanup(self._model_patcher.stop)
|
|
1502
|
+
|
|
1503
|
+
def _read_config(self):
|
|
1504
|
+
if not self.config_path.exists():
|
|
1505
|
+
return None
|
|
1506
|
+
return json.loads(self.config_path.read_text(encoding="utf-8"))
|
|
1507
|
+
|
|
1508
|
+
def _set_sites(self, sites):
|
|
1509
|
+
queryset = _FakeQuerySet(sites)
|
|
1510
|
+
|
|
1511
|
+
class _Manager:
|
|
1512
|
+
@staticmethod
|
|
1513
|
+
def filter(**kwargs):
|
|
1514
|
+
return queryset
|
|
1515
|
+
|
|
1516
|
+
self.mock_get_model.return_value = SimpleNamespace(objects=_Manager())
|
|
1517
|
+
|
|
1518
|
+
def test_managed_site_persists_configuration(self):
|
|
1519
|
+
self._set_sites([SimpleNamespace(domain="example.com", require_https=True)])
|
|
1520
|
+
site_config.update_local_nginx_scripts()
|
|
1521
|
+
config = self._read_config()
|
|
1522
|
+
self.assertEqual(
|
|
1523
|
+
config,
|
|
1524
|
+
[
|
|
1525
|
+
{
|
|
1526
|
+
"domain": "example.com",
|
|
1527
|
+
"require_https": True,
|
|
1528
|
+
}
|
|
1529
|
+
],
|
|
1530
|
+
)
|
|
1531
|
+
|
|
1532
|
+
def test_disabling_managed_site_removes_entry(self):
|
|
1533
|
+
primary = SimpleNamespace(domain="primary.test", require_https=False)
|
|
1534
|
+
secondary = SimpleNamespace(domain="secondary.test", require_https=False)
|
|
1535
|
+
self._set_sites([primary, secondary])
|
|
1536
|
+
site_config.update_local_nginx_scripts()
|
|
1537
|
+
config = self._read_config()
|
|
1538
|
+
self.assertEqual(
|
|
1539
|
+
[entry["domain"] for entry in config],
|
|
1540
|
+
["primary.test", "secondary.test"],
|
|
1541
|
+
)
|
|
1542
|
+
|
|
1543
|
+
self._set_sites([secondary])
|
|
1544
|
+
site_config.update_local_nginx_scripts()
|
|
1545
|
+
config = self._read_config()
|
|
1546
|
+
self.assertEqual(config, [{"domain": "secondary.test", "require_https": False}])
|
|
1547
|
+
|
|
1548
|
+
self._set_sites([])
|
|
1549
|
+
site_config.update_local_nginx_scripts()
|
|
1550
|
+
self.assertIsNone(self._read_config())
|
|
1551
|
+
|
|
1552
|
+
def test_require_https_toggle_updates_configuration(self):
|
|
1553
|
+
site = SimpleNamespace(domain="secure.example", require_https=False)
|
|
1554
|
+
self._set_sites([site])
|
|
1555
|
+
site_config.update_local_nginx_scripts()
|
|
1556
|
+
config = self._read_config()
|
|
1557
|
+
self.assertEqual(config, [{"domain": "secure.example", "require_https": False}])
|
|
1558
|
+
|
|
1559
|
+
site.require_https = True
|
|
1560
|
+
self._set_sites([site])
|
|
1561
|
+
site_config.update_local_nginx_scripts()
|
|
1562
|
+
config = self._read_config()
|
|
1563
|
+
self.assertEqual(config, [{"domain": "secure.example", "require_https": True}])
|
|
1564
|
+
|
|
1565
|
+
|
|
1566
|
+
class SiteRequireHttpsMiddlewareTests(SimpleTestCase):
|
|
1567
|
+
def setUp(self):
|
|
1568
|
+
self.factory = RequestFactory()
|
|
1569
|
+
self.middleware = SiteHttpsRedirectMiddleware(lambda request: HttpResponse("ok"))
|
|
1570
|
+
self.secure_site = SimpleNamespace(domain="secure.test", require_https=True)
|
|
1571
|
+
|
|
1572
|
+
def test_http_request_redirects_to_https(self):
|
|
1573
|
+
request = self.factory.get("/", HTTP_HOST="secure.test")
|
|
1574
|
+
request.site = self.secure_site
|
|
1575
|
+
response = self.middleware(request)
|
|
1576
|
+
self.assertEqual(response.status_code, 301)
|
|
1577
|
+
self.assertTrue(response["Location"].startswith("https://secure.test"))
|
|
1578
|
+
|
|
1579
|
+
def test_secure_request_not_redirected(self):
|
|
1580
|
+
request = self.factory.get("/", HTTP_HOST="secure.test", secure=True)
|
|
1581
|
+
request.site = self.secure_site
|
|
1582
|
+
response = self.middleware(request)
|
|
1583
|
+
self.assertEqual(response.status_code, 200)
|
|
1584
|
+
|
|
1585
|
+
def test_forwarded_proto_respected(self):
|
|
1586
|
+
request = self.factory.get(
|
|
1587
|
+
"/", HTTP_HOST="secure.test", HTTP_X_FORWARDED_PROTO="https"
|
|
1588
|
+
)
|
|
1589
|
+
request.site = self.secure_site
|
|
1590
|
+
response = self.middleware(request)
|
|
1591
|
+
self.assertEqual(response.status_code, 200)
|
|
1592
|
+
|
|
1593
|
+
self.secure_site.require_https = False
|
|
1594
|
+
request = self.factory.get("/", HTTP_HOST="secure.test")
|
|
1595
|
+
request.site = self.secure_site
|
|
1596
|
+
response = self.middleware(request)
|
|
1597
|
+
self.assertEqual(response.status_code, 200)
|
|
1157
1598
|
|
|
1158
1599
|
|
|
1159
1600
|
class SiteAdminRegisterCurrentTests(TestCase):
|
|
@@ -1188,6 +1629,7 @@ class SiteAdminRegisterCurrentTests(TestCase):
|
|
|
1188
1629
|
self.assertEqual(site.name, "")
|
|
1189
1630
|
|
|
1190
1631
|
|
|
1632
|
+
@pytest.mark.feature("screenshot-poll")
|
|
1191
1633
|
class SiteAdminScreenshotTests(TestCase):
|
|
1192
1634
|
def setUp(self):
|
|
1193
1635
|
self.client = Client()
|
|
@@ -1307,17 +1749,17 @@ class NavAppsTests(TestCase):
|
|
|
1307
1749
|
)
|
|
1308
1750
|
app = Application.objects.create(name="Readme")
|
|
1309
1751
|
Module.objects.create(
|
|
1310
|
-
node_role=role, application=app, path="/", is_default=True
|
|
1752
|
+
node_role=role, application=app, path="/", is_default=True, menu="Cookbooks"
|
|
1311
1753
|
)
|
|
1312
1754
|
|
|
1313
1755
|
def test_nav_pill_renders(self):
|
|
1314
1756
|
resp = self.client.get(reverse("pages:index"))
|
|
1315
|
-
self.assertContains(resp, "
|
|
1757
|
+
self.assertContains(resp, "COOKBOOKS")
|
|
1316
1758
|
self.assertContains(resp, "badge rounded-pill")
|
|
1317
1759
|
|
|
1318
1760
|
def test_nav_pill_renders_with_port(self):
|
|
1319
|
-
resp = self.client.get(reverse("pages:index"), HTTP_HOST="127.0.0.1:
|
|
1320
|
-
self.assertContains(resp, "
|
|
1761
|
+
resp = self.client.get(reverse("pages:index"), HTTP_HOST="127.0.0.1:8888")
|
|
1762
|
+
self.assertContains(resp, "COOKBOOKS")
|
|
1321
1763
|
|
|
1322
1764
|
def test_nav_pill_uses_menu_field(self):
|
|
1323
1765
|
site_app = Module.objects.get()
|
|
@@ -1325,7 +1767,7 @@ class NavAppsTests(TestCase):
|
|
|
1325
1767
|
site_app.save()
|
|
1326
1768
|
resp = self.client.get(reverse("pages:index"))
|
|
1327
1769
|
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">DOCS')
|
|
1328
|
-
self.assertNotContains(resp, 'badge rounded-pill text-bg-secondary">
|
|
1770
|
+
self.assertNotContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
|
|
1329
1771
|
|
|
1330
1772
|
def test_app_without_root_url_excluded(self):
|
|
1331
1773
|
role = NodeRole.objects.get(name="Terminal")
|
|
@@ -1390,20 +1832,22 @@ class RoleLandingRedirectTests(TestCase):
|
|
|
1390
1832
|
|
|
1391
1833
|
def test_satellite_redirects_to_dashboard(self):
|
|
1392
1834
|
target = self._configure_role_landing(
|
|
1393
|
-
"Satellite", "/ocpp/", "CPMS Online Dashboard"
|
|
1835
|
+
"Satellite", "/ocpp/cpms/dashboard/", "CPMS Online Dashboard"
|
|
1394
1836
|
)
|
|
1395
1837
|
resp = self.client.get(reverse("pages:index"))
|
|
1396
1838
|
self.assertRedirects(resp, target, fetch_redirect_response=False)
|
|
1397
1839
|
|
|
1398
1840
|
def test_control_redirects_to_rfid(self):
|
|
1399
1841
|
target = self._configure_role_landing(
|
|
1400
|
-
"Control", "/ocpp/rfid/", "RFID Tag Validator"
|
|
1842
|
+
"Control", "/ocpp/rfid/validator/", "RFID Tag Validator"
|
|
1401
1843
|
)
|
|
1402
1844
|
resp = self.client.get(reverse("pages:index"))
|
|
1403
1845
|
self.assertRedirects(resp, target, fetch_redirect_response=False)
|
|
1404
1846
|
|
|
1405
1847
|
def test_security_group_redirect_takes_priority(self):
|
|
1406
|
-
self._configure_role_landing(
|
|
1848
|
+
self._configure_role_landing(
|
|
1849
|
+
"Control", "/ocpp/rfid/validator/", "RFID Tag Validator"
|
|
1850
|
+
)
|
|
1407
1851
|
role = self.node.role
|
|
1408
1852
|
group = SecurityGroup.objects.create(name="Operators")
|
|
1409
1853
|
group_landing = self._ensure_landing(role, "/ocpp/group/", "Group Landing")
|
|
@@ -1420,7 +1864,9 @@ class RoleLandingRedirectTests(TestCase):
|
|
|
1420
1864
|
)
|
|
1421
1865
|
|
|
1422
1866
|
def test_user_redirect_overrides_group_with_higher_priority(self):
|
|
1423
|
-
self._configure_role_landing(
|
|
1867
|
+
self._configure_role_landing(
|
|
1868
|
+
"Control", "/ocpp/rfid/validator/", "RFID Tag Validator"
|
|
1869
|
+
)
|
|
1424
1870
|
role = self.node.role
|
|
1425
1871
|
group = SecurityGroup.objects.create(name="Operators")
|
|
1426
1872
|
group_landing = self._ensure_landing(role, "/ocpp/group/", "Group Landing")
|
|
@@ -1442,10 +1888,10 @@ class RoleLandingRedirectTests(TestCase):
|
|
|
1442
1888
|
)
|
|
1443
1889
|
|
|
1444
1890
|
|
|
1445
|
-
class
|
|
1891
|
+
class WatchtowerNavTests(TestCase):
|
|
1446
1892
|
def setUp(self):
|
|
1447
1893
|
self.client = Client()
|
|
1448
|
-
role, _ = NodeRole.objects.get_or_create(name="
|
|
1894
|
+
role, _ = NodeRole.objects.get_or_create(name="Watchtower")
|
|
1449
1895
|
Node.objects.update_or_create(
|
|
1450
1896
|
mac_address=Node.get_current_mac(),
|
|
1451
1897
|
defaults={
|
|
@@ -1455,38 +1901,56 @@ class ConstellationNavTests(TestCase):
|
|
|
1455
1901
|
},
|
|
1456
1902
|
)
|
|
1457
1903
|
Site.objects.update_or_create(
|
|
1458
|
-
id=1, defaults={"domain": "
|
|
1904
|
+
id=1, defaults={"domain": "arthexis.com", "name": "Arthexis"}
|
|
1459
1905
|
)
|
|
1460
1906
|
fixtures = [
|
|
1461
1907
|
Path(
|
|
1462
1908
|
settings.BASE_DIR,
|
|
1463
1909
|
"pages",
|
|
1464
1910
|
"fixtures",
|
|
1465
|
-
"
|
|
1911
|
+
"default__application_pages.json",
|
|
1912
|
+
),
|
|
1913
|
+
Path(
|
|
1914
|
+
settings.BASE_DIR,
|
|
1915
|
+
"pages",
|
|
1916
|
+
"fixtures",
|
|
1917
|
+
"watchtower__application_ocpp.json",
|
|
1918
|
+
),
|
|
1919
|
+
Path(
|
|
1920
|
+
settings.BASE_DIR,
|
|
1921
|
+
"pages",
|
|
1922
|
+
"fixtures",
|
|
1923
|
+
"watchtower__module_ocpp.json",
|
|
1924
|
+
),
|
|
1925
|
+
Path(
|
|
1926
|
+
settings.BASE_DIR,
|
|
1927
|
+
"pages",
|
|
1928
|
+
"fixtures",
|
|
1929
|
+
"watchtower__landing_ocpp_dashboard.json",
|
|
1466
1930
|
),
|
|
1467
1931
|
Path(
|
|
1468
1932
|
settings.BASE_DIR,
|
|
1469
1933
|
"pages",
|
|
1470
1934
|
"fixtures",
|
|
1471
|
-
"
|
|
1935
|
+
"watchtower__landing_ocpp_cp_simulator.json",
|
|
1472
1936
|
),
|
|
1473
1937
|
Path(
|
|
1474
1938
|
settings.BASE_DIR,
|
|
1475
1939
|
"pages",
|
|
1476
1940
|
"fixtures",
|
|
1477
|
-
"
|
|
1941
|
+
"watchtower__landing_ocpp_rfid.json",
|
|
1478
1942
|
),
|
|
1479
1943
|
Path(
|
|
1480
1944
|
settings.BASE_DIR,
|
|
1481
1945
|
"pages",
|
|
1482
1946
|
"fixtures",
|
|
1483
|
-
"
|
|
1947
|
+
"watchtower__module_readme.json",
|
|
1484
1948
|
),
|
|
1485
1949
|
Path(
|
|
1486
1950
|
settings.BASE_DIR,
|
|
1487
1951
|
"pages",
|
|
1488
1952
|
"fixtures",
|
|
1489
|
-
"
|
|
1953
|
+
"watchtower__landing_readme.json",
|
|
1490
1954
|
),
|
|
1491
1955
|
]
|
|
1492
1956
|
call_command("loaddata", *map(str, fixtures))
|
|
@@ -1499,13 +1963,13 @@ class ConstellationNavTests(TestCase):
|
|
|
1499
1963
|
self.assertNotIn("RFID", nav_labels)
|
|
1500
1964
|
self.assertTrue(
|
|
1501
1965
|
Module.objects.filter(
|
|
1502
|
-
path="/ocpp/", node_role__name="
|
|
1966
|
+
path="/ocpp/", node_role__name="Watchtower"
|
|
1503
1967
|
).exists()
|
|
1504
1968
|
)
|
|
1505
1969
|
self.assertFalse(
|
|
1506
1970
|
Module.objects.filter(
|
|
1507
1971
|
path="/ocpp/rfid/",
|
|
1508
|
-
node_role__name="
|
|
1972
|
+
node_role__name="Watchtower",
|
|
1509
1973
|
is_deleted=False,
|
|
1510
1974
|
).exists()
|
|
1511
1975
|
)
|
|
@@ -1517,9 +1981,16 @@ class ConstellationNavTests(TestCase):
|
|
|
1517
1981
|
landing_labels = [landing.label for landing in ocpp_module.enabled_landings]
|
|
1518
1982
|
self.assertIn("RFID Tag Validator", landing_labels)
|
|
1519
1983
|
|
|
1984
|
+
@override_settings(ALLOWED_HOSTS=["testserver", "arthexis.com"])
|
|
1985
|
+
def test_cookbooks_pill_visible_for_arthexis(self):
|
|
1986
|
+
resp = self.client.get(
|
|
1987
|
+
reverse("pages:index"), HTTP_HOST="arthexis.com"
|
|
1988
|
+
)
|
|
1989
|
+
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
|
|
1990
|
+
|
|
1520
1991
|
def test_ocpp_dashboard_visible(self):
|
|
1521
1992
|
resp = self.client.get(reverse("pages:index"))
|
|
1522
|
-
self.assertContains(resp, 'href="/ocpp/"')
|
|
1993
|
+
self.assertContains(resp, 'href="/ocpp/cpms/dashboard/"')
|
|
1523
1994
|
|
|
1524
1995
|
|
|
1525
1996
|
class ReleaseModuleNavTests(TestCase):
|
|
@@ -1661,7 +2132,7 @@ class ControlNavTests(TestCase):
|
|
|
1661
2132
|
self.client.force_login(user)
|
|
1662
2133
|
resp = self.client.get(reverse("pages:index"))
|
|
1663
2134
|
self.assertEqual(resp.status_code, 200)
|
|
1664
|
-
self.assertContains(resp, 'href="/ocpp/"')
|
|
2135
|
+
self.assertContains(resp, 'href="/ocpp/cpms/dashboard/"')
|
|
1665
2136
|
self.assertContains(
|
|
1666
2137
|
resp, 'badge rounded-pill text-bg-secondary">CHARGERS'
|
|
1667
2138
|
)
|
|
@@ -1693,10 +2164,76 @@ class ControlNavTests(TestCase):
|
|
|
1693
2164
|
self.assertFalse(resp.context["header_references"])
|
|
1694
2165
|
self.assertNotContains(resp, "https://example.com/hidden")
|
|
1695
2166
|
|
|
2167
|
+
def test_header_link_hidden_when_only_site_matches(self):
|
|
2168
|
+
terminal_role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
2169
|
+
site = Site.objects.get(domain="testserver")
|
|
2170
|
+
reference = Reference.objects.create(
|
|
2171
|
+
alt_text="Restricted",
|
|
2172
|
+
value="https://example.com/restricted",
|
|
2173
|
+
show_in_header=True,
|
|
2174
|
+
)
|
|
2175
|
+
reference.roles.add(terminal_role)
|
|
2176
|
+
reference.sites.add(site)
|
|
2177
|
+
|
|
2178
|
+
resp = self.client.get(reverse("pages:index"))
|
|
2179
|
+
|
|
2180
|
+
self.assertIn("header_references", resp.context)
|
|
2181
|
+
self.assertFalse(resp.context["header_references"])
|
|
2182
|
+
self.assertNotContains(resp, "https://example.com/restricted")
|
|
2183
|
+
|
|
1696
2184
|
def test_readme_pill_visible(self):
|
|
1697
2185
|
resp = self.client.get(reverse("pages:readme"))
|
|
1698
|
-
self.assertContains(resp, 'href="/
|
|
1699
|
-
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">
|
|
2186
|
+
self.assertContains(resp, 'href="/read/docs/cookbooks/install-start-stop-upgrade-uninstall"')
|
|
2187
|
+
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
|
|
2188
|
+
|
|
2189
|
+
def test_cookbook_pill_has_no_dropdown(self):
|
|
2190
|
+
module = Module.objects.get(node_role__name="Control", path="/read/")
|
|
2191
|
+
Landing.objects.create(
|
|
2192
|
+
module=module,
|
|
2193
|
+
path="/man/",
|
|
2194
|
+
label="Manuals",
|
|
2195
|
+
enabled=True,
|
|
2196
|
+
)
|
|
2197
|
+
|
|
2198
|
+
resp = self.client.get(reverse("pages:readme"))
|
|
2199
|
+
|
|
2200
|
+
self.assertContains(
|
|
2201
|
+
resp,
|
|
2202
|
+
'<a class="nav-link" href="/read/docs/cookbooks/install-start-stop-upgrade-uninstall"><span class="badge rounded-pill text-bg-secondary">COOKBOOKS</span></a>',
|
|
2203
|
+
html=True,
|
|
2204
|
+
)
|
|
2205
|
+
self.assertNotContains(resp, 'dropdown-item" href="/man/"')
|
|
2206
|
+
|
|
2207
|
+
def test_readme_page_includes_qr_share(self):
|
|
2208
|
+
resp = self.client.get(reverse("pages:readme"), {"section": "intro"})
|
|
2209
|
+
self.assertContains(resp, 'id="reader-qr"')
|
|
2210
|
+
self.assertContains(
|
|
2211
|
+
resp,
|
|
2212
|
+
'data-url="http://testserver/read/?section=intro"',
|
|
2213
|
+
)
|
|
2214
|
+
self.assertNotContains(resp, "Scan this page")
|
|
2215
|
+
self.assertNotContains(
|
|
2216
|
+
resp, 'class="small text-break text-muted mt-3 mb-0"'
|
|
2217
|
+
)
|
|
2218
|
+
|
|
2219
|
+
def test_readme_document_by_name(self):
|
|
2220
|
+
resp = self.client.get(reverse("pages:readme-document", args=["AGENTS.md"]))
|
|
2221
|
+
self.assertEqual(resp.status_code, 200)
|
|
2222
|
+
self.assertContains(resp, "Agent Guidelines")
|
|
2223
|
+
|
|
2224
|
+
def test_readme_document_by_relative_path(self):
|
|
2225
|
+
resp = self.client.get(
|
|
2226
|
+
reverse(
|
|
2227
|
+
"pages:readme-document",
|
|
2228
|
+
args=["docs/development/maintenance-roadmap.md"],
|
|
2229
|
+
)
|
|
2230
|
+
)
|
|
2231
|
+
self.assertEqual(resp.status_code, 200)
|
|
2232
|
+
self.assertContains(resp, "Maintenance Improvement Proposals")
|
|
2233
|
+
|
|
2234
|
+
def test_readme_document_rejects_traversal(self):
|
|
2235
|
+
resp = self.client.get("/read/../../SECRET.md")
|
|
2236
|
+
self.assertEqual(resp.status_code, 404)
|
|
1700
2237
|
|
|
1701
2238
|
|
|
1702
2239
|
class SatelliteNavTests(TestCase):
|
|
@@ -1768,8 +2305,8 @@ class SatelliteNavTests(TestCase):
|
|
|
1768
2305
|
|
|
1769
2306
|
def test_readme_pill_visible(self):
|
|
1770
2307
|
resp = self.client.get(reverse("pages:readme"))
|
|
1771
|
-
self.assertContains(resp, 'href="/
|
|
1772
|
-
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">
|
|
2308
|
+
self.assertContains(resp, 'href="/read/docs/cookbooks/install-start-stop-upgrade-uninstall"')
|
|
2309
|
+
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
|
|
1773
2310
|
|
|
1774
2311
|
|
|
1775
2312
|
class PowerNavTests(TestCase):
|
|
@@ -1804,9 +2341,9 @@ class PowerNavTests(TestCase):
|
|
|
1804
2341
|
power_module = module
|
|
1805
2342
|
break
|
|
1806
2343
|
self.assertIsNotNone(power_module)
|
|
1807
|
-
self.assertEqual(power_module.menu_label.upper(), "
|
|
2344
|
+
self.assertEqual(power_module.menu_label.upper(), "CALCULATORS")
|
|
1808
2345
|
landing_labels = {landing.label for landing in power_module.enabled_landings}
|
|
1809
|
-
self.assertIn("AWG Calculator", landing_labels)
|
|
2346
|
+
self.assertIn("AWG Cable Calculator", landing_labels)
|
|
1810
2347
|
|
|
1811
2348
|
def test_manual_pill_label(self):
|
|
1812
2349
|
resp = self.client.get(reverse("pages:index"))
|
|
@@ -1830,9 +2367,88 @@ class PowerNavTests(TestCase):
|
|
|
1830
2367
|
break
|
|
1831
2368
|
self.assertIsNotNone(power_module)
|
|
1832
2369
|
landing_labels = {landing.label for landing in power_module.enabled_landings}
|
|
1833
|
-
self.assertIn("AWG Calculator", landing_labels)
|
|
2370
|
+
self.assertIn("AWG Cable Calculator", landing_labels)
|
|
1834
2371
|
self.assertIn("Energy Tariff Calculator", landing_labels)
|
|
1835
2372
|
|
|
2373
|
+
def test_locked_landing_shows_lock_icon(self):
|
|
2374
|
+
resp = self.client.get(reverse("pages:index"))
|
|
2375
|
+
html = resp.content.decode()
|
|
2376
|
+
energy_index = html.find("Energy Tariff Calculator")
|
|
2377
|
+
self.assertGreaterEqual(energy_index, 0)
|
|
2378
|
+
icon_index = html.find("dropdown-lock-icon", energy_index, energy_index + 300)
|
|
2379
|
+
self.assertGreaterEqual(icon_index, 0)
|
|
2380
|
+
|
|
2381
|
+
def test_lock_icon_disappears_after_login(self):
|
|
2382
|
+
self.client.force_login(self.user)
|
|
2383
|
+
resp = self.client.get(reverse("pages:index"))
|
|
2384
|
+
html = resp.content.decode()
|
|
2385
|
+
energy_index = html.find("Energy Tariff Calculator")
|
|
2386
|
+
self.assertGreaterEqual(energy_index, 0)
|
|
2387
|
+
icon_index = html.find("dropdown-lock-icon", energy_index, energy_index + 300)
|
|
2388
|
+
self.assertEqual(icon_index, -1)
|
|
2389
|
+
|
|
2390
|
+
|
|
2391
|
+
class WatchtowerLandingLinkTests(TestCase):
|
|
2392
|
+
def setUp(self):
|
|
2393
|
+
self.client = Client()
|
|
2394
|
+
self.role, _ = NodeRole.objects.get_or_create(name="Watchtower")
|
|
2395
|
+
Node.objects.update_or_create(
|
|
2396
|
+
mac_address=Node.get_current_mac(),
|
|
2397
|
+
defaults={
|
|
2398
|
+
"hostname": "localhost",
|
|
2399
|
+
"address": "127.0.0.1",
|
|
2400
|
+
"role": self.role,
|
|
2401
|
+
},
|
|
2402
|
+
)
|
|
2403
|
+
Site.objects.update_or_create(
|
|
2404
|
+
id=1, defaults={"domain": "testserver", "name": ""}
|
|
2405
|
+
)
|
|
2406
|
+
self.ocpp_app, _ = Application.objects.get_or_create(name="ocpp")
|
|
2407
|
+
self.ocpp_module, _ = Module.objects.get_or_create(
|
|
2408
|
+
node_role=self.role,
|
|
2409
|
+
application=self.ocpp_app,
|
|
2410
|
+
path="/ocpp/",
|
|
2411
|
+
)
|
|
2412
|
+
self.ocpp_module.create_landings()
|
|
2413
|
+
|
|
2414
|
+
def _get_ocpp_module(self, response):
|
|
2415
|
+
for module in response.context["nav_modules"]:
|
|
2416
|
+
if module.path == "/ocpp/":
|
|
2417
|
+
return module
|
|
2418
|
+
return None
|
|
2419
|
+
|
|
2420
|
+
def test_ocpp_landings_present_for_anonymous_users(self):
|
|
2421
|
+
response = self.client.get(reverse("pages:index"))
|
|
2422
|
+
ocpp_module = self._get_ocpp_module(response)
|
|
2423
|
+
self.assertIsNotNone(ocpp_module)
|
|
2424
|
+
landing_by_label = {
|
|
2425
|
+
landing.label: landing for landing in ocpp_module.enabled_landings
|
|
2426
|
+
}
|
|
2427
|
+
expected_landings = {
|
|
2428
|
+
"CPMS Online Dashboard": "/ocpp/cpms/dashboard/",
|
|
2429
|
+
"Charge Point Simulator": "/ocpp/evcs/simulator/",
|
|
2430
|
+
"RFID Tag Validator": "/ocpp/rfid/validator/",
|
|
2431
|
+
}
|
|
2432
|
+
for label, path in expected_landings.items():
|
|
2433
|
+
with self.subTest(label=label):
|
|
2434
|
+
landing = landing_by_label.get(label)
|
|
2435
|
+
self.assertIsNotNone(landing)
|
|
2436
|
+
self.assertEqual(landing.path, path)
|
|
2437
|
+
self.assertTrue(path.startswith("/"))
|
|
2438
|
+
resolve(path)
|
|
2439
|
+
|
|
2440
|
+
def test_simulator_requires_login(self):
|
|
2441
|
+
response = self.client.get(reverse("pages:index"))
|
|
2442
|
+
ocpp_module = self._get_ocpp_module(response)
|
|
2443
|
+
self.assertIsNotNone(ocpp_module)
|
|
2444
|
+
locked_landings = {
|
|
2445
|
+
landing.label: landing
|
|
2446
|
+
for landing in ocpp_module.enabled_landings
|
|
2447
|
+
if getattr(landing, "nav_is_locked", False)
|
|
2448
|
+
}
|
|
2449
|
+
simulator = locked_landings.get("Charge Point Simulator")
|
|
2450
|
+
self.assertIsNotNone(simulator)
|
|
2451
|
+
self.assertTrue(simulator.nav_is_locked)
|
|
1836
2452
|
|
|
1837
2453
|
class StaffNavVisibilityTests(TestCase):
|
|
1838
2454
|
def setUp(self):
|
|
@@ -1854,12 +2470,12 @@ class StaffNavVisibilityTests(TestCase):
|
|
|
1854
2470
|
def test_nonstaff_pill_hidden(self):
|
|
1855
2471
|
self.client.login(username="user", password="pw")
|
|
1856
2472
|
resp = self.client.get(reverse("pages:index"))
|
|
1857
|
-
self.assertContains(resp, 'href="/ocpp/"')
|
|
2473
|
+
self.assertContains(resp, 'href="/ocpp/cpms/dashboard/"')
|
|
1858
2474
|
|
|
1859
2475
|
def test_staff_sees_pill(self):
|
|
1860
2476
|
self.client.login(username="staff", password="pw")
|
|
1861
2477
|
resp = self.client.get(reverse("pages:index"))
|
|
1862
|
-
self.assertContains(resp, 'href="/ocpp/"')
|
|
2478
|
+
self.assertContains(resp, 'href="/ocpp/cpms/dashboard/"')
|
|
1863
2479
|
|
|
1864
2480
|
|
|
1865
2481
|
class ModuleAdminReloadActionTests(TestCase):
|
|
@@ -1872,7 +2488,7 @@ class ModuleAdminReloadActionTests(TestCase):
|
|
|
1872
2488
|
password="pw",
|
|
1873
2489
|
)
|
|
1874
2490
|
self.client.force_login(self.superuser)
|
|
1875
|
-
self.role, _ = NodeRole.objects.get_or_create(name="
|
|
2491
|
+
self.role, _ = NodeRole.objects.get_or_create(name="Watchtower")
|
|
1876
2492
|
Application.objects.get_or_create(name="ocpp")
|
|
1877
2493
|
Application.objects.get_or_create(name="awg")
|
|
1878
2494
|
Site.objects.update_or_create(
|
|
@@ -1910,7 +2526,11 @@ class ModuleAdminReloadActionTests(TestCase):
|
|
|
1910
2526
|
)
|
|
1911
2527
|
self.assertSetEqual(
|
|
1912
2528
|
charger_landings,
|
|
1913
|
-
{
|
|
2529
|
+
{
|
|
2530
|
+
"/ocpp/cpms/dashboard/",
|
|
2531
|
+
"/ocpp/evcs/simulator/",
|
|
2532
|
+
"/ocpp/rfid/validator/",
|
|
2533
|
+
},
|
|
1914
2534
|
)
|
|
1915
2535
|
|
|
1916
2536
|
calculator_landings = set(
|
|
@@ -2048,6 +2668,47 @@ class UserManualAdminFormTests(TestCase):
|
|
|
2048
2668
|
self.assertEqual(form.cleaned_data["content_pdf"], self.manual.content_pdf)
|
|
2049
2669
|
|
|
2050
2670
|
|
|
2671
|
+
class UserManualModelTests(TestCase):
|
|
2672
|
+
def _build_manual(self, **overrides):
|
|
2673
|
+
defaults = {
|
|
2674
|
+
"slug": "manual-model-test",
|
|
2675
|
+
"title": "Manual Model",
|
|
2676
|
+
"description": "Manual description",
|
|
2677
|
+
"languages": "en",
|
|
2678
|
+
"content_html": "<p>Manual</p>",
|
|
2679
|
+
"content_pdf": base64.b64encode(b"initial").decode("ascii"),
|
|
2680
|
+
}
|
|
2681
|
+
defaults.update(overrides)
|
|
2682
|
+
return UserManual(**defaults)
|
|
2683
|
+
|
|
2684
|
+
def test_save_encodes_uploaded_file(self):
|
|
2685
|
+
upload = SimpleUploadedFile("manual.pdf", b"PDF data")
|
|
2686
|
+
manual = self._build_manual(slug="manual-upload", content_pdf=upload)
|
|
2687
|
+
manual.save()
|
|
2688
|
+
manual.refresh_from_db()
|
|
2689
|
+
self.assertEqual(
|
|
2690
|
+
manual.content_pdf,
|
|
2691
|
+
base64.b64encode(b"PDF data").decode("ascii"),
|
|
2692
|
+
)
|
|
2693
|
+
|
|
2694
|
+
def test_save_encodes_raw_bytes(self):
|
|
2695
|
+
manual = self._build_manual(slug="manual-bytes", content_pdf=b"PDF raw")
|
|
2696
|
+
manual.save()
|
|
2697
|
+
manual.refresh_from_db()
|
|
2698
|
+
self.assertEqual(
|
|
2699
|
+
manual.content_pdf,
|
|
2700
|
+
base64.b64encode(b"PDF raw").decode("ascii"),
|
|
2701
|
+
)
|
|
2702
|
+
|
|
2703
|
+
def test_save_strips_data_uri_prefix(self):
|
|
2704
|
+
encoded = base64.b64encode(b"PDF data").decode("ascii")
|
|
2705
|
+
data_uri = f"data:application/pdf;base64,{encoded}"
|
|
2706
|
+
manual = self._build_manual(slug="manual-data-uri", content_pdf=data_uri)
|
|
2707
|
+
manual.save()
|
|
2708
|
+
manual.refresh_from_db()
|
|
2709
|
+
self.assertEqual(manual.content_pdf, encoded)
|
|
2710
|
+
|
|
2711
|
+
|
|
2051
2712
|
class LandingCreationTests(TestCase):
|
|
2052
2713
|
def setUp(self):
|
|
2053
2714
|
role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
@@ -2069,12 +2730,12 @@ class LandingCreationTests(TestCase):
|
|
|
2069
2730
|
|
|
2070
2731
|
|
|
2071
2732
|
class LandingFixtureTests(TestCase):
|
|
2072
|
-
def
|
|
2733
|
+
def test_watchtower_fixture_loads_without_duplicates(self):
|
|
2073
2734
|
from glob import glob
|
|
2074
2735
|
|
|
2075
|
-
NodeRole.objects.get_or_create(name="
|
|
2736
|
+
NodeRole.objects.get_or_create(name="Watchtower")
|
|
2076
2737
|
fixtures = glob(
|
|
2077
|
-
str(Path(settings.BASE_DIR, "pages", "fixtures", "
|
|
2738
|
+
str(Path(settings.BASE_DIR, "pages", "fixtures", "watchtower__*.json"))
|
|
2078
2739
|
)
|
|
2079
2740
|
fixtures = sorted(
|
|
2080
2741
|
fixtures,
|
|
@@ -2084,9 +2745,11 @@ class LandingFixtureTests(TestCase):
|
|
|
2084
2745
|
)
|
|
2085
2746
|
call_command("loaddata", *fixtures)
|
|
2086
2747
|
call_command("loaddata", *fixtures)
|
|
2087
|
-
module = Module.objects.get(path="/ocpp/", node_role__name="
|
|
2748
|
+
module = Module.objects.get(path="/ocpp/", node_role__name="Watchtower")
|
|
2088
2749
|
module.create_landings()
|
|
2089
|
-
self.assertEqual(
|
|
2750
|
+
self.assertEqual(
|
|
2751
|
+
module.landings.filter(path="/ocpp/rfid/validator/").count(), 1
|
|
2752
|
+
)
|
|
2090
2753
|
|
|
2091
2754
|
|
|
2092
2755
|
class AllowedHostSubnetTests(TestCase):
|
|
@@ -2239,9 +2902,9 @@ class FaviconTests(TestCase):
|
|
|
2239
2902
|
)
|
|
2240
2903
|
self.assertContains(resp, b64)
|
|
2241
2904
|
|
|
2242
|
-
def
|
|
2905
|
+
def test_watchtower_nodes_use_goldenrod_favicon(self):
|
|
2243
2906
|
with override_settings(MEDIA_ROOT=self.tmpdir):
|
|
2244
|
-
role, _ = NodeRole.objects.get_or_create(name="
|
|
2907
|
+
role, _ = NodeRole.objects.get_or_create(name="Watchtower")
|
|
2245
2908
|
Node.objects.update_or_create(
|
|
2246
2909
|
mac_address=Node.get_current_mac(),
|
|
2247
2910
|
defaults={
|
|
@@ -2256,7 +2919,7 @@ class FaviconTests(TestCase):
|
|
|
2256
2919
|
resp = self.client.get(reverse("pages:index"))
|
|
2257
2920
|
b64 = (
|
|
2258
2921
|
Path(settings.BASE_DIR)
|
|
2259
|
-
.joinpath("pages", "fixtures", "data", "
|
|
2922
|
+
.joinpath("pages", "fixtures", "data", "favicon_watchtower.txt")
|
|
2260
2923
|
.read_text()
|
|
2261
2924
|
.strip()
|
|
2262
2925
|
)
|
|
@@ -2323,6 +2986,20 @@ class FavoriteTests(TestCase):
|
|
|
2323
2986
|
self.assertEqual(fav.custom_label, "Apps")
|
|
2324
2987
|
self.assertTrue(fav.user_data)
|
|
2325
2988
|
|
|
2989
|
+
def test_add_favorite_defaults_user_data_checked(self):
|
|
2990
|
+
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2991
|
+
url = reverse("admin:favorite_toggle", args=[ct.id])
|
|
2992
|
+
resp = self.client.get(url)
|
|
2993
|
+
self.assertContains(resp, 'name="user_data" checked')
|
|
2994
|
+
|
|
2995
|
+
def test_add_favorite_with_priority(self):
|
|
2996
|
+
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2997
|
+
url = reverse("admin:favorite_toggle", args=[ct.id])
|
|
2998
|
+
resp = self.client.post(url, {"priority": "7"})
|
|
2999
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
3000
|
+
fav = Favorite.objects.get(user=self.user, content_type=ct)
|
|
3001
|
+
self.assertEqual(fav.priority, 7)
|
|
3002
|
+
|
|
2326
3003
|
def test_cancel_link_uses_next(self):
|
|
2327
3004
|
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2328
3005
|
next_url = reverse("admin:pages_application_changelist")
|
|
@@ -2332,14 +3009,31 @@ class FavoriteTests(TestCase):
|
|
|
2332
3009
|
resp = self.client.get(url)
|
|
2333
3010
|
self.assertContains(resp, f'href="{next_url}"')
|
|
2334
3011
|
|
|
2335
|
-
def
|
|
3012
|
+
def test_existing_favorite_shows_update_form(self):
|
|
2336
3013
|
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2337
|
-
Favorite.objects.create(
|
|
3014
|
+
favorite = Favorite.objects.create(
|
|
3015
|
+
user=self.user, content_type=ct, custom_label="Apps", user_data=True
|
|
3016
|
+
)
|
|
2338
3017
|
url = reverse("admin:favorite_toggle", args=[ct.id])
|
|
2339
3018
|
resp = self.client.get(url)
|
|
2340
|
-
self.
|
|
2341
|
-
|
|
2342
|
-
self.assertContains(resp,
|
|
3019
|
+
self.assertContains(resp, "Update Favorite")
|
|
3020
|
+
self.assertContains(resp, "value=\"Apps\"")
|
|
3021
|
+
self.assertContains(resp, "checked")
|
|
3022
|
+
self.assertContains(resp, "name=\"remove\"")
|
|
3023
|
+
|
|
3024
|
+
resp = self.client.post(url, {"custom_label": "Apps Updated"})
|
|
3025
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
3026
|
+
favorite.refresh_from_db()
|
|
3027
|
+
self.assertEqual(favorite.custom_label, "Apps Updated")
|
|
3028
|
+
self.assertFalse(favorite.user_data)
|
|
3029
|
+
|
|
3030
|
+
def test_remove_existing_favorite_from_toggle(self):
|
|
3031
|
+
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
3032
|
+
Favorite.objects.create(user=self.user, content_type=ct)
|
|
3033
|
+
url = reverse("admin:favorite_toggle", args=[ct.id])
|
|
3034
|
+
resp = self.client.post(url, {"remove": "1"})
|
|
3035
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
3036
|
+
self.assertFalse(Favorite.objects.filter(user=self.user, content_type=ct).exists())
|
|
2343
3037
|
|
|
2344
3038
|
def test_update_user_data_from_list(self):
|
|
2345
3039
|
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
@@ -2350,6 +3044,15 @@ class FavoriteTests(TestCase):
|
|
|
2350
3044
|
fav.refresh_from_db()
|
|
2351
3045
|
self.assertTrue(fav.user_data)
|
|
2352
3046
|
|
|
3047
|
+
def test_update_priority_from_list(self):
|
|
3048
|
+
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
3049
|
+
fav = Favorite.objects.create(user=self.user, content_type=ct, priority=3)
|
|
3050
|
+
url = reverse("admin:favorite_list")
|
|
3051
|
+
resp = self.client.post(url, {f"priority_{fav.pk}": "12"})
|
|
3052
|
+
self.assertRedirects(resp, url)
|
|
3053
|
+
fav.refresh_from_db()
|
|
3054
|
+
self.assertEqual(fav.priority, 12)
|
|
3055
|
+
|
|
2353
3056
|
def test_dashboard_includes_favorites_and_user_data(self):
|
|
2354
3057
|
fav_ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2355
3058
|
Favorite.objects.create(
|
|
@@ -2360,6 +3063,12 @@ class FavoriteTests(TestCase):
|
|
|
2360
3063
|
self.assertContains(resp, reverse("admin:pages_application_changelist"))
|
|
2361
3064
|
self.assertContains(resp, reverse("admin:nodes_noderole_changelist"))
|
|
2362
3065
|
|
|
3066
|
+
def test_dashboard_shows_empty_todo_state(self):
|
|
3067
|
+
Todo.objects.all().delete()
|
|
3068
|
+
resp = self.client.get(reverse("admin:index"))
|
|
3069
|
+
self.assertContains(resp, "Release manager tasks")
|
|
3070
|
+
self.assertContains(resp, "No pending TODOs")
|
|
3071
|
+
|
|
2363
3072
|
def test_dashboard_merges_duplicate_future_actions(self):
|
|
2364
3073
|
ct = ContentType.objects.get_for_model(NodeRole)
|
|
2365
3074
|
Favorite.objects.create(user=self.user, content_type=ct)
|
|
@@ -2406,6 +3115,48 @@ class FavoriteTests(TestCase):
|
|
|
2406
3115
|
self.assertContains(resp, f'title="{badge_label}"')
|
|
2407
3116
|
self.assertContains(resp, f'aria-label="{badge_label}"')
|
|
2408
3117
|
|
|
3118
|
+
def test_dashboard_shows_charge_point_availability_badge(self):
|
|
3119
|
+
Charger.objects.create(
|
|
3120
|
+
charger_id="CP-001", connector_id=1, last_status="Available"
|
|
3121
|
+
)
|
|
3122
|
+
Charger.objects.create(charger_id="CP-002", last_status="Available")
|
|
3123
|
+
Charger.objects.create(
|
|
3124
|
+
charger_id="CP-003", connector_id=1, last_status="Unavailable"
|
|
3125
|
+
)
|
|
3126
|
+
|
|
3127
|
+
resp = self.client.get(reverse("admin:index"))
|
|
3128
|
+
|
|
3129
|
+
expected = "1 / 2"
|
|
3130
|
+
badge_label = gettext(
|
|
3131
|
+
"%(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."
|
|
3132
|
+
) % {"available": 1, "total": 2, "missing": 1}
|
|
3133
|
+
|
|
3134
|
+
self.assertContains(resp, expected)
|
|
3135
|
+
self.assertContains(resp, 'class="charger-availability-badge"')
|
|
3136
|
+
self.assertContains(resp, f'title="{badge_label}"')
|
|
3137
|
+
self.assertContains(resp, f'aria-label="{badge_label}"')
|
|
3138
|
+
|
|
3139
|
+
def test_dashboard_charge_point_badge_ignores_aggregator(self):
|
|
3140
|
+
Charger.objects.create(charger_id="CP-AGG", last_status="Available")
|
|
3141
|
+
Charger.objects.create(
|
|
3142
|
+
charger_id="CP-AGG", connector_id=1, last_status="Available"
|
|
3143
|
+
)
|
|
3144
|
+
Charger.objects.create(
|
|
3145
|
+
charger_id="CP-AGG", connector_id=2, last_status="Available"
|
|
3146
|
+
)
|
|
3147
|
+
|
|
3148
|
+
resp = self.client.get(reverse("admin:index"))
|
|
3149
|
+
|
|
3150
|
+
expected = "2 / 2"
|
|
3151
|
+
badge_label = gettext(
|
|
3152
|
+
"%(available)s chargers reporting Available status with a CP number."
|
|
3153
|
+
) % {"available": 2}
|
|
3154
|
+
|
|
3155
|
+
self.assertContains(resp, expected)
|
|
3156
|
+
self.assertContains(resp, 'class="charger-availability-badge"')
|
|
3157
|
+
self.assertContains(resp, f'title="{badge_label}"')
|
|
3158
|
+
self.assertContains(resp, f'aria-label="{badge_label}"')
|
|
3159
|
+
|
|
2409
3160
|
def test_nav_sidebar_hides_dashboard_badges(self):
|
|
2410
3161
|
InviteLead.objects.create(email="open@example.com")
|
|
2411
3162
|
RFID.objects.create(rfid="RFID0003", released=True, allowed=True)
|
|
@@ -2508,7 +3259,7 @@ class FavoriteTests(TestCase):
|
|
|
2508
3259
|
hostname="cached-node",
|
|
2509
3260
|
address="127.0.0.1",
|
|
2510
3261
|
mac_address="AA:BB:CC:DD:EE:FF",
|
|
2511
|
-
port=
|
|
3262
|
+
port=8888,
|
|
2512
3263
|
is_user_data=True,
|
|
2513
3264
|
)
|
|
2514
3265
|
|
|
@@ -2557,7 +3308,11 @@ class FavoriteTests(TestCase):
|
|
|
2557
3308
|
todo = Todo.objects.create(request="Do thing")
|
|
2558
3309
|
resp = self.client.get(reverse("admin:index"))
|
|
2559
3310
|
done_url = reverse("todo-done", args=[todo.pk])
|
|
2560
|
-
|
|
3311
|
+
tooltip = escape(todo.request)
|
|
3312
|
+
self.assertContains(resp, f'title="{tooltip}"')
|
|
3313
|
+
self.assertContains(resp, f'aria-label="{tooltip}"')
|
|
3314
|
+
task_label = gettext("Task %(counter)s") % {"counter": 1}
|
|
3315
|
+
self.assertContains(resp, task_label)
|
|
2561
3316
|
self.assertContains(resp, f'action="{done_url}"')
|
|
2562
3317
|
self.assertContains(resp, "DONE")
|
|
2563
3318
|
|
|
@@ -2568,6 +3323,15 @@ class FavoriteTests(TestCase):
|
|
|
2568
3323
|
resp, '<div class="todo-details">More info</div>', html=True
|
|
2569
3324
|
)
|
|
2570
3325
|
|
|
3326
|
+
def test_dashboard_hides_completed_todos(self):
|
|
3327
|
+
todo = Todo.objects.create(request="Completed task")
|
|
3328
|
+
Todo.objects.filter(pk=todo.pk).update(done_on=timezone.now())
|
|
3329
|
+
|
|
3330
|
+
resp = self.client.get(reverse("admin:index"))
|
|
3331
|
+
|
|
3332
|
+
self.assertNotContains(resp, todo.request)
|
|
3333
|
+
self.assertNotContains(resp, "Completed")
|
|
3334
|
+
|
|
2571
3335
|
def test_dashboard_shows_todos_when_node_unknown(self):
|
|
2572
3336
|
Todo.objects.create(request="Check fallback")
|
|
2573
3337
|
from nodes.models import Node
|
|
@@ -2576,7 +3340,8 @@ class FavoriteTests(TestCase):
|
|
|
2576
3340
|
|
|
2577
3341
|
resp = self.client.get(reverse("admin:index"))
|
|
2578
3342
|
self.assertContains(resp, "Release manager tasks")
|
|
2579
|
-
|
|
3343
|
+
tooltip = escape("Check fallback")
|
|
3344
|
+
self.assertContains(resp, f'title="{tooltip}"')
|
|
2580
3345
|
|
|
2581
3346
|
def test_dashboard_shows_todos_without_release_manager_profile(self):
|
|
2582
3347
|
Todo.objects.create(request="Unrestricted task")
|
|
@@ -2584,7 +3349,8 @@ class FavoriteTests(TestCase):
|
|
|
2584
3349
|
|
|
2585
3350
|
resp = self.client.get(reverse("admin:index"))
|
|
2586
3351
|
self.assertContains(resp, "Release manager tasks")
|
|
2587
|
-
|
|
3352
|
+
tooltip = escape("Unrestricted task")
|
|
3353
|
+
self.assertContains(resp, f'title="{tooltip}"')
|
|
2588
3354
|
|
|
2589
3355
|
def test_dashboard_excludes_todo_changelist_link(self):
|
|
2590
3356
|
ct = ContentType.objects.get_for_model(Todo)
|
|
@@ -2608,7 +3374,8 @@ class FavoriteTests(TestCase):
|
|
|
2608
3374
|
self.client.force_login(other_user)
|
|
2609
3375
|
resp = self.client.get(reverse("admin:index"))
|
|
2610
3376
|
self.assertContains(resp, "Release manager tasks")
|
|
2611
|
-
|
|
3377
|
+
tooltip = escape(todo.request)
|
|
3378
|
+
self.assertContains(resp, f'title="{tooltip}"')
|
|
2612
3379
|
|
|
2613
3380
|
def test_dashboard_shows_todos_for_non_terminal_node(self):
|
|
2614
3381
|
todo = Todo.objects.create(request="Terminal Tasks")
|
|
@@ -2619,7 +3386,8 @@ class FavoriteTests(TestCase):
|
|
|
2619
3386
|
self.node.save(update_fields=["role"])
|
|
2620
3387
|
resp = self.client.get(reverse("admin:index"))
|
|
2621
3388
|
self.assertContains(resp, "Release manager tasks")
|
|
2622
|
-
|
|
3389
|
+
tooltip = escape(todo.request)
|
|
3390
|
+
self.assertContains(resp, f'title="{tooltip}"')
|
|
2623
3391
|
|
|
2624
3392
|
def test_dashboard_shows_todos_for_delegate_release_manager(self):
|
|
2625
3393
|
todo = Todo.objects.create(request="Delegate Task")
|
|
@@ -2641,7 +3409,8 @@ class FavoriteTests(TestCase):
|
|
|
2641
3409
|
self.client.force_login(operator)
|
|
2642
3410
|
resp = self.client.get(reverse("admin:index"))
|
|
2643
3411
|
self.assertContains(resp, "Release manager tasks")
|
|
2644
|
-
|
|
3412
|
+
tooltip = escape(todo.request)
|
|
3413
|
+
self.assertContains(resp, f'title="{tooltip}"')
|
|
2645
3414
|
|
|
2646
3415
|
|
|
2647
3416
|
class AdminIndexQueryRegressionTests(TestCase):
|
|
@@ -2836,47 +3605,6 @@ class AdminModelGraphViewTests(TestCase):
|
|
|
2836
3605
|
self.assertEqual(kwargs.get("format"), "pdf")
|
|
2837
3606
|
|
|
2838
3607
|
|
|
2839
|
-
class DatasetteTests(TestCase):
|
|
2840
|
-
def setUp(self):
|
|
2841
|
-
self.client = Client()
|
|
2842
|
-
User = get_user_model()
|
|
2843
|
-
self.user = User.objects.create_user(username="ds", password="pwd")
|
|
2844
|
-
Site.objects.update_or_create(id=1, defaults={"name": "Terminal"})
|
|
2845
|
-
|
|
2846
|
-
def test_datasette_auth_endpoint(self):
|
|
2847
|
-
resp = self.client.get(reverse("pages:datasette-auth"))
|
|
2848
|
-
self.assertEqual(resp.status_code, 401)
|
|
2849
|
-
self.client.force_login(self.user)
|
|
2850
|
-
resp = self.client.get(reverse("pages:datasette-auth"))
|
|
2851
|
-
self.assertEqual(resp.status_code, 200)
|
|
2852
|
-
|
|
2853
|
-
def test_navbar_includes_datasette_when_enabled(self):
|
|
2854
|
-
lock_dir = Path(settings.BASE_DIR) / "locks"
|
|
2855
|
-
lock_dir.mkdir(exist_ok=True)
|
|
2856
|
-
lock_file = lock_dir / "datasette.lck"
|
|
2857
|
-
try:
|
|
2858
|
-
lock_file.touch()
|
|
2859
|
-
resp = self.client.get(reverse("pages:index"))
|
|
2860
|
-
self.assertContains(resp, 'href="/data/"')
|
|
2861
|
-
finally:
|
|
2862
|
-
lock_file.unlink(missing_ok=True)
|
|
2863
|
-
|
|
2864
|
-
def test_admin_home_includes_datasette_button_when_enabled(self):
|
|
2865
|
-
lock_dir = Path(settings.BASE_DIR) / "locks"
|
|
2866
|
-
lock_dir.mkdir(exist_ok=True)
|
|
2867
|
-
lock_file = lock_dir / "datasette.lck"
|
|
2868
|
-
try:
|
|
2869
|
-
lock_file.touch()
|
|
2870
|
-
self.user.is_staff = True
|
|
2871
|
-
self.user.is_superuser = True
|
|
2872
|
-
self.user.save()
|
|
2873
|
-
self.client.force_login(self.user)
|
|
2874
|
-
resp = self.client.get(reverse("admin:index"))
|
|
2875
|
-
self.assertContains(resp, 'href="/data/"')
|
|
2876
|
-
self.assertContains(resp, ">Datasette<")
|
|
2877
|
-
finally:
|
|
2878
|
-
lock_file.unlink(missing_ok=True)
|
|
2879
|
-
|
|
2880
3608
|
|
|
2881
3609
|
class UserStorySubmissionTests(TestCase):
|
|
2882
3610
|
def setUp(self):
|
|
@@ -2885,6 +3613,14 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2885
3613
|
self.url = reverse("pages:user-story-submit")
|
|
2886
3614
|
User = get_user_model()
|
|
2887
3615
|
self.user = User.objects.create_user(username="feedbacker", password="pwd")
|
|
3616
|
+
self.capture_patcher = patch("pages.views.capture_screenshot", autospec=True)
|
|
3617
|
+
self.save_patcher = patch("pages.views.save_screenshot", autospec=True)
|
|
3618
|
+
self.mock_capture = self.capture_patcher.start()
|
|
3619
|
+
self.mock_save = self.save_patcher.start()
|
|
3620
|
+
self.mock_capture.return_value = Path("/tmp/fake.png")
|
|
3621
|
+
self.mock_save.return_value = None
|
|
3622
|
+
self.addCleanup(self.capture_patcher.stop)
|
|
3623
|
+
self.addCleanup(self.save_patcher.stop)
|
|
2888
3624
|
|
|
2889
3625
|
def test_authenticated_submission_defaults_to_username(self):
|
|
2890
3626
|
self.client.force_login(self.user)
|
|
@@ -2913,12 +3649,121 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2913
3649
|
self.assertEqual(story.referer, "https://example.test/wizard/step-1/")
|
|
2914
3650
|
self.assertEqual(story.user_agent, "FeedbackBot/1.0")
|
|
2915
3651
|
self.assertEqual(story.ip_address, "127.0.0.1")
|
|
3652
|
+
expected_language = (translation.get_language() or "").split("-")[0]
|
|
3653
|
+
self.assertTrue(story.language_code)
|
|
3654
|
+
self.assertTrue(
|
|
3655
|
+
story.language_code.startswith(expected_language),
|
|
3656
|
+
story.language_code,
|
|
3657
|
+
)
|
|
3658
|
+
|
|
3659
|
+
def test_submission_records_request_language(self):
|
|
3660
|
+
self.client.cookies[settings.LANGUAGE_COOKIE_NAME] = "es"
|
|
3661
|
+
with translation.override("es"):
|
|
3662
|
+
response = self.client.post(
|
|
3663
|
+
self.url,
|
|
3664
|
+
{
|
|
3665
|
+
"rating": 4,
|
|
3666
|
+
"comments": "Buena experiencia",
|
|
3667
|
+
"path": "/es/soporte/",
|
|
3668
|
+
"take_screenshot": "1",
|
|
3669
|
+
},
|
|
3670
|
+
HTTP_ACCEPT_LANGUAGE="es",
|
|
3671
|
+
)
|
|
3672
|
+
|
|
3673
|
+
self.assertEqual(response.status_code, 200)
|
|
3674
|
+
story = UserStory.objects.get()
|
|
3675
|
+
self.assertEqual(story.language_code, "es")
|
|
2916
3676
|
|
|
2917
|
-
def
|
|
3677
|
+
def test_submission_prefers_original_referer(self):
|
|
3678
|
+
self.client.get(
|
|
3679
|
+
reverse("pages:index"),
|
|
3680
|
+
HTTP_REFERER="https://ads.example/original",
|
|
3681
|
+
)
|
|
2918
3682
|
response = self.client.post(
|
|
2919
3683
|
self.url,
|
|
2920
3684
|
{
|
|
2921
|
-
"
|
|
3685
|
+
"rating": 3,
|
|
3686
|
+
"comments": "Works well",
|
|
3687
|
+
"path": "/wizard/step-2/",
|
|
3688
|
+
"name": "visitor@example.com",
|
|
3689
|
+
"take_screenshot": "0",
|
|
3690
|
+
},
|
|
3691
|
+
HTTP_REFERER="http://testserver/wizard/step-2/",
|
|
3692
|
+
HTTP_USER_AGENT="FeedbackBot/2.0",
|
|
3693
|
+
)
|
|
3694
|
+
|
|
3695
|
+
self.assertEqual(response.status_code, 200)
|
|
3696
|
+
story = UserStory.objects.get()
|
|
3697
|
+
self.assertEqual(story.referer, "https://ads.example/original")
|
|
3698
|
+
|
|
3699
|
+
def test_superuser_submission_creates_triage_todo(self):
|
|
3700
|
+
Todo.objects.all().delete()
|
|
3701
|
+
superuser = get_user_model().objects.create_superuser(
|
|
3702
|
+
username="overseer", email="overseer@example.com", password="pwd"
|
|
3703
|
+
)
|
|
3704
|
+
Node.objects.update_or_create(
|
|
3705
|
+
mac_address=Node.get_current_mac(),
|
|
3706
|
+
defaults={
|
|
3707
|
+
"hostname": "local-node",
|
|
3708
|
+
"address": "127.0.0.1",
|
|
3709
|
+
"port": 8888,
|
|
3710
|
+
"public_endpoint": "local-node",
|
|
3711
|
+
},
|
|
3712
|
+
)
|
|
3713
|
+
self.client.force_login(superuser)
|
|
3714
|
+
comments = "Review analytics dashboard flow"
|
|
3715
|
+
response = self.client.post(
|
|
3716
|
+
self.url,
|
|
3717
|
+
{
|
|
3718
|
+
"rating": 5,
|
|
3719
|
+
"comments": comments,
|
|
3720
|
+
"path": "/reports/analytics/",
|
|
3721
|
+
"take_screenshot": "0",
|
|
3722
|
+
},
|
|
3723
|
+
)
|
|
3724
|
+
self.assertEqual(response.status_code, 200)
|
|
3725
|
+
self.assertEqual(Todo.objects.count(), 1)
|
|
3726
|
+
todo = Todo.objects.get()
|
|
3727
|
+
self.assertEqual(todo.request, f"Triage {comments}")
|
|
3728
|
+
self.assertTrue(todo.is_user_data)
|
|
3729
|
+
self.assertEqual(todo.original_user, superuser)
|
|
3730
|
+
self.assertTrue(todo.original_user_is_authenticated)
|
|
3731
|
+
self.assertEqual(todo.origin_node, Node.get_local())
|
|
3732
|
+
|
|
3733
|
+
def test_screenshot_request_links_saved_sample(self):
|
|
3734
|
+
self.client.force_login(self.user)
|
|
3735
|
+
screenshot_file = Path("/tmp/fake.png")
|
|
3736
|
+
self.mock_capture.return_value = screenshot_file
|
|
3737
|
+
sample = ContentSample.objects.create(kind=ContentSample.IMAGE)
|
|
3738
|
+
self.mock_save.return_value = sample
|
|
3739
|
+
|
|
3740
|
+
response = self.client.post(
|
|
3741
|
+
self.url,
|
|
3742
|
+
{
|
|
3743
|
+
"rating": 5,
|
|
3744
|
+
"comments": "Loved the experience!",
|
|
3745
|
+
"path": "/wizard/step-1/",
|
|
3746
|
+
"take_screenshot": "1",
|
|
3747
|
+
},
|
|
3748
|
+
HTTP_REFERER="https://example.test/wizard/step-1/",
|
|
3749
|
+
)
|
|
3750
|
+
|
|
3751
|
+
self.assertEqual(response.status_code, 200)
|
|
3752
|
+
story = UserStory.objects.get()
|
|
3753
|
+
self.assertEqual(story.screenshot, sample)
|
|
3754
|
+
self.mock_capture.assert_called_once_with("https://example.test/wizard/step-1/")
|
|
3755
|
+
self.mock_save.assert_called_once_with(
|
|
3756
|
+
screenshot_file,
|
|
3757
|
+
method="USER_STORY",
|
|
3758
|
+
user=self.user,
|
|
3759
|
+
link_duplicates=True,
|
|
3760
|
+
)
|
|
3761
|
+
|
|
3762
|
+
def test_anonymous_submission_uses_provided_email(self):
|
|
3763
|
+
response = self.client.post(
|
|
3764
|
+
self.url,
|
|
3765
|
+
{
|
|
3766
|
+
"name": "guest@example.com",
|
|
2922
3767
|
"rating": 3,
|
|
2923
3768
|
"comments": "It was fine.",
|
|
2924
3769
|
"path": "/status/",
|
|
@@ -2928,7 +3773,7 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2928
3773
|
self.assertEqual(response.status_code, 200)
|
|
2929
3774
|
self.assertEqual(UserStory.objects.count(), 1)
|
|
2930
3775
|
story = UserStory.objects.get()
|
|
2931
|
-
self.assertEqual(story.name, "
|
|
3776
|
+
self.assertEqual(story.name, "guest@example.com")
|
|
2932
3777
|
self.assertIsNone(story.user)
|
|
2933
3778
|
self.assertIsNone(story.owner)
|
|
2934
3779
|
self.assertEqual(story.comments, "It was fine.")
|
|
@@ -2950,7 +3795,7 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2950
3795
|
self.assertFalse(UserStory.objects.exists())
|
|
2951
3796
|
self.assertIn("rating", data.get("errors", {}))
|
|
2952
3797
|
|
|
2953
|
-
def
|
|
3798
|
+
def test_anonymous_submission_without_email_returns_errors(self):
|
|
2954
3799
|
response = self.client.post(
|
|
2955
3800
|
self.url,
|
|
2956
3801
|
{
|
|
@@ -2960,18 +3805,32 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2960
3805
|
"take_screenshot": "1",
|
|
2961
3806
|
},
|
|
2962
3807
|
)
|
|
2963
|
-
self.assertEqual(response.status_code,
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
self.
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
self.
|
|
3808
|
+
self.assertEqual(response.status_code, 400)
|
|
3809
|
+
self.assertFalse(UserStory.objects.exists())
|
|
3810
|
+
data = response.json()
|
|
3811
|
+
self.assertIn("name", data.get("errors", {}))
|
|
3812
|
+
|
|
3813
|
+
def test_anonymous_submission_with_invalid_email_returns_errors(self):
|
|
3814
|
+
response = self.client.post(
|
|
3815
|
+
self.url,
|
|
3816
|
+
{
|
|
3817
|
+
"name": "Guest Reviewer",
|
|
3818
|
+
"rating": 3,
|
|
3819
|
+
"comments": "Needs improvement.",
|
|
3820
|
+
"path": "/feedback/",
|
|
3821
|
+
"take_screenshot": "1",
|
|
3822
|
+
},
|
|
3823
|
+
)
|
|
3824
|
+
self.assertEqual(response.status_code, 400)
|
|
3825
|
+
self.assertFalse(UserStory.objects.exists())
|
|
3826
|
+
data = response.json()
|
|
3827
|
+
self.assertIn("name", data.get("errors", {}))
|
|
2970
3828
|
|
|
2971
3829
|
def test_submission_without_screenshot_request(self):
|
|
2972
3830
|
response = self.client.post(
|
|
2973
3831
|
self.url,
|
|
2974
3832
|
{
|
|
3833
|
+
"name": "guest@example.com",
|
|
2975
3834
|
"rating": 4,
|
|
2976
3835
|
"comments": "Skip the screenshot, please.",
|
|
2977
3836
|
"path": "/feedback/",
|
|
@@ -2981,9 +3840,14 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2981
3840
|
story = UserStory.objects.get()
|
|
2982
3841
|
self.assertFalse(story.take_screenshot)
|
|
2983
3842
|
self.assertIsNone(story.owner)
|
|
3843
|
+
self.assertIsNone(story.screenshot)
|
|
3844
|
+
self.assertEqual(story.status, UserStory.Status.OPEN)
|
|
3845
|
+
self.mock_capture.assert_not_called()
|
|
3846
|
+
self.mock_save.assert_not_called()
|
|
2984
3847
|
|
|
2985
3848
|
def test_rate_limit_blocks_repeated_submissions(self):
|
|
2986
3849
|
payload = {
|
|
3850
|
+
"name": "guest@example.com",
|
|
2987
3851
|
"rating": 4,
|
|
2988
3852
|
"comments": "Pretty good",
|
|
2989
3853
|
"path": "/feedback/",
|
|
@@ -3084,6 +3948,8 @@ class UserStoryAdminActionTests(TestCase):
|
|
|
3084
3948
|
comments="Helpful notes",
|
|
3085
3949
|
take_screenshot=True,
|
|
3086
3950
|
)
|
|
3951
|
+
self.story.language_code = "es"
|
|
3952
|
+
self.story.save(update_fields=["language_code"])
|
|
3087
3953
|
self.admin = UserStoryAdmin(UserStory, admin.site)
|
|
3088
3954
|
|
|
3089
3955
|
def _build_request(self):
|
|
@@ -3117,6 +3983,8 @@ class UserStoryAdminActionTests(TestCase):
|
|
|
3117
3983
|
args, kwargs = mock_create_issue.call_args
|
|
3118
3984
|
self.assertIn("Feedback for", args[0])
|
|
3119
3985
|
self.assertIn("**Rating:**", args[1])
|
|
3986
|
+
self.assertIn("**Language:**", args[1])
|
|
3987
|
+
self.assertIn("(es)", args[1])
|
|
3120
3988
|
self.assertEqual(kwargs.get("labels"), ["feedback"])
|
|
3121
3989
|
self.assertEqual(
|
|
3122
3990
|
kwargs.get("fingerprint"), f"user-story:{self.story.pk}"
|
|
@@ -3134,6 +4002,40 @@ class UserStoryAdminActionTests(TestCase):
|
|
|
3134
4002
|
|
|
3135
4003
|
mock_create_issue.assert_not_called()
|
|
3136
4004
|
|
|
4005
|
+
def test_create_github_issues_action_links_to_credentials_when_missing(self):
|
|
4006
|
+
request = self._build_request()
|
|
4007
|
+
queryset = UserStory.objects.filter(pk=self.story.pk)
|
|
4008
|
+
|
|
4009
|
+
mock_url = "/admin/core/releasemanager/"
|
|
4010
|
+
with (
|
|
4011
|
+
patch(
|
|
4012
|
+
"pages.admin.reverse", return_value=mock_url
|
|
4013
|
+
) as mock_reverse,
|
|
4014
|
+
patch.object(
|
|
4015
|
+
UserStory,
|
|
4016
|
+
"create_github_issue",
|
|
4017
|
+
side_effect=RuntimeError("GitHub token is not configured"),
|
|
4018
|
+
),
|
|
4019
|
+
):
|
|
4020
|
+
self.admin.create_github_issues(request, queryset)
|
|
4021
|
+
|
|
4022
|
+
messages_list = list(request._messages)
|
|
4023
|
+
self.assertTrue(messages_list)
|
|
4024
|
+
|
|
4025
|
+
opts = ReleaseManager._meta
|
|
4026
|
+
mock_reverse.assert_called_once_with(
|
|
4027
|
+
f"{self.admin.admin_site.name}:{opts.app_label}_{opts.model_name}_changelist"
|
|
4028
|
+
)
|
|
4029
|
+
self.assertTrue(
|
|
4030
|
+
any(mock_url in message.message for message in messages_list),
|
|
4031
|
+
)
|
|
4032
|
+
self.assertTrue(
|
|
4033
|
+
any("Configure GitHub credentials" in message.message for message in messages_list),
|
|
4034
|
+
)
|
|
4035
|
+
self.assertTrue(
|
|
4036
|
+
any(message.level == messages.ERROR for message in messages_list),
|
|
4037
|
+
)
|
|
4038
|
+
|
|
3137
4039
|
|
|
3138
4040
|
class ClientReportLiveUpdateTests(TestCase):
|
|
3139
4041
|
def setUp(self):
|
|
@@ -3141,9 +4043,31 @@ class ClientReportLiveUpdateTests(TestCase):
|
|
|
3141
4043
|
|
|
3142
4044
|
def test_client_report_includes_interval(self):
|
|
3143
4045
|
resp = self.client.get(reverse("pages:client-report"))
|
|
3144
|
-
self.assertEqual(resp.
|
|
4046
|
+
self.assertEqual(resp.wsgi_request.live_update_interval, 5)
|
|
3145
4047
|
self.assertContains(resp, "setInterval(() => location.reload()")
|
|
3146
4048
|
|
|
4049
|
+
def test_client_report_download_disables_refresh(self):
|
|
4050
|
+
User = get_user_model()
|
|
4051
|
+
user = User.objects.create_user(username="download-user", password="pwd")
|
|
4052
|
+
report = ClientReport.objects.create(
|
|
4053
|
+
start_date=date(2024, 1, 1),
|
|
4054
|
+
end_date=date(2024, 1, 2),
|
|
4055
|
+
data={},
|
|
4056
|
+
owner=user,
|
|
4057
|
+
disable_emails=True,
|
|
4058
|
+
language="en",
|
|
4059
|
+
title="",
|
|
4060
|
+
)
|
|
4061
|
+
|
|
4062
|
+
self.client.force_login(user)
|
|
4063
|
+
resp = self.client.get(
|
|
4064
|
+
reverse("pages:client-report"), {"download": report.pk}
|
|
4065
|
+
)
|
|
4066
|
+
|
|
4067
|
+
self.assertIsNone(getattr(resp.wsgi_request, "live_update_interval", None))
|
|
4068
|
+
self.assertContains(resp, "report-download-frame")
|
|
4069
|
+
self.assertNotContains(resp, "setInterval(() => location.reload()")
|
|
4070
|
+
|
|
3147
4071
|
|
|
3148
4072
|
class ScreenshotSpecInfrastructureTests(TestCase):
|
|
3149
4073
|
def test_runner_creates_outputs_and_cleans_old_samples(self):
|