arthexis 0.1.20__py3-none-any.whl → 0.1.22__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.

core/tests.py CHANGED
@@ -1290,11 +1290,10 @@ class ReleaseProcessTests(TestCase):
1290
1290
  run.assert_any_call(["git", "clean", "-fd"], check=False)
1291
1291
 
1292
1292
  @mock.patch("core.views.PackageRelease.dump_fixture")
1293
- @mock.patch("core.views._ensure_release_todo")
1294
1293
  @mock.patch("core.views._sync_with_origin_main")
1295
1294
  @mock.patch("core.views.subprocess.run")
1296
1295
  def test_pre_release_syncs_with_main(
1297
- self, run, sync_main, ensure_todo, dump_fixture
1296
+ self, run, sync_main, dump_fixture
1298
1297
  ):
1299
1298
  import subprocess as sp
1300
1299
 
@@ -1306,11 +1305,6 @@ class ReleaseProcessTests(TestCase):
1306
1305
  return sp.CompletedProcess(cmd, 0)
1307
1306
 
1308
1307
  run.side_effect = fake_run
1309
- ensure_todo.return_value = (
1310
- mock.Mock(request="Create release pkg 1.0.1", url="", request_details=""),
1311
- Path("core/fixtures/todos__next_release.json"),
1312
- )
1313
-
1314
1308
  version_path = Path("VERSION")
1315
1309
  original_version = version_path.read_text(encoding="utf-8")
1316
1310
 
@@ -1975,7 +1969,7 @@ class PackageReleaseAdminActionTests(TestCase):
1975
1969
 
1976
1970
  @mock.patch("core.admin.PackageRelease.dump_fixture")
1977
1971
  @mock.patch("core.admin.requests.get")
1978
- def test_refresh_from_pypi_creates_releases(self, mock_get, dump):
1972
+ def test_refresh_from_pypi_reports_missing_releases(self, mock_get, dump):
1979
1973
  mock_get.return_value.raise_for_status.return_value = None
1980
1974
  mock_get.return_value.json.return_value = {
1981
1975
  "releases": {
@@ -1988,13 +1982,17 @@ class PackageReleaseAdminActionTests(TestCase):
1988
1982
  }
1989
1983
  }
1990
1984
  self.admin.refresh_from_pypi(self.request, PackageRelease.objects.none())
