arthexis 0.1.16__py3-none-any.whl → 0.1.28__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of arthexis might be problematic. Click here for more details.
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/METADATA +95 -41
- arthexis-0.1.28.dist-info/RECORD +112 -0
- config/asgi.py +1 -15
- config/middleware.py +47 -1
- config/settings.py +21 -30
- config/settings_helpers.py +176 -1
- config/urls.py +69 -1
- core/admin.py +805 -473
- core/apps.py +6 -8
- core/auto_upgrade.py +19 -4
- core/backends.py +13 -3
- core/celery_utils.py +73 -0
- core/changelog.py +66 -5
- core/environment.py +4 -5
- core/models.py +1825 -218
- core/notifications.py +1 -1
- core/reference_utils.py +10 -11
- core/release.py +55 -7
- core/sigil_builder.py +2 -2
- core/sigil_resolver.py +1 -66
- core/system.py +285 -4
- core/tasks.py +439 -138
- core/test_system_info.py +43 -5
- core/tests.py +516 -18
- core/user_data.py +94 -21
- core/views.py +348 -186
- nodes/admin.py +904 -67
- nodes/apps.py +12 -1
- nodes/feature_checks.py +30 -0
- nodes/models.py +800 -127
- nodes/rfid_sync.py +1 -1
- nodes/tasks.py +98 -3
- nodes/tests.py +1381 -152
- nodes/urls.py +15 -1
- nodes/utils.py +51 -3
- nodes/views.py +1382 -152
- ocpp/admin.py +1970 -152
- ocpp/consumers.py +839 -34
- ocpp/models.py +968 -17
- ocpp/network.py +398 -0
- ocpp/store.py +411 -43
- ocpp/tasks.py +261 -3
- ocpp/test_export_import.py +1 -0
- ocpp/test_rfid.py +194 -6
- ocpp/tests.py +1918 -87
- ocpp/transactions_io.py +9 -1
- ocpp/urls.py +8 -3
- ocpp/views.py +700 -53
- pages/admin.py +262 -30
- pages/apps.py +35 -0
- pages/context_processors.py +28 -21
- pages/defaults.py +1 -1
- pages/forms.py +31 -8
- pages/middleware.py +6 -2
- pages/models.py +86 -2
- pages/module_defaults.py +5 -5
- pages/site_config.py +137 -0
- pages/tests.py +1050 -126
- pages/urls.py +14 -2
- pages/utils.py +70 -0
- pages/views.py +622 -56
- arthexis-0.1.16.dist-info/RECORD +0 -111
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/WHEEL +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/top_level.txt +0 -0
core/tests.py
CHANGED
|
@@ -5,9 +5,11 @@ import django
|
|
|
5
5
|
|
|
6
6
|
django.setup()
|
|
7
7
|
|
|
8
|
+
from django import forms
|
|
8
9
|
from django.test import Client, TestCase, RequestFactory, override_settings
|
|
9
10
|
from django.urls import reverse
|
|
10
11
|
from django.http import HttpRequest
|
|
12
|
+
from django.contrib import messages
|
|
11
13
|
import csv
|
|
12
14
|
import json
|
|
13
15
|
import importlib.util
|
|
@@ -20,6 +22,7 @@ import types
|
|
|
20
22
|
from glob import glob
|
|
21
23
|
from datetime import datetime, timedelta, timezone as datetime_timezone
|
|
22
24
|
import tempfile
|
|
25
|
+
from io import StringIO
|
|
23
26
|
from urllib.parse import quote
|
|
24
27
|
|
|
25
28
|
from django.utils import timezone
|
|
@@ -49,6 +52,7 @@ from core.admin import (
|
|
|
49
52
|
PackageReleaseAdmin,
|
|
50
53
|
PackageAdmin,
|
|
51
54
|
RFIDResource,
|
|
55
|
+
SecurityGroupAdmin,
|
|
52
56
|
UserAdmin,
|
|
53
57
|
USER_PROFILE_INLINES,
|
|
54
58
|
)
|
|
@@ -57,6 +61,7 @@ from nodes.models import ContentSample
|
|
|
57
61
|
|
|
58
62
|
from django.core.exceptions import ValidationError
|
|
59
63
|
from django.core.management import call_command
|
|
64
|
+
from django.core.management.base import CommandError
|
|
60
65
|
from django.db import IntegrityError
|
|
61
66
|
from .backends import LocalhostAdminBackend
|
|
62
67
|
from core.views import (
|
|
@@ -306,6 +311,16 @@ class UserAdminInlineTests(TestCase):
|
|
|
306
311
|
self.assertEqual(len(other_profiles), len(USER_PROFILE_INLINES))
|
|
307
312
|
|
|
308
313
|
|
|
314
|
+
class SecurityGroupAdminTests(TestCase):
|
|
315
|
+
def setUp(self):
|
|
316
|
+
self.site = AdminSite()
|
|
317
|
+
self.admin = SecurityGroupAdmin(SecurityGroup, self.site)
|
|
318
|
+
|
|
319
|
+
def test_search_fields_include_name_and_parent(self):
|
|
320
|
+
self.assertIn("name", self.admin.search_fields)
|
|
321
|
+
self.assertIn("parent__name", self.admin.search_fields)
|
|
322
|
+
|
|
323
|
+
|
|
309
324
|
class RFIDLoginTests(TestCase):
|
|
310
325
|
def setUp(self):
|
|
311
326
|
self.client = Client()
|
|
@@ -321,7 +336,9 @@ class RFIDLoginTests(TestCase):
|
|
|
321
336
|
content_type="application/json",
|
|
322
337
|
)
|
|
323
338
|
self.assertEqual(response.status_code, 200)
|
|
324
|
-
|
|
339
|
+
payload = response.json()
|
|
340
|
+
self.assertEqual(payload["username"], "alice")
|
|
341
|
+
self.assertEqual(payload["redirect"], "/")
|
|
325
342
|
|
|
326
343
|
def test_rfid_login_invalid(self):
|
|
327
344
|
response = self.client.post(
|
|
@@ -345,6 +362,7 @@ class RFIDLoginTests(TestCase):
|
|
|
345
362
|
)
|
|
346
363
|
|
|
347
364
|
self.assertEqual(response.status_code, 200)
|
|
365
|
+
self.assertEqual(response.json().get("redirect"), "/")
|
|
348
366
|
run_args, run_kwargs = mock_run.call_args
|
|
349
367
|
self.assertEqual(run_args[0], "echo ok")
|
|
350
368
|
self.assertTrue(run_kwargs.get("shell"))
|
|
@@ -381,6 +399,7 @@ class RFIDLoginTests(TestCase):
|
|
|
381
399
|
)
|
|
382
400
|
|
|
383
401
|
self.assertEqual(response.status_code, 200)
|
|
402
|
+
self.assertEqual(response.json().get("redirect"), "/")
|
|
384
403
|
mock_popen.assert_called_once()
|
|
385
404
|
args, kwargs = mock_popen.call_args
|
|
386
405
|
self.assertEqual(args[0], "echo welcome")
|
|
@@ -407,6 +426,24 @@ class RFIDLoginTests(TestCase):
|
|
|
407
426
|
self.assertEqual(response.status_code, 401)
|
|
408
427
|
mock_popen.assert_not_called()
|
|
409
428
|
|
|
429
|
+
def test_rfid_login_uses_next_redirect(self):
|
|
430
|
+
response = self.client.post(
|
|
431
|
+
reverse("rfid-login"),
|
|
432
|
+
data={"rfid": "CARD123", "next": "/dashboard/"},
|
|
433
|
+
content_type="application/json",
|
|
434
|
+
)
|
|
435
|
+
self.assertEqual(response.status_code, 200)
|
|
436
|
+
self.assertEqual(response.json().get("redirect"), "/dashboard/")
|
|
437
|
+
|
|
438
|
+
def test_rfid_login_ignores_unsafe_redirect(self):
|
|
439
|
+
response = self.client.post(
|
|
440
|
+
reverse("rfid-login"),
|
|
441
|
+
data={"rfid": "CARD123", "next": "https://example.com/"},
|
|
442
|
+
content_type="application/json",
|
|
443
|
+
)
|
|
444
|
+
self.assertEqual(response.status_code, 200)
|
|
445
|
+
self.assertEqual(response.json().get("redirect"), "/")
|
|
446
|
+
|
|
410
447
|
|
|
411
448
|
class RFIDBatchApiTests(TestCase):
|
|
412
449
|
def setUp(self):
|
|
@@ -439,6 +476,140 @@ class RFIDBatchApiTests(TestCase):
|
|
|
439
476
|
},
|
|
440
477
|
)
|
|
441
478
|
|
|
479
|
+
|
|
480
|
+
class UserAdminPasswordChangeTests(TestCase):
|
|
481
|
+
def setUp(self):
|
|
482
|
+
self.client = Client()
|
|
483
|
+
self.admin = User.objects.create_superuser(
|
|
484
|
+
username="admin", email="admin@example.com", password="adminpass"
|
|
485
|
+
)
|
|
486
|
+
self.user = User.objects.create_user(username="target", password="oldpass")
|
|
487
|
+
self.client.force_login(self.admin)
|
|
488
|
+
|
|
489
|
+
def test_change_password_form_excludes_rfid_field(self):
|
|
490
|
+
url = reverse("admin:core_user_password_change", args=[self.user.pk])
|
|
491
|
+
response = self.client.get(url)
|
|
492
|
+
self.assertEqual(response.status_code, 200)
|
|
493
|
+
self.assertNotContains(response, 'name="rfid"')
|
|
494
|
+
self.assertNotContains(response, "Login RFID")
|
|
495
|
+
|
|
496
|
+
def test_change_password_does_not_alter_existing_rfid_assignment(self):
|
|
497
|
+
account = EnergyAccount.objects.create(user=self.user, name="TARGET")
|
|
498
|
+
tag = RFID.objects.create(rfid="CARD778")
|
|
499
|
+
account.rfids.add(tag)
|
|
500
|
+
url = reverse("admin:core_user_password_change", args=[self.user.pk])
|
|
501
|
+
response = self.client.post(
|
|
502
|
+
url,
|
|
503
|
+
{
|
|
504
|
+
"new_password1": "NewStrongPass123",
|
|
505
|
+
"new_password2": "NewStrongPass123",
|
|
506
|
+
},
|
|
507
|
+
)
|
|
508
|
+
self.assertRedirects(
|
|
509
|
+
response, reverse("admin:core_user_change", args=[self.user.pk])
|
|
510
|
+
)
|
|
511
|
+
account.refresh_from_db()
|
|
512
|
+
self.assertTrue(account.rfids.filter(pk=tag.pk).exists())
|
|
513
|
+
self.user.refresh_from_db()
|
|
514
|
+
self.assertTrue(self.user.check_password("NewStrongPass123"))
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
class UserAdminLoginRFIDTests(TestCase):
|
|
518
|
+
def setUp(self):
|
|
519
|
+
self.client = Client()
|
|
520
|
+
self.admin = User.objects.create_superuser(
|
|
521
|
+
username="admin", email="admin@example.com", password="adminpass"
|
|
522
|
+
)
|
|
523
|
+
self.user = User.objects.create_user(username="target", password="oldpass")
|
|
524
|
+
self.client.force_login(self.admin)
|
|
525
|
+
|
|
526
|
+
def _build_change_form_data(self, response, **overrides):
|
|
527
|
+
form = response.context["adminform"].form
|
|
528
|
+
data = {}
|
|
529
|
+
for name, field in form.fields.items():
|
|
530
|
+
if isinstance(field, forms.ModelMultipleChoiceField):
|
|
531
|
+
initial = form.initial.get(name, field.initial) or []
|
|
532
|
+
data[name] = [str(value.pk) for value in initial]
|
|
533
|
+
elif isinstance(field, forms.BooleanField):
|
|
534
|
+
initial = form.initial.get(name, field.initial)
|
|
535
|
+
if initial:
|
|
536
|
+
data[name] = "on"
|
|
537
|
+
elif name != "login_rfid":
|
|
538
|
+
value = form.initial.get(name, field.initial)
|
|
539
|
+
data[name] = "" if value is None else value
|
|
540
|
+
data["_save"] = "Save"
|
|
541
|
+
data.update({key: value for key, value in overrides.items() if value is not None})
|
|
542
|
+
return data
|
|
543
|
+
|
|
544
|
+
def test_change_form_includes_login_rfid_field(self):
|
|
545
|
+
tag = RFID.objects.create(rfid="CARD777")
|
|
546
|
+
EnergyAccount.objects.create(user=self.user, name="TARGET")
|
|
547
|
+
url = reverse("admin:core_user_change", args=[self.user.pk])
|
|
548
|
+
response = self.client.get(url)
|
|
549
|
+
self.assertEqual(response.status_code, 200)
|
|
550
|
+
self.assertContains(response, 'name="login_rfid"')
|
|
551
|
+
self.assertContains(response, str(tag.pk))
|
|
552
|
+
|
|
553
|
+
def test_change_form_assigns_login_rfid(self):
|
|
554
|
+
account = EnergyAccount.objects.create(user=self.user, name="TARGET")
|
|
555
|
+
tag = RFID.objects.create(rfid="CARD778")
|
|
556
|
+
url = reverse("admin:core_user_change", args=[self.user.pk])
|
|
557
|
+
response = self.client.get(url)
|
|
558
|
+
form_data = self._build_change_form_data(response, login_rfid=str(tag.pk))
|
|
559
|
+
post_response = self.client.post(url, form_data)
|
|
560
|
+
self.assertRedirects(
|
|
561
|
+
post_response, reverse("admin:core_user_change", args=[self.user.pk])
|
|
562
|
+
)
|
|
563
|
+
account.refresh_from_db()
|
|
564
|
+
self.assertTrue(account.rfids.filter(pk=tag.pk).exists())
|
|
565
|
+
|
|
566
|
+
def test_change_form_creates_energy_account_when_missing(self):
|
|
567
|
+
tag = RFID.objects.create(rfid="CARD779")
|
|
568
|
+
url = reverse("admin:core_user_change", args=[self.user.pk])
|
|
569
|
+
response = self.client.get(url)
|
|
570
|
+
form_data = self._build_change_form_data(response, login_rfid=str(tag.pk))
|
|
571
|
+
post_response = self.client.post(url, form_data)
|
|
572
|
+
self.assertRedirects(
|
|
573
|
+
post_response, reverse("admin:core_user_change", args=[self.user.pk])
|
|
574
|
+
)
|
|
575
|
+
account = EnergyAccount.objects.get(user=self.user)
|
|
576
|
+
self.assertTrue(account.rfids.filter(pk=tag.pk).exists())
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
class AdminSitePasswordChangeTests(TestCase):
|
|
580
|
+
def setUp(self):
|
|
581
|
+
self.client = Client()
|
|
582
|
+
self.admin = User.objects.create_superuser(
|
|
583
|
+
username="admin", email="admin@example.com", password="adminpass"
|
|
584
|
+
)
|
|
585
|
+
self.client.force_login(self.admin)
|
|
586
|
+
|
|
587
|
+
def test_admin_password_change_form_excludes_rfid_field(self):
|
|
588
|
+
response = self.client.get(reverse("admin:password_change"))
|
|
589
|
+
self.assertEqual(response.status_code, 200)
|
|
590
|
+
self.assertNotContains(response, 'name="rfid"')
|
|
591
|
+
self.assertNotContains(response, "Login RFID")
|
|
592
|
+
|
|
593
|
+
def test_admin_password_change_keeps_existing_rfid_assignment(self):
|
|
594
|
+
account = EnergyAccount.objects.create(user=self.admin, name="ADMIN")
|
|
595
|
+
tag = RFID.objects.create(rfid="CARD881")
|
|
596
|
+
account.rfids.add(tag)
|
|
597
|
+
response = self.client.post(
|
|
598
|
+
reverse("admin:password_change"),
|
|
599
|
+
{
|
|
600
|
+
"old_password": "adminpass",
|
|
601
|
+
"new_password1": "UltraSecurePass123",
|
|
602
|
+
"new_password2": "UltraSecurePass123",
|
|
603
|
+
"password1": "UltraSecurePass123",
|
|
604
|
+
"password2": "UltraSecurePass123",
|
|
605
|
+
},
|
|
606
|
+
)
|
|
607
|
+
self.assertRedirects(response, reverse("admin:password_change_done"))
|
|
608
|
+
self.admin.refresh_from_db()
|
|
609
|
+
self.assertTrue(self.admin.check_password("UltraSecurePass123"))
|
|
610
|
+
account.refresh_from_db()
|
|
611
|
+
self.assertTrue(account.rfids.filter(pk=tag.pk).exists())
|
|
612
|
+
|
|
442
613
|
def test_export_rfids_color_filter(self):
|
|
443
614
|
RFID.objects.create(rfid="CARD111", color=RFID.WHITE)
|
|
444
615
|
response = self.client.get(reverse("rfid-batch"), {"color": "W"})
|
|
@@ -548,6 +719,15 @@ class RFIDValidationTests(TestCase):
|
|
|
548
719
|
tag = RFID.objects.create(rfid="DEADBEEF10")
|
|
549
720
|
self.assertEqual(tag.rfid, "DEADBEEF10")
|
|
550
721
|
|
|
722
|
+
def test_reversed_uid_updates_with_rfid(self):
|
|
723
|
+
tag = RFID.objects.create(rfid="A1B2C3D4")
|
|
724
|
+
self.assertEqual(tag.reversed_uid, "D4C3B2A1")
|
|
725
|
+
|
|
726
|
+
tag.rfid = "112233"
|
|
727
|
+
tag.save(update_fields=["rfid"])
|
|
728
|
+
tag.refresh_from_db()
|
|
729
|
+
self.assertEqual(tag.reversed_uid, "332211")
|
|
730
|
+
|
|
551
731
|
def test_find_user_by_rfid(self):
|
|
552
732
|
user = User.objects.create_user(username="finder", password="pwd")
|
|
553
733
|
acc = EnergyAccount.objects.create(user=user, name="FINDER")
|
|
@@ -978,11 +1158,77 @@ class RFIDImportExportCommandTests(TestCase):
|
|
|
978
1158
|
result = resource.import_data(dataset, dry_run=False)
|
|
979
1159
|
self.assertFalse(result.has_errors())
|
|
980
1160
|
|
|
1161
|
+
def test_admin_import_restores_soft_deleted_label(self):
|
|
1162
|
+
tag = RFID.objects.create(rfid="DELETEME01")
|
|
1163
|
+
label = tag.label_id
|
|
1164
|
+
tag.delete()
|
|
1165
|
+
self.assertFalse(RFID.objects.filter(label_id=label).exists())
|
|
1166
|
+
self.assertTrue(
|
|
1167
|
+
RFID.all_objects.filter(label_id=label, is_deleted=True).exists()
|
|
1168
|
+
)
|
|
1169
|
+
|
|
1170
|
+
resource = RFIDResource()
|
|
1171
|
+
headers = resource.get_export_headers()
|
|
1172
|
+
dataset = Dataset()
|
|
1173
|
+
dataset.headers = headers
|
|
1174
|
+
row = {header: "" for header in headers}
|
|
1175
|
+
row.update(
|
|
1176
|
+
{
|
|
1177
|
+
"label_id": str(label),
|
|
1178
|
+
"rfid": "DELETEME02",
|
|
1179
|
+
"allowed": "true",
|
|
1180
|
+
"color": RFID.BLACK,
|
|
1181
|
+
"kind": RFID.CLASSIC,
|
|
1182
|
+
}
|
|
1183
|
+
)
|
|
1184
|
+
dataset.append([row[h] for h in headers])
|
|
1185
|
+
|
|
1186
|
+
result = resource.import_data(dataset, dry_run=False)
|
|
1187
|
+
|
|
1188
|
+
self.assertFalse(result.has_errors())
|
|
1189
|
+
restored = RFID.all_objects.get(label_id=label)
|
|
1190
|
+
self.assertEqual(restored.rfid, "DELETEME02")
|
|
1191
|
+
self.assertFalse(restored.is_deleted)
|
|
1192
|
+
|
|
981
1193
|
account = EnergyAccount.objects.get(name="IMPORTED ADMIN ACCOUNT")
|
|
982
1194
|
tag = RFID.objects.get(rfid="NAMETAG002")
|
|
983
1195
|
self.assertTrue(tag.energy_accounts.filter(pk=account.pk).exists())
|
|
984
1196
|
|
|
985
1197
|
|
|
1198
|
+
class CheckRFIDCommandTests(TestCase):
|
|
1199
|
+
def test_successful_validation_outputs_json(self):
|
|
1200
|
+
out = StringIO()
|
|
1201
|
+
|
|
1202
|
+
call_command("check_rfid", "abcd1234", stdout=out)
|
|
1203
|
+
|
|
1204
|
+
payload = json.loads(out.getvalue())
|
|
1205
|
+
self.assertEqual(payload["rfid"], "ABCD1234")
|
|
1206
|
+
self.assertTrue(payload["created"])
|
|
1207
|
+
self.assertTrue(RFID.objects.filter(rfid="ABCD1234").exists())
|
|
1208
|
+
|
|
1209
|
+
def test_invalid_value_raises_error(self):
|
|
1210
|
+
with self.assertRaises(CommandError):
|
|
1211
|
+
call_command("check_rfid", "invalid!")
|
|
1212
|
+
|
|
1213
|
+
def test_kind_option_updates_existing_tag(self):
|
|
1214
|
+
tag = RFID.objects.create(rfid="EXISTING", allowed=False, kind=RFID.CLASSIC)
|
|
1215
|
+
out = StringIO()
|
|
1216
|
+
|
|
1217
|
+
call_command(
|
|
1218
|
+
"check_rfid",
|
|
1219
|
+
"existing",
|
|
1220
|
+
"--kind",
|
|
1221
|
+
RFID.NTAG215,
|
|
1222
|
+
stdout=out,
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
payload = json.loads(out.getvalue())
|
|
1226
|
+
tag.refresh_from_db()
|
|
1227
|
+
self.assertFalse(payload["created"])
|
|
1228
|
+
self.assertEqual(payload["kind"], RFID.NTAG215)
|
|
1229
|
+
self.assertEqual(tag.kind, RFID.NTAG215)
|
|
1230
|
+
|
|
1231
|
+
|
|
986
1232
|
class RFIDKeyVerificationFlagTests(TestCase):
|
|
987
1233
|
def test_flags_reset_on_key_change(self):
|
|
988
1234
|
tag = RFID.objects.create(
|
|
@@ -1037,6 +1283,44 @@ class ReleaseProcessTests(TestCase):
|
|
|
1037
1283
|
)
|
|
1038
1284
|
sync_main.assert_called_once_with(Path("rel.log"))
|
|
1039
1285
|
|
|
1286
|
+
def test_step_check_todos_logs_instruction_when_pending(self):
|
|
1287
|
+
log_path = Path("rel.log")
|
|
1288
|
+
log_path.unlink(missing_ok=True)
|
|
1289
|
+
Todo.objects.create(request="Review checklist")
|
|
1290
|
+
ctx: dict[str, object] = {}
|
|
1291
|
+
|
|
1292
|
+
try:
|
|
1293
|
+
with self.assertRaises(core_views.PendingTodos):
|
|
1294
|
+
core_views._step_check_todos(self.release, ctx, log_path)
|
|
1295
|
+
|
|
1296
|
+
contents = log_path.read_text(encoding="utf-8")
|
|
1297
|
+
message = "Release checklist requires acknowledgment before continuing."
|
|
1298
|
+
self.assertIn(message, contents)
|
|
1299
|
+
self.assertIn("Review outstanding TODO items", contents)
|
|
1300
|
+
|
|
1301
|
+
with self.assertRaises(core_views.PendingTodos):
|
|
1302
|
+
core_views._step_check_todos(self.release, ctx, log_path)
|
|
1303
|
+
|
|
1304
|
+
contents = log_path.read_text(encoding="utf-8")
|
|
1305
|
+
self.assertEqual(contents.count(message), 1)
|
|
1306
|
+
finally:
|
|
1307
|
+
log_path.unlink(missing_ok=True)
|
|
1308
|
+
|
|
1309
|
+
def test_step_check_todos_auto_ack_when_no_pending(self):
|
|
1310
|
+
log_path = Path("rel.log")
|
|
1311
|
+
log_path.unlink(missing_ok=True)
|
|
1312
|
+
ctx: dict[str, object] = {}
|
|
1313
|
+
|
|
1314
|
+
try:
|
|
1315
|
+
with mock.patch("core.views._refresh_changelog_once"):
|
|
1316
|
+
core_views._step_check_todos(self.release, ctx, log_path)
|
|
1317
|
+
finally:
|
|
1318
|
+
log_path.unlink(missing_ok=True)
|
|
1319
|
+
|
|
1320
|
+
self.assertTrue(ctx.get("todos_ack"))
|
|
1321
|
+
self.assertNotIn("todos_required", ctx)
|
|
1322
|
+
self.assertIsNone(ctx.get("todos"))
|
|
1323
|
+
|
|
1040
1324
|
@mock.patch("core.views._sync_with_origin_main")
|
|
1041
1325
|
@mock.patch("core.views.release_utils._git_clean", return_value=True)
|
|
1042
1326
|
@mock.patch("core.views.release_utils.network_available", return_value=False)
|
|
@@ -1206,11 +1490,10 @@ class ReleaseProcessTests(TestCase):
|
|
|
1206
1490
|
run.assert_any_call(["git", "clean", "-fd"], check=False)
|
|
1207
1491
|
|
|
1208
1492
|
@mock.patch("core.views.PackageRelease.dump_fixture")
|
|
1209
|
-
@mock.patch("core.views._ensure_release_todo")
|
|
1210
1493
|
@mock.patch("core.views._sync_with_origin_main")
|
|
1211
1494
|
@mock.patch("core.views.subprocess.run")
|
|
1212
1495
|
def test_pre_release_syncs_with_main(
|
|
1213
|
-
self, run, sync_main,
|
|
1496
|
+
self, run, sync_main, dump_fixture
|
|
1214
1497
|
):
|
|
1215
1498
|
import subprocess as sp
|
|
1216
1499
|
|
|
@@ -1222,11 +1505,6 @@ class ReleaseProcessTests(TestCase):
|
|
|
1222
1505
|
return sp.CompletedProcess(cmd, 0)
|
|
1223
1506
|
|
|
1224
1507
|
run.side_effect = fake_run
|
|
1225
|
-
ensure_todo.return_value = (
|
|
1226
|
-
mock.Mock(request="Create release pkg 1.0.1", url="", request_details=""),
|
|
1227
|
-
Path("core/fixtures/todos__next_release.json"),
|
|
1228
|
-
)
|
|
1229
|
-
|
|
1230
1508
|
version_path = Path("VERSION")
|
|
1231
1509
|
original_version = version_path.read_text(encoding="utf-8")
|
|
1232
1510
|
|
|
@@ -1527,6 +1805,54 @@ class ReleaseProcessTests(TestCase):
|
|
|
1527
1805
|
ctx = session.get(f"release_publish_{self.release.pk}")
|
|
1528
1806
|
self.assertTrue(ctx.get("dry_run"))
|
|
1529
1807
|
|
|
1808
|
+
def test_resume_button_shown_when_credentials_missing(self):
|
|
1809
|
+
user = User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1810
|
+
url = reverse("release-progress", args=[self.release.pk, "publish"])
|
|
1811
|
+
self.client.force_login(user)
|
|
1812
|
+
|
|
1813
|
+
self.client.get(f"{url}?start=1")
|
|
1814
|
+
|
|
1815
|
+
session = self.client.session
|
|
1816
|
+
ctx = session.get(f"release_publish_{self.release.pk}") or {}
|
|
1817
|
+
ctx.update({"step": 7, "started": True, "paused": False})
|
|
1818
|
+
session[f"release_publish_{self.release.pk}"] = ctx
|
|
1819
|
+
session.save()
|
|
1820
|
+
|
|
1821
|
+
response = self.client.get(f"{url}?step=7")
|
|
1822
|
+
self.assertEqual(response.status_code, 200)
|
|
1823
|
+
context = response.context
|
|
1824
|
+
if isinstance(context, list):
|
|
1825
|
+
context = context[-1]
|
|
1826
|
+
self.assertTrue(context["resume_available"])
|
|
1827
|
+
self.assertIn(b"Resume Publish", response.content)
|
|
1828
|
+
|
|
1829
|
+
def test_resume_without_step_parameter_defaults_to_current_progress(self):
|
|
1830
|
+
run: list[str] = []
|
|
1831
|
+
|
|
1832
|
+
def step_fn(release, ctx, log_path):
|
|
1833
|
+
run.append("step")
|
|
1834
|
+
|
|
1835
|
+
steps = [("Only step", step_fn)]
|
|
1836
|
+
user = User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1837
|
+
url = reverse("release-progress", args=[self.release.pk, "publish"])
|
|
1838
|
+
with mock.patch("core.views.PUBLISH_STEPS", steps):
|
|
1839
|
+
self.client.force_login(user)
|
|
1840
|
+
session = self.client.session
|
|
1841
|
+
session[f"release_publish_{self.release.pk}"] = {
|
|
1842
|
+
"step": 0,
|
|
1843
|
+
"started": True,
|
|
1844
|
+
"paused": False,
|
|
1845
|
+
}
|
|
1846
|
+
session.save()
|
|
1847
|
+
|
|
1848
|
+
response = self.client.get(f"{url}?resume=1")
|
|
1849
|
+
self.assertEqual(response.status_code, 200)
|
|
1850
|
+
self.assertEqual(run, ["step"])
|
|
1851
|
+
|
|
1852
|
+
session = self.client.session
|
|
1853
|
+
ctx = session.get(f"release_publish_{self.release.pk}")
|
|
1854
|
+
self.assertEqual(ctx.get("step"), 1)
|
|
1855
|
+
|
|
1530
1856
|
def test_new_todo_does_not_reset_pending_flow(self):
|
|
1531
1857
|
user = User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1532
1858
|
url = reverse("release-progress", args=[self.release.pk, "publish"])
|
|
@@ -1843,7 +2169,7 @@ class PackageReleaseAdminActionTests(TestCase):
|
|
|
1843
2169
|
|
|
1844
2170
|
@mock.patch("core.admin.PackageRelease.dump_fixture")
|
|
1845
2171
|
@mock.patch("core.admin.requests.get")
|
|
1846
|
-
def
|
|
2172
|
+
def test_refresh_from_pypi_reports_missing_releases(self, mock_get, dump):
|
|
1847
2173
|
mock_get.return_value.raise_for_status.return_value = None
|
|
1848
2174
|
mock_get.return_value.json.return_value = {
|
|
1849
2175
|
"releases": {
|
|
@@ -1856,13 +2182,17 @@ class PackageReleaseAdminActionTests(TestCase):
|
|
|
1856
2182
|
}
|
|
1857
2183
|
}
|
|
1858
2184
|
self.admin.refresh_from_pypi(self.request, PackageRelease.objects.none())
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
2185
|
+
self.assertFalse(
|
|
2186
|
+
PackageRelease.objects.filter(version="1.1.0").exists()
|
|
2187
|
+
)
|
|
2188
|
+
dump.assert_not_called()
|
|
2189
|
+
self.assertIn(
|
|
2190
|
+
(
|
|
2191
|
+
"Manual creation required for 1 release: 1.1.0",
|
|
2192
|
+
messages.WARNING,
|
|
2193
|
+
),
|
|
2194
|
+
self.messages,
|
|
1864
2195
|
)
|
|
1865
|
-
dump.assert_called_once()
|
|
1866
2196
|
|
|
1867
2197
|
@mock.patch("core.admin.PackageRelease.dump_fixture")
|
|
1868
2198
|
@mock.patch("core.admin.requests.get")
|
|
@@ -2008,6 +2338,25 @@ class PackageReleaseCurrentTests(TestCase):
|
|
|
2008
2338
|
self.package.save()
|
|
2009
2339
|
self.assertFalse(self.release.is_current)
|
|
2010
2340
|
|
|
2341
|
+
def test_is_current_false_when_version_has_plus(self):
|
|
2342
|
+
self.version_path.write_text("1.0.0+")
|
|
2343
|
+
self.assertFalse(self.release.is_current)
|
|
2344
|
+
|
|
2345
|
+
|
|
2346
|
+
class PackageReleaseRevisionTests(TestCase):
|
|
2347
|
+
def setUp(self):
|
|
2348
|
+
self.package = Package.objects.get(name="arthexis")
|
|
2349
|
+
self.release = PackageRelease.objects.create(
|
|
2350
|
+
package=self.package,
|
|
2351
|
+
version="1.0.0",
|
|
2352
|
+
revision="abcdef123456",
|
|
2353
|
+
)
|
|
2354
|
+
|
|
2355
|
+
def test_matches_revision_ignores_plus_suffix(self):
|
|
2356
|
+
self.assertTrue(
|
|
2357
|
+
PackageRelease.matches_revision("1.0.0+", "abcdef123456")
|
|
2358
|
+
)
|
|
2359
|
+
|
|
2011
2360
|
def test_is_current_false_when_version_differs(self):
|
|
2012
2361
|
self.release.version = "2.0.0"
|
|
2013
2362
|
self.release.save()
|
|
@@ -2058,13 +2407,22 @@ class PackageAdminPrepareNextReleaseTests(TestCase):
|
|
|
2058
2407
|
|
|
2059
2408
|
def test_prepare_next_release_active_creates_release(self):
|
|
2060
2409
|
PackageRelease.all_objects.filter(package=self.package).delete()
|
|
2061
|
-
request = self.factory.
|
|
2410
|
+
request = self.factory.post("/admin/core/package/prepare-next-release/")
|
|
2062
2411
|
response = self.admin.prepare_next_release_active(request)
|
|
2063
2412
|
self.assertEqual(response.status_code, 302)
|
|
2064
2413
|
self.assertEqual(
|
|
2065
2414
|
PackageRelease.all_objects.filter(package=self.package).count(), 1
|
|
2066
2415
|
)
|
|
2067
2416
|
|
|
2417
|
+
def test_prepare_next_release_active_get_creates_release(self):
|
|
2418
|
+
PackageRelease.all_objects.filter(package=self.package).delete()
|
|
2419
|
+
request = self.factory.get("/admin/core/package/prepare-next-release/")
|
|
2420
|
+
response = self.admin.prepare_next_release_active(request)
|
|
2421
|
+
self.assertEqual(response.status_code, 302)
|
|
2422
|
+
self.assertTrue(
|
|
2423
|
+
PackageRelease.all_objects.filter(package=self.package).exists()
|
|
2424
|
+
)
|
|
2425
|
+
|
|
2068
2426
|
|
|
2069
2427
|
class PackageAdminChangeViewTests(TestCase):
|
|
2070
2428
|
def setUp(self):
|
|
@@ -2086,13 +2444,97 @@ class TodoDoneTests(TestCase):
|
|
|
2086
2444
|
User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
2087
2445
|
self.client.force_login(User.objects.get(username="admin"))
|
|
2088
2446
|
|
|
2089
|
-
|
|
2447
|
+
@mock.patch("core.models.revision_utils.get_revision", return_value="rev123")
|
|
2448
|
+
def test_mark_done_sets_timestamp(self, _get_revision):
|
|
2090
2449
|
todo = Todo.objects.create(request="Task", is_seed_data=True)
|
|
2091
2450
|
resp = self.client.post(reverse("todo-done", args=[todo.pk]))
|
|
2092
2451
|
self.assertRedirects(resp, reverse("admin:index"))
|
|
2093
2452
|
todo.refresh_from_db()
|
|
2094
2453
|
self.assertIsNotNone(todo.done_on)
|
|
2095
2454
|
self.assertFalse(todo.is_deleted)
|
|
2455
|
+
self.assertIsNone(todo.done_node)
|
|
2456
|
+
version_path = Path(settings.BASE_DIR) / "VERSION"
|
|
2457
|
+
expected_version = ""
|
|
2458
|
+
if version_path.exists():
|
|
2459
|
+
expected_version = version_path.read_text(encoding="utf-8").strip()
|
|
2460
|
+
self.assertEqual(todo.done_version, expected_version)
|
|
2461
|
+
self.assertEqual(todo.done_revision, "rev123")
|
|
2462
|
+
self.assertEqual(todo.done_username, "admin")
|
|
2463
|
+
|
|
2464
|
+
def test_mark_done_updates_seed_fixture(self):
|
|
2465
|
+
todo = Todo.objects.create(request="Task", is_seed_data=True)
|
|
2466
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2467
|
+
base = Path(tmp)
|
|
2468
|
+
fixture_dir = base / "core" / "fixtures"
|
|
2469
|
+
fixture_dir.mkdir(parents=True)
|
|
2470
|
+
fixture_path = fixture_dir / "todo__task.json"
|
|
2471
|
+
fixture_path.write_text(
|
|
2472
|
+
json.dumps(
|
|
2473
|
+
[
|
|
2474
|
+
{
|
|
2475
|
+
"model": "core.todo",
|
|
2476
|
+
"fields": {
|
|
2477
|
+
"request": "Task",
|
|
2478
|
+
"url": "",
|
|
2479
|
+
"request_details": "",
|
|
2480
|
+
},
|
|
2481
|
+
}
|
|
2482
|
+
],
|
|
2483
|
+
indent=2,
|
|
2484
|
+
)
|
|
2485
|
+
+ "\n",
|
|
2486
|
+
encoding="utf-8",
|
|
2487
|
+
)
|
|
2488
|
+
|
|
2489
|
+
with override_settings(BASE_DIR=base):
|
|
2490
|
+
with mock.patch(
|
|
2491
|
+
"core.models.revision_utils.get_revision", return_value="rev456"
|
|
2492
|
+
):
|
|
2493
|
+
resp = self.client.post(reverse("todo-done", args=[todo.pk]))
|
|
2494
|
+
|
|
2495
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
2496
|
+
data = json.loads(fixture_path.read_text(encoding="utf-8"))
|
|
2497
|
+
self.assertEqual(len(data), 1)
|
|
2498
|
+
fields = data[0]["fields"]
|
|
2499
|
+
self.assertIn("done_on", fields)
|
|
2500
|
+
self.assertTrue(fields["done_on"])
|
|
2501
|
+
self.assertFalse(fields.get("is_deleted", False))
|
|
2502
|
+
self.assertIn("done_version", fields)
|
|
2503
|
+
self.assertEqual(fields.get("done_revision"), "rev456")
|
|
2504
|
+
self.assertEqual(fields.get("done_username"), "admin")
|
|
2505
|
+
|
|
2506
|
+
def test_soft_delete_updates_seed_fixture(self):
|
|
2507
|
+
todo = Todo.objects.create(request="Task", is_seed_data=True)
|
|
2508
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2509
|
+
base = Path(tmp)
|
|
2510
|
+
fixture_dir = base / "core" / "fixtures"
|
|
2511
|
+
fixture_dir.mkdir(parents=True)
|
|
2512
|
+
fixture_path = fixture_dir / "todo__task.json"
|
|
2513
|
+
fixture_path.write_text(
|
|
2514
|
+
json.dumps(
|
|
2515
|
+
[
|
|
2516
|
+
{
|
|
2517
|
+
"model": "core.todo",
|
|
2518
|
+
"fields": {
|
|
2519
|
+
"request": "Task",
|
|
2520
|
+
"url": "",
|
|
2521
|
+
"request_details": "",
|
|
2522
|
+
},
|
|
2523
|
+
}
|
|
2524
|
+
],
|
|
2525
|
+
indent=2,
|
|
2526
|
+
)
|
|
2527
|
+
+ "\n",
|
|
2528
|
+
encoding="utf-8",
|
|
2529
|
+
)
|
|
2530
|
+
|
|
2531
|
+
with override_settings(BASE_DIR=base):
|
|
2532
|
+
todo.delete()
|
|
2533
|
+
|
|
2534
|
+
data = json.loads(fixture_path.read_text(encoding="utf-8"))
|
|
2535
|
+
self.assertEqual(len(data), 1)
|
|
2536
|
+
fields = data[0]["fields"]
|
|
2537
|
+
self.assertTrue(fields.get("is_deleted"))
|
|
2096
2538
|
|
|
2097
2539
|
def test_mark_done_missing_task_refreshes(self):
|
|
2098
2540
|
todo = Todo.objects.create(request="Task", is_seed_data=True)
|
|
@@ -2198,6 +2640,61 @@ class TodoDoneTests(TestCase):
|
|
|
2198
2640
|
self.assertTrue(todo.is_seed_data)
|
|
2199
2641
|
|
|
2200
2642
|
|
|
2643
|
+
class TodoDeleteTests(TestCase):
|
|
2644
|
+
def setUp(self):
|
|
2645
|
+
self.client = Client()
|
|
2646
|
+
User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
2647
|
+
self.client.force_login(User.objects.get(username="admin"))
|
|
2648
|
+
|
|
2649
|
+
def test_delete_marks_task_deleted(self):
|
|
2650
|
+
todo = Todo.objects.create(request="Task", is_seed_data=True)
|
|
2651
|
+
resp = self.client.post(reverse("todo-delete", args=[todo.pk]))
|
|
2652
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
2653
|
+
todo.refresh_from_db()
|
|
2654
|
+
self.assertTrue(todo.is_deleted)
|
|
2655
|
+
self.assertIsNone(todo.done_on)
|
|
2656
|
+
|
|
2657
|
+
def test_delete_updates_seed_fixture(self):
|
|
2658
|
+
todo = Todo.objects.create(request="Task", is_seed_data=True)
|
|
2659
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2660
|
+
base = Path(tmp)
|
|
2661
|
+
fixture_dir = base / "core" / "fixtures"
|
|
2662
|
+
fixture_dir.mkdir(parents=True)
|
|
2663
|
+
fixture_path = fixture_dir / "todo__task.json"
|
|
2664
|
+
fixture_path.write_text(
|
|
2665
|
+
json.dumps(
|
|
2666
|
+
[
|
|
2667
|
+
{
|
|
2668
|
+
"model": "core.todo",
|
|
2669
|
+
"fields": {
|
|
2670
|
+
"request": "Task",
|
|
2671
|
+
"url": "",
|
|
2672
|
+
"request_details": "",
|
|
2673
|
+
},
|
|
2674
|
+
}
|
|
2675
|
+
],
|
|
2676
|
+
indent=2,
|
|
2677
|
+
)
|
|
2678
|
+
+ "\n",
|
|
2679
|
+
encoding="utf-8",
|
|
2680
|
+
)
|
|
2681
|
+
|
|
2682
|
+
with override_settings(BASE_DIR=base):
|
|
2683
|
+
resp = self.client.post(reverse("todo-delete", args=[todo.pk]))
|
|
2684
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
2685
|
+
data = json.loads(fixture_path.read_text(encoding="utf-8"))
|
|
2686
|
+
self.assertEqual(len(data), 1)
|
|
2687
|
+
fields = data[0]["fields"]
|
|
2688
|
+
self.assertTrue(fields.get("is_deleted"))
|
|
2689
|
+
|
|
2690
|
+
def test_delete_missing_task_redirects(self):
|
|
2691
|
+
todo = Todo.objects.create(request="Task")
|
|
2692
|
+
todo.is_deleted = True
|
|
2693
|
+
todo.save(update_fields=["is_deleted"])
|
|
2694
|
+
resp = self.client.post(reverse("todo-delete", args=[todo.pk]))
|
|
2695
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
2696
|
+
|
|
2697
|
+
|
|
2201
2698
|
class TodoFocusViewTests(TestCase):
|
|
2202
2699
|
def setUp(self):
|
|
2203
2700
|
self.client = Client()
|
|
@@ -2215,6 +2712,7 @@ class TodoFocusViewTests(TestCase):
|
|
|
2215
2712
|
self.assertEqual(resp["X-Frame-Options"], "SAMEORIGIN")
|
|
2216
2713
|
self.assertContains(resp, f'src="{todo.url}"')
|
|
2217
2714
|
self.assertContains(resp, "Done")
|
|
2715
|
+
self.assertContains(resp, "Delete")
|
|
2218
2716
|
self.assertContains(resp, "Back")
|
|
2219
2717
|
self.assertContains(resp, "Take Snapshot")
|
|
2220
2718
|
snapshot_url = reverse("todo-snapshot", args=[todo.pk])
|
|
@@ -2236,7 +2734,7 @@ class TodoFocusViewTests(TestCase):
|
|
|
2236
2734
|
def test_focus_view_sanitizes_loopback_absolute_url(self):
|
|
2237
2735
|
todo = Todo.objects.create(
|
|
2238
2736
|
request="Task",
|
|
2239
|
-
url="http://127.0.0.1:
|
|
2737
|
+
url="http://127.0.0.1:8888/docs/?section=chart",
|
|
2240
2738
|
)
|
|
2241
2739
|
resp = self.client.get(reverse("todo-focus", args=[todo.pk]))
|
|
2242
2740
|
self.assertContains(resp, 'src="/docs/?section=chart"')
|