arthexis 0.1.19__py3-none-any.whl → 0.1.20__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.
- {arthexis-0.1.19.dist-info → arthexis-0.1.20.dist-info}/METADATA +3 -3
- {arthexis-0.1.19.dist-info → arthexis-0.1.20.dist-info}/RECORD +38 -38
- core/admin.py +142 -1
- core/backends.py +8 -2
- core/environment.py +221 -4
- core/models.py +124 -25
- core/notifications.py +1 -1
- core/reference_utils.py +10 -11
- core/sigil_builder.py +2 -2
- core/tasks.py +24 -1
- core/tests.py +1 -0
- core/views.py +70 -36
- nodes/admin.py +133 -1
- nodes/models.py +294 -48
- nodes/rfid_sync.py +1 -1
- nodes/tasks.py +100 -2
- nodes/tests.py +532 -15
- nodes/urls.py +4 -0
- nodes/views.py +500 -95
- ocpp/admin.py +101 -3
- ocpp/consumers.py +106 -9
- ocpp/models.py +83 -1
- ocpp/tasks.py +4 -0
- ocpp/test_export_import.py +1 -0
- ocpp/test_rfid.py +3 -1
- ocpp/tests.py +100 -9
- ocpp/transactions_io.py +9 -1
- ocpp/urls.py +3 -3
- ocpp/views.py +101 -28
- pages/context_processors.py +15 -9
- pages/defaults.py +1 -1
- pages/module_defaults.py +5 -5
- pages/tests.py +110 -38
- pages/urls.py +1 -0
- pages/views.py +108 -8
- {arthexis-0.1.19.dist-info → arthexis-0.1.20.dist-info}/WHEEL +0 -0
- {arthexis-0.1.19.dist-info → arthexis-0.1.20.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.19.dist-info → arthexis-0.1.20.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arthexis
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.20
|
|
4
4
|
Summary: Power & Energy Infrastructure
|
|
5
5
|
Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
|
|
6
6
|
License-Expression: GPL-3.0-only
|
|
@@ -171,7 +171,7 @@ Arthexis Constellation ships in four node roles tailored to different deployment
|
|
|
171
171
|
<td valign="top"><strong>Multi-Device Edge, Network & Data Acquisition</strong><br />Features: AP Router, Celery Queue, NGINX Server, RFID Scanner</td>
|
|
172
172
|
</tr>
|
|
173
173
|
<tr>
|
|
174
|
-
<td valign="top"><strong>
|
|
174
|
+
<td valign="top"><strong>Watchtower</strong></td>
|
|
175
175
|
<td valign="top"><strong>Multi-User Cloud & Orchestration</strong><br />Features: Celery Queue, NGINX Server</td>
|
|
176
176
|
</tr>
|
|
177
177
|
</tbody>
|
|
@@ -184,7 +184,7 @@ Arthexis Constellation ships in four node roles tailored to different deployment
|
|
|
184
184
|
- **[Windows](https://en.wikipedia.org/wiki/Microsoft_Windows)**: open [PowerShell](https://learn.microsoft.com/powershell/) or [Git Bash](https://gitforwindows.org/) and run the same command.
|
|
185
185
|
|
|
186
186
|
### 2. Start and stop
|
|
187
|
-
Terminal nodes can start directly with the scripts below without installing; Control, Satellite, and
|
|
187
|
+
Terminal nodes can start directly with the scripts below without installing; Control, Satellite, and Watchtower roles require installation first. Both approaches listen on [`http://localhost:8000/`](http://localhost:8000/) by default.
|
|
188
188
|
|
|
189
189
|
- **[VS Code](https://code.visualstudio.com/)**
|
|
190
190
|
- Open the folder and go to the **Run and Debug** panel (`Ctrl+Shift+D`).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
arthexis-0.1.
|
|
1
|
+
arthexis-0.1.20.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
2
2
|
config/__init__.py,sha256=AwpOX7il-DAOmkdJ5dVfVJ3CWWebn1lHyQNmkw1EkDw,103
|
|
3
3
|
config/active_app.py,sha256=KJqYh-o91nPQjVXPEdbiJHzsI6cN9IZsBZ9O3iZ6Hyc,373
|
|
4
4
|
config/asgi.py,sha256=T-0QSbtieEWKPIDkEcEdd-q6qjK8ZCwwjCaISOBkWdM,1296
|
|
@@ -15,15 +15,15 @@ config/settings_helpers.py,sha256=0BdBciUHIkwsWa0vV_RKAd4wDuEzgE7G-42XYiES4YQ,31
|
|
|
15
15
|
config/urls.py,sha256=zvU4FSMKPlXUrGDjUgJCRFQztWb78wo1urW2DQf8qdI,5463
|
|
16
16
|
config/wsgi.py,sha256=zU_mKlya6hejQ21PxKacTui3dUWd4ca_-YJNSYAoMX0,433
|
|
17
17
|
core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
core/admin.py,sha256=
|
|
18
|
+
core/admin.py,sha256=3MhT1TA8QUAgmjlwBmLD6Z1nT19ZUrXLCBSchV1R6LE,151562
|
|
19
19
|
core/admin_history.py,sha256=XZ4b0ryufIka-xcwboK3DzmOL-INSx5Y2fJO-aJdV70,1783
|
|
20
20
|
core/admindocs.py,sha256=ycD0bJ_VE6rTGf9ebXTiKdYkD8Y8hD2oQ4HxxoBURCM,6756
|
|
21
21
|
core/apps.py,sha256=L_UMYI72-5jTo6nt8mfCbgdLhlP32D-8k76EZw0QyAA,14348
|
|
22
22
|
core/auto_upgrade.py,sha256=1EffHHFylgydWdZM_id6CppV0QqBtdNw7cwBYVdbNdk,1715
|
|
23
|
-
core/backends.py,sha256=
|
|
23
|
+
core/backends.py,sha256=okrNW5Z-zDhJB9Al529aTOHGsTLACeQ6v_hMl4cyfnA,10704
|
|
24
24
|
core/changelog.py,sha256=SRn37i5N-qb-RYV4Gpu9fg7Kv8gu4TH8ZwEmDRgN-Vo,12594
|
|
25
25
|
core/entity.py,sha256=o4VteOXePGEsIWJFZ3fpq3DZsdWr3hpQ9A6kFbKosSE,4844
|
|
26
|
-
core/environment.py,sha256=
|
|
26
|
+
core/environment.py,sha256=DfmVn2HVwX0rru-BSak6n3R3ier0UiHCZUoNszjt6oA,10182
|
|
27
27
|
core/fields.py,sha256=d-qGahdcv4SRcO4fwCJ6_-NnEAP5xW0k3kODdAAAHSA,5412
|
|
28
28
|
core/form_fields.py,sha256=h2xT8sO8EWbznsiARkxukFk69yoW6mQwqpgonA-d6aA,2496
|
|
29
29
|
core/github_helper.py,sha256=fkjoUPwOB19zbGuk39LNLJ5AbIVKFf3rNCtnu-JISIc,5733
|
|
@@ -34,79 +34,79 @@ core/liveupdate.py,sha256=22m0ueQ10-6b-9pQJHY0_5WRYA98fysXKEXOWzIr550,691
|
|
|
34
34
|
core/log_paths.py,sha256=lxvgXPgJtVNZ-kYrqV8VFle4GFQrSxG-yRTglqvclmU,3318
|
|
35
35
|
core/mailer.py,sha256=JpW0RnD9uZ4O-wvlqeW7CMw95IFeCSkdvbankJDwHq0,2886
|
|
36
36
|
core/middleware.py,sha256=j19K9SX-Emkv7BDDtAacR9g6RWsxhKHwCc8w23JFvMM,3388
|
|
37
|
-
core/models.py,sha256=
|
|
38
|
-
core/notifications.py,sha256=
|
|
37
|
+
core/models.py,sha256=U4oyaIuh1VSvpK7xwyZek09hRzVtY75zYXh1at7-_Pk,131276
|
|
38
|
+
core/notifications.py,sha256=jNLSuSCrhb8x5cDu_APeDlkrmbMejufk5eJOhssAC4I,3917
|
|
39
39
|
core/public_wifi.py,sha256=yydLgxOo9DmJJbM4X_23wGR3gxL3YzHno54v9GssuFA,7213
|
|
40
|
-
core/reference_utils.py,sha256=
|
|
40
|
+
core/reference_utils.py,sha256=tffCoyE1w4_SmYzXVWOsW8aR_ZVVTSPzrGhBq8K2xzA,3631
|
|
41
41
|
core/release.py,sha256=y5NRs0XwB7RQVvMEZoNWYjTBxuG68dOMizUXLRx7-x8,31561
|
|
42
42
|
core/rfid_import_export.py,sha256=petyhPvL0WUpehc6uGUDUhjYQ9AVvc6O49zuhDs6YFw,3516
|
|
43
|
-
core/sigil_builder.py,sha256=
|
|
43
|
+
core/sigil_builder.py,sha256=nMuhYlw3j3LosrK85Q0pYsMcfGWCmrmdnv8UG7GTq_o,4856
|
|
44
44
|
core/sigil_context.py,sha256=GCzjfM6fcVvBtSbVNfmE6sx3HU8QnxnXrCIytnNpQzM,439
|
|
45
45
|
core/sigil_resolver.py,sha256=rCsypuX-0oWNfKyM1T9ZLWHY0Ezwhtk4VmI0L3krnsE,11098
|
|
46
46
|
core/system.py,sha256=RyBqooWezM0Li_KCRskchD5Lub0cdqBmrnU6ilC8MPE,39823
|
|
47
|
-
core/tasks.py,sha256=
|
|
47
|
+
core/tasks.py,sha256=d0MQP5fmn5pA2VCFGxDMEX0xppDIIh8IAzPfGdnk8J4,12340
|
|
48
48
|
core/temp_passwords.py,sha256=FieUnIUeQHmA1DoXvfJ5U6-Ayv3oDz-hSln5s_vNbA4,5271
|
|
49
49
|
core/test_system_info.py,sha256=IMPz21KEs6OC5YbL7YaIBdmJVLjRY6MgPuZpldJB5OI,6935
|
|
50
|
-
core/tests.py,sha256=
|
|
50
|
+
core/tests.py,sha256=a78FZug8JPbvIMx1MgxmcYGyTpMr3WboTvqjnxI9evs,98715
|
|
51
51
|
core/tests_liveupdate.py,sha256=IquU8ztk6zbzC1bQu3Nrr3RzGzuujtPwDkANJHbxg98,510
|
|
52
52
|
core/urls.py,sha256=YPippON1MAP2KeZZ8jHpcLO6mvbnKn1q7fdMv5Vm9dY,425
|
|
53
53
|
core/user_data.py,sha256=02CfvxayELWSWZJCxWpv1Yz7EGg08yEu5MM31Khsi0U,21083
|
|
54
|
-
core/views.py,sha256=
|
|
54
|
+
core/views.py,sha256=U3hwNqJz_I9ybmmdJvLrljrynJaUzGMeHhsiaBJD4mk,90297
|
|
55
55
|
core/widgets.py,sha256=vlR9PlFfZGlkHm5X2cqNXuEBZSj8gmWaR6MO1mMy6kg,6904
|
|
56
56
|
core/workgroup_urls.py,sha256=XR9IqwsSBI8epW7_-hHhWFU9wsyJfZehHwNQBhCgmpM,407
|
|
57
57
|
core/workgroup_views.py,sha256=vtumF3-8YaTD-K6nSd8eYvUyq3ftpvWSEwtcp5B-P6o,2889
|
|
58
58
|
nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
|
-
nodes/admin.py,sha256=
|
|
59
|
+
nodes/admin.py,sha256=20gUgL5clMr_hNugybDqYdhoZh-4cWB2dVp6lgnHo_I,66960
|
|
60
60
|
nodes/apps.py,sha256=AxK-sh9JBJZwNOLjqw9omCQGUQWw-45VRdYH07XhVJU,2732
|
|
61
61
|
nodes/backends.py,sha256=dmmbS0X2YIlCDz2KjoDf_L62dy--nuqZF1rEDoi2JHM,5921
|
|
62
62
|
nodes/dns.py,sha256=D5smXD7Rkh6E4MdL6TBL2WY8GgJg7Rx9z88LZrcMbTw,7048
|
|
63
63
|
nodes/feature_checks.py,sha256=27e4PCkZ8BGWnJCOwMcY2Bo9z7LoeZWiTZuISWGnrzk,3996
|
|
64
64
|
nodes/lcd.py,sha256=iKA8Wmq85KZD52aTzAU8ZmS144_gbdGMOXcE8yuECps,5758
|
|
65
|
-
nodes/models.py,sha256=
|
|
65
|
+
nodes/models.py,sha256=S2Gc68Q2_8r8RO1ZKxASN5mEOj1q4UWtxdum5A-p0-4,71693
|
|
66
66
|
nodes/reports.py,sha256=NRYh3Y0SlZFhx31Zh2K03yO12ZrpxEHEY6T-dODA6WE,12059
|
|
67
|
-
nodes/rfid_sync.py,sha256=
|
|
67
|
+
nodes/rfid_sync.py,sha256=oeblawcp6xeLApdIuhsJS83OAk58Eu7pVVmgpAc0Nt8,6953
|
|
68
68
|
nodes/signals.py,sha256=PtOKdQfb08mV1LgSZvn7ZAcfOyy2c3Xkq4AOpBQyUdE,622
|
|
69
|
-
nodes/tasks.py,sha256=
|
|
70
|
-
nodes/tests.py,sha256=
|
|
71
|
-
nodes/urls.py,sha256
|
|
69
|
+
nodes/tasks.py,sha256=7m9pKO-iI6JDdfPQ-GWRGown4mdyKrcroOnhbiWN7dY,5246
|
|
70
|
+
nodes/tests.py,sha256=OS9yI940avVZNNE9tn0i-Tr-BnyHKiUFKz0O1adNsUQ,176567
|
|
71
|
+
nodes/urls.py,sha256=-o9_pLo6XHerKMQwL0TW80wm6wmtVZqyNWcUhpdq9vk,915
|
|
72
72
|
nodes/utils.py,sha256=wt7UuSXGuq79A-g-B6EW3kK49QWJBb7zhhkw4pun4k8,4474
|
|
73
|
-
nodes/views.py,sha256=
|
|
73
|
+
nodes/views.py,sha256=ZaUlTLQUaRbQsiLCfTuwigxXSpoeUqBkrs938KsjmjA,36962
|
|
74
74
|
ocpp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
75
|
-
ocpp/admin.py,sha256=
|
|
75
|
+
ocpp/admin.py,sha256=1X2g-ICBJyp2Queh8Bexu8vXex6qU9onWN5yruczZ0s,38318
|
|
76
76
|
ocpp/apps.py,sha256=i3NqrmIamNEQBT33CIqh7HOSOPmJXCMKrZ-DUd3whqg,842
|
|
77
|
-
ocpp/consumers.py,sha256=
|
|
77
|
+
ocpp/consumers.py,sha256=wt9tauYSlpWnGbaGsJKIPt4D13HyloLIHwmlK8iqA0Q,71674
|
|
78
78
|
ocpp/evcs.py,sha256=q1mZrCVSZxXTrtYsDqH6lkeEcJ6tfSC7p9YxkDmpSCw,28883
|
|
79
79
|
ocpp/evcs_discovery.py,sha256=OmrzgaOHwveDRJs8AIhrM3apX8_k2PPXh_oYaYpNW3c,3876
|
|
80
|
-
ocpp/models.py,sha256=
|
|
80
|
+
ocpp/models.py,sha256=wUGYSh382e_zYXq059GCaWqUCjQTTvYbbt0PdlONMFc,34411
|
|
81
81
|
ocpp/reference_utils.py,sha256=_UR82GfE93kv4766mHyVIfdhhyYvrT59660r3H6W55M,1072
|
|
82
82
|
ocpp/routing.py,sha256=3kQya-MdJ00778xDmX0esQLBP05P200V45asg-CGNoo,438
|
|
83
83
|
ocpp/simulator.py,sha256=vnyd59QffT79AaPhmfM_jipni_nqfG57X5tXyx1rBoc,28016
|
|
84
84
|
ocpp/status_display.py,sha256=YGFosd5HJETA0DcLdsjvx6EfhZSnI8Aa3cMnHG2WsBE,939
|
|
85
85
|
ocpp/store.py,sha256=gLCSaP9KKF7li2ALlE3O3RW5eVJtoe-_YHfKhdf0VOM,18943
|
|
86
|
-
ocpp/tasks.py,sha256=
|
|
87
|
-
ocpp/test_export_import.py,sha256=
|
|
88
|
-
ocpp/test_rfid.py,sha256=
|
|
89
|
-
ocpp/tests.py,sha256=
|
|
90
|
-
ocpp/transactions_io.py,sha256=
|
|
91
|
-
ocpp/urls.py,sha256=
|
|
92
|
-
ocpp/views.py,sha256=
|
|
86
|
+
ocpp/tasks.py,sha256=n2axf1Oo7brZtXRe-uCwt6K0f57ZlHUNvp6c5W0Gdzo,6035
|
|
87
|
+
ocpp/test_export_import.py,sha256=ouQbTCp4mxfqoK6gondlu3PPcyrT9jSbWAX5gqqgaNk,4561
|
|
88
|
+
ocpp/test_rfid.py,sha256=IhFSlvsI8A8D3S32sRE298nYfrmqxbv7GfVErtNU3DQ,39137
|
|
89
|
+
ocpp/tests.py,sha256=8Tzb_agTQdLdHnccAGCTfYbcRQHznz5DJI3Hb94mg5M,190065
|
|
90
|
+
ocpp/transactions_io.py,sha256=p2aUsKlCDYnZ4ZBrOM7pxXoW_w3Tbm-tvRFSjnR3x24,7738
|
|
91
|
+
ocpp/urls.py,sha256=5ZomUtznJe3kfs8E-DtVp12eFva5jUuJdpTEczIsQ5w,1730
|
|
92
|
+
ocpp/views.py,sha256=G2y2g55XJBJ0gzZPUqA4pplKfQIrO3-fjH5CkOYqDsw,63152
|
|
93
93
|
pages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
94
94
|
pages/admin.py,sha256=E-jcxUI-89gFgqGI38HMSHbfDw1inpSUiRR83utJ0fc,32512
|
|
95
95
|
pages/apps.py,sha256=o8gQP-VdZOk9LXIEo6IDmOSqX3TP8XypBvKGGWLoQ0k,351
|
|
96
96
|
pages/checks.py,sha256=sM8_hUVM_HOIocvtTb2sY3AaSEvbTnOlO46UchGVd-0,1527
|
|
97
|
-
pages/context_processors.py,sha256=
|
|
98
|
-
pages/defaults.py,sha256=
|
|
97
|
+
pages/context_processors.py,sha256=0Ie3_lPY6-j6f8eXtSzMlWpprMzY0tvX9fcOdpJr6Vs,5788
|
|
98
|
+
pages/defaults.py,sha256=3tjv3nFPxwpFu6poJ1Ez1MP92Q6ZvyRluftKHlU-zeI,522
|
|
99
99
|
pages/forms.py,sha256=T0atqxdNds3IBP8N-9c5-ACf3iR9FzzmhzK4MOa24e8,7058
|
|
100
100
|
pages/middleware.py,sha256=MYd5Nko4AnFg3orY6MuyvvNg_I6GCIf8mDW8znSOgvQ,7042
|
|
101
101
|
pages/models.py,sha256=Ms_m_tzzstNghN_JOzyfwsbllDBIl_AKnYdTRTJthqM,22173
|
|
102
|
-
pages/module_defaults.py,sha256=
|
|
102
|
+
pages/module_defaults.py,sha256=rCAY8aTyxYNL0M5zDr393rX-Gi-svXqKtuLXm0rILrQ,5444
|
|
103
103
|
pages/site_config.py,sha256=f1Me0GFdHeGbIeyMlQNzD2e6hym59YHqbz92U_ppffY,4057
|
|
104
104
|
pages/tasks.py,sha256=ivcba_3wSQ1-cku0oDplzw6vLeQ9hBq3R4TG-LmR5gs,1913
|
|
105
|
-
pages/tests.py,sha256=
|
|
106
|
-
pages/urls.py,sha256=
|
|
105
|
+
pages/tests.py,sha256=rgMYFiei8ONBNts4vJzukjaWN-WUGLxr6LFwtSHJQ_8,141550
|
|
106
|
+
pages/urls.py,sha256=rcK0zEJTSCurYhx77ZK_GVVMSHTS5ZJWoKTJkh1lj-M,1305
|
|
107
107
|
pages/utils.py,sha256=CR4D1debgJLGgXsw9kap2ggpe7fIpSoWS_ivbgMNp2k,564
|
|
108
|
-
pages/views.py,sha256=
|
|
109
|
-
arthexis-0.1.
|
|
110
|
-
arthexis-0.1.
|
|
111
|
-
arthexis-0.1.
|
|
112
|
-
arthexis-0.1.
|
|
108
|
+
pages/views.py,sha256=2ik28HW70a0S84K8hMQ8qL9yzVH1WggXT7S8G5ZaLgA,50940
|
|
109
|
+
arthexis-0.1.20.dist-info/METADATA,sha256=RLz9veHKDPM6UzcxnJgZcxj3ri6TuTPVLSK9QBn48mM,11751
|
|
110
|
+
arthexis-0.1.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
111
|
+
arthexis-0.1.20.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
|
|
112
|
+
arthexis-0.1.20.dist-info/RECORD,,
|
core/admin.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
1
2
|
from io import BytesIO
|
|
2
3
|
import os
|
|
3
4
|
|
|
@@ -2909,7 +2910,7 @@ class CopyRFIDForm(forms.Form):
|
|
|
2909
2910
|
normalized = (cleaned or "").strip().upper()
|
|
2910
2911
|
if not normalized:
|
|
2911
2912
|
raise forms.ValidationError(_("RFID value is required."))
|
|
2912
|
-
if RFID.
|
|
2913
|
+
if RFID.matching_queryset(normalized).exists():
|
|
2913
2914
|
raise forms.ValidationError(
|
|
2914
2915
|
_("An RFID with this value already exists.")
|
|
2915
2916
|
)
|
|
@@ -2943,6 +2944,7 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
|
|
|
2943
2944
|
"print_card_labels",
|
|
2944
2945
|
"print_release_form",
|
|
2945
2946
|
"copy_rfids",
|
|
2947
|
+
"merge_rfids",
|
|
2946
2948
|
"toggle_selected_user_data",
|
|
2947
2949
|
"toggle_selected_released",
|
|
2948
2950
|
"toggle_selected_allowed",
|
|
@@ -3177,6 +3179,145 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
|
|
|
3177
3179
|
context["media"] = self.media + form.media
|
|
3178
3180
|
return TemplateResponse(request, "admin/core/rfid/copy.html", context)
|
|
3179
3181
|
|
|
3182
|
+
@admin.action(description=_("Merge RFID cards"))
|
|
3183
|
+
def merge_rfids(self, request, queryset):
|
|
3184
|
+
tags = list(queryset.prefetch_related("energy_accounts"))
|
|
3185
|
+
if len(tags) < 2:
|
|
3186
|
+
self.message_user(
|
|
3187
|
+
request,
|
|
3188
|
+
_("Select at least two RFIDs to merge."),
|
|
3189
|
+
level=messages.WARNING,
|
|
3190
|
+
)
|
|
3191
|
+
return None
|
|
3192
|
+
|
|
3193
|
+
normalized_map: dict[int, str] = {}
|
|
3194
|
+
groups: defaultdict[str, list[RFID]] = defaultdict(list)
|
|
3195
|
+
unmatched = 0
|
|
3196
|
+
for tag in tags:
|
|
3197
|
+
normalized = RFID.normalize_code(tag.rfid)
|
|
3198
|
+
normalized_map[tag.pk] = normalized
|
|
3199
|
+
if not normalized:
|
|
3200
|
+
unmatched += 1
|
|
3201
|
+
continue
|
|
3202
|
+
prefix = normalized[: RFID.MATCH_PREFIX_LENGTH]
|
|
3203
|
+
groups[prefix].append(tag)
|
|
3204
|
+
|
|
3205
|
+
merge_groups: list[list[RFID]] = []
|
|
3206
|
+
skipped = unmatched
|
|
3207
|
+
for prefix, group in groups.items():
|
|
3208
|
+
if len(group) < 2:
|
|
3209
|
+
skipped += len(group)
|
|
3210
|
+
continue
|
|
3211
|
+
group.sort(
|
|
3212
|
+
key=lambda item: (
|
|
3213
|
+
len(normalized_map.get(item.pk, "")),
|
|
3214
|
+
normalized_map.get(item.pk, ""),
|
|
3215
|
+
item.pk,
|
|
3216
|
+
)
|
|
3217
|
+
)
|
|
3218
|
+
merge_groups.append(group)
|
|
3219
|
+
|
|
3220
|
+
if not merge_groups:
|
|
3221
|
+
self.message_user(
|
|
3222
|
+
request,
|
|
3223
|
+
_("No matching RFIDs were found to merge."),
|
|
3224
|
+
level=messages.WARNING,
|
|
3225
|
+
)
|
|
3226
|
+
return None
|
|
3227
|
+
|
|
3228
|
+
merged_tags = 0
|
|
3229
|
+
merged_groups = 0
|
|
3230
|
+
conflicting_accounts = 0
|
|
3231
|
+
with transaction.atomic():
|
|
3232
|
+
for group in merge_groups:
|
|
3233
|
+
canonical = group[0]
|
|
3234
|
+
update_fields: set[str] = set()
|
|
3235
|
+
existing_account_ids = set(
|
|
3236
|
+
canonical.energy_accounts.values_list("pk", flat=True)
|
|
3237
|
+
)
|
|
3238
|
+
for tag in group[1:]:
|
|
3239
|
+
other_value = normalized_map.get(tag.pk, "")
|
|
3240
|
+
if canonical.adopt_rfid(other_value):
|
|
3241
|
+
update_fields.add("rfid")
|
|
3242
|
+
normalized_map[canonical.pk] = RFID.normalize_code(
|
|
3243
|
+
canonical.rfid
|
|
3244
|
+
)
|
|
3245
|
+
accounts = list(tag.energy_accounts.all())
|
|
3246
|
+
if accounts:
|
|
3247
|
+
transferable: list[EnergyAccount] = []
|
|
3248
|
+
for account in accounts:
|
|
3249
|
+
if existing_account_ids and account.pk not in existing_account_ids:
|
|
3250
|
+
conflicting_accounts += 1
|
|
3251
|
+
continue
|
|
3252
|
+
transferable.append(account)
|
|
3253
|
+
if transferable:
|
|
3254
|
+
canonical.energy_accounts.add(*transferable)
|
|
3255
|
+
existing_account_ids.update(
|
|
3256
|
+
account.pk for account in transferable
|
|
3257
|
+
)
|
|
3258
|
+
if tag.allowed and not canonical.allowed:
|
|
3259
|
+
canonical.allowed = True
|
|
3260
|
+
update_fields.add("allowed")
|
|
3261
|
+
if tag.released and not canonical.released:
|
|
3262
|
+
canonical.released = True
|
|
3263
|
+
update_fields.add("released")
|
|
3264
|
+
if tag.key_a_verified and not canonical.key_a_verified:
|
|
3265
|
+
canonical.key_a_verified = True
|
|
3266
|
+
update_fields.add("key_a_verified")
|
|
3267
|
+
if tag.key_b_verified and not canonical.key_b_verified:
|
|
3268
|
+
canonical.key_b_verified = True
|
|
3269
|
+
update_fields.add("key_b_verified")
|
|
3270
|
+
if tag.last_seen_on and (
|
|
3271
|
+
not canonical.last_seen_on
|
|
3272
|
+
or tag.last_seen_on > canonical.last_seen_on
|
|
3273
|
+
):
|
|
3274
|
+
canonical.last_seen_on = tag.last_seen_on
|
|
3275
|
+
update_fields.add("last_seen_on")
|
|
3276
|
+
if not canonical.origin_node and tag.origin_node_id:
|
|
3277
|
+
canonical.origin_node = tag.origin_node
|
|
3278
|
+
update_fields.add("origin_node")
|
|
3279
|
+
merged_tags += 1
|
|
3280
|
+
tag.delete()
|
|
3281
|
+
if update_fields:
|
|
3282
|
+
canonical.save(update_fields=sorted(update_fields))
|
|
3283
|
+
merged_groups += 1
|
|
3284
|
+
|
|
3285
|
+
if merged_tags:
|
|
3286
|
+
self.message_user(
|
|
3287
|
+
request,
|
|
3288
|
+
ngettext(
|
|
3289
|
+
"Merged %(removed)d RFID into %(groups)d canonical record.",
|
|
3290
|
+
"Merged %(removed)d RFIDs into %(groups)d canonical records.",
|
|
3291
|
+
merged_tags,
|
|
3292
|
+
)
|
|
3293
|
+
% {"removed": merged_tags, "groups": merged_groups},
|
|
3294
|
+
level=messages.SUCCESS,
|
|
3295
|
+
)
|
|
3296
|
+
|
|
3297
|
+
if skipped:
|
|
3298
|
+
self.message_user(
|
|
3299
|
+
request,
|
|
3300
|
+
ngettext(
|
|
3301
|
+
"Skipped %(count)d RFID because it did not share the first %(length)d characters with another selection.",
|
|
3302
|
+
"Skipped %(count)d RFIDs because they did not share the first %(length)d characters with another selection.",
|
|
3303
|
+
skipped,
|
|
3304
|
+
)
|
|
3305
|
+
% {"count": skipped, "length": RFID.MATCH_PREFIX_LENGTH},
|
|
3306
|
+
level=messages.WARNING,
|
|
3307
|
+
)
|
|
3308
|
+
|
|
3309
|
+
if conflicting_accounts:
|
|
3310
|
+
self.message_user(
|
|
3311
|
+
request,
|
|
3312
|
+
ngettext(
|
|
3313
|
+
"Skipped %(count)d energy account because the RFID was already linked to a different account.",
|
|
3314
|
+
"Skipped %(count)d energy accounts because the RFID was already linked to a different account.",
|
|
3315
|
+
conflicting_accounts,
|
|
3316
|
+
)
|
|
3317
|
+
% {"count": conflicting_accounts},
|
|
3318
|
+
level=messages.WARNING,
|
|
3319
|
+
)
|
|
3320
|
+
|
|
3180
3321
|
def _render_card_labels(
|
|
3181
3322
|
self,
|
|
3182
3323
|
request,
|
core/backends.py
CHANGED
|
@@ -81,10 +81,16 @@ class RFIDBackend:
|
|
|
81
81
|
if not rfid_value:
|
|
82
82
|
return None
|
|
83
83
|
|
|
84
|
-
tag = RFID.
|
|
85
|
-
if not tag
|
|
84
|
+
tag = RFID.matching_queryset(rfid_value).filter(allowed=True).first()
|
|
85
|
+
if not tag:
|
|
86
86
|
return None
|
|
87
87
|
|
|
88
|
+
update_fields: list[str] = []
|
|
89
|
+
if tag.adopt_rfid(rfid_value):
|
|
90
|
+
update_fields.append("rfid")
|
|
91
|
+
if update_fields:
|
|
92
|
+
tag.save(update_fields=update_fields)
|
|
93
|
+
|
|
88
94
|
command = (tag.external_command or "").strip()
|
|
89
95
|
if command:
|
|
90
96
|
env = os.environ.copy()
|
core/environment.py
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import re
|
|
5
|
+
import shlex
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
4
8
|
|
|
9
|
+
from django import forms
|
|
5
10
|
from django.conf import settings
|
|
6
11
|
from django.contrib import admin
|
|
12
|
+
from django.core.exceptions import PermissionDenied
|
|
7
13
|
from django.template.response import TemplateResponse
|
|
8
|
-
from django.urls import path
|
|
14
|
+
from django.urls import path, reverse
|
|
9
15
|
from django.utils.translation import gettext_lazy as _
|
|
10
16
|
|
|
11
17
|
|
|
@@ -15,23 +21,229 @@ def _get_django_settings():
|
|
|
15
21
|
)
|
|
16
22
|
|
|
17
23
|
|
|
24
|
+
class NetworkSetupForm(forms.Form):
|
|
25
|
+
prompt_for_password = forms.BooleanField(
|
|
26
|
+
label=_("Prompt for new WiFi password"),
|
|
27
|
+
required=False,
|
|
28
|
+
help_text=_("Add --password to request a password even when one is already configured."),
|
|
29
|
+
)
|
|
30
|
+
access_point_name = forms.CharField(
|
|
31
|
+
label=_("Access point name"),
|
|
32
|
+
required=False,
|
|
33
|
+
max_length=32,
|
|
34
|
+
help_text=_("Use --ap to set the wlan0 access point name."),
|
|
35
|
+
)
|
|
36
|
+
skip_firewall_validation = forms.BooleanField(
|
|
37
|
+
label=_("Skip firewall validation"),
|
|
38
|
+
required=False,
|
|
39
|
+
help_text=_("Add --no-firewall to bypass firewall port checks."),
|
|
40
|
+
)
|
|
41
|
+
skip_access_point_configuration = forms.BooleanField(
|
|
42
|
+
label=_("Skip access point configuration"),
|
|
43
|
+
required=False,
|
|
44
|
+
help_text=_("Add --no-ap to leave the access point configuration unchanged."),
|
|
45
|
+
)
|
|
46
|
+
allow_unsafe_changes = forms.BooleanField(
|
|
47
|
+
label=_("Allow modifying the active internet connection"),
|
|
48
|
+
required=False,
|
|
49
|
+
help_text=_("Include --unsafe to allow changes that may interrupt connectivity."),
|
|
50
|
+
)
|
|
51
|
+
interactive = forms.BooleanField(
|
|
52
|
+
label=_("Prompt before each step"),
|
|
53
|
+
required=False,
|
|
54
|
+
help_text=_("Run the script with --interactive to confirm each action."),
|
|
55
|
+
)
|
|
56
|
+
install_watchdog = forms.BooleanField(
|
|
57
|
+
label=_("Install WiFi watchdog service"),
|
|
58
|
+
required=False,
|
|
59
|
+
initial=True,
|
|
60
|
+
help_text=_("Keep selected to retain the watchdog or clear to add --no-watchdog."),
|
|
61
|
+
)
|
|
62
|
+
vnc_validation = forms.ChoiceField(
|
|
63
|
+
label=_("VNC validation"),
|
|
64
|
+
choices=(
|
|
65
|
+
("default", _("Use script default (skip validation)")),
|
|
66
|
+
("require", _("Require that a VNC service is enabled (--vnc)")),
|
|
67
|
+
),
|
|
68
|
+
initial="default",
|
|
69
|
+
required=True,
|
|
70
|
+
)
|
|
71
|
+
ethernet_subnet = forms.CharField(
|
|
72
|
+
label=_("Ethernet subnet"),
|
|
73
|
+
required=False,
|
|
74
|
+
help_text=_("Provide N or N/P (prefix 16 or 24) to supply --subnet."),
|
|
75
|
+
)
|
|
76
|
+
update_ap_password_only = forms.BooleanField(
|
|
77
|
+
label=_("Update access point password only"),
|
|
78
|
+
required=False,
|
|
79
|
+
help_text=_("Use --ap-set-password without running other setup steps."),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def clean_ethernet_subnet(self) -> str:
|
|
83
|
+
value = self.cleaned_data.get("ethernet_subnet", "")
|
|
84
|
+
if not value:
|
|
85
|
+
return ""
|
|
86
|
+
raw = value.strip()
|
|
87
|
+
match = re.fullmatch(r"(?P<subnet>\d{1,3})(?:/(?P<prefix>\d{1,2}))?", raw)
|
|
88
|
+
if not match:
|
|
89
|
+
raise forms.ValidationError(
|
|
90
|
+
_("Enter a subnet in the form N or N/P with prefix 16 or 24."),
|
|
91
|
+
)
|
|
92
|
+
subnet = int(match.group("subnet"))
|
|
93
|
+
if subnet < 0 or subnet > 254:
|
|
94
|
+
raise forms.ValidationError(
|
|
95
|
+
_("Subnet value must be between 0 and 254."),
|
|
96
|
+
)
|
|
97
|
+
prefix_value = match.group("prefix")
|
|
98
|
+
if prefix_value:
|
|
99
|
+
prefix = int(prefix_value)
|
|
100
|
+
if prefix not in {16, 24}:
|
|
101
|
+
raise forms.ValidationError(
|
|
102
|
+
_("Subnet prefix must be 16 or 24."),
|
|
103
|
+
)
|
|
104
|
+
return f"{subnet}/{prefix}"
|
|
105
|
+
return str(subnet)
|
|
106
|
+
|
|
107
|
+
def clean(self) -> dict:
|
|
108
|
+
cleaned_data = super().clean()
|
|
109
|
+
if cleaned_data.get("update_ap_password_only"):
|
|
110
|
+
other_flags = [
|
|
111
|
+
cleaned_data.get("prompt_for_password"),
|
|
112
|
+
bool(cleaned_data.get("access_point_name")),
|
|
113
|
+
cleaned_data.get("skip_firewall_validation"),
|
|
114
|
+
cleaned_data.get("skip_access_point_configuration"),
|
|
115
|
+
cleaned_data.get("allow_unsafe_changes"),
|
|
116
|
+
cleaned_data.get("interactive"),
|
|
117
|
+
bool(cleaned_data.get("ethernet_subnet")),
|
|
118
|
+
cleaned_data.get("vnc_validation") == "require",
|
|
119
|
+
not cleaned_data.get("install_watchdog", True),
|
|
120
|
+
]
|
|
121
|
+
if any(other_flags):
|
|
122
|
+
raise forms.ValidationError(
|
|
123
|
+
_(
|
|
124
|
+
"Update access point password only cannot be combined with other network-setup options."
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
return cleaned_data
|
|
128
|
+
|
|
129
|
+
def build_command(self, script_path: Path) -> list[str]:
|
|
130
|
+
command = [str(script_path)]
|
|
131
|
+
data = self.cleaned_data
|
|
132
|
+
if data.get("update_ap_password_only"):
|
|
133
|
+
command.append("--ap-set-password")
|
|
134
|
+
return command
|
|
135
|
+
if data.get("prompt_for_password"):
|
|
136
|
+
command.append("--password")
|
|
137
|
+
access_point_name = data.get("access_point_name")
|
|
138
|
+
if access_point_name:
|
|
139
|
+
command.extend(["--ap", access_point_name])
|
|
140
|
+
if data.get("skip_firewall_validation"):
|
|
141
|
+
command.append("--no-firewall")
|
|
142
|
+
if data.get("skip_access_point_configuration"):
|
|
143
|
+
command.append("--no-ap")
|
|
144
|
+
if data.get("allow_unsafe_changes"):
|
|
145
|
+
command.append("--unsafe")
|
|
146
|
+
if data.get("interactive"):
|
|
147
|
+
command.append("--interactive")
|
|
148
|
+
if not data.get("install_watchdog"):
|
|
149
|
+
command.append("--no-watchdog")
|
|
150
|
+
if data.get("vnc_validation") == "require":
|
|
151
|
+
command.append("--vnc")
|
|
152
|
+
ethernet_subnet = data.get("ethernet_subnet")
|
|
153
|
+
if ethernet_subnet:
|
|
154
|
+
command.extend(["--subnet", ethernet_subnet])
|
|
155
|
+
return command
|
|
156
|
+
|
|
157
|
+
|
|
18
158
|
def _environment_view(request):
|
|
19
159
|
env_vars = sorted(os.environ.items())
|
|
20
160
|
context = admin.site.each_context(request)
|
|
161
|
+
environment_tasks: list[dict[str, str]] = []
|
|
162
|
+
if request.user.is_superuser:
|
|
163
|
+
environment_tasks.append(
|
|
164
|
+
{
|
|
165
|
+
"name": _("Run network-setup"),
|
|
166
|
+
"description": _(
|
|
167
|
+
"Configure network services, stage managed NGINX sites, and review script output."
|
|
168
|
+
),
|
|
169
|
+
"url": reverse("admin:environment-network-setup"),
|
|
170
|
+
}
|
|
171
|
+
)
|
|
21
172
|
context.update(
|
|
22
173
|
{
|
|
23
|
-
"title": _("
|
|
174
|
+
"title": _("Environment"),
|
|
24
175
|
"env_vars": env_vars,
|
|
176
|
+
"environment_tasks": environment_tasks,
|
|
25
177
|
}
|
|
26
178
|
)
|
|
27
179
|
return TemplateResponse(request, "admin/environment.html", context)
|
|
28
180
|
|
|
29
181
|
|
|
182
|
+
def _environment_network_setup_view(request):
|
|
183
|
+
if not request.user.is_superuser:
|
|
184
|
+
raise PermissionDenied
|
|
185
|
+
|
|
186
|
+
script_path = Path(settings.BASE_DIR) / "network-setup.sh"
|
|
187
|
+
command_result: dict[str, object] | None = None
|
|
188
|
+
|
|
189
|
+
if request.method == "POST":
|
|
190
|
+
form = NetworkSetupForm(request.POST)
|
|
191
|
+
if form.is_valid():
|
|
192
|
+
command = form.build_command(script_path)
|
|
193
|
+
if not script_path.exists():
|
|
194
|
+
form.add_error(None, _("The network-setup.sh script could not be found."))
|
|
195
|
+
else:
|
|
196
|
+
try:
|
|
197
|
+
completed = subprocess.run(
|
|
198
|
+
command,
|
|
199
|
+
capture_output=True,
|
|
200
|
+
text=True,
|
|
201
|
+
cwd=settings.BASE_DIR,
|
|
202
|
+
check=False,
|
|
203
|
+
)
|
|
204
|
+
except FileNotFoundError:
|
|
205
|
+
form.add_error(None, _("The network-setup.sh script could not be executed."))
|
|
206
|
+
except OSError as exc:
|
|
207
|
+
form.add_error(
|
|
208
|
+
None,
|
|
209
|
+
_("Unable to execute network-setup.sh: %(error)s")
|
|
210
|
+
% {"error": str(exc)},
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
if hasattr(shlex, "join"):
|
|
214
|
+
command_display = shlex.join(command)
|
|
215
|
+
else:
|
|
216
|
+
command_display = " ".join(shlex.quote(part) for part in command)
|
|
217
|
+
command_result = {
|
|
218
|
+
"command": command_display,
|
|
219
|
+
"stdout": completed.stdout,
|
|
220
|
+
"stderr": completed.stderr,
|
|
221
|
+
"returncode": completed.returncode,
|
|
222
|
+
"succeeded": completed.returncode == 0,
|
|
223
|
+
}
|
|
224
|
+
else:
|
|
225
|
+
form = NetworkSetupForm()
|
|
226
|
+
|
|
227
|
+
context = admin.site.each_context(request)
|
|
228
|
+
context.update(
|
|
229
|
+
{
|
|
230
|
+
"title": _("Run network-setup"),
|
|
231
|
+
"form": form,
|
|
232
|
+
"command_result": command_result,
|
|
233
|
+
"task_description": _(
|
|
234
|
+
"Configure script flags, execute network-setup, and review the captured output."
|
|
235
|
+
),
|
|
236
|
+
"back_url": reverse("admin:environment"),
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
return TemplateResponse(request, "admin/environment_network_setup.html", context)
|
|
240
|
+
|
|
241
|
+
|
|
30
242
|
def _config_view(request):
|
|
31
243
|
context = admin.site.each_context(request)
|
|
32
244
|
context.update(
|
|
33
245
|
{
|
|
34
|
-
"title": _("
|
|
246
|
+
"title": _("Django Settings"),
|
|
35
247
|
"django_settings": _get_django_settings(),
|
|
36
248
|
}
|
|
37
249
|
)
|
|
@@ -39,12 +251,17 @@ def _config_view(request):
|
|
|
39
251
|
|
|
40
252
|
|
|
41
253
|
def patch_admin_environment_view() -> None:
|
|
42
|
-
"""
|
|
254
|
+
"""Register the Environment and Config admin views on the main admin site."""
|
|
43
255
|
original_get_urls = admin.site.get_urls
|
|
44
256
|
|
|
45
257
|
def get_urls():
|
|
46
258
|
urls = original_get_urls()
|
|
47
259
|
custom = [
|
|
260
|
+
path(
|
|
261
|
+
"environment/network-setup/",
|
|
262
|
+
admin.site.admin_view(_environment_network_setup_view),
|
|
263
|
+
name="environment-network-setup",
|
|
264
|
+
),
|
|
48
265
|
path(
|
|
49
266
|
"environment/",
|
|
50
267
|
admin.site.admin_view(_environment_view),
|