1991
- new_release = PackageRelease.objects.get(version="1.1.0")
1992
- self.assertEqual(new_release.revision, "")
1993
- self.assertEqual(
1994
- new_release.release_on,
1995
- datetime(2024, 2, 2, 15, 45, tzinfo=datetime_timezone.utc),
1985
+ self.assertFalse(
1986
+ PackageRelease.objects.filter(version="1.1.0").exists()
1987
+ )
1988
+ dump.assert_not_called()
1989
+ self.assertIn(
1990
+ (
1991
+ "Manual creation required for 1 release: 1.1.0",
1992
+ messages.WARNING,
1993
+ ),
1994
+ self.messages,
1996
1995
  )
1997
- dump.assert_called_once()
1998
1996
 
1999
1997
  @mock.patch("core.admin.PackageRelease.dump_fixture")
2000
1998
  @mock.patch("core.admin.requests.get")
@@ -2218,13 +2216,97 @@ class TodoDoneTests(TestCase):
2218
2216
  User.objects.create_superuser("admin", "admin@example.com", "pw")
2219
2217
  self.client.force_login(User.objects.get(username="admin"))
2220
2218
 
2221
- def test_mark_done_sets_timestamp(self):
2219
+ @mock.patch("core.models.revision_utils.get_revision", return_value="rev123")
2220
+ def test_mark_done_sets_timestamp(self, _get_revision):
2222
2221
  todo = Todo.objects.create(request="Task", is_seed_data=True)
2223
2222
  resp = self.client.post(reverse("todo-done", args=[todo.pk]))
2224
2223
  self.assertRedirects(resp, reverse("admin:index"))
2225
2224
  todo.refresh_from_db()
2226
2225
  self.assertIsNotNone(todo.done_on)
2227
2226
  self.assertFalse(todo.is_deleted)
2227
+ self.assertIsNone(todo.done_node)
2228
+ version_path = Path(settings.BASE_DIR) / "VERSION"
2229
+ expected_version = ""
2230
+ if version_path.exists():
2231
+ expected_version = version_path.read_text(encoding="utf-8").strip()
2232
+ self.assertEqual(todo.done_version, expected_version)
2233
+ self.assertEqual(todo.done_revision, "rev123")
2234
+ self.assertEqual(todo.done_username, "admin")
2235
+
2236
+ def test_mark_done_updates_seed_fixture(self):
2237
+ todo = Todo.objects.create(request="Task", is_seed_data=True)
2238
+ with tempfile.TemporaryDirectory() as tmp:
2239
+ base = Path(tmp)
2240
+ fixture_dir = base / "core" / "fixtures"
2241
+ fixture_dir.mkdir(parents=True)
2242
+ fixture_path = fixture_dir / "todo__task.json"
2243
+ fixture_path.write_text(
2244
+ json.dumps(
2245
+ [
2246
+ {
2247
+ "model": "core.todo",
2248
+ "fields": {
2249
+ "request": "Task",
2250
+ "url": "",
2251
+ "request_details": "",
2252
+ },
2253
+ }
2254
+ ],
2255
+ indent=2,
2256
+ )
2257
+ + "\n",
2258
+ encoding="utf-8",
2259
+ )
2260
+
2261
+ with override_settings(BASE_DIR=base):
2262
+ with mock.patch(
2263
+ "core.models.revision_utils.get_revision", return_value="rev456"
2264
+ ):
2265
+ resp = self.client.post(reverse("todo-done", args=[todo.pk]))
2266
+
2267
+ self.assertRedirects(resp, reverse("admin:index"))
2268
+ data = json.loads(fixture_path.read_text(encoding="utf-8"))
2269
+ self.assertEqual(len(data), 1)
2270
+ fields = data[0]["fields"]
2271
+ self.assertIn("done_on", fields)
2272
+ self.assertTrue(fields["done_on"])
2273
+ self.assertFalse(fields.get("is_deleted", False))
2274
+ self.assertIn("done_version", fields)
2275
+ self.assertEqual(fields.get("done_revision"), "rev456")
2276
+ self.assertEqual(fields.get("done_username"), "admin")
2277
+
2278
+ def test_soft_delete_updates_seed_fixture(self):
2279
+ todo = Todo.objects.create(request="Task", is_seed_data=True)
2280
+ with tempfile.TemporaryDirectory() as tmp:
2281
+ base = Path(tmp)
2282
+ fixture_dir = base / "core" / "fixtures"
2283
+ fixture_dir.mkdir(parents=True)
2284
+ fixture_path = fixture_dir / "todo__task.json"
2285
+ fixture_path.write_text(
2286
+ json.dumps(
2287
+ [
2288
+ {
2289
+ "model": "core.todo",
2290
+ "fields": {
2291
+ "request": "Task",
2292
+ "url": "",
2293
+ "request_details": "",
2294
+ },
2295
+ }
2296
+ ],
2297
+ indent=2,
2298
+ )
2299
+ + "\n",
2300
+ encoding="utf-8",
2301
+ )
2302
+
2303
+ with override_settings(BASE_DIR=base):
2304
+ todo.delete()
2305
+
2306
+ data = json.loads(fixture_path.read_text(encoding="utf-8"))
2307
+ self.assertEqual(len(data), 1)
2308
+ fields = data[0]["fields"]
2309
+ self.assertTrue(fields.get("is_deleted"))
2228
2310
 
2229
2311
  def test_mark_done_missing_task_refreshes(self):
2230
2312
  todo = Todo.objects.create(request="Task", is_seed_data=True)
@@ -2330,6 +2412,61 @@ class TodoDoneTests(TestCase):
2330
2412
  self.assertTrue(todo.is_seed_data)
2331
2413
 
2332
2414
 
2415
+ class TodoDeleteTests(TestCase):
2416
+ def setUp(self):
2417
+ self.client = Client()
2418
+ User.objects.create_superuser("admin", "admin@example.com", "pw")
2419
+ self.client.force_login(User.objects.get(username="admin"))
2420
+
2421
+ def test_delete_marks_task_deleted(self):
2422
+ todo = Todo.objects.create(request="Task", is_seed_data=True)
2423
+ resp = self.client.post(reverse("todo-delete", args=[todo.pk]))
2424
+ self.assertRedirects(resp, reverse("admin:index"))
2425
+ todo.refresh_from_db()
2426
+ self.assertTrue(todo.is_deleted)
2427
+ self.assertIsNone(todo.done_on)
2428
+
2429
+ def test_delete_updates_seed_fixture(self):
2430
+ todo = Todo.objects.create(request="Task", is_seed_data=True)
2431
+ with tempfile.TemporaryDirectory() as tmp:
2432
+ base = Path(tmp)
2433
+ fixture_dir = base / "core" / "fixtures"
2434
+ fixture_dir.mkdir(parents=True)
2435
+ fixture_path = fixture_dir / "todo__task.json"
2436
+ fixture_path.write_text(
2437
+ json.dumps(
2438
+ [
2439
+ {
2440
+ "model": "core.todo",
2441
+ "fields": {
2442
+ "request": "Task",
2443
+ "url": "",
2444
+ "request_details": "",
2445
+ },
2446
+ }
2447
+ ],
2448
+ indent=2,
2449
+ )
2450
+ + "\n",
2451
+ encoding="utf-8",
2452
+ )
2453
+
2454
+ with override_settings(BASE_DIR=base):
2455
+ resp = self.client.post(reverse("todo-delete", args=[todo.pk]))
2456
+ self.assertRedirects(resp, reverse("admin:index"))
2457
+ data = json.loads(fixture_path.read_text(encoding="utf-8"))
2458
+ self.assertEqual(len(data), 1)
2459
+ fields = data[0]["fields"]
2460
+ self.assertTrue(fields.get("is_deleted"))
2461
+
2462
+ def test_delete_missing_task_redirects(self):
2463
+ todo = Todo.objects.create(request="Task")
2464
+ todo.is_deleted = True
2465
+ todo.save(update_fields=["is_deleted"])
2466
+ resp = self.client.post(reverse("todo-delete", args=[todo.pk]))
2467
+ self.assertRedirects(resp, reverse("admin:index"))
2468
+
2469
+
2333
2470
  class TodoFocusViewTests(TestCase):
2334
2471
  def setUp(self):
2335
2472
  self.client = Client()
@@ -2347,6 +2484,7 @@ class TodoFocusViewTests(TestCase):
2347
2484
  self.assertEqual(resp["X-Frame-Options"], "SAMEORIGIN")
2348
2485
  self.assertContains(resp, f'src="{todo.url}"')
2349
2486
  self.assertContains(resp, "Done")
2487
+ self.assertContains(resp, "Delete")
2350
2488
  self.assertContains(resp, "Back")
2351
2489
  self.assertContains(resp, "Take Snapshot")
2352
2490
  snapshot_url = reverse("todo-snapshot", args=[todo.pk])
core/views.py CHANGED
@@ -19,7 +19,6 @@ from django.shortcuts import get_object_or_404, redirect, render, resolve_url
19
19
  from django.template.response import TemplateResponse
20
20
  from django.utils import timezone
21
21
  from django.utils.html import strip_tags
22
- from django.utils.text import slugify
23
22
  from django.utils.translation import gettext as _
24
23
  from django.urls import NoReverseMatch, reverse
25
24
  from django.views.decorators.csrf import csrf_exempt
@@ -366,6 +365,7 @@ def request_temp_password(request):
366
365
  )
