arthexis 0.1.26__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.26.dist-info → arthexis-0.1.28.dist-info}/METADATA +16 -11
- {arthexis-0.1.26.dist-info → arthexis-0.1.28.dist-info}/RECORD +39 -38
- config/settings.py +7 -1
- config/settings_helpers.py +176 -1
- config/urls.py +18 -2
- core/admin.py +265 -23
- core/apps.py +6 -2
- core/celery_utils.py +73 -0
- core/models.py +307 -63
- core/system.py +17 -2
- core/tasks.py +304 -129
- core/test_system_info.py +43 -5
- core/tests.py +202 -2
- core/user_data.py +52 -19
- core/views.py +70 -3
- nodes/admin.py +348 -3
- nodes/apps.py +1 -1
- nodes/feature_checks.py +30 -0
- nodes/models.py +146 -18
- nodes/tasks.py +1 -1
- nodes/tests.py +181 -48
- nodes/views.py +148 -3
- ocpp/admin.py +1001 -10
- ocpp/consumers.py +572 -7
- ocpp/models.py +499 -33
- ocpp/store.py +406 -40
- ocpp/tasks.py +109 -145
- ocpp/test_rfid.py +73 -2
- ocpp/tests.py +982 -90
- ocpp/urls.py +5 -0
- ocpp/views.py +172 -70
- pages/context_processors.py +2 -0
- pages/models.py +9 -0
- pages/tests.py +166 -18
- pages/urls.py +1 -0
- pages/views.py +66 -3
- {arthexis-0.1.26.dist-info → arthexis-0.1.28.dist-info}/WHEEL +0 -0
- {arthexis-0.1.26.dist-info → arthexis-0.1.28.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.26.dist-info → arthexis-0.1.28.dist-info}/top_level.txt +0 -0
core/tests.py
CHANGED
|
@@ -5,6 +5,7 @@ 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
|
|
@@ -51,6 +52,7 @@ from core.admin import (
|
|
|
51
52
|
PackageReleaseAdmin,
|
|
52
53
|
PackageAdmin,
|
|
53
54
|
RFIDResource,
|
|
55
|
+
SecurityGroupAdmin,
|
|
54
56
|
UserAdmin,
|
|
55
57
|
USER_PROFILE_INLINES,
|
|
56
58
|
)
|
|
@@ -309,6 +311,16 @@ class UserAdminInlineTests(TestCase):
|
|
|
309
311
|
self.assertEqual(len(other_profiles), len(USER_PROFILE_INLINES))
|
|
310
312
|
|
|
311
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
|
+
|
|
312
324
|
class RFIDLoginTests(TestCase):
|
|
313
325
|
def setUp(self):
|
|
314
326
|
self.client = Client()
|
|
@@ -324,7 +336,9 @@ class RFIDLoginTests(TestCase):
|
|
|
324
336
|
content_type="application/json",
|
|
325
337
|
)
|
|
326
338
|
self.assertEqual(response.status_code, 200)
|
|
327
|
-
|
|
339
|
+
payload = response.json()
|
|
340
|
+
self.assertEqual(payload["username"], "alice")
|
|
341
|
+
self.assertEqual(payload["redirect"], "/")
|
|
328
342
|
|
|
329
343
|
def test_rfid_login_invalid(self):
|
|
330
344
|
response = self.client.post(
|
|
@@ -348,6 +362,7 @@ class RFIDLoginTests(TestCase):
|
|
|
348
362
|
)
|
|
349
363
|
|
|
350
364
|
self.assertEqual(response.status_code, 200)
|
|
365
|
+
self.assertEqual(response.json().get("redirect"), "/")
|
|
351
366
|
run_args, run_kwargs = mock_run.call_args
|
|
352
367
|
self.assertEqual(run_args[0], "echo ok")
|
|
353
368
|
self.assertTrue(run_kwargs.get("shell"))
|
|
@@ -384,6 +399,7 @@ class RFIDLoginTests(TestCase):
|
|
|
384
399
|
)
|
|
385
400
|
|
|
386
401
|
self.assertEqual(response.status_code, 200)
|
|
402
|
+
self.assertEqual(response.json().get("redirect"), "/")
|
|
387
403
|
mock_popen.assert_called_once()
|
|
388
404
|
args, kwargs = mock_popen.call_args
|
|
389
405
|
self.assertEqual(args[0], "echo welcome")
|
|
@@ -410,6 +426,24 @@ class RFIDLoginTests(TestCase):
|
|
|
410
426
|
self.assertEqual(response.status_code, 401)
|
|
411
427
|
mock_popen.assert_not_called()
|
|
412
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
|
+
|
|
413
447
|
|
|
414
448
|
class RFIDBatchApiTests(TestCase):
|
|
415
449
|
def setUp(self):
|
|
@@ -442,6 +476,140 @@ class RFIDBatchApiTests(TestCase):
|
|
|
442
476
|
},
|
|
443
477
|
)
|
|
444
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
|
+
|
|
445
613
|
def test_export_rfids_color_filter(self):
|
|
446
614
|
RFID.objects.create(rfid="CARD111", color=RFID.WHITE)
|
|
447
615
|
response = self.client.get(reverse("rfid-batch"), {"color": "W"})
|
|
@@ -990,6 +1158,38 @@ class RFIDImportExportCommandTests(TestCase):
|
|
|
990
1158
|
result = resource.import_data(dataset, dry_run=False)
|
|
991
1159
|
self.assertFalse(result.has_errors())
|
|
992
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
|
+
|
|
993
1193
|
account = EnergyAccount.objects.get(name="IMPORTED ADMIN ACCOUNT")
|
|
994
1194
|
tag = RFID.objects.get(rfid="NAMETAG002")
|
|
995
1195
|
self.assertTrue(tag.energy_accounts.filter(pk=account.pk).exists())
|
|
@@ -2534,7 +2734,7 @@ class TodoFocusViewTests(TestCase):
|
|
|
2534
2734
|
def test_focus_view_sanitizes_loopback_absolute_url(self):
|
|
2535
2735
|
todo = Todo.objects.create(
|
|
2536
2736
|
request="Task",
|
|
2537
|
-
url="http://127.0.0.1:
|
|
2737
|
+
url="http://127.0.0.1:8888/docs/?section=chart",
|
|
2538
2738
|
)
|
|
2539
2739
|
resp = self.client.get(reverse("todo-focus", args=[todo.pk]))
|
|
2540
2740
|
self.assertContains(resp, 'src="/docs/?section=chart"')
|
core/user_data.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from functools import lru_cache
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from io import BytesIO
|
|
5
6
|
from zipfile import ZipFile
|
|
@@ -65,9 +66,13 @@ def _fixture_path(user, instance) -> Path:
|
|
|
65
66
|
return _data_dir(user) / filename
|
|
66
67
|
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
_SEED_FIXTURE_IGNORED_FIELDS = {"is_seed_data", "is_deleted", "is_user_data"}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@lru_cache(maxsize=1)
|
|
73
|
+
def _seed_fixture_index():
|
|
70
74
|
base = Path(settings.BASE_DIR)
|
|
75
|
+
index: dict[str, dict[str, object]] = {}
|
|
71
76
|
for path in base.glob("**/fixtures/*.json"):
|
|
72
77
|
try:
|
|
73
78
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
@@ -76,28 +81,55 @@ def _seed_fixture_path(instance) -> Path | None:
|
|
|
76
81
|
if not isinstance(data, list) or not data:
|
|
77
82
|
continue
|
|
78
83
|
obj = data[0]
|
|
79
|
-
if obj
|
|
84
|
+
if not isinstance(obj, dict):
|
|
80
85
|
continue
|
|
81
|
-
|
|
82
|
-
if
|
|
83
|
-
|
|
84
|
-
fields = obj.get("fields"
|
|
86
|
+
label = obj.get("model")
|
|
87
|
+
if not isinstance(label, str):
|
|
88
|
+
continue
|
|
89
|
+
fields = obj.get("fields") or {}
|
|
90
|
+
if not isinstance(fields, dict):
|
|
91
|
+
fields = {}
|
|
85
92
|
comparable_fields = {
|
|
86
93
|
key: value
|
|
87
94
|
for key, value in fields.items()
|
|
88
|
-
if key not in
|
|
95
|
+
if key not in _SEED_FIXTURE_IGNORED_FIELDS
|
|
89
96
|
}
|
|
97
|
+
pk = obj.get("pk")
|
|
98
|
+
entries = index.setdefault(label, {"pk": {}, "fields": []})
|
|
99
|
+
pk_index = entries.setdefault("pk", {})
|
|
100
|
+
field_index = entries.setdefault("fields", [])
|
|
101
|
+
if pk is not None:
|
|
102
|
+
pk_index[pk] = path
|
|
90
103
|
if comparable_fields:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
field_index.append((comparable_fields, path))
|
|
105
|
+
return index
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _seed_fixture_path(instance, *, index=None) -> Path | None:
|
|
109
|
+
label = f"{instance._meta.app_label}.{instance._meta.model_name}"
|
|
110
|
+
fixture_index = index or _seed_fixture_index()
|
|
111
|
+
entries = fixture_index.get(label)
|
|
112
|
+
if not entries:
|
|
113
|
+
return None
|
|
114
|
+
pk = getattr(instance, "pk", None)
|
|
115
|
+
pk_index = entries.get("pk", {})
|
|
116
|
+
if pk is not None:
|
|
117
|
+
path = pk_index.get(pk)
|
|
118
|
+
if path is not None:
|
|
119
|
+
return path
|
|
120
|
+
for comparable_fields, path in entries.get("fields", []):
|
|
121
|
+
match = True
|
|
122
|
+
if not isinstance(comparable_fields, dict):
|
|
123
|
+
continue
|
|
124
|
+
for field_name, value in comparable_fields.items():
|
|
125
|
+
if not hasattr(instance, field_name):
|
|
126
|
+
match = False
|
|
127
|
+
break
|
|
128
|
+
if getattr(instance, field_name) != value:
|
|
129
|
+
match = False
|
|
130
|
+
break
|
|
131
|
+
if match:
|
|
132
|
+
return path
|
|
101
133
|
return None
|
|
102
134
|
|
|
103
135
|
|
|
@@ -581,6 +613,7 @@ def _iter_entity_admin_models():
|
|
|
581
613
|
|
|
582
614
|
def _seed_data_view(request):
|
|
583
615
|
sections = []
|
|
616
|
+
fixture_index = _seed_fixture_index()
|
|
584
617
|
for model, model_admin in _iter_entity_admin_models():
|
|
585
618
|
objs = model.objects.filter(is_seed_data=True)
|
|
586
619
|
if not objs.exists():
|
|
@@ -591,7 +624,7 @@ def _seed_data_view(request):
|
|
|
591
624
|
f"admin:{obj._meta.app_label}_{obj._meta.model_name}_change",
|
|
592
625
|
args=[obj.pk],
|
|
593
626
|
)
|
|
594
|
-
fixture = _seed_fixture_path(obj)
|
|
627
|
+
fixture = _seed_fixture_path(obj, index=fixture_index)
|
|
595
628
|
items.append({"url": url, "label": str(obj), "fixture": fixture})
|
|
596
629
|
sections.append({"opts": model._meta, "items": items})
|
|
597
630
|
context = admin.site.each_context(request)
|
core/views.py
CHANGED
|
@@ -11,7 +11,7 @@ import requests
|
|
|
11
11
|
from django.conf import settings
|
|
12
12
|
from django.contrib.admin.sites import site as admin_site
|
|
13
13
|
from django.contrib.admin.views.decorators import staff_member_required
|
|
14
|
-
from django.contrib.auth import authenticate, login
|
|
14
|
+
from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login
|
|
15
15
|
from django.contrib import messages
|
|
16
16
|
from django.contrib.sites.models import Site
|
|
17
17
|
from django.http import Http404, JsonResponse, HttpResponse
|
|
@@ -99,7 +99,7 @@ def odoo_quote_report(request):
|
|
|
99
99
|
|
|
100
100
|
if not profile or not profile.is_verified:
|
|
101
101
|
context["error"] = _(
|
|
102
|
-
"Configure and verify your
|
|
102
|
+
"Configure and verify your CRM employee credentials before generating the report."
|
|
103
103
|
)
|
|
104
104
|
return TemplateResponse(
|
|
105
105
|
request, "admin/core/odoo_quote_report.html", context
|
|
@@ -470,9 +470,60 @@ def _resolve_release_log_dir(preferred: Path) -> tuple[Path, str | None]:
|
|
|
470
470
|
return fallback, warning
|
|
471
471
|
|
|
472
472
|
|
|
473
|
+
def _commit_todo_fixtures_if_needed(log_path: Path) -> None:
|
|
474
|
+
"""Stage and commit modified TODO fixtures before syncing with origin/main."""
|
|
475
|
+
|
|
476
|
+
try:
|
|
477
|
+
status = subprocess.run(
|
|
478
|
+
["git", "status", "--porcelain"],
|
|
479
|
+
check=True,
|
|
480
|
+
capture_output=True,
|
|
481
|
+
text=True,
|
|
482
|
+
)
|
|
483
|
+
except subprocess.CalledProcessError:
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
todo_paths: set[Path] = set()
|
|
487
|
+
for line in (status.stdout or "").splitlines():
|
|
488
|
+
if not line:
|
|
489
|
+
continue
|
|
490
|
+
path_fragment = line[3:].strip()
|
|
491
|
+
if "->" in path_fragment:
|
|
492
|
+
path_fragment = path_fragment.split("->", 1)[1].strip()
|
|
493
|
+
path = Path(path_fragment)
|
|
494
|
+
if path.suffix == ".json" and path.parent == Path("core/fixtures"):
|
|
495
|
+
if path.name.startswith("todo__"):
|
|
496
|
+
todo_paths.add(path)
|
|
497
|
+
|
|
498
|
+
if not todo_paths:
|
|
499
|
+
return
|
|
500
|
+
|
|
501
|
+
sorted_paths = sorted(todo_paths)
|
|
502
|
+
subprocess.run(
|
|
503
|
+
["git", "add", *[str(path) for path in sorted_paths]],
|
|
504
|
+
check=True,
|
|
505
|
+
)
|
|
506
|
+
formatted = ", ".join(_format_path(path) for path in sorted_paths)
|
|
507
|
+
_append_log(log_path, f"Staged TODO fixtures {formatted}")
|
|
508
|
+
|
|
509
|
+
diff = subprocess.run(
|
|
510
|
+
["git", "diff", "--cached", "--name-only", "--", *[str(path) for path in sorted_paths]],
|
|
511
|
+
check=True,
|
|
512
|
+
capture_output=True,
|
|
513
|
+
text=True,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
if (diff.stdout or "").strip():
|
|
517
|
+
message = "chore: update TODO fixtures"
|
|
518
|
+
subprocess.run(["git", "commit", "-m", message], check=True)
|
|
519
|
+
_append_log(log_path, f"Committed TODO fixtures ({message})")
|
|
520
|
+
|
|
521
|
+
|
|
473
522
|
def _sync_with_origin_main(log_path: Path) -> None:
|
|
474
523
|
"""Ensure the current branch is rebased onto ``origin/main``."""
|
|
475
524
|
|
|
525
|
+
_commit_todo_fixtures_if_needed(log_path)
|
|
526
|
+
|
|
476
527
|
if not _has_remote("origin"):
|
|
477
528
|
_append_log(log_path, "No git remote configured; skipping sync with origin/main")
|
|
478
529
|
return
|
|
@@ -1549,12 +1600,28 @@ def rfid_login(request):
|
|
|
1549
1600
|
if not rfid:
|
|
1550
1601
|
return JsonResponse({"detail": "rfid required"}, status=400)
|
|
1551
1602
|
|
|
1603
|
+
redirect_to = data.get(REDIRECT_FIELD_NAME) or data.get("next")
|
|
1604
|
+
if redirect_to and not url_has_allowed_host_and_scheme(
|
|
1605
|
+
redirect_to,
|
|
1606
|
+
allowed_hosts={request.get_host()},
|
|
1607
|
+
require_https=request.is_secure(),
|
|
1608
|
+
):
|
|
1609
|
+
redirect_to = ""
|
|
1610
|
+
|
|
1552
1611
|
user = authenticate(request, rfid=rfid)
|
|
1553
1612
|
if user is None:
|
|
1554
1613
|
return JsonResponse({"detail": "invalid RFID"}, status=401)
|
|
1555
1614
|
|
|
1556
1615
|
login(request, user)
|
|
1557
|
-
|
|
1616
|
+
if redirect_to:
|
|
1617
|
+
target = redirect_to
|
|
1618
|
+
elif user.is_staff:
|
|
1619
|
+
target = reverse("admin:index")
|
|
1620
|
+
else:
|
|
1621
|
+
target = "/"
|
|
1622
|
+
return JsonResponse(
|
|
1623
|
+
{"id": user.id, "username": user.username, "redirect": target}
|
|
1624
|
+
)
|
|
1558
1625
|
|
|
1559
1626
|
|
|
1560
1627
|
@api_login_required
|