arthexis 0.1.21__py3-none-any.whl → 0.1.23__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.21.dist-info → arthexis-0.1.23.dist-info}/METADATA +9 -8
- {arthexis-0.1.21.dist-info → arthexis-0.1.23.dist-info}/RECORD +33 -33
- config/settings.py +4 -0
- config/urls.py +5 -0
- core/admin.py +224 -32
- core/environment.py +2 -239
- core/models.py +903 -65
- core/release.py +0 -5
- core/system.py +76 -0
- core/tests.py +181 -9
- core/user_data.py +42 -2
- core/views.py +68 -27
- nodes/admin.py +211 -60
- nodes/apps.py +11 -0
- nodes/models.py +35 -7
- nodes/tests.py +288 -1
- nodes/views.py +101 -48
- ocpp/admin.py +32 -2
- ocpp/consumers.py +1 -0
- ocpp/models.py +52 -3
- ocpp/tasks.py +99 -1
- ocpp/tests.py +350 -2
- ocpp/views.py +300 -6
- pages/admin.py +112 -15
- pages/apps.py +32 -0
- pages/forms.py +31 -8
- pages/models.py +42 -2
- pages/tests.py +386 -28
- pages/urls.py +10 -0
- pages/views.py +347 -18
- {arthexis-0.1.21.dist-info → arthexis-0.1.23.dist-info}/WHEEL +0 -0
- {arthexis-0.1.21.dist-info → arthexis-0.1.23.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.21.dist-info → arthexis-0.1.23.dist-info}/top_level.txt +0 -0
core/release.py
CHANGED
|
@@ -520,11 +520,6 @@ def build(
|
|
|
520
520
|
if not version_path.exists():
|
|
521
521
|
raise ReleaseError("VERSION file not found")
|
|
522
522
|
version = version_path.read_text().strip()
|
|
523
|
-
if bump:
|
|
524
|
-
major, minor, patch = map(int, version.split("."))
|
|
525
|
-
patch += 1
|
|
526
|
-
version = f"{major}.{minor}.{patch}"
|
|
527
|
-
version_path.write_text(version + "\n")
|
|
528
523
|
else:
|
|
529
524
|
# Ensure the VERSION file reflects the provided release version
|
|
530
525
|
if version_path.parent != Path("."):
|
core/system.py
CHANGED
|
@@ -101,6 +101,72 @@ def _open_changelog_entries() -> list[dict[str, str]]:
|
|
|
101
101
|
return entries
|
|
102
102
|
|
|
103
103
|
|
|
104
|
+
def _latest_release_changelog() -> dict[str, object]:
|
|
105
|
+
"""Return the most recent tagged release entries for display."""
|
|
106
|
+
|
|
107
|
+
changelog_path = Path("CHANGELOG.rst")
|
|
108
|
+
try:
|
|
109
|
+
text = changelog_path.read_text(encoding="utf-8")
|
|
110
|
+
except (FileNotFoundError, OSError):
|
|
111
|
+
return {"title": "", "entries": []}
|
|
112
|
+
|
|
113
|
+
lines = text.splitlines()
|
|
114
|
+
state = "before"
|
|
115
|
+
release_title = ""
|
|
116
|
+
entries: list[dict[str, str]] = []
|
|
117
|
+
|
|
118
|
+
for raw_line in lines:
|
|
119
|
+
stripped = raw_line.strip()
|
|
120
|
+
|
|
121
|
+
if state == "before":
|
|
122
|
+
if stripped == "Unreleased":
|
|
123
|
+
state = "unreleased-heading"
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
if state == "unreleased-heading":
|
|
127
|
+
if set(stripped) == {"-"}:
|
|
128
|
+
state = "unreleased-body"
|
|
129
|
+
else:
|
|
130
|
+
state = "unreleased-body"
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
if state == "unreleased-body":
|
|
134
|
+
if not stripped:
|
|
135
|
+
state = "after-unreleased"
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
if state == "after-unreleased":
|
|
139
|
+
if not stripped:
|
|
140
|
+
continue
|
|
141
|
+
release_title = stripped
|
|
142
|
+
state = "release-heading"
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
if state == "release-heading":
|
|
146
|
+
if set(stripped) == {"-"}:
|
|
147
|
+
state = "release-body"
|
|
148
|
+
else:
|
|
149
|
+
state = "release-body"
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
if state == "release-body":
|
|
153
|
+
if not stripped:
|
|
154
|
+
if entries:
|
|
155
|
+
break
|
|
156
|
+
continue
|
|
157
|
+
if not stripped.startswith("- "):
|
|
158
|
+
break
|
|
159
|
+
trimmed = stripped[2:].strip()
|
|
160
|
+
if not trimmed:
|
|
161
|
+
continue
|
|
162
|
+
parts = trimmed.split(" ", 1)
|
|
163
|
+
sha = parts[0]
|
|
164
|
+
message = parts[1] if len(parts) > 1 else ""
|
|
165
|
+
entries.append({"sha": sha, "message": message})
|
|
166
|
+
|
|
167
|
+
return {"title": release_title, "entries": entries}
|
|
168
|
+
|
|
169
|
+
|
|
104
170
|
def _exclude_changelog_entries(shas: Iterable[str]) -> int:
|
|
105
171
|
"""Remove entries matching ``shas`` from the changelog.
|
|
106
172
|
|
|
@@ -1073,6 +1139,7 @@ def _system_changelog_report_view(request):
|
|
|
1073
1139
|
{
|
|
1074
1140
|
"title": _("Changelog Report"),
|
|
1075
1141
|
"open_changelog_entries": _open_changelog_entries(),
|
|
1142
|
+
"latest_release_changelog": _latest_release_changelog(),
|
|
1076
1143
|
}
|
|
1077
1144
|
)
|
|
1078
1145
|
return TemplateResponse(request, "admin/system_changelog_report.html", context)
|
|
@@ -1119,6 +1186,14 @@ class PendingTodoForm(forms.ModelForm):
|
|
|
1119
1186
|
for name in ["request_details", "on_done_condition"]:
|
|
1120
1187
|
self.fields[name].widget.attrs.setdefault("class", "vLargeTextField")
|
|
1121
1188
|
|
|
1189
|
+
mark_done_widget = self.fields["mark_done"].widget
|
|
1190
|
+
existing_classes = mark_done_widget.attrs.get("class", "").split()
|
|
1191
|
+
if "approve-checkbox" not in existing_classes:
|
|
1192
|
+
existing_classes.append("approve-checkbox")
|
|
1193
|
+
mark_done_widget.attrs["class"] = " ".join(
|
|
1194
|
+
class_name for class_name in existing_classes if class_name
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1122
1197
|
|
|
1123
1198
|
PendingTodoFormSet = modelformset_factory(Todo, form=PendingTodoForm, extra=0)
|
|
1124
1199
|
|
|
@@ -1144,6 +1219,7 @@ def _system_pending_todos_report_view(request):
|
|
|
1144
1219
|
has_changes = form.has_changed()
|
|
1145
1220
|
if mark_done and todo.done_on is None:
|
|
1146
1221
|
todo.done_on = timezone.now()
|
|
1222
|
+
todo.populate_done_metadata(request.user)
|
|
1147
1223
|
approved_count += 1
|
|
1148
1224
|
has_changes = True
|
|
1149
1225
|
if has_changes:
|
core/tests.py
CHANGED
|
@@ -1969,7 +1969,7 @@ class PackageReleaseAdminActionTests(TestCase):
|
|
|
1969
1969
|
|
|
1970
1970
|
@mock.patch("core.admin.PackageRelease.dump_fixture")
|
|
1971
1971
|
@mock.patch("core.admin.requests.get")
|
|
1972
|
-
def
|
|
1972
|
+
def test_refresh_from_pypi_reports_missing_releases(self, mock_get, dump):
|
|
1973
1973
|
mock_get.return_value.raise_for_status.return_value = None
|
|
1974
1974
|
mock_get.return_value.json.return_value = {
|
|
1975
1975
|
"releases": {
|
|
@@ -1982,13 +1982,17 @@ class PackageReleaseAdminActionTests(TestCase):
|
|
|
1982
1982
|
}
|
|
1983
1983
|
}
|
|
1984
1984
|
self.admin.refresh_from_pypi(self.request, PackageRelease.objects.none())
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
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,
|
|
1990
1995
|
)
|
|
1991
|
-
dump.assert_called_once()
|
|
1992
1996
|
|
|
1993
1997
|
@mock.patch("core.admin.PackageRelease.dump_fixture")
|
|
1994
1998
|
@mock.patch("core.admin.requests.get")
|
|
@@ -2134,6 +2138,25 @@ class PackageReleaseCurrentTests(TestCase):
|
|
|
2134
2138
|
self.package.save()
|
|
2135
2139
|
self.assertFalse(self.release.is_current)
|
|
2136
2140
|
|
|
2141
|
+
def test_is_current_false_when_version_has_plus(self):
|
|
2142
|
+
self.version_path.write_text("1.0.0+")
|
|
2143
|
+
self.assertFalse(self.release.is_current)
|
|
2144
|
+
|
|
2145
|
+
|
|
2146
|
+
class PackageReleaseRevisionTests(TestCase):
|
|
2147
|
+
def setUp(self):
|
|
2148
|
+
self.package = Package.objects.get(name="arthexis")
|
|
2149
|
+
self.release = PackageRelease.objects.create(
|
|
2150
|
+
package=self.package,
|
|
2151
|
+
version="1.0.0",
|
|
2152
|
+
revision="abcdef123456",
|
|
2153
|
+
)
|
|
2154
|
+
|
|
2155
|
+
def test_matches_revision_ignores_plus_suffix(self):
|
|
2156
|
+
self.assertTrue(
|
|
2157
|
+
PackageRelease.matches_revision("1.0.0+", "abcdef123456")
|
|
2158
|
+
)
|
|
2159
|
+
|
|
2137
2160
|
def test_is_current_false_when_version_differs(self):
|
|
2138
2161
|
self.release.version = "2.0.0"
|
|
2139
2162
|
self.release.save()
|
|
@@ -2184,13 +2207,22 @@ class PackageAdminPrepareNextReleaseTests(TestCase):
|
|
|
2184
2207
|
|
|
2185
2208
|
def test_prepare_next_release_active_creates_release(self):
|
|
2186
2209
|
PackageRelease.all_objects.filter(package=self.package).delete()
|
|
2187
|
-
request = self.factory.
|
|
2210
|
+
request = self.factory.post("/admin/core/package/prepare-next-release/")
|
|
2188
2211
|
response = self.admin.prepare_next_release_active(request)
|
|
2189
2212
|
self.assertEqual(response.status_code, 302)
|
|
2190
2213
|
self.assertEqual(
|
|
2191
2214
|
PackageRelease.all_objects.filter(package=self.package).count(), 1
|
|
2192
2215
|
)
|
|
2193
2216
|
|
|
2217
|
+
def test_prepare_next_release_active_get_creates_release(self):
|
|
2218
|
+
PackageRelease.all_objects.filter(package=self.package).delete()
|
|
2219
|
+
request = self.factory.get("/admin/core/package/prepare-next-release/")
|
|
2220
|
+
response = self.admin.prepare_next_release_active(request)
|
|
2221
|
+
self.assertEqual(response.status_code, 302)
|
|
2222
|
+
self.assertTrue(
|
|
2223
|
+
PackageRelease.all_objects.filter(package=self.package).exists()
|
|
2224
|
+
)
|
|
2225
|
+
|
|
2194
2226
|
|
|
2195
2227
|
class PackageAdminChangeViewTests(TestCase):
|
|
2196
2228
|
def setUp(self):
|
|
@@ -2212,13 +2244,97 @@ class TodoDoneTests(TestCase):
|
|
|
2212
2244
|
User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
2213
2245
|
self.client.force_login(User.objects.get(username="admin"))
|
|
2214
2246
|
|
|
2215
|
-
|
|
2247
|
+
@mock.patch("core.models.revision_utils.get_revision", return_value="rev123")
|
|
2248
|
+
def test_mark_done_sets_timestamp(self, _get_revision):
|
|
2216
2249
|
todo = Todo.objects.create(request="Task", is_seed_data=True)
|
|
2217
2250
|
resp = self.client.post(reverse("todo-done", args=[todo.pk]))
|
|
2218
2251
|
self.assertRedirects(resp, reverse("admin:index"))
|
|
2219
2252
|
todo.refresh_from_db()
|
|
2220
2253
|
self.assertIsNotNone(todo.done_on)
|
|
2221
2254
|
self.assertFalse(todo.is_deleted)
|
|
2255
|
+
self.assertIsNone(todo.done_node)
|
|
2256
|
+
version_path = Path(settings.BASE_DIR) / "VERSION"
|
|
2257
|
+
expected_version = ""
|
|
2258
|
+
if version_path.exists():
|
|
2259
|
+
expected_version = version_path.read_text(encoding="utf-8").strip()
|
|
2260
|
+
self.assertEqual(todo.done_version, expected_version)
|
|
2261
|
+
self.assertEqual(todo.done_revision, "rev123")
|
|
2262
|
+
self.assertEqual(todo.done_username, "admin")
|
|
2263
|
+
|
|
2264
|
+
def test_mark_done_updates_seed_fixture(self):
|
|
2265
|
+
todo = Todo.objects.create(request="Task", is_seed_data=True)
|
|
2266
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2267
|
+
base = Path(tmp)
|
|
2268
|
+
fixture_dir = base / "core" / "fixtures"
|
|
2269
|
+
fixture_dir.mkdir(parents=True)
|
|
2270
|
+
fixture_path = fixture_dir / "todo__task.json"
|
|
2271
|
+
fixture_path.write_text(
|
|
2272
|
+
json.dumps(
|
|
2273
|
+
[
|
|
2274
|
+
{
|
|
2275
|
+
"model": "core.todo",
|
|
2276
|
+
"fields": {
|
|
2277
|
+
"request": "Task",
|
|
2278
|
+
"url": "",
|
|
2279
|
+
"request_details": "",
|
|
2280
|
+
},
|
|
2281
|
+
}
|
|
2282
|
+
],
|
|
2283
|
+
indent=2,
|
|
2284
|
+
)
|
|
2285
|
+
+ "\n",
|
|
2286
|
+
encoding="utf-8",
|
|
2287
|
+
)
|
|
2288
|
+
|
|
2289
|
+
with override_settings(BASE_DIR=base):
|
|
2290
|
+
with mock.patch(
|
|
2291
|
+
"core.models.revision_utils.get_revision", return_value="rev456"
|
|
2292
|
+
):
|
|
2293
|
+
resp = self.client.post(reverse("todo-done", args=[todo.pk]))
|
|
2294
|
+
|
|
2295
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
2296
|
+
data = json.loads(fixture_path.read_text(encoding="utf-8"))
|
|
2297
|
+
self.assertEqual(len(data), 1)
|
|
2298
|
+
fields = data[0]["fields"]
|
|
2299
|
+
self.assertIn("done_on", fields)
|
|
2300
|
+
self.assertTrue(fields["done_on"])
|
|
2301
|
+
self.assertFalse(fields.get("is_deleted", False))
|
|
2302
|
+
self.assertIn("done_version", fields)
|
|
2303
|
+
self.assertEqual(fields.get("done_revision"), "rev456")
|
|
2304
|
+
self.assertEqual(fields.get("done_username"), "admin")
|
|
2305
|
+
|
|
2306
|
+
def test_soft_delete_updates_seed_fixture(self):
|
|
2307
|
+
todo = Todo.objects.create(request="Task", is_seed_data=True)
|
|
2308
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2309
|
+
base = Path(tmp)
|
|
2310
|
+
fixture_dir = base / "core" / "fixtures"
|
|
2311
|
+
fixture_dir.mkdir(parents=True)
|
|
2312
|
+
fixture_path = fixture_dir / "todo__task.json"
|
|
2313
|
+
fixture_path.write_text(
|
|
2314
|
+
json.dumps(
|
|
2315
|
+
[
|
|
2316
|
+
{
|
|
2317
|
+
"model": "core.todo",
|
|
2318
|
+
"fields": {
|
|
2319
|
+
"request": "Task",
|
|
2320
|
+
"url": "",
|
|
2321
|
+
"request_details": "",
|
|
2322
|
+
},
|
|
2323
|
+
}
|
|
2324
|
+
],
|
|
2325
|
+
indent=2,
|
|
2326
|
+
)
|
|
2327
|
+
+ "\n",
|
|
2328
|
+
encoding="utf-8",
|
|
2329
|
+
)
|
|
2330
|
+
|
|
2331
|
+
with override_settings(BASE_DIR=base):
|
|
2332
|
+
todo.delete()
|
|
2333
|
+
|
|
2334
|
+
data = json.loads(fixture_path.read_text(encoding="utf-8"))
|
|
2335
|
+
self.assertEqual(len(data), 1)
|
|
2336
|
+
fields = data[0]["fields"]
|
|
2337
|
+
self.assertTrue(fields.get("is_deleted"))
|
|
2222
2338
|
|
|
2223
2339
|
def test_mark_done_missing_task_refreshes(self):
|
|
2224
2340
|
todo = Todo.objects.create(request="Task", is_seed_data=True)
|
|
@@ -2324,6 +2440,61 @@ class TodoDoneTests(TestCase):
|
|
|
2324
2440
|
self.assertTrue(todo.is_seed_data)
|
|
2325
2441
|
|
|
2326
2442
|
|
|
2443
|
+
class TodoDeleteTests(TestCase):
|
|
2444
|
+
def setUp(self):
|
|
2445
|
+
self.client = Client()
|
|
2446
|
+
User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
2447
|
+
self.client.force_login(User.objects.get(username="admin"))
|
|
2448
|
+
|
|
2449
|
+
def test_delete_marks_task_deleted(self):
|
|
2450
|
+
todo = Todo.objects.create(request="Task", is_seed_data=True)
|
|
2451
|
+
resp = self.client.post(reverse("todo-delete", args=[todo.pk]))
|
|
2452
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
2453
|
+
todo.refresh_from_db()
|
|
2454
|
+
self.assertTrue(todo.is_deleted)
|
|
2455
|
+
self.assertIsNone(todo.done_on)
|
|
2456
|
+
|
|
2457
|
+
def test_delete_updates_seed_fixture(self):
|
|
2458
|
+
todo = Todo.objects.create(request="Task", is_seed_data=True)
|
|
2459
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
2460
|
+
base = Path(tmp)
|
|
2461
|
+
fixture_dir = base / "core" / "fixtures"
|
|
2462
|
+
fixture_dir.mkdir(parents=True)
|
|
2463
|
+
fixture_path = fixture_dir / "todo__task.json"
|
|
2464
|
+
fixture_path.write_text(
|
|
2465
|
+
json.dumps(
|
|
2466
|
+
[
|
|
2467
|
+
{
|
|
2468
|
+
"model": "core.todo",
|
|
2469
|
+
"fields": {
|
|
2470
|
+
"request": "Task",
|
|
2471
|
+
"url": "",
|
|
2472
|
+
"request_details": "",
|
|
2473
|
+
},
|
|
2474
|
+
}
|
|
2475
|
+
],
|
|
2476
|
+
indent=2,
|
|
2477
|
+
)
|
|
2478
|
+
+ "\n",
|
|
2479
|
+
encoding="utf-8",
|
|
2480
|
+
)
|
|
2481
|
+
|
|
2482
|
+
with override_settings(BASE_DIR=base):
|
|
2483
|
+
resp = self.client.post(reverse("todo-delete", args=[todo.pk]))
|
|
2484
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
2485
|
+
data = json.loads(fixture_path.read_text(encoding="utf-8"))
|
|
2486
|
+
self.assertEqual(len(data), 1)
|
|
2487
|
+
fields = data[0]["fields"]
|
|
2488
|
+
self.assertTrue(fields.get("is_deleted"))
|
|
2489
|
+
|
|
2490
|
+
def test_delete_missing_task_redirects(self):
|
|
2491
|
+
todo = Todo.objects.create(request="Task")
|
|
2492
|
+
todo.is_deleted = True
|
|
2493
|
+
todo.save(update_fields=["is_deleted"])
|
|
2494
|
+
resp = self.client.post(reverse("todo-delete", args=[todo.pk]))
|
|
2495
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
2496
|
+
|
|
2497
|
+
|
|
2327
2498
|
class TodoFocusViewTests(TestCase):
|
|
2328
2499
|
def setUp(self):
|
|
2329
2500
|
self.client = Client()
|
|
@@ -2341,6 +2512,7 @@ class TodoFocusViewTests(TestCase):
|
|
|
2341
2512
|
self.assertEqual(resp["X-Frame-Options"], "SAMEORIGIN")
|
|
2342
2513
|
self.assertContains(resp, f'src="{todo.url}"')
|
|
2343
2514
|
self.assertContains(resp, "Done")
|
|
2515
|
+
self.assertContains(resp, "Delete")
|
|
2344
2516
|
self.assertContains(resp, "Back")
|
|
2345
2517
|
self.assertContains(resp, "Take Snapshot")
|
|
2346
2518
|
snapshot_url = reverse("todo-snapshot", args=[todo.pk])
|
core/user_data.py
CHANGED
|
@@ -201,9 +201,49 @@ def dump_user_fixture(instance, user=None) -> None:
|
|
|
201
201
|
|
|
202
202
|
def delete_user_fixture(instance, user=None) -> None:
|
|
203
203
|
target_user = user or _resolve_fixture_user(instance)
|
|
204
|
-
|
|
204
|
+
filename = (
|
|
205
|
+
f"{instance._meta.app_label}_{instance._meta.model_name}_{instance.pk}.json"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def _remove_for_user(candidate) -> None:
|
|
209
|
+
if candidate is None:
|
|
210
|
+
return
|
|
211
|
+
base_path = Path(
|
|
212
|
+
getattr(candidate, "data_path", "") or Path(settings.BASE_DIR) / "data"
|
|
213
|
+
)
|
|
214
|
+
username = _username_for(candidate)
|
|
215
|
+
if not username:
|
|
216
|
+
return
|
|
217
|
+
user_dir = base_path / username
|
|
218
|
+
if user_dir.exists():
|
|
219
|
+
(user_dir / filename).unlink(missing_ok=True)
|
|
220
|
+
|
|
221
|
+
if target_user is not None:
|
|
222
|
+
_remove_for_user(target_user)
|
|
205
223
|
return
|
|
206
|
-
|
|
224
|
+
|
|
225
|
+
root = Path(settings.BASE_DIR) / "data"
|
|
226
|
+
if root.exists():
|
|
227
|
+
(root / filename).unlink(missing_ok=True)
|
|
228
|
+
for path in root.iterdir():
|
|
229
|
+
if path.is_dir():
|
|
230
|
+
(path / filename).unlink(missing_ok=True)
|
|
231
|
+
|
|
232
|
+
UserModel = get_user_model()
|
|
233
|
+
manager = getattr(UserModel, "all_objects", UserModel._default_manager)
|
|
234
|
+
for candidate in manager.all():
|
|
235
|
+
data_path = getattr(candidate, "data_path", "")
|
|
236
|
+
if not data_path:
|
|
237
|
+
continue
|
|
238
|
+
base_path = Path(data_path)
|
|
239
|
+
if not base_path.exists():
|
|
240
|
+
continue
|
|
241
|
+
username = _username_for(candidate)
|
|
242
|
+
if not username:
|
|
243
|
+
continue
|
|
244
|
+
user_dir = base_path / username
|
|
245
|
+
if user_dir.exists():
|
|
246
|
+
(user_dir / filename).unlink(missing_ok=True)
|
|
207
247
|
|
|
208
248
|
|
|
209
249
|
def _mark_fixture_user_data(path: Path) -> None:
|
core/views.py
CHANGED
|
@@ -365,6 +365,7 @@ def request_temp_password(request):
|
|
|
365
365
|
)
|
|
366
366
|
|
|
367
367
|
|
|
368
|
+
@staff_member_required
|
|
368
369
|
@require_GET
|
|
369
370
|
def version_info(request):
|
|
370
371
|
"""Return the running application version and Git revision."""
|
|
@@ -495,6 +496,16 @@ def _sync_with_origin_main(log_path: Path) -> None:
|
|
|
495
496
|
if stderr:
|
|
496
497
|
_append_log(log_path, "git errors:\n" + stderr)
|
|
497
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
|
+
|
|
498
509
|
branch = _current_branch() or "(detached HEAD)"
|
|
499
510
|
instructions = [
|
|
500
511
|
"Manual intervention required to finish syncing with origin/main.",
|
|
@@ -679,16 +690,17 @@ def _ensure_origin_main_unchanged(log_path: Path) -> None:
|
|
|
679
690
|
def _next_patch_version(version: str) -> str:
|
|
680
691
|
from packaging.version import InvalidVersion, Version
|
|
681
692
|
|
|
693
|
+
cleaned = version.rstrip("+")
|
|
682
694
|
try:
|
|
683
|
-
parsed = Version(
|
|
695
|
+
parsed = Version(cleaned)
|
|
684
696
|
except InvalidVersion:
|
|
685
|
-
parts =
|
|
697
|
+
parts = cleaned.split(".") if cleaned else []
|
|
686
698
|
for index in range(len(parts) - 1, -1, -1):
|
|
687
699
|
segment = parts[index]
|
|
688
700
|
if segment.isdigit():
|
|
689
701
|
parts[index] = str(int(segment) + 1)
|
|
690
702
|
return ".".join(parts)
|
|
691
|
-
return version
|
|
703
|
+
return cleaned or version
|
|
692
704
|
return f"{parsed.major}.{parsed.minor}.{parsed.micro + 1}"
|
|
693
705
|
|
|
694
706
|
|
|
@@ -757,7 +769,9 @@ def _sync_release_with_revision(release: PackageRelease) -> tuple[bool, str]:
|
|
|
757
769
|
version_path = Path("VERSION")
|
|
758
770
|
if version_path.exists():
|
|
759
771
|
try:
|
|
760
|
-
|
|
772
|
+
raw_version = version_path.read_text(encoding="utf-8").strip()
|
|
773
|
+
cleaned_version = raw_version.rstrip("+") or "0.0.0"
|
|
774
|
+
repo_version = Version(cleaned_version)
|
|
761
775
|
except InvalidVersion:
|
|
762
776
|
repo_version = None
|
|
763
777
|
|
|
@@ -924,7 +938,7 @@ def _refresh_changelog_once(ctx, log_path: Path) -> None:
|
|
|
924
938
|
ctx["changelog_refreshed"] = True
|
|
925
939
|
|
|
926
940
|
|
|
927
|
-
def _step_check_todos(release, ctx, log_path: Path) -> None:
|
|
941
|
+
def _step_check_todos(release, ctx, log_path: Path, *, user=None) -> None:
|
|
928
942
|
_refresh_changelog_once(ctx, log_path)
|
|
929
943
|
|
|
930
944
|
pending_qs = Todo.objects.filter(is_deleted=False, done_on__isnull=True)
|
|
@@ -964,7 +978,7 @@ def _step_check_todos(release, ctx, log_path: Path) -> None:
|
|
|
964
978
|
ctx["todos_ack"] = True
|
|
965
979
|
|
|
966
980
|
|
|
967
|
-
def _step_check_version(release, ctx, log_path: Path) -> None:
|
|
981
|
+
def _step_check_version(release, ctx, log_path: Path, *, user=None) -> None:
|
|
968
982
|
from . import release as release_utils
|
|
969
983
|
from packaging.version import InvalidVersion, Version
|
|
970
984
|
|
|
@@ -1099,10 +1113,12 @@ def _step_check_version(release, ctx, log_path: Path) -> None:
|
|
|
1099
1113
|
version_path = Path("VERSION")
|
|
1100
1114
|
if version_path.exists():
|
|
1101
1115
|
current = version_path.read_text(encoding="utf-8").strip()
|
|
1102
|
-
if current
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1116
|
+
if current:
|
|
1117
|
+
current_clean = current.rstrip("+") or "0.0.0"
|
|
1118
|
+
if Version(release.version) < Version(current_clean):
|
|
1119
|
+
raise Exception(
|
|
1120
|
+
f"Version {release.version} is older than existing {current}"
|
|
1121
|
+
)
|
|
1106
1122
|
|
|
1107
1123
|
_append_log(log_path, f"Checking if version {release.version} exists on PyPI")
|
|
1108
1124
|
if release_utils.network_available():
|
|
@@ -1152,17 +1168,17 @@ def _step_check_version(release, ctx, log_path: Path) -> None:
|
|
|
1152
1168
|
_append_log(log_path, "Network unavailable, skipping PyPI check")
|
|
1153
1169
|
|
|
1154
1170
|
|
|
1155
|
-
def _step_handle_migrations(release, ctx, log_path: Path) -> None:
|
|
1171
|
+
def _step_handle_migrations(release, ctx, log_path: Path, *, user=None) -> None:
|
|
1156
1172
|
_append_log(log_path, "Freeze, squash and approve migrations")
|
|
1157
1173
|
_append_log(log_path, "Migration review acknowledged (manual step)")
|
|
1158
1174
|
|
|
1159
1175
|
|
|
1160
|
-
def _step_changelog_docs(release, ctx, log_path: Path) -> None:
|
|
1176
|
+
def _step_changelog_docs(release, ctx, log_path: Path, *, user=None) -> None:
|
|
1161
1177
|
_append_log(log_path, "Compose CHANGELOG and documentation")
|
|
1162
1178
|
_append_log(log_path, "CHANGELOG and documentation review recorded")
|
|
1163
1179
|
|
|
1164
1180
|
|
|
1165
|
-
def _step_pre_release_actions(release, ctx, log_path: Path) -> None:
|
|
1181
|
+
def _step_pre_release_actions(release, ctx, log_path: Path, *, user=None) -> None:
|
|
1166
1182
|
_append_log(log_path, "Execute pre-release actions")
|
|
1167
1183
|
if ctx.get("dry_run"):
|
|
1168
1184
|
_append_log(log_path, "Dry run: skipping pre-release actions")
|
|
@@ -1238,12 +1254,12 @@ def _step_pre_release_actions(release, ctx, log_path: Path) -> None:
|
|
|
1238
1254
|
_append_log(log_path, "Pre-release actions complete")
|
|
1239
1255
|
|
|
1240
1256
|
|
|
1241
|
-
def _step_run_tests(release, ctx, log_path: Path) -> None:
|
|
1257
|
+
def _step_run_tests(release, ctx, log_path: Path, *, user=None) -> None:
|
|
1242
1258
|
_append_log(log_path, "Complete test suite with --all flag")
|
|
1243
1259
|
_append_log(log_path, "Test suite completion acknowledged")
|
|
1244
1260
|
|
|
1245
1261
|
|
|
1246
|
-
def _step_promote_build(release, ctx, log_path: Path) -> None:
|
|
1262
|
+
def _step_promote_build(release, ctx, log_path: Path, *, user=None) -> None:
|
|
1247
1263
|
from . import release as release_utils
|
|
1248
1264
|
|
|
1249
1265
|
_append_log(log_path, "Generating build files")
|
|
@@ -1255,7 +1271,7 @@ def _step_promote_build(release, ctx, log_path: Path) -> None:
|
|
|
1255
1271
|
release_utils.promote(
|
|
1256
1272
|
package=release.to_package(),
|
|
1257
1273
|
version=release.version,
|
|
1258
|
-
creds=release.to_credentials(),
|
|
1274
|
+
creds=release.to_credentials(user=user),
|
|
1259
1275
|
)
|
|
1260
1276
|
_append_log(
|
|
1261
1277
|
log_path,
|
|
@@ -1303,8 +1319,10 @@ def _step_promote_build(release, ctx, log_path: Path) -> None:
|
|
|
1303
1319
|
_append_log(new_log, "Build complete")
|
|
1304
1320
|
|
|
1305
1321
|
|
|
1306
|
-
def _step_release_manager_approval(
|
|
1307
|
-
|
|
1322
|
+
def _step_release_manager_approval(
|
|
1323
|
+
release, ctx, log_path: Path, *, user=None
|
|
1324
|
+
) -> None:
|
|
1325
|
+
if release.to_credentials(user=user) is None:
|
|
1308
1326
|
ctx.pop("release_approval", None)
|
|
1309
1327
|
if not ctx.get("approval_credentials_missing"):
|
|
1310
1328
|
_append_log(log_path, "Release manager publishing credentials missing")
|
|
@@ -1338,14 +1356,14 @@ def _step_release_manager_approval(release, ctx, log_path: Path) -> None:
|
|
|
1338
1356
|
raise ApprovalRequired()
|
|
1339
1357
|
|
|
1340
1358
|
|
|
1341
|
-
def _step_publish(release, ctx, log_path: Path) -> None:
|
|
1359
|
+
def _step_publish(release, ctx, log_path: Path, *, user=None) -> None:
|
|
1342
1360
|
from . import release as release_utils
|
|
1343
1361
|
|
|
1344
1362
|
if ctx.get("dry_run"):
|
|
1345
1363
|
test_repository_url = os.environ.get(
|
|
1346
1364
|
"PYPI_TEST_REPOSITORY_URL", "https://test.pypi.org/legacy/"
|
|
1347
1365
|
)
|
|
1348
|
-
test_creds = release.to_credentials()
|
|
1366
|
+
test_creds = release.to_credentials(user=user)
|
|
1349
1367
|
if not (test_creds and test_creds.has_auth()):
|
|
1350
1368
|
test_creds = release_utils.Credentials(
|
|
1351
1369
|
token=os.environ.get("PYPI_TEST_API_TOKEN"),
|
|
@@ -1385,7 +1403,7 @@ def _step_publish(release, ctx, log_path: Path) -> None:
|
|
|
1385
1403
|
release_utils.build(
|
|
1386
1404
|
package=package,
|
|
1387
1405
|
version=release.version,
|
|
1388
|
-
creds=release.to_credentials(),
|
|
1406
|
+
creds=release.to_credentials(user=user),
|
|
1389
1407
|
dist=True,
|
|
1390
1408
|
tests=False,
|
|
1391
1409
|
twine=False,
|
|
@@ -1414,13 +1432,13 @@ def _step_publish(release, ctx, log_path: Path) -> None:
|
|
|
1414
1432
|
release_utils.publish(
|
|
1415
1433
|
package=release.to_package(),
|
|
1416
1434
|
version=release.version,
|
|
1417
|
-
creds=target.credentials or release.to_credentials(),
|
|
1435
|
+
creds=target.credentials or release.to_credentials(user=user),
|
|
1418
1436
|
repositories=[target],
|
|
1419
1437
|
)
|
|
1420
1438
|
_append_log(log_path, "Dry run: skipped release metadata updates")
|
|
1421
1439
|
return
|
|
1422
1440
|
|
|
1423
|
-
targets = release.build_publish_targets()
|
|
1441
|
+
targets = release.build_publish_targets(user=user)
|
|
1424
1442
|
repo_labels = []
|
|
1425
1443
|
for target in targets:
|
|
1426
1444
|
label = target.name
|
|
@@ -1439,7 +1457,7 @@ def _step_publish(release, ctx, log_path: Path) -> None:
|
|
|
1439
1457
|
release_utils.publish(
|
|
1440
1458
|
package=release.to_package(),
|
|
1441
1459
|
version=release.version,
|
|
1442
|
-
creds=release.to_credentials(),
|
|
1460
|
+
creds=release.to_credentials(user=user),
|
|
1443
1461
|
repositories=targets,
|
|
1444
1462
|
)
|
|
1445
1463
|
except release_utils.PostPublishWarning as warning:
|
|
@@ -1808,7 +1826,7 @@ def release_progress(request, pk: int, action: str):
|
|
|
1808
1826
|
return redirect(request.path)
|
|
1809
1827
|
|
|
1810
1828
|
manager = release.release_manager or release.package.release_manager
|
|
1811
|
-
credentials_ready = bool(release.to_credentials())
|
|
1829
|
+
credentials_ready = bool(release.to_credentials(user=request.user))
|
|
1812
1830
|
if credentials_ready and ctx.get("approval_credentials_missing"):
|
|
1813
1831
|
ctx.pop("approval_credentials_missing", None)
|
|
1814
1832
|
|
|
@@ -1979,7 +1997,7 @@ def release_progress(request, pk: int, action: str):
|
|
|
1979
1997
|
if to_run == step_count:
|
|
1980
1998
|
name, func = steps[to_run]
|
|
1981
1999
|
try:
|
|
1982
|
-
func(release, ctx, log_path)
|
|
2000
|
+
func(release, ctx, log_path, user=request.user)
|
|
1983
2001
|
except PendingTodos:
|
|
1984
2002
|
pass
|
|
1985
2003
|
except ApprovalRequired:
|
|
@@ -2360,6 +2378,7 @@ def todo_focus(request, pk: int):
|
|
|
2360
2378
|
"focus_auth": focus_auth,
|
|
2361
2379
|
"next_url": _get_return_url(request),
|
|
2362
2380
|
"done_url": reverse("todo-done", args=[todo.pk]),
|
|
2381
|
+
"delete_url": reverse("todo-delete", args=[todo.pk]),
|
|
2363
2382
|
"snapshot_url": reverse("todo-snapshot", args=[todo.pk]),
|
|
2364
2383
|
}
|
|
2365
2384
|
return render(request, "core/todo_focus.html", context)
|
|
@@ -2378,7 +2397,29 @@ def todo_done(request, pk: int):
|
|
|
2378
2397
|
messages.error(request, _format_condition_failure(todo, result))
|
|
2379
2398
|
return redirect(redirect_to)
|
|
2380
2399
|
todo.done_on = timezone.now()
|
|
2381
|
-
todo.
|
|
2400
|
+
todo.populate_done_metadata(request.user)
|
|
2401
|
+
todo.save(
|
|
2402
|
+
update_fields=[
|
|
2403
|
+
"done_on",
|
|
2404
|
+
"done_node",
|
|
2405
|
+
"done_version",
|
|
2406
|
+
"done_revision",
|
|
2407
|
+
"done_username",
|
|
2408
|
+
]
|
|
2409
|
+
)
|
|
2410
|
+
return redirect(redirect_to)
|
|
2411
|
+
|
|
2412
|
+
|
|
2413
|
+
@staff_member_required
|
|
2414
|
+
@require_POST
|
|
2415
|
+
def todo_delete(request, pk: int):
|
|
2416
|
+
redirect_to = reverse("admin:index")
|
|
2417
|
+
try:
|
|
2418
|
+
todo = Todo.objects.get(pk=pk, is_deleted=False)
|
|
2419
|
+
except Todo.DoesNotExist:
|
|
2420
|
+
return redirect(redirect_to)
|
|
2421
|
+
todo.is_deleted = True
|
|
2422
|
+
todo.save(update_fields=["is_deleted"])
|
|
2382
2423
|
return redirect(redirect_to)
|
|
2383
2424
|
|
|
2384
2425
|
|