arthexis 0.1.16__py3-none-any.whl → 0.1.17__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.17.dist-info}/METADATA +1 -1
- {arthexis-0.1.16.dist-info → arthexis-0.1.17.dist-info}/RECORD +20 -20
- config/settings.py +3 -0
- core/admin.py +68 -8
- core/backends.py +2 -0
- core/changelog.py +66 -5
- core/models.py +57 -6
- core/release.py +55 -2
- core/system.py +1 -1
- core/tasks.py +0 -6
- core/tests.py +122 -0
- core/views.py +58 -7
- ocpp/admin.py +92 -10
- ocpp/test_rfid.py +48 -3
- ocpp/tests.py +132 -0
- ocpp/views.py +23 -5
- pages/tests.py +26 -1
- {arthexis-0.1.16.dist-info → arthexis-0.1.17.dist-info}/WHEEL +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.17.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.17.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
arthexis-0.1.
|
|
1
|
+
arthexis-0.1.17.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
|
|
@@ -10,18 +10,18 @@ config/loadenv.py,sha256=CjXx-wBaTt1wixub4GJ5CMSMFqtiK5JURc7cPXpqO7s,287
|
|
|
10
10
|
config/logging.py,sha256=1cIbPgRshHuMKnVEEH0jKpRAlJSpewvLFbYDz7sCBG4,2104
|
|
11
11
|
config/middleware.py,sha256=mDU5tye8H4WCjpJqocwd0vmrzoVEYwdz9WTP4Hcr6dI,719
|
|
12
12
|
config/offline.py,sha256=X-yDcyoI4C44Y27lpkUwszY_09GwwFfazEsthKJpQ70,1382
|
|
13
|
-
config/settings.py,sha256=
|
|
13
|
+
config/settings.py,sha256=WFywFlWRTkEqDksWYAOd6DdSpHLEu2ETgLeeSGruwrA,21516
|
|
14
14
|
config/settings_helpers.py,sha256=0BdBciUHIkwsWa0vV_RKAd4wDuEzgE7G-42XYiES4YQ,3127
|
|
15
15
|
config/urls.py,sha256=lXl2KKsbIehjOW0W6FHAsxkZJ-3DAo37f2ICb1dvvz8,5320
|
|
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=tEMop2REJ-P4o73ZWKZQuWm29zF3S-dWKu26wUDfvi4,145556
|
|
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=Vl_Z_tLo93w2ZDgObFFIfLSzid3KXraByghMmJeCfNk,126647
|
|
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=ojHab5JtcHuVDc1zXK_QVms1cID1XXjuRcHmPcqVqD0,98368
|
|
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=Gt2J51RyIOsR2gzl2q3ChPbbIVDzrscty3yIGRW07P8,87141
|
|
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,7 +72,7 @@ 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
77
|
ocpp/consumers.py,sha256=LgplrJQOfs8CKCtmBcRQLcDVB4Tz7YZpb3I7r2lAorQ,66352
|
|
78
78
|
ocpp/evcs.py,sha256=q1mZrCVSZxXTrtYsDqH6lkeEcJ6tfSC7p9YxkDmpSCw,28883
|
|
@@ -85,11 +85,11 @@ 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=0Zczbg1x_5vhIV5TITHeaUkNONMdx20pD4St7Zc-ghM,36524
|
|
89
|
+
ocpp/tests.py,sha256=BfOapD5vWrmA43Q4WI7or2lP2pmcBdrU5_YCK31JgHE,182621
|
|
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=wntIO3LHFoPAg40SFGMoRPAiA3xyDKPwNgIsCBSEOcI,57164
|
|
93
93
|
pages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
94
94
|
pages/admin.py,sha256=f2IYr-nGg9FmafQfDmIRrv01UuXh4mdhFJbnw-ytzHU,27459
|
|
95
95
|
pages/apps.py,sha256=AzUNXQX0yRUX5jus-5EDReDb0nOEY8DBgYaM970u6Io,288
|
|
@@ -101,11 +101,11 @@ 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
103
|
pages/tasks.py,sha256=ivcba_3wSQ1-cku0oDplzw6vLeQ9hBq3R4TG-LmR5gs,1913
|
|
104
|
-
pages/tests.py,sha256
|
|
104
|
+
pages/tests.py,sha256=i1HLp8rNm63WuqFf9YgAEGWYowFC7SOyyg_7j17_buQ,126102
|
|
105
105
|
pages/urls.py,sha256=Ne6yYJxgUAMieDpppJ149E-yh-oVi92fARiRPe-n4-s,1166
|
|
106
106
|
pages/utils.py,sha256=CR4D1debgJLGgXsw9kap2ggpe7fIpSoWS_ivbgMNp2k,564
|
|
107
107
|
pages/views.py,sha256=Ye7qGlO7IwkZO0oR1SzCpkEDTtGCJPmDJT-x6QQ8vaQ,45848
|
|
108
|
-
arthexis-0.1.
|
|
109
|
-
arthexis-0.1.
|
|
110
|
-
arthexis-0.1.
|
|
111
|
-
arthexis-0.1.
|
|
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,,
|
config/settings.py
CHANGED
|
@@ -480,6 +480,9 @@ AUTHENTICATION_BACKENDS = [
|
|
|
480
480
|
"core.backends.RFIDBackend",
|
|
481
481
|
]
|
|
482
482
|
|
|
483
|
+
# Use the custom login view for all authentication redirects.
|
|
484
|
+
LOGIN_URL = "pages:login"
|
|
485
|
+
|
|
483
486
|
# Issuer name used when generating otpauth URLs for authenticator apps.
|
|
484
487
|
OTP_TOTP_ISSUER = os.environ.get("OTP_TOTP_ISSUER", "Arthexis")
|
|
485
488
|
|
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,6 +2944,8 @@ 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
2950
|
readonly_fields = ("added_on", "last_seen_on")
|
|
2938
2951
|
form = RFIDForm
|
|
@@ -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
|
@@ -1851,6 +1851,17 @@ class RFID(Entity):
|
|
|
1851
1851
|
choices=KIND_CHOICES,
|
|
1852
1852
|
default=CLASSIC,
|
|
1853
1853
|
)
|
|
1854
|
+
BIG_ENDIAN = "BIG"
|
|
1855
|
+
LITTLE_ENDIAN = "LITTLE"
|
|
1856
|
+
ENDIANNESS_CHOICES = [
|
|
1857
|
+
(BIG_ENDIAN, _("Big endian")),
|
|
1858
|
+
(LITTLE_ENDIAN, _("Little endian")),
|
|
1859
|
+
]
|
|
1860
|
+
endianness = models.CharField(
|
|
1861
|
+
max_length=6,
|
|
1862
|
+
choices=ENDIANNESS_CHOICES,
|
|
1863
|
+
default=BIG_ENDIAN,
|
|
1864
|
+
)
|
|
1854
1865
|
reference = models.ForeignKey(
|
|
1855
1866
|
"Reference",
|
|
1856
1867
|
null=True,
|
|
@@ -1902,6 +1913,8 @@ class RFID(Entity):
|
|
|
1902
1913
|
self.key_b = self.key_b.upper()
|
|
1903
1914
|
if self.kind:
|
|
1904
1915
|
self.kind = self.kind.upper()
|
|
1916
|
+
if self.endianness:
|
|
1917
|
+
self.endianness = self.normalize_endianness(self.endianness)
|
|
1905
1918
|
super().save(*args, **kwargs)
|
|
1906
1919
|
if not self.allowed:
|
|
1907
1920
|
self.energy_accounts.clear()
|
|
@@ -1909,6 +1922,17 @@ class RFID(Entity):
|
|
|
1909
1922
|
def __str__(self): # pragma: no cover - simple representation
|
|
1910
1923
|
return str(self.label_id)
|
|
1911
1924
|
|
|
1925
|
+
@classmethod
|
|
1926
|
+
def normalize_endianness(cls, value: object) -> str:
|
|
1927
|
+
"""Return a valid endianness value, defaulting to BIG."""
|
|
1928
|
+
|
|
1929
|
+
if isinstance(value, str):
|
|
1930
|
+
candidate = value.strip().upper()
|
|
1931
|
+
valid = {choice[0] for choice in cls.ENDIANNESS_CHOICES}
|
|
1932
|
+
if candidate in valid:
|
|
1933
|
+
return candidate
|
|
1934
|
+
return cls.BIG_ENDIAN
|
|
1935
|
+
|
|
1912
1936
|
@classmethod
|
|
1913
1937
|
def next_scan_label(
|
|
1914
1938
|
cls, *, step: int | None = None, start: int | None = None
|
|
@@ -1971,13 +1995,39 @@ class RFID(Entity):
|
|
|
1971
1995
|
|
|
1972
1996
|
@classmethod
|
|
1973
1997
|
def register_scan(
|
|
1974
|
-
cls,
|
|
1998
|
+
cls,
|
|
1999
|
+
rfid: str,
|
|
2000
|
+
*,
|
|
2001
|
+
kind: str | None = None,
|
|
2002
|
+
endianness: str | None = None,
|
|
1975
2003
|
) -> tuple["RFID", bool]:
|
|
1976
2004
|
"""Return or create an RFID that was detected via scanning."""
|
|
1977
2005
|
|
|
1978
|
-
normalized = (rfid or "").upper()
|
|
1979
|
-
|
|
2006
|
+
normalized = "".join((rfid or "").split()).upper()
|
|
2007
|
+
desired_endianness = cls.normalize_endianness(endianness)
|
|
2008
|
+
alternate = None
|
|
2009
|
+
if normalized and len(normalized) % 2 == 0:
|
|
2010
|
+
bytes_list = [normalized[i : i + 2] for i in range(0, len(normalized), 2)]
|
|
2011
|
+
bytes_list.reverse()
|
|
2012
|
+
alternate_candidate = "".join(bytes_list)
|
|
2013
|
+
if alternate_candidate != normalized:
|
|
2014
|
+
alternate = alternate_candidate
|
|
2015
|
+
|
|
2016
|
+
existing = None
|
|
2017
|
+
if normalized:
|
|
2018
|
+
existing = cls.objects.filter(rfid=normalized).first()
|
|
2019
|
+
if not existing and alternate:
|
|
2020
|
+
existing = cls.objects.filter(rfid=alternate).first()
|
|
1980
2021
|
if existing:
|
|
2022
|
+
update_fields: list[str] = []
|
|
2023
|
+
if normalized and existing.rfid != normalized:
|
|
2024
|
+
existing.rfid = normalized
|
|
2025
|
+
update_fields.append("rfid")
|
|
2026
|
+
if existing.endianness != desired_endianness:
|
|
2027
|
+
existing.endianness = desired_endianness
|
|
2028
|
+
update_fields.append("endianness")
|
|
2029
|
+
if update_fields:
|
|
2030
|
+
existing.save(update_fields=update_fields)
|
|
1981
2031
|
return existing, False
|
|
1982
2032
|
|
|
1983
2033
|
attempts = 0
|
|
@@ -1990,6 +2040,7 @@ class RFID(Entity):
|
|
|
1990
2040
|
"rfid": normalized,
|
|
1991
2041
|
"allowed": True,
|
|
1992
2042
|
"released": False,
|
|
2043
|
+
"endianness": desired_endianness,
|
|
1993
2044
|
}
|
|
1994
2045
|
if kind:
|
|
1995
2046
|
create_kwargs["kind"] = kind
|
|
@@ -3539,7 +3590,8 @@ class AssistantProfile(Profile):
|
|
|
3539
3590
|
"""
|
|
3540
3591
|
|
|
3541
3592
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
3542
|
-
profile_fields = ("user_key_hash", "scopes", "is_active")
|
|
3593
|
+
profile_fields = ("assistant_name", "user_key_hash", "scopes", "is_active")
|
|
3594
|
+
assistant_name = models.CharField(max_length=100, default="Assistant")
|
|
3543
3595
|
user_key_hash = models.CharField(max_length=64, unique=True)
|
|
3544
3596
|
scopes = models.JSONField(default=list, blank=True)
|
|
3545
3597
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
@@ -3586,8 +3638,7 @@ class AssistantProfile(Profile):
|
|
|
3586
3638
|
self.save(update_fields=["last_used_at"])
|
|
3587
3639
|
|
|
3588
3640
|
def __str__(self) -> str: # pragma: no cover - simple representation
|
|
3589
|
-
|
|
3590
|
-
return f"AssistantProfile for {owner}" if owner else "AssistantProfile"
|
|
3641
|
+
return self.assistant_name or "AssistantProfile"
|
|
3591
3642
|
|
|
3592
3643
|
|
|
3593
3644
|
def validate_relative_url(value: str) -> None:
|
core/release.py
CHANGED
|
@@ -114,6 +114,21 @@ class ReleaseError(Exception):
|
|
|
114
114
|
pass
|
|
115
115
|
|
|
116
116
|
|
|
117
|
+
class PostPublishWarning(ReleaseError):
|
|
118
|
+
"""Raised when distribution uploads succeed but post-publish tasks need attention."""
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
message: str,
|
|
123
|
+
*,
|
|
124
|
+
uploaded: Sequence[str],
|
|
125
|
+
followups: Optional[Sequence[str]] = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
super().__init__(message)
|
|
128
|
+
self.uploaded = list(uploaded)
|
|
129
|
+
self.followups = list(followups or [])
|
|
130
|
+
|
|
131
|
+
|
|
117
132
|
class TestsFailed(ReleaseError):
|
|
118
133
|
"""Raised when the test suite fails.
|
|
119
134
|
|
|
@@ -711,8 +726,46 @@ def publish(
|
|
|
711
726
|
uploaded.append(target.name)
|
|
712
727
|
|
|
713
728
|
tag_name = f"v{version}"
|
|
714
|
-
|
|
715
|
-
|
|
729
|
+
try:
|
|
730
|
+
_run(["git", "tag", tag_name])
|
|
731
|
+
except subprocess.CalledProcessError as exc:
|
|
732
|
+
details = _format_subprocess_error(exc)
|
|
733
|
+
if uploaded:
|
|
734
|
+
uploads = ", ".join(uploaded)
|
|
735
|
+
if details:
|
|
736
|
+
message = (
|
|
737
|
+
f"Upload to {uploads} completed, but creating git tag {tag_name} failed: {details}"
|
|
738
|
+
)
|
|
739
|
+
else:
|
|
740
|
+
message = (
|
|
741
|
+
f"Upload to {uploads} completed, but creating git tag {tag_name} failed."
|
|
742
|
+
)
|
|
743
|
+
followups = [f"Create and push git tag {tag_name} manually once the repository is ready."]
|
|
744
|
+
raise PostPublishWarning(
|
|
745
|
+
message,
|
|
746
|
+
uploaded=uploaded,
|
|
747
|
+
followups=followups,
|
|
748
|
+
) from exc
|
|
749
|
+
raise ReleaseError(
|
|
750
|
+
f"Failed to create git tag {tag_name}: {details or exc}"
|
|
751
|
+
) from exc
|
|
752
|
+
|
|
753
|
+
try:
|
|
754
|
+
_push_tag(tag_name, package)
|
|
755
|
+
except ReleaseError as exc:
|
|
756
|
+
if uploaded:
|
|
757
|
+
uploads = ", ".join(uploaded)
|
|
758
|
+
message = f"Upload to {uploads} completed, but {exc}"
|
|
759
|
+
followups = [
|
|
760
|
+
f"Push git tag {tag_name} to origin after resolving the reported issue."
|
|
761
|
+
]
|
|
762
|
+
warning = PostPublishWarning(
|
|
763
|
+
message,
|
|
764
|
+
uploaded=uploaded,
|
|
765
|
+
followups=followups,
|
|
766
|
+
)
|
|
767
|
+
raise warning from exc
|
|
768
|
+
raise
|
|
716
769
|
return uploaded
|
|
717
770
|
|
|
718
771
|
|
core/system.py
CHANGED
|
@@ -171,7 +171,7 @@ def _regenerate_changelog() -> None:
|
|
|
171
171
|
previous_text = (
|
|
172
172
|
changelog_path.read_text(encoding="utf-8") if changelog_path.exists() else None
|
|
173
173
|
)
|
|
174
|
-
range_spec = changelog_utils.determine_range_spec()
|
|
174
|
+
range_spec = changelog_utils.determine_range_spec(previous_text=previous_text)
|
|
175
175
|
sections = changelog_utils.collect_sections(
|
|
176
176
|
range_spec=range_spec, previous_text=previous_text
|
|
177
177
|
)
|
core/tasks.py
CHANGED
|
@@ -230,12 +230,6 @@ def check_github_updates() -> None:
|
|
|
230
230
|
|
|
231
231
|
subprocess.run(args, cwd=base_dir, check=True)
|
|
232
232
|
|
|
233
|
-
if shutil.which("gway"):
|
|
234
|
-
try:
|
|
235
|
-
subprocess.run(["gway", "upgrade"], check=True)
|
|
236
|
-
except subprocess.CalledProcessError:
|
|
237
|
-
logger.warning("gway upgrade failed; continuing anyway", exc_info=True)
|
|
238
|
-
|
|
239
233
|
service_file = base_dir / "locks/service.lck"
|
|
240
234
|
if service_file.exists():
|
|
241
235
|
service = service_file.read_text().strip()
|