arthexis 0.1.14__py3-none-any.whl → 0.1.16__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.14.dist-info → arthexis-0.1.16.dist-info}/METADATA +3 -2
- {arthexis-0.1.14.dist-info → arthexis-0.1.16.dist-info}/RECORD +41 -39
- config/urls.py +5 -0
- core/admin.py +200 -9
- core/admindocs.py +44 -3
- core/apps.py +1 -1
- core/backends.py +44 -8
- core/entity.py +17 -1
- core/github_issues.py +12 -7
- core/log_paths.py +24 -10
- core/mailer.py +9 -5
- core/models.py +92 -23
- core/release.py +173 -2
- core/system.py +411 -4
- core/tasks.py +5 -1
- core/test_system_info.py +16 -0
- core/tests.py +280 -0
- core/views.py +252 -38
- nodes/admin.py +25 -1
- nodes/models.py +99 -6
- nodes/rfid_sync.py +15 -0
- nodes/tests.py +142 -3
- nodes/utils.py +3 -0
- ocpp/consumers.py +38 -0
- ocpp/models.py +19 -4
- ocpp/tasks.py +156 -2
- ocpp/test_rfid.py +44 -2
- ocpp/tests.py +111 -1
- pages/admin.py +188 -5
- pages/context_processors.py +20 -1
- pages/middleware.py +4 -0
- pages/models.py +39 -1
- pages/module_defaults.py +156 -0
- pages/tasks.py +74 -0
- pages/tests.py +629 -8
- pages/urls.py +2 -0
- pages/utils.py +11 -0
- pages/views.py +106 -38
- {arthexis-0.1.14.dist-info → arthexis-0.1.16.dist-info}/WHEEL +0 -0
- {arthexis-0.1.14.dist-info → arthexis-0.1.16.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.14.dist-info → arthexis-0.1.16.dist-info}/top_level.txt +0 -0
core/tests.py
CHANGED
|
@@ -10,6 +10,7 @@ from django.urls import reverse
|
|
|
10
10
|
from django.http import HttpRequest
|
|
11
11
|
import csv
|
|
12
12
|
import json
|
|
13
|
+
import importlib.util
|
|
13
14
|
from decimal import Decimal
|
|
14
15
|
from unittest import mock
|
|
15
16
|
from unittest.mock import patch
|
|
@@ -22,6 +23,7 @@ import tempfile
|
|
|
22
23
|
from urllib.parse import quote
|
|
23
24
|
|
|
24
25
|
from django.utils import timezone
|
|
26
|
+
from django.conf import settings
|
|
25
27
|
from django.contrib.auth.models import Permission
|
|
26
28
|
from django.contrib.messages import get_messages
|
|
27
29
|
from tablib import Dataset
|
|
@@ -51,6 +53,7 @@ from core.admin import (
|
|
|
51
53
|
USER_PROFILE_INLINES,
|
|
52
54
|
)
|
|
53
55
|
from ocpp.models import Transaction, Charger
|
|
56
|
+
from nodes.models import ContentSample
|
|
54
57
|
|
|
55
58
|
from django.core.exceptions import ValidationError
|
|
56
59
|
from django.core.management import call_command
|
|
@@ -365,6 +368,45 @@ class RFIDLoginTests(TestCase):
|
|
|
365
368
|
self.assertEqual(response.status_code, 401)
|
|
366
369
|
mock_run.assert_called_once()
|
|
367
370
|
|
|
371
|
+
@patch("core.backends.subprocess.Popen")
|
|
372
|
+
def test_rfid_login_post_command_runs_after_success(self, mock_popen):
|
|
373
|
+
tag = self.account.rfids.first()
|
|
374
|
+
tag.post_auth_command = "echo welcome"
|
|
375
|
+
tag.save(update_fields=["post_auth_command"])
|
|
376
|
+
|
|
377
|
+
response = self.client.post(
|
|
378
|
+
reverse("rfid-login"),
|
|
379
|
+
data={"rfid": "CARD123"},
|
|
380
|
+
content_type="application/json",
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
self.assertEqual(response.status_code, 200)
|
|
384
|
+
mock_popen.assert_called_once()
|
|
385
|
+
args, kwargs = mock_popen.call_args
|
|
386
|
+
self.assertEqual(args[0], "echo welcome")
|
|
387
|
+
self.assertTrue(kwargs.get("shell"))
|
|
388
|
+
env = kwargs.get("env", {})
|
|
389
|
+
self.assertEqual(env.get("RFID_VALUE"), "CARD123")
|
|
390
|
+
self.assertEqual(env.get("RFID_LABEL_ID"), str(tag.pk))
|
|
391
|
+
self.assertIs(kwargs.get("stdout"), subprocess.DEVNULL)
|
|
392
|
+
self.assertIs(kwargs.get("stderr"), subprocess.DEVNULL)
|
|
393
|
+
|
|
394
|
+
@patch("core.backends.subprocess.Popen")
|
|
395
|
+
def test_rfid_login_post_command_skipped_on_failure(self, mock_popen):
|
|
396
|
+
tag = self.account.rfids.first()
|
|
397
|
+
tag.post_auth_command = "echo welcome"
|
|
398
|
+
tag.allowed = False
|
|
399
|
+
tag.save(update_fields=["post_auth_command", "allowed"])
|
|
400
|
+
|
|
401
|
+
response = self.client.post(
|
|
402
|
+
reverse("rfid-login"),
|
|
403
|
+
data={"rfid": "CARD123"},
|
|
404
|
+
content_type="application/json",
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
self.assertEqual(response.status_code, 401)
|
|
408
|
+
mock_popen.assert_not_called()
|
|
409
|
+
|
|
368
410
|
|
|
369
411
|
class RFIDBatchApiTests(TestCase):
|
|
370
412
|
def setUp(self):
|
|
@@ -387,6 +429,8 @@ class RFIDBatchApiTests(TestCase):
|
|
|
387
429
|
"rfid": "CARD999",
|
|
388
430
|
"custom_label": "Main Tag",
|
|
389
431
|
"energy_accounts": [self.account.id],
|
|
432
|
+
"external_command": "",
|
|
433
|
+
"post_auth_command": "",
|
|
390
434
|
"allowed": True,
|
|
391
435
|
"color": "B",
|
|
392
436
|
"released": False,
|
|
@@ -406,6 +450,8 @@ class RFIDBatchApiTests(TestCase):
|
|
|
406
450
|
"rfid": "CARD111",
|
|
407
451
|
"custom_label": "",
|
|
408
452
|
"energy_accounts": [],
|
|
453
|
+
"external_command": "",
|
|
454
|
+
"post_auth_command": "",
|
|
409
455
|
"allowed": True,
|
|
410
456
|
"color": "W",
|
|
411
457
|
"released": False,
|
|
@@ -426,6 +472,8 @@ class RFIDBatchApiTests(TestCase):
|
|
|
426
472
|
"rfid": "CARD112",
|
|
427
473
|
"custom_label": "",
|
|
428
474
|
"energy_accounts": [],
|
|
475
|
+
"external_command": "",
|
|
476
|
+
"post_auth_command": "",
|
|
429
477
|
"allowed": True,
|
|
430
478
|
"color": "B",
|
|
431
479
|
"released": True,
|
|
@@ -441,6 +489,8 @@ class RFIDBatchApiTests(TestCase):
|
|
|
441
489
|
"rfid": "A1B2C3D4",
|
|
442
490
|
"custom_label": "Imported Tag",
|
|
443
491
|
"energy_accounts": [self.account.id],
|
|
492
|
+
"external_command": "echo pre",
|
|
493
|
+
"post_auth_command": "echo post",
|
|
444
494
|
"allowed": True,
|
|
445
495
|
"color": "W",
|
|
446
496
|
"released": True,
|
|
@@ -459,6 +509,8 @@ class RFIDBatchApiTests(TestCase):
|
|
|
459
509
|
rfid="A1B2C3D4",
|
|
460
510
|
custom_label="Imported Tag",
|
|
461
511
|
energy_accounts=self.account,
|
|
512
|
+
external_command="echo pre",
|
|
513
|
+
post_auth_command="echo post",
|
|
462
514
|
color=RFID.WHITE,
|
|
463
515
|
released=True,
|
|
464
516
|
).exists()
|
|
@@ -1051,6 +1103,79 @@ class ReleaseProcessTests(TestCase):
|
|
|
1051
1103
|
requests_get.assert_called_once()
|
|
1052
1104
|
sync_main.assert_called_once_with(Path("rel.log"))
|
|
1053
1105
|
|
|
1106
|
+
@mock.patch("core.views.release_utils.network_available", return_value=False)
|
|
1107
|
+
@mock.patch("core.views._collect_dirty_files")
|
|
1108
|
+
@mock.patch("core.views._sync_with_origin_main")
|
|
1109
|
+
@mock.patch("core.views.subprocess.run")
|
|
1110
|
+
@mock.patch("core.views.release_utils._git_clean", return_value=False)
|
|
1111
|
+
def test_step_check_commits_release_prep_changes(
|
|
1112
|
+
self,
|
|
1113
|
+
git_clean,
|
|
1114
|
+
subprocess_run,
|
|
1115
|
+
sync_main,
|
|
1116
|
+
collect_dirty,
|
|
1117
|
+
network_available,
|
|
1118
|
+
):
|
|
1119
|
+
fixture_path = next(Path("core/fixtures").glob("releases__*.json"))
|
|
1120
|
+
collect_dirty.return_value = [
|
|
1121
|
+
{
|
|
1122
|
+
"path": str(fixture_path),
|
|
1123
|
+
"status": "M",
|
|
1124
|
+
"status_label": "Modified",
|
|
1125
|
+
},
|
|
1126
|
+
{"path": "CHANGELOG.rst", "status": "M", "status_label": "Modified"},
|
|
1127
|
+
]
|
|
1128
|
+
subprocess_run.return_value = mock.Mock(returncode=0, stdout="", stderr="")
|
|
1129
|
+
|
|
1130
|
+
ctx: dict[str, object] = {}
|
|
1131
|
+
_step_check_version(self.release, ctx, Path("rel.log"))
|
|
1132
|
+
|
|
1133
|
+
add_call = mock.call(
|
|
1134
|
+
["git", "add", str(fixture_path), "CHANGELOG.rst"],
|
|
1135
|
+
check=True,
|
|
1136
|
+
)
|
|
1137
|
+
commit_call = mock.call(
|
|
1138
|
+
[
|
|
1139
|
+
"git",
|
|
1140
|
+
"commit",
|
|
1141
|
+
"-m",
|
|
1142
|
+
"chore: sync release fixtures and changelog",
|
|
1143
|
+
],
|
|
1144
|
+
check=True,
|
|
1145
|
+
)
|
|
1146
|
+
self.assertIn(add_call, subprocess_run.call_args_list)
|
|
1147
|
+
self.assertIn(commit_call, subprocess_run.call_args_list)
|
|
1148
|
+
self.assertNotIn("dirty_files", ctx)
|
|
1149
|
+
|
|
1150
|
+
@mock.patch("core.views.release_utils.network_available", return_value=False)
|
|
1151
|
+
@mock.patch("core.views._collect_dirty_files")
|
|
1152
|
+
@mock.patch("core.views._sync_with_origin_main")
|
|
1153
|
+
@mock.patch("core.views.subprocess.run")
|
|
1154
|
+
@mock.patch("core.views.release_utils._git_clean", return_value=False)
|
|
1155
|
+
def test_step_check_commits_changelog_only(
|
|
1156
|
+
self,
|
|
1157
|
+
git_clean,
|
|
1158
|
+
subprocess_run,
|
|
1159
|
+
sync_main,
|
|
1160
|
+
collect_dirty,
|
|
1161
|
+
network_available,
|
|
1162
|
+
):
|
|
1163
|
+
collect_dirty.return_value = [
|
|
1164
|
+
{"path": "CHANGELOG.rst", "status": "M", "status_label": "Modified"}
|
|
1165
|
+
]
|
|
1166
|
+
subprocess_run.return_value = mock.Mock(returncode=0, stdout="", stderr="")
|
|
1167
|
+
|
|
1168
|
+
ctx: dict[str, object] = {}
|
|
1169
|
+
_step_check_version(self.release, ctx, Path("rel.log"))
|
|
1170
|
+
|
|
1171
|
+
subprocess_run.assert_any_call(
|
|
1172
|
+
["git", "add", "CHANGELOG.rst"], check=True
|
|
1173
|
+
)
|
|
1174
|
+
subprocess_run.assert_any_call(
|
|
1175
|
+
["git", "commit", "-m", "docs: refresh changelog"], check=True
|
|
1176
|
+
)
|
|
1177
|
+
self.assertNotIn("dirty_files", ctx)
|
|
1178
|
+
|
|
1054
1179
|
@mock.patch("core.models.PackageRelease.dump_fixture")
|
|
1055
1180
|
def test_save_does_not_dump_fixture(self, dump):
|
|
1056
1181
|
self.release.pypi_url = "https://example.com"
|
|
@@ -1479,6 +1604,71 @@ class ReleaseProcessTests(TestCase):
|
|
|
1479
1604
|
self.assertEqual(count_file.read_text(), "1")
|
|
1480
1605
|
|
|
1481
1606
|
|
|
1607
|
+
class PackageReleaseFixtureTests(TestCase):
|
|
1608
|
+
def setUp(self):
|
|
1609
|
+
self.base = Path("core/fixtures")
|
|
1610
|
+
self.existing_fixtures = {
|
|
1611
|
+
path: path.read_text(encoding="utf-8")
|
|
1612
|
+
for path in self.base.glob("releases__*.json")
|
|
1613
|
+
}
|
|
1614
|
+
self.addCleanup(self._restore_fixtures)
|
|
1615
|
+
|
|
1616
|
+
self.package = Package.objects.create(name="fixture-pkg")
|
|
1617
|
+
self.release_one = PackageRelease.objects.create(
|
|
1618
|
+
package=self.package,
|
|
1619
|
+
version="9.9.9",
|
|
1620
|
+
)
|
|
1621
|
+
self.release_two = PackageRelease.objects.create(
|
|
1622
|
+
package=self.package,
|
|
1623
|
+
version="9.9.10",
|
|
1624
|
+
)
|
|
1625
|
+
|
|
1626
|
+
def _restore_fixtures(self):
|
|
1627
|
+
current_paths = set(self.base.glob("releases__*.json"))
|
|
1628
|
+
for path in current_paths:
|
|
1629
|
+
if path not in self.existing_fixtures:
|
|
1630
|
+
path.unlink()
|
|
1631
|
+
for path, content in self.existing_fixtures.items():
|
|
1632
|
+
path.write_text(content, encoding="utf-8")
|
|
1633
|
+
|
|
1634
|
+
def test_dump_fixture_only_writes_changed_releases(self):
|
|
1635
|
+
PackageRelease.dump_fixture()
|
|
1636
|
+
target_one = self.base / "releases__packagerelease_9_9_9.json"
|
|
1637
|
+
target_two = self.base / "releases__packagerelease_9_9_10.json"
|
|
1638
|
+
|
|
1639
|
+
self.assertTrue(target_one.exists())
|
|
1640
|
+
self.assertTrue(target_two.exists())
|
|
1641
|
+
|
|
1642
|
+
self.release_two.changelog = "updated notes"
|
|
1643
|
+
self.release_two.save(update_fields=["changelog"])
|
|
1644
|
+
|
|
1645
|
+
original_write = Path.write_text
|
|
1646
|
+
written_paths: list[Path] = []
|
|
1647
|
+
|
|
1648
|
+
def tracking_write_text(path_obj, data, *args, **kwargs):
|
|
1649
|
+
written_paths.append(path_obj)
|
|
1650
|
+
return original_write(path_obj, data, *args, **kwargs)
|
|
1651
|
+
|
|
1652
|
+
with mock.patch("pathlib.Path.write_text", tracking_write_text):
|
|
1653
|
+
PackageRelease.dump_fixture()
|
|
1654
|
+
|
|
1655
|
+
written_set = set(written_paths)
|
|
1656
|
+
self.assertNotIn(target_one, written_set)
|
|
1657
|
+
self.assertIn(target_two, written_set)
|
|
1658
|
+
self.assertIn("updated notes", target_two.read_text(encoding="utf-8"))
|
|
1659
|
+
|
|
1660
|
+
def test_dump_fixture_removes_missing_release_files(self):
|
|
1661
|
+
PackageRelease.dump_fixture()
|
|
1662
|
+
target_two = self.base / "releases__packagerelease_9_9_10.json"
|
|
1663
|
+
self.assertTrue(target_two.exists())
|
|
1664
|
+
|
|
1665
|
+
self.release_two.delete()
|
|
1666
|
+
|
|
1667
|
+
PackageRelease.dump_fixture()
|
|
1668
|
+
|
|
1669
|
+
self.assertFalse(target_two.exists())
|
|
1670
|
+
|
|
1671
|
+
|
|
1482
1672
|
class ReleaseProgressSyncTests(TestCase):
|
|
1483
1673
|
def setUp(self):
|
|
1484
1674
|
self.client = Client()
|
|
@@ -1971,6 +2161,42 @@ class TodoDoneTests(TestCase):
|
|
|
1971
2161
|
todo.refresh_from_db()
|
|
1972
2162
|
self.assertIsNotNone(todo.done_on)
|
|
1973
2163
|
|
|
2164
|
+
def test_env_refresh_preserves_completed_fixture_todo(self):
|
|
2165
|
+
base_dir = Path(settings.BASE_DIR)
|
|
2166
|
+
fixture_path = base_dir / "core" / "fixtures" / "todo__validate_screen_system_reports.json"
|
|
2167
|
+
spec = importlib.util.spec_from_file_location(
|
|
2168
|
+
"env_refresh_todo", base_dir / "env-refresh.py"
|
|
2169
|
+
)
|
|
2170
|
+
env_refresh = importlib.util.module_from_spec(spec)
|
|
2171
|
+
spec.loader.exec_module(env_refresh)
|
|
2172
|
+
fixture_rel_path = str(fixture_path.relative_to(base_dir))
|
|
2173
|
+
env_refresh._fixture_files = lambda: [fixture_rel_path]
|
|
2174
|
+
|
|
2175
|
+
from django.core.management import call_command as django_call
|
|
2176
|
+
|
|
2177
|
+
def fake_call_command(name, *args, **kwargs):
|
|
2178
|
+
if name == "loaddata":
|
|
2179
|
+
return django_call(name, *args, **kwargs)
|
|
2180
|
+
return None
|
|
2181
|
+
|
|
2182
|
+
env_refresh.call_command = fake_call_command
|
|
2183
|
+
env_refresh.load_shared_user_fixtures = lambda *args, **kwargs: None
|
|
2184
|
+
env_refresh.load_user_fixtures = lambda *args, **kwargs: None
|
|
2185
|
+
env_refresh.generate_model_sigils = lambda: None
|
|
2186
|
+
|
|
2187
|
+
env_refresh.run_database_tasks()
|
|
2188
|
+
|
|
2189
|
+
todo = Todo.objects.get(request="Validate screen System Reports")
|
|
2190
|
+
self.assertTrue(todo.is_seed_data)
|
|
2191
|
+
todo.done_on = timezone.now()
|
|
2192
|
+
todo.save(update_fields=["done_on"])
|
|
2193
|
+
|
|
2194
|
+
env_refresh.run_database_tasks()
|
|
2195
|
+
|
|
2196
|
+
todo.refresh_from_db()
|
|
2197
|
+
self.assertIsNotNone(todo.done_on)
|
|
2198
|
+
self.assertTrue(todo.is_seed_data)
|
|
2199
|
+
|
|
1974
2200
|
|
|
1975
2201
|
class TodoFocusViewTests(TestCase):
|
|
1976
2202
|
def setUp(self):
|
|
@@ -1990,6 +2216,9 @@ class TodoFocusViewTests(TestCase):
|
|
|
1990
2216
|
self.assertContains(resp, f'src="{todo.url}"')
|
|
1991
2217
|
self.assertContains(resp, "Done")
|
|
1992
2218
|
self.assertContains(resp, "Back")
|
|
2219
|
+
self.assertContains(resp, "Take Snapshot")
|
|
2220
|
+
snapshot_url = reverse("todo-snapshot", args=[todo.pk])
|
|
2221
|
+
self.assertContains(resp, snapshot_url)
|
|
1993
2222
|
|
|
1994
2223
|
def test_focus_view_uses_admin_change_when_no_url(self):
|
|
1995
2224
|
todo = Todo.objects.create(request="Task")
|
|
@@ -2061,6 +2290,57 @@ class TodoFocusViewTests(TestCase):
|
|
|
2061
2290
|
self.assertRedirects(resp, next_url, target_status_code=200)
|
|
2062
2291
|
|
|
2063
2292
|
|
|
2293
|
+
class TodoSnapshotViewTests(TestCase):
|
|
2294
|
+
PNG_PIXEL = (
|
|
2295
|
+
"data:image/png;base64," "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR42mP8/5+hHgAFgwJ/lSdX6QAAAABJRU5ErkJggg=="
|
|
2296
|
+
)
|
|
2297
|
+
|
|
2298
|
+
def setUp(self):
|
|
2299
|
+
self.user = User.objects.create_user(
|
|
2300
|
+
username="manager", password="secret", is_staff=True
|
|
2301
|
+
)
|
|
2302
|
+
self.client.force_login(self.user)
|
|
2303
|
+
self.todo = Todo.objects.create(
|
|
2304
|
+
request="QA release notes", request_details="Verify layout"
|
|
2305
|
+
)
|
|
2306
|
+
|
|
2307
|
+
def test_snapshot_creates_content_sample(self):
|
|
2308
|
+
tmpdir = tempfile.TemporaryDirectory()
|
|
2309
|
+
self.addCleanup(tmpdir.cleanup)
|
|
2310
|
+
with override_settings(LOG_DIR=Path(tmpdir.name)):
|
|
2311
|
+
response = self.client.post(
|
|
2312
|
+
reverse("todo-snapshot", args=[self.todo.pk]),
|
|
2313
|
+
data=json.dumps({"image": self.PNG_PIXEL}),
|
|
2314
|
+
content_type="application/json",
|
|
2315
|
+
)
|
|
2316
|
+
self.assertEqual(response.status_code, 200)
|
|
2317
|
+
payload = response.json()
|
|
2318
|
+
self.assertIn("sample", payload)
|
|
2319
|
+
self.assertEqual(ContentSample.objects.filter(kind=ContentSample.IMAGE).count(), 1)
|
|
2320
|
+
sample = ContentSample.objects.get(pk=payload["sample"])
|
|
2321
|
+
self.assertEqual(sample.method, "TODO_QA")
|
|
2322
|
+
self.assertEqual(sample.user, self.user)
|
|
2323
|
+
self.assertEqual(sample.content, "QA release notes — Verify layout")
|
|
2324
|
+
rel_path = Path(sample.path)
|
|
2325
|
+
self.assertTrue(rel_path.parts)
|
|
2326
|
+
self.assertEqual(rel_path.parts[0], "screenshots")
|
|
2327
|
+
self.assertTrue((Path(tmpdir.name) / rel_path).exists())
|
|
2328
|
+
|
|
2329
|
+
def test_snapshot_rejects_completed_todo(self):
|
|
2330
|
+
self.todo.done_on = timezone.now()
|
|
2331
|
+
self.todo.save(update_fields=["done_on"])
|
|
2332
|
+
tmpdir = tempfile.TemporaryDirectory()
|
|
2333
|
+
self.addCleanup(tmpdir.cleanup)
|
|
2334
|
+
with override_settings(LOG_DIR=Path(tmpdir.name)):
|
|
2335
|
+
response = self.client.post(
|
|
2336
|
+
reverse("todo-snapshot", args=[self.todo.pk]),
|
|
2337
|
+
data=json.dumps({"image": self.PNG_PIXEL}),
|
|
2338
|
+
content_type="application/json",
|
|
2339
|
+
)
|
|
2340
|
+
self.assertEqual(response.status_code, 400)
|
|
2341
|
+
self.assertEqual(ContentSample.objects.count(), 0)
|
|
2342
|
+
|
|
2343
|
+
|
|
2064
2344
|
class TodoUrlValidationTests(TestCase):
|
|
2065
2345
|
def test_relative_url_valid(self):
|
|
2066
2346
|
todo = Todo(request="Task", url="/path")
|