arthexis 0.1.7__py3-none-any.whl → 0.1.9__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.9.dist-info/METADATA +168 -0
- arthexis-0.1.9.dist-info/RECORD +92 -0
- arthexis-0.1.9.dist-info/licenses/LICENSE +674 -0
- config/__init__.py +0 -1
- config/auth_app.py +0 -1
- config/celery.py +1 -2
- config/context_processors.py +1 -1
- config/offline.py +2 -0
- config/settings.py +134 -16
- config/urls.py +71 -3
- core/admin.py +1331 -165
- core/admin_history.py +50 -0
- core/admindocs.py +151 -0
- core/apps.py +158 -3
- core/backends.py +46 -4
- core/entity.py +62 -48
- core/fields.py +6 -1
- core/github_helper.py +25 -0
- core/github_issues.py +172 -0
- core/lcd_screen.py +1 -0
- core/liveupdate.py +25 -0
- core/log_paths.py +100 -0
- core/mailer.py +83 -0
- core/middleware.py +57 -0
- core/models.py +1136 -259
- core/notifications.py +11 -1
- core/public_wifi.py +227 -0
- core/release.py +27 -20
- core/sigil_builder.py +131 -0
- core/sigil_context.py +20 -0
- core/sigil_resolver.py +284 -0
- core/system.py +129 -10
- core/tasks.py +118 -19
- core/test_system_info.py +22 -0
- core/tests.py +445 -58
- core/tests_liveupdate.py +17 -0
- core/urls.py +2 -2
- core/user_data.py +329 -167
- core/views.py +383 -57
- core/widgets.py +51 -0
- core/workgroup_urls.py +17 -0
- core/workgroup_views.py +94 -0
- nodes/actions.py +0 -2
- nodes/admin.py +159 -284
- nodes/apps.py +9 -15
- nodes/backends.py +53 -0
- nodes/lcd.py +24 -10
- nodes/models.py +375 -178
- nodes/tasks.py +1 -5
- nodes/tests.py +524 -129
- nodes/utils.py +13 -2
- nodes/views.py +66 -23
- ocpp/admin.py +150 -61
- ocpp/apps.py +4 -3
- ocpp/consumers.py +432 -69
- ocpp/evcs.py +25 -8
- ocpp/models.py +408 -68
- ocpp/simulator.py +13 -6
- ocpp/store.py +258 -30
- ocpp/tasks.py +11 -7
- ocpp/test_export_import.py +8 -7
- ocpp/test_rfid.py +211 -16
- ocpp/tests.py +1198 -135
- ocpp/transactions_io.py +68 -22
- ocpp/urls.py +35 -2
- ocpp/views.py +654 -101
- pages/admin.py +173 -13
- pages/checks.py +0 -1
- pages/context_processors.py +19 -6
- pages/middleware.py +153 -0
- pages/models.py +37 -9
- pages/tests.py +759 -40
- pages/urls.py +3 -0
- pages/utils.py +0 -1
- pages/views.py +576 -25
- arthexis-0.1.7.dist-info/METADATA +0 -126
- arthexis-0.1.7.dist-info/RECORD +0 -77
- arthexis-0.1.7.dist-info/licenses/LICENSE +0 -21
- config/workgroup_app.py +0 -7
- core/checks.py +0 -29
- {arthexis-0.1.7.dist-info → arthexis-0.1.9.dist-info}/WHEEL +0 -0
- {arthexis-0.1.7.dist-info → arthexis-0.1.9.dist-info}/top_level.txt +0 -0
pages/tests.py
CHANGED
|
@@ -6,24 +6,42 @@ from django.contrib.sites.models import Site
|
|
|
6
6
|
from django.contrib import admin
|
|
7
7
|
from django.core.exceptions import DisallowedHost
|
|
8
8
|
import socket
|
|
9
|
-
from pages.models import Application, Module, SiteBadge, Favorite
|
|
10
|
-
from core.user_data import UserDatum
|
|
9
|
+
from pages.models import Application, Module, SiteBadge, Favorite, ViewHistory
|
|
11
10
|
from pages.admin import ApplicationAdmin
|
|
11
|
+
from pages.screenshot_specs import (
|
|
12
|
+
ScreenshotSpec,
|
|
13
|
+
ScreenshotSpecRunner,
|
|
14
|
+
ScreenshotUnavailable,
|
|
15
|
+
registry,
|
|
16
|
+
)
|
|
12
17
|
from django.apps import apps as django_apps
|
|
13
|
-
from core
|
|
18
|
+
from core import mailer
|
|
19
|
+
from core.models import AdminHistory, InviteLead, Package, ReleaseManager, Todo
|
|
14
20
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
15
21
|
import base64
|
|
16
22
|
import tempfile
|
|
17
23
|
import shutil
|
|
24
|
+
from io import StringIO
|
|
18
25
|
from django.conf import settings
|
|
19
26
|
from pathlib import Path
|
|
20
|
-
from unittest.mock import patch
|
|
21
|
-
from
|
|
27
|
+
from unittest.mock import patch, Mock
|
|
28
|
+
from types import SimpleNamespace
|
|
22
29
|
from django.core.management import call_command
|
|
23
30
|
import re
|
|
24
31
|
from django.contrib.contenttypes.models import ContentType
|
|
32
|
+
from datetime import date, timedelta
|
|
33
|
+
from django.core import mail
|
|
34
|
+
from django.utils import timezone
|
|
35
|
+
from django.utils.text import slugify
|
|
25
36
|
|
|
26
|
-
from nodes.models import
|
|
37
|
+
from nodes.models import (
|
|
38
|
+
EmailOutbox,
|
|
39
|
+
Node,
|
|
40
|
+
ContentSample,
|
|
41
|
+
NodeRole,
|
|
42
|
+
NodeFeature,
|
|
43
|
+
NodeFeatureAssignment,
|
|
44
|
+
)
|
|
27
45
|
|
|
28
46
|
|
|
29
47
|
class LoginViewTests(TestCase):
|
|
@@ -66,7 +84,21 @@ class LoginViewTests(TestCase):
|
|
|
66
84
|
)
|
|
67
85
|
self.assertRedirects(resp, "/nodes/list/")
|
|
68
86
|
|
|
87
|
+
@override_settings(EMAIL_BACKEND="django.core.mail.backends.dummy.EmailBackend")
|
|
88
|
+
def test_login_page_hides_request_link_without_email_backend(self):
|
|
89
|
+
resp = self.client.get(reverse("pages:login"))
|
|
90
|
+
self.assertFalse(resp.context["can_request_invite"])
|
|
91
|
+
self.assertNotContains(resp, reverse("pages:request-invite"))
|
|
92
|
+
|
|
93
|
+
@override_settings(EMAIL_BACKEND="django.core.mail.backends.dummy.EmailBackend")
|
|
94
|
+
def test_login_page_shows_request_link_when_outbox_configured(self):
|
|
95
|
+
EmailOutbox.objects.create(host="smtp.example.com")
|
|
96
|
+
resp = self.client.get(reverse("pages:login"))
|
|
97
|
+
self.assertTrue(resp.context["can_request_invite"])
|
|
98
|
+
self.assertContains(resp, reverse("pages:request-invite"))
|
|
69
99
|
|
|
100
|
+
|
|
101
|
+
@override_settings(EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend")
|
|
70
102
|
class InvitationTests(TestCase):
|
|
71
103
|
def setUp(self):
|
|
72
104
|
self.client = Client()
|
|
@@ -110,14 +142,27 @@ class InvitationTests(TestCase):
|
|
|
110
142
|
self.assertIn("_auth_user_id", self.client.session)
|
|
111
143
|
|
|
112
144
|
def test_request_invite_handles_email_errors(self):
|
|
113
|
-
with patch("pages.views.
|
|
145
|
+
with patch("pages.views.mailer.send", side_effect=Exception("fail")):
|
|
114
146
|
resp = self.client.post(
|
|
115
147
|
reverse("pages:request-invite"), {"email": "invite@example.com"}
|
|
116
148
|
)
|
|
117
149
|
self.assertEqual(resp.status_code, 200)
|
|
118
|
-
self.assertContains(
|
|
119
|
-
|
|
150
|
+
self.assertContains(resp, "If the email exists, an invitation has been sent.")
|
|
151
|
+
lead = InviteLead.objects.get()
|
|
152
|
+
self.assertIsNone(lead.sent_on)
|
|
153
|
+
self.assertIn("fail", lead.error)
|
|
154
|
+
self.assertIn("email service", lead.error)
|
|
155
|
+
self.assertEqual(len(mail.outbox), 0)
|
|
156
|
+
|
|
157
|
+
def test_request_invite_records_send_time(self):
|
|
158
|
+
resp = self.client.post(
|
|
159
|
+
reverse("pages:request-invite"), {"email": "invite@example.com"}
|
|
120
160
|
)
|
|
161
|
+
self.assertEqual(resp.status_code, 200)
|
|
162
|
+
lead = InviteLead.objects.get()
|
|
163
|
+
self.assertIsNotNone(lead.sent_on)
|
|
164
|
+
self.assertEqual(lead.error, "")
|
|
165
|
+
self.assertEqual(len(mail.outbox), 1)
|
|
121
166
|
|
|
122
167
|
def test_request_invite_creates_lead_with_comment(self):
|
|
123
168
|
resp = self.client.post(
|
|
@@ -128,6 +173,76 @@ class InvitationTests(TestCase):
|
|
|
128
173
|
lead = InviteLead.objects.get()
|
|
129
174
|
self.assertEqual(lead.email, "new@example.com")
|
|
130
175
|
self.assertEqual(lead.comment, "Hello")
|
|
176
|
+
self.assertIsNone(lead.sent_on)
|
|
177
|
+
self.assertEqual(lead.error, "")
|
|
178
|
+
self.assertEqual(lead.mac_address, "")
|
|
179
|
+
self.assertEqual(len(mail.outbox), 0)
|
|
180
|
+
|
|
181
|
+
def test_request_invite_falls_back_to_send_mail(self):
|
|
182
|
+
node = Node.objects.create(
|
|
183
|
+
hostname="local", address="127.0.0.1", mac_address="00:11:22:33:44:55"
|
|
184
|
+
)
|
|
185
|
+
with (
|
|
186
|
+
patch("pages.views.Node.get_local", return_value=node),
|
|
187
|
+
patch.object(
|
|
188
|
+
node, "send_mail", side_effect=Exception("node fail")
|
|
189
|
+
) as node_send,
|
|
190
|
+
patch("pages.views.mailer.send", wraps=mailer.send) as fallback,
|
|
191
|
+
):
|
|
192
|
+
resp = self.client.post(
|
|
193
|
+
reverse("pages:request-invite"), {"email": "invite@example.com"}
|
|
194
|
+
)
|
|
195
|
+
self.assertEqual(resp.status_code, 200)
|
|
196
|
+
lead = InviteLead.objects.get()
|
|
197
|
+
self.assertIsNotNone(lead.sent_on)
|
|
198
|
+
self.assertIn("node fail", lead.error)
|
|
199
|
+
self.assertIn("default mail backend", lead.error)
|
|
200
|
+
self.assertTrue(node_send.called)
|
|
201
|
+
self.assertTrue(fallback.called)
|
|
202
|
+
self.assertEqual(len(mail.outbox), 1)
|
|
203
|
+
|
|
204
|
+
@patch(
|
|
205
|
+
"pages.views.public_wifi.resolve_mac_address",
|
|
206
|
+
return_value="aa:bb:cc:dd:ee:ff",
|
|
207
|
+
)
|
|
208
|
+
def test_request_invite_records_mac_address(self, mock_resolve):
|
|
209
|
+
resp = self.client.post(
|
|
210
|
+
reverse("pages:request-invite"), {"email": "invite@example.com"}
|
|
211
|
+
)
|
|
212
|
+
self.assertEqual(resp.status_code, 200)
|
|
213
|
+
lead = InviteLead.objects.get()
|
|
214
|
+
self.assertEqual(lead.mac_address, "aa:bb:cc:dd:ee:ff")
|
|
215
|
+
|
|
216
|
+
@patch("pages.views.public_wifi.grant_public_access")
|
|
217
|
+
@patch(
|
|
218
|
+
"pages.views.public_wifi.resolve_mac_address",
|
|
219
|
+
return_value="aa:bb:cc:dd:ee:ff",
|
|
220
|
+
)
|
|
221
|
+
def test_invitation_login_grants_public_wifi_access(self, mock_resolve, mock_grant):
|
|
222
|
+
control_role, _ = NodeRole.objects.get_or_create(name="Control")
|
|
223
|
+
feature = NodeFeature.objects.create(
|
|
224
|
+
slug="ap-public-wifi", display="AP Public Wi-Fi"
|
|
225
|
+
)
|
|
226
|
+
feature.roles.add(control_role)
|
|
227
|
+
node = Node.objects.create(
|
|
228
|
+
hostname="control",
|
|
229
|
+
address="127.0.0.1",
|
|
230
|
+
mac_address=Node.get_current_mac(),
|
|
231
|
+
role=control_role,
|
|
232
|
+
)
|
|
233
|
+
NodeFeatureAssignment.objects.create(node=node, feature=feature)
|
|
234
|
+
with patch("pages.views.Node.get_local", return_value=node):
|
|
235
|
+
resp = self.client.post(
|
|
236
|
+
reverse("pages:request-invite"), {"email": "invite@example.com"}
|
|
237
|
+
)
|
|
238
|
+
self.assertEqual(resp.status_code, 200)
|
|
239
|
+
link = re.search(r"http://testserver[\S]+", mail.outbox[0].body).group(0)
|
|
240
|
+
with patch("pages.views.Node.get_local", return_value=node):
|
|
241
|
+
resp = self.client.post(link)
|
|
242
|
+
self.assertEqual(resp.status_code, 302)
|
|
243
|
+
self.user.refresh_from_db()
|
|
244
|
+
self.assertTrue(self.user.is_active)
|
|
245
|
+
mock_grant.assert_called_once_with(self.user, "aa:bb:cc:dd:ee:ff")
|
|
131
246
|
|
|
132
247
|
|
|
133
248
|
class NavbarBrandTests(TestCase):
|
|
@@ -139,16 +254,12 @@ class NavbarBrandTests(TestCase):
|
|
|
139
254
|
|
|
140
255
|
def test_site_name_displayed_when_known(self):
|
|
141
256
|
resp = self.client.get(reverse("pages:index"))
|
|
142
|
-
self.assertContains(
|
|
143
|
-
resp, '<a class="navbar-brand" href="/">Terminal</a>'
|
|
144
|
-
)
|
|
257
|
+
self.assertContains(resp, '<a class="navbar-brand" href="/">Terminal</a>')
|
|
145
258
|
|
|
146
259
|
def test_default_brand_when_unknown(self):
|
|
147
260
|
Site.objects.filter(id=1).update(domain="example.com")
|
|
148
261
|
resp = self.client.get(reverse("pages:index"))
|
|
149
|
-
self.assertContains(
|
|
150
|
-
resp, '<a class="navbar-brand" href="/">Arthexis</a>'
|
|
151
|
-
)
|
|
262
|
+
self.assertContains(resp, '<a class="navbar-brand" href="/">Arthexis</a>')
|
|
152
263
|
|
|
153
264
|
@override_settings(ALLOWED_HOSTS=["127.0.0.1", "testserver"])
|
|
154
265
|
def test_brand_uses_role_name_when_site_name_blank(self):
|
|
@@ -198,7 +309,11 @@ class AdminBadgesTests(TestCase):
|
|
|
198
309
|
self.node.role = role
|
|
199
310
|
self.node.save()
|
|
200
311
|
resp = self.client.get(reverse("admin:index"))
|
|
312
|
+
role_list = reverse("admin:nodes_noderole_changelist")
|
|
313
|
+
role_change = reverse("admin:nodes_noderole_change", args=[role.pk])
|
|
201
314
|
self.assertContains(resp, "ROLE: Dev")
|
|
315
|
+
self.assertContains(resp, f'href="{role_list}"')
|
|
316
|
+
self.assertContains(resp, f'href="{role_change}"')
|
|
202
317
|
|
|
203
318
|
def test_badges_warn_when_node_missing(self):
|
|
204
319
|
from nodes.models import Node
|
|
@@ -242,6 +357,138 @@ class AdminSidebarTests(TestCase):
|
|
|
242
357
|
self.assertContains(resp, 'id="admin-collapsible-apps"')
|
|
243
358
|
|
|
244
359
|
|
|
360
|
+
class ViewHistoryLoggingTests(TestCase):
|
|
361
|
+
def setUp(self):
|
|
362
|
+
self.client = Client()
|
|
363
|
+
Site.objects.update_or_create(id=1, defaults={"name": "Terminal"})
|
|
364
|
+
|
|
365
|
+
def test_successful_visit_creates_entry(self):
|
|
366
|
+
resp = self.client.get(reverse("pages:index"))
|
|
367
|
+
self.assertEqual(resp.status_code, 200)
|
|
368
|
+
entry = ViewHistory.objects.order_by("-visited_at").first()
|
|
369
|
+
self.assertIsNotNone(entry)
|
|
370
|
+
self.assertEqual(entry.path, "/")
|
|
371
|
+
self.assertEqual(entry.status_code, 200)
|
|
372
|
+
self.assertEqual(entry.error_message, "")
|
|
373
|
+
|
|
374
|
+
def test_error_visit_records_message(self):
|
|
375
|
+
resp = self.client.get("/missing-page/")
|
|
376
|
+
self.assertEqual(resp.status_code, 404)
|
|
377
|
+
entry = (
|
|
378
|
+
ViewHistory.objects.filter(path="/missing-page/")
|
|
379
|
+
.order_by("-visited_at")
|
|
380
|
+
.first()
|
|
381
|
+
)
|
|
382
|
+
self.assertIsNotNone(entry)
|
|
383
|
+
self.assertEqual(entry.status_code, 404)
|
|
384
|
+
self.assertNotEqual(entry.error_message, "")
|
|
385
|
+
|
|
386
|
+
def test_debug_toolbar_requests_not_tracked(self):
|
|
387
|
+
resp = self.client.get(reverse("pages:index"), {"djdt": "toolbar"})
|
|
388
|
+
self.assertEqual(resp.status_code, 200)
|
|
389
|
+
self.assertFalse(ViewHistory.objects.exists())
|
|
390
|
+
|
|
391
|
+
def test_authenticated_user_last_visit_ip_updated(self):
|
|
392
|
+
User = get_user_model()
|
|
393
|
+
user = User.objects.create_user(
|
|
394
|
+
username="history_user", password="pwd", email="history@example.com"
|
|
395
|
+
)
|
|
396
|
+
self.assertTrue(self.client.login(username="history_user", password="pwd"))
|
|
397
|
+
|
|
398
|
+
resp = self.client.get(
|
|
399
|
+
reverse("pages:index"),
|
|
400
|
+
HTTP_X_FORWARDED_FOR="203.0.113.5",
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
self.assertEqual(resp.status_code, 200)
|
|
404
|
+
user.refresh_from_db()
|
|
405
|
+
self.assertEqual(user.last_visit_ip_address, "203.0.113.5")
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class ViewHistoryAdminTests(TestCase):
|
|
409
|
+
def setUp(self):
|
|
410
|
+
self.client = Client()
|
|
411
|
+
User = get_user_model()
|
|
412
|
+
self.admin = User.objects.create_superuser(
|
|
413
|
+
username="history_admin", password="pwd", email="admin@example.com"
|
|
414
|
+
)
|
|
415
|
+
self.client.force_login(self.admin)
|
|
416
|
+
Site.objects.update_or_create(
|
|
417
|
+
id=1, defaults={"name": "test", "domain": "testserver"}
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
def _create_history(self, path: str, days_offset: int = 0, count: int = 1):
|
|
421
|
+
for _ in range(count):
|
|
422
|
+
entry = ViewHistory.objects.create(
|
|
423
|
+
path=path,
|
|
424
|
+
method="GET",
|
|
425
|
+
status_code=200,
|
|
426
|
+
status_text="OK",
|
|
427
|
+
error_message="",
|
|
428
|
+
view_name="pages:index",
|
|
429
|
+
)
|
|
430
|
+
if days_offset:
|
|
431
|
+
entry.visited_at = timezone.now() - timedelta(days=days_offset)
|
|
432
|
+
entry.save(update_fields=["visited_at"])
|
|
433
|
+
|
|
434
|
+
def test_change_list_includes_graph_link(self):
|
|
435
|
+
resp = self.client.get(reverse("admin:pages_viewhistory_changelist"))
|
|
436
|
+
self.assertContains(resp, reverse("admin:pages_viewhistory_traffic_graph"))
|
|
437
|
+
self.assertContains(resp, "Traffic graph")
|
|
438
|
+
|
|
439
|
+
def test_graph_view_renders_canvas(self):
|
|
440
|
+
resp = self.client.get(reverse("admin:pages_viewhistory_traffic_graph"))
|
|
441
|
+
self.assertContains(resp, "viewhistory-chart")
|
|
442
|
+
self.assertContains(resp, reverse("admin:pages_viewhistory_changelist"))
|
|
443
|
+
|
|
444
|
+
def test_graph_data_endpoint(self):
|
|
445
|
+
self._create_history("/", count=2)
|
|
446
|
+
self._create_history("/about/", days_offset=1)
|
|
447
|
+
url = reverse("admin:pages_viewhistory_traffic_data")
|
|
448
|
+
resp = self.client.get(url)
|
|
449
|
+
self.assertEqual(resp.status_code, 200)
|
|
450
|
+
data = resp.json()
|
|
451
|
+
self.assertIn("labels", data)
|
|
452
|
+
self.assertIn("datasets", data)
|
|
453
|
+
self.assertGreater(len(data["labels"]), 0)
|
|
454
|
+
totals = {
|
|
455
|
+
dataset["label"]: sum(dataset["data"]) for dataset in data["datasets"]
|
|
456
|
+
}
|
|
457
|
+
self.assertEqual(totals.get("/"), 2)
|
|
458
|
+
self.assertEqual(totals.get("/about/"), 1)
|
|
459
|
+
|
|
460
|
+
def test_admin_index_displays_widget(self):
|
|
461
|
+
resp = self.client.get(reverse("admin:index"))
|
|
462
|
+
self.assertContains(resp, "viewhistory-mini-module")
|
|
463
|
+
self.assertContains(resp, reverse("admin:pages_viewhistory_traffic_graph"))
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class AdminModelStatusTests(TestCase):
|
|
467
|
+
def setUp(self):
|
|
468
|
+
self.client = Client()
|
|
469
|
+
User = get_user_model()
|
|
470
|
+
self.admin = User.objects.create_superuser(
|
|
471
|
+
username="status_admin", password="pwd", email="admin@example.com"
|
|
472
|
+
)
|
|
473
|
+
self.client.force_login(self.admin)
|
|
474
|
+
Site.objects.update_or_create(
|
|
475
|
+
id=1, defaults={"name": "test", "domain": "testserver"}
|
|
476
|
+
)
|
|
477
|
+
from nodes.models import Node
|
|
478
|
+
|
|
479
|
+
Node.objects.create(hostname="testserver", address="127.0.0.1")
|
|
480
|
+
|
|
481
|
+
@patch("pages.templatetags.admin_extras.connection.introspection.table_names")
|
|
482
|
+
def test_status_dots_render(self, mock_tables):
|
|
483
|
+
from django.db import connection
|
|
484
|
+
|
|
485
|
+
tables = type(connection.introspection).table_names(connection.introspection)
|
|
486
|
+
mock_tables.return_value = [t for t in tables if t != "pages_module"]
|
|
487
|
+
resp = self.client.get(reverse("admin:index"))
|
|
488
|
+
self.assertContains(resp, 'class="model-status ok"')
|
|
489
|
+
self.assertContains(resp, 'class="model-status missing"', count=1)
|
|
490
|
+
|
|
491
|
+
|
|
245
492
|
class SiteAdminRegisterCurrentTests(TestCase):
|
|
246
493
|
def setUp(self):
|
|
247
494
|
self.client = Client()
|
|
@@ -382,6 +629,81 @@ class NavAppsTests(TestCase):
|
|
|
382
629
|
self.assertNotContains(resp, 'href="/core/"')
|
|
383
630
|
|
|
384
631
|
|
|
632
|
+
class ConstellationNavTests(TestCase):
|
|
633
|
+
def setUp(self):
|
|
634
|
+
self.client = Client()
|
|
635
|
+
role, _ = NodeRole.objects.get_or_create(name="Constellation")
|
|
636
|
+
Node.objects.update_or_create(
|
|
637
|
+
mac_address=Node.get_current_mac(),
|
|
638
|
+
defaults={
|
|
639
|
+
"hostname": "localhost",
|
|
640
|
+
"address": "127.0.0.1",
|
|
641
|
+
"role": role,
|
|
642
|
+
},
|
|
643
|
+
)
|
|
644
|
+
Site.objects.update_or_create(
|
|
645
|
+
id=1, defaults={"domain": "testserver", "name": ""}
|
|
646
|
+
)
|
|
647
|
+
fixtures = [
|
|
648
|
+
Path(
|
|
649
|
+
settings.BASE_DIR,
|
|
650
|
+
"pages",
|
|
651
|
+
"fixtures",
|
|
652
|
+
"constellation__application_ocpp.json",
|
|
653
|
+
),
|
|
654
|
+
Path(
|
|
655
|
+
settings.BASE_DIR,
|
|
656
|
+
"pages",
|
|
657
|
+
"fixtures",
|
|
658
|
+
"constellation__module_ocpp.json",
|
|
659
|
+
),
|
|
660
|
+
Path(
|
|
661
|
+
settings.BASE_DIR,
|
|
662
|
+
"pages",
|
|
663
|
+
"fixtures",
|
|
664
|
+
"constellation__module_rfid.json",
|
|
665
|
+
),
|
|
666
|
+
Path(
|
|
667
|
+
settings.BASE_DIR,
|
|
668
|
+
"pages",
|
|
669
|
+
"fixtures",
|
|
670
|
+
"constellation__landing_ocpp_dashboard.json",
|
|
671
|
+
),
|
|
672
|
+
Path(
|
|
673
|
+
settings.BASE_DIR,
|
|
674
|
+
"pages",
|
|
675
|
+
"fixtures",
|
|
676
|
+
"constellation__landing_ocpp_cp_simulator.json",
|
|
677
|
+
),
|
|
678
|
+
Path(
|
|
679
|
+
settings.BASE_DIR,
|
|
680
|
+
"pages",
|
|
681
|
+
"fixtures",
|
|
682
|
+
"constellation__landing_ocpp_rfid.json",
|
|
683
|
+
),
|
|
684
|
+
]
|
|
685
|
+
call_command("loaddata", *map(str, fixtures))
|
|
686
|
+
|
|
687
|
+
def test_rfid_pill_hidden(self):
|
|
688
|
+
resp = self.client.get(reverse("pages:index"))
|
|
689
|
+
nav_labels = [
|
|
690
|
+
module.menu_label.upper() for module in resp.context["nav_modules"]
|
|
691
|
+
]
|
|
692
|
+
self.assertNotIn("RFID", nav_labels)
|
|
693
|
+
self.assertTrue(
|
|
694
|
+
Module.objects.filter(
|
|
695
|
+
path="/ocpp/", node_role__name="Constellation"
|
|
696
|
+
).exists()
|
|
697
|
+
)
|
|
698
|
+
self.assertFalse(
|
|
699
|
+
Module.objects.filter(
|
|
700
|
+
path="/ocpp/rfid/",
|
|
701
|
+
node_role__name="Constellation",
|
|
702
|
+
is_deleted=False,
|
|
703
|
+
).exists()
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
|
|
385
707
|
class StaffNavVisibilityTests(TestCase):
|
|
386
708
|
def setUp(self):
|
|
387
709
|
self.client = Client()
|
|
@@ -472,16 +794,30 @@ class LandingCreationTests(TestCase):
|
|
|
472
794
|
self.role = role
|
|
473
795
|
|
|
474
796
|
def test_landings_created_on_module_creation(self):
|
|
475
|
-
module = Module.objects.create(
|
|
797
|
+
module = Module.objects.create(
|
|
798
|
+
node_role=self.role, application=self.app, path="/"
|
|
799
|
+
)
|
|
476
800
|
self.assertTrue(module.landings.filter(path="/").exists())
|
|
477
801
|
|
|
478
802
|
|
|
479
803
|
class LandingFixtureTests(TestCase):
|
|
480
804
|
def test_constellation_fixture_loads_without_duplicates(self):
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
805
|
+
from glob import glob
|
|
806
|
+
|
|
807
|
+
NodeRole.objects.get_or_create(name="Constellation")
|
|
808
|
+
fixtures = glob(
|
|
809
|
+
str(Path(settings.BASE_DIR, "pages", "fixtures", "constellation__*.json"))
|
|
810
|
+
)
|
|
811
|
+
fixtures = sorted(
|
|
812
|
+
fixtures,
|
|
813
|
+
key=lambda path: (
|
|
814
|
+
0 if "__application_" in path else 1 if "__module_" in path else 2
|
|
815
|
+
),
|
|
816
|
+
)
|
|
817
|
+
call_command("loaddata", *fixtures)
|
|
818
|
+
call_command("loaddata", *fixtures)
|
|
484
819
|
module = Module.objects.get(path="/ocpp/", node_role__name="Constellation")
|
|
820
|
+
module.create_landings()
|
|
485
821
|
self.assertEqual(module.landings.filter(path="/ocpp/rfid/").count(), 1)
|
|
486
822
|
|
|
487
823
|
|
|
@@ -494,20 +830,14 @@ class AllowedHostSubnetTests(TestCase):
|
|
|
494
830
|
|
|
495
831
|
@override_settings(ALLOWED_HOSTS=["10.42.0.0/16", "192.168.0.0/16"])
|
|
496
832
|
def test_private_network_hosts_allowed(self):
|
|
497
|
-
resp = self.client.get(
|
|
498
|
-
reverse("pages:index"), HTTP_HOST="10.42.1.5"
|
|
499
|
-
)
|
|
833
|
+
resp = self.client.get(reverse("pages:index"), HTTP_HOST="10.42.1.5")
|
|
500
834
|
self.assertEqual(resp.status_code, 200)
|
|
501
|
-
resp = self.client.get(
|
|
502
|
-
reverse("pages:index"), HTTP_HOST="192.168.2.3"
|
|
503
|
-
)
|
|
835
|
+
resp = self.client.get(reverse("pages:index"), HTTP_HOST="192.168.2.3")
|
|
504
836
|
self.assertEqual(resp.status_code, 200)
|
|
505
837
|
|
|
506
838
|
@override_settings(ALLOWED_HOSTS=["10.42.0.0/16"])
|
|
507
839
|
def test_host_outside_subnets_disallowed(self):
|
|
508
|
-
resp = self.client.get(
|
|
509
|
-
reverse("pages:index"), HTTP_HOST="11.0.0.1"
|
|
510
|
-
)
|
|
840
|
+
resp = self.client.get(reverse("pages:index"), HTTP_HOST="11.0.0.1")
|
|
511
841
|
self.assertEqual(resp.status_code, 400)
|
|
512
842
|
|
|
513
843
|
|
|
@@ -540,7 +870,11 @@ class FaviconTests(TestCase):
|
|
|
540
870
|
role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
541
871
|
Node.objects.update_or_create(
|
|
542
872
|
mac_address=Node.get_current_mac(),
|
|
543
|
-
defaults={
|
|
873
|
+
defaults={
|
|
874
|
+
"hostname": "localhost",
|
|
875
|
+
"address": "127.0.0.1",
|
|
876
|
+
"role": role,
|
|
877
|
+
},
|
|
544
878
|
)
|
|
545
879
|
site, _ = Site.objects.update_or_create(
|
|
546
880
|
id=1, defaults={"domain": "testserver", "name": ""}
|
|
@@ -564,7 +898,11 @@ class FaviconTests(TestCase):
|
|
|
564
898
|
role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
565
899
|
Node.objects.update_or_create(
|
|
566
900
|
mac_address=Node.get_current_mac(),
|
|
567
|
-
defaults={
|
|
901
|
+
defaults={
|
|
902
|
+
"hostname": "localhost",
|
|
903
|
+
"address": "127.0.0.1",
|
|
904
|
+
"role": role,
|
|
905
|
+
},
|
|
568
906
|
)
|
|
569
907
|
site, _ = Site.objects.update_or_create(
|
|
570
908
|
id=1, defaults={"domain": "testserver", "name": ""}
|
|
@@ -584,7 +922,11 @@ class FaviconTests(TestCase):
|
|
|
584
922
|
role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
585
923
|
Node.objects.update_or_create(
|
|
586
924
|
mac_address=Node.get_current_mac(),
|
|
587
|
-
defaults={
|
|
925
|
+
defaults={
|
|
926
|
+
"hostname": "localhost",
|
|
927
|
+
"address": "127.0.0.1",
|
|
928
|
+
"role": role,
|
|
929
|
+
},
|
|
588
930
|
)
|
|
589
931
|
Site.objects.update_or_create(
|
|
590
932
|
id=1, defaults={"domain": "testserver", "name": ""}
|
|
@@ -606,13 +948,30 @@ class FavoriteTests(TestCase):
|
|
|
606
948
|
self.user = User.objects.create_superuser(
|
|
607
949
|
username="favadmin", password="pwd", email="fav@example.com"
|
|
608
950
|
)
|
|
951
|
+
ReleaseManager.objects.create(user=self.user)
|
|
609
952
|
self.client.force_login(self.user)
|
|
610
|
-
Site.objects.update_or_create(
|
|
953
|
+
Site.objects.update_or_create(
|
|
954
|
+
id=1, defaults={"name": "test", "domain": "testserver"}
|
|
955
|
+
)
|
|
956
|
+
from nodes.models import Node, NodeRole
|
|
957
|
+
|
|
958
|
+
terminal_role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
959
|
+
self.node, _ = Node.objects.update_or_create(
|
|
960
|
+
mac_address=Node.get_current_mac(),
|
|
961
|
+
defaults={
|
|
962
|
+
"hostname": "localhost",
|
|
963
|
+
"address": "127.0.0.1",
|
|
964
|
+
"role": terminal_role,
|
|
965
|
+
},
|
|
966
|
+
)
|
|
967
|
+
ContentType.objects.clear_cache()
|
|
611
968
|
|
|
612
969
|
def test_add_favorite(self):
|
|
613
970
|
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
614
971
|
next_url = reverse("admin:pages_application_changelist")
|
|
615
|
-
url =
|
|
972
|
+
url = (
|
|
973
|
+
reverse("admin:favorite_toggle", args=[ct.id]) + f"?next={quote(next_url)}"
|
|
974
|
+
)
|
|
616
975
|
resp = self.client.post(url, {"custom_label": "Apps", "user_data": "on"})
|
|
617
976
|
self.assertRedirects(resp, next_url)
|
|
618
977
|
fav = Favorite.objects.get(user=self.user, content_type=ct)
|
|
@@ -622,7 +981,9 @@ class FavoriteTests(TestCase):
|
|
|
622
981
|
def test_cancel_link_uses_next(self):
|
|
623
982
|
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
624
983
|
next_url = reverse("admin:pages_application_changelist")
|
|
625
|
-
url =
|
|
984
|
+
url = (
|
|
985
|
+
reverse("admin:favorite_toggle", args=[ct.id]) + f"?next={quote(next_url)}"
|
|
986
|
+
)
|
|
626
987
|
resp = self.client.get(url)
|
|
627
988
|
self.assertContains(resp, f'href="{next_url}"')
|
|
628
989
|
|
|
@@ -646,10 +1007,10 @@ class FavoriteTests(TestCase):
|
|
|
646
1007
|
|
|
647
1008
|
def test_dashboard_includes_favorites_and_user_data(self):
|
|
648
1009
|
fav_ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
649
|
-
Favorite.objects.create(
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
1010
|
+
Favorite.objects.create(
|
|
1011
|
+
user=self.user, content_type=fav_ct, custom_label="Apps"
|
|
1012
|
+
)
|
|
1013
|
+
NodeRole.objects.create(name="DataRole", is_user_data=True)
|
|
653
1014
|
resp = self.client.get(reverse("admin:index"))
|
|
654
1015
|
self.assertContains(resp, reverse("admin:pages_application_changelist"))
|
|
655
1016
|
self.assertContains(resp, reverse("admin:nodes_noderole_changelist"))
|
|
@@ -657,8 +1018,7 @@ class FavoriteTests(TestCase):
|
|
|
657
1018
|
def test_dashboard_merges_duplicate_future_actions(self):
|
|
658
1019
|
ct = ContentType.objects.get_for_model(NodeRole)
|
|
659
1020
|
Favorite.objects.create(user=self.user, content_type=ct)
|
|
660
|
-
|
|
661
|
-
UserDatum.objects.create(user=self.user, content_type=ct, object_id=role.pk)
|
|
1021
|
+
NodeRole.objects.create(name="DataRole2", is_user_data=True)
|
|
662
1022
|
AdminHistory.objects.create(
|
|
663
1023
|
user=self.user,
|
|
664
1024
|
content_type=ct,
|
|
@@ -668,3 +1028,362 @@ class FavoriteTests(TestCase):
|
|
|
668
1028
|
url = reverse("admin:nodes_noderole_changelist")
|
|
669
1029
|
self.assertGreaterEqual(resp.content.decode().count(url), 1)
|
|
670
1030
|
self.assertContains(resp, NodeRole._meta.verbose_name_plural)
|
|
1031
|
+
|
|
1032
|
+
def test_dashboard_limits_future_actions_to_top_four(self):
|
|
1033
|
+
from pages.templatetags.admin_extras import future_action_items
|
|
1034
|
+
|
|
1035
|
+
role_ct = ContentType.objects.get_for_model(NodeRole)
|
|
1036
|
+
role_url = reverse("admin:nodes_noderole_changelist")
|
|
1037
|
+
AdminHistory.objects.create(
|
|
1038
|
+
user=self.user,
|
|
1039
|
+
content_type=role_ct,
|
|
1040
|
+
url=role_url,
|
|
1041
|
+
)
|
|
1042
|
+
AdminHistory.objects.create(
|
|
1043
|
+
user=self.user,
|
|
1044
|
+
content_type=role_ct,
|
|
1045
|
+
url=f"{role_url}?page=2",
|
|
1046
|
+
)
|
|
1047
|
+
AdminHistory.objects.create(
|
|
1048
|
+
user=self.user,
|
|
1049
|
+
content_type=role_ct,
|
|
1050
|
+
url=f"{role_url}?page=3",
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
app_ct = ContentType.objects.get_for_model(Application)
|
|
1054
|
+
app_url = reverse("admin:pages_application_changelist")
|
|
1055
|
+
AdminHistory.objects.create(
|
|
1056
|
+
user=self.user,
|
|
1057
|
+
content_type=app_ct,
|
|
1058
|
+
url=app_url,
|
|
1059
|
+
)
|
|
1060
|
+
AdminHistory.objects.create(
|
|
1061
|
+
user=self.user,
|
|
1062
|
+
content_type=app_ct,
|
|
1063
|
+
url=f"{app_url}?page=2",
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1066
|
+
module_ct = ContentType.objects.get_for_model(Module)
|
|
1067
|
+
module_url = reverse("admin:pages_module_changelist")
|
|
1068
|
+
AdminHistory.objects.create(
|
|
1069
|
+
user=self.user,
|
|
1070
|
+
content_type=module_ct,
|
|
1071
|
+
url=module_url,
|
|
1072
|
+
)
|
|
1073
|
+
AdminHistory.objects.create(
|
|
1074
|
+
user=self.user,
|
|
1075
|
+
content_type=module_ct,
|
|
1076
|
+
url=f"{module_url}?page=2",
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
package_ct = ContentType.objects.get_for_model(Package)
|
|
1080
|
+
package_url = reverse("admin:core_package_changelist")
|
|
1081
|
+
AdminHistory.objects.create(
|
|
1082
|
+
user=self.user,
|
|
1083
|
+
content_type=package_ct,
|
|
1084
|
+
url=package_url,
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
view_history_ct = ContentType.objects.get_for_model(ViewHistory)
|
|
1088
|
+
view_history_url = reverse("admin:pages_viewhistory_changelist")
|
|
1089
|
+
AdminHistory.objects.create(
|
|
1090
|
+
user=self.user,
|
|
1091
|
+
content_type=view_history_ct,
|
|
1092
|
+
url=view_history_url,
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
resp = self.client.get(reverse("admin:index"))
|
|
1096
|
+
items = future_action_items({"request": resp.wsgi_request})["models"]
|
|
1097
|
+
labels = {item["label"] for item in items}
|
|
1098
|
+
self.assertEqual(len(items), 4)
|
|
1099
|
+
self.assertIn("Node Roles", labels)
|
|
1100
|
+
self.assertIn("Modules", labels)
|
|
1101
|
+
self.assertIn("applications", labels)
|
|
1102
|
+
self.assertIn("View Histories", labels)
|
|
1103
|
+
self.assertNotIn("Packages", labels)
|
|
1104
|
+
ContentType.objects.clear_cache()
|
|
1105
|
+
|
|
1106
|
+
def test_favorite_ct_id_recreates_missing_content_type(self):
|
|
1107
|
+
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
1108
|
+
ct.delete()
|
|
1109
|
+
from pages.templatetags.favorites import favorite_ct_id
|
|
1110
|
+
|
|
1111
|
+
new_id = favorite_ct_id("pages", "Application")
|
|
1112
|
+
self.assertIsNotNone(new_id)
|
|
1113
|
+
self.assertTrue(
|
|
1114
|
+
ContentType.objects.filter(
|
|
1115
|
+
pk=new_id, app_label="pages", model="application"
|
|
1116
|
+
).exists()
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
def test_dashboard_uses_browse_label(self):
|
|
1120
|
+
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
1121
|
+
Favorite.objects.create(user=self.user, content_type=ct)
|
|
1122
|
+
resp = self.client.get(reverse("admin:index"))
|
|
1123
|
+
self.assertContains(resp, "Browse Applications")
|
|
1124
|
+
|
|
1125
|
+
def test_dashboard_uses_todo_url_if_set(self):
|
|
1126
|
+
Todo.objects.create(request="Check docs", url="/docs/")
|
|
1127
|
+
resp = self.client.get(reverse("admin:index"))
|
|
1128
|
+
self.assertContains(resp, 'href="/docs/"')
|
|
1129
|
+
|
|
1130
|
+
def test_dashboard_shows_todo_with_done_button(self):
|
|
1131
|
+
todo = Todo.objects.create(request="Do thing")
|
|
1132
|
+
resp = self.client.get(reverse("admin:index"))
|
|
1133
|
+
done_url = reverse("todo-done", args=[todo.pk])
|
|
1134
|
+
self.assertContains(resp, todo.request)
|
|
1135
|
+
self.assertContains(resp, f'action="{done_url}"')
|
|
1136
|
+
self.assertContains(resp, "DONE")
|
|
1137
|
+
|
|
1138
|
+
def test_dashboard_shows_request_details(self):
|
|
1139
|
+
Todo.objects.create(request="Do thing", request_details="More info")
|
|
1140
|
+
resp = self.client.get(reverse("admin:index"))
|
|
1141
|
+
self.assertContains(
|
|
1142
|
+
resp, '<div class="todo-details">More info</div>', html=True
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
def test_dashboard_excludes_todo_changelist_link(self):
|
|
1146
|
+
ct = ContentType.objects.get_for_model(Todo)
|
|
1147
|
+
Favorite.objects.create(user=self.user, content_type=ct)
|
|
1148
|
+
AdminHistory.objects.create(
|
|
1149
|
+
user=self.user,
|
|
1150
|
+
content_type=ct,
|
|
1151
|
+
url=reverse("admin:core_todo_changelist"),
|
|
1152
|
+
)
|
|
1153
|
+
Todo.objects.create(request="Task", is_user_data=True)
|
|
1154
|
+
resp = self.client.get(reverse("admin:index"))
|
|
1155
|
+
changelist = reverse("admin:core_todo_changelist")
|
|
1156
|
+
self.assertNotContains(resp, f'href="{changelist}"')
|
|
1157
|
+
|
|
1158
|
+
def test_dashboard_hides_todos_without_release_manager(self):
|
|
1159
|
+
todo = Todo.objects.create(request="Only Release Manager")
|
|
1160
|
+
User = get_user_model()
|
|
1161
|
+
other_user = User.objects.create_superuser(
|
|
1162
|
+
username="norole", password="pwd", email="norole@example.com"
|
|
1163
|
+
)
|
|
1164
|
+
self.client.force_login(other_user)
|
|
1165
|
+
resp = self.client.get(reverse("admin:index"))
|
|
1166
|
+
self.assertNotContains(resp, "Release manager tasks")
|
|
1167
|
+
self.assertNotContains(resp, todo.request)
|
|
1168
|
+
|
|
1169
|
+
def test_dashboard_hides_todos_for_non_terminal_node(self):
|
|
1170
|
+
todo = Todo.objects.create(request="Terminal Tasks")
|
|
1171
|
+
from nodes.models import NodeRole
|
|
1172
|
+
|
|
1173
|
+
control_role, _ = NodeRole.objects.get_or_create(name="Control")
|
|
1174
|
+
self.node.role = control_role
|
|
1175
|
+
self.node.save(update_fields=["role"])
|
|
1176
|
+
resp = self.client.get(reverse("admin:index"))
|
|
1177
|
+
self.assertNotContains(resp, "Release manager tasks")
|
|
1178
|
+
self.assertNotContains(resp, todo.request)
|
|
1179
|
+
|
|
1180
|
+
def test_dashboard_shows_todos_for_delegate_release_manager(self):
|
|
1181
|
+
todo = Todo.objects.create(request="Delegate Task")
|
|
1182
|
+
User = get_user_model()
|
|
1183
|
+
delegate = User.objects.create_superuser(
|
|
1184
|
+
username="delegate",
|
|
1185
|
+
password="pwd",
|
|
1186
|
+
email="delegate@example.com",
|
|
1187
|
+
)
|
|
1188
|
+
ReleaseManager.objects.create(user=delegate)
|
|
1189
|
+
operator = User.objects.create_superuser(
|
|
1190
|
+
username="operator",
|
|
1191
|
+
password="pwd",
|
|
1192
|
+
email="operator@example.com",
|
|
1193
|
+
)
|
|
1194
|
+
operator.operate_as = delegate
|
|
1195
|
+
operator.full_clean()
|
|
1196
|
+
operator.save()
|
|
1197
|
+
self.client.force_login(operator)
|
|
1198
|
+
resp = self.client.get(reverse("admin:index"))
|
|
1199
|
+
self.assertContains(resp, "Release manager tasks")
|
|
1200
|
+
self.assertContains(resp, todo.request)
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
class AdminModelGraphViewTests(TestCase):
|
|
1204
|
+
def setUp(self):
|
|
1205
|
+
self.client = Client()
|
|
1206
|
+
User = get_user_model()
|
|
1207
|
+
self.user = User.objects.create_user(
|
|
1208
|
+
username="graph-staff", password="pwd", is_staff=True
|
|
1209
|
+
)
|
|
1210
|
+
Site.objects.update_or_create(id=1, defaults={"name": "Terminal"})
|
|
1211
|
+
self.client.force_login(self.user)
|
|
1212
|
+
|
|
1213
|
+
def _mock_graph(self):
|
|
1214
|
+
fake_graph = Mock()
|
|
1215
|
+
fake_graph.source = "digraph {}"
|
|
1216
|
+
fake_graph.engine = "dot"
|
|
1217
|
+
|
|
1218
|
+
def pipe_side_effect(*args, **kwargs):
|
|
1219
|
+
fmt = kwargs.get("format") or (args[0] if args else None)
|
|
1220
|
+
if fmt == "svg":
|
|
1221
|
+
return '<svg xmlns="http://www.w3.org/2000/svg"></svg>'
|
|
1222
|
+
if fmt == "pdf":
|
|
1223
|
+
return b"%PDF-1.4 mock"
|
|
1224
|
+
raise AssertionError(f"Unexpected format: {fmt}")
|
|
1225
|
+
|
|
1226
|
+
fake_graph.pipe.side_effect = pipe_side_effect
|
|
1227
|
+
return fake_graph
|
|
1228
|
+
|
|
1229
|
+
def test_model_graph_renders_controls_and_download_link(self):
|
|
1230
|
+
url = reverse("admin-model-graph", args=["pages"])
|
|
1231
|
+
graph = self._mock_graph()
|
|
1232
|
+
with (
|
|
1233
|
+
patch("pages.views._build_model_graph", return_value=graph),
|
|
1234
|
+
patch("pages.views.shutil.which", return_value="/usr/bin/dot"),
|
|
1235
|
+
):
|
|
1236
|
+
response = self.client.get(url)
|
|
1237
|
+
|
|
1238
|
+
self.assertEqual(response.status_code, 200)
|
|
1239
|
+
self.assertContains(response, "data-model-graph")
|
|
1240
|
+
self.assertContains(response, 'data-graph-action="zoom-in"')
|
|
1241
|
+
self.assertContains(response, "Download PDF")
|
|
1242
|
+
self.assertIn("?format=pdf", response.context_data["download_url"])
|
|
1243
|
+
args, kwargs = graph.pipe.call_args
|
|
1244
|
+
self.assertEqual(kwargs.get("format"), "svg")
|
|
1245
|
+
self.assertEqual(kwargs.get("encoding"), "utf-8")
|
|
1246
|
+
|
|
1247
|
+
def test_model_graph_pdf_download(self):
|
|
1248
|
+
url = reverse("admin-model-graph", args=["pages"])
|
|
1249
|
+
graph = self._mock_graph()
|
|
1250
|
+
with (
|
|
1251
|
+
patch("pages.views._build_model_graph", return_value=graph),
|
|
1252
|
+
patch("pages.views.shutil.which", return_value="/usr/bin/dot"),
|
|
1253
|
+
):
|
|
1254
|
+
response = self.client.get(url, {"format": "pdf"})
|
|
1255
|
+
|
|
1256
|
+
self.assertEqual(response.status_code, 200)
|
|
1257
|
+
self.assertEqual(response["Content-Type"], "application/pdf")
|
|
1258
|
+
app_config = django_apps.get_app_config("pages")
|
|
1259
|
+
expected_slug = slugify(app_config.verbose_name) or app_config.label
|
|
1260
|
+
self.assertIn(
|
|
1261
|
+
f"{expected_slug}-model-graph.pdf", response["Content-Disposition"]
|
|
1262
|
+
)
|
|
1263
|
+
self.assertEqual(response.content, b"%PDF-1.4 mock")
|
|
1264
|
+
args, kwargs = graph.pipe.call_args
|
|
1265
|
+
self.assertEqual(kwargs.get("format"), "pdf")
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
class DatasetteTests(TestCase):
|
|
1269
|
+
def setUp(self):
|
|
1270
|
+
self.client = Client()
|
|
1271
|
+
User = get_user_model()
|
|
1272
|
+
self.user = User.objects.create_user(username="ds", password="pwd")
|
|
1273
|
+
Site.objects.update_or_create(id=1, defaults={"name": "Terminal"})
|
|
1274
|
+
|
|
1275
|
+
def test_datasette_auth_endpoint(self):
|
|
1276
|
+
resp = self.client.get(reverse("pages:datasette-auth"))
|
|
1277
|
+
self.assertEqual(resp.status_code, 401)
|
|
1278
|
+
self.client.force_login(self.user)
|
|
1279
|
+
resp = self.client.get(reverse("pages:datasette-auth"))
|
|
1280
|
+
self.assertEqual(resp.status_code, 200)
|
|
1281
|
+
|
|
1282
|
+
def test_navbar_includes_datasette_when_enabled(self):
|
|
1283
|
+
lock_dir = Path(settings.BASE_DIR) / "locks"
|
|
1284
|
+
lock_dir.mkdir(exist_ok=True)
|
|
1285
|
+
lock_file = lock_dir / "datasette.lck"
|
|
1286
|
+
try:
|
|
1287
|
+
lock_file.touch()
|
|
1288
|
+
resp = self.client.get(reverse("pages:index"))
|
|
1289
|
+
self.assertContains(resp, 'href="/data/"')
|
|
1290
|
+
finally:
|
|
1291
|
+
lock_file.unlink(missing_ok=True)
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
class ClientReportLiveUpdateTests(TestCase):
|
|
1295
|
+
def setUp(self):
|
|
1296
|
+
self.client = Client()
|
|
1297
|
+
|
|
1298
|
+
def test_client_report_includes_interval(self):
|
|
1299
|
+
resp = self.client.get(reverse("pages:client-report"))
|
|
1300
|
+
self.assertEqual(resp.context["request"].live_update_interval, 5)
|
|
1301
|
+
self.assertContains(resp, "setInterval(() => location.reload()")
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
class ScreenshotSpecInfrastructureTests(TestCase):
|
|
1305
|
+
def test_runner_creates_outputs_and_cleans_old_samples(self):
|
|
1306
|
+
spec = ScreenshotSpec(slug="spec-test", url="/")
|
|
1307
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1308
|
+
temp_dir = Path(tmp)
|
|
1309
|
+
screenshot_path = temp_dir / "source.png"
|
|
1310
|
+
screenshot_path.write_bytes(b"fake")
|
|
1311
|
+
ContentSample.objects.create(
|
|
1312
|
+
kind=ContentSample.IMAGE,
|
|
1313
|
+
path="old.png",
|
|
1314
|
+
method="spec:old",
|
|
1315
|
+
hash="old-hash",
|
|
1316
|
+
)
|
|
1317
|
+
ContentSample.objects.filter(hash="old-hash").update(
|
|
1318
|
+
created_at=timezone.now() - timedelta(days=8)
|
|
1319
|
+
)
|
|
1320
|
+
with (
|
|
1321
|
+
patch(
|
|
1322
|
+
"pages.screenshot_specs.base.capture_screenshot",
|
|
1323
|
+
return_value=screenshot_path,
|
|
1324
|
+
) as capture_mock,
|
|
1325
|
+
patch(
|
|
1326
|
+
"pages.screenshot_specs.base.save_screenshot", return_value=None
|
|
1327
|
+
) as save_mock,
|
|
1328
|
+
):
|
|
1329
|
+
with ScreenshotSpecRunner(temp_dir) as runner:
|
|
1330
|
+
result = runner.run(spec)
|
|
1331
|
+
self.assertTrue(result.image_path.exists())
|
|
1332
|
+
self.assertTrue(result.base64_path.exists())
|
|
1333
|
+
self.assertEqual(ContentSample.objects.filter(hash="old-hash").count(), 0)
|
|
1334
|
+
capture_mock.assert_called_once()
|
|
1335
|
+
save_mock.assert_called_once_with(screenshot_path, method="spec:spec-test")
|
|
1336
|
+
|
|
1337
|
+
def test_runner_respects_manual_reason(self):
|
|
1338
|
+
spec = ScreenshotSpec(slug="manual-spec", url="/", manual_reason="hardware")
|
|
1339
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1340
|
+
with ScreenshotSpecRunner(Path(tmp)) as runner:
|
|
1341
|
+
with self.assertRaises(ScreenshotUnavailable):
|
|
1342
|
+
runner.run(spec)
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
class CaptureUIScreenshotsCommandTests(TestCase):
|
|
1346
|
+
def tearDown(self):
|
|
1347
|
+
registry.unregister("manual-cmd")
|
|
1348
|
+
registry.unregister("auto-cmd")
|
|
1349
|
+
|
|
1350
|
+
def test_manual_spec_emits_warning(self):
|
|
1351
|
+
spec = ScreenshotSpec(slug="manual-cmd", url="/", manual_reason="manual")
|
|
1352
|
+
registry.register(spec)
|
|
1353
|
+
out = StringIO()
|
|
1354
|
+
call_command("capture_ui_screenshots", "--spec", spec.slug, stdout=out)
|
|
1355
|
+
self.assertIn("Skipping manual screenshot", out.getvalue())
|
|
1356
|
+
|
|
1357
|
+
def test_command_invokes_runner(self):
|
|
1358
|
+
spec = ScreenshotSpec(slug="auto-cmd", url="/")
|
|
1359
|
+
registry.register(spec)
|
|
1360
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1361
|
+
tmp_path = Path(tmp)
|
|
1362
|
+
image_path = tmp_path / "auto-cmd.png"
|
|
1363
|
+
base64_path = tmp_path / "auto-cmd.base64"
|
|
1364
|
+
image_path.write_bytes(b"fake")
|
|
1365
|
+
base64_path.write_text("Zg==", encoding="utf-8")
|
|
1366
|
+
runner = Mock()
|
|
1367
|
+
runner.__enter__ = Mock(return_value=runner)
|
|
1368
|
+
runner.__exit__ = Mock(return_value=None)
|
|
1369
|
+
runner.run.return_value = SimpleNamespace(
|
|
1370
|
+
image_path=image_path,
|
|
1371
|
+
base64_path=base64_path,
|
|
1372
|
+
sample=None,
|
|
1373
|
+
)
|
|
1374
|
+
with patch(
|
|
1375
|
+
"pages.management.commands.capture_ui_screenshots.ScreenshotSpecRunner",
|
|
1376
|
+
return_value=runner,
|
|
1377
|
+
) as runner_cls:
|
|
1378
|
+
out = StringIO()
|
|
1379
|
+
call_command(
|
|
1380
|
+
"capture_ui_screenshots",
|
|
1381
|
+
"--spec",
|
|
1382
|
+
spec.slug,
|
|
1383
|
+
"--output-dir",
|
|
1384
|
+
tmp_path,
|
|
1385
|
+
stdout=out,
|
|
1386
|
+
)
|
|
1387
|
+
runner_cls.assert_called_once()
|
|
1388
|
+
runner.run.assert_called_once_with(spec)
|
|
1389
|
+
self.assertIn("Captured 'auto-cmd'", out.getvalue())
|