arthexis 0.1.17__py3-none-any.whl → 0.1.19__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.17.dist-info → arthexis-0.1.19.dist-info}/METADATA +37 -10
- {arthexis-0.1.17.dist-info → arthexis-0.1.19.dist-info}/RECORD +35 -34
- config/middleware.py +47 -1
- config/settings.py +2 -5
- config/urls.py +5 -0
- core/admin.py +1 -1
- core/models.py +31 -1
- core/system.py +125 -0
- core/tasks.py +0 -22
- core/tests.py +9 -0
- core/views.py +87 -19
- nodes/admin.py +1 -2
- nodes/models.py +18 -23
- nodes/tests.py +42 -34
- nodes/urls.py +0 -1
- nodes/views.py +2 -15
- ocpp/admin.py +23 -2
- ocpp/consumers.py +63 -19
- ocpp/models.py +7 -0
- ocpp/store.py +6 -4
- ocpp/test_rfid.py +70 -0
- ocpp/tests.py +107 -1
- ocpp/views.py +84 -10
- pages/admin.py +150 -15
- pages/apps.py +3 -0
- pages/context_processors.py +11 -0
- pages/middleware.py +3 -0
- pages/models.py +35 -0
- pages/site_config.py +137 -0
- pages/tests.py +352 -30
- pages/urls.py +2 -1
- pages/views.py +70 -23
- {arthexis-0.1.17.dist-info → arthexis-0.1.19.dist-info}/WHEEL +0 -0
- {arthexis-0.1.17.dist-info → arthexis-0.1.19.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.17.dist-info → arthexis-0.1.19.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.19
|
|
4
4
|
Summary: Power & Energy Infrastructure
|
|
5
5
|
Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
|
|
6
6
|
License-Expression: GPL-3.0-only
|
|
@@ -15,7 +15,7 @@ Requires-Dist: amqp==5.3.1
|
|
|
15
15
|
Requires-Dist: annotated-types==0.7.0
|
|
16
16
|
Requires-Dist: anyio==4.9.0
|
|
17
17
|
Requires-Dist: asgiref==3.10.0
|
|
18
|
-
Requires-Dist: atproto==0.0.
|
|
18
|
+
Requires-Dist: atproto==0.0.62
|
|
19
19
|
Requires-Dist: attrs==25.3.0
|
|
20
20
|
Requires-Dist: autobahn==24.4.2
|
|
21
21
|
Requires-Dist: Automat==25.4.16
|
|
@@ -41,7 +41,7 @@ Requires-Dist: defusedxml==0.7.1
|
|
|
41
41
|
Requires-Dist: Django==5.2.7
|
|
42
42
|
Requires-Dist: django-celery-beat==2.8.1
|
|
43
43
|
Requires-Dist: django-debug-toolbar==6.0.0
|
|
44
|
-
Requires-Dist: django-import-export==4.3.
|
|
44
|
+
Requires-Dist: django-import-export==4.3.12
|
|
45
45
|
Requires-Dist: django-object-actions==5.0.0
|
|
46
46
|
Requires-Dist: django-otp==1.5.4
|
|
47
47
|
Requires-Dist: django-timezone-field==7.1
|
|
@@ -53,28 +53,28 @@ Requires-Dist: h11==0.16.0
|
|
|
53
53
|
Requires-Dist: httpcore==1.0.9
|
|
54
54
|
Requires-Dist: httpx==0.28.1
|
|
55
55
|
Requires-Dist: hyperlink==21.0.0
|
|
56
|
-
Requires-Dist: idna==3.
|
|
56
|
+
Requires-Dist: idna==3.11
|
|
57
57
|
Requires-Dist: incremental==24.7.2
|
|
58
58
|
Requires-Dist: kombu==5.5.4
|
|
59
59
|
Requires-Dist: libipld==3.1.1
|
|
60
60
|
Requires-Dist: Markdown==3.8.2
|
|
61
61
|
Requires-Dist: mdx_truly_sane_lists==1.3
|
|
62
|
-
Requires-Dist: mcp==1.
|
|
62
|
+
Requires-Dist: mcp==1.18.0
|
|
63
63
|
Requires-Dist: mfrc522==0.0.7; sys_platform == "linux"
|
|
64
64
|
Requires-Dist: outcome==1.3.0.post0
|
|
65
65
|
Requires-Dist: packaging==25.0
|
|
66
66
|
Requires-Dist: pillow==11.3.0
|
|
67
67
|
Requires-Dist: prompt_toolkit==3.0.51
|
|
68
|
-
Requires-Dist: psutil==
|
|
68
|
+
Requires-Dist: psutil==7.1.1
|
|
69
69
|
Requires-Dist: psycopg==3.2.9
|
|
70
|
-
Requires-Dist: psycopg-binary==3.2.
|
|
70
|
+
Requires-Dist: psycopg-binary==3.2.11
|
|
71
71
|
Requires-Dist: pyasn1==0.6.1
|
|
72
72
|
Requires-Dist: pyasn1_modules==0.4.2
|
|
73
73
|
Requires-Dist: pycparser==2.22
|
|
74
74
|
Requires-Dist: pydantic==2.11.7
|
|
75
75
|
Requires-Dist: pydantic_core==2.33.2
|
|
76
76
|
Requires-Dist: pyOpenSSL==25.1.0
|
|
77
|
-
Requires-Dist: pyperclip==1.
|
|
77
|
+
Requires-Dist: pyperclip==1.11.0
|
|
78
78
|
Requires-Dist: PySocks==1.7.1
|
|
79
79
|
Requires-Dist: python-crontab==3.3.0
|
|
80
80
|
Requires-Dist: python-dateutil==2.9.0.post0
|
|
@@ -99,7 +99,7 @@ Requires-Dist: trio-websocket==0.12.2
|
|
|
99
99
|
Requires-Dist: Twisted==25.5.0
|
|
100
100
|
Requires-Dist: twine==6.1.0
|
|
101
101
|
Requires-Dist: txaio==25.6.1
|
|
102
|
-
Requires-Dist: typing-inspection==0.4.
|
|
102
|
+
Requires-Dist: typing-inspection==0.4.2
|
|
103
103
|
Requires-Dist: typing_extensions==4.14.1
|
|
104
104
|
Requires-Dist: tzdata==2025.2
|
|
105
105
|
Requires-Dist: urllib3==2.5.0
|
|
@@ -108,7 +108,7 @@ Requires-Dist: wcwidth==0.2.13
|
|
|
108
108
|
Requires-Dist: webencodings==0.5.1
|
|
109
109
|
Requires-Dist: websocket-client==1.8.0
|
|
110
110
|
Requires-Dist: websockets==13.1
|
|
111
|
-
Requires-Dist: whitenoise==6.
|
|
111
|
+
Requires-Dist: whitenoise==6.11.0
|
|
112
112
|
Requires-Dist: plyer==2.1.0; sys_platform == "win32"
|
|
113
113
|
Requires-Dist: wsproto==1.2.0
|
|
114
114
|
Requires-Dist: zope.interface==7.2
|
|
@@ -212,10 +212,37 @@ Terminal nodes can start directly with the scripts below without installing; Con
|
|
|
212
212
|
### 4. Administration
|
|
213
213
|
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.
|
|
214
214
|
|
|
215
|
+
## Sigils
|
|
216
|
+
|
|
217
|
+
Sigils are bracketed tokens such as `[ENV.SMTP_PASSWORD]` that Arthexis expands at runtime. They make it possible to reference configuration secrets, system metadata, or records stored in other apps without duplicating values across the project.
|
|
218
|
+
|
|
219
|
+
### Syntax at a glance
|
|
220
|
+
|
|
221
|
+
- `[PREFIX.KEY]` — returns a field or attribute. Hyphens and casing are normalized automatically.
|
|
222
|
+
- `[PREFIX=IDENTIFIER.FIELD]` — selects a specific record by primary key or any unique field.
|
|
223
|
+
- `[PREFIX:FIELD=VALUE.ATTRIBUTE]` — filters by a custom field instead of the primary key.
|
|
224
|
+
- `[PREFIX.FIELD=[OTHER.SIGIL]]` — nests sigils so the value after `=` resolves before the outer token.
|
|
225
|
+
- `[PREFIX]` — for entity prefixes, returns the serialized object in JSON; for configuration prefixes, resolves to an empty string when the key is missing.
|
|
226
|
+
|
|
227
|
+
The platform ships with three configuration prefixes:
|
|
228
|
+
|
|
229
|
+
- `ENV` reads environment variables.
|
|
230
|
+
- `CONF` reads Django settings.
|
|
231
|
+
- `SYS` exposes computed system information such as build metadata.
|
|
232
|
+
|
|
233
|
+
Additional prefixes are defined through **Sigil Roots**, which map a short code (for example `ROLE`, `ODOO`, or `USER`) to a Django model. You can review them from **Admin → Sigil Builder** (`/admin/sigil-builder/`), where a test console is also available.
|
|
234
|
+
|
|
235
|
+
Unknown prefixes remain in place (e.g. `[UNKNOWN.VALUE]`) and are logged. When the optional `gway` CLI is installed, the resolver will attempt to delegate unresolved tokens to it before falling back to the original text.
|
|
236
|
+
|
|
215
237
|
## Support
|
|
216
238
|
|
|
217
239
|
Contact us at [tecnologia@gelectriic.com](mailto:tecnologia@gelectriic.com) or visit our [web page](https://www.gelectriic.com/) for [professional services](https://en.wikipedia.org/wiki/Professional_services) and [commercial support](https://en.wikipedia.org/wiki/Technical_support).
|
|
218
240
|
|
|
241
|
+
## Project Guidelines
|
|
242
|
+
|
|
243
|
+
- [AGENTS](AGENTS.md) – operating handbook for repository workflows, testing, and release management.
|
|
244
|
+
- [DESIGN](DESIGN.md) – visual, UX, and branding guidance that all interfaces must follow.
|
|
245
|
+
|
|
219
246
|
## About Me
|
|
220
247
|
|
|
221
248
|
> "What, you want to know about me too? Well, I enjoy [developing software](https://en.wikipedia.org/wiki/Software_development), [role-playing games](https://en.wikipedia.org/wiki/Role-playing_game), long walks on the [beach](https://en.wikipedia.org/wiki/Beach) and a fourth secret thing."
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
arthexis-0.1.
|
|
1
|
+
arthexis-0.1.19.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
2
2
|
config/__init__.py,sha256=AwpOX7il-DAOmkdJ5dVfVJ3CWWebn1lHyQNmkw1EkDw,103
|
|
3
3
|
config/active_app.py,sha256=KJqYh-o91nPQjVXPEdbiJHzsI6cN9IZsBZ9O3iZ6Hyc,373
|
|
4
4
|
config/asgi.py,sha256=T-0QSbtieEWKPIDkEcEdd-q6qjK8ZCwwjCaISOBkWdM,1296
|
|
@@ -8,14 +8,14 @@ config/context_processors.py,sha256=p74ocuzPRFI9vKSeIaJ42Vu0V2GtGph1t-2DkRo4NMw,
|
|
|
8
8
|
config/horologia_app.py,sha256=puO_hObEYcLvE7PqcY_sGv1thnxJ018YKHKZWqNXha4,187
|
|
9
9
|
config/loadenv.py,sha256=CjXx-wBaTt1wixub4GJ5CMSMFqtiK5JURc7cPXpqO7s,287
|
|
10
10
|
config/logging.py,sha256=1cIbPgRshHuMKnVEEH0jKpRAlJSpewvLFbYDz7sCBG4,2104
|
|
11
|
-
config/middleware.py,sha256=
|
|
11
|
+
config/middleware.py,sha256=zF8Cma0n5G8NNdh2LVeNJi7Hgl1G4mF9msRE2eRi1RU,2328
|
|
12
12
|
config/offline.py,sha256=X-yDcyoI4C44Y27lpkUwszY_09GwwFfazEsthKJpQ70,1382
|
|
13
|
-
config/settings.py,sha256=
|
|
13
|
+
config/settings.py,sha256=620NySqqkpp-U-hDRyHp5M-U_JBweqvyxi4FUJ2rc_M,21454
|
|
14
14
|
config/settings_helpers.py,sha256=0BdBciUHIkwsWa0vV_RKAd4wDuEzgE7G-42XYiES4YQ,3127
|
|
15
|
-
config/urls.py,sha256=
|
|
15
|
+
config/urls.py,sha256=zvU4FSMKPlXUrGDjUgJCRFQztWb78wo1urW2DQf8qdI,5463
|
|
16
16
|
config/wsgi.py,sha256=zU_mKlya6hejQ21PxKacTui3dUWd4ca_-YJNSYAoMX0,433
|
|
17
17
|
core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
core/admin.py,sha256=
|
|
18
|
+
core/admin.py,sha256=tVoWAdi4LzNaGQBOkvyIAmmALn3cXj-fU9rdYUPBYNg,145572
|
|
19
19
|
core/admin_history.py,sha256=XZ4b0ryufIka-xcwboK3DzmOL-INSx5Y2fJO-aJdV70,1783
|
|
20
20
|
core/admindocs.py,sha256=ycD0bJ_VE6rTGf9ebXTiKdYkD8Y8hD2oQ4HxxoBURCM,6756
|
|
21
21
|
core/apps.py,sha256=L_UMYI72-5jTo6nt8mfCbgdLhlP32D-8k76EZw0QyAA,14348
|
|
@@ -34,7 +34,7 @@ core/liveupdate.py,sha256=22m0ueQ10-6b-9pQJHY0_5WRYA98fysXKEXOWzIr550,691
|
|
|
34
34
|
core/log_paths.py,sha256=lxvgXPgJtVNZ-kYrqV8VFle4GFQrSxG-yRTglqvclmU,3318
|
|
35
35
|
core/mailer.py,sha256=JpW0RnD9uZ4O-wvlqeW7CMw95IFeCSkdvbankJDwHq0,2886
|
|
36
36
|
core/middleware.py,sha256=j19K9SX-Emkv7BDDtAacR9g6RWsxhKHwCc8w23JFvMM,3388
|
|
37
|
-
core/models.py,sha256=
|
|
37
|
+
core/models.py,sha256=QIZazU5lwTaNQyO68Fm0Jg2AXDAjvg_HsK_djnKIv_Q,127829
|
|
38
38
|
core/notifications.py,sha256=LYktoKM5k4q7YYWAJuqdeKM-p0Q-3gXgfqdq71qLS68,3916
|
|
39
39
|
core/public_wifi.py,sha256=yydLgxOo9DmJJbM4X_23wGR3gxL3YzHno54v9GssuFA,7213
|
|
40
40
|
core/reference_utils.py,sha256=jeox3V4cZNxzM2Jj31g_mdb3O55zy9S2iXAZu70R1Zc,3627
|
|
@@ -43,69 +43,70 @@ core/rfid_import_export.py,sha256=petyhPvL0WUpehc6uGUDUhjYQ9AVvc6O49zuhDs6YFw,35
|
|
|
43
43
|
core/sigil_builder.py,sha256=VLwbrrD7Zr3SHfIDYV-V7uv7LEGiIelCSkeGswHibuc,4843
|
|
44
44
|
core/sigil_context.py,sha256=GCzjfM6fcVvBtSbVNfmE6sx3HU8QnxnXrCIytnNpQzM,439
|
|
45
45
|
core/sigil_resolver.py,sha256=rCsypuX-0oWNfKyM1T9ZLWHY0Ezwhtk4VmI0L3krnsE,11098
|
|
46
|
-
core/system.py,sha256=
|
|
47
|
-
core/tasks.py,sha256=
|
|
46
|
+
core/system.py,sha256=RyBqooWezM0Li_KCRskchD5Lub0cdqBmrnU6ilC8MPE,39823
|
|
47
|
+
core/tasks.py,sha256=Vjv3wQaK-eRs3osSTfVbtgzBoBQvTHXHbrOKxDY7V00,11617
|
|
48
48
|
core/temp_passwords.py,sha256=FieUnIUeQHmA1DoXvfJ5U6-Ayv3oDz-hSln5s_vNbA4,5271
|
|
49
49
|
core/test_system_info.py,sha256=IMPz21KEs6OC5YbL7YaIBdmJVLjRY6MgPuZpldJB5OI,6935
|
|
50
|
-
core/tests.py,sha256=
|
|
50
|
+
core/tests.py,sha256=H0vPRcwcD3HNscRInbMGvdzs9ixvmkLh3d4YSEeQfEc,98679
|
|
51
51
|
core/tests_liveupdate.py,sha256=IquU8ztk6zbzC1bQu3Nrr3RzGzuujtPwDkANJHbxg98,510
|
|
52
52
|
core/urls.py,sha256=YPippON1MAP2KeZZ8jHpcLO6mvbnKn1q7fdMv5Vm9dY,425
|
|
53
53
|
core/user_data.py,sha256=02CfvxayELWSWZJCxWpv1Yz7EGg08yEu5MM31Khsi0U,21083
|
|
54
|
-
core/views.py,sha256=
|
|
54
|
+
core/views.py,sha256=oH210iFack4Yvt4ug5DGM3mx24GhPGFhZOlzi4WMV_w,89298
|
|
55
55
|
core/widgets.py,sha256=vlR9PlFfZGlkHm5X2cqNXuEBZSj8gmWaR6MO1mMy6kg,6904
|
|
56
56
|
core/workgroup_urls.py,sha256=XR9IqwsSBI8epW7_-hHhWFU9wsyJfZehHwNQBhCgmpM,407
|
|
57
57
|
core/workgroup_views.py,sha256=vtumF3-8YaTD-K6nSd8eYvUyq3ftpvWSEwtcp5B-P6o,2889
|
|
58
58
|
nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
|
-
nodes/admin.py,sha256=
|
|
59
|
+
nodes/admin.py,sha256=QuBB7JMqUvu2io6ZoZVTdqXLOX0CaB-394yUuwiRqDM,62022
|
|
60
60
|
nodes/apps.py,sha256=AxK-sh9JBJZwNOLjqw9omCQGUQWw-45VRdYH07XhVJU,2732
|
|
61
61
|
nodes/backends.py,sha256=dmmbS0X2YIlCDz2KjoDf_L62dy--nuqZF1rEDoi2JHM,5921
|
|
62
62
|
nodes/dns.py,sha256=D5smXD7Rkh6E4MdL6TBL2WY8GgJg7Rx9z88LZrcMbTw,7048
|
|
63
63
|
nodes/feature_checks.py,sha256=27e4PCkZ8BGWnJCOwMcY2Bo9z7LoeZWiTZuISWGnrzk,3996
|
|
64
64
|
nodes/lcd.py,sha256=iKA8Wmq85KZD52aTzAU8ZmS144_gbdGMOXcE8yuECps,5758
|
|
65
|
-
nodes/models.py,sha256=
|
|
65
|
+
nodes/models.py,sha256=ay2FE-M0I82SR3__BPdfrXAaOvBL4CKua9_xOOm3mKg,62593
|
|
66
66
|
nodes/reports.py,sha256=NRYh3Y0SlZFhx31Zh2K03yO12ZrpxEHEY6T-dODA6WE,12059
|
|
67
67
|
nodes/rfid_sync.py,sha256=SP_BRUhgYMBH-zXrcM7uShgFSGYmmuIMb1OdcU1e-5U,6956
|
|
68
68
|
nodes/signals.py,sha256=PtOKdQfb08mV1LgSZvn7ZAcfOyy2c3Xkq4AOpBQyUdE,622
|
|
69
69
|
nodes/tasks.py,sha256=ur59ebu9z02idmvy_IvUQt3eu9LWRyyNpkg2szvIHCQ,1522
|
|
70
|
-
nodes/tests.py,sha256=
|
|
71
|
-
nodes/urls.py,sha256=
|
|
70
|
+
nodes/tests.py,sha256=w_vUQNatk2K-FIu1yd1I4iD95q68DQ0H9uDYx1Cl3gA,156975
|
|
71
|
+
nodes/urls.py,sha256=OaPypFt5gcmVe87NDzt1WMiC3YwHezih4jV_pGkICcE,601
|
|
72
72
|
nodes/utils.py,sha256=wt7UuSXGuq79A-g-B6EW3kK49QWJBb7zhhkw4pun4k8,4474
|
|
73
|
-
nodes/views.py,sha256=
|
|
73
|
+
nodes/views.py,sha256=qJnAW5lIO5Up6n3IiTutERHZJTU-eZ_3ChpuZBIZ3oU,22526
|
|
74
74
|
ocpp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
75
|
-
ocpp/admin.py,sha256=
|
|
75
|
+
ocpp/admin.py,sha256=2gnFJFvDDTJmmMuuPVf9ZEuKLLsc6Z7_HFYSlg6TlwE,35336
|
|
76
76
|
ocpp/apps.py,sha256=i3NqrmIamNEQBT33CIqh7HOSOPmJXCMKrZ-DUd3whqg,842
|
|
77
|
-
ocpp/consumers.py,sha256=
|
|
77
|
+
ocpp/consumers.py,sha256=s51784IwUoWfLaMI_zfnCve7ouZIB5xAnlgwThb40Gw,67979
|
|
78
78
|
ocpp/evcs.py,sha256=q1mZrCVSZxXTrtYsDqH6lkeEcJ6tfSC7p9YxkDmpSCw,28883
|
|
79
79
|
ocpp/evcs_discovery.py,sha256=OmrzgaOHwveDRJs8AIhrM3apX8_k2PPXh_oYaYpNW3c,3876
|
|
80
|
-
ocpp/models.py,sha256=
|
|
80
|
+
ocpp/models.py,sha256=6BG-maPpcalLFoEHcbpecuXqia9mQx2ZATOu2pvUaBU,31846
|
|
81
81
|
ocpp/reference_utils.py,sha256=_UR82GfE93kv4766mHyVIfdhhyYvrT59660r3H6W55M,1072
|
|
82
82
|
ocpp/routing.py,sha256=3kQya-MdJ00778xDmX0esQLBP05P200V45asg-CGNoo,438
|
|
83
83
|
ocpp/simulator.py,sha256=vnyd59QffT79AaPhmfM_jipni_nqfG57X5tXyx1rBoc,28016
|
|
84
84
|
ocpp/status_display.py,sha256=YGFosd5HJETA0DcLdsjvx6EfhZSnI8Aa3cMnHG2WsBE,939
|
|
85
|
-
ocpp/store.py,sha256=
|
|
85
|
+
ocpp/store.py,sha256=gLCSaP9KKF7li2ALlE3O3RW5eVJtoe-_YHfKhdf0VOM,18943
|
|
86
86
|
ocpp/tasks.py,sha256=OxIaI4OSLz9AfwLexnXhiBILBimTs3gVrPd197Jguqg,5819
|
|
87
87
|
ocpp/test_export_import.py,sha256=Zp6xUBlRq7XkdKjOs78BhkujNQdklxi4RLxU8c-udWY,4530
|
|
88
|
-
ocpp/test_rfid.py,sha256=
|
|
89
|
-
ocpp/tests.py,sha256=
|
|
88
|
+
ocpp/test_rfid.py,sha256=hMFQwYDPhwfTW2XdDT5q__gKrL8YPbv7DNNMdwzJ7BQ,39105
|
|
89
|
+
ocpp/tests.py,sha256=Y9Z4hfeiqRRCFP8utsVadFTLcKBOwTVIQuDJK3dYqas,186492
|
|
90
90
|
ocpp/transactions_io.py,sha256=YnxI-Tv5UFxv0JuFK3XpoqFYP8eRT8sMuDiqkiMHPtU,7387
|
|
91
91
|
ocpp/urls.py,sha256=3T5O5DSwVk4PbhPx5p4D3UseCWvC5xV5HwJLSM6AfA8,1700
|
|
92
|
-
ocpp/views.py,sha256=
|
|
92
|
+
ocpp/views.py,sha256=TRsfkm8VpygNPW7WIK_hQGX5a2ohrKnTcPfTM11WXVc,60141
|
|
93
93
|
pages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
94
|
-
pages/admin.py,sha256=
|
|
95
|
-
pages/apps.py,sha256=
|
|
94
|
+
pages/admin.py,sha256=E-jcxUI-89gFgqGI38HMSHbfDw1inpSUiRR83utJ0fc,32512
|
|
95
|
+
pages/apps.py,sha256=o8gQP-VdZOk9LXIEo6IDmOSqX3TP8XypBvKGGWLoQ0k,351
|
|
96
96
|
pages/checks.py,sha256=sM8_hUVM_HOIocvtTb2sY3AaSEvbTnOlO46UchGVd-0,1527
|
|
97
|
-
pages/context_processors.py,sha256=
|
|
97
|
+
pages/context_processors.py,sha256=JELk1V5R5yOVIm5h3JfikG1_V6QSUN3LDlKWF8p9zCE,5543
|
|
98
98
|
pages/defaults.py,sha256=l36APPAZO4ub2A8Pp-lQGujKeOVYcyzU6t7-kOk8VoA,522
|
|
99
99
|
pages/forms.py,sha256=T0atqxdNds3IBP8N-9c5-ACf3iR9FzzmhzK4MOa24e8,7058
|
|
100
|
-
pages/middleware.py,sha256=
|
|
101
|
-
pages/models.py,sha256=
|
|
100
|
+
pages/middleware.py,sha256=MYd5Nko4AnFg3orY6MuyvvNg_I6GCIf8mDW8znSOgvQ,7042
|
|
101
|
+
pages/models.py,sha256=Ms_m_tzzstNghN_JOzyfwsbllDBIl_AKnYdTRTJthqM,22173
|
|
102
102
|
pages/module_defaults.py,sha256=R8n6eQDjNRMpO-DW86OFGvyRarju5Bx7Fnb275R_z24,5411
|
|
103
|
+
pages/site_config.py,sha256=f1Me0GFdHeGbIeyMlQNzD2e6hym59YHqbz92U_ppffY,4057
|
|
103
104
|
pages/tasks.py,sha256=ivcba_3wSQ1-cku0oDplzw6vLeQ9hBq3R4TG-LmR5gs,1913
|
|
104
|
-
pages/tests.py,sha256=
|
|
105
|
-
pages/urls.py,sha256=
|
|
105
|
+
pages/tests.py,sha256=5rRi1e_mSoS6VGykrz58VZTb428RJRR9zMi3-Pfg3FU,138871
|
|
106
|
+
pages/urls.py,sha256=BbfEl6SbNfyFo2acA8Wze6aHV2kJR-0339FCIOi6cD4,1231
|
|
106
107
|
pages/utils.py,sha256=CR4D1debgJLGgXsw9kap2ggpe7fIpSoWS_ivbgMNp2k,564
|
|
107
|
-
pages/views.py,sha256=
|
|
108
|
-
arthexis-0.1.
|
|
109
|
-
arthexis-0.1.
|
|
110
|
-
arthexis-0.1.
|
|
111
|
-
arthexis-0.1.
|
|
108
|
+
pages/views.py,sha256=hthdoV352cZRru-Wi3Wtv0ZXOjOba7z7rIpHLlzfuVo,47665
|
|
109
|
+
arthexis-0.1.19.dist-info/METADATA,sha256=ko-yFBbCoQEs9EUfs6BzAcFK5qfn_7uvRJcqbk6pgpQ,11757
|
|
110
|
+
arthexis-0.1.19.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
111
|
+
arthexis-0.1.19.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
|
|
112
|
+
arthexis-0.1.19.dist-info/RECORD,,
|
config/middleware.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
from utils.sites import get_site
|
|
2
1
|
import socket
|
|
2
|
+
from django.core.exceptions import DisallowedHost
|
|
3
|
+
from django.http import HttpResponsePermanentRedirect
|
|
3
4
|
from nodes.models import Node
|
|
5
|
+
from utils.sites import get_site
|
|
4
6
|
|
|
5
7
|
from .active_app import set_active_app
|
|
6
8
|
|
|
@@ -17,9 +19,53 @@ class ActiveAppMiddleware:
|
|
|
17
19
|
role_name = node.role.name if node and node.role else "Terminal"
|
|
18
20
|
active = site.name or role_name
|
|
19
21
|
set_active_app(active)
|
|
22
|
+
request.site = site
|
|
20
23
|
request.active_app = active
|
|
21
24
|
try:
|
|
22
25
|
response = self.get_response(request)
|
|
23
26
|
finally:
|
|
24
27
|
set_active_app(socket.gethostname())
|
|
25
28
|
return response
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _is_https_request(request) -> bool:
|
|
32
|
+
if request.is_secure():
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
forwarded_proto = request.META.get("HTTP_X_FORWARDED_PROTO", "")
|
|
36
|
+
if forwarded_proto:
|
|
37
|
+
candidate = forwarded_proto.split(",")[0].strip().lower()
|
|
38
|
+
if candidate == "https":
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
forwarded_header = request.META.get("HTTP_FORWARDED", "")
|
|
42
|
+
for forwarded_part in forwarded_header.split(","):
|
|
43
|
+
for element in forwarded_part.split(";"):
|
|
44
|
+
key, _, value = element.partition("=")
|
|
45
|
+
if key.strip().lower() == "proto" and value.strip().strip('"').lower() == "https":
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SiteHttpsRedirectMiddleware:
|
|
52
|
+
"""Redirect HTTP traffic to HTTPS for sites that require it."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, get_response):
|
|
55
|
+
self.get_response = get_response
|
|
56
|
+
|
|
57
|
+
def __call__(self, request):
|
|
58
|
+
site = getattr(request, "site", None)
|
|
59
|
+
if site is None:
|
|
60
|
+
site = get_site(request)
|
|
61
|
+
request.site = site
|
|
62
|
+
|
|
63
|
+
if getattr(site, "require_https", False) and not _is_https_request(request):
|
|
64
|
+
try:
|
|
65
|
+
host = request.get_host()
|
|
66
|
+
except DisallowedHost: # pragma: no cover - defensive guard
|
|
67
|
+
host = request.META.get("HTTP_HOST", "")
|
|
68
|
+
redirect_url = f"https://{host}{request.get_full_path()}"
|
|
69
|
+
return HttpResponsePermanentRedirect(redirect_url)
|
|
70
|
+
|
|
71
|
+
return self.get_response(request)
|
config/settings.py
CHANGED
|
@@ -390,6 +390,7 @@ MIDDLEWARE = [
|
|
|
390
390
|
"whitenoise.middleware.WhiteNoiseMiddleware",
|
|
391
391
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
|
392
392
|
"config.middleware.ActiveAppMiddleware",
|
|
393
|
+
"config.middleware.SiteHttpsRedirectMiddleware",
|
|
393
394
|
"django.middleware.locale.LocaleMiddleware",
|
|
394
395
|
"django.middleware.common.CommonMiddleware",
|
|
395
396
|
"django.middleware.csrf.CsrfViewMiddleware",
|
|
@@ -584,7 +585,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
|
|
584
585
|
LANGUAGE_CODE = "en-us"
|
|
585
586
|
|
|
586
587
|
LANGUAGES = [
|
|
587
|
-
("es", _("Spanish")),
|
|
588
|
+
("es", _("Spanish (Latin America)")),
|
|
588
589
|
("en", _("English")),
|
|
589
590
|
("it", _("Italian")),
|
|
590
591
|
("de", _("German")),
|
|
@@ -687,8 +688,4 @@ CELERY_BEAT_SCHEDULE = {
|
|
|
687
688
|
"task": "core.tasks.heartbeat",
|
|
688
689
|
"schedule": crontab(minute="*/5"),
|
|
689
690
|
},
|
|
690
|
-
"birthday_greetings": {
|
|
691
|
-
"task": "core.tasks.birthday_greetings",
|
|
692
|
-
"schedule": crontab(hour=9, minute=0),
|
|
693
|
-
},
|
|
694
691
|
}
|
config/urls.py
CHANGED
|
@@ -149,6 +149,11 @@ urlpatterns = [
|
|
|
149
149
|
core_views.odoo_quote_report,
|
|
150
150
|
name="odoo-quote-report",
|
|
151
151
|
),
|
|
152
|
+
path(
|
|
153
|
+
"admin/request-temp-password/",
|
|
154
|
+
core_views.request_temp_password,
|
|
155
|
+
name="admin-request-temp-password",
|
|
156
|
+
),
|
|
152
157
|
path("admin/", admin.site.urls),
|
|
153
158
|
path("i18n/setlang/", csrf_exempt(set_language), name="set_language"),
|
|
154
159
|
path("api/", include("core.workgroup_urls")),
|
core/admin.py
CHANGED
|
@@ -2947,7 +2947,7 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
|
|
|
2947
2947
|
"toggle_selected_released",
|
|
2948
2948
|
"toggle_selected_allowed",
|
|
2949
2949
|
]
|
|
2950
|
-
readonly_fields = ("added_on", "last_seen_on")
|
|
2950
|
+
readonly_fields = ("added_on", "last_seen_on", "reversed_uid")
|
|
2951
2951
|
form = RFIDForm
|
|
2952
2952
|
|
|
2953
2953
|
def get_import_resource_kwargs(self, request, form=None, **kwargs):
|
core/models.py
CHANGED
|
@@ -1775,6 +1775,14 @@ class RFID(Entity):
|
|
|
1775
1775
|
)
|
|
1776
1776
|
],
|
|
1777
1777
|
)
|
|
1778
|
+
reversed_uid = models.CharField(
|
|
1779
|
+
max_length=255,
|
|
1780
|
+
default="",
|
|
1781
|
+
blank=True,
|
|
1782
|
+
editable=False,
|
|
1783
|
+
verbose_name="Reversed UID",
|
|
1784
|
+
help_text="UID value stored with opposite endianness for reference.",
|
|
1785
|
+
)
|
|
1778
1786
|
custom_label = models.CharField(
|
|
1779
1787
|
max_length=32,
|
|
1780
1788
|
blank=True,
|
|
@@ -1906,7 +1914,16 @@ class RFID(Entity):
|
|
|
1906
1914
|
if self.key_b and old["key_b"] != self.key_b.upper():
|
|
1907
1915
|
self.key_b_verified = False
|
|
1908
1916
|
if self.rfid:
|
|
1909
|
-
|
|
1917
|
+
normalized_rfid = self.rfid.upper()
|
|
1918
|
+
self.rfid = normalized_rfid
|
|
1919
|
+
reversed_uid = self.reverse_uid(normalized_rfid)
|
|
1920
|
+
if reversed_uid != self.reversed_uid:
|
|
1921
|
+
self.reversed_uid = reversed_uid
|
|
1922
|
+
if update_fields:
|
|
1923
|
+
fields = set(update_fields)
|
|
1924
|
+
if "reversed_uid" not in fields:
|
|
1925
|
+
fields.add("reversed_uid")
|
|
1926
|
+
kwargs["update_fields"] = tuple(fields)
|
|
1910
1927
|
if self.key_a:
|
|
1911
1928
|
self.key_a = self.key_a.upper()
|
|
1912
1929
|
if self.key_b:
|
|
@@ -1933,6 +1950,19 @@ class RFID(Entity):
|
|
|
1933
1950
|
return candidate
|
|
1934
1951
|
return cls.BIG_ENDIAN
|
|
1935
1952
|
|
|
1953
|
+
@staticmethod
|
|
1954
|
+
def reverse_uid(value: str) -> str:
|
|
1955
|
+
"""Return ``value`` with reversed byte order for reference storage."""
|
|
1956
|
+
|
|
1957
|
+
normalized = "".join((value or "").split()).upper()
|
|
1958
|
+
if not normalized:
|
|
1959
|
+
return ""
|
|
1960
|
+
if len(normalized) % 2 != 0:
|
|
1961
|
+
return normalized[::-1]
|
|
1962
|
+
bytes_list = [normalized[index : index + 2] for index in range(0, len(normalized), 2)]
|
|
1963
|
+
bytes_list.reverse()
|
|
1964
|
+
return "".join(bytes_list)
|
|
1965
|
+
|
|
1936
1966
|
@classmethod
|
|
1937
1967
|
def next_scan_label(
|
|
1938
1968
|
cls, *, step: int | None = None, start: int | None = None
|
core/system.py
CHANGED
|
@@ -13,8 +13,10 @@ import shutil
|
|
|
13
13
|
import logging
|
|
14
14
|
from typing import Callable, Iterable, Optional
|
|
15
15
|
|
|
16
|
+
from django import forms
|
|
16
17
|
from django.conf import settings
|
|
17
18
|
from django.contrib import admin, messages
|
|
19
|
+
from django.forms import modelformset_factory
|
|
18
20
|
from django.template.response import TemplateResponse
|
|
19
21
|
from django.http import HttpResponseRedirect
|
|
20
22
|
from django.urls import path, reverse
|
|
@@ -32,6 +34,7 @@ from core.release import (
|
|
|
32
34
|
_remote_with_credentials,
|
|
33
35
|
)
|
|
34
36
|
from core.tasks import check_github_updates
|
|
37
|
+
from core.models import Todo
|
|
35
38
|
from utils import revision
|
|
36
39
|
|
|
37
40
|
|
|
@@ -1086,6 +1089,123 @@ def _system_upgrade_report_view(request):
|
|
|
1086
1089
|
return TemplateResponse(request, "admin/system_upgrade_report.html", context)
|
|
1087
1090
|
|
|
1088
1091
|
|
|
1092
|
+
class PendingTodoForm(forms.ModelForm):
|
|
1093
|
+
mark_done = forms.BooleanField(required=False, label=_("Approve"))
|
|
1094
|
+
|
|
1095
|
+
class Meta:
|
|
1096
|
+
model = Todo
|
|
1097
|
+
fields = [
|
|
1098
|
+
"request",
|
|
1099
|
+
"request_details",
|
|
1100
|
+
"url",
|
|
1101
|
+
"generated_for_version",
|
|
1102
|
+
"generated_for_revision",
|
|
1103
|
+
"on_done_condition",
|
|
1104
|
+
]
|
|
1105
|
+
widgets = {
|
|
1106
|
+
"request_details": forms.Textarea(attrs={"rows": 3}),
|
|
1107
|
+
"on_done_condition": forms.Textarea(attrs={"rows": 2}),
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
def __init__(self, *args, **kwargs):
|
|
1111
|
+
super().__init__(*args, **kwargs)
|
|
1112
|
+
for name in [
|
|
1113
|
+
"request",
|
|
1114
|
+
"url",
|
|
1115
|
+
"generated_for_version",
|
|
1116
|
+
"generated_for_revision",
|
|
1117
|
+
]:
|
|
1118
|
+
self.fields[name].widget.attrs.setdefault("class", "vTextField")
|
|
1119
|
+
for name in ["request_details", "on_done_condition"]:
|
|
1120
|
+
self.fields[name].widget.attrs.setdefault("class", "vLargeTextField")
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
PendingTodoFormSet = modelformset_factory(Todo, form=PendingTodoForm, extra=0)
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
def _system_pending_todos_report_view(request):
|
|
1127
|
+
queryset = (
|
|
1128
|
+
Todo.objects.filter(is_deleted=False, done_on__isnull=True)
|
|
1129
|
+
.order_by("request")
|
|
1130
|
+
)
|
|
1131
|
+
formset = PendingTodoFormSet(
|
|
1132
|
+
request.POST or None,
|
|
1133
|
+
queryset=queryset,
|
|
1134
|
+
prefix="todos",
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
if request.method == "POST":
|
|
1138
|
+
if formset.is_valid():
|
|
1139
|
+
approved_count = 0
|
|
1140
|
+
edited_count = 0
|
|
1141
|
+
for form in formset.forms:
|
|
1142
|
+
mark_done = form.cleaned_data.get("mark_done")
|
|
1143
|
+
todo = form.save(commit=False)
|
|
1144
|
+
has_changes = form.has_changed()
|
|
1145
|
+
if mark_done and todo.done_on is None:
|
|
1146
|
+
todo.done_on = timezone.now()
|
|
1147
|
+
approved_count += 1
|
|
1148
|
+
has_changes = True
|
|
1149
|
+
if has_changes:
|
|
1150
|
+
todo.save()
|
|
1151
|
+
if form.has_changed():
|
|
1152
|
+
edited_count += 1
|
|
1153
|
+
if has_changes and form.has_changed():
|
|
1154
|
+
form.save_m2m()
|
|
1155
|
+
|
|
1156
|
+
if approved_count or edited_count:
|
|
1157
|
+
message_parts: list[str] = []
|
|
1158
|
+
if edited_count:
|
|
1159
|
+
message_parts.append(
|
|
1160
|
+
ngettext(
|
|
1161
|
+
"%(count)d TODO updated.",
|
|
1162
|
+
"%(count)d TODOs updated.",
|
|
1163
|
+
edited_count,
|
|
1164
|
+
)
|
|
1165
|
+
% {"count": edited_count}
|
|
1166
|
+
)
|
|
1167
|
+
if approved_count:
|
|
1168
|
+
message_parts.append(
|
|
1169
|
+
ngettext(
|
|
1170
|
+
"%(count)d TODO approved.",
|
|
1171
|
+
"%(count)d TODOs approved.",
|
|
1172
|
+
approved_count,
|
|
1173
|
+
)
|
|
1174
|
+
% {"count": approved_count}
|
|
1175
|
+
)
|
|
1176
|
+
messages.success(request, " ".join(message_parts))
|
|
1177
|
+
else:
|
|
1178
|
+
messages.info(
|
|
1179
|
+
request,
|
|
1180
|
+
_("No changes were applied to the pending TODOs."),
|
|
1181
|
+
)
|
|
1182
|
+
return HttpResponseRedirect(reverse("admin:system-pending-todos-report"))
|
|
1183
|
+
else:
|
|
1184
|
+
messages.error(request, _("Please correct the errors below."))
|
|
1185
|
+
|
|
1186
|
+
rows = [
|
|
1187
|
+
{
|
|
1188
|
+
"form": form,
|
|
1189
|
+
"todo": form.instance,
|
|
1190
|
+
}
|
|
1191
|
+
for form in formset.forms
|
|
1192
|
+
]
|
|
1193
|
+
|
|
1194
|
+
context = admin.site.each_context(request)
|
|
1195
|
+
context.update(
|
|
1196
|
+
{
|
|
1197
|
+
"title": _("Pending TODOs Report"),
|
|
1198
|
+
"formset": formset,
|
|
1199
|
+
"rows": rows,
|
|
1200
|
+
}
|
|
1201
|
+
)
|
|
1202
|
+
return TemplateResponse(
|
|
1203
|
+
request,
|
|
1204
|
+
"admin/system_pending_todos_report.html",
|
|
1205
|
+
context,
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
|
|
1089
1209
|
def _trigger_upgrade_check() -> bool:
|
|
1090
1210
|
"""Return ``True`` when the upgrade check was queued asynchronously."""
|
|
1091
1211
|
|
|
@@ -1142,6 +1262,11 @@ def patch_admin_system_view() -> None:
|
|
|
1142
1262
|
admin.site.admin_view(_system_changelog_report_view),
|
|
1143
1263
|
name="system-changelog-report",
|
|
1144
1264
|
),
|
|
1265
|
+
path(
|
|
1266
|
+
"system/pending-todos-report/",
|
|
1267
|
+
admin.site.admin_view(_system_pending_todos_report_view),
|
|
1268
|
+
name="system-pending-todos-report",
|
|
1269
|
+
),
|
|
1145
1270
|
path(
|
|
1146
1271
|
"system/upgrade-report/",
|
|
1147
1272
|
admin.site.admin_view(_system_upgrade_report_view),
|
core/tasks.py
CHANGED
|
@@ -8,14 +8,9 @@ import urllib.error
|
|
|
8
8
|
import urllib.request
|
|
9
9
|
|
|
10
10
|
from celery import shared_task
|
|
11
|
-
from django.conf import settings
|
|
12
|
-
from django.contrib.auth import get_user_model
|
|
13
|
-
from core import mailer
|
|
14
11
|
from core import github_issues
|
|
15
12
|
from django.utils import timezone
|
|
16
13
|
|
|
17
|
-
from nodes.models import NetMessage
|
|
18
|
-
|
|
19
14
|
|
|
20
15
|
AUTO_UPGRADE_HEALTH_DELAY_SECONDS = 30
|
|
21
16
|
AUTO_UPGRADE_SKIP_LOCK_NAME = "auto_upgrade_skip_revisions.lck"
|
|
@@ -30,23 +25,6 @@ def heartbeat() -> None:
|
|
|
30
25
|
logger.info("Heartbeat task executed")
|
|
31
26
|
|
|
32
27
|
|
|
33
|
-
@shared_task
|
|
34
|
-
def birthday_greetings() -> None:
|
|
35
|
-
"""Send birthday greetings to users via Net Message and email."""
|
|
36
|
-
User = get_user_model()
|
|
37
|
-
today = timezone.localdate()
|
|
38
|
-
for user in User.objects.filter(birthday=today):
|
|
39
|
-
NetMessage.broadcast("Happy bday!", user.username)
|
|
40
|
-
if user.email:
|
|
41
|
-
mailer.send(
|
|
42
|
-
"Happy bday!",
|
|
43
|
-
f"Happy bday! {user.username}",
|
|
44
|
-
[user.email],
|
|
45
|
-
settings.DEFAULT_FROM_EMAIL,
|
|
46
|
-
fail_silently=True,
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
|
|
50
28
|
def _auto_upgrade_log_path(base_dir: Path) -> Path:
|
|
51
29
|
"""Return the log file used for auto-upgrade events."""
|
|
52
30
|
|
core/tests.py
CHANGED
|
@@ -550,6 +550,15 @@ class RFIDValidationTests(TestCase):
|
|
|
550
550
|
tag = RFID.objects.create(rfid="DEADBEEF10")
|
|
551
551
|
self.assertEqual(tag.rfid, "DEADBEEF10")
|
|
552
552
|
|
|
553
|
+
def test_reversed_uid_updates_with_rfid(self):
|
|
554
|
+
tag = RFID.objects.create(rfid="A1B2C3D4")
|
|
555
|
+
self.assertEqual(tag.reversed_uid, "D4C3B2A1")
|
|
556
|
+
|
|
557
|
+
tag.rfid = "112233"
|
|
558
|
+
tag.save(update_fields=["rfid"])
|
|
559
|
+
tag.refresh_from_db()
|
|
560
|
+
self.assertEqual(tag.reversed_uid, "332211")
|
|
561
|
+
|
|
553
562
|
def test_find_user_by_rfid(self):
|
|
554
563
|
user = User.objects.create_user(username="finder", password="pwd")
|
|
555
564
|
acc = EnergyAccount.objects.create(user=user, name="FINDER")
|