arthexis 0.1.16__py3-none-any.whl → 0.1.18__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.16.dist-info → arthexis-0.1.18.dist-info}/METADATA +1 -1
- {arthexis-0.1.16.dist-info → arthexis-0.1.18.dist-info}/RECORD +26 -25
- config/middleware.py +47 -1
- config/settings.py +4 -0
- config/urls.py +5 -0
- core/admin.py +69 -9
- core/backends.py +2 -0
- core/changelog.py +66 -5
- core/models.py +88 -7
- core/release.py +55 -2
- core/system.py +1 -1
- core/tasks.py +0 -6
- core/tests.py +131 -0
- core/views.py +112 -24
- ocpp/admin.py +92 -10
- ocpp/consumers.py +63 -19
- ocpp/test_rfid.py +118 -3
- ocpp/tests.py +225 -0
- ocpp/views.py +46 -7
- pages/admin.py +87 -5
- pages/apps.py +3 -0
- pages/site_config.py +137 -0
- pages/tests.py +206 -2
- {arthexis-0.1.16.dist-info → arthexis-0.1.18.dist-info}/WHEEL +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.18.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.18.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
arthexis-0.1.
|
|
1
|
+
arthexis-0.1.18.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,20 +8,20 @@ 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=fkLL3nbh01KqTVS9M7QH19i3HOuvVD6OTEvApy56Y4w,21569
|
|
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
|
|
22
22
|
core/auto_upgrade.py,sha256=1EffHHFylgydWdZM_id6CppV0QqBtdNw7cwBYVdbNdk,1715
|
|
23
|
-
core/backends.py,sha256=
|
|
24
|
-
core/changelog.py,sha256=
|
|
23
|
+
core/backends.py,sha256=Mzv_0YYF3iNWYAaKAJHHk75X2im-Kihu1zsg8eBeW2c,10509
|
|
24
|
+
core/changelog.py,sha256=SRn37i5N-qb-RYV4Gpu9fg7Kv8gu4TH8ZwEmDRgN-Vo,12594
|
|
25
25
|
core/entity.py,sha256=o4VteOXePGEsIWJFZ3fpq3DZsdWr3hpQ9A6kFbKosSE,4844
|
|
26
26
|
core/environment.py,sha256=JLcvxAwU3OTL8O6kzwcUCFNZ3T28KanHrU_4mDBFamU,1584
|
|
27
27
|
core/fields.py,sha256=d-qGahdcv4SRcO4fwCJ6_-NnEAP5xW0k3kODdAAAHSA,5412
|
|
@@ -34,24 +34,24 @@ 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
|
|
41
|
-
core/release.py,sha256=
|
|
41
|
+
core/release.py,sha256=y5NRs0XwB7RQVvMEZoNWYjTBxuG68dOMizUXLRx7-x8,31561
|
|
42
42
|
core/rfid_import_export.py,sha256=petyhPvL0WUpehc6uGUDUhjYQ9AVvc6O49zuhDs6YFw,3516
|
|
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=tqx8-4kyViMGKI3EAaxztrbyes4TSTPQ9YsIKzdVs6c,35731
|
|
47
|
+
core/tasks.py,sha256=MtijKTtRHUEsTP4nVJFYx5B8Ls8EXmtzpBuq8FU5b9s,12302
|
|
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=BTnbaGgSWz4lX7VKVttVIJGWyg7oHnYMMEQTOX44Zhc,88240
|
|
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
|
|
@@ -72,9 +72,9 @@ nodes/urls.py,sha256=HmAxj6sr6nMf0lii_1UX7sNBJUcrkaiKm3R9ofUWhvM,677
|
|
|
72
72
|
nodes/utils.py,sha256=wt7UuSXGuq79A-g-B6EW3kK49QWJBb7zhhkw4pun4k8,4474
|
|
73
73
|
nodes/views.py,sha256=TyW7exkVaR-o2_XkJXSi9jQ_BygXOE2cQFs4xlI20Xc,22905
|
|
74
74
|
ocpp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
75
|
-
ocpp/admin.py,sha256=
|
|
75
|
+
ocpp/admin.py,sha256=gMxHkT5KSp4kPWJcDJ1Y65VqgrwFTZl8Y516FO8oi3g,34658
|
|
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
80
|
ocpp/models.py,sha256=QjEaygY7Tl47Q6z2uxP6ftUn4JeD8-JQX2fcwrCaEEg,31631
|
|
@@ -85,14 +85,14 @@ ocpp/status_display.py,sha256=YGFosd5HJETA0DcLdsjvx6EfhZSnI8Aa3cMnHG2WsBE,939
|
|
|
85
85
|
ocpp/store.py,sha256=rHrP2Iq2ycMFbal1UEJVXb7r4gDtI5yifaE3nT0tjJw,18855
|
|
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=gFOMB3ioTa7dxwoA9pYwpP2cDu2GvhBsJeMKT7XyzAw,185987
|
|
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=PYuSUclq9IZrKrS4iHP2EJ_-alRcLgXDXabmmenhda0,57970
|
|
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=25vuZVdOJKMnTAnI1BUIyDhO8R-BuaAu8Wj56BNxyJ0,30349
|
|
95
|
+
pages/apps.py,sha256=o8gQP-VdZOk9LXIEo6IDmOSqX3TP8XypBvKGGWLoQ0k,351
|
|
96
96
|
pages/checks.py,sha256=sM8_hUVM_HOIocvtTb2sY3AaSEvbTnOlO46UchGVd-0,1527
|
|
97
97
|
pages/context_processors.py,sha256=oINGTI0owXz60FV-XFEjnTkY2xlSDE-W6X1TK8IK800,5072
|
|
98
98
|
pages/defaults.py,sha256=l36APPAZO4ub2A8Pp-lQGujKeOVYcyzU6t7-kOk8VoA,522
|
|
@@ -100,12 +100,13 @@ pages/forms.py,sha256=T0atqxdNds3IBP8N-9c5-ACf3iR9FzzmhzK4MOa24e8,7058
|
|
|
100
100
|
pages/middleware.py,sha256=6PMLiyuHAHbfLeHwwQxIVy2fJ32ramEO9SHAN05Set4,6967
|
|
101
101
|
pages/models.py,sha256=Sp8e2VB5a7yg4eSUlz_VcsSlAuDVap26xBKYYggxmLM,20952
|
|
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/tests.py,sha256=Lg3Jq_hOyF9KjiTeXr_AabFybW9KR3rvP5g_caomhGs,132912
|
|
105
106
|
pages/urls.py,sha256=Ne6yYJxgUAMieDpppJ149E-yh-oVi92fARiRPe-n4-s,1166
|
|
106
107
|
pages/utils.py,sha256=CR4D1debgJLGgXsw9kap2ggpe7fIpSoWS_ivbgMNp2k,564
|
|
107
108
|
pages/views.py,sha256=Ye7qGlO7IwkZO0oR1SzCpkEDTtGCJPmDJT-x6QQ8vaQ,45848
|
|
108
|
-
arthexis-0.1.
|
|
109
|
-
arthexis-0.1.
|
|
110
|
-
arthexis-0.1.
|
|
111
|
-
arthexis-0.1.
|
|
109
|
+
arthexis-0.1.18.dist-info/METADATA,sha256=C6OI88vHzosjqQvb7waV2pcPy0O5RbkOpdDBM-yOPBI,9998
|
|
110
|
+
arthexis-0.1.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
111
|
+
arthexis-0.1.18.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
|
|
112
|
+
arthexis-0.1.18.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",
|
|
@@ -480,6 +481,9 @@ AUTHENTICATION_BACKENDS = [
|
|
|
480
481
|
"core.backends.RFIDBackend",
|
|
481
482
|
]
|
|
482
483
|
|
|
484
|
+
# Use the custom login view for all authentication redirects.
|
|
485
|
+
LOGIN_URL = "pages:login"
|
|
486
|
+
|
|
483
487
|
# Issuer name used when generating otpauth URLs for authenticator apps.
|
|
484
488
|
OTP_TOTP_ISSUER = os.environ.get("OTP_TOTP_ISSUER", "Arthexis")
|
|
485
489
|
|
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
|
@@ -1309,11 +1309,11 @@ class AssistantProfileInlineForm(ProfileFormMixin, forms.ModelForm):
|
|
|
1309
1309
|
widget=forms.PasswordInput(render_value=True),
|
|
1310
1310
|
help_text="Provide a plain key to create or rotate credentials.",
|
|
1311
1311
|
)
|
|
1312
|
-
profile_fields = ("user_key", "scopes", "is_active")
|
|
1312
|
+
profile_fields = ("assistant_name", "user_key", "scopes", "is_active")
|
|
1313
1313
|
|
|
1314
1314
|
class Meta:
|
|
1315
1315
|
model = AssistantProfile
|
|
1316
|
-
fields = ("scopes", "is_active")
|
|
1316
|
+
fields = ("assistant_name", "scopes", "is_active")
|
|
1317
1317
|
|
|
1318
1318
|
def __init__(self, *args, **kwargs):
|
|
1319
1319
|
super().__init__(*args, **kwargs)
|
|
@@ -1475,7 +1475,7 @@ PROFILE_INLINE_CONFIG = {
|
|
|
1475
1475
|
},
|
|
1476
1476
|
AssistantProfile: {
|
|
1477
1477
|
"form": AssistantProfileInlineForm,
|
|
1478
|
-
"fields": ("user_key", "scopes", "is_active"),
|
|
1478
|
+
"fields": ("assistant_name", "user_key", "scopes", "is_active"),
|
|
1479
1479
|
"readonly_fields": ("user_key_hash", "created_at", "last_used_at"),
|
|
1480
1480
|
"template": "admin/edit_inline/profile_stacked.html",
|
|
1481
1481
|
},
|
|
@@ -2016,7 +2016,7 @@ class EmailInboxAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmi
|
|
|
2016
2016
|
class AssistantProfileAdmin(
|
|
2017
2017
|
ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin
|
|
2018
2018
|
):
|
|
2019
|
-
list_display = ("owner", "created_at", "last_used_at", "is_active")
|
|
2019
|
+
list_display = ("assistant_name", "owner", "created_at", "last_used_at", "is_active")
|
|
2020
2020
|
readonly_fields = ("user_key_hash", "created_at", "last_used_at")
|
|
2021
2021
|
|
|
2022
2022
|
change_form_template = "admin/workgroupassistantprofile_change_form.html"
|
|
@@ -2028,7 +2028,15 @@ class AssistantProfileAdmin(
|
|
|
2028
2028
|
("Credentials", {"fields": ("user_key_hash",)}),
|
|
2029
2029
|
(
|
|
2030
2030
|
"Configuration",
|
|
2031
|
-
{
|
|
2031
|
+
{
|
|
2032
|
+
"fields": (
|
|
2033
|
+
"assistant_name",
|
|
2034
|
+
"scopes",
|
|
2035
|
+
"is_active",
|
|
2036
|
+
"created_at",
|
|
2037
|
+
"last_used_at",
|
|
2038
|
+
)
|
|
2039
|
+
},
|
|
2032
2040
|
),
|
|
2033
2041
|
)
|
|
2034
2042
|
|
|
@@ -2835,6 +2843,7 @@ class RFIDResource(resources.ModelResource):
|
|
|
2835
2843
|
"post_auth_command",
|
|
2836
2844
|
"allowed",
|
|
2837
2845
|
"color",
|
|
2846
|
+
"endianness",
|
|
2838
2847
|
"kind",
|
|
2839
2848
|
"released",
|
|
2840
2849
|
"last_seen_on",
|
|
@@ -2849,6 +2858,7 @@ class RFIDResource(resources.ModelResource):
|
|
|
2849
2858
|
"post_auth_command",
|
|
2850
2859
|
"allowed",
|
|
2851
2860
|
"color",
|
|
2861
|
+
"endianness",
|
|
2852
2862
|
"kind",
|
|
2853
2863
|
"released",
|
|
2854
2864
|
"last_seen_on",
|
|
@@ -2919,11 +2929,12 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
|
|
|
2919
2929
|
"user_data_flag",
|
|
2920
2930
|
"color",
|
|
2921
2931
|
"kind",
|
|
2932
|
+
"endianness",
|
|
2922
2933
|
"released",
|
|
2923
2934
|
"allowed",
|
|
2924
2935
|
"last_seen_on",
|
|
2925
2936
|
)
|
|
2926
|
-
list_filter = ("color", "released", "allowed")
|
|
2937
|
+
list_filter = ("color", "endianness", "released", "allowed")
|
|
2927
2938
|
search_fields = ("label_id", "rfid", "custom_label")
|
|
2928
2939
|
autocomplete_fields = ["energy_accounts"]
|
|
2929
2940
|
raw_id_fields = ["reference"]
|
|
@@ -2933,8 +2944,10 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
|
|
|
2933
2944
|
"print_release_form",
|
|
2934
2945
|
"copy_rfids",
|
|
2935
2946
|
"toggle_selected_user_data",
|
|
2947
|
+
"toggle_selected_released",
|
|
2948
|
+
"toggle_selected_allowed",
|
|
2936
2949
|
]
|
|
2937
|
-
readonly_fields = ("added_on", "last_seen_on")
|
|
2950
|
+
readonly_fields = ("added_on", "last_seen_on", "reversed_uid")
|
|
2938
2951
|
form = RFIDForm
|
|
2939
2952
|
|
|
2940
2953
|
def get_import_resource_kwargs(self, request, form=None, **kwargs):
|
|
@@ -3030,6 +3043,50 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
|
|
|
3030
3043
|
level=messages.WARNING,
|
|
3031
3044
|
)
|
|
3032
3045
|
|
|
3046
|
+
@admin.action(description=_("Toggle Released flag"))
|
|
3047
|
+
def toggle_selected_released(self, request, queryset):
|
|
3048
|
+
manager = getattr(self.model, "all_objects", self.model.objects)
|
|
3049
|
+
toggled = 0
|
|
3050
|
+
for tag in queryset:
|
|
3051
|
+
new_state = not tag.released
|
|
3052
|
+
manager.filter(pk=tag.pk).update(released=new_state)
|
|
3053
|
+
tag.released = new_state
|
|
3054
|
+
toggled += 1
|
|
3055
|
+
|
|
3056
|
+
if toggled:
|
|
3057
|
+
self.message_user(
|
|
3058
|
+
request,
|
|
3059
|
+
ngettext(
|
|
3060
|
+
"Toggled released flag for %(count)d RFID.",
|
|
3061
|
+
"Toggled released flag for %(count)d RFIDs.",
|
|
3062
|
+
toggled,
|
|
3063
|
+
)
|
|
3064
|
+
% {"count": toggled},
|
|
3065
|
+
level=messages.SUCCESS,
|
|
3066
|
+
)
|
|
3067
|
+
|
|
3068
|
+
@admin.action(description=_("Toggle Allowed flag"))
|
|
3069
|
+
def toggle_selected_allowed(self, request, queryset):
|
|
3070
|
+
manager = getattr(self.model, "all_objects", self.model.objects)
|
|
3071
|
+
toggled = 0
|
|
3072
|
+
for tag in queryset:
|
|
3073
|
+
new_state = not tag.allowed
|
|
3074
|
+
manager.filter(pk=tag.pk).update(allowed=new_state)
|
|
3075
|
+
tag.allowed = new_state
|
|
3076
|
+
toggled += 1
|
|
3077
|
+
|
|
3078
|
+
if toggled:
|
|
3079
|
+
self.message_user(
|
|
3080
|
+
request,
|
|
3081
|
+
ngettext(
|
|
3082
|
+
"Toggled allowed flag for %(count)d RFID.",
|
|
3083
|
+
"Toggled allowed flag for %(count)d RFIDs.",
|
|
3084
|
+
toggled,
|
|
3085
|
+
)
|
|
3086
|
+
% {"count": toggled},
|
|
3087
|
+
level=messages.SUCCESS,
|
|
3088
|
+
)
|
|
3089
|
+
|
|
3033
3090
|
@admin.action(description=_("Copy RFID"))
|
|
3034
3091
|
def copy_rfids(self, request, queryset):
|
|
3035
3092
|
if queryset.count() != 1:
|
|
@@ -3497,6 +3554,7 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
|
|
|
3497
3554
|
context["title"] = _("Scan RFIDs")
|
|
3498
3555
|
context["opts"] = self.model._meta
|
|
3499
3556
|
context["show_release_info"] = True
|
|
3557
|
+
context["default_endianness"] = RFID.BIG_ENDIAN
|
|
3500
3558
|
return render(request, "admin/core/rfid/scan.html", context)
|
|
3501
3559
|
|
|
3502
3560
|
def scan_next(self, request):
|
|
@@ -3510,9 +3568,11 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
|
|
|
3510
3568
|
return JsonResponse({"error": "Invalid JSON payload"}, status=400)
|
|
3511
3569
|
rfid = payload.get("rfid") or payload.get("value")
|
|
3512
3570
|
kind = payload.get("kind")
|
|
3513
|
-
|
|
3571
|
+
endianness = payload.get("endianness")
|
|
3572
|
+
result = validate_rfid_value(rfid, kind=kind, endianness=endianness)
|
|
3514
3573
|
else:
|
|
3515
|
-
|
|
3574
|
+
endianness = request.GET.get("endianness")
|
|
3575
|
+
result = scan_sources(request, endianness=endianness)
|
|
3516
3576
|
status = 500 if result.get("error") else 200
|
|
3517
3577
|
return JsonResponse(result, status=status)
|
|
3518
3578
|
|
core/backends.py
CHANGED
|
@@ -90,6 +90,7 @@ class RFIDBackend:
|
|
|
90
90
|
env = os.environ.copy()
|
|
91
91
|
env["RFID_VALUE"] = rfid_value
|
|
92
92
|
env["RFID_LABEL_ID"] = str(tag.pk)
|
|
93
|
+
env["RFID_ENDIANNESS"] = getattr(tag, "endianness", RFID.BIG_ENDIAN)
|
|
93
94
|
try:
|
|
94
95
|
completed = subprocess.run(
|
|
95
96
|
command,
|
|
@@ -117,6 +118,7 @@ class RFIDBackend:
|
|
|
117
118
|
env = os.environ.copy()
|
|
118
119
|
env["RFID_VALUE"] = rfid_value
|
|
119
120
|
env["RFID_LABEL_ID"] = str(tag.pk)
|
|
121
|
+
env["RFID_ENDIANNESS"] = getattr(tag, "endianness", RFID.BIG_ENDIAN)
|
|
120
122
|
with contextlib.suppress(Exception):
|
|
121
123
|
subprocess.Popen(
|
|
122
124
|
post_command,
|
core/changelog.py
CHANGED
|
@@ -154,9 +154,53 @@ def _parse_sections(text: str) -> List[ChangelogSection]:
|
|
|
154
154
|
return sections
|
|
155
155
|
|
|
156
156
|
|
|
157
|
+
def _latest_release_version(previous_text: str) -> Optional[str]:
|
|
158
|
+
for section in _parse_sections(previous_text):
|
|
159
|
+
if section.version:
|
|
160
|
+
return section.version
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _find_release_commit(version: str) -> Optional[str]:
|
|
165
|
+
normalized = version.lstrip("v")
|
|
166
|
+
search_terms = [
|
|
167
|
+
f"Release v{normalized}",
|
|
168
|
+
f"Release {normalized}",
|
|
169
|
+
f"pre-release commit v{normalized}",
|
|
170
|
+
f"pre-release commit {normalized}",
|
|
171
|
+
]
|
|
172
|
+
for term in search_terms:
|
|
173
|
+
proc = subprocess.run(
|
|
174
|
+
[
|
|
175
|
+
"git",
|
|
176
|
+
"log",
|
|
177
|
+
"--max-count=1",
|
|
178
|
+
"--format=%H",
|
|
179
|
+
"--fixed-strings",
|
|
180
|
+
f"--grep={term}",
|
|
181
|
+
],
|
|
182
|
+
capture_output=True,
|
|
183
|
+
text=True,
|
|
184
|
+
check=False,
|
|
185
|
+
)
|
|
186
|
+
sha = proc.stdout.strip()
|
|
187
|
+
if sha:
|
|
188
|
+
return sha.splitlines()[0]
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _resolve_release_commit_from_text(previous_text: str) -> Optional[str]:
|
|
193
|
+
version = _latest_release_version(previous_text)
|
|
194
|
+
if not version:
|
|
195
|
+
return None
|
|
196
|
+
return _find_release_commit(version)
|
|
197
|
+
|
|
198
|
+
|
|
157
199
|
def _merge_sections(
|
|
158
200
|
new_sections: Iterable[ChangelogSection],
|
|
159
201
|
old_sections: Iterable[ChangelogSection],
|
|
202
|
+
*,
|
|
203
|
+
reopen_latest: bool = False,
|
|
160
204
|
) -> List[ChangelogSection]:
|
|
161
205
|
merged = list(new_sections)
|
|
162
206
|
old_sections_list = list(old_sections)
|
|
@@ -199,7 +243,8 @@ def _merge_sections(
|
|
|
199
243
|
existing = version_to_section.get(old.version)
|
|
200
244
|
if existing is None:
|
|
201
245
|
if (
|
|
202
|
-
|
|
246
|
+
reopen_latest
|
|
247
|
+
and first_release_version
|
|
203
248
|
and old.version == first_release_version
|
|
204
249
|
and not reopened_latest_version
|
|
205
250
|
and unreleased_section is not None
|
|
@@ -274,29 +319,45 @@ def _resolve_start_tag(explicit: str | None = None) -> Optional[str]:
|
|
|
274
319
|
return None
|
|
275
320
|
|
|
276
321
|
|
|
277
|
-
def determine_range_spec(
|
|
322
|
+
def determine_range_spec(
|
|
323
|
+
start_tag: str | None = None, *, previous_text: str | None = None
|
|
324
|
+
) -> str:
|
|
278
325
|
"""Return the git range specification to build the changelog."""
|
|
279
326
|
|
|
280
327
|
resolved = _resolve_start_tag(start_tag)
|
|
281
328
|
if resolved:
|
|
282
329
|
return f"{resolved}..HEAD"
|
|
330
|
+
|
|
331
|
+
if previous_text:
|
|
332
|
+
release_commit = _resolve_release_commit_from_text(previous_text)
|
|
333
|
+
if release_commit:
|
|
334
|
+
return f"{release_commit}..HEAD"
|
|
335
|
+
|
|
283
336
|
return "HEAD"
|
|
284
337
|
|
|
285
338
|
|
|
286
339
|
def collect_sections(
|
|
287
|
-
*,
|
|
340
|
+
*,
|
|
341
|
+
range_spec: str = "HEAD",
|
|
342
|
+
previous_text: str | None = None,
|
|
343
|
+
reopen_latest: bool = False,
|
|
288
344
|
) -> List[ChangelogSection]:
|
|
289
345
|
"""Return changelog sections for *range_spec*.
|
|
290
346
|
|
|
291
347
|
When ``previous_text`` is provided, sections not regenerated in the current run
|
|
292
|
-
are appended so long as they can be parsed from the existing changelog.
|
|
348
|
+
are appended so long as they can be parsed from the existing changelog. Set
|
|
349
|
+
``reopen_latest`` to ``True`` when the caller intends to move the most recent
|
|
350
|
+
release notes back into the ``Unreleased`` section (for example, when
|
|
351
|
+
preparing a release retry before a new tag is created).
|
|
293
352
|
"""
|
|
294
353
|
|
|
295
354
|
commits = _read_commits(range_spec)
|
|
296
355
|
sections = _sections_from_commits(commits)
|
|
297
356
|
if previous_text:
|
|
298
357
|
old_sections = _parse_sections(previous_text)
|
|
299
|
-
sections = _merge_sections(
|
|
358
|
+
sections = _merge_sections(
|
|
359
|
+
sections, old_sections, reopen_latest=reopen_latest
|
|
360
|
+
)
|
|
300
361
|
return sections
|
|
301
362
|
|
|
302
363
|
|
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,
|
|
@@ -1851,6 +1859,17 @@ class RFID(Entity):
|
|
|
1851
1859
|
choices=KIND_CHOICES,
|
|
1852
1860
|
default=CLASSIC,
|
|
1853
1861
|
)
|
|
1862
|
+
BIG_ENDIAN = "BIG"
|
|
1863
|
+
LITTLE_ENDIAN = "LITTLE"
|
|
1864
|
+
ENDIANNESS_CHOICES = [
|
|
1865
|
+
(BIG_ENDIAN, _("Big endian")),
|
|
1866
|
+
(LITTLE_ENDIAN, _("Little endian")),
|
|
1867
|
+
]
|
|
1868
|
+
endianness = models.CharField(
|
|
1869
|
+
max_length=6,
|
|
1870
|
+
choices=ENDIANNESS_CHOICES,
|
|
1871
|
+
default=BIG_ENDIAN,
|
|
1872
|
+
)
|
|
1854
1873
|
reference = models.ForeignKey(
|
|
1855
1874
|
"Reference",
|
|
1856
1875
|
null=True,
|
|
@@ -1895,13 +1914,24 @@ class RFID(Entity):
|
|
|
1895
1914
|
if self.key_b and old["key_b"] != self.key_b.upper():
|
|
1896
1915
|
self.key_b_verified = False
|
|
1897
1916
|
if self.rfid:
|
|
1898
|
-
|
|
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)
|
|
1899
1927
|
if self.key_a:
|
|
1900
1928
|
self.key_a = self.key_a.upper()
|
|
1901
1929
|
if self.key_b:
|
|
1902
1930
|
self.key_b = self.key_b.upper()
|
|
1903
1931
|
if self.kind:
|
|
1904
1932
|
self.kind = self.kind.upper()
|
|
1933
|
+
if self.endianness:
|
|
1934
|
+
self.endianness = self.normalize_endianness(self.endianness)
|
|
1905
1935
|
super().save(*args, **kwargs)
|
|
1906
1936
|
if not self.allowed:
|
|
1907
1937
|
self.energy_accounts.clear()
|
|
@@ -1909,6 +1939,30 @@ class RFID(Entity):
|
|
|
1909
1939
|
def __str__(self): # pragma: no cover - simple representation
|
|
1910
1940
|
return str(self.label_id)
|
|
1911
1941
|
|
|
1942
|
+
@classmethod
|
|
1943
|
+
def normalize_endianness(cls, value: object) -> str:
|
|
1944
|
+
"""Return a valid endianness value, defaulting to BIG."""
|
|
1945
|
+
|
|
1946
|
+
if isinstance(value, str):
|
|
1947
|
+
candidate = value.strip().upper()
|
|
1948
|
+
valid = {choice[0] for choice in cls.ENDIANNESS_CHOICES}
|
|
1949
|
+
if candidate in valid:
|
|
1950
|
+
return candidate
|
|
1951
|
+
return cls.BIG_ENDIAN
|
|
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
|
+
|
|
1912
1966
|
@classmethod
|
|
1913
1967
|
def next_scan_label(
|
|
1914
1968
|
cls, *, step: int | None = None, start: int | None = None
|
|
@@ -1971,13 +2025,39 @@ class RFID(Entity):
|
|
|
1971
2025
|
|
|
1972
2026
|
@classmethod
|
|
1973
2027
|
def register_scan(
|
|
1974
|
-
cls,
|
|
2028
|
+
cls,
|
|
2029
|
+
rfid: str,
|
|
2030
|
+
*,
|
|
2031
|
+
kind: str | None = None,
|
|
2032
|
+
endianness: str | None = None,
|
|
1975
2033
|
) -> tuple["RFID", bool]:
|
|
1976
2034
|
"""Return or create an RFID that was detected via scanning."""
|
|
1977
2035
|
|
|
1978
|
-
normalized = (rfid or "").upper()
|
|
1979
|
-
|
|
2036
|
+
normalized = "".join((rfid or "").split()).upper()
|
|
2037
|
+
desired_endianness = cls.normalize_endianness(endianness)
|
|
2038
|
+
alternate = None
|
|
2039
|
+
if normalized and len(normalized) % 2 == 0:
|
|
2040
|
+
bytes_list = [normalized[i : i + 2] for i in range(0, len(normalized), 2)]
|
|
2041
|
+
bytes_list.reverse()
|
|
2042
|
+
alternate_candidate = "".join(bytes_list)
|
|
2043
|
+
if alternate_candidate != normalized:
|
|
2044
|
+
alternate = alternate_candidate
|
|
2045
|
+
|
|
2046
|
+
existing = None
|
|
2047
|
+
if normalized:
|
|
2048
|
+
existing = cls.objects.filter(rfid=normalized).first()
|
|
2049
|
+
if not existing and alternate:
|
|
2050
|
+
existing = cls.objects.filter(rfid=alternate).first()
|
|
1980
2051
|
if existing:
|
|
2052
|
+
update_fields: list[str] = []
|
|
2053
|
+
if normalized and existing.rfid != normalized:
|
|
2054
|
+
existing.rfid = normalized
|
|
2055
|
+
update_fields.append("rfid")
|
|
2056
|
+
if existing.endianness != desired_endianness:
|
|
2057
|
+
existing.endianness = desired_endianness
|
|
2058
|
+
update_fields.append("endianness")
|
|
2059
|
+
if update_fields:
|
|
2060
|
+
existing.save(update_fields=update_fields)
|
|
1981
2061
|
return existing, False
|
|
1982
2062
|
|
|
1983
2063
|
attempts = 0
|
|
@@ -1990,6 +2070,7 @@ class RFID(Entity):
|
|
|
1990
2070
|
"rfid": normalized,
|
|
1991
2071
|
"allowed": True,
|
|
1992
2072
|
"released": False,
|
|
2073
|
+
"endianness": desired_endianness,
|
|
1993
2074
|
}
|
|
1994
2075
|
if kind:
|
|
1995
2076
|
create_kwargs["kind"] = kind
|
|
@@ -3539,7 +3620,8 @@ class AssistantProfile(Profile):
|
|
|
3539
3620
|
"""
|
|
3540
3621
|
|
|
3541
3622
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
3542
|
-
profile_fields = ("user_key_hash", "scopes", "is_active")
|
|
3623
|
+
profile_fields = ("assistant_name", "user_key_hash", "scopes", "is_active")
|
|
3624
|
+
assistant_name = models.CharField(max_length=100, default="Assistant")
|
|
3543
3625
|
user_key_hash = models.CharField(max_length=64, unique=True)
|
|
3544
3626
|
scopes = models.JSONField(default=list, blank=True)
|
|
3545
3627
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
@@ -3586,8 +3668,7 @@ class AssistantProfile(Profile):
|
|
|
3586
3668
|
self.save(update_fields=["last_used_at"])
|
|
3587
3669
|
|
|
3588
3670
|
def __str__(self) -> str: # pragma: no cover - simple representation
|
|
3589
|
-
|
|
3590
|
-
return f"AssistantProfile for {owner}" if owner else "AssistantProfile"
|
|
3671
|
+
return self.assistant_name or "AssistantProfile"
|
|
3591
3672
|
|
|
3592
3673
|
|
|
3593
3674
|
def validate_relative_url(value: str) -> None:
|