arthexis 0.1.19__py3-none-any.whl → 0.1.21__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.
- {arthexis-0.1.19.dist-info → arthexis-0.1.21.dist-info}/METADATA +5 -6
- {arthexis-0.1.19.dist-info → arthexis-0.1.21.dist-info}/RECORD +42 -44
- config/asgi.py +1 -15
- config/settings.py +0 -26
- config/urls.py +0 -1
- core/admin.py +143 -234
- core/apps.py +0 -6
- core/backends.py +8 -2
- core/environment.py +240 -4
- core/models.py +132 -102
- core/notifications.py +1 -1
- core/reference_utils.py +10 -11
- core/sigil_builder.py +2 -2
- core/tasks.py +24 -1
- core/tests.py +2 -7
- core/views.py +70 -132
- nodes/admin.py +162 -7
- nodes/models.py +294 -48
- nodes/rfid_sync.py +1 -1
- nodes/tasks.py +100 -2
- nodes/tests.py +581 -15
- nodes/urls.py +4 -0
- nodes/views.py +560 -96
- ocpp/admin.py +144 -4
- ocpp/consumers.py +106 -9
- ocpp/models.py +131 -1
- ocpp/tasks.py +4 -0
- ocpp/test_export_import.py +1 -0
- ocpp/test_rfid.py +3 -1
- ocpp/tests.py +183 -9
- ocpp/transactions_io.py +9 -1
- ocpp/urls.py +3 -3
- ocpp/views.py +186 -31
- pages/context_processors.py +15 -21
- pages/defaults.py +1 -1
- pages/module_defaults.py +5 -5
- pages/tests.py +110 -79
- pages/urls.py +1 -1
- pages/views.py +108 -13
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- {arthexis-0.1.19.dist-info → arthexis-0.1.21.dist-info}/WHEEL +0 -0
- {arthexis-0.1.19.dist-info → arthexis-0.1.21.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.19.dist-info → arthexis-0.1.21.dist-info}/top_level.txt +0 -0
core/admin.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
1
2
|
from io import BytesIO
|
|
2
3
|
import os
|
|
3
4
|
|
|
@@ -90,9 +91,7 @@ from .models import (
|
|
|
90
91
|
SecurityGroup,
|
|
91
92
|
InviteLead,
|
|
92
93
|
PublicWifiAccess,
|
|
93
|
-
AssistantProfile,
|
|
94
94
|
Todo,
|
|
95
|
-
hash_key,
|
|
96
95
|
)
|
|
97
96
|
from .user_data import (
|
|
98
97
|
EntityModelAdmin,
|
|
@@ -109,8 +108,6 @@ from .rfid_import_export import (
|
|
|
109
108
|
parse_accounts,
|
|
110
109
|
serialize_accounts,
|
|
111
110
|
)
|
|
112
|
-
from .mcp import process as mcp_process
|
|
113
|
-
from .mcp.server import resolve_base_urls
|
|
114
111
|
from . import release as release_utils
|
|
115
112
|
|
|
116
113
|
logger = logging.getLogger(__name__)
|
|
@@ -1303,46 +1300,6 @@ class ReleaseManagerInlineForm(ProfileFormMixin, forms.ModelForm):
|
|
|
1303
1300
|
}
|
|
1304
1301
|
|
|
1305
1302
|
|
|
1306
|
-
class AssistantProfileInlineForm(ProfileFormMixin, forms.ModelForm):
|
|
1307
|
-
user_key = forms.CharField(
|
|
1308
|
-
required=False,
|
|
1309
|
-
widget=forms.PasswordInput(render_value=True),
|
|
1310
|
-
help_text="Provide a plain key to create or rotate credentials.",
|
|
1311
|
-
)
|
|
1312
|
-
profile_fields = ("assistant_name", "user_key", "scopes", "is_active")
|
|
1313
|
-
|
|
1314
|
-
class Meta:
|
|
1315
|
-
model = AssistantProfile
|
|
1316
|
-
fields = ("assistant_name", "scopes", "is_active")
|
|
1317
|
-
|
|
1318
|
-
def __init__(self, *args, **kwargs):
|
|
1319
|
-
super().__init__(*args, **kwargs)
|
|
1320
|
-
if not self.instance.pk and "is_active" in self.fields:
|
|
1321
|
-
self.fields["is_active"].initial = False
|
|
1322
|
-
|
|
1323
|
-
def clean(self):
|
|
1324
|
-
cleaned = super().clean()
|
|
1325
|
-
if cleaned.get("DELETE"):
|
|
1326
|
-
return cleaned
|
|
1327
|
-
if not self.instance.pk and not cleaned.get("user_key"):
|
|
1328
|
-
if cleaned.get("scopes") or cleaned.get("is_active"):
|
|
1329
|
-
raise forms.ValidationError(
|
|
1330
|
-
"Provide a user key to create an assistant profile."
|
|
1331
|
-
)
|
|
1332
|
-
return cleaned
|
|
1333
|
-
|
|
1334
|
-
def save(self, commit=True):
|
|
1335
|
-
instance = super().save(commit=False)
|
|
1336
|
-
user_key = self.cleaned_data.get("user_key")
|
|
1337
|
-
if user_key:
|
|
1338
|
-
instance.user_key_hash = hash_key(user_key)
|
|
1339
|
-
instance.last_used_at = None
|
|
1340
|
-
if commit:
|
|
1341
|
-
instance.save()
|
|
1342
|
-
self.save_m2m()
|
|
1343
|
-
return instance
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
1303
|
PROFILE_INLINE_CONFIG = {
|
|
1347
1304
|
OdooProfile: {
|
|
1348
1305
|
"form": OdooProfileInlineForm,
|
|
@@ -1473,12 +1430,6 @@ PROFILE_INLINE_CONFIG = {
|
|
|
1473
1430
|
"secondary_pypi_url",
|
|
1474
1431
|
),
|
|
1475
1432
|
},
|
|
1476
|
-
AssistantProfile: {
|
|
1477
|
-
"form": AssistantProfileInlineForm,
|
|
1478
|
-
"fields": ("assistant_name", "user_key", "scopes", "is_active"),
|
|
1479
|
-
"readonly_fields": ("user_key_hash", "created_at", "last_used_at"),
|
|
1480
|
-
"template": "admin/edit_inline/profile_stacked.html",
|
|
1481
|
-
},
|
|
1482
1433
|
}
|
|
1483
1434
|
|
|
1484
1435
|
|
|
@@ -1525,7 +1476,6 @@ PROFILE_MODELS = (
|
|
|
1525
1476
|
EmailOutbox,
|
|
1526
1477
|
SocialProfile,
|
|
1527
1478
|
ReleaseManager,
|
|
1528
|
-
AssistantProfile,
|
|
1529
1479
|
)
|
|
1530
1480
|
USER_PROFILE_INLINES = [
|
|
1531
1481
|
_build_profile_inline(model, "user") for model in PROFILE_MODELS
|
|
@@ -2012,188 +1962,6 @@ class EmailInboxAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmi
|
|
|
2012
1962
|
return TemplateResponse(request, "admin/core/emailinbox/search.html", context)
|
|
2013
1963
|
|
|
2014
1964
|
|
|
2015
|
-
@admin.register(AssistantProfile)
|
|
2016
|
-
class AssistantProfileAdmin(
|
|
2017
|
-
ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin
|
|
2018
|
-
):
|
|
2019
|
-
list_display = ("assistant_name", "owner", "created_at", "last_used_at", "is_active")
|
|
2020
|
-
readonly_fields = ("user_key_hash", "created_at", "last_used_at")
|
|
2021
|
-
|
|
2022
|
-
change_form_template = "admin/workgroupassistantprofile_change_form.html"
|
|
2023
|
-
change_list_template = "admin/assistantprofile_change_list.html"
|
|
2024
|
-
change_actions = ["my_profile_action"]
|
|
2025
|
-
changelist_actions = ["my_profile"]
|
|
2026
|
-
fieldsets = (
|
|
2027
|
-
("Owner", {"fields": ("user", "group")}),
|
|
2028
|
-
("Credentials", {"fields": ("user_key_hash",)}),
|
|
2029
|
-
(
|
|
2030
|
-
"Configuration",
|
|
2031
|
-
{
|
|
2032
|
-
"fields": (
|
|
2033
|
-
"assistant_name",
|
|
2034
|
-
"scopes",
|
|
2035
|
-
"is_active",
|
|
2036
|
-
"created_at",
|
|
2037
|
-
"last_used_at",
|
|
2038
|
-
)
|
|
2039
|
-
},
|
|
2040
|
-
),
|
|
2041
|
-
)
|
|
2042
|
-
|
|
2043
|
-
def owner(self, obj):
|
|
2044
|
-
return obj.owner_display()
|
|
2045
|
-
|
|
2046
|
-
owner.short_description = "Owner"
|
|
2047
|
-
|
|
2048
|
-
def get_urls(self):
|
|
2049
|
-
urls = super().get_urls()
|
|
2050
|
-
opts = self.model._meta
|
|
2051
|
-
app_label = opts.app_label
|
|
2052
|
-
model_name = opts.model_name
|
|
2053
|
-
custom = [
|
|
2054
|
-
path(
|
|
2055
|
-
"<path:object_id>/generate-key/",
|
|
2056
|
-
self.admin_site.admin_view(self.generate_key),
|
|
2057
|
-
name=f"{app_label}_{model_name}_generate_key",
|
|
2058
|
-
),
|
|
2059
|
-
path(
|
|
2060
|
-
"server/start/",
|
|
2061
|
-
self.admin_site.admin_view(self.start_server),
|
|
2062
|
-
name=f"{app_label}_{model_name}_start_server",
|
|
2063
|
-
),
|
|
2064
|
-
path(
|
|
2065
|
-
"server/stop/",
|
|
2066
|
-
self.admin_site.admin_view(self.stop_server),
|
|
2067
|
-
name=f"{app_label}_{model_name}_stop_server",
|
|
2068
|
-
),
|
|
2069
|
-
path(
|
|
2070
|
-
"server/status/",
|
|
2071
|
-
self.admin_site.admin_view(self.server_status),
|
|
2072
|
-
name=f"{app_label}_{model_name}_status",
|
|
2073
|
-
),
|
|
2074
|
-
]
|
|
2075
|
-
return custom + urls
|
|
2076
|
-
|
|
2077
|
-
def changelist_view(self, request, extra_context=None):
|
|
2078
|
-
extra_context = extra_context or {}
|
|
2079
|
-
status = mcp_process.get_status()
|
|
2080
|
-
opts = self.model._meta
|
|
2081
|
-
app_label = opts.app_label
|
|
2082
|
-
model_name = opts.model_name
|
|
2083
|
-
extra_context.update(
|
|
2084
|
-
{
|
|
2085
|
-
"mcp_status": status,
|
|
2086
|
-
"mcp_server_actions": {
|
|
2087
|
-
"start": reverse(f"admin:{app_label}_{model_name}_start_server"),
|
|
2088
|
-
"stop": reverse(f"admin:{app_label}_{model_name}_stop_server"),
|
|
2089
|
-
"status": reverse(f"admin:{app_label}_{model_name}_status"),
|
|
2090
|
-
},
|
|
2091
|
-
}
|
|
2092
|
-
)
|
|
2093
|
-
return super().changelist_view(request, extra_context=extra_context)
|
|
2094
|
-
|
|
2095
|
-
def _redirect_to_changelist(self):
|
|
2096
|
-
opts = self.model._meta
|
|
2097
|
-
return HttpResponseRedirect(
|
|
2098
|
-
reverse(f"admin:{opts.app_label}_{opts.model_name}_changelist")
|
|
2099
|
-
)
|
|
2100
|
-
|
|
2101
|
-
def generate_key(self, request, object_id, *args, **kwargs):
|
|
2102
|
-
profile = self.get_object(request, object_id)
|
|
2103
|
-
if profile is None:
|
|
2104
|
-
return HttpResponseRedirect("../")
|
|
2105
|
-
if profile.user is None:
|
|
2106
|
-
self.message_user(
|
|
2107
|
-
request,
|
|
2108
|
-
"Assign a user before generating a key.",
|
|
2109
|
-
level=messages.ERROR,
|
|
2110
|
-
)
|
|
2111
|
-
return HttpResponseRedirect("../")
|
|
2112
|
-
profile, key = AssistantProfile.issue_key(profile.user)
|
|
2113
|
-
context = {
|
|
2114
|
-
**self.admin_site.each_context(request),
|
|
2115
|
-
"opts": self.model._meta,
|
|
2116
|
-
"original": profile,
|
|
2117
|
-
"user_key": key,
|
|
2118
|
-
}
|
|
2119
|
-
return TemplateResponse(request, "admin/assistantprofile_key.html", context)
|
|
2120
|
-
|
|
2121
|
-
def render_change_form(
|
|
2122
|
-
self, request, context, add=False, change=False, form_url="", obj=None
|
|
2123
|
-
):
|
|
2124
|
-
response = super().render_change_form(
|
|
2125
|
-
request, context, add=add, change=change, form_url=form_url, obj=obj
|
|
2126
|
-
)
|
|
2127
|
-
config = dict(getattr(settings, "MCP_SIGIL_SERVER", {}))
|
|
2128
|
-
host = config.get("host") or "127.0.0.1"
|
|
2129
|
-
port = config.get("port", 8800)
|
|
2130
|
-
base_url, issuer_url = resolve_base_urls(config)
|
|
2131
|
-
mount_path = config.get("mount_path") or "/"
|
|
2132
|
-
display_base_url = base_url or f"http://{host}:{port}"
|
|
2133
|
-
display_issuer_url = issuer_url or display_base_url
|
|
2134
|
-
chat_endpoint = f"{display_base_url.rstrip('/')}/api/chat/"
|
|
2135
|
-
if isinstance(response, dict):
|
|
2136
|
-
response.setdefault("mcp_server_host", host)
|
|
2137
|
-
response.setdefault("mcp_server_port", port)
|
|
2138
|
-
response.setdefault("mcp_server_base_url", display_base_url)
|
|
2139
|
-
response.setdefault("mcp_server_issuer_url", display_issuer_url)
|
|
2140
|
-
response.setdefault("mcp_server_mount_path", mount_path)
|
|
2141
|
-
response.setdefault("mcp_server_chat_endpoint", chat_endpoint)
|
|
2142
|
-
else:
|
|
2143
|
-
context_data = getattr(response, "context_data", None)
|
|
2144
|
-
if context_data is not None:
|
|
2145
|
-
context_data.setdefault("mcp_server_host", host)
|
|
2146
|
-
context_data.setdefault("mcp_server_port", port)
|
|
2147
|
-
context_data.setdefault("mcp_server_base_url", display_base_url)
|
|
2148
|
-
context_data.setdefault("mcp_server_issuer_url", display_issuer_url)
|
|
2149
|
-
context_data.setdefault("mcp_server_mount_path", mount_path)
|
|
2150
|
-
context_data.setdefault("mcp_server_chat_endpoint", chat_endpoint)
|
|
2151
|
-
return response
|
|
2152
|
-
|
|
2153
|
-
def start_server(self, request):
|
|
2154
|
-
try:
|
|
2155
|
-
pid = mcp_process.start_server()
|
|
2156
|
-
except mcp_process.ServerAlreadyRunningError as exc:
|
|
2157
|
-
self.message_user(request, str(exc), level=messages.WARNING)
|
|
2158
|
-
except mcp_process.ServerStartError as exc:
|
|
2159
|
-
self.message_user(request, str(exc), level=messages.ERROR)
|
|
2160
|
-
else:
|
|
2161
|
-
self.message_user(
|
|
2162
|
-
request,
|
|
2163
|
-
f"Started MCP server (PID {pid}).",
|
|
2164
|
-
level=messages.SUCCESS,
|
|
2165
|
-
)
|
|
2166
|
-
return self._redirect_to_changelist()
|
|
2167
|
-
|
|
2168
|
-
def stop_server(self, request):
|
|
2169
|
-
try:
|
|
2170
|
-
pid = mcp_process.stop_server()
|
|
2171
|
-
except mcp_process.ServerNotRunningError as exc:
|
|
2172
|
-
self.message_user(request, str(exc), level=messages.WARNING)
|
|
2173
|
-
except mcp_process.ServerStopError as exc:
|
|
2174
|
-
self.message_user(request, str(exc), level=messages.ERROR)
|
|
2175
|
-
else:
|
|
2176
|
-
self.message_user(
|
|
2177
|
-
request,
|
|
2178
|
-
f"Stopped MCP server (PID {pid}).",
|
|
2179
|
-
level=messages.SUCCESS,
|
|
2180
|
-
)
|
|
2181
|
-
return self._redirect_to_changelist()
|
|
2182
|
-
|
|
2183
|
-
def server_status(self, request):
|
|
2184
|
-
status = mcp_process.get_status()
|
|
2185
|
-
if status["running"]:
|
|
2186
|
-
msg = f"MCP server is running (PID {status['pid']})."
|
|
2187
|
-
level = messages.INFO
|
|
2188
|
-
else:
|
|
2189
|
-
msg = "MCP server is not running."
|
|
2190
|
-
level = messages.WARNING
|
|
2191
|
-
if status.get("last_error"):
|
|
2192
|
-
msg = f"{msg} {status['last_error']}"
|
|
2193
|
-
self.message_user(request, msg, level=level)
|
|
2194
|
-
return self._redirect_to_changelist()
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
1965
|
class EnergyCreditInline(admin.TabularInline):
|
|
2198
1966
|
model = EnergyCredit
|
|
2199
1967
|
fields = ("amount_kw", "created_by", "created_on")
|
|
@@ -2909,7 +2677,7 @@ class CopyRFIDForm(forms.Form):
|
|
|
2909
2677
|
normalized = (cleaned or "").strip().upper()
|
|
2910
2678
|
if not normalized:
|
|
2911
2679
|
raise forms.ValidationError(_("RFID value is required."))
|
|
2912
|
-
if RFID.
|
|
2680
|
+
if RFID.matching_queryset(normalized).exists():
|
|
2913
2681
|
raise forms.ValidationError(
|
|
2914
2682
|
_("An RFID with this value already exists.")
|
|
2915
2683
|
)
|
|
@@ -2943,6 +2711,7 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
|
|
|
2943
2711
|
"print_card_labels",
|
|
2944
2712
|
"print_release_form",
|
|
2945
2713
|
"copy_rfids",
|
|
2714
|
+
"merge_rfids",
|
|
2946
2715
|
"toggle_selected_user_data",
|
|
2947
2716
|
"toggle_selected_released",
|
|
2948
2717
|
"toggle_selected_allowed",
|
|
@@ -3177,6 +2946,145 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
|
|
|
3177
2946
|
context["media"] = self.media + form.media
|
|
3178
2947
|
return TemplateResponse(request, "admin/core/rfid/copy.html", context)
|
|
3179
2948
|
|
|
2949
|
+
@admin.action(description=_("Merge RFID cards"))
|
|
2950
|
+
def merge_rfids(self, request, queryset):
|
|
2951
|
+
tags = list(queryset.prefetch_related("energy_accounts"))
|
|
2952
|
+
if len(tags) < 2:
|
|
2953
|
+
self.message_user(
|
|
2954
|
+
request,
|
|
2955
|
+
_("Select at least two RFIDs to merge."),
|
|
2956
|
+
level=messages.WARNING,
|
|
2957
|
+
)
|
|
2958
|
+
return None
|
|
2959
|
+
|
|
2960
|
+
normalized_map: dict[int, str] = {}
|
|
2961
|
+
groups: defaultdict[str, list[RFID]] = defaultdict(list)
|
|
2962
|
+
unmatched = 0
|
|
2963
|
+
for tag in tags:
|
|
2964
|
+
normalized = RFID.normalize_code(tag.rfid)
|
|
2965
|
+
normalized_map[tag.pk] = normalized
|
|
2966
|
+
if not normalized:
|
|
2967
|
+
unmatched += 1
|
|
2968
|
+
continue
|
|
2969
|
+
prefix = normalized[: RFID.MATCH_PREFIX_LENGTH]
|
|
2970
|
+
groups[prefix].append(tag)
|
|
2971
|
+
|
|
2972
|
+
merge_groups: list[list[RFID]] = []
|
|
2973
|
+
skipped = unmatched
|
|
2974
|
+
for prefix, group in groups.items():
|
|
2975
|
+
if len(group) < 2:
|
|
2976
|
+
skipped += len(group)
|
|
2977
|
+
continue
|
|
2978
|
+
group.sort(
|
|
2979
|
+
key=lambda item: (
|
|
2980
|
+
len(normalized_map.get(item.pk, "")),
|
|
2981
|
+
normalized_map.get(item.pk, ""),
|
|
2982
|
+
item.pk,
|
|
2983
|
+
)
|
|
2984
|
+
)
|
|
2985
|
+
merge_groups.append(group)
|
|
2986
|
+
|
|
2987
|
+
if not merge_groups:
|
|
2988
|
+
self.message_user(
|
|
2989
|
+
request,
|
|
2990
|
+
_("No matching RFIDs were found to merge."),
|
|
2991
|
+
level=messages.WARNING,
|
|
2992
|
+
)
|
|
2993
|
+
return None
|
|
2994
|
+
|
|
2995
|
+
merged_tags = 0
|
|
2996
|
+
merged_groups = 0
|
|
2997
|
+
conflicting_accounts = 0
|
|
2998
|
+
with transaction.atomic():
|
|
2999
|
+
for group in merge_groups:
|
|
3000
|
+
canonical = group[0]
|
|
3001
|
+
update_fields: set[str] = set()
|
|
3002
|
+
existing_account_ids = set(
|
|
3003
|
+
canonical.energy_accounts.values_list("pk", flat=True)
|
|
3004
|
+
)
|
|
3005
|
+
for tag in group[1:]:
|
|
3006
|
+
other_value = normalized_map.get(tag.pk, "")
|
|
3007
|
+
if canonical.adopt_rfid(other_value):
|
|
3008
|
+
update_fields.add("rfid")
|
|
3009
|
+
normalized_map[canonical.pk] = RFID.normalize_code(
|
|
3010
|
+
canonical.rfid
|
|
3011
|
+
)
|
|
3012
|
+
accounts = list(tag.energy_accounts.all())
|
|
3013
|
+
if accounts:
|
|
3014
|
+
transferable: list[EnergyAccount] = []
|
|
3015
|
+
for account in accounts:
|
|
3016
|
+
if existing_account_ids and account.pk not in existing_account_ids:
|
|
3017
|
+
conflicting_accounts += 1
|
|
3018
|
+
continue
|
|
3019
|
+
transferable.append(account)
|
|
3020
|
+
if transferable:
|
|
3021
|
+
canonical.energy_accounts.add(*transferable)
|
|
3022
|
+
existing_account_ids.update(
|
|
3023
|
+
account.pk for account in transferable
|
|
3024
|
+
)
|
|
3025
|
+
if tag.allowed and not canonical.allowed:
|
|
3026
|
+
canonical.allowed = True
|
|
3027
|
+
update_fields.add("allowed")
|
|
3028
|
+
if tag.released and not canonical.released:
|
|
3029
|
+
canonical.released = True
|
|
3030
|
+
update_fields.add("released")
|
|
3031
|
+
if tag.key_a_verified and not canonical.key_a_verified:
|
|
3032
|
+
canonical.key_a_verified = True
|
|
3033
|
+
update_fields.add("key_a_verified")
|
|
3034
|
+
if tag.key_b_verified and not canonical.key_b_verified:
|
|
3035
|
+
canonical.key_b_verified = True
|
|
3036
|
+
update_fields.add("key_b_verified")
|
|
3037
|
+
if tag.last_seen_on and (
|
|
3038
|
+
not canonical.last_seen_on
|
|
3039
|
+
or tag.last_seen_on > canonical.last_seen_on
|
|
3040
|
+
):
|
|
3041
|
+
canonical.last_seen_on = tag.last_seen_on
|
|
3042
|
+
update_fields.add("last_seen_on")
|
|
3043
|
+
if not canonical.origin_node and tag.origin_node_id:
|
|
3044
|
+
canonical.origin_node = tag.origin_node
|
|
3045
|
+
update_fields.add("origin_node")
|
|
3046
|
+
merged_tags += 1
|
|
3047
|
+
tag.delete()
|
|
3048
|
+
if update_fields:
|
|
3049
|
+
canonical.save(update_fields=sorted(update_fields))
|
|
3050
|
+
merged_groups += 1
|
|
3051
|
+
|
|
3052
|
+
if merged_tags:
|
|
3053
|
+
self.message_user(
|
|
3054
|
+
request,
|
|
3055
|
+
ngettext(
|
|
3056
|
+
"Merged %(removed)d RFID into %(groups)d canonical record.",
|
|
3057
|
+
"Merged %(removed)d RFIDs into %(groups)d canonical records.",
|
|
3058
|
+
merged_tags,
|
|
3059
|
+
)
|
|
3060
|
+
% {"removed": merged_tags, "groups": merged_groups},
|
|
3061
|
+
level=messages.SUCCESS,
|
|
3062
|
+
)
|
|
3063
|
+
|
|
3064
|
+
if skipped:
|
|
3065
|
+
self.message_user(
|
|
3066
|
+
request,
|
|
3067
|
+
ngettext(
|
|
3068
|
+
"Skipped %(count)d RFID because it did not share the first %(length)d characters with another selection.",
|
|
3069
|
+
"Skipped %(count)d RFIDs because they did not share the first %(length)d characters with another selection.",
|
|
3070
|
+
skipped,
|
|
3071
|
+
)
|
|
3072
|
+
% {"count": skipped, "length": RFID.MATCH_PREFIX_LENGTH},
|
|
3073
|
+
level=messages.WARNING,
|
|
3074
|
+
)
|
|
3075
|
+
|
|
3076
|
+
if conflicting_accounts:
|
|
3077
|
+
self.message_user(
|
|
3078
|
+
request,
|
|
3079
|
+
ngettext(
|
|
3080
|
+
"Skipped %(count)d energy account because the RFID was already linked to a different account.",
|
|
3081
|
+
"Skipped %(count)d energy accounts because the RFID was already linked to a different account.",
|
|
3082
|
+
conflicting_accounts,
|
|
3083
|
+
)
|
|
3084
|
+
% {"count": conflicting_accounts},
|
|
3085
|
+
level=messages.WARNING,
|
|
3086
|
+
)
|
|
3087
|
+
|
|
3180
3088
|
def _render_card_labels(
|
|
3181
3089
|
self,
|
|
3182
3090
|
request,
|
|
@@ -3549,6 +3457,7 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
|
|
|
3549
3457
|
"toggle_url": toggle_url,
|
|
3550
3458
|
"toggle_label": toggle_label,
|
|
3551
3459
|
"public_view_url": public_view_url,
|
|
3460
|
+
"deep_read_url": reverse("rfid-scan-deep"),
|
|
3552
3461
|
}
|
|
3553
3462
|
)
|
|
3554
3463
|
context["title"] = _("Scan RFIDs")
|
core/apps.py
CHANGED
|
@@ -348,9 +348,3 @@ class CoreConfig(AppConfig):
|
|
|
348
348
|
weak=False,
|
|
349
349
|
)
|
|
350
350
|
|
|
351
|
-
try:
|
|
352
|
-
from .mcp.auto_start import schedule_auto_start
|
|
353
|
-
|
|
354
|
-
schedule_auto_start(check_profiles_immediately=False)
|
|
355
|
-
except Exception: # pragma: no cover - defensive
|
|
356
|
-
logger.exception("Failed to schedule MCP auto-start")
|
core/backends.py
CHANGED
|
@@ -81,10 +81,16 @@ class RFIDBackend:
|
|
|
81
81
|
if not rfid_value:
|
|
82
82
|
return None
|
|
83
83
|
|
|
84
|
-
tag = RFID.
|
|
85
|
-
if not tag
|
|
84
|
+
tag = RFID.matching_queryset(rfid_value).filter(allowed=True).first()
|
|
85
|
+
if not tag:
|
|
86
86
|
return None
|
|
87
87
|
|
|
88
|
+
update_fields: list[str] = []
|
|
89
|
+
if tag.adopt_rfid(rfid_value):
|
|
90
|
+
update_fields.append("rfid")
|
|
91
|
+
if update_fields:
|
|
92
|
+
tag.save(update_fields=update_fields)
|
|
93
|
+
|
|
88
94
|
command = (tag.external_command or "").strip()
|
|
89
95
|
if command:
|
|
90
96
|
env = os.environ.copy()
|