arthexis 0.1.24__py3-none-any.whl → 0.1.25__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.24.dist-info → arthexis-0.1.25.dist-info}/METADATA +35 -14
- {arthexis-0.1.24.dist-info → arthexis-0.1.25.dist-info}/RECORD +30 -29
- config/settings.py +6 -3
- config/urls.py +2 -0
- core/admin.py +1 -186
- core/backends.py +3 -1
- core/models.py +74 -8
- core/system.py +67 -2
- core/views.py +0 -3
- nodes/admin.py +444 -251
- nodes/models.py +299 -23
- nodes/tasks.py +13 -16
- nodes/tests.py +211 -1
- nodes/urls.py +5 -0
- nodes/utils.py +9 -2
- nodes/views.py +128 -80
- ocpp/admin.py +190 -2
- ocpp/consumers.py +98 -0
- ocpp/models.py +271 -0
- ocpp/network.py +398 -0
- ocpp/tasks.py +108 -267
- ocpp/tests.py +179 -0
- ocpp/views.py +2 -0
- pages/middleware.py +3 -2
- pages/tests.py +40 -0
- pages/utils.py +70 -0
- pages/views.py +4 -2
- {arthexis-0.1.24.dist-info → arthexis-0.1.25.dist-info}/WHEEL +0 -0
- {arthexis-0.1.24.dist-info → arthexis-0.1.25.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.24.dist-info → arthexis-0.1.25.dist-info}/top_level.txt +0 -0
nodes/tests.py
CHANGED
|
@@ -36,6 +36,7 @@ from django.test import Client, SimpleTestCase, TestCase, TransactionTestCase, o
|
|
|
36
36
|
from django.urls import reverse
|
|
37
37
|
from django.contrib.auth import get_user_model
|
|
38
38
|
from django.contrib import admin
|
|
39
|
+
from django.contrib.admin import helpers
|
|
39
40
|
from django.contrib.auth.models import Permission
|
|
40
41
|
from django_celery_beat.models import IntervalSchedule, PeriodicTask
|
|
41
42
|
from django.conf import settings
|
|
@@ -133,6 +134,12 @@ class NodeGetLocalDatabaseUnavailableTests(SimpleTestCase):
|
|
|
133
134
|
|
|
134
135
|
|
|
135
136
|
class NodeGetLocalTests(TestCase):
|
|
137
|
+
def setUp(self):
|
|
138
|
+
super().setUp()
|
|
139
|
+
User = get_user_model()
|
|
140
|
+
self.user = User.objects.create_user(username="localtester", password="pwd")
|
|
141
|
+
self.client.force_login(self.user)
|
|
142
|
+
|
|
136
143
|
def test_normalize_relation_handles_various_inputs(self):
|
|
137
144
|
self.assertEqual(
|
|
138
145
|
Node.normalize_relation(Node.Relation.UPSTREAM),
|
|
@@ -174,6 +181,7 @@ class NodeGetLocalTests(TestCase):
|
|
|
174
181
|
patch(
|
|
175
182
|
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
176
183
|
),
|
|
184
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
177
185
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
178
186
|
patch.object(Node, "ensure_keys"),
|
|
179
187
|
):
|
|
@@ -202,6 +210,7 @@ class NodeGetLocalTests(TestCase):
|
|
|
202
210
|
patch(
|
|
203
211
|
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
204
212
|
),
|
|
213
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
205
214
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
206
215
|
patch.object(Node, "ensure_keys"),
|
|
207
216
|
patch.object(Node, "notify_peers_of_update"),
|
|
@@ -221,6 +230,7 @@ class NodeGetLocalTests(TestCase):
|
|
|
221
230
|
patch(
|
|
222
231
|
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
223
232
|
),
|
|
233
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
224
234
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
225
235
|
patch.object(Node, "ensure_keys"),
|
|
226
236
|
patch.object(Node, "notify_peers_of_update"),
|
|
@@ -242,6 +252,7 @@ class NodeGetLocalTests(TestCase):
|
|
|
242
252
|
patch(
|
|
243
253
|
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
244
254
|
),
|
|
255
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
245
256
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
246
257
|
patch.object(Node, "ensure_keys"),
|
|
247
258
|
patch.object(Node, "notify_peers_of_update"),
|
|
@@ -260,6 +271,7 @@ class NodeGetLocalTests(TestCase):
|
|
|
260
271
|
patch("nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"),
|
|
261
272
|
patch("nodes.models.socket.gethostname", return_value="localhost"),
|
|
262
273
|
patch("nodes.models.socket.gethostbyname", return_value="127.0.0.1"),
|
|
274
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
263
275
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
264
276
|
patch.object(Node, "ensure_keys"),
|
|
265
277
|
patch.object(Node, "notify_peers_of_update"),
|
|
@@ -282,6 +294,7 @@ class NodeGetLocalTests(TestCase):
|
|
|
282
294
|
patch("nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:56"),
|
|
283
295
|
patch("nodes.models.socket.gethostname", return_value="localhost"),
|
|
284
296
|
patch("nodes.models.socket.gethostbyname", return_value="127.0.0.1"),
|
|
297
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
285
298
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
286
299
|
patch.object(Node, "ensure_keys"),
|
|
287
300
|
patch.object(Node, "notify_peers_of_update"),
|
|
@@ -377,6 +390,37 @@ class NodeGetLocalTests(TestCase):
|
|
|
377
390
|
self.assertNotEqual(node_one.public_endpoint, node_two.public_endpoint)
|
|
378
391
|
self.assertTrue(node_two.public_endpoint.startswith("duplicate-host-"))
|
|
379
392
|
|
|
393
|
+
def test_register_node_accepts_network_hostname_without_address(self):
|
|
394
|
+
response = self.client.post(
|
|
395
|
+
reverse("register-node"),
|
|
396
|
+
data={
|
|
397
|
+
"hostname": "domain-node",
|
|
398
|
+
"network_hostname": "domain-node.example.com",
|
|
399
|
+
"port": 8050,
|
|
400
|
+
"mac_address": "aa:bb:cc:dd:ee:ff",
|
|
401
|
+
},
|
|
402
|
+
content_type="application/json",
|
|
403
|
+
)
|
|
404
|
+
self.assertEqual(response.status_code, 200)
|
|
405
|
+
node = Node.objects.get(mac_address="aa:bb:cc:dd:ee:ff")
|
|
406
|
+
self.assertEqual(node.network_hostname, "domain-node.example.com")
|
|
407
|
+
self.assertIsNone(node.address)
|
|
408
|
+
self.assertIsNone(node.ipv4_address)
|
|
409
|
+
self.assertIsNone(node.ipv6_address)
|
|
410
|
+
|
|
411
|
+
def test_register_node_requires_contact_information(self):
|
|
412
|
+
response = self.client.post(
|
|
413
|
+
reverse("register-node"),
|
|
414
|
+
data={
|
|
415
|
+
"hostname": "missing-host",
|
|
416
|
+
"port": 8051,
|
|
417
|
+
"mac_address": "aa:bb:cc:dd:ee:00",
|
|
418
|
+
},
|
|
419
|
+
content_type="application/json",
|
|
420
|
+
)
|
|
421
|
+
self.assertEqual(response.status_code, 400)
|
|
422
|
+
self.assertIn("at least one", response.json()["detail"])
|
|
423
|
+
|
|
380
424
|
def test_register_node_assigns_interface_role_and_returns_uuid(self):
|
|
381
425
|
NodeRole.objects.get_or_create(name="Interface")
|
|
382
426
|
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
@@ -806,7 +850,10 @@ class NodeInfoViewTests(TestCase):
|
|
|
806
850
|
self.addCleanup(self.patcher.stop)
|
|
807
851
|
self.node = Node.objects.create(
|
|
808
852
|
hostname="local",
|
|
853
|
+
network_hostname="local.example.com",
|
|
809
854
|
address="10.0.0.10",
|
|
855
|
+
ipv4_address="10.0.0.10",
|
|
856
|
+
ipv6_address="2001:db8::10",
|
|
810
857
|
port=8000,
|
|
811
858
|
mac_address=self.mac,
|
|
812
859
|
public_endpoint="local",
|
|
@@ -834,6 +881,8 @@ class NodeInfoViewTests(TestCase):
|
|
|
834
881
|
self.assertEqual(response.status_code, 200)
|
|
835
882
|
payload = response.json()
|
|
836
883
|
self.assertEqual(payload["port"], 80)
|
|
884
|
+
self.assertEqual(payload.get("network_hostname"), "local.example.com")
|
|
885
|
+
self.assertIn("local.example.com", payload.get("contact_hosts", []))
|
|
837
886
|
|
|
838
887
|
def test_preserves_explicit_port_in_host_header(self):
|
|
839
888
|
with self.settings(ALLOWED_HOSTS=["arthexis.com"]):
|
|
@@ -855,6 +904,8 @@ class NodeInfoViewTests(TestCase):
|
|
|
855
904
|
self.assertEqual(response.status_code, 200)
|
|
856
905
|
payload = response.json()
|
|
857
906
|
self.assertEqual(payload.get("role"), "Terminal")
|
|
907
|
+
self.assertEqual(payload.get("ipv4_address"), "10.0.0.10")
|
|
908
|
+
self.assertEqual(payload.get("ipv6_address"), "2001:db8::10")
|
|
858
909
|
|
|
859
910
|
|
|
860
911
|
class RegisterVisitorNodeMessageTests(TestCase):
|
|
@@ -936,6 +987,7 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
936
987
|
patch(
|
|
937
988
|
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
938
989
|
),
|
|
990
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
939
991
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
940
992
|
patch.object(Node, "ensure_keys"),
|
|
941
993
|
patch.object(Node, "notify_peers_of_update") as mock_notify,
|
|
@@ -963,6 +1015,7 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
963
1015
|
patch(
|
|
964
1016
|
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
965
1017
|
),
|
|
1018
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
966
1019
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
967
1020
|
patch.object(Node, "ensure_keys"),
|
|
968
1021
|
):
|
|
@@ -981,6 +1034,7 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
981
1034
|
patch(
|
|
982
1035
|
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
983
1036
|
),
|
|
1037
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
984
1038
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
985
1039
|
patch.object(Node, "ensure_keys"),
|
|
986
1040
|
):
|
|
@@ -1000,6 +1054,7 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
1000
1054
|
patch(
|
|
1001
1055
|
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
1002
1056
|
),
|
|
1057
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
1003
1058
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
1004
1059
|
patch.object(Node, "ensure_keys"),
|
|
1005
1060
|
):
|
|
@@ -1025,10 +1080,20 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
1025
1080
|
return_value="00:ff:ee:dd:cc:bb",
|
|
1026
1081
|
),
|
|
1027
1082
|
patch("nodes.models.socket.gethostname", return_value="localnode"),
|
|
1083
|
+
patch("nodes.models.socket.getfqdn", return_value="localnode.example.com"),
|
|
1028
1084
|
patch(
|
|
1029
1085
|
"nodes.models.socket.gethostbyname",
|
|
1030
1086
|
return_value="192.168.1.5",
|
|
1031
1087
|
),
|
|
1088
|
+
patch.dict(os.environ, {"HOSTNAME": ""}, clear=False),
|
|
1089
|
+
patch.object(
|
|
1090
|
+
Node,
|
|
1091
|
+
"_resolve_ip_addresses",
|
|
1092
|
+
return_value=(
|
|
1093
|
+
["192.168.1.5", "93.184.216.34"],
|
|
1094
|
+
["fe80::1", "2001:4860:4860::8888"],
|
|
1095
|
+
),
|
|
1096
|
+
),
|
|
1032
1097
|
patch("nodes.models.revision.get_revision", return_value="newrev"),
|
|
1033
1098
|
patch("requests.post") as mock_post,
|
|
1034
1099
|
):
|
|
@@ -1052,6 +1117,9 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
1052
1117
|
self.assertEqual(payload["hostname"], "localnode")
|
|
1053
1118
|
self.assertEqual(payload["installed_version"], "2.0.0")
|
|
1054
1119
|
self.assertEqual(payload["installed_revision"], "newrev")
|
|
1120
|
+
self.assertEqual(payload.get("network_hostname"), "localnode.example.com")
|
|
1121
|
+
self.assertEqual(payload.get("ipv4_address"), "93.184.216.34")
|
|
1122
|
+
self.assertEqual(payload.get("ipv6_address"), "2001:4860:4860::8888")
|
|
1055
1123
|
|
|
1056
1124
|
def test_register_current_notifies_peers_without_version_change(self):
|
|
1057
1125
|
Node.objects.create(
|
|
@@ -1070,10 +1138,20 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
1070
1138
|
return_value="00:ff:ee:dd:cc:cc",
|
|
1071
1139
|
),
|
|
1072
1140
|
patch("nodes.models.socket.gethostname", return_value="samever"),
|
|
1141
|
+
patch("nodes.models.socket.getfqdn", return_value="samever.example.com"),
|
|
1073
1142
|
patch(
|
|
1074
1143
|
"nodes.models.socket.gethostbyname",
|
|
1075
1144
|
return_value="192.168.1.6",
|
|
1076
1145
|
),
|
|
1146
|
+
patch.dict(os.environ, {"HOSTNAME": ""}, clear=False),
|
|
1147
|
+
patch.object(
|
|
1148
|
+
Node,
|
|
1149
|
+
"_resolve_ip_addresses",
|
|
1150
|
+
return_value=(
|
|
1151
|
+
["192.168.1.6", "93.184.216.35"],
|
|
1152
|
+
["fe80::2", "2001:4860:4860::8844"],
|
|
1153
|
+
),
|
|
1154
|
+
),
|
|
1077
1155
|
patch("nodes.models.revision.get_revision", return_value="rev1"),
|
|
1078
1156
|
patch("requests.post") as mock_post,
|
|
1079
1157
|
):
|
|
@@ -1095,6 +1173,44 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
1095
1173
|
payload = json.loads(kwargs["data"])
|
|
1096
1174
|
self.assertEqual(payload["installed_version"], "1.0.0")
|
|
1097
1175
|
self.assertEqual(payload.get("installed_revision"), "rev1")
|
|
1176
|
+
self.assertEqual(payload.get("network_hostname"), "samever.example.com")
|
|
1177
|
+
self.assertEqual(payload.get("ipv4_address"), "93.184.216.35")
|
|
1178
|
+
self.assertEqual(payload.get("ipv6_address"), "2001:4860:4860::8844")
|
|
1179
|
+
|
|
1180
|
+
def test_register_current_populates_network_fields(self):
|
|
1181
|
+
with TemporaryDirectory() as tmp:
|
|
1182
|
+
base = Path(tmp)
|
|
1183
|
+
with override_settings(BASE_DIR=base):
|
|
1184
|
+
with (
|
|
1185
|
+
patch(
|
|
1186
|
+
"nodes.models.Node.get_current_mac",
|
|
1187
|
+
return_value="00:12:34:56:78:90",
|
|
1188
|
+
),
|
|
1189
|
+
patch("nodes.models.socket.gethostname", return_value="nodehost"),
|
|
1190
|
+
patch("nodes.models.socket.getfqdn", return_value="nodehost.example.com"),
|
|
1191
|
+
patch(
|
|
1192
|
+
"nodes.models.socket.gethostbyname",
|
|
1193
|
+
return_value="10.0.0.5",
|
|
1194
|
+
),
|
|
1195
|
+
patch.dict(os.environ, {"HOSTNAME": ""}, clear=False),
|
|
1196
|
+
patch.object(
|
|
1197
|
+
Node,
|
|
1198
|
+
"_resolve_ip_addresses",
|
|
1199
|
+
return_value=(
|
|
1200
|
+
["10.0.0.5", "93.184.216.36"],
|
|
1201
|
+
["fe80::5", "2001:4860:4860::1"],
|
|
1202
|
+
),
|
|
1203
|
+
),
|
|
1204
|
+
patch("nodes.models.revision.get_revision", return_value="revX"),
|
|
1205
|
+
patch.object(Node, "ensure_keys"),
|
|
1206
|
+
patch.object(Node, "notify_peers_of_update"),
|
|
1207
|
+
):
|
|
1208
|
+
node, created = Node.register_current()
|
|
1209
|
+
self.assertTrue(created)
|
|
1210
|
+
self.assertEqual(node.network_hostname, "nodehost.example.com")
|
|
1211
|
+
self.assertEqual(node.ipv4_address, "93.184.216.36")
|
|
1212
|
+
self.assertEqual(node.ipv6_address, "2001:4860:4860::1")
|
|
1213
|
+
self.assertEqual(node.address, "93.184.216.36")
|
|
1098
1214
|
|
|
1099
1215
|
@patch("nodes.views.capture_screenshot")
|
|
1100
1216
|
def test_capture_screenshot(self, mock_capture):
|
|
@@ -1731,7 +1847,10 @@ class NodeAdminTests(TestCase):
|
|
|
1731
1847
|
|
|
1732
1848
|
payload = {
|
|
1733
1849
|
"hostname": node.hostname,
|
|
1850
|
+
"network_hostname": "remote-node.example.com",
|
|
1734
1851
|
"address": node.address,
|
|
1852
|
+
"ipv4_address": "198.51.100.10",
|
|
1853
|
+
"ipv6_address": "2001:db8::10",
|
|
1735
1854
|
"port": node.port,
|
|
1736
1855
|
"role": "Control",
|
|
1737
1856
|
}
|
|
@@ -1740,7 +1859,13 @@ class NodeAdminTests(TestCase):
|
|
|
1740
1859
|
node.refresh_from_db()
|
|
1741
1860
|
|
|
1742
1861
|
self.assertIn("role", changed)
|
|
1862
|
+
self.assertIn("network_hostname", changed)
|
|
1863
|
+
self.assertIn("ipv4_address", changed)
|
|
1864
|
+
self.assertIn("ipv6_address", changed)
|
|
1743
1865
|
self.assertEqual(node.role, control)
|
|
1866
|
+
self.assertEqual(node.network_hostname, "remote-node.example.com")
|
|
1867
|
+
self.assertEqual(node.ipv4_address, "198.51.100.10")
|
|
1868
|
+
self.assertEqual(node.ipv6_address, "2001:db8::10")
|
|
1744
1869
|
self.assertTrue(control.node_set.filter(pk=node.pk).exists())
|
|
1745
1870
|
self.assertFalse(terminal.node_set.filter(pk=node.pk).exists())
|
|
1746
1871
|
|
|
@@ -1758,7 +1883,10 @@ class NodeAdminTests(TestCase):
|
|
|
1758
1883
|
|
|
1759
1884
|
payload = {
|
|
1760
1885
|
"hostname": node.hostname,
|
|
1886
|
+
"network_hostname": "role-name-node.example.com",
|
|
1761
1887
|
"address": node.address,
|
|
1888
|
+
"ipv4_address": "198.51.100.11",
|
|
1889
|
+
"ipv6_address": "2001:db8::11",
|
|
1762
1890
|
"port": node.port,
|
|
1763
1891
|
"role_name": "Control",
|
|
1764
1892
|
}
|
|
@@ -1767,6 +1895,9 @@ class NodeAdminTests(TestCase):
|
|
|
1767
1895
|
node.refresh_from_db()
|
|
1768
1896
|
|
|
1769
1897
|
self.assertIn("role", changed)
|
|
1898
|
+
self.assertEqual(node.network_hostname, "role-name-node.example.com")
|
|
1899
|
+
self.assertEqual(node.ipv4_address, "198.51.100.11")
|
|
1900
|
+
self.assertEqual(node.ipv6_address, "2001:db8::11")
|
|
1770
1901
|
self.assertEqual(node.role, control)
|
|
1771
1902
|
self.assertTrue(control.node_set.filter(pk=node.pk).exists())
|
|
1772
1903
|
self.assertFalse(terminal.node_set.filter(pk=node.pk).exists())
|
|
@@ -2407,6 +2538,53 @@ class NodeAdminTests(TestCase):
|
|
|
2407
2538
|
)
|
|
2408
2539
|
self.assertContains(response, str(remote))
|
|
2409
2540
|
|
|
2541
|
+
def test_send_net_message_action_displays_form(self):
|
|
2542
|
+
target = Node.objects.create(
|
|
2543
|
+
hostname="remote-one", address="10.0.0.10", port=8020
|
|
2544
|
+
)
|
|
2545
|
+
response = self.client.post(
|
|
2546
|
+
reverse("admin:nodes_node_changelist"),
|
|
2547
|
+
{
|
|
2548
|
+
"action": "send_net_message",
|
|
2549
|
+
helpers.ACTION_CHECKBOX_NAME: [str(target.pk)],
|
|
2550
|
+
},
|
|
2551
|
+
follow=False,
|
|
2552
|
+
)
|
|
2553
|
+
self.assertEqual(response.status_code, 200)
|
|
2554
|
+
response.render()
|
|
2555
|
+
self.assertContains(response, "Send Net Message")
|
|
2556
|
+
self.assertContains(response, str(target))
|
|
2557
|
+
self.assertContains(response, 'name="apply"')
|
|
2558
|
+
self.assertContains(response, "Selected node (1)")
|
|
2559
|
+
|
|
2560
|
+
@patch("nodes.admin.NetMessage.propagate")
|
|
2561
|
+
def test_send_net_message_action_creates_messages(self, mock_propagate):
|
|
2562
|
+
first = Node.objects.create(
|
|
2563
|
+
hostname="remote-two", address="10.0.0.11", port=8021
|
|
2564
|
+
)
|
|
2565
|
+
second = Node.objects.create(
|
|
2566
|
+
hostname="remote-three", address="10.0.0.12", port=8022
|
|
2567
|
+
)
|
|
2568
|
+
url = reverse("admin:nodes_node_changelist")
|
|
2569
|
+
payload = {
|
|
2570
|
+
"action": "send_net_message",
|
|
2571
|
+
"apply": "1",
|
|
2572
|
+
helpers.ACTION_CHECKBOX_NAME: [str(first.pk), str(second.pk)],
|
|
2573
|
+
"subject": "Maintenance",
|
|
2574
|
+
"body": "We will reboot tonight.",
|
|
2575
|
+
}
|
|
2576
|
+
existing_ids = set(NetMessage.objects.values_list("pk", flat=True))
|
|
2577
|
+
response = self.client.post(url, payload, follow=True)
|
|
2578
|
+
self.assertEqual(response.status_code, 200)
|
|
2579
|
+
new_messages = NetMessage.objects.exclude(pk__in=existing_ids)
|
|
2580
|
+
self.assertEqual(new_messages.count(), 2)
|
|
2581
|
+
self.assertEqual(mock_propagate.call_count, 2)
|
|
2582
|
+
for node in (first, second):
|
|
2583
|
+
message = new_messages.get(filter_node=node)
|
|
2584
|
+
self.assertEqual(message.subject, "Maintenance")
|
|
2585
|
+
self.assertEqual(message.body, "We will reboot tonight.")
|
|
2586
|
+
self.assertContains(response, "Sent 2 net messages.")
|
|
2587
|
+
|
|
2410
2588
|
@patch("nodes.admin.requests.post")
|
|
2411
2589
|
@patch("nodes.admin.requests.get")
|
|
2412
2590
|
def test_update_selected_nodes_progress_updates_remote(
|
|
@@ -3064,7 +3242,7 @@ class NetMessagePropagationTests(TestCase):
|
|
|
3064
3242
|
with patch.object(Node, "get_local", return_value=self.local):
|
|
3065
3243
|
msg = NetMessage.broadcast(subject="subject", body="body")
|
|
3066
3244
|
self.assertEqual(msg.node_origin, self.local)
|
|
3067
|
-
self.
|
|
3245
|
+
self.assertEqual(msg.reach, self.role)
|
|
3068
3246
|
|
|
3069
3247
|
@patch("requests.post")
|
|
3070
3248
|
@patch("core.notifications.notify")
|
|
@@ -4707,6 +4885,38 @@ class ContentClassifierTests(TestCase):
|
|
|
4707
4885
|
tags = ContentClassification.objects.filter(sample=sample)
|
|
4708
4886
|
self.assertTrue(tags.filter(tag__slug="screenshot-tag").exists())
|
|
4709
4887
|
|
|
4888
|
+
def test_save_screenshot_returns_none_for_duplicate_without_linking(self):
|
|
4889
|
+
with TemporaryDirectory() as tmp:
|
|
4890
|
+
base = Path(tmp)
|
|
4891
|
+
first_path = base / "capture.png"
|
|
4892
|
+
first_path.write_bytes(b"binary image data")
|
|
4893
|
+
duplicate_path = base / "duplicate.png"
|
|
4894
|
+
duplicate_path.write_bytes(b"binary image data")
|
|
4895
|
+
with override_settings(LOG_DIR=base):
|
|
4896
|
+
original = save_screenshot(first_path, method="TEST")
|
|
4897
|
+
duplicate = save_screenshot(duplicate_path, method="TEST")
|
|
4898
|
+
|
|
4899
|
+
self.assertIsNotNone(original)
|
|
4900
|
+
self.assertIsNone(duplicate)
|
|
4901
|
+
self.assertEqual(ContentSample.objects.count(), 1)
|
|
4902
|
+
|
|
4903
|
+
def test_save_screenshot_reuses_existing_sample_when_linking(self):
|
|
4904
|
+
with TemporaryDirectory() as tmp:
|
|
4905
|
+
base = Path(tmp)
|
|
4906
|
+
first_path = base / "capture.png"
|
|
4907
|
+
first_path.write_bytes(b"binary image data")
|
|
4908
|
+
duplicate_path = base / "duplicate.png"
|
|
4909
|
+
duplicate_path.write_bytes(b"binary image data")
|
|
4910
|
+
with override_settings(LOG_DIR=base):
|
|
4911
|
+
original = save_screenshot(first_path, method="TEST")
|
|
4912
|
+
reused = save_screenshot(
|
|
4913
|
+
duplicate_path, method="TEST", link_duplicates=True
|
|
4914
|
+
)
|
|
4915
|
+
|
|
4916
|
+
self.assertIsNotNone(original)
|
|
4917
|
+
self.assertEqual(reused, original)
|
|
4918
|
+
self.assertEqual(ContentSample.objects.count(), 1)
|
|
4919
|
+
|
|
4710
4920
|
def test_text_sample_runs_default_classifiers_without_duplicates(self):
|
|
4711
4921
|
sample = ContentSample.objects.create(
|
|
4712
4922
|
content="Example content", kind=ContentSample.TEXT
|
nodes/urls.py
CHANGED
|
@@ -12,6 +12,11 @@ urlpatterns = [
|
|
|
12
12
|
path("rfid/export/", views.export_rfids, name="node-rfid-export"),
|
|
13
13
|
path("rfid/import/", views.import_rfids, name="node-rfid-import"),
|
|
14
14
|
path("network/chargers/", views.network_chargers, name="node-network-chargers"),
|
|
15
|
+
path(
|
|
16
|
+
"network/chargers/forward/",
|
|
17
|
+
views.forward_chargers,
|
|
18
|
+
name="node-network-forward-chargers",
|
|
19
|
+
),
|
|
15
20
|
path(
|
|
16
21
|
"network/chargers/action/",
|
|
17
22
|
views.network_charger_action,
|
nodes/utils.py
CHANGED
|
@@ -90,10 +90,13 @@ def save_screenshot(
|
|
|
90
90
|
*,
|
|
91
91
|
content: str | None = None,
|
|
92
92
|
user=None,
|
|
93
|
+
link_duplicates: bool = False,
|
|
93
94
|
):
|
|
94
95
|
"""Save screenshot file info if not already recorded.
|
|
95
96
|
|
|
96
|
-
Returns the created :class:`ContentSample
|
|
97
|
+
Returns the created :class:`ContentSample`. If ``link_duplicates`` is ``True``
|
|
98
|
+
and a sample with identical content already exists, the existing record is
|
|
99
|
+
returned instead of ``None``.
|
|
97
100
|
"""
|
|
98
101
|
|
|
99
102
|
original = path
|
|
@@ -101,7 +104,11 @@ def save_screenshot(
|
|
|
101
104
|
path = settings.LOG_DIR / path
|
|
102
105
|
with path.open("rb") as fh:
|
|
103
106
|
digest = hashlib.sha256(fh.read()).hexdigest()
|
|
104
|
-
|
|
107
|
+
existing = ContentSample.objects.filter(hash=digest).first()
|
|
108
|
+
if existing:
|
|
109
|
+
if link_duplicates:
|
|
110
|
+
logger.info("Duplicate screenshot content; reusing existing sample")
|
|
111
|
+
return existing
|
|
105
112
|
logger.info("Duplicate screenshot content; record not created")
|
|
106
113
|
return None
|
|
107
114
|
stored_path = (original if not original.is_absolute() else path).as_posix()
|