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.
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/METADATA +76 -23
- arthexis-0.1.11.dist-info/RECORD +99 -0
- config/context_processors.py +1 -0
- config/settings.py +245 -26
- config/urls.py +11 -4
- core/admin.py +585 -57
- core/apps.py +29 -1
- core/auto_upgrade.py +57 -0
- core/backends.py +115 -3
- core/environment.py +23 -5
- core/fields.py +93 -0
- core/mailer.py +3 -1
- core/models.py +482 -38
- core/reference_utils.py +108 -0
- core/sigil_builder.py +23 -5
- core/sigil_resolver.py +35 -4
- core/system.py +400 -140
- core/tasks.py +151 -8
- core/temp_passwords.py +181 -0
- core/test_system_info.py +97 -1
- core/tests.py +393 -15
- core/user_data.py +154 -16
- core/views.py +499 -20
- nodes/admin.py +149 -6
- nodes/backends.py +125 -18
- nodes/dns.py +203 -0
- nodes/models.py +498 -9
- nodes/tests.py +682 -3
- nodes/views.py +154 -7
- ocpp/admin.py +63 -3
- ocpp/consumers.py +255 -41
- ocpp/evcs.py +6 -3
- ocpp/models.py +52 -7
- ocpp/reference_utils.py +42 -0
- ocpp/simulator.py +62 -5
- ocpp/store.py +30 -0
- ocpp/test_rfid.py +169 -7
- ocpp/tests.py +414 -8
- ocpp/views.py +109 -76
- pages/admin.py +9 -1
- pages/context_processors.py +24 -4
- pages/defaults.py +14 -0
- pages/forms.py +131 -0
- pages/models.py +53 -14
- pages/tests.py +450 -14
- pages/urls.py +4 -0
- pages/views.py +419 -110
- arthexis-0.1.9.dist-info/RECORD +0 -92
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/WHEEL +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {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.
|
|
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
|
+
[](https://github.com/arthexis/arthexis/actions/workflows/coverage.yml) [](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
|
-
-
|
|
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
|
-
##
|
|
147
|
+
## Role Architecture
|
|
129
148
|
|
|
130
149
|
Arthexis Constellation ships in four node roles tailored to different deployment scenarios.
|
|
131
150
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
<table border="1" cellpadding="8" cellspacing="0">
|
|
152
|
+
<thead>
|
|
153
|
+
<tr>
|
|
154
|
+
<th align="left">Role</th>
|
|
155
|
+
<th align="left">Description & 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 & 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 & 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 & 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 & 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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
-
|
|
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
|
|
155
|
-
-
|
|
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:
|
|
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,,
|
config/context_processors.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
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
|
-
("
|
|
411
|
-
("
|
|
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
|
|