arthexis 0.1.15__py3-none-any.whl → 0.1.17__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.15.dist-info → arthexis-0.1.17.dist-info}/METADATA +1 -2
- {arthexis-0.1.15.dist-info → arthexis-0.1.17.dist-info}/RECORD +40 -39
- config/settings.py +3 -0
- config/urls.py +5 -0
- core/admin.py +242 -15
- core/admindocs.py +44 -3
- core/apps.py +1 -1
- core/backends.py +46 -8
- core/changelog.py +66 -5
- core/github_issues.py +12 -7
- core/mailer.py +9 -5
- core/models.py +121 -29
- core/release.py +107 -2
- core/system.py +209 -2
- core/tasks.py +5 -7
- core/test_system_info.py +16 -0
- core/tests.py +329 -0
- core/views.py +279 -40
- nodes/admin.py +25 -1
- nodes/models.py +70 -4
- nodes/rfid_sync.py +15 -0
- nodes/tests.py +119 -0
- nodes/utils.py +3 -0
- ocpp/admin.py +92 -10
- ocpp/consumers.py +38 -0
- ocpp/models.py +19 -4
- ocpp/tasks.py +156 -2
- ocpp/test_rfid.py +92 -5
- ocpp/tests.py +243 -1
- ocpp/views.py +23 -5
- pages/admin.py +126 -4
- pages/context_processors.py +20 -1
- pages/models.py +3 -1
- pages/module_defaults.py +156 -0
- pages/tests.py +241 -8
- pages/urls.py +1 -0
- pages/views.py +61 -4
- {arthexis-0.1.15.dist-info → arthexis-0.1.17.dist-info}/WHEEL +0 -0
- {arthexis-0.1.15.dist-info → arthexis-0.1.17.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.15.dist-info → arthexis-0.1.17.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
|
|
@@ -19,9 +20,11 @@ import types
|
|
|
19
20
|
from glob import glob
|
|
20
21
|
from datetime import datetime, timedelta, timezone as datetime_timezone
|
|
21
22
|
import tempfile
|
|
23
|
+
from io import StringIO
|
|
22
24
|
from urllib.parse import quote
|
|
23
25
|
|
|
24
26
|
from django.utils import timezone
|
|
27
|
+
from django.conf import settings
|
|
25
28
|
from django.contrib.auth.models import Permission
|
|
26
29
|
from django.contrib.messages import get_messages
|
|
27
30
|
from tablib import Dataset
|
|
@@ -51,9 +54,11 @@ from core.admin import (
|
|
|
51
54
|
USER_PROFILE_INLINES,
|
|
52
55
|
)
|
|
53
56
|
from ocpp.models import Transaction, Charger
|
|
57
|
+
from nodes.models import ContentSample
|
|
54
58
|
|
|
55
59
|
from django.core.exceptions import ValidationError
|
|
56
60
|
from django.core.management import call_command
|
|
61
|
+
from django.core.management.base import CommandError
|
|
57
62
|
from django.db import IntegrityError
|
|
58
63
|
from .backends import LocalhostAdminBackend
|
|
59
64
|
from core.views import (
|
|
@@ -365,6 +370,45 @@ class RFIDLoginTests(TestCase):
|
|
|
365
370
|
self.assertEqual(response.status_code, 401)
|
|
366
371
|
mock_run.assert_called_once()
|
|
367
372
|
|
|
373
|
+
@patch("core.backends.subprocess.Popen")
|
|
374
|
+
def test_rfid_login_post_command_runs_after_success(self, mock_popen):
|
|
375
|
+
tag = self.account.rfids.first()
|
|
376
|
+
tag.post_auth_command = "echo welcome"
|
|
377
|
+
tag.save(update_fields=["post_auth_command"])
|
|
378
|
+
|
|
379
|
+
response = self.client.post(
|
|
380
|
+
reverse("rfid-login"),
|
|
381
|
+
data={"rfid": "CARD123"},
|
|
382
|
+
content_type="application/json",
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
self.assertEqual(response.status_code, 200)
|
|
386
|
+
mock_popen.assert_called_once()
|
|
387
|
+
args, kwargs = mock_popen.call_args
|
|
388
|
+
self.assertEqual(args[0], "echo welcome")
|
|
389
|
+
self.assertTrue(kwargs.get("shell"))
|
|
390
|
+
env = kwargs.get("env", {})
|
|
391
|
+
self.assertEqual(env.get("RFID_VALUE"), "CARD123")
|
|
392
|
+
self.assertEqual(env.get("RFID_LABEL_ID"), str(tag.pk))
|
|
393
|
+
self.assertIs(kwargs.get("stdout"), subprocess.DEVNULL)
|
|
394
|
+
self.assertIs(kwargs.get("stderr"), subprocess.DEVNULL)
|
|
395
|
+
|
|
396
|
+
@patch("core.backends.subprocess.Popen")
|
|
397
|
+
def test_rfid_login_post_command_skipped_on_failure(self, mock_popen):
|
|
398
|
+
tag = self.account.rfids.first()
|
|
399
|
+
tag.post_auth_command = "echo welcome"
|
|
400
|
+
tag.allowed = False
|
|
401
|
+
tag.save(update_fields=["post_auth_command", "allowed"])
|
|
402
|
+
|
|
403
|
+
response = self.client.post(
|
|
404
|
+
reverse("rfid-login"),
|
|
405
|
+
data={"rfid": "CARD123"},
|
|
406
|
+
content_type="application/json",
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
self.assertEqual(response.status_code, 401)
|
|
410
|
+
mock_popen.assert_not_called()
|
|
411
|
+
|
|
368
412
|
|
|
369
413
|
class RFIDBatchApiTests(TestCase):
|
|
370
414
|
def setUp(self):
|
|
@@ -387,6 +431,8 @@ class RFIDBatchApiTests(TestCase):
|
|
|
387
431
|
"rfid": "CARD999",
|
|
388
432
|
"custom_label": "Main Tag",
|
|
389
433
|
"energy_accounts": [self.account.id],
|
|
434
|
+
"external_command": "",
|
|
435
|
+
"post_auth_command": "",
|
|
390
436
|
"allowed": True,
|
|
391
437
|
"color": "B",
|
|
392
438
|
"released": False,
|
|
@@ -406,6 +452,8 @@ class RFIDBatchApiTests(TestCase):
|
|
|
406
452
|
"rfid": "CARD111",
|
|
407
453
|
"custom_label": "",
|
|
408
454
|
"energy_accounts": [],
|
|
455
|
+
"external_command": "",
|
|
456
|
+
"post_auth_command": "",
|
|
409
457
|
"allowed": True,
|
|
410
458
|
"color": "W",
|
|
411
459
|
"released": False,
|
|
@@ -426,6 +474,8 @@ class RFIDBatchApiTests(TestCase):
|
|
|
426
474
|
"rfid": "CARD112",
|
|
427
475
|
"custom_label": "",
|
|
428
476
|
"energy_accounts": [],
|
|
477
|
+
"external_command": "",
|
|
478
|
+
"post_auth_command": "",
|
|
429
479
|
"allowed": True,
|
|
430
480
|
"color": "B",
|
|
431
481
|
"released": True,
|
|
@@ -441,6 +491,8 @@ class RFIDBatchApiTests(TestCase):
|
|
|
441
491
|
"rfid": "A1B2C3D4",
|
|
442
492
|
"custom_label": "Imported Tag",
|
|
443
493
|
"energy_accounts": [self.account.id],
|
|
494
|
+
"external_command": "echo pre",
|
|
495
|
+
"post_auth_command": "echo post",
|
|
444
496
|
"allowed": True,
|
|
445
497
|
"color": "W",
|
|
446
498
|
"released": True,
|
|
@@ -459,6 +511,8 @@ class RFIDBatchApiTests(TestCase):
|
|
|
459
511
|
rfid="A1B2C3D4",
|
|
460
512
|
custom_label="Imported Tag",
|
|
461
513
|
energy_accounts=self.account,
|
|
514
|
+
external_command="echo pre",
|
|
515
|
+
post_auth_command="echo post",
|
|
462
516
|
color=RFID.WHITE,
|
|
463
517
|
released=True,
|
|
464
518
|
).exists()
|
|
@@ -931,6 +985,40 @@ class RFIDImportExportCommandTests(TestCase):
|
|
|
931
985
|
self.assertTrue(tag.energy_accounts.filter(pk=account.pk).exists())
|
|
932
986
|
|
|
933
987
|
|
|
988
|
+
class CheckRFIDCommandTests(TestCase):
|
|
989
|
+
def test_successful_validation_outputs_json(self):
|
|
990
|
+
out = StringIO()
|
|
991
|
+
|
|
992
|
+
call_command("check_rfid", "abcd1234", stdout=out)
|
|
993
|
+
|
|
994
|
+
payload = json.loads(out.getvalue())
|
|
995
|
+
self.assertEqual(payload["rfid"], "ABCD1234")
|
|
996
|
+
self.assertTrue(payload["created"])
|
|
997
|
+
self.assertTrue(RFID.objects.filter(rfid="ABCD1234").exists())
|
|
998
|
+
|
|
999
|
+
def test_invalid_value_raises_error(self):
|
|
1000
|
+
with self.assertRaises(CommandError):
|
|
1001
|
+
call_command("check_rfid", "invalid!")
|
|
1002
|
+
|
|
1003
|
+
def test_kind_option_updates_existing_tag(self):
|
|
1004
|
+
tag = RFID.objects.create(rfid="EXISTING", allowed=False, kind=RFID.CLASSIC)
|
|
1005
|
+
out = StringIO()
|
|
1006
|
+
|
|
1007
|
+
call_command(
|
|
1008
|
+
"check_rfid",
|
|
1009
|
+
"existing",
|
|
1010
|
+
"--kind",
|
|
1011
|
+
RFID.NTAG215,
|
|
1012
|
+
stdout=out,
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
payload = json.loads(out.getvalue())
|
|
1016
|
+
tag.refresh_from_db()
|
|
1017
|
+
self.assertFalse(payload["created"])
|
|
1018
|
+
self.assertEqual(payload["kind"], RFID.NTAG215)
|
|
1019
|
+
self.assertEqual(tag.kind, RFID.NTAG215)
|
|
1020
|
+
|
|
1021
|
+
|
|
934
1022
|
class RFIDKeyVerificationFlagTests(TestCase):
|
|
935
1023
|
def test_flags_reset_on_key_change(self):
|
|
936
1024
|
tag = RFID.objects.create(
|
|
@@ -985,6 +1073,44 @@ class ReleaseProcessTests(TestCase):
|
|
|
985
1073
|
)
|
|
986
1074
|
sync_main.assert_called_once_with(Path("rel.log"))
|
|
987
1075
|
|
|
1076
|
+
def test_step_check_todos_logs_instruction_when_pending(self):
|
|
1077
|
+
log_path = Path("rel.log")
|
|
1078
|
+
log_path.unlink(missing_ok=True)
|
|
1079
|
+
Todo.objects.create(request="Review checklist")
|
|
1080
|
+
ctx: dict[str, object] = {}
|
|
1081
|
+
|
|
1082
|
+
try:
|
|
1083
|
+
with self.assertRaises(core_views.PendingTodos):
|
|
1084
|
+
core_views._step_check_todos(self.release, ctx, log_path)
|
|
1085
|
+
|
|
1086
|
+
contents = log_path.read_text(encoding="utf-8")
|
|
1087
|
+
message = "Release checklist requires acknowledgment before continuing."
|
|
1088
|
+
self.assertIn(message, contents)
|
|
1089
|
+
self.assertIn("Review outstanding TODO items", contents)
|
|
1090
|
+
|
|
1091
|
+
with self.assertRaises(core_views.PendingTodos):
|
|
1092
|
+
core_views._step_check_todos(self.release, ctx, log_path)
|
|
1093
|
+
|
|
1094
|
+
contents = log_path.read_text(encoding="utf-8")
|
|
1095
|
+
self.assertEqual(contents.count(message), 1)
|
|
1096
|
+
finally:
|
|
1097
|
+
log_path.unlink(missing_ok=True)
|
|
1098
|
+
|
|
1099
|
+
def test_step_check_todos_auto_ack_when_no_pending(self):
|
|
1100
|
+
log_path = Path("rel.log")
|
|
1101
|
+
log_path.unlink(missing_ok=True)
|
|
1102
|
+
ctx: dict[str, object] = {}
|
|
1103
|
+
|
|
1104
|
+
try:
|
|
1105
|
+
with mock.patch("core.views._refresh_changelog_once"):
|
|
1106
|
+
core_views._step_check_todos(self.release, ctx, log_path)
|
|
1107
|
+
finally:
|
|
1108
|
+
log_path.unlink(missing_ok=True)
|
|
1109
|
+
|
|
1110
|
+
self.assertTrue(ctx.get("todos_ack"))
|
|
1111
|
+
self.assertNotIn("todos_required", ctx)
|
|
1112
|
+
self.assertIsNone(ctx.get("todos"))
|
|
1113
|
+
|
|
988
1114
|
@mock.patch("core.views._sync_with_origin_main")
|
|
989
1115
|
@mock.patch("core.views.release_utils._git_clean", return_value=True)
|
|
990
1116
|
@mock.patch("core.views.release_utils.network_available", return_value=False)
|
|
@@ -1475,6 +1601,54 @@ class ReleaseProcessTests(TestCase):
|
|
|
1475
1601
|
ctx = session.get(f"release_publish_{self.release.pk}")
|
|
1476
1602
|
self.assertTrue(ctx.get("dry_run"))
|
|
1477
1603
|
|
|
1604
|
+
def test_resume_button_shown_when_credentials_missing(self):
|
|
1605
|
+
user = User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1606
|
+
url = reverse("release-progress", args=[self.release.pk, "publish"])
|
|
1607
|
+
self.client.force_login(user)
|
|
1608
|
+
|
|
1609
|
+
self.client.get(f"{url}?start=1")
|
|
1610
|
+
|
|
1611
|
+
session = self.client.session
|
|
1612
|
+
ctx = session.get(f"release_publish_{self.release.pk}") or {}
|
|
1613
|
+
ctx.update({"step": 7, "started": True, "paused": False})
|
|
1614
|
+
session[f"release_publish_{self.release.pk}"] = ctx
|
|
1615
|
+
session.save()
|
|
1616
|
+
|
|
1617
|
+
response = self.client.get(f"{url}?step=7")
|
|
1618
|
+
self.assertEqual(response.status_code, 200)
|
|
1619
|
+
context = response.context
|
|
1620
|
+
if isinstance(context, list):
|
|
1621
|
+
context = context[-1]
|
|
1622
|
+
self.assertTrue(context["resume_available"])
|
|
1623
|
+
self.assertIn(b"Resume Publish", response.content)
|
|
1624
|
+
|
|
1625
|
+
def test_resume_without_step_parameter_defaults_to_current_progress(self):
|
|
1626
|
+
run: list[str] = []
|
|
1627
|
+
|
|
1628
|
+
def step_fn(release, ctx, log_path):
|
|
1629
|
+
run.append("step")
|
|
1630
|
+
|
|
1631
|
+
steps = [("Only step", step_fn)]
|
|
1632
|
+
user = User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1633
|
+
url = reverse("release-progress", args=[self.release.pk, "publish"])
|
|
1634
|
+
with mock.patch("core.views.PUBLISH_STEPS", steps):
|
|
1635
|
+
self.client.force_login(user)
|
|
1636
|
+
session = self.client.session
|
|
1637
|
+
session[f"release_publish_{self.release.pk}"] = {
|
|
1638
|
+
"step": 0,
|
|
1639
|
+
"started": True,
|
|
1640
|
+
"paused": False,
|
|
1641
|
+
}
|
|
1642
|
+
session.save()
|
|
1643
|
+
|
|
1644
|
+
response = self.client.get(f"{url}?resume=1")
|
|
1645
|
+
self.assertEqual(response.status_code, 200)
|
|
1646
|
+
self.assertEqual(run, ["step"])
|
|
1647
|
+
|
|
1648
|
+
session = self.client.session
|
|
1649
|
+
ctx = session.get(f"release_publish_{self.release.pk}")
|
|
1650
|
+
self.assertEqual(ctx.get("step"), 1)
|
|
1651
|
+
|
|
1478
1652
|
def test_new_todo_does_not_reset_pending_flow(self):
|
|
1479
1653
|
user = User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1480
1654
|
url = reverse("release-progress", args=[self.release.pk, "publish"])
|
|
@@ -1552,6 +1726,71 @@ class ReleaseProcessTests(TestCase):
|
|
|
1552
1726
|
self.assertEqual(count_file.read_text(), "1")
|
|
1553
1727
|
|
|
1554
1728
|
|
|
1729
|
+
class PackageReleaseFixtureTests(TestCase):
|
|
1730
|
+
def setUp(self):
|
|
1731
|
+
self.base = Path("core/fixtures")
|
|
1732
|
+
self.existing_fixtures = {
|
|
1733
|
+
path: path.read_text(encoding="utf-8")
|
|
1734
|
+
for path in self.base.glob("releases__*.json")
|
|
1735
|
+
}
|
|
1736
|
+
self.addCleanup(self._restore_fixtures)
|
|
1737
|
+
|
|
1738
|
+
self.package = Package.objects.create(name="fixture-pkg")
|
|
1739
|
+
self.release_one = PackageRelease.objects.create(
|
|
1740
|
+
package=self.package,
|
|
1741
|
+
version="9.9.9",
|
|
1742
|
+
)
|
|
1743
|
+
self.release_two = PackageRelease.objects.create(
|
|
1744
|
+
package=self.package,
|
|
1745
|
+
version="9.9.10",
|
|
1746
|
+
)
|
|
1747
|
+
|
|
1748
|
+
def _restore_fixtures(self):
|
|
1749
|
+
current_paths = set(self.base.glob("releases__*.json"))
|
|
1750
|
+
for path in current_paths:
|
|
1751
|
+
if path not in self.existing_fixtures:
|
|
1752
|
+
path.unlink()
|
|
1753
|
+
for path, content in self.existing_fixtures.items():
|
|
1754
|
+
path.write_text(content, encoding="utf-8")
|
|
1755
|
+
|
|
1756
|
+
def test_dump_fixture_only_writes_changed_releases(self):
|
|
1757
|
+
PackageRelease.dump_fixture()
|
|
1758
|
+
target_one = self.base / "releases__packagerelease_9_9_9.json"
|
|
1759
|
+
target_two = self.base / "releases__packagerelease_9_9_10.json"
|
|
1760
|
+
|
|
1761
|
+
self.assertTrue(target_one.exists())
|
|
1762
|
+
self.assertTrue(target_two.exists())
|
|
1763
|
+
|
|
1764
|
+
self.release_two.changelog = "updated notes"
|
|
1765
|
+
self.release_two.save(update_fields=["changelog"])
|
|
1766
|
+
|
|
1767
|
+
original_write = Path.write_text
|
|
1768
|
+
written_paths: list[Path] = []
|
|
1769
|
+
|
|
1770
|
+
def tracking_write_text(path_obj, data, *args, **kwargs):
|
|
1771
|
+
written_paths.append(path_obj)
|
|
1772
|
+
return original_write(path_obj, data, *args, **kwargs)
|
|
1773
|
+
|
|
1774
|
+
with mock.patch("pathlib.Path.write_text", tracking_write_text):
|
|
1775
|
+
PackageRelease.dump_fixture()
|
|
1776
|
+
|
|
1777
|
+
written_set = set(written_paths)
|
|
1778
|
+
self.assertNotIn(target_one, written_set)
|
|
1779
|
+
self.assertIn(target_two, written_set)
|
|
1780
|
+
self.assertIn("updated notes", target_two.read_text(encoding="utf-8"))
|
|
1781
|
+
|
|
1782
|
+
def test_dump_fixture_removes_missing_release_files(self):
|
|
1783
|
+
PackageRelease.dump_fixture()
|
|
1784
|
+
target_two = self.base / "releases__packagerelease_9_9_10.json"
|
|
1785
|
+
self.assertTrue(target_two.exists())
|
|
1786
|
+
|
|
1787
|
+
self.release_two.delete()
|
|
1788
|
+
|
|
1789
|
+
PackageRelease.dump_fixture()
|
|
1790
|
+
|
|
1791
|
+
self.assertFalse(target_two.exists())
|
|
1792
|
+
|
|
1793
|
+
|
|
1555
1794
|
class ReleaseProgressSyncTests(TestCase):
|
|
1556
1795
|
def setUp(self):
|
|
1557
1796
|
self.client = Client()
|
|
@@ -2044,6 +2283,42 @@ class TodoDoneTests(TestCase):
|
|
|
2044
2283
|
todo.refresh_from_db()
|
|
2045
2284
|
self.assertIsNotNone(todo.done_on)
|
|
2046
2285
|
|
|
2286
|
+
def test_env_refresh_preserves_completed_fixture_todo(self):
|
|
2287
|
+
base_dir = Path(settings.BASE_DIR)
|
|
2288
|
+
fixture_path = base_dir / "core" / "fixtures" / "todo__validate_screen_system_reports.json"
|
|
2289
|
+
spec = importlib.util.spec_from_file_location(
|
|
2290
|
+
"env_refresh_todo", base_dir / "env-refresh.py"
|
|
2291
|
+
)
|
|
2292
|
+
env_refresh = importlib.util.module_from_spec(spec)
|
|
2293
|
+
spec.loader.exec_module(env_refresh)
|
|
2294
|
+
fixture_rel_path = str(fixture_path.relative_to(base_dir))
|
|
2295
|
+
env_refresh._fixture_files = lambda: [fixture_rel_path]
|
|
2296
|
+
|
|
2297
|
+
from django.core.management import call_command as django_call
|
|
2298
|
+
|
|
2299
|
+
def fake_call_command(name, *args, **kwargs):
|
|
2300
|
+
if name == "loaddata":
|
|
2301
|
+
return django_call(name, *args, **kwargs)
|
|
2302
|
+
return None
|
|
2303
|
+
|
|
2304
|
+
env_refresh.call_command = fake_call_command
|
|
2305
|
+
env_refresh.load_shared_user_fixtures = lambda *args, **kwargs: None
|
|
2306
|
+
env_refresh.load_user_fixtures = lambda *args, **kwargs: None
|
|
2307
|
+
env_refresh.generate_model_sigils = lambda: None
|
|
2308
|
+
|
|
2309
|
+
env_refresh.run_database_tasks()
|
|
2310
|
+
|
|
2311
|
+
todo = Todo.objects.get(request="Validate screen System Reports")
|
|
2312
|
+
self.assertTrue(todo.is_seed_data)
|
|
2313
|
+
todo.done_on = timezone.now()
|
|
2314
|
+
todo.save(update_fields=["done_on"])
|
|
2315
|
+
|
|
2316
|
+
env_refresh.run_database_tasks()
|
|
2317
|
+
|
|
2318
|
+
todo.refresh_from_db()
|
|
2319
|
+
self.assertIsNotNone(todo.done_on)
|
|
2320
|
+
self.assertTrue(todo.is_seed_data)
|
|
2321
|
+
|
|
2047
2322
|
|
|
2048
2323
|
class TodoFocusViewTests(TestCase):
|
|
2049
2324
|
def setUp(self):
|
|
@@ -2063,6 +2338,9 @@ class TodoFocusViewTests(TestCase):
|
|
|
2063
2338
|
self.assertContains(resp, f'src="{todo.url}"')
|
|
2064
2339
|
self.assertContains(resp, "Done")
|
|
2065
2340
|
self.assertContains(resp, "Back")
|
|
2341
|
+
self.assertContains(resp, "Take Snapshot")
|
|
2342
|
+
snapshot_url = reverse("todo-snapshot", args=[todo.pk])
|
|
2343
|
+
self.assertContains(resp, snapshot_url)
|
|
2066
2344
|
|
|
2067
2345
|
def test_focus_view_uses_admin_change_when_no_url(self):
|
|
2068
2346
|
todo = Todo.objects.create(request="Task")
|
|
@@ -2134,6 +2412,57 @@ class TodoFocusViewTests(TestCase):
|
|
|
2134
2412
|
self.assertRedirects(resp, next_url, target_status_code=200)
|
|
2135
2413
|
|
|
2136
2414
|
|
|
2415
|
+
class TodoSnapshotViewTests(TestCase):
|
|
2416
|
+
PNG_PIXEL = (
|
|
2417
|
+
"data:image/png;base64," "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR42mP8/5+hHgAFgwJ/lSdX6QAAAABJRU5ErkJggg=="
|
|
2418
|
+
)
|
|
2419
|
+
|
|
2420
|
+
def setUp(self):
|
|
2421
|
+
self.user = User.objects.create_user(
|
|
2422
|
+
username="manager", password="secret", is_staff=True
|
|
2423
|
+
)
|
|
2424
|
+
self.client.force_login(self.user)
|
|
2425
|
+
self.todo = Todo.objects.create(
|
|
2426
|
+
request="QA release notes", request_details="Verify layout"
|
|
2427
|
+
)
|
|
2428
|
+
|
|
2429
|
+
def test_snapshot_creates_content_sample(self):
|
|
2430
|
+
tmpdir = tempfile.TemporaryDirectory()
|
|
2431
|
+
self.addCleanup(tmpdir.cleanup)
|
|
2432
|
+
with override_settings(LOG_DIR=Path(tmpdir.name)):
|
|
2433
|
+
response = self.client.post(
|
|
2434
|
+
reverse("todo-snapshot", args=[self.todo.pk]),
|
|
2435
|
+
data=json.dumps({"image": self.PNG_PIXEL}),
|
|
2436
|
+
content_type="application/json",
|
|
2437
|
+
)
|
|
2438
|
+
self.assertEqual(response.status_code, 200)
|
|
2439
|
+
payload = response.json()
|
|
2440
|
+
self.assertIn("sample", payload)
|
|
2441
|
+
self.assertEqual(ContentSample.objects.filter(kind=ContentSample.IMAGE).count(), 1)
|
|
2442
|
+
sample = ContentSample.objects.get(pk=payload["sample"])
|
|
2443
|
+
self.assertEqual(sample.method, "TODO_QA")
|
|
2444
|
+
self.assertEqual(sample.user, self.user)
|
|
2445
|
+
self.assertEqual(sample.content, "QA release notes — Verify layout")
|
|
2446
|
+
rel_path = Path(sample.path)
|
|
2447
|
+
self.assertTrue(rel_path.parts)
|
|
2448
|
+
self.assertEqual(rel_path.parts[0], "screenshots")
|
|
2449
|
+
self.assertTrue((Path(tmpdir.name) / rel_path).exists())
|
|
2450
|
+
|
|
2451
|
+
def test_snapshot_rejects_completed_todo(self):
|
|
2452
|
+
self.todo.done_on = timezone.now()
|
|
2453
|
+
self.todo.save(update_fields=["done_on"])
|
|
2454
|
+
tmpdir = tempfile.TemporaryDirectory()
|
|
2455
|
+
self.addCleanup(tmpdir.cleanup)
|
|
2456
|
+
with override_settings(LOG_DIR=Path(tmpdir.name)):
|
|
2457
|
+
response = self.client.post(
|
|
2458
|
+
reverse("todo-snapshot", args=[self.todo.pk]),
|
|
2459
|
+
data=json.dumps({"image": self.PNG_PIXEL}),
|
|
2460
|
+
content_type="application/json",
|
|
2461
|
+
)
|
|
2462
|
+
self.assertEqual(response.status_code, 400)
|
|
2463
|
+
self.assertEqual(ContentSample.objects.count(), 0)
|
|
2464
|
+
|
|
2465
|
+
|
|
2137
2466
|
class TodoUrlValidationTests(TestCase):
|
|
2138
2467
|
def test_relative_url_valid(self):
|
|
2139
2468
|
todo = Todo(request="Task", url="/path")
|