arthexis 0.1.9__py3-none-any.whl → 0.1.26__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.26.dist-info/METADATA +272 -0
- arthexis-0.1.26.dist-info/RECORD +111 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +674 -674
- config/__init__.py +5 -5
- config/active_app.py +15 -15
- config/asgi.py +29 -29
- config/auth_app.py +7 -7
- config/celery.py +32 -25
- config/context_processors.py +67 -68
- config/horologia_app.py +7 -7
- config/loadenv.py +11 -11
- config/logging.py +59 -48
- config/middleware.py +71 -25
- config/offline.py +49 -49
- config/settings.py +676 -492
- config/settings_helpers.py +109 -0
- config/urls.py +228 -159
- config/wsgi.py +17 -17
- core/admin.py +4052 -2066
- core/admin_history.py +50 -50
- core/admindocs.py +192 -151
- core/apps.py +350 -223
- core/auto_upgrade.py +72 -0
- core/backends.py +311 -124
- core/changelog.py +403 -0
- core/entity.py +149 -133
- core/environment.py +60 -43
- core/fields.py +168 -75
- core/form_fields.py +75 -0
- core/github_helper.py +188 -25
- core/github_issues.py +183 -172
- core/github_repos.py +72 -0
- core/lcd_screen.py +78 -78
- core/liveupdate.py +25 -25
- core/log_paths.py +114 -100
- core/mailer.py +89 -83
- core/middleware.py +91 -91
- core/models.py +5041 -2195
- core/notifications.py +105 -105
- core/public_wifi.py +267 -227
- core/reference_utils.py +107 -0
- core/release.py +940 -346
- core/rfid_import_export.py +113 -0
- core/sigil_builder.py +149 -131
- core/sigil_context.py +20 -20
- core/sigil_resolver.py +250 -284
- core/system.py +1425 -230
- core/tasks.py +538 -199
- core/temp_passwords.py +181 -0
- core/test_system_info.py +202 -43
- core/tests.py +2673 -1069
- core/tests_liveupdate.py +17 -17
- core/urls.py +11 -11
- core/user_data.py +681 -495
- core/views.py +2484 -789
- core/widgets.py +213 -51
- nodes/admin.py +2236 -445
- nodes/apps.py +98 -70
- nodes/backends.py +160 -53
- nodes/dns.py +203 -0
- nodes/feature_checks.py +133 -0
- nodes/lcd.py +165 -165
- nodes/models.py +2375 -870
- nodes/reports.py +411 -0
- nodes/rfid_sync.py +210 -0
- nodes/signals.py +18 -0
- nodes/tasks.py +141 -46
- nodes/tests.py +5045 -1489
- nodes/urls.py +29 -13
- nodes/utils.py +172 -73
- nodes/views.py +1768 -304
- ocpp/admin.py +1775 -481
- ocpp/apps.py +25 -25
- ocpp/consumers.py +1843 -630
- ocpp/evcs.py +844 -928
- ocpp/evcs_discovery.py +158 -0
- ocpp/models.py +1417 -640
- ocpp/network.py +398 -0
- ocpp/reference_utils.py +42 -0
- ocpp/routing.py +11 -9
- ocpp/simulator.py +745 -368
- ocpp/status_display.py +26 -0
- ocpp/store.py +603 -403
- ocpp/tasks.py +479 -31
- ocpp/test_export_import.py +131 -130
- ocpp/test_rfid.py +1072 -540
- ocpp/tests.py +5494 -2296
- ocpp/transactions_io.py +197 -165
- ocpp/urls.py +50 -50
- ocpp/views.py +2024 -912
- pages/admin.py +1123 -396
- pages/apps.py +45 -10
- pages/checks.py +40 -40
- pages/context_processors.py +151 -85
- pages/defaults.py +13 -0
- pages/forms.py +221 -0
- pages/middleware.py +213 -153
- pages/models.py +720 -252
- pages/module_defaults.py +156 -0
- pages/site_config.py +137 -0
- pages/tasks.py +74 -0
- pages/tests.py +4009 -1389
- pages/urls.py +38 -20
- pages/utils.py +93 -12
- pages/views.py +1736 -762
- arthexis-0.1.9.dist-info/METADATA +0 -168
- arthexis-0.1.9.dist-info/RECORD +0 -92
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- nodes/actions.py +0 -70
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/top_level.txt +0 -0
config/settings.py
CHANGED
|
@@ -1,492 +1,676 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Django settings for config project.
|
|
3
|
-
|
|
4
|
-
Generated by 'django-admin startproject' using Django 5.2.4.
|
|
5
|
-
|
|
6
|
-
For more information on this file, see
|
|
7
|
-
https://docs.djangoproject.com/en/5.2/topics/settings/
|
|
8
|
-
|
|
9
|
-
For the full list of settings and their values, see
|
|
10
|
-
https://docs.djangoproject.com/en/5.2/ref/settings/
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
import contextlib
|
|
15
|
-
import os
|
|
16
|
-
import sys
|
|
17
|
-
import ipaddress
|
|
18
|
-
import socket
|
|
19
|
-
from core.log_paths import select_log_dir
|
|
20
|
-
from django.utils.translation import gettext_lazy as _
|
|
21
|
-
from
|
|
22
|
-
|
|
23
|
-
from
|
|
24
|
-
from django.
|
|
25
|
-
from django.
|
|
26
|
-
from django.
|
|
27
|
-
from django.core.
|
|
28
|
-
from
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
ALLOWED_HOSTS
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
]
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
"
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
]
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
#
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
"
|
|
456
|
-
"
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
"
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
"
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
"
|
|
490
|
-
"
|
|
491
|
-
|
|
492
|
-
}
|
|
1
|
+
"""
|
|
2
|
+
Django settings for config project.
|
|
3
|
+
|
|
4
|
+
Generated by 'django-admin startproject' using Django 5.2.4.
|
|
5
|
+
|
|
6
|
+
For more information on this file, see
|
|
7
|
+
https://docs.djangoproject.com/en/5.2/topics/settings/
|
|
8
|
+
|
|
9
|
+
For the full list of settings and their values, see
|
|
10
|
+
https://docs.djangoproject.com/en/5.2/ref/settings/
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import contextlib
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import ipaddress
|
|
18
|
+
import socket
|
|
19
|
+
from core.log_paths import select_log_dir
|
|
20
|
+
from django.utils.translation import gettext_lazy as _
|
|
21
|
+
from datetime import timedelta
|
|
22
|
+
|
|
23
|
+
from celery.schedules import crontab
|
|
24
|
+
from django.http import request as http_request
|
|
25
|
+
from django.http.request import split_domain_port
|
|
26
|
+
from django.middleware.csrf import CsrfViewMiddleware
|
|
27
|
+
from django.core.exceptions import DisallowedHost, ImproperlyConfigured
|
|
28
|
+
from django.contrib.sites import shortcuts as sites_shortcuts
|
|
29
|
+
from django.contrib.sites.requests import RequestSite
|
|
30
|
+
from urllib.parse import urlsplit
|
|
31
|
+
import django.utils.encoding as encoding
|
|
32
|
+
|
|
33
|
+
from config.settings_helpers import (
|
|
34
|
+
extract_ip_from_host,
|
|
35
|
+
install_validate_host_with_subnets,
|
|
36
|
+
load_secret_key,
|
|
37
|
+
strip_ipv6_brackets,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if not hasattr(encoding, "force_text"): # pragma: no cover - Django>=5 compatibility
|
|
41
|
+
from django.utils.encoding import force_str
|
|
42
|
+
|
|
43
|
+
encoding.force_text = force_str
|
|
44
|
+
install_validate_host_with_subnets()
|
|
45
|
+
|
|
46
|
+
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
|
47
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
48
|
+
|
|
49
|
+
ACRONYMS: list[str] = []
|
|
50
|
+
with contextlib.suppress(FileNotFoundError):
|
|
51
|
+
ACRONYMS = [
|
|
52
|
+
line.strip()
|
|
53
|
+
for line in (BASE_DIR / "config" / "data" / "ACRONYMS.txt")
|
|
54
|
+
.read_text()
|
|
55
|
+
.splitlines()
|
|
56
|
+
if line.strip()
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Quick-start development settings - unsuitable for production
|
|
61
|
+
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
|
62
|
+
|
|
63
|
+
# SECURITY WARNING: keep the secret key used in production secret!
|
|
64
|
+
SECRET_KEY = load_secret_key(BASE_DIR)
|
|
65
|
+
|
|
66
|
+
# SECURITY WARNING: don't run with debug turned on in production!
|
|
67
|
+
|
|
68
|
+
# Determine the current node role for role-specific settings while leaving
|
|
69
|
+
# DEBUG control to the environment.
|
|
70
|
+
NODE_ROLE = os.environ.get("NODE_ROLE")
|
|
71
|
+
if NODE_ROLE is None:
|
|
72
|
+
role_lock = BASE_DIR / "locks" / "role.lck"
|
|
73
|
+
NODE_ROLE = role_lock.read_text().strip() if role_lock.exists() else "Terminal"
|
|
74
|
+
|
|
75
|
+
def _env_bool(name: str, default: bool) -> bool:
|
|
76
|
+
value = os.environ.get(name)
|
|
77
|
+
if value is None:
|
|
78
|
+
return default
|
|
79
|
+
|
|
80
|
+
normalized = value.strip().lower()
|
|
81
|
+
if normalized in {"1", "true", "yes", "on"}:
|
|
82
|
+
return True
|
|
83
|
+
if normalized in {"0", "false", "no", "off"}:
|
|
84
|
+
return False
|
|
85
|
+
return default
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
DEBUG = _env_bool("DEBUG", False)
|
|
89
|
+
|
|
90
|
+
ALLOWED_HOSTS = [
|
|
91
|
+
"localhost",
|
|
92
|
+
"127.0.0.1",
|
|
93
|
+
"testserver",
|
|
94
|
+
"10.42.0.0/16",
|
|
95
|
+
"192.168.0.0/16",
|
|
96
|
+
"arthexis.com",
|
|
97
|
+
"www.arthexis.com",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
_DEFAULT_PORTS = {"http": "80", "https": "443"}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _get_allowed_hosts() -> list[str]:
|
|
105
|
+
from django.conf import settings as django_settings
|
|
106
|
+
|
|
107
|
+
configured = getattr(django_settings, "ALLOWED_HOSTS", None)
|
|
108
|
+
if configured is None:
|
|
109
|
+
return ALLOWED_HOSTS
|
|
110
|
+
return list(configured)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _iter_local_hostnames(hostname: str, fqdn: str | None = None) -> list[str]:
|
|
114
|
+
"""Return unique hostname variants for the current machine."""
|
|
115
|
+
|
|
116
|
+
hostnames: list[str] = []
|
|
117
|
+
seen: set[str] = set()
|
|
118
|
+
|
|
119
|
+
def _append(candidate: str | None) -> None:
|
|
120
|
+
if not candidate:
|
|
121
|
+
return
|
|
122
|
+
normalized = candidate.strip()
|
|
123
|
+
if not normalized or normalized in seen:
|
|
124
|
+
return
|
|
125
|
+
hostnames.append(normalized)
|
|
126
|
+
seen.add(normalized)
|
|
127
|
+
|
|
128
|
+
_append(hostname)
|
|
129
|
+
_append(fqdn)
|
|
130
|
+
if hostname and "." not in hostname:
|
|
131
|
+
_append(f"{hostname}.local")
|
|
132
|
+
|
|
133
|
+
return hostnames
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
_local_hostname = socket.gethostname().strip()
|
|
137
|
+
_local_fqdn = ""
|
|
138
|
+
with contextlib.suppress(Exception):
|
|
139
|
+
_local_fqdn = socket.getfqdn().strip()
|
|
140
|
+
|
|
141
|
+
for host in _iter_local_hostnames(_local_hostname, _local_fqdn):
|
|
142
|
+
if host not in ALLOWED_HOSTS:
|
|
143
|
+
ALLOWED_HOSTS.append(host)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Allow CSRF origin verification for hosts within allowed subnets.
|
|
147
|
+
_original_origin_verified = CsrfViewMiddleware._origin_verified
|
|
148
|
+
_original_check_referer = CsrfViewMiddleware._check_referer
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _host_is_allowed(host: str, allowed_hosts: list[str]) -> bool:
|
|
152
|
+
if http_request.validate_host(host, allowed_hosts):
|
|
153
|
+
return True
|
|
154
|
+
domain, _port = split_domain_port(host)
|
|
155
|
+
if domain and domain != host:
|
|
156
|
+
return http_request.validate_host(domain, allowed_hosts)
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _parse_forwarded_header(header_value: str) -> list[dict[str, str]]:
|
|
161
|
+
entries: list[dict[str, str]] = []
|
|
162
|
+
if not header_value:
|
|
163
|
+
return entries
|
|
164
|
+
for forwarded_part in header_value.split(","):
|
|
165
|
+
entry: dict[str, str] = {}
|
|
166
|
+
for element in forwarded_part.split(";"):
|
|
167
|
+
if "=" not in element:
|
|
168
|
+
continue
|
|
169
|
+
key, value = element.split("=", 1)
|
|
170
|
+
entry[key.strip().lower()] = value.strip().strip('"')
|
|
171
|
+
if entry:
|
|
172
|
+
entries.append(entry)
|
|
173
|
+
return entries
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _get_request_scheme(request, forwarded_entry: dict[str, str] | None = None) -> str:
|
|
177
|
+
"""Return the scheme used by the client, honoring proxy headers."""
|
|
178
|
+
|
|
179
|
+
if forwarded_entry and forwarded_entry.get("proto", "").lower() in {"http", "https"}:
|
|
180
|
+
return forwarded_entry["proto"].lower()
|
|
181
|
+
|
|
182
|
+
if request.is_secure():
|
|
183
|
+
return "https"
|
|
184
|
+
|
|
185
|
+
forwarded_proto = request.META.get("HTTP_X_FORWARDED_PROTO", "")
|
|
186
|
+
if forwarded_proto:
|
|
187
|
+
candidate = forwarded_proto.split(",")[0].strip().lower()
|
|
188
|
+
if candidate in {"http", "https"}:
|
|
189
|
+
return candidate
|
|
190
|
+
|
|
191
|
+
forwarded_header = request.META.get("HTTP_FORWARDED", "")
|
|
192
|
+
for forwarded_entry in _parse_forwarded_header(forwarded_header):
|
|
193
|
+
candidate = forwarded_entry.get("proto", "").lower()
|
|
194
|
+
if candidate in {"http", "https"}:
|
|
195
|
+
return candidate
|
|
196
|
+
|
|
197
|
+
return "http"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _normalize_origin_tuple(scheme: str | None, host: str) -> tuple[str, str, str | None] | None:
|
|
201
|
+
if not scheme or scheme.lower() not in {"http", "https"}:
|
|
202
|
+
return None
|
|
203
|
+
domain, port = split_domain_port(host)
|
|
204
|
+
normalized_host = strip_ipv6_brackets(domain.strip().lower())
|
|
205
|
+
if not normalized_host:
|
|
206
|
+
return None
|
|
207
|
+
normalized_port = port.strip() if isinstance(port, str) else port
|
|
208
|
+
if not normalized_port:
|
|
209
|
+
normalized_port = _DEFAULT_PORTS.get(scheme.lower())
|
|
210
|
+
if normalized_port is not None:
|
|
211
|
+
normalized_port = str(normalized_port)
|
|
212
|
+
return scheme.lower(), normalized_host, normalized_port
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _normalized_request_origin(origin: str) -> tuple[str, str, str | None] | None:
|
|
216
|
+
parsed = urlsplit(origin)
|
|
217
|
+
if not parsed.scheme or not parsed.hostname:
|
|
218
|
+
return None
|
|
219
|
+
scheme = parsed.scheme.lower()
|
|
220
|
+
host = parsed.hostname.lower()
|
|
221
|
+
port = str(parsed.port) if parsed.port is not None else _DEFAULT_PORTS.get(scheme)
|
|
222
|
+
return scheme, host, port
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _candidate_origin_tuples(request, allowed_hosts: list[str]) -> list[tuple[str, str, str | None]]:
|
|
226
|
+
default_scheme = _get_request_scheme(request)
|
|
227
|
+
candidates: list[tuple[str, str, str | None]] = []
|
|
228
|
+
seen: set[tuple[str, str, str | None]] = set()
|
|
229
|
+
|
|
230
|
+
def _append_candidate(scheme: str | None, host: str) -> None:
|
|
231
|
+
if not scheme or not host:
|
|
232
|
+
return
|
|
233
|
+
normalized = _normalize_origin_tuple(scheme, host)
|
|
234
|
+
if normalized is None:
|
|
235
|
+
return
|
|
236
|
+
if not _host_is_allowed(host, allowed_hosts):
|
|
237
|
+
return
|
|
238
|
+
if normalized in seen:
|
|
239
|
+
return
|
|
240
|
+
candidates.append(normalized)
|
|
241
|
+
seen.add(normalized)
|
|
242
|
+
|
|
243
|
+
forwarded_header = request.META.get("HTTP_FORWARDED", "")
|
|
244
|
+
for forwarded_entry in _parse_forwarded_header(forwarded_header):
|
|
245
|
+
host = forwarded_entry.get("host", "").strip()
|
|
246
|
+
scheme = _get_request_scheme(request, forwarded_entry)
|
|
247
|
+
_append_candidate(scheme, host)
|
|
248
|
+
|
|
249
|
+
forwarded_host = request.META.get("HTTP_X_FORWARDED_HOST", "")
|
|
250
|
+
if forwarded_host:
|
|
251
|
+
host = forwarded_host.split(",")[0].strip()
|
|
252
|
+
_append_candidate(default_scheme, host)
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
good_host = request.get_host()
|
|
256
|
+
except DisallowedHost:
|
|
257
|
+
good_host = ""
|
|
258
|
+
if good_host:
|
|
259
|
+
_append_candidate(default_scheme, good_host)
|
|
260
|
+
|
|
261
|
+
return candidates
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _origin_verified_with_subnets(self, request):
|
|
265
|
+
request_origin = request.META["HTTP_ORIGIN"]
|
|
266
|
+
allowed_hosts = _get_allowed_hosts()
|
|
267
|
+
normalized_origin = _normalized_request_origin(request_origin)
|
|
268
|
+
if normalized_origin is None:
|
|
269
|
+
return _original_origin_verified(self, request)
|
|
270
|
+
|
|
271
|
+
origin_ip = extract_ip_from_host(normalized_origin[1])
|
|
272
|
+
|
|
273
|
+
for candidate in _candidate_origin_tuples(request, allowed_hosts):
|
|
274
|
+
if candidate == normalized_origin:
|
|
275
|
+
return True
|
|
276
|
+
|
|
277
|
+
candidate_ip = extract_ip_from_host(candidate[1])
|
|
278
|
+
if origin_ip and candidate_ip:
|
|
279
|
+
for pattern in allowed_hosts:
|
|
280
|
+
try:
|
|
281
|
+
network = ipaddress.ip_network(pattern)
|
|
282
|
+
except ValueError:
|
|
283
|
+
continue
|
|
284
|
+
if origin_ip in network and candidate_ip in network:
|
|
285
|
+
return True
|
|
286
|
+
return _original_origin_verified(self, request)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
CsrfViewMiddleware._origin_verified = _origin_verified_with_subnets
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _check_referer_with_forwarded(self, request):
|
|
293
|
+
referer = request.META.get("HTTP_REFERER")
|
|
294
|
+
if referer is None:
|
|
295
|
+
return _original_check_referer(self, request)
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
parsed = urlsplit(referer)
|
|
299
|
+
except ValueError:
|
|
300
|
+
return _original_check_referer(self, request)
|
|
301
|
+
|
|
302
|
+
if "" in (parsed.scheme, parsed.netloc):
|
|
303
|
+
return _original_check_referer(self, request)
|
|
304
|
+
|
|
305
|
+
if parsed.scheme.lower() != "https":
|
|
306
|
+
return _original_check_referer(self, request)
|
|
307
|
+
|
|
308
|
+
normalized_referer = _normalize_origin_tuple(parsed.scheme.lower(), parsed.netloc)
|
|
309
|
+
if normalized_referer is None:
|
|
310
|
+
return _original_check_referer(self, request)
|
|
311
|
+
|
|
312
|
+
allowed_hosts = _get_allowed_hosts()
|
|
313
|
+
referer_ip = extract_ip_from_host(normalized_referer[1])
|
|
314
|
+
|
|
315
|
+
for candidate in _candidate_origin_tuples(request, allowed_hosts):
|
|
316
|
+
if candidate == normalized_referer:
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
candidate_ip = extract_ip_from_host(candidate[1])
|
|
320
|
+
if referer_ip and candidate_ip:
|
|
321
|
+
for pattern in allowed_hosts:
|
|
322
|
+
try:
|
|
323
|
+
network = ipaddress.ip_network(pattern)
|
|
324
|
+
except ValueError:
|
|
325
|
+
continue
|
|
326
|
+
if referer_ip in network and candidate_ip in network:
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
return _original_check_referer(self, request)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
CsrfViewMiddleware._check_referer = _check_referer_with_forwarded
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# Application definition
|
|
336
|
+
|
|
337
|
+
LOCAL_APPS = [
|
|
338
|
+
"api",
|
|
339
|
+
"nodes",
|
|
340
|
+
"core",
|
|
341
|
+
"ocpp",
|
|
342
|
+
"awg",
|
|
343
|
+
"pages",
|
|
344
|
+
"teams",
|
|
345
|
+
]
|
|
346
|
+
|
|
347
|
+
INSTALLED_APPS = [
|
|
348
|
+
"whitenoise.runserver_nostatic",
|
|
349
|
+
"django.contrib.admin",
|
|
350
|
+
"django.contrib.admindocs",
|
|
351
|
+
"django_otp",
|
|
352
|
+
"django_otp.plugins.otp_totp",
|
|
353
|
+
"config.auth_app.AuthConfig",
|
|
354
|
+
"django.contrib.contenttypes",
|
|
355
|
+
"django.contrib.sessions",
|
|
356
|
+
"django.contrib.messages",
|
|
357
|
+
"django.contrib.staticfiles",
|
|
358
|
+
"import_export",
|
|
359
|
+
"django_object_actions",
|
|
360
|
+
"django.contrib.sites",
|
|
361
|
+
"channels",
|
|
362
|
+
"config.horologia_app.HorologiaConfig",
|
|
363
|
+
] + LOCAL_APPS
|
|
364
|
+
|
|
365
|
+
if DEBUG:
|
|
366
|
+
try:
|
|
367
|
+
import debug_toolbar # type: ignore
|
|
368
|
+
except ModuleNotFoundError: # pragma: no cover - optional dependency
|
|
369
|
+
pass
|
|
370
|
+
else:
|
|
371
|
+
INSTALLED_APPS += ["debug_toolbar"]
|
|
372
|
+
|
|
373
|
+
SITE_ID = 1
|
|
374
|
+
|
|
375
|
+
_original_get_current_site = sites_shortcuts.get_current_site
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _get_current_site_with_request_fallback(request=None):
|
|
379
|
+
try:
|
|
380
|
+
return _original_get_current_site(request)
|
|
381
|
+
except Exception as exc:
|
|
382
|
+
from django.contrib.sites.models import Site
|
|
383
|
+
|
|
384
|
+
if request is not None and isinstance(exc, Site.DoesNotExist):
|
|
385
|
+
return RequestSite(request)
|
|
386
|
+
raise
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
sites_shortcuts.get_current_site = _get_current_site_with_request_fallback
|
|
390
|
+
|
|
391
|
+
MIDDLEWARE = [
|
|
392
|
+
"django.middleware.security.SecurityMiddleware",
|
|
393
|
+
"whitenoise.middleware.WhiteNoiseMiddleware",
|
|
394
|
+
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
395
|
+
"config.middleware.ActiveAppMiddleware",
|
|
396
|
+
"config.middleware.SiteHttpsRedirectMiddleware",
|
|
397
|
+
"django.middleware.locale.LocaleMiddleware",
|
|
398
|
+
"django.middleware.common.CommonMiddleware",
|
|
399
|
+
"django.middleware.csrf.CsrfViewMiddleware",
|
|
400
|
+
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
|
401
|
+
"django_otp.middleware.OTPMiddleware",
|
|
402
|
+
"core.middleware.AdminHistoryMiddleware",
|
|
403
|
+
"core.middleware.SigilContextMiddleware",
|
|
404
|
+
"pages.middleware.ViewHistoryMiddleware",
|
|
405
|
+
"django.contrib.messages.middleware.MessageMiddleware",
|
|
406
|
+
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
|
407
|
+
]
|
|
408
|
+
|
|
409
|
+
if DEBUG:
|
|
410
|
+
try:
|
|
411
|
+
import debug_toolbar # type: ignore
|
|
412
|
+
except ModuleNotFoundError: # pragma: no cover - optional dependency
|
|
413
|
+
pass
|
|
414
|
+
else:
|
|
415
|
+
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
|
|
416
|
+
INTERNAL_IPS = ["127.0.0.1", "localhost"]
|
|
417
|
+
|
|
418
|
+
CSRF_FAILURE_VIEW = "pages.views.csrf_failure"
|
|
419
|
+
|
|
420
|
+
# Allow staff TODO pages to embed internal admin views inside iframes.
|
|
421
|
+
X_FRAME_OPTIONS = "SAMEORIGIN"
|
|
422
|
+
|
|
423
|
+
ROOT_URLCONF = "config.urls"
|
|
424
|
+
|
|
425
|
+
TEMPLATES = [
|
|
426
|
+
{
|
|
427
|
+
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
428
|
+
"DIRS": [BASE_DIR / "pages" / "templates"],
|
|
429
|
+
"APP_DIRS": True,
|
|
430
|
+
"OPTIONS": {
|
|
431
|
+
"context_processors": [
|
|
432
|
+
"django.template.context_processors.request",
|
|
433
|
+
"django.contrib.auth.context_processors.auth",
|
|
434
|
+
"django.template.context_processors.i18n",
|
|
435
|
+
"django.contrib.messages.context_processors.messages",
|
|
436
|
+
"pages.context_processors.nav_links",
|
|
437
|
+
"config.context_processors.site_and_node",
|
|
438
|
+
],
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
]
|
|
442
|
+
|
|
443
|
+
WSGI_APPLICATION = "config.wsgi.application"
|
|
444
|
+
ASGI_APPLICATION = "config.asgi.application"
|
|
445
|
+
|
|
446
|
+
# Channels configuration
|
|
447
|
+
CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}}
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# Custom user model
|
|
451
|
+
AUTH_USER_MODEL = "core.User"
|
|
452
|
+
|
|
453
|
+
# Enable RFID authentication backend and restrict default admin login to localhost
|
|
454
|
+
AUTHENTICATION_BACKENDS = [
|
|
455
|
+
"core.backends.TempPasswordBackend",
|
|
456
|
+
"core.backends.LocalhostAdminBackend",
|
|
457
|
+
"core.backends.TOTPBackend",
|
|
458
|
+
"core.backends.RFIDBackend",
|
|
459
|
+
]
|
|
460
|
+
|
|
461
|
+
# Use the custom login view for all authentication redirects.
|
|
462
|
+
LOGIN_URL = "pages:login"
|
|
463
|
+
|
|
464
|
+
# Issuer name used when generating otpauth URLs for authenticator apps.
|
|
465
|
+
OTP_TOTP_ISSUER = os.environ.get("OTP_TOTP_ISSUER", "Arthexis")
|
|
466
|
+
|
|
467
|
+
# Database
|
|
468
|
+
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
|
469
|
+
|
|
470
|
+
FORCED_DB_BACKEND = os.environ.get("ARTHEXIS_FORCE_DB_BACKEND", "").strip().lower()
|
|
471
|
+
if FORCED_DB_BACKEND and FORCED_DB_BACKEND not in {"sqlite", "postgres"}:
|
|
472
|
+
raise ImproperlyConfigured(
|
|
473
|
+
"ARTHEXIS_FORCE_DB_BACKEND must be 'sqlite' or 'postgres' when defined."
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _postgres_available() -> bool:
|
|
478
|
+
if FORCED_DB_BACKEND == "sqlite":
|
|
479
|
+
return False
|
|
480
|
+
try:
|
|
481
|
+
import psycopg
|
|
482
|
+
except Exception:
|
|
483
|
+
return False
|
|
484
|
+
|
|
485
|
+
params = {
|
|
486
|
+
"dbname": os.environ.get("POSTGRES_DB", "postgres"),
|
|
487
|
+
"user": os.environ.get("POSTGRES_USER", "postgres"),
|
|
488
|
+
"password": os.environ.get("POSTGRES_PASSWORD", ""),
|
|
489
|
+
"host": os.environ.get("POSTGRES_HOST", "localhost"),
|
|
490
|
+
"port": os.environ.get("POSTGRES_PORT", "5432"),
|
|
491
|
+
"connect_timeout": 10,
|
|
492
|
+
}
|
|
493
|
+
try:
|
|
494
|
+
with contextlib.closing(psycopg.connect(**params)):
|
|
495
|
+
return True
|
|
496
|
+
except psycopg.OperationalError:
|
|
497
|
+
return False
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
if FORCED_DB_BACKEND == "postgres":
|
|
501
|
+
_use_postgres = True
|
|
502
|
+
elif FORCED_DB_BACKEND == "sqlite":
|
|
503
|
+
_use_postgres = False
|
|
504
|
+
else:
|
|
505
|
+
_use_postgres = _postgres_available()
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
if _use_postgres:
|
|
509
|
+
DATABASES = {
|
|
510
|
+
"default": {
|
|
511
|
+
"ENGINE": "django.db.backends.postgresql",
|
|
512
|
+
"NAME": os.environ.get("POSTGRES_DB", "postgres"),
|
|
513
|
+
"USER": os.environ.get("POSTGRES_USER", "postgres"),
|
|
514
|
+
"PASSWORD": os.environ.get("POSTGRES_PASSWORD", ""),
|
|
515
|
+
"HOST": os.environ.get("POSTGRES_HOST", "localhost"),
|
|
516
|
+
"PORT": os.environ.get("POSTGRES_PORT", "5432"),
|
|
517
|
+
"OPTIONS": {"options": "-c timezone=UTC"},
|
|
518
|
+
"TEST": {
|
|
519
|
+
"NAME": f"{os.environ.get('POSTGRES_DB', 'postgres')}_test",
|
|
520
|
+
},
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else:
|
|
524
|
+
_sqlite_override = os.environ.get("ARTHEXIS_SQLITE_PATH")
|
|
525
|
+
if _sqlite_override:
|
|
526
|
+
SQLITE_DB_PATH = Path(_sqlite_override)
|
|
527
|
+
else:
|
|
528
|
+
SQLITE_DB_PATH = BASE_DIR / "db.sqlite3"
|
|
529
|
+
|
|
530
|
+
DATABASES = {
|
|
531
|
+
"default": {
|
|
532
|
+
"ENGINE": "django.db.backends.sqlite3",
|
|
533
|
+
"NAME": SQLITE_DB_PATH,
|
|
534
|
+
"OPTIONS": {"timeout": 60},
|
|
535
|
+
"TEST": {"NAME": BASE_DIR / "test_db.sqlite3"},
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
# Password validation
|
|
541
|
+
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
|
542
|
+
|
|
543
|
+
AUTH_PASSWORD_VALIDATORS = [
|
|
544
|
+
{
|
|
545
|
+
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
|
555
|
+
},
|
|
556
|
+
]
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
# Internationalization
|
|
560
|
+
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
|
561
|
+
|
|
562
|
+
LANGUAGE_CODE = "en-us"
|
|
563
|
+
|
|
564
|
+
LANGUAGES = [
|
|
565
|
+
("es", _("Spanish (Latin America)")),
|
|
566
|
+
("en", _("English")),
|
|
567
|
+
("it", _("Italian")),
|
|
568
|
+
("de", _("German")),
|
|
569
|
+
]
|
|
570
|
+
|
|
571
|
+
LOCALE_PATHS = [BASE_DIR / "locale"]
|
|
572
|
+
|
|
573
|
+
FORMAT_MODULE_PATH = ["config.formats"]
|
|
574
|
+
|
|
575
|
+
TIME_ZONE = "America/Monterrey"
|
|
576
|
+
|
|
577
|
+
USE_I18N = True
|
|
578
|
+
|
|
579
|
+
USE_TZ = True
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
# Static files (CSS, JavaScript, Images)
|
|
583
|
+
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
|
584
|
+
|
|
585
|
+
STATIC_URL = "/static/"
|
|
586
|
+
STATIC_ROOT = BASE_DIR / "static"
|
|
587
|
+
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
|
588
|
+
|
|
589
|
+
# Allow development and freshly-updated environments to serve assets which have
|
|
590
|
+
# not yet been collected into ``STATIC_ROOT``. Without this setting WhiteNoise
|
|
591
|
+
# only looks for files inside ``STATIC_ROOT`` and dashboards like the public
|
|
592
|
+
# traffic chart fail to load their JavaScript dependencies.
|
|
593
|
+
WHITENOISE_USE_FINDERS = True
|
|
594
|
+
WHITENOISE_AUTOREFRESH = DEBUG
|
|
595
|
+
MEDIA_URL = "/media/"
|
|
596
|
+
MEDIA_ROOT = BASE_DIR / "media"
|
|
597
|
+
|
|
598
|
+
# Email settings
|
|
599
|
+
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
|
600
|
+
DEFAULT_FROM_EMAIL = "arthexis@gmail.com"
|
|
601
|
+
SERVER_EMAIL = DEFAULT_FROM_EMAIL
|
|
602
|
+
|
|
603
|
+
# Default primary key field type
|
|
604
|
+
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
|
605
|
+
|
|
606
|
+
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
|
607
|
+
|
|
608
|
+
# GitHub issue reporting
|
|
609
|
+
GITHUB_ISSUE_REPORTING_ENABLED = True
|
|
610
|
+
GITHUB_ISSUE_REPORTING_COOLDOWN = 3600 # seconds
|
|
611
|
+
|
|
612
|
+
# Logging configuration
|
|
613
|
+
LOG_DIR = select_log_dir(BASE_DIR)
|
|
614
|
+
os.environ.setdefault("ARTHEXIS_LOG_DIR", str(LOG_DIR))
|
|
615
|
+
OLD_LOG_DIR = LOG_DIR / "old"
|
|
616
|
+
OLD_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
617
|
+
LOG_FILE_NAME = "tests.log" if "test" in sys.argv else f"{socket.gethostname()}.log"
|
|
618
|
+
|
|
619
|
+
LOGGING = {
|
|
620
|
+
"version": 1,
|
|
621
|
+
"disable_existing_loggers": False,
|
|
622
|
+
"formatters": {
|
|
623
|
+
"standard": {
|
|
624
|
+
"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
"handlers": {
|
|
628
|
+
"file": {
|
|
629
|
+
"class": "config.logging.ActiveAppFileHandler",
|
|
630
|
+
"filename": str(LOG_DIR / LOG_FILE_NAME),
|
|
631
|
+
"when": "midnight",
|
|
632
|
+
"backupCount": 7,
|
|
633
|
+
"encoding": "utf-8",
|
|
634
|
+
"formatter": "standard",
|
|
635
|
+
},
|
|
636
|
+
"error_file": {
|
|
637
|
+
"class": "config.logging.ErrorFileHandler",
|
|
638
|
+
"filename": str(LOG_DIR / "error.log"),
|
|
639
|
+
"when": "midnight",
|
|
640
|
+
"backupCount": 7,
|
|
641
|
+
"encoding": "utf-8",
|
|
642
|
+
"formatter": "standard",
|
|
643
|
+
"level": "ERROR",
|
|
644
|
+
},
|
|
645
|
+
"console": {
|
|
646
|
+
"class": "logging.StreamHandler",
|
|
647
|
+
"level": "ERROR",
|
|
648
|
+
"formatter": "standard",
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
"root": {
|
|
652
|
+
"handlers": ["file", "error_file", "console"],
|
|
653
|
+
"level": "DEBUG",
|
|
654
|
+
},
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
# Celery configuration
|
|
659
|
+
CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "memory://")
|
|
660
|
+
CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", "cache+memory://")
|
|
661
|
+
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
|
|
662
|
+
|
|
663
|
+
CELERY_BEAT_SCHEDULE = {
|
|
664
|
+
"heartbeat": {
|
|
665
|
+
"task": "core.tasks.heartbeat",
|
|
666
|
+
"schedule": crontab(minute="*/5"),
|
|
667
|
+
},
|
|
668
|
+
"ocpp_configuration_check": {
|
|
669
|
+
"task": "ocpp.tasks.schedule_daily_charge_point_configuration_checks",
|
|
670
|
+
"schedule": crontab(minute=0, hour=0),
|
|
671
|
+
},
|
|
672
|
+
"ocpp_forwarding_push": {
|
|
673
|
+
"task": "ocpp.tasks.push_forwarded_charge_points",
|
|
674
|
+
"schedule": timedelta(seconds=5),
|
|
675
|
+
},
|
|
676
|
+
}
|