arthexis 0.1.9__py3-none-any.whl → 0.1.10__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.9.dist-info → arthexis-0.1.10.dist-info}/METADATA +63 -20
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/RECORD +39 -36
- config/settings.py +221 -23
- config/urls.py +6 -0
- core/admin.py +401 -35
- core/apps.py +3 -0
- core/auto_upgrade.py +57 -0
- core/backends.py +77 -3
- core/fields.py +93 -0
- core/models.py +212 -7
- core/reference_utils.py +97 -0
- core/sigil_builder.py +16 -3
- core/system.py +157 -143
- core/tasks.py +151 -8
- core/test_system_info.py +37 -1
- core/tests.py +288 -12
- core/user_data.py +103 -8
- core/views.py +257 -15
- nodes/admin.py +12 -4
- nodes/backends.py +109 -17
- nodes/models.py +205 -2
- nodes/tests.py +370 -1
- nodes/views.py +140 -7
- ocpp/admin.py +63 -3
- ocpp/consumers.py +252 -41
- ocpp/evcs.py +6 -3
- ocpp/models.py +49 -7
- ocpp/simulator.py +62 -5
- ocpp/store.py +30 -0
- ocpp/tests.py +384 -8
- ocpp/views.py +101 -76
- pages/context_processors.py +20 -0
- pages/forms.py +131 -0
- pages/tests.py +434 -13
- pages/urls.py +1 -0
- pages/views.py +334 -92
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/WHEEL +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/top_level.txt +0 -0
core/tests.py
CHANGED
|
@@ -17,9 +17,11 @@ import subprocess
|
|
|
17
17
|
from glob import glob
|
|
18
18
|
from datetime import timedelta
|
|
19
19
|
import tempfile
|
|
20
|
+
from urllib.parse import quote
|
|
20
21
|
|
|
21
22
|
from django.utils import timezone
|
|
22
23
|
from django.contrib.auth.models import Permission
|
|
24
|
+
from django.contrib.messages import get_messages
|
|
23
25
|
from .models import (
|
|
24
26
|
User,
|
|
25
27
|
UserPhoneNumber,
|
|
@@ -51,6 +53,7 @@ from django.core.management import call_command
|
|
|
51
53
|
from django.db import IntegrityError
|
|
52
54
|
from .backends import LocalhostAdminBackend
|
|
53
55
|
from core.views import _step_check_version, _step_promote_build, _step_publish
|
|
56
|
+
from core import views as core_views
|
|
54
57
|
from core import public_wifi
|
|
55
58
|
|
|
56
59
|
|
|
@@ -901,6 +904,139 @@ class ReleaseProcessTests(TestCase):
|
|
|
901
904
|
self.assertEqual(count_file.read_text(), "1")
|
|
902
905
|
|
|
903
906
|
|
|
907
|
+
class ReleaseProgressSyncTests(TestCase):
|
|
908
|
+
def setUp(self):
|
|
909
|
+
self.client = Client()
|
|
910
|
+
self.user = User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
911
|
+
self.client.force_login(self.user)
|
|
912
|
+
self.package = Package.objects.get(name="arthexis")
|
|
913
|
+
self.version_path = Path("VERSION")
|
|
914
|
+
self.original_version = self.version_path.read_text(encoding="utf-8")
|
|
915
|
+
self.version_path.write_text("1.2.3", encoding="utf-8")
|
|
916
|
+
|
|
917
|
+
def tearDown(self):
|
|
918
|
+
self.version_path.write_text(self.original_version, encoding="utf-8")
|
|
919
|
+
|
|
920
|
+
@mock.patch("core.views.PackageRelease.dump_fixture")
|
|
921
|
+
@mock.patch("core.views.revision.get_revision", return_value="abc123")
|
|
922
|
+
def test_unpublished_release_syncs_version_and_revision(
|
|
923
|
+
self, get_revision, dump_fixture
|
|
924
|
+
):
|
|
925
|
+
release = PackageRelease.objects.create(
|
|
926
|
+
package=self.package,
|
|
927
|
+
version="1.0.0",
|
|
928
|
+
)
|
|
929
|
+
release.revision = "oldrev"
|
|
930
|
+
release.save(update_fields=["revision"])
|
|
931
|
+
|
|
932
|
+
url = reverse("release-progress", args=[release.pk, "publish"])
|
|
933
|
+
response = self.client.get(url)
|
|
934
|
+
|
|
935
|
+
self.assertEqual(response.status_code, 200)
|
|
936
|
+
release.refresh_from_db()
|
|
937
|
+
self.assertEqual(release.version, "1.2.4")
|
|
938
|
+
self.assertEqual(release.revision, "abc123")
|
|
939
|
+
dump_fixture.assert_called_once()
|
|
940
|
+
|
|
941
|
+
def test_published_release_not_current_returns_404(self):
|
|
942
|
+
release = PackageRelease.objects.create(
|
|
943
|
+
package=self.package,
|
|
944
|
+
version="1.2.4",
|
|
945
|
+
pypi_url="https://example.com",
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
url = reverse("release-progress", args=[release.pk, "publish"])
|
|
949
|
+
response = self.client.get(url)
|
|
950
|
+
|
|
951
|
+
self.assertEqual(response.status_code, 404)
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
class ReleaseProgressFixtureVisibilityTests(TestCase):
|
|
955
|
+
def setUp(self):
|
|
956
|
+
self.client = Client()
|
|
957
|
+
self.user = User.objects.create_superuser(
|
|
958
|
+
"fixture-check", "fixture@example.com", "pw"
|
|
959
|
+
)
|
|
960
|
+
self.client.force_login(self.user)
|
|
961
|
+
current_version = Path("VERSION").read_text(encoding="utf-8").strip()
|
|
962
|
+
package = Package.objects.filter(is_active=True).first()
|
|
963
|
+
if package is None:
|
|
964
|
+
package = Package.objects.create(name="fixturepkg", is_active=True)
|
|
965
|
+
try:
|
|
966
|
+
self.release = PackageRelease.objects.get(
|
|
967
|
+
package=package, version=current_version
|
|
968
|
+
)
|
|
969
|
+
except PackageRelease.DoesNotExist:
|
|
970
|
+
self.release = PackageRelease.objects.create(
|
|
971
|
+
package=package, version=current_version
|
|
972
|
+
)
|
|
973
|
+
self.session_key = f"release_publish_{self.release.pk}"
|
|
974
|
+
self.log_name = f"{self.release.package.name}-{self.release.version}.log"
|
|
975
|
+
self.lock_path = Path("locks") / f"{self.session_key}.json"
|
|
976
|
+
self.restart_path = Path("locks") / f"{self.session_key}.restarts"
|
|
977
|
+
self.log_path = Path("logs") / self.log_name
|
|
978
|
+
for path in (self.lock_path, self.restart_path, self.log_path):
|
|
979
|
+
if path.exists():
|
|
980
|
+
path.unlink()
|
|
981
|
+
try:
|
|
982
|
+
self.fixture_step_index = next(
|
|
983
|
+
idx
|
|
984
|
+
for idx, (name, _) in enumerate(core_views.PUBLISH_STEPS)
|
|
985
|
+
if name == core_views.FIXTURE_REVIEW_STEP_NAME
|
|
986
|
+
)
|
|
987
|
+
except StopIteration: # pragma: no cover - defensive guard
|
|
988
|
+
self.fail("Fixture review step not configured in publish steps")
|
|
989
|
+
self.url = reverse("release-progress", args=[self.release.pk, "publish"])
|
|
990
|
+
|
|
991
|
+
def tearDown(self):
|
|
992
|
+
session = self.client.session
|
|
993
|
+
if self.session_key in session:
|
|
994
|
+
session.pop(self.session_key)
|
|
995
|
+
session.save()
|
|
996
|
+
for path in (self.lock_path, self.restart_path, self.log_path):
|
|
997
|
+
if path.exists():
|
|
998
|
+
path.unlink()
|
|
999
|
+
super().tearDown()
|
|
1000
|
+
|
|
1001
|
+
def _set_session(self, step: int, fixtures: list[dict]):
|
|
1002
|
+
session = self.client.session
|
|
1003
|
+
session[self.session_key] = {
|
|
1004
|
+
"step": step,
|
|
1005
|
+
"fixtures": fixtures,
|
|
1006
|
+
"log": self.log_name,
|
|
1007
|
+
"started": True,
|
|
1008
|
+
}
|
|
1009
|
+
session.save()
|
|
1010
|
+
|
|
1011
|
+
def test_fixture_summary_visible_until_migration_step(self):
|
|
1012
|
+
fixtures = [
|
|
1013
|
+
{
|
|
1014
|
+
"path": "core/fixtures/example.json",
|
|
1015
|
+
"count": 2,
|
|
1016
|
+
"models": ["core.Model"],
|
|
1017
|
+
}
|
|
1018
|
+
]
|
|
1019
|
+
self._set_session(self.fixture_step_index, fixtures)
|
|
1020
|
+
response = self.client.get(self.url)
|
|
1021
|
+
self.assertEqual(response.status_code, 200)
|
|
1022
|
+
self.assertEqual(response.context["fixtures"], fixtures)
|
|
1023
|
+
self.assertContains(response, "Fixture changes")
|
|
1024
|
+
|
|
1025
|
+
def test_fixture_summary_hidden_after_migration_step(self):
|
|
1026
|
+
fixtures = [
|
|
1027
|
+
{
|
|
1028
|
+
"path": "core/fixtures/example.json",
|
|
1029
|
+
"count": 2,
|
|
1030
|
+
"models": ["core.Model"],
|
|
1031
|
+
}
|
|
1032
|
+
]
|
|
1033
|
+
self._set_session(self.fixture_step_index + 1, fixtures)
|
|
1034
|
+
response = self.client.get(self.url)
|
|
1035
|
+
self.assertEqual(response.status_code, 200)
|
|
1036
|
+
self.assertIsNone(response.context["fixtures"])
|
|
1037
|
+
self.assertNotContains(response, "Fixture changes")
|
|
1038
|
+
|
|
1039
|
+
|
|
904
1040
|
class PackageReleaseAdminActionTests(TestCase):
|
|
905
1041
|
def setUp(self):
|
|
906
1042
|
self.factory = RequestFactory()
|
|
@@ -982,6 +1118,40 @@ class PackageReleaseCurrentTests(TestCase):
|
|
|
982
1118
|
self.assertFalse(self.release.is_current)
|
|
983
1119
|
|
|
984
1120
|
|
|
1121
|
+
class PackageReleaseChangelistTests(TestCase):
|
|
1122
|
+
def setUp(self):
|
|
1123
|
+
self.client = Client()
|
|
1124
|
+
User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1125
|
+
self.client.force_login(User.objects.get(username="admin"))
|
|
1126
|
+
|
|
1127
|
+
def test_prepare_next_release_button_present(self):
|
|
1128
|
+
response = self.client.get(reverse("admin:core_packagerelease_changelist"))
|
|
1129
|
+
prepare_url = reverse(
|
|
1130
|
+
"admin:core_packagerelease_actions", args=["prepare_next_release"]
|
|
1131
|
+
)
|
|
1132
|
+
self.assertContains(response, prepare_url, html=False)
|
|
1133
|
+
|
|
1134
|
+
def test_refresh_from_pypi_button_present(self):
|
|
1135
|
+
response = self.client.get(reverse("admin:core_packagerelease_changelist"))
|
|
1136
|
+
refresh_url = reverse(
|
|
1137
|
+
"admin:core_packagerelease_actions", args=["refresh_from_pypi"]
|
|
1138
|
+
)
|
|
1139
|
+
self.assertContains(response, refresh_url, html=False)
|
|
1140
|
+
|
|
1141
|
+
def test_prepare_next_release_action_creates_release(self):
|
|
1142
|
+
package = Package.objects.get(name="arthexis")
|
|
1143
|
+
PackageRelease.all_objects.filter(package=package).delete()
|
|
1144
|
+
response = self.client.post(
|
|
1145
|
+
reverse(
|
|
1146
|
+
"admin:core_packagerelease_actions", args=["prepare_next_release"]
|
|
1147
|
+
)
|
|
1148
|
+
)
|
|
1149
|
+
self.assertEqual(response.status_code, 302)
|
|
1150
|
+
self.assertTrue(
|
|
1151
|
+
PackageRelease.all_objects.filter(package=package).exists()
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
|
|
985
1155
|
class PackageAdminPrepareNextReleaseTests(TestCase):
|
|
986
1156
|
def setUp(self):
|
|
987
1157
|
self.factory = RequestFactory()
|
|
@@ -1000,24 +1170,18 @@ class PackageAdminPrepareNextReleaseTests(TestCase):
|
|
|
1000
1170
|
)
|
|
1001
1171
|
|
|
1002
1172
|
|
|
1003
|
-
class
|
|
1173
|
+
class PackageAdminChangeViewTests(TestCase):
|
|
1004
1174
|
def setUp(self):
|
|
1005
1175
|
self.client = Client()
|
|
1006
1176
|
User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1007
1177
|
self.client.force_login(User.objects.get(username="admin"))
|
|
1178
|
+
self.package = Package.objects.get(name="arthexis")
|
|
1008
1179
|
|
|
1009
|
-
def
|
|
1010
|
-
response = self.client.get(
|
|
1011
|
-
|
|
1012
|
-
response, reverse("admin:core_package_prepare_next_release"), html=False
|
|
1180
|
+
def test_prepare_next_release_button_visible_on_change_view(self):
|
|
1181
|
+
response = self.client.get(
|
|
1182
|
+
reverse("admin:core_package_change", args=[self.package.pk])
|
|
1013
1183
|
)
|
|
1014
|
-
|
|
1015
|
-
def test_refresh_from_pypi_button_present(self):
|
|
1016
|
-
response = self.client.get(reverse("admin:core_packagerelease_changelist"))
|
|
1017
|
-
refresh_url = reverse(
|
|
1018
|
-
"admin:core_packagerelease_actions", args=["refresh_from_pypi"]
|
|
1019
|
-
)
|
|
1020
|
-
self.assertContains(response, refresh_url, html=False)
|
|
1184
|
+
self.assertContains(response, "Prepare next Release")
|
|
1021
1185
|
|
|
1022
1186
|
|
|
1023
1187
|
class TodoDoneTests(TestCase):
|
|
@@ -1034,6 +1198,118 @@ class TodoDoneTests(TestCase):
|
|
|
1034
1198
|
self.assertIsNotNone(todo.done_on)
|
|
1035
1199
|
self.assertFalse(todo.is_deleted)
|
|
1036
1200
|
|
|
1201
|
+
def test_mark_done_condition_failure_shows_message(self):
|
|
1202
|
+
todo = Todo.objects.create(
|
|
1203
|
+
request="Task",
|
|
1204
|
+
on_done_condition="1 = 0",
|
|
1205
|
+
)
|
|
1206
|
+
resp = self.client.post(reverse("todo-done", args=[todo.pk]))
|
|
1207
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
1208
|
+
messages = [m.message for m in get_messages(resp.wsgi_request)]
|
|
1209
|
+
self.assertTrue(messages)
|
|
1210
|
+
self.assertIn("1 = 0", messages[0])
|
|
1211
|
+
todo.refresh_from_db()
|
|
1212
|
+
self.assertIsNone(todo.done_on)
|
|
1213
|
+
|
|
1214
|
+
def test_mark_done_condition_invalid_expression(self):
|
|
1215
|
+
todo = Todo.objects.create(
|
|
1216
|
+
request="Task",
|
|
1217
|
+
on_done_condition="1; SELECT 1",
|
|
1218
|
+
)
|
|
1219
|
+
resp = self.client.post(reverse("todo-done", args=[todo.pk]))
|
|
1220
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
1221
|
+
messages = [m.message for m in get_messages(resp.wsgi_request)]
|
|
1222
|
+
self.assertTrue(messages)
|
|
1223
|
+
self.assertIn("Semicolons", messages[0])
|
|
1224
|
+
todo.refresh_from_db()
|
|
1225
|
+
self.assertIsNone(todo.done_on)
|
|
1226
|
+
|
|
1227
|
+
def test_mark_done_condition_resolves_sigils(self):
|
|
1228
|
+
todo = Todo.objects.create(
|
|
1229
|
+
request="Task",
|
|
1230
|
+
on_done_condition="[TEST]",
|
|
1231
|
+
)
|
|
1232
|
+
with mock.patch.object(Todo, "resolve_sigils", return_value="1 = 1") as resolver:
|
|
1233
|
+
resp = self.client.post(reverse("todo-done", args=[todo.pk]))
|
|
1234
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
1235
|
+
resolver.assert_called_once_with("on_done_condition")
|
|
1236
|
+
todo.refresh_from_db()
|
|
1237
|
+
self.assertIsNotNone(todo.done_on)
|
|
1238
|
+
|
|
1239
|
+
def test_mark_done_respects_next_parameter(self):
|
|
1240
|
+
todo = Todo.objects.create(request="Task")
|
|
1241
|
+
next_url = reverse("admin:index") + "?section=todos"
|
|
1242
|
+
resp = self.client.post(
|
|
1243
|
+
reverse("todo-done", args=[todo.pk]),
|
|
1244
|
+
{"next": next_url},
|
|
1245
|
+
)
|
|
1246
|
+
self.assertRedirects(resp, next_url, target_status_code=200)
|
|
1247
|
+
todo.refresh_from_db()
|
|
1248
|
+
self.assertIsNotNone(todo.done_on)
|
|
1249
|
+
|
|
1250
|
+
def test_mark_done_rejects_external_next(self):
|
|
1251
|
+
todo = Todo.objects.create(request="Task")
|
|
1252
|
+
resp = self.client.post(
|
|
1253
|
+
reverse("todo-done", args=[todo.pk]),
|
|
1254
|
+
{"next": "https://example.com/"},
|
|
1255
|
+
)
|
|
1256
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
1257
|
+
todo.refresh_from_db()
|
|
1258
|
+
self.assertIsNotNone(todo.done_on)
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
class TodoFocusViewTests(TestCase):
|
|
1262
|
+
def setUp(self):
|
|
1263
|
+
self.client = Client()
|
|
1264
|
+
User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1265
|
+
self.client.force_login(User.objects.get(username="admin"))
|
|
1266
|
+
|
|
1267
|
+
def test_focus_view_renders_requested_page(self):
|
|
1268
|
+
todo = Todo.objects.create(request="Task", url="/docs/")
|
|
1269
|
+
next_url = reverse("admin:index")
|
|
1270
|
+
resp = self.client.get(
|
|
1271
|
+
f"{reverse('todo-focus', args=[todo.pk])}?next={quote(next_url)}"
|
|
1272
|
+
)
|
|
1273
|
+
self.assertEqual(resp.status_code, 200)
|
|
1274
|
+
self.assertContains(resp, todo.request)
|
|
1275
|
+
self.assertEqual(resp["X-Frame-Options"], "SAMEORIGIN")
|
|
1276
|
+
self.assertContains(resp, f'src="{todo.url}"')
|
|
1277
|
+
self.assertContains(resp, "Done")
|
|
1278
|
+
self.assertContains(resp, "Back")
|
|
1279
|
+
|
|
1280
|
+
def test_focus_view_uses_admin_change_when_no_url(self):
|
|
1281
|
+
todo = Todo.objects.create(request="Task")
|
|
1282
|
+
resp = self.client.get(reverse("todo-focus", args=[todo.pk]))
|
|
1283
|
+
change_url = reverse("admin:core_todo_change", args=[todo.pk])
|
|
1284
|
+
self.assertContains(resp, f'src="{change_url}"')
|
|
1285
|
+
|
|
1286
|
+
def test_focus_view_sanitizes_loopback_absolute_url(self):
|
|
1287
|
+
todo = Todo.objects.create(
|
|
1288
|
+
request="Task",
|
|
1289
|
+
url="http://127.0.0.1:8000/docs/?section=chart",
|
|
1290
|
+
)
|
|
1291
|
+
resp = self.client.get(reverse("todo-focus", args=[todo.pk]))
|
|
1292
|
+
self.assertContains(resp, 'src="/docs/?section=chart"')
|
|
1293
|
+
|
|
1294
|
+
def test_focus_view_rejects_external_absolute_url(self):
|
|
1295
|
+
todo = Todo.objects.create(
|
|
1296
|
+
request="Task",
|
|
1297
|
+
url="https://outside.invalid/external/",
|
|
1298
|
+
)
|
|
1299
|
+
resp = self.client.get(reverse("todo-focus", args=[todo.pk]))
|
|
1300
|
+
change_url = reverse("admin:core_todo_change", args=[todo.pk])
|
|
1301
|
+
self.assertContains(resp, f'src="{change_url}"')
|
|
1302
|
+
|
|
1303
|
+
def test_focus_view_redirects_if_todo_completed(self):
|
|
1304
|
+
todo = Todo.objects.create(request="Task")
|
|
1305
|
+
todo.done_on = timezone.now()
|
|
1306
|
+
todo.save(update_fields=["done_on"])
|
|
1307
|
+
next_url = reverse("admin:index")
|
|
1308
|
+
resp = self.client.get(
|
|
1309
|
+
f"{reverse('todo-focus', args=[todo.pk])}?next={quote(next_url)}"
|
|
1310
|
+
)
|
|
1311
|
+
self.assertRedirects(resp, next_url, target_status_code=200)
|
|
1312
|
+
|
|
1037
1313
|
|
|
1038
1314
|
class TodoUrlValidationTests(TestCase):
|
|
1039
1315
|
def test_relative_url_valid(self):
|
core/user_data.py
CHANGED
|
@@ -16,6 +16,7 @@ from django.dispatch import receiver
|
|
|
16
16
|
from django.http import HttpResponse, HttpResponseRedirect
|
|
17
17
|
from django.template.response import TemplateResponse
|
|
18
18
|
from django.urls import path, reverse
|
|
19
|
+
from django.utils.functional import LazyObject
|
|
19
20
|
from django.utils.translation import gettext as _
|
|
20
21
|
|
|
21
22
|
from .entity import Entity
|
|
@@ -39,7 +40,14 @@ def _username_for(user) -> str:
|
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
def _user_allows_user_data(user) -> bool:
|
|
42
|
-
|
|
43
|
+
if not user:
|
|
44
|
+
return False
|
|
45
|
+
username = _username_for(user)
|
|
46
|
+
UserModel = get_user_model()
|
|
47
|
+
system_username = getattr(UserModel, "SYSTEM_USERNAME", "")
|
|
48
|
+
if system_username and username == system_username:
|
|
49
|
+
return True
|
|
50
|
+
return not getattr(user, "is_profile_restricted", False)
|
|
43
51
|
|
|
44
52
|
|
|
45
53
|
def _data_dir(user) -> Path:
|
|
@@ -93,18 +101,68 @@ def _seed_fixture_path(instance) -> Path | None:
|
|
|
93
101
|
return None
|
|
94
102
|
|
|
95
103
|
|
|
104
|
+
def _coerce_user(candidate, user_model):
|
|
105
|
+
if candidate is None:
|
|
106
|
+
return None
|
|
107
|
+
if isinstance(candidate, user_model):
|
|
108
|
+
return candidate
|
|
109
|
+
if isinstance(candidate, LazyObject):
|
|
110
|
+
try:
|
|
111
|
+
candidate._setup()
|
|
112
|
+
except Exception:
|
|
113
|
+
return None
|
|
114
|
+
return _coerce_user(candidate._wrapped, user_model)
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _select_fixture_user(candidate, user_model):
|
|
119
|
+
user = _coerce_user(candidate, user_model)
|
|
120
|
+
visited: set[int] = set()
|
|
121
|
+
while user is not None:
|
|
122
|
+
identifier = user.pk or id(user)
|
|
123
|
+
if identifier in visited:
|
|
124
|
+
break
|
|
125
|
+
visited.add(identifier)
|
|
126
|
+
username = _username_for(user)
|
|
127
|
+
admin_username = getattr(user_model, "ADMIN_USERNAME", "")
|
|
128
|
+
if admin_username and username == admin_username:
|
|
129
|
+
try:
|
|
130
|
+
delegate = getattr(user, "operate_as", None)
|
|
131
|
+
except user_model.DoesNotExist:
|
|
132
|
+
delegate = None
|
|
133
|
+
else:
|
|
134
|
+
delegate = _coerce_user(delegate, user_model)
|
|
135
|
+
if delegate is not None and delegate is not user:
|
|
136
|
+
user = delegate
|
|
137
|
+
continue
|
|
138
|
+
if _user_allows_user_data(user):
|
|
139
|
+
return user
|
|
140
|
+
try:
|
|
141
|
+
delegate = getattr(user, "operate_as", None)
|
|
142
|
+
except user_model.DoesNotExist:
|
|
143
|
+
delegate = None
|
|
144
|
+
user = _coerce_user(delegate, user_model)
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
96
148
|
def _resolve_fixture_user(instance, fallback=None):
|
|
97
149
|
UserModel = get_user_model()
|
|
98
150
|
owner = getattr(instance, "user", None)
|
|
99
|
-
|
|
100
|
-
|
|
151
|
+
selected = _select_fixture_user(owner, UserModel)
|
|
152
|
+
if selected is not None:
|
|
153
|
+
return selected
|
|
101
154
|
if hasattr(instance, "owner"):
|
|
102
155
|
try:
|
|
103
156
|
owner_value = instance.owner
|
|
104
157
|
except Exception:
|
|
105
158
|
owner_value = None
|
|
106
|
-
|
|
107
|
-
|
|
159
|
+
else:
|
|
160
|
+
selected = _select_fixture_user(owner_value, UserModel)
|
|
161
|
+
if selected is not None:
|
|
162
|
+
return selected
|
|
163
|
+
selected = _select_fixture_user(fallback, UserModel)
|
|
164
|
+
if selected is not None:
|
|
165
|
+
return selected
|
|
108
166
|
return fallback
|
|
109
167
|
|
|
110
168
|
|
|
@@ -230,6 +288,30 @@ def _is_user_fixture(path: Path) -> bool:
|
|
|
230
288
|
return len(parts) >= 2 and parts[1].lower() == "user"
|
|
231
289
|
|
|
232
290
|
|
|
291
|
+
def _get_request_ip(request) -> str:
|
|
292
|
+
"""Return the best-effort client IP for ``request``."""
|
|
293
|
+
|
|
294
|
+
if request is None:
|
|
295
|
+
return ""
|
|
296
|
+
|
|
297
|
+
meta = getattr(request, "META", None)
|
|
298
|
+
if not getattr(meta, "get", None):
|
|
299
|
+
return ""
|
|
300
|
+
|
|
301
|
+
forwarded = meta.get("HTTP_X_FORWARDED_FOR")
|
|
302
|
+
if forwarded:
|
|
303
|
+
for value in str(forwarded).split(","):
|
|
304
|
+
candidate = value.strip()
|
|
305
|
+
if candidate:
|
|
306
|
+
return candidate
|
|
307
|
+
|
|
308
|
+
remote = meta.get("REMOTE_ADDR")
|
|
309
|
+
if remote:
|
|
310
|
+
return str(remote).strip()
|
|
311
|
+
|
|
312
|
+
return ""
|
|
313
|
+
|
|
314
|
+
|
|
233
315
|
_shared_fixtures_loaded = False
|
|
234
316
|
|
|
235
317
|
|
|
@@ -260,6 +342,18 @@ def load_user_fixtures(user, *, include_shared: bool = False) -> None:
|
|
|
260
342
|
def _on_login(sender, request, user, **kwargs):
|
|
261
343
|
load_user_fixtures(user, include_shared=not _shared_fixtures_loaded)
|
|
262
344
|
|
|
345
|
+
if not (
|
|
346
|
+
getattr(user, "is_staff", False) or getattr(user, "is_superuser", False)
|
|
347
|
+
):
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
username = _username_for(user) or "unknown"
|
|
351
|
+
ip_address = _get_request_ip(request) or "unknown"
|
|
352
|
+
|
|
353
|
+
from nodes.models import NetMessage
|
|
354
|
+
|
|
355
|
+
NetMessage.broadcast(subject=f"login {username}", body=f"@ {ip_address}")
|
|
356
|
+
|
|
263
357
|
|
|
264
358
|
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
|
|
265
359
|
def _on_user_created(sender, instance, created, **kwargs):
|
|
@@ -274,9 +368,7 @@ class UserDatumAdminMixin(admin.ModelAdmin):
|
|
|
274
368
|
def render_change_form(
|
|
275
369
|
self, request, context, add=False, change=False, form_url="", obj=None
|
|
276
370
|
):
|
|
277
|
-
context["show_user_datum"] = issubclass(
|
|
278
|
-
self.model, Entity
|
|
279
|
-
) and _user_allows_user_data(request.user)
|
|
371
|
+
context["show_user_datum"] = issubclass(self.model, Entity)
|
|
280
372
|
context["show_seed_datum"] = issubclass(self.model, Entity)
|
|
281
373
|
context["show_save_as_copy"] = issubclass(self.model, Entity) or hasattr(
|
|
282
374
|
self.model, "clone"
|
|
@@ -317,6 +409,9 @@ class EntityModelAdmin(UserDatumAdminMixin, admin.ModelAdmin):
|
|
|
317
409
|
)
|
|
318
410
|
if copied:
|
|
319
411
|
return
|
|
412
|
+
if getattr(self, "_skip_entity_user_datum", False):
|
|
413
|
+
return
|
|
414
|
+
|
|
320
415
|
target_user = _resolve_fixture_user(obj, request.user)
|
|
321
416
|
allow_user_data = _user_allows_user_data(target_user)
|
|
322
417
|
if request.POST.get("_user_datum") == "on":
|