367
366
 
368
367
 
368
+ @staff_member_required
369
369
  @require_GET
370
370
  def version_info(request):
371
371
  """Return the running application version and Git revision."""
@@ -496,6 +496,16 @@ def _sync_with_origin_main(log_path: Path) -> None:
496
496
  if stderr:
497
497
  _append_log(log_path, "git errors:\n" + stderr)
498
498
 
499
+ status = subprocess.run(
500
+ ["git", "status"], capture_output=True, text=True, check=False
501
+ )
502
+ status_output = (status.stdout or "").strip()
503
+ status_errors = (status.stderr or "").strip()
504
+ if status_output:
505
+ _append_log(log_path, "git status:\n" + status_output)
506
+ if status_errors:
507
+ _append_log(log_path, "git status errors:\n" + status_errors)
508
+
499
509
  branch = _current_branch() or "(detached HEAD)"
500
510
  instructions = [
501
511
  "Manual intervention required to finish syncing with origin/main.",
@@ -693,29 +703,6 @@ def _next_patch_version(version: str) -> str:
693
703
  return f"{parsed.major}.{parsed.minor}.{parsed.micro + 1}"
694
704
 
695
705
 
696
- def _write_todo_fixture(todo: Todo) -> Path:
697
- safe_request = todo.request.replace(".", " ")
698
- slug = slugify(safe_request).replace("-", "_")
699
- if not slug:
700
- slug = "todo"
701
- path = TODO_FIXTURE_DIR / f"todos__{slug}.json"
702
- path.parent.mkdir(parents=True, exist_ok=True)
703
- data = [
704
- {
705
- "model": "core.todo",
706
- "fields": {
707
- "request": todo.request,
708
- "url": todo.url,
709
- "request_details": todo.request_details,
710
- "generated_for_version": todo.generated_for_version,
711
- "generated_for_revision": todo.generated_for_revision,
712
- },
713
- }
714
- ]
715
- path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
716
- return path
717
-
718
-
719
706
  def _should_use_python_changelog(exc: OSError) -> bool:
720
707
  winerror = getattr(exc, "winerror", None)
721
708
  if winerror in {193}:
@@ -736,46 +723,6 @@ def _generate_changelog_with_python(log_path: Path) -> None:
736
723
  _append_log(log_path, "Regenerated CHANGELOG.rst using Python fallback")
737
724
 
738
725
 
739
- def _ensure_release_todo(
740
- release, *, previous_version: str | None = None
741
- ) -> tuple[Todo, Path]:
742
- previous_version = (previous_version or "").strip()
743
- target_version = _next_patch_version(release.version)
744
- if previous_version:
745
- try:
746
- from packaging.version import InvalidVersion, Version
747
-
748
- parsed_previous = Version(previous_version)
749
- parsed_target = Version(target_version)
750
- except InvalidVersion:
751
- pass
752
- else:
753
- if parsed_target <= parsed_previous:
754
- target_version = _next_patch_version(previous_version)
755
- request = f"Create release {release.package.name} {target_version}"
756
- try:
757
- url = reverse("admin:core_packagerelease_changelist")
758
- except NoReverseMatch:
759
- url = ""
760
- todo, _ = Todo.all_objects.update_or_create(
761
- request__iexact=request,
762
- defaults={
763
- "request": request,
764
- "url": url,
765
- "request_details": "",
766
- "generated_for_version": release.version or "",
767
- "generated_for_revision": release.revision or "",
768
- "is_seed_data": True,
769
- "is_deleted": False,
770
- "is_user_data": False,
771
- "done_on": None,
772
- "on_done_condition": "",
773
- },
774
- )
775
- fixture_path = _write_todo_fixture(todo)
776
- return todo, fixture_path
777
-
778
-
779
726
  def _todo_blocks_publish(todo: Todo, release: PackageRelease) -> bool:
780
727
  """Return ``True`` when ``todo`` should block the release workflow."""
781
728
 
@@ -1226,36 +1173,6 @@ def _step_changelog_docs(release, ctx, log_path: Path) -> None:
1226
1173
  _append_log(log_path, "CHANGELOG and documentation review recorded")
1227
1174
 
1228
1175
 
1229
- def _record_release_todo(
1230
- release, ctx, log_path: Path, *, previous_version: str | None = None
1231
- ) -> None:
1232
- previous_version = previous_version or ctx.pop(
1233
- "release_todo_previous_version",
1234
- getattr(release, "_repo_version_before_sync", ""),
1235
- )
1236
- todo, fixture_path = _ensure_release_todo(
1237
- release, previous_version=previous_version
1238
- )
1239
- fixture_display = _format_path(fixture_path)
1240
- _append_log(log_path, f"Added TODO: {todo.request}")
1241
- _append_log(log_path, f"Wrote TODO fixture {fixture_display}")
1242
- subprocess.run(["git", "add", str(fixture_path)], check=True)
1243
- _append_log(log_path, f"Staged TODO fixture {fixture_display}")
1244
- fixture_diff = subprocess.run(
1245
- ["git", "diff", "--cached", "--quiet", "--", str(fixture_path)],
1246
- check=False,
1247
- )
1248
- if fixture_diff.returncode != 0:
1249
- commit_message = f"chore: add release TODO for {release.package.name}"
1250
- subprocess.run(["git", "commit", "-m", commit_message], check=True)
1251
- _append_log(log_path, f"Committed TODO fixture {fixture_display}")
1252
- else:
1253
- _append_log(
1254
- log_path,
1255
- f"No changes detected for TODO fixture {fixture_display}; skipping commit",
1256
- )
1257
-
1258
-
1259
1176
  def _step_pre_release_actions(release, ctx, log_path: Path) -> None:
1260
1177
  _append_log(log_path, "Execute pre-release actions")
1261
1178
  if ctx.get("dry_run"):
@@ -1329,7 +1246,6 @@ def _step_pre_release_actions(release, ctx, log_path: Path) -> None:
1329
1246
  for path in staged_release_fixtures:
1330
1247
  subprocess.run(["git", "reset", "HEAD", str(path)], check=False)
1331
1248
  _append_log(log_path, f"Unstaged release fixture {_format_path(path)}")
1332
- ctx["release_todo_previous_version"] = repo_version_before_sync
1333
1249
  _append_log(log_path, "Pre-release actions complete")
1334
1250
 
1335
1251
 
@@ -1383,7 +1299,6 @@ def _step_promote_build(release, ctx, log_path: Path) -> None:
1383
1299
  _push_release_changes(log_path)
1384
1300
  PackageRelease.dump_fixture()
1385
1301
  _append_log(log_path, "Updated release fixtures")
1386
- _record_release_todo(release, ctx, log_path)
1387
1302
  except Exception:
1388
1303
  _clean_repo()
1389
1304
  raise
@@ -2456,6 +2371,7 @@ def todo_focus(request, pk: int):
2456
2371
  "focus_auth": focus_auth,
2457
2372
  "next_url": _get_return_url(request),
2458
2373
  "done_url": reverse("todo-done", args=[todo.pk]),
2374
+ "delete_url": reverse("todo-delete", args=[todo.pk]),
2459
2375
  "snapshot_url": reverse("todo-snapshot", args=[todo.pk]),
2460
2376
  }
2461
2377
  return render(request, "core/todo_focus.html", context)
@@ -2474,7 +2390,29 @@ def todo_done(request, pk: int):
2474
2390
  messages.error(request, _format_condition_failure(todo, result))
2475
2391
  return redirect(redirect_to)
2476
2392
  todo.done_on = timezone.now()
2477
- todo.save(update_fields=["done_on"])
2393
+ todo.populate_done_metadata(request.user)
2394
+ todo.save(
2395
+ update_fields=[
2396
+ "done_on",
2397
+ "done_node",
2398
+ "done_version",
2399
+ "done_revision",
2400
+ "done_username",
2401
+ ]
2402
+ )
2403
+ return redirect(redirect_to)
2404
+
2405
+
2406
+ @staff_member_required
2407
+ @require_POST
2408
+ def todo_delete(request, pk: int):
2409
+ redirect_to = reverse("admin:index")
2410
+ try:
2411
+ todo = Todo.objects.get(pk=pk, is_deleted=False)
2412
+ except Todo.DoesNotExist:
2413
+ return redirect(redirect_to)
2414
+ todo.is_deleted = True
2415
+ todo.save(update_fields=["is_deleted"])
2478
2416
  return redirect(redirect_to)
2479
2417
 
2480
2418