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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arthexis
3
- Version: 0.1.17
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.61
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.9
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.10
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.16.0
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==5.9.8
68
+ Requires-Dist: psutil==7.1.1
69
69
  Requires-Dist: psycopg==3.2.9
70
- Requires-Dist: psycopg-binary==3.2.9
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.9.0
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.1
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.9.0
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]` &mdash; returns a field or attribute. Hyphens and casing are normalized automatically.
222
+ - `[PREFIX=IDENTIFIER.FIELD]` &mdash; selects a specific record by primary key or any unique field.
223
+ - `[PREFIX:FIELD=VALUE.ATTRIBUTE]` &mdash; filters by a custom field instead of the primary key.
224
+ - `[PREFIX.FIELD=[OTHER.SIGIL]]` &mdash; nests sigils so the value after `=` resolves before the outer token.
225
+ - `[PREFIX]` &mdash; 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 &rarr; 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.17.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
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=mDU5tye8H4WCjpJqocwd0vmrzoVEYwdz9WTP4Hcr6dI,719
11
+ config/middleware.py,sha256=zF8Cma0n5G8NNdh2LVeNJi7Hgl1G4mF9msRE2eRi1RU,2328
12
12
  config/offline.py,sha256=X-yDcyoI4C44Y27lpkUwszY_09GwwFfazEsthKJpQ70,1382
13
- config/settings.py,sha256=WFywFlWRTkEqDksWYAOd6DdSpHLEu2ETgLeeSGruwrA,21516
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=lXl2KKsbIehjOW0W6FHAsxkZJ-3DAo37f2ICb1dvvz8,5320
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=tEMop2REJ-P4o73ZWKZQuWm29zF3S-dWKu26wUDfvi4,145556
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=Vl_Z_tLo93w2ZDgObFFIfLSzid3KXraByghMmJeCfNk,126647
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=tqx8-4kyViMGKI3EAaxztrbyes4TSTPQ9YsIKzdVs6c,35731
47
- core/tasks.py,sha256=MtijKTtRHUEsTP4nVJFYx5B8Ls8EXmtzpBuq8FU5b9s,12302
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=ojHab5JtcHuVDc1zXK_QVms1cID1XXjuRcHmPcqVqD0,98368
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=Gt2J51RyIOsR2gzl2q3ChPbbIVDzrscty3yIGRW07P8,87141
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=liGwvusPI3o0RAxhamA8Hs1CMev_DRiFbbNhHWNqIfk,62079
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=1aoifwRm_VHhYCcP4i7P7ZbiDFRLQkTO57FozknWd9A,62840
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=deqjQVAt6sXyI_DdY9zj-Ha3ad1TlnVbhjGhJ-LFqKg,155932
71
- nodes/urls.py,sha256=HmAxj6sr6nMf0lii_1UX7sNBJUcrkaiKm3R9ofUWhvM,677
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=TyW7exkVaR-o2_XkJXSi9jQ_BygXOE2cQFs4xlI20Xc,22905
73
+ nodes/views.py,sha256=qJnAW5lIO5Up6n3IiTutERHZJTU-eZ_3ChpuZBIZ3oU,22526
74
74
  ocpp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
- ocpp/admin.py,sha256=gMxHkT5KSp4kPWJcDJ1Y65VqgrwFTZl8Y516FO8oi3g,34658
75
+ ocpp/admin.py,sha256=2gnFJFvDDTJmmMuuPVf9ZEuKLLsc6Z7_HFYSlg6TlwE,35336
76
76
  ocpp/apps.py,sha256=i3NqrmIamNEQBT33CIqh7HOSOPmJXCMKrZ-DUd3whqg,842
77
- ocpp/consumers.py,sha256=LgplrJQOfs8CKCtmBcRQLcDVB4Tz7YZpb3I7r2lAorQ,66352
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=QjEaygY7Tl47Q6z2uxP6ftUn4JeD8-JQX2fcwrCaEEg,31631
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=rHrP2Iq2ycMFbal1UEJVXb7r4gDtI5yifaE3nT0tjJw,18855
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=0Zczbg1x_5vhIV5TITHeaUkNONMdx20pD4St7Zc-ghM,36524
89
- ocpp/tests.py,sha256=BfOapD5vWrmA43Q4WI7or2lP2pmcBdrU5_YCK31JgHE,182621
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=wntIO3LHFoPAg40SFGMoRPAiA3xyDKPwNgIsCBSEOcI,57164
92
+ ocpp/views.py,sha256=TRsfkm8VpygNPW7WIK_hQGX5a2ohrKnTcPfTM11WXVc,60141
93
93
  pages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
94
- pages/admin.py,sha256=f2IYr-nGg9FmafQfDmIRrv01UuXh4mdhFJbnw-ytzHU,27459
95
- pages/apps.py,sha256=AzUNXQX0yRUX5jus-5EDReDb0nOEY8DBgYaM970u6Io,288
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=oINGTI0owXz60FV-XFEjnTkY2xlSDE-W6X1TK8IK800,5072
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=6PMLiyuHAHbfLeHwwQxIVy2fJ32ramEO9SHAN05Set4,6967
101
- pages/models.py,sha256=Sp8e2VB5a7yg4eSUlz_VcsSlAuDVap26xBKYYggxmLM,20952
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=i1HLp8rNm63WuqFf9YgAEGWYowFC7SOyyg_7j17_buQ,126102
105
- pages/urls.py,sha256=Ne6yYJxgUAMieDpppJ149E-yh-oVi92fARiRPe-n4-s,1166
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=Ye7qGlO7IwkZO0oR1SzCpkEDTtGCJPmDJT-x6QQ8vaQ,45848
108
- arthexis-0.1.17.dist-info/METADATA,sha256=rvPpFn4fwAZGm0JJ6Esu1r1A80uFC-tWCYhJOj0UNd8,9998
109
- arthexis-0.1.17.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
110
- arthexis-0.1.17.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
111
- arthexis-0.1.17.dist-info/RECORD,,
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
- self.rfid = self.rfid.upper()
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")