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

Files changed (51) hide show
  1. {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/METADATA +76 -23
  2. arthexis-0.1.11.dist-info/RECORD +99 -0
  3. config/context_processors.py +1 -0
  4. config/settings.py +245 -26
  5. config/urls.py +11 -4
  6. core/admin.py +585 -57
  7. core/apps.py +29 -1
  8. core/auto_upgrade.py +57 -0
  9. core/backends.py +115 -3
  10. core/environment.py +23 -5
  11. core/fields.py +93 -0
  12. core/mailer.py +3 -1
  13. core/models.py +482 -38
  14. core/reference_utils.py +108 -0
  15. core/sigil_builder.py +23 -5
  16. core/sigil_resolver.py +35 -4
  17. core/system.py +400 -140
  18. core/tasks.py +151 -8
  19. core/temp_passwords.py +181 -0
  20. core/test_system_info.py +97 -1
  21. core/tests.py +393 -15
  22. core/user_data.py +154 -16
  23. core/views.py +499 -20
  24. nodes/admin.py +149 -6
  25. nodes/backends.py +125 -18
  26. nodes/dns.py +203 -0
  27. nodes/models.py +498 -9
  28. nodes/tests.py +682 -3
  29. nodes/views.py +154 -7
  30. ocpp/admin.py +63 -3
  31. ocpp/consumers.py +255 -41
  32. ocpp/evcs.py +6 -3
  33. ocpp/models.py +52 -7
  34. ocpp/reference_utils.py +42 -0
  35. ocpp/simulator.py +62 -5
  36. ocpp/store.py +30 -0
  37. ocpp/test_rfid.py +169 -7
  38. ocpp/tests.py +414 -8
  39. ocpp/views.py +109 -76
  40. pages/admin.py +9 -1
  41. pages/context_processors.py +24 -4
  42. pages/defaults.py +14 -0
  43. pages/forms.py +131 -0
  44. pages/models.py +53 -14
  45. pages/tests.py +450 -14
  46. pages/urls.py +4 -0
  47. pages/views.py +419 -110
  48. arthexis-0.1.9.dist-info/RECORD +0 -92
  49. {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/WHEEL +0 -0
  50. {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/licenses/LICENSE +0 -0
  51. {arthexis-0.1.9.dist-info → arthexis-0.1.11.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.9
3
+ Version: 0.1.11
4
4
  Summary: Django-based MESH system
5
5
  Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
6
6
  License-Expression: GPL-3.0-only
@@ -43,6 +43,7 @@ Requires-Dist: django-celery-beat==2.8.1
43
43
  Requires-Dist: django-debug-toolbar==6.0.0
44
44
  Requires-Dist: django-import-export==4.3.9
45
45
  Requires-Dist: django-object-actions==5.0.0
46
+ Requires-Dist: django-otp==1.5.4
46
47
  Requires-Dist: django-timezone-field==7.1
47
48
  Requires-Dist: dnspython==2.7.0
48
49
  Requires-Dist: docutils==0.22
@@ -57,12 +58,14 @@ Requires-Dist: incremental==24.7.2
57
58
  Requires-Dist: kombu==5.5.4
58
59
  Requires-Dist: libipld==3.1.1
59
60
  Requires-Dist: Markdown==3.8.2
61
+ Requires-Dist: mdx_truly_sane_lists==1.3
60
62
  Requires-Dist: mcp==1.14.0
61
63
  Requires-Dist: mfrc522==0.0.7; sys_platform == "linux"
62
64
  Requires-Dist: outcome==1.3.0.post0
63
65
  Requires-Dist: packaging==25.0
64
66
  Requires-Dist: pillow==11.3.0
65
67
  Requires-Dist: prompt_toolkit==3.0.51
68
+ Requires-Dist: psutil==5.9.8
66
69
  Requires-Dist: psycopg==3.2.9
67
70
  Requires-Dist: psycopg-binary==3.2.9
68
71
  Requires-Dist: pyasn1==0.6.1
@@ -112,50 +115,100 @@ Dynamic: license-file
112
115
 
113
116
  # Arthexis Constellation
114
117
 
118
+ [![Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/coverage.svg)](https://github.com/arthexis/arthexis/actions/workflows/coverage.yml) [![OCPP 1.6 Coverage](https://raw.githubusercontent.com/arthexis/arthexis/main/ocpp_coverage.svg)](https://raw.githubusercontent.com/arthexis/arthexis/main/ocpp_coverage.svg)
119
+
115
120
  ## Purpose
116
121
 
117
122
  Arthexis Constellation is a [narrative-driven](https://en.wikipedia.org/wiki/Narrative) [Django](https://www.djangoproject.com/)-based [software suite](https://en.wikipedia.org/wiki/Software_suite) that centralizes tools for managing [electric vehicle charging infrastructure](https://en.wikipedia.org/wiki/Charging_station) and orchestrating [energy](https://en.wikipedia.org/wiki/Energy)-related [products](https://en.wikipedia.org/wiki/Product_(business)) and [services](https://en.wikipedia.org/wiki/Service_(economics)).
118
123
 
119
- ## Features
120
-
121
- - Compatible with the [Open Charge Point Protocol (OCPP) 1.6](https://www.openchargealliance.org/protocols/ocpp-16/)
122
- - [API](https://en.wikipedia.org/wiki/API) integration with [Odoo](https://www.odoo.com/) 1.6
124
+ ## Current Features
125
+
126
+ - Compatible with the [Open Charge Point Protocol (OCPP) 1.6](https://www.openchargealliance.org/protocols/ocpp-16/) central system, handling:
127
+ - Lifecycle & sessions
128
+ - `BootNotification`
129
+ - `Heartbeat`
130
+ - `StatusNotification`
131
+ - `StartTransaction`
132
+ - `StopTransaction`
133
+ - Access & metering
134
+ - `Authorize`
135
+ - `MeterValues`
136
+ - Maintenance & firmware
137
+ - `DiagnosticsStatusNotification`
138
+ - `FirmwareStatusNotification`
139
+ - [API](https://en.wikipedia.org/wiki/API) integration with [Odoo](https://www.odoo.com/), syncing:
140
+ - Employee credentials via `res.users`
141
+ - Product catalog lookups via `product.product`
123
142
  - Runs on [Windows 11](https://www.microsoft.com/windows/windows-11) and [Ubuntu 22.04 LTS](https://releases.ubuntu.com/22.04/)
124
143
  - Tested for the [Raspberry Pi 4 Model B](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/)
125
144
 
126
- Project under active development.
145
+ Project under rapid active and open development.
127
146
 
128
- ## Four Role Architecture
147
+ ## Role Architecture
129
148
 
130
149
  Arthexis Constellation ships in four node roles tailored to different deployment scenarios.
131
150
 
132
- | Role | Description & Common Features |
133
- | --- | --- |
134
- | Terminal | Single-User Research & Development<br>Features: GUI Toast |
135
- | Control | Single-Device Testing & Special Task Appliances<br>Features: AP Public Wi-Fi, Celery Queue, GUI Toast, LCD Screen, NGINX Server, RFID Scanner |
136
- | Satellite | Multi-Device Edge, Network & Data Acquisition<br>Features: AP Router, Celery Queue, NGINX Server, RFID Scanner |
137
- | Constellation | Multi-User Cloud & Orchestration<br>Features: Celery Queue, NGINX Server |
151
+ <table border="1" cellpadding="8" cellspacing="0">
152
+ <thead>
153
+ <tr>
154
+ <th align="left">Role</th>
155
+ <th align="left">Description &amp; Common Features</th>
156
+ </tr>
157
+ </thead>
158
+ <tbody>
159
+ <tr>
160
+ <td valign="top"><strong>Terminal</strong></td>
161
+ <td valign="top"><strong>Single-User Research &amp; Development</strong><br />Features: GUI Toast</td>
162
+ </tr>
163
+ <tr>
164
+ <td valign="top"><strong>Control</strong></td>
165
+ <td valign="top"><strong>Single-Device Testing &amp; Special Task Appliances</strong><br />Features: AP Public Wi-Fi, Celery Queue, GUI Toast, LCD Screen, NGINX Server, RFID Scanner</td>
166
+ </tr>
167
+ <tr>
168
+ <td valign="top"><strong>Satellite</strong></td>
169
+ <td valign="top"><strong>Multi-Device Edge, Network &amp; Data Acquisition</strong><br />Features: AP Router, Celery Queue, NGINX Server, RFID Scanner</td>
170
+ </tr>
171
+ <tr>
172
+ <td valign="top"><strong>Constellation</strong></td>
173
+ <td valign="top"><strong>Multi-User Cloud &amp; Orchestration</strong><br />Features: Celery Queue, NGINX Server</td>
174
+ </tr>
175
+ </tbody>
176
+ </table>
138
177
 
139
178
  ## Quick Guide
140
179
 
141
180
  ### 1. Clone
142
- - **[Linux](https://en.wikipedia.org/wiki/Linux)**: open a [terminal](https://en.wikipedia.org/wiki/Command-line_interface) and run
143
- `git clone https://github.com/arthexis/arthexis.git`
181
+ - **[Linux](https://en.wikipedia.org/wiki/Linux)**: open a [terminal](https://en.wikipedia.org/wiki/Command-line_interface) and run `git clone https://github.com/arthexis/arthexis.git`.
144
182
  - **[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.
145
183
 
146
184
  ### 2. Start and stop
147
- - **[VS Code](https://code.visualstudio.com/)**: open the folder, go to the
148
- **Run and Debug** panel (`Ctrl+Shift+D`), select the **Run Server** (or
149
- **Debug Server**) configuration, and press the green start button. Stop the
150
- server with the red square button (`Shift+F5`).
151
- - **[Shell](https://en.wikipedia.org/wiki/Shell_(computing))**: on Linux run [`./start.sh`](start.sh) and stop with [`./stop.sh`](stop.sh); on Windows run [`start.bat`](start.bat) and stop with `Ctrl+C`.
185
+ Terminal nodes can start directly with the scripts below without installing; Control, Satellite, and Constellation roles require installation first. Both approaches listen on [`http://localhost:8000/`](http://localhost:8000/) by default.
186
+
187
+ - **[VS Code](https://code.visualstudio.com/)**
188
+ - Open the folder and go to the **Run and Debug** panel (`Ctrl+Shift+D`).
189
+ - Select the **Run Server** (or **Debug Server**) configuration.
190
+ - Press the green start button. Stop the server with the red square button (`Shift+F5`).
191
+
192
+ - **[Shell](https://en.wikipedia.org/wiki/Shell_(computing))**
193
+ - Linux: run [`./start.sh`](start.sh) and stop with [`./stop.sh`](stop.sh).
194
+ - Windows: run [`start.bat`](start.bat) and stop with `Ctrl+C`.
152
195
 
153
196
  ### 3. Install and upgrade
154
- - **Linux**: use [`./install.sh`](install.sh) with options like `--service NAME`, `--public` or `--internal`, `--port PORT`, `--upgrade`, `--auto-upgrade`, `--latest`, `--celery`, `--lcd-screen`, `--no-lcd-screen`, `--clean`, `--datasette`. Upgrade with [`./upgrade.sh`](upgrade.sh) using flags such as `--latest`, `--clean`, or `--no-restart`.
155
- - **Windows**: run [`install.bat`](install.bat) to install and [`upgrade.bat`](upgrade.bat) to upgrade.
197
+ - **Linux:**
198
+ - Run [`./install.sh`](install.sh) with a node role flag:
199
+ - `--terminal` – default when unspecified and recommended if you're unsure. Terminal nodes can also use the start/stop scripts above without installing.
200
+ - `--control` – prepares the single-device testing appliance.
201
+ - `--satellite` – configures the edge data acquisition node.
202
+ - `--constellation` – enables the multi-user orchestration stack.
203
+ - Use `./install.sh --help` to list every available flag if you need to customize the node beyond the role defaults.
204
+ - Upgrade with [`./upgrade.sh`](upgrade.sh).
205
+
206
+ - **Windows:**
207
+ - Run [`install.bat`](install.bat) to install (Terminal role) and [`upgrade.bat`](upgrade.bat) to upgrade.
208
+ - Installation is not required to start in Terminal mode (the default).
156
209
 
157
210
  ### 4. Administration
158
- Visit [`http://localhost:8888/admin/`](http://localhost:8888/admin/) for the [Django admin](https://docs.djangoproject.com/en/stable/ref/contrib/admin/) and [`http://localhost:8888/admindocs/`](http://localhost:8888/admindocs/) for the [admindocs](https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/). Use port `8000` if you started with [`start.bat`](start.bat) or the `--public` option.
211
+ Visit [`http://localhost:8000/admin/`](http://localhost:8000/admin/) for the [Django admin](https://docs.djangoproject.com/en/stable/ref/contrib/admin/) and [`http://localhost:8000/admindocs/`](http://localhost:8000/admindocs/) for the [admindocs](https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/). Use `--port` with the start scripts or installer when you need to expose a different port.
159
212
 
160
213
  ## Support
161
214
 
@@ -0,0 +1,99 @@
1
+ arthexis-0.1.11.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
2
+ config/__init__.py,sha256=8_b7rx_-Xcuzu3Z7mSR94q3PAhjyYqLFQi3IOEz6hcI,108
3
+ config/active_app.py,sha256=MET_G7oHL7GkoSo3VkkMzymM-PwsSZazMLZxpgjFLTo,388
4
+ config/asgi.py,sha256=n09URedOmQ_59II3UCl3iodGSDWOuN_A8DFyfLjuylA,803
5
+ config/auth_app.py,sha256=2NkC_iYQxnpbv0gYxW4xp5DgQtdkVLpa-JzAF-638ZE,205
6
+ config/celery.py,sha256=qRTHgNYfT2OyIz4cW49YiiKgVgzLs2mSfAEQBgso0M4,743
7
+ config/context_processors.py,sha256=bjLSqbz7Qw6knPosIc4KNFEl5HsJHOe23htoNsul40E,2404
8
+ config/horologia_app.py,sha256=u1hTYcEmIqh82Gt5YNPvR5ta2MnVatELvD9ByFrCH1A,194
9
+ config/loadenv.py,sha256=bhFbHTbRJSkSwrFk3UInKEKQ8ZY-poatOGi7rC57YAI,298
10
+ config/logging.py,sha256=334jADN4dM5GNHaCWlYPOKYa5BhfxbsuejH_QDALG6g,1793
11
+ config/middleware.py,sha256=EvraDumepnKwCDswHGXb1mK7vud_dEEoZ4eh0IQ7fhQ,744
12
+ config/offline.py,sha256=mhQjCUzdOwSzZ6oLgPDJR48xaPIDzOi34ARUEz43seE,1431
13
+ config/settings.py,sha256=pJzArbxvM6Imyn4gs1YRn1WP3GaZThXFJM8ZxAAmNzg,22548
14
+ config/urls.py,sha256=MmbES50lTHyMZ6risgXAGfevncN7j4HC74jR4PX_5xY,5228
15
+ config/wsgi.py,sha256=Fu-ONO2SgYeU6rhmy909P-uLX-n8ALJQObdm9MHPS-k,450
16
+ core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ core/admin.py,sha256=MKubxItiTTtEOo2Lh8CqUvQ3KLBa80e5C5Y0gsMqhe4,93074
18
+ core/admin_history.py,sha256=NIDWkosJoHMaucBvUjq8VmmL-0e8ngJ4l4XA89d4jwQ,1833
19
+ core/admindocs.py,sha256=GufdugiNEG87xGSDYVq4CBMhGRubsQCzgz-FqDIqzpM,5367
20
+ core/apps.py,sha256=qOKq5hB4FSRb85I3Sv3OjZzhVkn7xONix65r1qMg3HM,10312
21
+ core/auto_upgrade.py,sha256=BkoE7rJuYAmwoMux22NqujWZYjYXtN40GBloC0sNMY4,1799
22
+ core/backends.py,sha256=VsZZwskII6QLnxP6Ff593V7o9lqXXfN2_bIfZXrvjyI,8222
23
+ core/entity.py,sha256=dkPywTVk981fV8bOEoZw-1SMrdh8T0jVAUZnRxg3dDg,4505
24
+ core/environment.py,sha256=egNX3vP9xRJt0dqxNZFXi25ZMhe3IZH3Vjne1_k7hxo,1645
25
+ core/fields.py,sha256=4nJ_tngso8NHs6HBl8nn5PWvbcOiuAgkoM6hixrFm64,5580
26
+ core/github_helper.py,sha256=k9LmxL0Ec5MzFwZsbMpWMMJ-5tGMe-5yRoI8V0sbFsw,682
27
+ core/github_issues.py,sha256=LKqt-Ilx5TRYnx2LXttc54Nvuh3_1VRP0u_M3whe09c,4963
28
+ core/lcd_screen.py,sha256=mkKtIJjHGDLaV4t80L-JScsZXbLhlYUDknAcvV9Ijr0,2651
29
+ core/liveupdate.py,sha256=kTgbE2gnU3PPIV-88Bw2swSl1aGp6stSdBYqBFbLvx0,716
30
+ core/log_paths.py,sha256=6UXYk6QIUmRO3ecFaFH3dgJ_pf4C_wUN6b0JqUhLVBY,3045
31
+ core/mailer.py,sha256=OF3UgrTVs2St60tQG3ORV7_N6AWK6EtuB04KQXDop_Q,2829
32
+ core/middleware.py,sha256=a4XL0pld4YiG-vanHrzYbJNHv64s75lvmG9inoG1ln0,3479
33
+ core/models.py,sha256=ZpovGr3Tnyayu0f9Id2F0uYtnVhHo90flMFH9ixjubQ,90459
34
+ core/notifications.py,sha256=YtNDGxNveZ6t3tlMXJ7wIaZZTWIZfOKy6vN9mXkeYnA,4021
35
+ core/public_wifi.py,sha256=A08IPJqcdgUKSWbktyqsV4ol8C0uxDxZy1s1ECuPdBE,6526
36
+ core/reference_utils.py,sha256=sRkwL68c16neg8EBMeZTVGjXQvVhB1zQqrd3EIsb4nA,3735
37
+ core/release.py,sha256=thtgbfAnxHlOODM8xUVETgY5aAIbddWan0wW9lKmtQI,11064
38
+ core/sigil_builder.py,sha256=63KSTh7AohG0szr_amBov0zoZuTE8bWqmgsEfPsZHCg,4992
39
+ core/sigil_context.py,sha256=8xrGiB2L1dFfSTrVLsFPLKfkhRwCXXZ0-EXvZdPeTMU,459
40
+ core/sigil_resolver.py,sha256=PAGwF3ugsqN85tKyIvT0YCDCM2MU_Mct2mCJz8XY1s4,11413
41
+ core/system.py,sha256=i-35lmzW6JxfwDZSUuDRT0cwPjqbIkWEYR0WVvqfADM,15080
42
+ core/tasks.py,sha256=hwowcVsbwME-75KN90D2r0kX_YPMZEVevOrrxIxpM34,10595
43
+ core/temp_passwords.py,sha256=Kp4C-y1Hh4GeV1vSOCw3ZyIjcRMts186lUgtEkd3rxY,5452
44
+ core/test_system_info.py,sha256=sHCuo-qyw0BKacbGXFhTWxkuPgywdMGiQ_6YK022n-E,4803
45
+ core/tests.py,sha256=A_85lR9Qf5TE9xuCHGuM3l0_0SrH8F1qsi6sf-ip8q4,58690
46
+ core/tests_liveupdate.py,sha256=D1o2gPopnK7wDCeDQlJ-tfitWh4umZQFRxSTCFY-puc,527
47
+ core/urls.py,sha256=x-LNCxCgrLINdBsJTUUcAuMS5EK5Sh1ybvsVuUnJfLw,436
48
+ core/user_data.py,sha256=kOzcWJcR-d05E7QYoNzKqQSm9bnKJvtG6zq0zRf-tDI,21371
49
+ core/views.py,sha256=xnwgWVv3q6SnpYZ4ItIrILmjtD5bKWAsjtUYQ-5vnd0,44773
50
+ core/widgets.py,sha256=ihah_-NtFJ3oRCS3TdcT6iHCUTlg1tUULJsVCu05C0o,1379
51
+ core/workgroup_urls.py,sha256=2bC8mOMkxIj04WsNis0Td6AmmJFr6z27Ol5epIvhO28,424
52
+ core/workgroup_views.py,sha256=pFJ4PIRN3WWpRyombpWDKKteYQYoI7lu7ddSEKojD7I,2983
53
+ nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
+ nodes/actions.py,sha256=HHnwByTBc3guOORvrKOuvUFID-_BpBq6OENE_PDgk9s,2335
55
+ nodes/admin.py,sha256=40YXBolvHjhzZRJM1o4svnIyghMI9KWUDlS-PtKheTc,21483
56
+ nodes/apps.py,sha256=KijGydsQBS-8Q8uBc0ahDZXSSCMzP3tQcYIEigJxdCE,2077
57
+ nodes/backends.py,sha256=-g7PIbGtIn_3kGoi2w_mJ6zVjvUKTRRWhHABPnnf29g,6081
58
+ nodes/dns.py,sha256=X8PML9rPR7i6NwCSqqxnlO1qpHHRo8uA-XePDTQZYrY,7251
59
+ nodes/lcd.py,sha256=7MqS3jV_De2ysSmTtXQfYTWaxdfbmm6YfNdb_7qIVZc,5923
60
+ nodes/models.py,sha256=D-MPUe672JaF5NUU-ASAes860hJmiHeU5VHUD03Pe-c,46986
61
+ nodes/tasks.py,sha256=jorbN4h0PqWMBRMFdkGgJtvG8GPFwGr5Hxlm5In3EeY,1568
62
+ nodes/tests.py,sha256=4lKlM_uHYKirtLgOne4ygwEDowi2PPl2ZvEtwJzUfhI,86073
63
+ nodes/urls.py,sha256=20yZDZf4DNgIZ9hQWsUzjp8k5Fryg9ukk761_KGQt9k,548
64
+ nodes/utils.py,sha256=B9BD3bKBkwDjIqyp0pyjnKQQlRbGRVQndfoaMEJoNDc,2815
65
+ nodes/views.py,sha256=lrT90AsOXeqpvkZnlCPv78mXILGJ8FwJzgsjU-wAZ4Y,15611
66
+ ocpp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
+ ocpp/admin.py,sha256=Rw0PinweVSaBjIQIcgj_cgfalNRBHcrAh3JInx8Hn7A,17129
68
+ ocpp/apps.py,sha256=mCZ5Z0ei7z7c62luIcIhbEuwL1N4czQFSToOkGvRmms,867
69
+ ocpp/consumers.py,sha256=0kLqiDAt0rsC62s8lmS6mW2Oj-LZuzjs0Pz4ci6L4oQ,36577
70
+ ocpp/evcs.py,sha256=O53rCHdxKcgPsj7o57rDiNHTVvEii3DTtQ3djWFTohw,34065
71
+ ocpp/models.py,sha256=46BYGGX4AhbBigXLV3PWLKP043JF96Wolho1q-ZPoa8,23929
72
+ ocpp/reference_utils.py,sha256=sTgbXfmz00f23LBBkpO-sBGoJf1qaEshHeSofLReYCc,1114
73
+ ocpp/routing.py,sha256=g9vPnLw-D1N8L_mW0_oCe-nTDibjC0Et-SFxe8NFAOI,308
74
+ ocpp/simulator.py,sha256=es6SJNzKUwLwrLy-zDdRu__n34V2RZSQRb8SeZHh4Fw,15636
75
+ ocpp/store.py,sha256=FyyZW2YKTWleuNdHTo_RsUO2InZZJhvMYyuZmLgQZe4,14051
76
+ ocpp/tasks.py,sha256=cOcJBshckFKs8GnACvmYZUBG116amtLRAzEP-JNqlZ0,905
77
+ ocpp/test_export_import.py,sha256=TK1__E4K5WrLYPIx-1iJWoIhuRCnOtXc2cYEpGigd3U,4660
78
+ ocpp/test_rfid.py,sha256=hX8VZ2HRyBGjtQLClZ1gy28dOj25RT2r9eK3l9XkmJg,25877
79
+ ocpp/tests.py,sha256=CcTn-bRDsb78NbK2hbH66TnlUwOno8y7ug0u8bgIz9k,107230
80
+ ocpp/transactions_io.py,sha256=OvRynP3DeC9ZpHD3Ez-0YPNPJXFcdSOSKg0nRrwzBJ0,6507
81
+ ocpp/urls.py,sha256=jl6AQLtmLMvrNXP3dQKgPe9Kvaul4oaYd80DskpzVBc,1750
82
+ ocpp/views.py,sha256=3E6hJ0Tqr-_KRjvB7cunEsjtNI8Y9XzktPDhaAlIM7o,37166
83
+ pages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
84
+ pages/admin.py,sha256=JcwBwtQD8E2XKhPpt6bfznW3cU18XZyWvp5B8Zlr0eQ,12772
85
+ pages/apps.py,sha256=mfCxegmnRqKcszyEwpQhZpW9JWOuEYdVereU_w49BXg,298
86
+ pages/checks.py,sha256=an-MlMCIG-FSKmdgOhYV0VOoB_wDQD7dxKD03Hjrzts,1567
87
+ pages/context_processors.py,sha256=iNSTFYFJsrhj0DjsB3ixVNDZw7nrowlnZAcCDyR4NS4,3697
88
+ pages/defaults.py,sha256=rF_V5oD5JQX1tMxdMs_rCsS21Vn_NZpTESahzLNTPws,595
89
+ pages/forms.py,sha256=NuG7ONP1HYY6O5gRoQaRSwSRchcSonRm3rulsqGSqvY,5081
90
+ pages/middleware.py,sha256=fUdAscLa6h2EqJTFUjhVijfSf82gBfm6XUZTVygWAnk,5227
91
+ pages/models.py,sha256=mSlw5JTgh2s_SsFzrNgq48BOhX8uc6mjZFy-kYXUjMo,9955
92
+ pages/tests.py,sha256=6WXtb2p301tqXPT_327Y8Zw5uAsqp5hREh6nF8wWa-o,74552
93
+ pages/urls.py,sha256=hvbdrMDc735CFwCbCU96t80IY9jDpMTpI0zQhoaxL0M,981
94
+ pages/utils.py,sha256=7kik1W0Gk6SFxYGhg6shfI2W9Xdcv1sCpkRCQ883a88,311
95
+ pages/views.py,sha256=X_ZoJH2DEQp8xUZMtZotTxnslABwsmFKQzYfWopktZM,41135
96
+ arthexis-0.1.11.dist-info/METADATA,sha256=uA-HTbP7vjQzwm9FtJjxgl28g8MszSr7ntnxH9MyVlM,10175
97
+ arthexis-0.1.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
98
+ arthexis-0.1.11.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
99
+ arthexis-0.1.11.dist-info/RECORD,,
@@ -64,5 +64,6 @@ def site_and_node(request: HttpRequest):
64
64
  "badge_admin_site_name": site_name or (site.domain if site else ""),
65
65
  "badge_site_color": site_color,
66
66
  "badge_node_color": node_color,
67
+ "current_site_domain": site.domain if site else host,
67
68
  "TIME_ZONE": settings.TIME_ZONE,
68
69
  }
config/settings.py CHANGED
@@ -20,6 +20,7 @@ from core.log_paths import select_log_dir
20
20
  from django.utils.translation import gettext_lazy as _
21
21
  from celery.schedules import crontab
22
22
  from django.http import request as http_request
23
+ from django.http.request import split_domain_port
23
24
  from django.middleware.csrf import CsrfViewMiddleware
24
25
  from django.core.exceptions import DisallowedHost
25
26
  from django.contrib.sites import shortcuts as sites_shortcuts
@@ -34,13 +35,36 @@ if not hasattr(encoding, "force_text"): # pragma: no cover - Django>=5 compatib
34
35
  encoding.force_text = force_str
35
36
 
36
37
 
38
+
37
39
  _original_validate_host = http_request.validate_host
38
40
 
39
41
 
40
- def _validate_host_with_subnets(host, allowed_hosts):
42
+ def _strip_ipv6_brackets(host: str) -> str:
43
+ if host.startswith("[") and host.endswith("]"):
44
+ return host[1:-1]
45
+ return host
46
+
47
+
48
+ def _extract_ip_from_host(host: str):
49
+ """Return an :mod:`ipaddress` object for ``host`` when possible."""
50
+
51
+ candidate = _strip_ipv6_brackets(host)
41
52
  try:
42
- ip = ipaddress.ip_address(host)
53
+ return ipaddress.ip_address(candidate)
43
54
  except ValueError:
55
+ domain, _port = split_domain_port(host)
56
+ if domain and domain != host:
57
+ candidate = _strip_ipv6_brackets(domain)
58
+ try:
59
+ return ipaddress.ip_address(candidate)
60
+ except ValueError:
61
+ return None
62
+ return None
63
+
64
+
65
+ def _validate_host_with_subnets(host, allowed_hosts):
66
+ ip = _extract_ip_from_host(host)
67
+ if ip is None:
44
68
  return _original_validate_host(host, allowed_hosts)
45
69
  for pattern in allowed_hosts:
46
70
  try:
@@ -98,13 +122,27 @@ SECRET_KEY = _load_secret_key()
98
122
 
99
123
  # SECURITY WARNING: don't run with debug turned on in production!
100
124
 
101
- # Enable DEBUG and related tooling when running in Terminal mode.
125
+ # Determine the current node role for role-specific settings while leaving
126
+ # DEBUG control to the environment.
102
127
  NODE_ROLE = os.environ.get("NODE_ROLE")
103
128
  if NODE_ROLE is None:
104
129
  role_lock = BASE_DIR / "locks" / "role.lck"
105
130
  NODE_ROLE = role_lock.read_text().strip() if role_lock.exists() else "Terminal"
106
131
 
107
- DEBUG = NODE_ROLE == "Terminal"
132
+ def _env_bool(name: str, default: bool) -> bool:
133
+ value = os.environ.get(name)
134
+ if value is None:
135
+ return default
136
+
137
+ normalized = value.strip().lower()
138
+ if normalized in {"1", "true", "yes", "on"}:
139
+ return True
140
+ if normalized in {"0", "false", "no", "off"}:
141
+ return False
142
+ return default
143
+
144
+
145
+ DEBUG = _env_bool("DEBUG", False)
108
146
 
109
147
  ALLOWED_HOSTS = [
110
148
  "localhost",
@@ -117,6 +155,18 @@ ALLOWED_HOSTS = [
117
155
  ]
118
156
 
119
157
 
158
+ _DEFAULT_PORTS = {"http": "80", "https": "443"}
159
+
160
+
161
+ def _get_allowed_hosts() -> list[str]:
162
+ from django.conf import settings as django_settings
163
+
164
+ configured = getattr(django_settings, "ALLOWED_HOSTS", None)
165
+ if configured is None:
166
+ return ALLOWED_HOSTS
167
+ return list(configured)
168
+
169
+
120
170
  def _iter_local_hostnames(hostname: str, fqdn: str | None = None) -> list[str]:
121
171
  """Return unique hostname variants for the current machine."""
122
172
 
@@ -152,34 +202,143 @@ for host in _iter_local_hostnames(_local_hostname, _local_fqdn):
152
202
 
153
203
  # Allow CSRF origin verification for hosts within allowed subnets.
154
204
  _original_origin_verified = CsrfViewMiddleware._origin_verified
205
+ _original_check_referer = CsrfViewMiddleware._check_referer
206
+
207
+
208
+ def _host_is_allowed(host: str, allowed_hosts: list[str]) -> bool:
209
+ if http_request.validate_host(host, allowed_hosts):
210
+ return True
211
+ domain, _port = split_domain_port(host)
212
+ if domain and domain != host:
213
+ return http_request.validate_host(domain, allowed_hosts)
214
+ return False
215
+
216
+
217
+ def _parse_forwarded_header(header_value: str) -> list[dict[str, str]]:
218
+ entries: list[dict[str, str]] = []
219
+ if not header_value:
220
+ return entries
221
+ for forwarded_part in header_value.split(","):
222
+ entry: dict[str, str] = {}
223
+ for element in forwarded_part.split(";"):
224
+ if "=" not in element:
225
+ continue
226
+ key, value = element.split("=", 1)
227
+ entry[key.strip().lower()] = value.strip().strip('"')
228
+ if entry:
229
+ entries.append(entry)
230
+ return entries
231
+
232
+
233
+ def _get_request_scheme(request, forwarded_entry: dict[str, str] | None = None) -> str:
234
+ """Return the scheme used by the client, honoring proxy headers."""
235
+
236
+ if forwarded_entry and forwarded_entry.get("proto", "").lower() in {"http", "https"}:
237
+ return forwarded_entry["proto"].lower()
238
+
239
+ if request.is_secure():
240
+ return "https"
241
+
242
+ forwarded_proto = request.META.get("HTTP_X_FORWARDED_PROTO", "")
243
+ if forwarded_proto:
244
+ candidate = forwarded_proto.split(",")[0].strip().lower()
245
+ if candidate in {"http", "https"}:
246
+ return candidate
247
+
248
+ forwarded_header = request.META.get("HTTP_FORWARDED", "")
249
+ for forwarded_entry in _parse_forwarded_header(forwarded_header):
250
+ candidate = forwarded_entry.get("proto", "").lower()
251
+ if candidate in {"http", "https"}:
252
+ return candidate
253
+
254
+ return "http"
255
+
256
+
257
+ def _normalize_origin_tuple(scheme: str | None, host: str) -> tuple[str, str, str | None] | None:
258
+ if not scheme or scheme.lower() not in {"http", "https"}:
259
+ return None
260
+ domain, port = split_domain_port(host)
261
+ normalized_host = _strip_ipv6_brackets(domain.strip().lower())
262
+ if not normalized_host:
263
+ return None
264
+ normalized_port = port.strip() if isinstance(port, str) else port
265
+ if not normalized_port:
266
+ normalized_port = _DEFAULT_PORTS.get(scheme.lower())
267
+ if normalized_port is not None:
268
+ normalized_port = str(normalized_port)
269
+ return scheme.lower(), normalized_host, normalized_port
270
+
271
+
272
+ def _normalized_request_origin(origin: str) -> tuple[str, str, str | None] | None:
273
+ parsed = urlsplit(origin)
274
+ if not parsed.scheme or not parsed.hostname:
275
+ return None
276
+ scheme = parsed.scheme.lower()
277
+ host = parsed.hostname.lower()
278
+ port = str(parsed.port) if parsed.port is not None else _DEFAULT_PORTS.get(scheme)
279
+ return scheme, host, port
280
+
281
+
282
+ def _candidate_origin_tuples(request, allowed_hosts: list[str]) -> list[tuple[str, str, str | None]]:
283
+ default_scheme = _get_request_scheme(request)
284
+ candidates: list[tuple[str, str, str | None]] = []
285
+ seen: set[tuple[str, str, str | None]] = set()
286
+
287
+ def _append_candidate(scheme: str | None, host: str) -> None:
288
+ if not scheme or not host:
289
+ return
290
+ normalized = _normalize_origin_tuple(scheme, host)
291
+ if normalized is None:
292
+ return
293
+ if not _host_is_allowed(host, allowed_hosts):
294
+ return
295
+ if normalized in seen:
296
+ return
297
+ candidates.append(normalized)
298
+ seen.add(normalized)
155
299
 
300
+ forwarded_header = request.META.get("HTTP_FORWARDED", "")
301
+ for forwarded_entry in _parse_forwarded_header(forwarded_header):
302
+ host = forwarded_entry.get("host", "").strip()
303
+ scheme = _get_request_scheme(request, forwarded_entry)
304
+ _append_candidate(scheme, host)
305
+
306
+ forwarded_host = request.META.get("HTTP_X_FORWARDED_HOST", "")
307
+ if forwarded_host:
308
+ host = forwarded_host.split(",")[0].strip()
309
+ _append_candidate(default_scheme, host)
156
310
 
157
- def _origin_verified_with_subnets(self, request):
158
- request_origin = request.META["HTTP_ORIGIN"]
159
311
  try:
160
312
  good_host = request.get_host()
161
313
  except DisallowedHost:
162
- pass
163
- else:
164
- good_origin = "%s://%s" % (
165
- "https" if request.is_secure() else "http",
166
- good_host,
167
- )
168
- if request_origin == good_origin:
314
+ good_host = ""
315
+ if good_host:
316
+ _append_candidate(default_scheme, good_host)
317
+
318
+ return candidates
319
+
320
+
321
+ def _origin_verified_with_subnets(self, request):
322
+ request_origin = request.META["HTTP_ORIGIN"]
323
+ allowed_hosts = _get_allowed_hosts()
324
+ normalized_origin = _normalized_request_origin(request_origin)
325
+ if normalized_origin is None:
326
+ return _original_origin_verified(self, request)
327
+
328
+ origin_ip = _extract_ip_from_host(normalized_origin[1])
329
+
330
+ for candidate in _candidate_origin_tuples(request, allowed_hosts):
331
+ if candidate == normalized_origin:
169
332
  return True
170
- try:
171
- origin_host = urlsplit(request_origin).hostname
172
- origin_ip = ipaddress.ip_address(origin_host)
173
- request_ip = ipaddress.ip_address(good_host.split(":")[0])
174
- except ValueError:
175
- pass
176
- else:
177
- for pattern in ALLOWED_HOSTS:
333
+
334
+ candidate_ip = _extract_ip_from_host(candidate[1])
335
+ if origin_ip and candidate_ip:
336
+ for pattern in allowed_hosts:
178
337
  try:
179
338
  network = ipaddress.ip_network(pattern)
180
339
  except ValueError:
181
340
  continue
182
- if origin_ip in network and request_ip in network:
341
+ if origin_ip in network and candidate_ip in network:
183
342
  return True
184
343
  return _original_origin_verified(self, request)
185
344
 
@@ -187,6 +346,49 @@ def _origin_verified_with_subnets(self, request):
187
346
  CsrfViewMiddleware._origin_verified = _origin_verified_with_subnets
188
347
 
189
348
 
349
+ def _check_referer_with_forwarded(self, request):
350
+ referer = request.META.get("HTTP_REFERER")
351
+ if referer is None:
352
+ return _original_check_referer(self, request)
353
+
354
+ try:
355
+ parsed = urlsplit(referer)
356
+ except ValueError:
357
+ return _original_check_referer(self, request)
358
+
359
+ if "" in (parsed.scheme, parsed.netloc):
360
+ return _original_check_referer(self, request)
361
+
362
+ if parsed.scheme.lower() != "https":
363
+ return _original_check_referer(self, request)
364
+
365
+ normalized_referer = _normalize_origin_tuple(parsed.scheme.lower(), parsed.netloc)
366
+ if normalized_referer is None:
367
+ return _original_check_referer(self, request)
368
+
369
+ allowed_hosts = _get_allowed_hosts()
370
+ referer_ip = _extract_ip_from_host(normalized_referer[1])
371
+
372
+ for candidate in _candidate_origin_tuples(request, allowed_hosts):
373
+ if candidate == normalized_referer:
374
+ return
375
+
376
+ candidate_ip = _extract_ip_from_host(candidate[1])
377
+ if referer_ip and candidate_ip:
378
+ for pattern in allowed_hosts:
379
+ try:
380
+ network = ipaddress.ip_network(pattern)
381
+ except ValueError:
382
+ continue
383
+ if referer_ip in network and candidate_ip in network:
384
+ return
385
+
386
+ return _original_check_referer(self, request)
387
+
388
+
389
+ CsrfViewMiddleware._check_referer = _check_referer_with_forwarded
390
+
391
+
190
392
  # Application definition
191
393
 
192
394
  LOCAL_APPS = [
@@ -195,7 +397,6 @@ LOCAL_APPS = [
195
397
  "ocpp",
196
398
  "awg",
197
399
  "pages",
198
- "man",
199
400
  "teams",
200
401
  ]
201
402
 
@@ -203,6 +404,8 @@ INSTALLED_APPS = [
203
404
  "whitenoise.runserver_nostatic",
204
405
  "django.contrib.admin",
205
406
  "django.contrib.admindocs",
407
+ "django_otp",
408
+ "django_otp.plugins.otp_totp",
206
409
  "config.auth_app.AuthConfig",
207
410
  "django.contrib.contenttypes",
208
411
  "django.contrib.sessions",
@@ -250,6 +453,7 @@ MIDDLEWARE = [
250
453
  "django.middleware.common.CommonMiddleware",
251
454
  "django.middleware.csrf.CsrfViewMiddleware",
252
455
  "django.contrib.auth.middleware.AuthenticationMiddleware",
456
+ "django_otp.middleware.OTPMiddleware",
253
457
  "core.middleware.AdminHistoryMiddleware",
254
458
  "core.middleware.SigilContextMiddleware",
255
459
  "pages.middleware.ViewHistoryMiddleware",
@@ -268,6 +472,9 @@ if DEBUG:
268
472
 
269
473
  CSRF_FAILURE_VIEW = "pages.views.csrf_failure"
270
474
 
475
+ # Allow staff TODO pages to embed internal admin views inside iframes.
476
+ X_FRAME_OPTIONS = "SAMEORIGIN"
477
+
271
478
  ROOT_URLCONF = "config.urls"
272
479
 
273
480
  TEMPLATES = [
@@ -325,10 +532,15 @@ AUTH_USER_MODEL = "core.User"
325
532
 
326
533
  # Enable RFID authentication backend and restrict default admin login to localhost
327
534
  AUTHENTICATION_BACKENDS = [
535
+ "core.backends.TempPasswordBackend",
328
536
  "core.backends.LocalhostAdminBackend",
537
+ "core.backends.TOTPBackend",
329
538
  "core.backends.RFIDBackend",
330
539
  ]
331
540
 
541
+ # Issuer name used when generating otpauth URLs for authenticator apps.
542
+ OTP_TOTP_ISSUER = os.environ.get("OTP_TOTP_ISSUER", "Arthexis")
543
+
332
544
  # Database
333
545
  # https://docs.djangoproject.com/en/5.2/ref/settings/#databases
334
546
 
@@ -405,10 +617,10 @@ AUTH_PASSWORD_VALIDATORS = [
405
617
  LANGUAGE_CODE = "en-us"
406
618
 
407
619
  LANGUAGES = [
408
- ("en", _("English")),
409
620
  ("es", _("Spanish")),
410
- ("fr", _("French")),
411
- ("ru", _("Russian")),
621
+ ("en", _("English")),
622
+ ("it", _("Italian")),
623
+ ("de", _("German")),
412
624
  ]
413
625
 
414
626
  LOCALE_PATHS = [BASE_DIR / "locale"]
@@ -426,6 +638,13 @@ USE_TZ = True
426
638
  STATIC_URL = "/static/"
427
639
  STATIC_ROOT = BASE_DIR / "static"
428
640
  STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
641
+
642
+ # Allow development and freshly-updated environments to serve assets which have
643
+ # not yet been collected into ``STATIC_ROOT``. Without this setting WhiteNoise
644
+ # only looks for files inside ``STATIC_ROOT`` and dashboards like the public
645
+ # traffic chart fail to load their JavaScript dependencies.
646
+ WHITENOISE_USE_FINDERS = True
647
+ WHITENOISE_AUTOREFRESH = DEBUG
429
648
  MEDIA_URL = "/media/"
430
649
  MEDIA_ROOT = BASE_DIR / "media"
431
650