arthexis 0.1.9__py3-none-any.whl → 0.1.11__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 → arthexis-0.1.11.dist-info}/METADATA +76 -23
- arthexis-0.1.11.dist-info/RECORD +99 -0
- config/context_processors.py +1 -0
- config/settings.py +245 -26
- config/urls.py +11 -4
- core/admin.py +585 -57
- core/apps.py +29 -1
- core/auto_upgrade.py +57 -0
- core/backends.py +115 -3
- core/environment.py +23 -5
- core/fields.py +93 -0
- core/mailer.py +3 -1
- core/models.py +482 -38
- core/reference_utils.py +108 -0
- core/sigil_builder.py +23 -5
- core/sigil_resolver.py +35 -4
- core/system.py +400 -140
- core/tasks.py +151 -8
- core/temp_passwords.py +181 -0
- core/test_system_info.py +97 -1
- core/tests.py +393 -15
- core/user_data.py +154 -16
- core/views.py +499 -20
- nodes/admin.py +149 -6
- nodes/backends.py +125 -18
- nodes/dns.py +203 -0
- nodes/models.py +498 -9
- nodes/tests.py +682 -3
- nodes/views.py +154 -7
- ocpp/admin.py +63 -3
- ocpp/consumers.py +255 -41
- ocpp/evcs.py +6 -3
- ocpp/models.py +52 -7
- ocpp/reference_utils.py +42 -0
- ocpp/simulator.py +62 -5
- ocpp/store.py +30 -0
- ocpp/test_rfid.py +169 -7
- ocpp/tests.py +414 -8
- ocpp/views.py +109 -76
- pages/admin.py +9 -1
- pages/context_processors.py +24 -4
- pages/defaults.py +14 -0
- pages/forms.py +131 -0
- pages/models.py +53 -14
- pages/tests.py +450 -14
- pages/urls.py +4 -0
- pages/views.py +419 -110
- arthexis-0.1.9.dist-info/RECORD +0 -92
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/WHEEL +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/top_level.txt +0 -0
pages/tests.py
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
4
|
+
|
|
5
|
+
import django
|
|
6
|
+
|
|
7
|
+
django.setup()
|
|
8
|
+
|
|
9
|
+
from django.test import Client, RequestFactory, TestCase, override_settings
|
|
2
10
|
from django.urls import reverse
|
|
11
|
+
from django.templatetags.static import static
|
|
3
12
|
from urllib.parse import quote
|
|
4
13
|
from django.contrib.auth import get_user_model
|
|
5
14
|
from django.contrib.sites.models import Site
|
|
@@ -16,7 +25,15 @@ from pages.screenshot_specs import (
|
|
|
16
25
|
)
|
|
17
26
|
from django.apps import apps as django_apps
|
|
18
27
|
from core import mailer
|
|
19
|
-
from core.
|
|
28
|
+
from core.admin import ProfileAdminMixin
|
|
29
|
+
from core.models import (
|
|
30
|
+
AdminHistory,
|
|
31
|
+
InviteLead,
|
|
32
|
+
Package,
|
|
33
|
+
Reference,
|
|
34
|
+
ReleaseManager,
|
|
35
|
+
Todo,
|
|
36
|
+
)
|
|
20
37
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
21
38
|
import base64
|
|
22
39
|
import tempfile
|
|
@@ -33,6 +50,11 @@ from datetime import date, timedelta
|
|
|
33
50
|
from django.core import mail
|
|
34
51
|
from django.utils import timezone
|
|
35
52
|
from django.utils.text import slugify
|
|
53
|
+
from django_otp import DEVICE_ID_SESSION_KEY
|
|
54
|
+
from django_otp.oath import TOTP
|
|
55
|
+
from django_otp.plugins.otp_totp.models import TOTPDevice
|
|
56
|
+
from core.backends import TOTP_DEVICE_NAME
|
|
57
|
+
import time
|
|
36
58
|
|
|
37
59
|
from nodes.models import (
|
|
38
60
|
EmailOutbox,
|
|
@@ -58,6 +80,10 @@ class LoginViewTests(TestCase):
|
|
|
58
80
|
resp = self.client.get(reverse("pages:index"))
|
|
59
81
|
self.assertContains(resp, 'href="/login/"')
|
|
60
82
|
|
|
83
|
+
def test_login_page_shows_authenticator_toggle(self):
|
|
84
|
+
resp = self.client.get(reverse("pages:login"))
|
|
85
|
+
self.assertContains(resp, "Use Authenticator app")
|
|
86
|
+
|
|
61
87
|
def test_staff_login_redirects_admin(self):
|
|
62
88
|
resp = self.client.post(
|
|
63
89
|
reverse("pages:login"),
|
|
@@ -65,6 +91,49 @@ class LoginViewTests(TestCase):
|
|
|
65
91
|
)
|
|
66
92
|
self.assertRedirects(resp, reverse("admin:index"))
|
|
67
93
|
|
|
94
|
+
def test_login_with_authenticator_code(self):
|
|
95
|
+
device = TOTPDevice.objects.create(
|
|
96
|
+
user=self.staff,
|
|
97
|
+
name=TOTP_DEVICE_NAME,
|
|
98
|
+
confirmed=True,
|
|
99
|
+
)
|
|
100
|
+
totp = TOTP(device.bin_key, device.step, device.t0, device.digits, device.drift)
|
|
101
|
+
totp.time = time.time()
|
|
102
|
+
token = f"{totp.token():0{device.digits}d}"
|
|
103
|
+
|
|
104
|
+
resp = self.client.post(
|
|
105
|
+
reverse("pages:login"),
|
|
106
|
+
{
|
|
107
|
+
"username": "staff",
|
|
108
|
+
"auth_method": "otp",
|
|
109
|
+
"otp_token": token,
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
114
|
+
session = self.client.session
|
|
115
|
+
self.assertIn(DEVICE_ID_SESSION_KEY, session)
|
|
116
|
+
self.assertEqual(session[DEVICE_ID_SESSION_KEY], device.persistent_id)
|
|
117
|
+
|
|
118
|
+
def test_login_with_invalid_authenticator_code(self):
|
|
119
|
+
TOTPDevice.objects.create(
|
|
120
|
+
user=self.staff,
|
|
121
|
+
name=TOTP_DEVICE_NAME,
|
|
122
|
+
confirmed=True,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
resp = self.client.post(
|
|
126
|
+
reverse("pages:login"),
|
|
127
|
+
{
|
|
128
|
+
"username": "staff",
|
|
129
|
+
"auth_method": "otp",
|
|
130
|
+
"otp_token": "000000",
|
|
131
|
+
},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
self.assertEqual(resp.status_code, 200)
|
|
135
|
+
self.assertContains(resp, "authenticator code is invalid", status_code=200)
|
|
136
|
+
|
|
68
137
|
def test_already_logged_in_staff_redirects(self):
|
|
69
138
|
self.client.force_login(self.staff)
|
|
70
139
|
resp = self.client.get(reverse("pages:login"))
|
|
@@ -84,6 +153,8 @@ class LoginViewTests(TestCase):
|
|
|
84
153
|
)
|
|
85
154
|
self.assertRedirects(resp, "/nodes/list/")
|
|
86
155
|
|
|
156
|
+
|
|
157
|
+
|
|
87
158
|
@override_settings(EMAIL_BACKEND="django.core.mail.backends.dummy.EmailBackend")
|
|
88
159
|
def test_login_page_hides_request_link_without_email_backend(self):
|
|
89
160
|
resp = self.client.get(reverse("pages:login"))
|
|
@@ -97,6 +168,173 @@ class LoginViewTests(TestCase):
|
|
|
97
168
|
self.assertTrue(resp.context["can_request_invite"])
|
|
98
169
|
self.assertContains(resp, reverse("pages:request-invite"))
|
|
99
170
|
|
|
171
|
+
@override_settings(ALLOWED_HOSTS=["gway-qk32000"])
|
|
172
|
+
def test_login_allows_forwarded_https_origin(self):
|
|
173
|
+
secure_client = Client(enforce_csrf_checks=True)
|
|
174
|
+
login_url = reverse("pages:login")
|
|
175
|
+
response = secure_client.get(login_url, HTTP_HOST="gway-qk32000")
|
|
176
|
+
csrf_cookie = response.cookies["csrftoken"].value
|
|
177
|
+
submit = secure_client.post(
|
|
178
|
+
login_url,
|
|
179
|
+
{
|
|
180
|
+
"username": "staff",
|
|
181
|
+
"password": "pwd",
|
|
182
|
+
"csrfmiddlewaretoken": csrf_cookie,
|
|
183
|
+
},
|
|
184
|
+
HTTP_HOST="gway-qk32000",
|
|
185
|
+
HTTP_ORIGIN="https://gway-qk32000",
|
|
186
|
+
HTTP_X_FORWARDED_PROTO="https",
|
|
187
|
+
HTTP_REFERER="https://gway-qk32000/login/",
|
|
188
|
+
)
|
|
189
|
+
self.assertRedirects(submit, reverse("admin:index"))
|
|
190
|
+
|
|
191
|
+
@override_settings(ALLOWED_HOSTS=["10.42.0.0/16", "gway-qk32000"])
|
|
192
|
+
def test_login_allows_forwarded_origin_with_private_host_header(self):
|
|
193
|
+
secure_client = Client(enforce_csrf_checks=True)
|
|
194
|
+
login_url = reverse("pages:login")
|
|
195
|
+
response = secure_client.get(login_url, HTTP_HOST="10.42.0.2")
|
|
196
|
+
csrf_cookie = response.cookies["csrftoken"].value
|
|
197
|
+
submit = secure_client.post(
|
|
198
|
+
login_url,
|
|
199
|
+
{
|
|
200
|
+
"username": "staff",
|
|
201
|
+
"password": "pwd",
|
|
202
|
+
"csrfmiddlewaretoken": csrf_cookie,
|
|
203
|
+
},
|
|
204
|
+
HTTP_HOST="10.42.0.2",
|
|
205
|
+
HTTP_ORIGIN="https://gway-qk32000",
|
|
206
|
+
HTTP_X_FORWARDED_PROTO="https",
|
|
207
|
+
HTTP_X_FORWARDED_HOST="gway-qk32000",
|
|
208
|
+
HTTP_REFERER="https://gway-qk32000/login/",
|
|
209
|
+
)
|
|
210
|
+
self.assertRedirects(submit, reverse("admin:index"))
|
|
211
|
+
|
|
212
|
+
@override_settings(ALLOWED_HOSTS=["10.42.0.0/16", "gway-qk32000"])
|
|
213
|
+
def test_login_allows_forwarded_header_host_and_proto(self):
|
|
214
|
+
secure_client = Client(enforce_csrf_checks=True)
|
|
215
|
+
login_url = reverse("pages:login")
|
|
216
|
+
response = secure_client.get(login_url, HTTP_HOST="10.42.0.2")
|
|
217
|
+
csrf_cookie = response.cookies["csrftoken"].value
|
|
218
|
+
submit = secure_client.post(
|
|
219
|
+
login_url,
|
|
220
|
+
{
|
|
221
|
+
"username": "staff",
|
|
222
|
+
"password": "pwd",
|
|
223
|
+
"csrfmiddlewaretoken": csrf_cookie,
|
|
224
|
+
},
|
|
225
|
+
HTTP_HOST="10.42.0.2",
|
|
226
|
+
HTTP_ORIGIN="https://gway-qk32000",
|
|
227
|
+
HTTP_FORWARDED="proto=https;host=gway-qk32000",
|
|
228
|
+
HTTP_REFERER="https://gway-qk32000/login/",
|
|
229
|
+
)
|
|
230
|
+
self.assertRedirects(submit, reverse("admin:index"))
|
|
231
|
+
|
|
232
|
+
@override_settings(ALLOWED_HOSTS=["10.42.0.0/16", "gway-qk32000"])
|
|
233
|
+
def test_login_allows_forwarded_referer_without_origin(self):
|
|
234
|
+
secure_client = Client(enforce_csrf_checks=True)
|
|
235
|
+
login_url = reverse("pages:login")
|
|
236
|
+
response = secure_client.get(login_url, HTTP_HOST="10.42.0.2")
|
|
237
|
+
csrf_cookie = response.cookies["csrftoken"].value
|
|
238
|
+
submit = secure_client.post(
|
|
239
|
+
login_url,
|
|
240
|
+
{
|
|
241
|
+
"username": "staff",
|
|
242
|
+
"password": "pwd",
|
|
243
|
+
"csrfmiddlewaretoken": csrf_cookie,
|
|
244
|
+
},
|
|
245
|
+
HTTP_HOST="10.42.0.2",
|
|
246
|
+
HTTP_X_FORWARDED_PROTO="https",
|
|
247
|
+
HTTP_X_FORWARDED_HOST="gway-qk32000",
|
|
248
|
+
HTTP_REFERER="https://gway-qk32000/login/",
|
|
249
|
+
)
|
|
250
|
+
self.assertRedirects(submit, reverse("admin:index"))
|
|
251
|
+
|
|
252
|
+
@override_settings(ALLOWED_HOSTS=["gway-qk32000"])
|
|
253
|
+
def test_login_allows_forwarded_origin_with_explicit_port(self):
|
|
254
|
+
secure_client = Client(enforce_csrf_checks=True)
|
|
255
|
+
login_url = reverse("pages:login")
|
|
256
|
+
response = secure_client.get(login_url, HTTP_HOST="gway-qk32000")
|
|
257
|
+
csrf_cookie = response.cookies["csrftoken"].value
|
|
258
|
+
submit = secure_client.post(
|
|
259
|
+
login_url,
|
|
260
|
+
{
|
|
261
|
+
"username": "staff",
|
|
262
|
+
"password": "pwd",
|
|
263
|
+
"csrfmiddlewaretoken": csrf_cookie,
|
|
264
|
+
},
|
|
265
|
+
HTTP_HOST="gway-qk32000",
|
|
266
|
+
HTTP_ORIGIN="https://gway-qk32000:4443",
|
|
267
|
+
HTTP_X_FORWARDED_PROTO="https",
|
|
268
|
+
HTTP_X_FORWARDED_HOST="gway-qk32000:4443",
|
|
269
|
+
HTTP_REFERER="https://gway-qk32000:4443/login/",
|
|
270
|
+
)
|
|
271
|
+
self.assertRedirects(submit, reverse("admin:index"))
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class AuthenticatorSetupTests(TestCase):
|
|
275
|
+
def setUp(self):
|
|
276
|
+
self.client = Client()
|
|
277
|
+
User = get_user_model()
|
|
278
|
+
self.staff = User.objects.create_user(
|
|
279
|
+
username="staffer", password="pwd", is_staff=True
|
|
280
|
+
)
|
|
281
|
+
Site.objects.update_or_create(id=1, defaults={"name": "Terminal"})
|
|
282
|
+
self.client.force_login(self.staff)
|
|
283
|
+
|
|
284
|
+
def _current_token(self, device):
|
|
285
|
+
totp = TOTP(device.bin_key, device.step, device.t0, device.digits, device.drift)
|
|
286
|
+
totp.time = time.time()
|
|
287
|
+
return f"{totp.token():0{device.digits}d}"
|
|
288
|
+
|
|
289
|
+
def test_generate_creates_pending_device(self):
|
|
290
|
+
resp = self.client.post(
|
|
291
|
+
reverse("pages:authenticator-setup"), {"action": "generate"}
|
|
292
|
+
)
|
|
293
|
+
self.assertRedirects(resp, reverse("pages:authenticator-setup"))
|
|
294
|
+
device = TOTPDevice.objects.get(user=self.staff)
|
|
295
|
+
self.assertFalse(device.confirmed)
|
|
296
|
+
self.assertEqual(device.name, TOTP_DEVICE_NAME)
|
|
297
|
+
|
|
298
|
+
def test_device_config_url_includes_issuer_prefix(self):
|
|
299
|
+
self.client.post(reverse("pages:authenticator-setup"), {"action": "generate"})
|
|
300
|
+
device = TOTPDevice.objects.get(user=self.staff)
|
|
301
|
+
config_url = device.config_url
|
|
302
|
+
label = quote(f"{settings.OTP_TOTP_ISSUER}:{self.staff.username}")
|
|
303
|
+
self.assertIn(label, config_url)
|
|
304
|
+
self.assertIn(f"issuer={quote(settings.OTP_TOTP_ISSUER)}", config_url)
|
|
305
|
+
|
|
306
|
+
def test_pending_device_context_includes_qr(self):
|
|
307
|
+
self.client.post(reverse("pages:authenticator-setup"), {"action": "generate"})
|
|
308
|
+
resp = self.client.get(reverse("pages:authenticator-setup"))
|
|
309
|
+
self.assertEqual(resp.status_code, 200)
|
|
310
|
+
self.assertTrue(resp.context["qr_data_uri"].startswith("data:image/png;base64,"))
|
|
311
|
+
self.assertTrue(resp.context["manual_key"])
|
|
312
|
+
|
|
313
|
+
def test_confirm_pending_device(self):
|
|
314
|
+
self.client.post(reverse("pages:authenticator-setup"), {"action": "generate"})
|
|
315
|
+
device = TOTPDevice.objects.get(user=self.staff)
|
|
316
|
+
token = self._current_token(device)
|
|
317
|
+
resp = self.client.post(
|
|
318
|
+
reverse("pages:authenticator-setup"),
|
|
319
|
+
{"action": "confirm", "token": token},
|
|
320
|
+
)
|
|
321
|
+
self.assertRedirects(resp, reverse("pages:authenticator-setup"))
|
|
322
|
+
device.refresh_from_db()
|
|
323
|
+
self.assertTrue(device.confirmed)
|
|
324
|
+
|
|
325
|
+
def test_remove_device(self):
|
|
326
|
+
self.client.post(reverse("pages:authenticator-setup"), {"action": "generate"})
|
|
327
|
+
device = TOTPDevice.objects.get(user=self.staff)
|
|
328
|
+
token = self._current_token(device)
|
|
329
|
+
self.client.post(
|
|
330
|
+
reverse("pages:authenticator-setup"),
|
|
331
|
+
{"action": "confirm", "token": token},
|
|
332
|
+
)
|
|
333
|
+
resp = self.client.post(
|
|
334
|
+
reverse("pages:authenticator-setup"), {"action": "remove"}
|
|
335
|
+
)
|
|
336
|
+
self.assertRedirects(resp, reverse("pages:authenticator-setup"))
|
|
337
|
+
self.assertFalse(TOTPDevice.objects.filter(user=self.staff).exists())
|
|
100
338
|
|
|
101
339
|
@override_settings(EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend")
|
|
102
340
|
class InvitationTests(TestCase):
|
|
@@ -336,6 +574,56 @@ class AdminBadgesTests(TestCase):
|
|
|
336
574
|
self.assertContains(resp, f'href="{node_change}"')
|
|
337
575
|
|
|
338
576
|
|
|
577
|
+
class AdminDashboardAppListTests(TestCase):
|
|
578
|
+
def setUp(self):
|
|
579
|
+
self.client = Client()
|
|
580
|
+
User = get_user_model()
|
|
581
|
+
self.admin = User.objects.create_superuser(
|
|
582
|
+
username="dashboard_admin", password="pwd", email="admin@example.com"
|
|
583
|
+
)
|
|
584
|
+
self.client.force_login(self.admin)
|
|
585
|
+
Site.objects.update_or_create(
|
|
586
|
+
id=1, defaults={"name": "test", "domain": "testserver"}
|
|
587
|
+
)
|
|
588
|
+
self.locks_dir = Path(settings.BASE_DIR) / "locks"
|
|
589
|
+
self.locks_dir.mkdir(parents=True, exist_ok=True)
|
|
590
|
+
self.celery_lock = self.locks_dir / "celery.lck"
|
|
591
|
+
if self.celery_lock.exists():
|
|
592
|
+
self.celery_lock.unlink()
|
|
593
|
+
self.addCleanup(self._remove_celery_lock)
|
|
594
|
+
self.node, _ = Node.objects.update_or_create(
|
|
595
|
+
mac_address=Node.get_current_mac(),
|
|
596
|
+
defaults={
|
|
597
|
+
"hostname": socket.gethostname(),
|
|
598
|
+
"address": socket.gethostbyname(socket.gethostname()),
|
|
599
|
+
"base_path": settings.BASE_DIR,
|
|
600
|
+
"port": 8000,
|
|
601
|
+
},
|
|
602
|
+
)
|
|
603
|
+
self.node.features.clear()
|
|
604
|
+
|
|
605
|
+
def _remove_celery_lock(self):
|
|
606
|
+
try:
|
|
607
|
+
self.celery_lock.unlink()
|
|
608
|
+
except FileNotFoundError:
|
|
609
|
+
pass
|
|
610
|
+
|
|
611
|
+
def test_horologia_hidden_without_celery_feature(self):
|
|
612
|
+
resp = self.client.get(reverse("admin:index"))
|
|
613
|
+
self.assertNotContains(resp, "5. Horologia MODELS")
|
|
614
|
+
|
|
615
|
+
def test_horologia_visible_with_celery_feature(self):
|
|
616
|
+
feature = NodeFeature.objects.create(slug="celery-queue", display="Celery Queue")
|
|
617
|
+
NodeFeatureAssignment.objects.create(node=self.node, feature=feature)
|
|
618
|
+
resp = self.client.get(reverse("admin:index"))
|
|
619
|
+
self.assertContains(resp, "5. Horologia MODELS")
|
|
620
|
+
|
|
621
|
+
def test_horologia_visible_with_celery_lock(self):
|
|
622
|
+
self.celery_lock.write_text("")
|
|
623
|
+
resp = self.client.get(reverse("admin:index"))
|
|
624
|
+
self.assertContains(resp, "5. Horologia MODELS")
|
|
625
|
+
|
|
626
|
+
|
|
339
627
|
class AdminSidebarTests(TestCase):
|
|
340
628
|
def setUp(self):
|
|
341
629
|
self.client = Client()
|
|
@@ -440,6 +728,7 @@ class ViewHistoryAdminTests(TestCase):
|
|
|
440
728
|
resp = self.client.get(reverse("admin:pages_viewhistory_traffic_graph"))
|
|
441
729
|
self.assertContains(resp, "viewhistory-chart")
|
|
442
730
|
self.assertContains(resp, reverse("admin:pages_viewhistory_changelist"))
|
|
731
|
+
self.assertContains(resp, static("core/vendor/chart.umd.min.js"))
|
|
443
732
|
|
|
444
733
|
def test_graph_data_endpoint(self):
|
|
445
734
|
self._create_history("/", count=2)
|
|
@@ -461,6 +750,7 @@ class ViewHistoryAdminTests(TestCase):
|
|
|
461
750
|
resp = self.client.get(reverse("admin:index"))
|
|
462
751
|
self.assertContains(resp, "viewhistory-mini-module")
|
|
463
752
|
self.assertContains(resp, reverse("admin:pages_viewhistory_traffic_graph"))
|
|
753
|
+
self.assertContains(resp, static("core/vendor/chart.umd.min.js"))
|
|
464
754
|
|
|
465
755
|
|
|
466
756
|
class AdminModelStatusTests(TestCase):
|
|
@@ -657,12 +947,6 @@ class ConstellationNavTests(TestCase):
|
|
|
657
947
|
"fixtures",
|
|
658
948
|
"constellation__module_ocpp.json",
|
|
659
949
|
),
|
|
660
|
-
Path(
|
|
661
|
-
settings.BASE_DIR,
|
|
662
|
-
"pages",
|
|
663
|
-
"fixtures",
|
|
664
|
-
"constellation__module_rfid.json",
|
|
665
|
-
),
|
|
666
950
|
Path(
|
|
667
951
|
settings.BASE_DIR,
|
|
668
952
|
"pages",
|
|
@@ -702,6 +986,106 @@ class ConstellationNavTests(TestCase):
|
|
|
702
986
|
is_deleted=False,
|
|
703
987
|
).exists()
|
|
704
988
|
)
|
|
989
|
+
ocpp_module = next(
|
|
990
|
+
module
|
|
991
|
+
for module in resp.context["nav_modules"]
|
|
992
|
+
if module.menu_label.upper() == "CHARGERS"
|
|
993
|
+
)
|
|
994
|
+
landing_labels = [landing.label for landing in ocpp_module.enabled_landings]
|
|
995
|
+
self.assertIn("RFID Tag Validator", landing_labels)
|
|
996
|
+
|
|
997
|
+
def test_ocpp_dashboard_visible(self):
|
|
998
|
+
resp = self.client.get(reverse("pages:index"))
|
|
999
|
+
self.assertContains(resp, 'href="/ocpp/"')
|
|
1000
|
+
|
|
1001
|
+
def test_header_links_visible_when_defined(self):
|
|
1002
|
+
Reference.objects.create(
|
|
1003
|
+
alt_text="Console",
|
|
1004
|
+
value="https://example.com/console",
|
|
1005
|
+
show_in_header=True,
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
resp = self.client.get(reverse("pages:index"))
|
|
1009
|
+
|
|
1010
|
+
self.assertIn("header_references", resp.context)
|
|
1011
|
+
self.assertTrue(resp.context["header_references"])
|
|
1012
|
+
self.assertContains(resp, "LINKS")
|
|
1013
|
+
self.assertContains(resp, 'href="https://example.com/console"')
|
|
1014
|
+
|
|
1015
|
+
def test_header_links_hidden_when_flag_false(self):
|
|
1016
|
+
Reference.objects.create(
|
|
1017
|
+
alt_text="Hidden",
|
|
1018
|
+
value="https://example.com/hidden",
|
|
1019
|
+
show_in_header=False,
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
resp = self.client.get(reverse("pages:index"))
|
|
1023
|
+
|
|
1024
|
+
self.assertIn("header_references", resp.context)
|
|
1025
|
+
self.assertFalse(resp.context["header_references"])
|
|
1026
|
+
self.assertNotContains(resp, "https://example.com/hidden")
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
class PowerNavTests(TestCase):
|
|
1030
|
+
def setUp(self):
|
|
1031
|
+
self.client = Client()
|
|
1032
|
+
role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
1033
|
+
Node.objects.update_or_create(
|
|
1034
|
+
mac_address=Node.get_current_mac(),
|
|
1035
|
+
defaults={"hostname": "localhost", "address": "127.0.0.1", "role": role},
|
|
1036
|
+
)
|
|
1037
|
+
Site.objects.update_or_create(
|
|
1038
|
+
id=1, defaults={"domain": "testserver", "name": ""}
|
|
1039
|
+
)
|
|
1040
|
+
awg_app, _ = Application.objects.get_or_create(name="awg")
|
|
1041
|
+
awg_module, _ = Module.objects.get_or_create(
|
|
1042
|
+
node_role=role, application=awg_app, path="/awg/"
|
|
1043
|
+
)
|
|
1044
|
+
awg_module.create_landings()
|
|
1045
|
+
manuals_app, _ = Application.objects.get_or_create(name="pages")
|
|
1046
|
+
man_module, _ = Module.objects.get_or_create(
|
|
1047
|
+
node_role=role, application=manuals_app, path="/man/"
|
|
1048
|
+
)
|
|
1049
|
+
man_module.create_landings()
|
|
1050
|
+
User = get_user_model()
|
|
1051
|
+
self.user = User.objects.create_user("user", password="pw")
|
|
1052
|
+
|
|
1053
|
+
def test_power_pill_lists_calculators(self):
|
|
1054
|
+
resp = self.client.get(reverse("pages:index"))
|
|
1055
|
+
power_module = None
|
|
1056
|
+
for module in resp.context["nav_modules"]:
|
|
1057
|
+
if module.path == "/awg/":
|
|
1058
|
+
power_module = module
|
|
1059
|
+
break
|
|
1060
|
+
self.assertIsNotNone(power_module)
|
|
1061
|
+
self.assertEqual(power_module.menu_label.upper(), "CALCULATE")
|
|
1062
|
+
landing_labels = {landing.label for landing in power_module.enabled_landings}
|
|
1063
|
+
self.assertIn("AWG Calculator", landing_labels)
|
|
1064
|
+
|
|
1065
|
+
def test_manual_pill_label(self):
|
|
1066
|
+
resp = self.client.get(reverse("pages:index"))
|
|
1067
|
+
manuals_module = None
|
|
1068
|
+
for module in resp.context["nav_modules"]:
|
|
1069
|
+
if module.path == "/man/":
|
|
1070
|
+
manuals_module = module
|
|
1071
|
+
break
|
|
1072
|
+
self.assertIsNotNone(manuals_module)
|
|
1073
|
+
self.assertEqual(manuals_module.menu_label.upper(), "MANUAL")
|
|
1074
|
+
landing_labels = {landing.label for landing in manuals_module.enabled_landings}
|
|
1075
|
+
self.assertIn("Manuals", landing_labels)
|
|
1076
|
+
|
|
1077
|
+
def test_energy_tariff_visible_when_logged_in(self):
|
|
1078
|
+
self.client.force_login(self.user)
|
|
1079
|
+
resp = self.client.get(reverse("pages:index"))
|
|
1080
|
+
power_module = None
|
|
1081
|
+
for module in resp.context["nav_modules"]:
|
|
1082
|
+
if module.path == "/awg/":
|
|
1083
|
+
power_module = module
|
|
1084
|
+
break
|
|
1085
|
+
self.assertIsNotNone(power_module)
|
|
1086
|
+
landing_labels = {landing.label for landing in power_module.enabled_landings}
|
|
1087
|
+
self.assertIn("AWG Calculator", landing_labels)
|
|
1088
|
+
self.assertIn("Energy Tariff Calculator", landing_labels)
|
|
705
1089
|
|
|
706
1090
|
|
|
707
1091
|
class StaffNavVisibilityTests(TestCase):
|
|
@@ -779,6 +1163,13 @@ class ApplicationAdminDisplayTests(TestCase):
|
|
|
779
1163
|
config = django_apps.get_app_config("ocpp")
|
|
780
1164
|
self.assertContains(resp, config.verbose_name)
|
|
781
1165
|
|
|
1166
|
+
def test_changelist_shows_description(self):
|
|
1167
|
+
Application.objects.create(
|
|
1168
|
+
name="awg", description="Power, Energy and Cost calculations."
|
|
1169
|
+
)
|
|
1170
|
+
resp = self.client.get(reverse("admin:pages_application_changelist"))
|
|
1171
|
+
self.assertContains(resp, "Power, Energy and Cost calculations.")
|
|
1172
|
+
|
|
782
1173
|
|
|
783
1174
|
class LandingCreationTests(TestCase):
|
|
784
1175
|
def setUp(self):
|
|
@@ -847,8 +1238,16 @@ class RFIDPageTests(TestCase):
|
|
|
847
1238
|
Site.objects.update_or_create(
|
|
848
1239
|
id=1, defaults={"domain": "testserver", "name": "pages"}
|
|
849
1240
|
)
|
|
1241
|
+
User = get_user_model()
|
|
1242
|
+
self.user = User.objects.create_user("rfid-user", password="pwd")
|
|
1243
|
+
|
|
1244
|
+
def test_page_redirects_when_anonymous(self):
|
|
1245
|
+
resp = self.client.get(reverse("rfid-reader"))
|
|
1246
|
+
self.assertEqual(resp.status_code, 302)
|
|
1247
|
+
self.assertIn(reverse("pages:login"), resp.url)
|
|
850
1248
|
|
|
851
|
-
def
|
|
1249
|
+
def test_page_renders_for_authenticated_user(self):
|
|
1250
|
+
self.client.force_login(self.user)
|
|
852
1251
|
resp = self.client.get(reverse("rfid-reader"))
|
|
853
1252
|
self.assertContains(resp, "Scanner ready")
|
|
854
1253
|
|
|
@@ -1116,16 +1515,22 @@ class FavoriteTests(TestCase):
|
|
|
1116
1515
|
).exists()
|
|
1117
1516
|
)
|
|
1118
1517
|
|
|
1119
|
-
def
|
|
1518
|
+
def test_dashboard_uses_change_label(self):
|
|
1120
1519
|
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
1121
1520
|
Favorite.objects.create(user=self.user, content_type=ct)
|
|
1122
1521
|
resp = self.client.get(reverse("admin:index"))
|
|
1123
|
-
self.assertContains(resp, "
|
|
1522
|
+
self.assertContains(resp, "Change Applications")
|
|
1523
|
+
self.assertContains(resp, 'target="_blank" rel="noopener noreferrer"')
|
|
1124
1524
|
|
|
1125
|
-
def
|
|
1126
|
-
Todo.objects.create(request="Check docs", url="/docs/")
|
|
1525
|
+
def test_dashboard_links_to_focus_view(self):
|
|
1526
|
+
todo = Todo.objects.create(request="Check docs", url="/docs/")
|
|
1127
1527
|
resp = self.client.get(reverse("admin:index"))
|
|
1128
|
-
|
|
1528
|
+
focus_url = reverse("todo-focus", args=[todo.pk])
|
|
1529
|
+
expected_next = quote(reverse("admin:index"))
|
|
1530
|
+
self.assertContains(
|
|
1531
|
+
resp,
|
|
1532
|
+
f'href="{focus_url}?next={expected_next}"',
|
|
1533
|
+
)
|
|
1129
1534
|
|
|
1130
1535
|
def test_dashboard_shows_todo_with_done_button(self):
|
|
1131
1536
|
todo = Todo.objects.create(request="Do thing")
|
|
@@ -1200,6 +1605,37 @@ class FavoriteTests(TestCase):
|
|
|
1200
1605
|
self.assertContains(resp, todo.request)
|
|
1201
1606
|
|
|
1202
1607
|
|
|
1608
|
+
class AdminActionListTests(TestCase):
|
|
1609
|
+
def setUp(self):
|
|
1610
|
+
User = get_user_model()
|
|
1611
|
+
User.objects.filter(username="action-admin").delete()
|
|
1612
|
+
self.user = User.objects.create_superuser(
|
|
1613
|
+
username="action-admin",
|
|
1614
|
+
password="pwd",
|
|
1615
|
+
email="action@example.com",
|
|
1616
|
+
)
|
|
1617
|
+
self.factory = RequestFactory()
|
|
1618
|
+
|
|
1619
|
+
def test_profile_actions_available_without_selection(self):
|
|
1620
|
+
from pages.templatetags.admin_extras import model_admin_actions
|
|
1621
|
+
|
|
1622
|
+
request = self.factory.get("/")
|
|
1623
|
+
request.user = self.user
|
|
1624
|
+
context = {"request": request}
|
|
1625
|
+
|
|
1626
|
+
registered = [
|
|
1627
|
+
(model._meta.app_label, model._meta.object_name)
|
|
1628
|
+
for model, admin_instance in admin.site._registry.items()
|
|
1629
|
+
if isinstance(admin_instance, ProfileAdminMixin)
|
|
1630
|
+
]
|
|
1631
|
+
|
|
1632
|
+
for app_label, object_name in registered:
|
|
1633
|
+
with self.subTest(model=f"{app_label}.{object_name}"):
|
|
1634
|
+
actions = model_admin_actions(context, app_label, object_name)
|
|
1635
|
+
labels = {action["label"] for action in actions}
|
|
1636
|
+
self.assertIn("Active Profile", labels)
|
|
1637
|
+
|
|
1638
|
+
|
|
1203
1639
|
class AdminModelGraphViewTests(TestCase):
|
|
1204
1640
|
def setUp(self):
|
|
1205
1641
|
self.client = Client()
|
pages/urls.py
CHANGED
|
@@ -10,6 +10,7 @@ urlpatterns = [
|
|
|
10
10
|
path("client-report/", views.client_report, name="client-report"),
|
|
11
11
|
path("release-checklist", views.release_checklist, name="release-checklist"),
|
|
12
12
|
path("login/", views.login_view, name="login"),
|
|
13
|
+
path("authenticator/setup/", views.authenticator_setup, name="authenticator-setup"),
|
|
13
14
|
path("request-invite/", views.request_invite, name="request-invite"),
|
|
14
15
|
path(
|
|
15
16
|
"invitation/<uidb64>/<token>/",
|
|
@@ -17,4 +18,7 @@ urlpatterns = [
|
|
|
17
18
|
name="invitation-login",
|
|
18
19
|
),
|
|
19
20
|
path("datasette-auth/", views.datasette_auth, name="datasette-auth"),
|
|
21
|
+
path("man/", views.manual_list, name="manual-list"),
|
|
22
|
+
path("man/<slug:slug>/", views.manual_detail, name="manual-detail"),
|
|
23
|
+
path("man/<slug:slug>/pdf/", views.manual_pdf, name="manual-pdf"),
|
|
20
24
|
]
|