arthexis 0.1.9__py3-none-any.whl → 0.1.10__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.10.dist-info}/METADATA +63 -20
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/RECORD +39 -36
- config/settings.py +221 -23
- config/urls.py +6 -0
- core/admin.py +401 -35
- core/apps.py +3 -0
- core/auto_upgrade.py +57 -0
- core/backends.py +77 -3
- core/fields.py +93 -0
- core/models.py +212 -7
- core/reference_utils.py +97 -0
- core/sigil_builder.py +16 -3
- core/system.py +157 -143
- core/tasks.py +151 -8
- core/test_system_info.py +37 -1
- core/tests.py +288 -12
- core/user_data.py +103 -8
- core/views.py +257 -15
- nodes/admin.py +12 -4
- nodes/backends.py +109 -17
- nodes/models.py +205 -2
- nodes/tests.py +370 -1
- nodes/views.py +140 -7
- ocpp/admin.py +63 -3
- ocpp/consumers.py +252 -41
- ocpp/evcs.py +6 -3
- ocpp/models.py +49 -7
- ocpp/simulator.py +62 -5
- ocpp/store.py +30 -0
- ocpp/tests.py +384 -8
- ocpp/views.py +101 -76
- pages/context_processors.py +20 -0
- pages/forms.py +131 -0
- pages/tests.py +434 -13
- pages/urls.py +1 -0
- pages/views.py +334 -92
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/WHEEL +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.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
|
+
man_app, _ = Application.objects.get_or_create(name="man")
|
|
1046
|
+
man_module, _ = Module.objects.get_or_create(
|
|
1047
|
+
node_role=role, application=man_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(), "MANUALS")
|
|
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):
|
|
@@ -1116,16 +1500,22 @@ class FavoriteTests(TestCase):
|
|
|
1116
1500
|
).exists()
|
|
1117
1501
|
)
|
|
1118
1502
|
|
|
1119
|
-
def
|
|
1503
|
+
def test_dashboard_uses_change_label(self):
|
|
1120
1504
|
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
1121
1505
|
Favorite.objects.create(user=self.user, content_type=ct)
|
|
1122
1506
|
resp = self.client.get(reverse("admin:index"))
|
|
1123
|
-
self.assertContains(resp, "
|
|
1507
|
+
self.assertContains(resp, "Change Applications")
|
|
1508
|
+
self.assertContains(resp, 'target="_blank" rel="noopener noreferrer"')
|
|
1124
1509
|
|
|
1125
|
-
def
|
|
1126
|
-
Todo.objects.create(request="Check docs", url="/docs/")
|
|
1510
|
+
def test_dashboard_links_to_focus_view(self):
|
|
1511
|
+
todo = Todo.objects.create(request="Check docs", url="/docs/")
|
|
1127
1512
|
resp = self.client.get(reverse("admin:index"))
|
|
1128
|
-
|
|
1513
|
+
focus_url = reverse("todo-focus", args=[todo.pk])
|
|
1514
|
+
expected_next = quote(reverse("admin:index"))
|
|
1515
|
+
self.assertContains(
|
|
1516
|
+
resp,
|
|
1517
|
+
f'href="{focus_url}?next={expected_next}"',
|
|
1518
|
+
)
|
|
1129
1519
|
|
|
1130
1520
|
def test_dashboard_shows_todo_with_done_button(self):
|
|
1131
1521
|
todo = Todo.objects.create(request="Do thing")
|
|
@@ -1200,6 +1590,37 @@ class FavoriteTests(TestCase):
|
|
|
1200
1590
|
self.assertContains(resp, todo.request)
|
|
1201
1591
|
|
|
1202
1592
|
|
|
1593
|
+
class AdminActionListTests(TestCase):
|
|
1594
|
+
def setUp(self):
|
|
1595
|
+
User = get_user_model()
|
|
1596
|
+
User.objects.filter(username="action-admin").delete()
|
|
1597
|
+
self.user = User.objects.create_superuser(
|
|
1598
|
+
username="action-admin",
|
|
1599
|
+
password="pwd",
|
|
1600
|
+
email="action@example.com",
|
|
1601
|
+
)
|
|
1602
|
+
self.factory = RequestFactory()
|
|
1603
|
+
|
|
1604
|
+
def test_profile_actions_available_without_selection(self):
|
|
1605
|
+
from pages.templatetags.admin_extras import model_admin_actions
|
|
1606
|
+
|
|
1607
|
+
request = self.factory.get("/")
|
|
1608
|
+
request.user = self.user
|
|
1609
|
+
context = {"request": request}
|
|
1610
|
+
|
|
1611
|
+
registered = [
|
|
1612
|
+
(model._meta.app_label, model._meta.object_name)
|
|
1613
|
+
for model, admin_instance in admin.site._registry.items()
|
|
1614
|
+
if isinstance(admin_instance, ProfileAdminMixin)
|
|
1615
|
+
]
|
|
1616
|
+
|
|
1617
|
+
for app_label, object_name in registered:
|
|
1618
|
+
with self.subTest(model=f"{app_label}.{object_name}"):
|
|
1619
|
+
actions = model_admin_actions(context, app_label, object_name)
|
|
1620
|
+
labels = {action["label"] for action in actions}
|
|
1621
|
+
self.assertIn("Active Profile", labels)
|
|
1622
|
+
|
|
1623
|
+
|
|
1203
1624
|
class AdminModelGraphViewTests(TestCase):
|
|
1204
1625
|
def setUp(self):
|
|
1205
1626
|
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>/",
|