arthexis 0.1.20__py3-none-any.whl → 0.1.22__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.20.dist-info → arthexis-0.1.22.dist-info}/METADATA +10 -11
- {arthexis-0.1.20.dist-info → arthexis-0.1.22.dist-info}/RECORD +34 -36
- config/asgi.py +1 -15
- config/settings.py +4 -26
- config/urls.py +5 -1
- core/admin.py +140 -252
- core/apps.py +0 -6
- core/environment.py +2 -220
- core/models.py +425 -77
- core/system.py +76 -0
- core/tests.py +153 -15
- core/views.py +35 -97
- nodes/admin.py +165 -32
- nodes/apps.py +11 -0
- nodes/models.py +26 -6
- nodes/tests.py +263 -1
- nodes/views.py +61 -1
- ocpp/admin.py +68 -7
- ocpp/consumers.py +1 -0
- ocpp/models.py +71 -1
- ocpp/tasks.py +99 -1
- ocpp/tests.py +310 -2
- ocpp/views.py +365 -5
- pages/admin.py +112 -15
- pages/apps.py +32 -0
- pages/context_processors.py +0 -12
- pages/forms.py +31 -8
- pages/models.py +42 -2
- pages/tests.py +361 -63
- pages/urls.py +5 -1
- pages/views.py +264 -16
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- {arthexis-0.1.20.dist-info → arthexis-0.1.22.dist-info}/WHEEL +0 -0
- {arthexis-0.1.20.dist-info → arthexis-0.1.22.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.20.dist-info → arthexis-0.1.22.dist-info}/top_level.txt +0 -0
core/admin.py
CHANGED
|
@@ -83,6 +83,7 @@ from .models import (
|
|
|
83
83
|
OdooProfile,
|
|
84
84
|
OpenPayProfile,
|
|
85
85
|
EmailInbox,
|
|
86
|
+
GoogleCalendarProfile,
|
|
86
87
|
SocialProfile,
|
|
87
88
|
EmailCollector,
|
|
88
89
|
Package,
|
|
@@ -91,9 +92,7 @@ from .models import (
|
|
|
91
92
|
SecurityGroup,
|
|
92
93
|
InviteLead,
|
|
93
94
|
PublicWifiAccess,
|
|
94
|
-
AssistantProfile,
|
|
95
95
|
Todo,
|
|
96
|
-
hash_key,
|
|
97
96
|
)
|
|
98
97
|
from .user_data import (
|
|
99
98
|
EntityModelAdmin,
|
|
@@ -110,8 +109,6 @@ from .rfid_import_export import (
|
|
|
110
109
|
parse_accounts,
|
|
111
110
|
serialize_accounts,
|
|
112
111
|
)
|
|
113
|
-
from .mcp import process as mcp_process
|
|
114
|
-
from .mcp.server import resolve_base_urls
|
|
115
112
|
from . import release as release_utils
|
|
116
113
|
|
|
117
114
|
logger = logging.getLogger(__name__)
|
|
@@ -1009,6 +1006,41 @@ class OpenPayProfileAdminForm(forms.ModelForm):
|
|
|
1009
1006
|
)
|
|
1010
1007
|
|
|
1011
1008
|
|
|
1009
|
+
class GoogleCalendarProfileAdminForm(forms.ModelForm):
|
|
1010
|
+
"""Admin form for :class:`core.models.GoogleCalendarProfile`."""
|
|
1011
|
+
|
|
1012
|
+
api_key = forms.CharField(
|
|
1013
|
+
widget=forms.PasswordInput(render_value=True),
|
|
1014
|
+
required=False,
|
|
1015
|
+
help_text="Leave blank to keep the current key.",
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
class Meta:
|
|
1019
|
+
model = GoogleCalendarProfile
|
|
1020
|
+
fields = "__all__"
|
|
1021
|
+
|
|
1022
|
+
def __init__(self, *args, **kwargs):
|
|
1023
|
+
super().__init__(*args, **kwargs)
|
|
1024
|
+
if self.instance.pk:
|
|
1025
|
+
self.fields["api_key"].initial = ""
|
|
1026
|
+
self.initial["api_key"] = ""
|
|
1027
|
+
else:
|
|
1028
|
+
self.fields["api_key"].required = True
|
|
1029
|
+
|
|
1030
|
+
def clean_api_key(self):
|
|
1031
|
+
key = self.cleaned_data.get("api_key")
|
|
1032
|
+
if not key and self.instance.pk:
|
|
1033
|
+
return keep_existing("api_key")
|
|
1034
|
+
return key
|
|
1035
|
+
|
|
1036
|
+
def _post_clean(self):
|
|
1037
|
+
super()._post_clean()
|
|
1038
|
+
_restore_sigil_values(
|
|
1039
|
+
self,
|
|
1040
|
+
["calendar_id", "api_key", "display_name", "timezone"],
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
|
|
1012
1044
|
class MaskedPasswordFormMixin:
|
|
1013
1045
|
"""Mixin that hides stored passwords while allowing updates."""
|
|
1014
1046
|
|
|
@@ -1226,6 +1258,15 @@ class OpenPayProfileInlineForm(ProfileFormMixin, OpenPayProfileAdminForm):
|
|
|
1226
1258
|
return cleaned
|
|
1227
1259
|
|
|
1228
1260
|
|
|
1261
|
+
class GoogleCalendarProfileInlineForm(
|
|
1262
|
+
ProfileFormMixin, GoogleCalendarProfileAdminForm
|
|
1263
|
+
):
|
|
1264
|
+
profile_fields = GoogleCalendarProfile.profile_fields
|
|
1265
|
+
|
|
1266
|
+
class Meta(GoogleCalendarProfileAdminForm.Meta):
|
|
1267
|
+
exclude = ("user", "group")
|
|
1268
|
+
|
|
1269
|
+
|
|
1229
1270
|
class EmailInboxInlineForm(ProfileFormMixin, EmailInboxAdminForm):
|
|
1230
1271
|
profile_fields = EmailInbox.profile_fields
|
|
1231
1272
|
|
|
@@ -1304,46 +1345,6 @@ class ReleaseManagerInlineForm(ProfileFormMixin, forms.ModelForm):
|
|
|
1304
1345
|
}
|
|
1305
1346
|
|
|
1306
1347
|
|
|
1307
|
-
class AssistantProfileInlineForm(ProfileFormMixin, forms.ModelForm):
|
|
1308
|
-
user_key = forms.CharField(
|
|
1309
|
-
required=False,
|
|
1310
|
-
widget=forms.PasswordInput(render_value=True),
|
|
1311
|
-
help_text="Provide a plain key to create or rotate credentials.",
|
|
1312
|
-
)
|
|
1313
|
-
profile_fields = ("assistant_name", "user_key", "scopes", "is_active")
|
|
1314
|
-
|
|
1315
|
-
class Meta:
|
|
1316
|
-
model = AssistantProfile
|
|
1317
|
-
fields = ("assistant_name", "scopes", "is_active")
|
|
1318
|
-
|
|
1319
|
-
def __init__(self, *args, **kwargs):
|
|
1320
|
-
super().__init__(*args, **kwargs)
|
|
1321
|
-
if not self.instance.pk and "is_active" in self.fields:
|
|
1322
|
-
self.fields["is_active"].initial = False
|
|
1323
|
-
|
|
1324
|
-
def clean(self):
|
|
1325
|
-
cleaned = super().clean()
|
|
1326
|
-
if cleaned.get("DELETE"):
|
|
1327
|
-
return cleaned
|
|
1328
|
-
if not self.instance.pk and not cleaned.get("user_key"):
|
|
1329
|
-
if cleaned.get("scopes") or cleaned.get("is_active"):
|
|
1330
|
-
raise forms.ValidationError(
|
|
1331
|
-
"Provide a user key to create an assistant profile."
|
|
1332
|
-
)
|
|
1333
|
-
return cleaned
|
|
1334
|
-
|
|
1335
|
-
def save(self, commit=True):
|
|
1336
|
-
instance = super().save(commit=False)
|
|
1337
|
-
user_key = self.cleaned_data.get("user_key")
|
|
1338
|
-
if user_key:
|
|
1339
|
-
instance.user_key_hash = hash_key(user_key)
|
|
1340
|
-
instance.last_used_at = None
|
|
1341
|
-
if commit:
|
|
1342
|
-
instance.save()
|
|
1343
|
-
self.save_m2m()
|
|
1344
|
-
return instance
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
1348
|
PROFILE_INLINE_CONFIG = {
|
|
1348
1349
|
OdooProfile: {
|
|
1349
1350
|
"form": OdooProfileInlineForm,
|
|
@@ -1390,6 +1391,16 @@ PROFILE_INLINE_CONFIG = {
|
|
|
1390
1391
|
),
|
|
1391
1392
|
"readonly_fields": ("verified_on", "verification_reference"),
|
|
1392
1393
|
},
|
|
1394
|
+
GoogleCalendarProfile: {
|
|
1395
|
+
"form": GoogleCalendarProfileInlineForm,
|
|
1396
|
+
"fields": (
|
|
1397
|
+
"display_name",
|
|
1398
|
+
"calendar_id",
|
|
1399
|
+
"api_key",
|
|
1400
|
+
"max_events",
|
|
1401
|
+
"timezone",
|
|
1402
|
+
),
|
|
1403
|
+
},
|
|
1393
1404
|
EmailInbox: {
|
|
1394
1405
|
"form": EmailInboxInlineForm,
|
|
1395
1406
|
"fields": (
|
|
@@ -1474,12 +1485,6 @@ PROFILE_INLINE_CONFIG = {
|
|
|
1474
1485
|
"secondary_pypi_url",
|
|
1475
1486
|
),
|
|
1476
1487
|
},
|
|
1477
|
-
AssistantProfile: {
|
|
1478
|
-
"form": AssistantProfileInlineForm,
|
|
1479
|
-
"fields": ("assistant_name", "user_key", "scopes", "is_active"),
|
|
1480
|
-
"readonly_fields": ("user_key_hash", "created_at", "last_used_at"),
|
|
1481
|
-
"template": "admin/edit_inline/profile_stacked.html",
|
|
1482
|
-
},
|
|
1483
1488
|
}
|
|
1484
1489
|
|
|
1485
1490
|
|
|
@@ -1526,7 +1531,6 @@ PROFILE_MODELS = (
|
|
|
1526
1531
|
EmailOutbox,
|
|
1527
1532
|
SocialProfile,
|
|
1528
1533
|
ReleaseManager,
|
|
1529
|
-
AssistantProfile,
|
|
1530
1534
|
)
|
|
1531
1535
|
USER_PROFILE_INLINES = [
|
|
1532
1536
|
_build_profile_inline(model, "user") for model in PROFILE_MODELS
|
|
@@ -1868,6 +1872,44 @@ class OpenPayProfileAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModel
|
|
|
1868
1872
|
verify_credentials_action.short_description = _("Test credentials")
|
|
1869
1873
|
|
|
1870
1874
|
|
|
1875
|
+
class GoogleCalendarProfileAdmin(
|
|
1876
|
+
ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin
|
|
1877
|
+
):
|
|
1878
|
+
form = GoogleCalendarProfileAdminForm
|
|
1879
|
+
list_display = ("owner", "calendar_identifier", "max_events")
|
|
1880
|
+
search_fields = (
|
|
1881
|
+
"display_name",
|
|
1882
|
+
"calendar_id",
|
|
1883
|
+
"user__username",
|
|
1884
|
+
"group__name",
|
|
1885
|
+
)
|
|
1886
|
+
changelist_actions = ["my_profile"]
|
|
1887
|
+
change_actions = ["my_profile_action"]
|
|
1888
|
+
fieldsets = (
|
|
1889
|
+
(_("Owner"), {"fields": ("user", "group")}),
|
|
1890
|
+
(
|
|
1891
|
+
_("Calendar"),
|
|
1892
|
+
{
|
|
1893
|
+
"fields": (
|
|
1894
|
+
"display_name",
|
|
1895
|
+
"calendar_id",
|
|
1896
|
+
"api_key",
|
|
1897
|
+
"max_events",
|
|
1898
|
+
"timezone",
|
|
1899
|
+
)
|
|
1900
|
+
},
|
|
1901
|
+
),
|
|
1902
|
+
)
|
|
1903
|
+
|
|
1904
|
+
@admin.display(description=_("Owner"))
|
|
1905
|
+
def owner(self, obj):
|
|
1906
|
+
return obj.owner_display()
|
|
1907
|
+
|
|
1908
|
+
@admin.display(description=_("Calendar"))
|
|
1909
|
+
def calendar_identifier(self, obj):
|
|
1910
|
+
display = obj.get_display_name()
|
|
1911
|
+
return display or obj.resolved_calendar_id()
|
|
1912
|
+
|
|
1871
1913
|
class EmailSearchForm(forms.Form):
|
|
1872
1914
|
subject = forms.CharField(
|
|
1873
1915
|
required=False, widget=forms.TextInput(attrs={"style": "width: 40em;"})
|
|
@@ -2013,188 +2055,6 @@ class EmailInboxAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmi
|
|
|
2013
2055
|
return TemplateResponse(request, "admin/core/emailinbox/search.html", context)
|
|
2014
2056
|
|
|
2015
2057
|
|
|
2016
|
-
@admin.register(AssistantProfile)
|
|
2017
|
-
class AssistantProfileAdmin(
|
|
2018
|
-
ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin
|
|
2019
|
-
):
|
|
2020
|
-
list_display = ("assistant_name", "owner", "created_at", "last_used_at", "is_active")
|
|
2021
|
-
readonly_fields = ("user_key_hash", "created_at", "last_used_at")
|
|
2022
|
-
|
|
2023
|
-
change_form_template = "admin/workgroupassistantprofile_change_form.html"
|
|
2024
|
-
change_list_template = "admin/assistantprofile_change_list.html"
|
|
2025
|
-
change_actions = ["my_profile_action"]
|
|
2026
|
-
changelist_actions = ["my_profile"]
|
|
2027
|
-
fieldsets = (
|
|
2028
|
-
("Owner", {"fields": ("user", "group")}),
|
|
2029
|
-
("Credentials", {"fields": ("user_key_hash",)}),
|
|
2030
|
-
(
|
|
2031
|
-
"Configuration",
|
|
2032
|
-
{
|
|
2033
|
-
"fields": (
|
|
2034
|
-
"assistant_name",
|
|
2035
|
-
"scopes",
|
|
2036
|
-
"is_active",
|
|
2037
|
-
"created_at",
|
|
2038
|
-
"last_used_at",
|
|
2039
|
-
)
|
|
2040
|
-
},
|
|
2041
|
-
),
|
|
2042
|
-
)
|
|
2043
|
-
|
|
2044
|
-
def owner(self, obj):
|
|
2045
|
-
return obj.owner_display()
|
|
2046
|
-
|
|
2047
|
-
owner.short_description = "Owner"
|
|
2048
|
-
|
|
2049
|
-
def get_urls(self):
|
|
2050
|
-
urls = super().get_urls()
|
|
2051
|
-
opts = self.model._meta
|
|
2052
|
-
app_label = opts.app_label
|
|
2053
|
-
model_name = opts.model_name
|
|
2054
|
-
custom = [
|
|
2055
|
-
path(
|
|
2056
|
-
"<path:object_id>/generate-key/",
|
|
2057
|
-
self.admin_site.admin_view(self.generate_key),
|
|
2058
|
-
name=f"{app_label}_{model_name}_generate_key",
|
|
2059
|
-
),
|
|
2060
|
-
path(
|
|
2061
|
-
"server/start/",
|
|
2062
|
-
self.admin_site.admin_view(self.start_server),
|
|
2063
|
-
name=f"{app_label}_{model_name}_start_server",
|
|
2064
|
-
),
|
|
2065
|
-
path(
|
|
2066
|
-
"server/stop/",
|
|
2067
|
-
self.admin_site.admin_view(self.stop_server),
|
|
2068
|
-
name=f"{app_label}_{model_name}_stop_server",
|
|
2069
|
-
),
|
|
2070
|
-
path(
|
|
2071
|
-
"server/status/",
|
|
2072
|
-
self.admin_site.admin_view(self.server_status),
|
|
2073
|
-
name=f"{app_label}_{model_name}_status",
|
|
2074
|
-
),
|
|
2075
|
-
]
|
|
2076
|
-
return custom + urls
|
|
2077
|
-
|
|
2078
|
-
def changelist_view(self, request, extra_context=None):
|
|
2079
|
-
extra_context = extra_context or {}
|
|
2080
|
-
status = mcp_process.get_status()
|
|
2081
|
-
opts = self.model._meta
|
|
2082
|
-
app_label = opts.app_label
|
|
2083
|
-
model_name = opts.model_name
|
|
2084
|
-
extra_context.update(
|
|
2085
|
-
{
|
|
2086
|
-
"mcp_status": status,
|
|
2087
|
-
"mcp_server_actions": {
|
|
2088
|
-
"start": reverse(f"admin:{app_label}_{model_name}_start_server"),
|
|
2089
|
-
"stop": reverse(f"admin:{app_label}_{model_name}_stop_server"),
|
|
2090
|
-
"status": reverse(f"admin:{app_label}_{model_name}_status"),
|
|
2091
|
-
},
|
|
2092
|
-
}
|
|
2093
|
-
)
|
|
2094
|
-
return super().changelist_view(request, extra_context=extra_context)
|
|
2095
|
-
|
|
2096
|
-
def _redirect_to_changelist(self):
|
|
2097
|
-
opts = self.model._meta
|
|
2098
|
-
return HttpResponseRedirect(
|
|
2099
|
-
reverse(f"admin:{opts.app_label}_{opts.model_name}_changelist")
|
|
2100
|
-
)
|
|
2101
|
-
|
|
2102
|
-
def generate_key(self, request, object_id, *args, **kwargs):
|
|
2103
|
-
profile = self.get_object(request, object_id)
|
|
2104
|
-
if profile is None:
|
|
2105
|
-
return HttpResponseRedirect("../")
|
|
2106
|
-
if profile.user is None:
|
|
2107
|
-
self.message_user(
|
|
2108
|
-
request,
|
|
2109
|
-
"Assign a user before generating a key.",
|
|
2110
|
-
level=messages.ERROR,
|
|
2111
|
-
)
|
|
2112
|
-
return HttpResponseRedirect("../")
|
|
2113
|
-
profile, key = AssistantProfile.issue_key(profile.user)
|
|
2114
|
-
context = {
|
|
2115
|
-
**self.admin_site.each_context(request),
|
|
2116
|
-
"opts": self.model._meta,
|
|
2117
|
-
"original": profile,
|
|
2118
|
-
"user_key": key,
|
|
2119
|
-
}
|
|
2120
|
-
return TemplateResponse(request, "admin/assistantprofile_key.html", context)
|
|
2121
|
-
|
|
2122
|
-
def render_change_form(
|
|
2123
|
-
self, request, context, add=False, change=False, form_url="", obj=None
|
|
2124
|
-
):
|
|
2125
|
-
response = super().render_change_form(
|
|
2126
|
-
request, context, add=add, change=change, form_url=form_url, obj=obj
|
|
2127
|
-
)
|
|
2128
|
-
config = dict(getattr(settings, "MCP_SIGIL_SERVER", {}))
|
|
2129
|
-
host = config.get("host") or "127.0.0.1"
|
|
2130
|
-
port = config.get("port", 8800)
|
|
2131
|
-
base_url, issuer_url = resolve_base_urls(config)
|
|
2132
|
-
mount_path = config.get("mount_path") or "/"
|
|
2133
|
-
display_base_url = base_url or f"http://{host}:{port}"
|
|
2134
|
-
display_issuer_url = issuer_url or display_base_url
|
|
2135
|
-
chat_endpoint = f"{display_base_url.rstrip('/')}/api/chat/"
|
|
2136
|
-
if isinstance(response, dict):
|
|
2137
|
-
response.setdefault("mcp_server_host", host)
|
|
2138
|
-
response.setdefault("mcp_server_port", port)
|
|
2139
|
-
response.setdefault("mcp_server_base_url", display_base_url)
|
|
2140
|
-
response.setdefault("mcp_server_issuer_url", display_issuer_url)
|
|
2141
|
-
response.setdefault("mcp_server_mount_path", mount_path)
|
|
2142
|
-
response.setdefault("mcp_server_chat_endpoint", chat_endpoint)
|
|
2143
|
-
else:
|
|
2144
|
-
context_data = getattr(response, "context_data", None)
|
|
2145
|
-
if context_data is not None:
|
|
2146
|
-
context_data.setdefault("mcp_server_host", host)
|
|
2147
|
-
context_data.setdefault("mcp_server_port", port)
|
|
2148
|
-
context_data.setdefault("mcp_server_base_url", display_base_url)
|
|
2149
|
-
context_data.setdefault("mcp_server_issuer_url", display_issuer_url)
|
|
2150
|
-
context_data.setdefault("mcp_server_mount_path", mount_path)
|
|
2151
|
-
context_data.setdefault("mcp_server_chat_endpoint", chat_endpoint)
|
|
2152
|
-
return response
|
|
2153
|
-
|
|
2154
|
-
def start_server(self, request):
|
|
2155
|
-
try:
|
|
2156
|
-
pid = mcp_process.start_server()
|
|
2157
|
-
except mcp_process.ServerAlreadyRunningError as exc:
|
|
2158
|
-
self.message_user(request, str(exc), level=messages.WARNING)
|
|
2159
|
-
except mcp_process.ServerStartError as exc:
|
|
2160
|
-
self.message_user(request, str(exc), level=messages.ERROR)
|
|
2161
|
-
else:
|
|
2162
|
-
self.message_user(
|
|
2163
|
-
request,
|
|
2164
|
-
f"Started MCP server (PID {pid}).",
|
|
2165
|
-
level=messages.SUCCESS,
|
|
2166
|
-
)
|
|
2167
|
-
return self._redirect_to_changelist()
|
|
2168
|
-
|
|
2169
|
-
def stop_server(self, request):
|
|
2170
|
-
try:
|
|
2171
|
-
pid = mcp_process.stop_server()
|
|
2172
|
-
except mcp_process.ServerNotRunningError as exc:
|
|
2173
|
-
self.message_user(request, str(exc), level=messages.WARNING)
|
|
2174
|
-
except mcp_process.ServerStopError as exc:
|
|
2175
|
-
self.message_user(request, str(exc), level=messages.ERROR)
|
|
2176
|
-
else:
|
|
2177
|
-
self.message_user(
|
|
2178
|
-
request,
|
|
2179
|
-
f"Stopped MCP server (PID {pid}).",
|
|
2180
|
-
level=messages.SUCCESS,
|
|
2181
|
-
)
|
|
2182
|
-
return self._redirect_to_changelist()
|
|
2183
|
-
|
|
2184
|
-
def server_status(self, request):
|
|
2185
|
-
status = mcp_process.get_status()
|
|
2186
|
-
if status["running"]:
|
|
2187
|
-
msg = f"MCP server is running (PID {status['pid']})."
|
|
2188
|
-
level = messages.INFO
|
|
2189
|
-
else:
|
|
2190
|
-
msg = "MCP server is not running."
|
|
2191
|
-
level = messages.WARNING
|
|
2192
|
-
if status.get("last_error"):
|
|
2193
|
-
msg = f"{msg} {status['last_error']}"
|
|
2194
|
-
self.message_user(request, msg, level=level)
|
|
2195
|
-
return self._redirect_to_changelist()
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
2058
|
class EnergyCreditInline(admin.TabularInline):
|
|
2199
2059
|
model = EnergyCredit
|
|
2200
2060
|
fields = ("amount_kw", "created_by", "created_on")
|
|
@@ -2649,8 +2509,30 @@ class ProductAdmin(EntityModelAdmin):
|
|
|
2649
2509
|
],
|
|
2650
2510
|
limit=0,
|
|
2651
2511
|
)
|
|
2652
|
-
except Exception:
|
|
2512
|
+
except Exception as exc:
|
|
2513
|
+
logger.exception(
|
|
2514
|
+
"Failed to fetch Odoo products for user %s (profile_id=%s, host=%s, database=%s)",
|
|
2515
|
+
getattr(getattr(request, "user", None), "pk", None),
|
|
2516
|
+
getattr(profile, "pk", None),
|
|
2517
|
+
getattr(profile, "host", None),
|
|
2518
|
+
getattr(profile, "database", None),
|
|
2519
|
+
)
|
|
2653
2520
|
context["error"] = _("Unable to fetch products from Odoo.")
|
|
2521
|
+
if getattr(request.user, "is_superuser", False):
|
|
2522
|
+
fault = getattr(exc, "faultString", "")
|
|
2523
|
+
message = str(exc)
|
|
2524
|
+
details = [
|
|
2525
|
+
f"Host: {getattr(profile, 'host', '')}",
|
|
2526
|
+
f"Database: {getattr(profile, 'database', '')}",
|
|
2527
|
+
f"User ID: {getattr(profile, 'odoo_uid', '')}",
|
|
2528
|
+
]
|
|
2529
|
+
if fault and fault != message:
|
|
2530
|
+
details.append(f"Fault: {fault}")
|
|
2531
|
+
if message:
|
|
2532
|
+
details.append(f"Exception: {type(exc).__name__}: {message}")
|
|
2533
|
+
else:
|
|
2534
|
+
details.append(f"Exception type: {type(exc).__name__}")
|
|
2535
|
+
context["debug_error"] = "\n".join(details)
|
|
2654
2536
|
return context, []
|
|
2655
2537
|
|
|
2656
2538
|
context["has_credentials"] = True
|
|
@@ -2930,7 +2812,7 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
|
|
|
2930
2812
|
"user_data_flag",
|
|
2931
2813
|
"color",
|
|
2932
2814
|
"kind",
|
|
2933
|
-
"
|
|
2815
|
+
"endianness_short",
|
|
2934
2816
|
"released",
|
|
2935
2817
|
"allowed",
|
|
2936
2818
|
"last_seen_on",
|
|
@@ -2995,6 +2877,14 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
|
|
|
2995
2877
|
def user_data_flag(self, obj):
|
|
2996
2878
|
return getattr(obj, "is_user_data", False)
|
|
2997
2879
|
|
|
2880
|
+
@admin.display(description=_("End"), ordering="endianness")
|
|
2881
|
+
def endianness_short(self, obj):
|
|
2882
|
+
labels = {
|
|
2883
|
+
RFID.BIG_ENDIAN: _("Big"),
|
|
2884
|
+
RFID.LITTLE_ENDIAN: _("Little"),
|
|
2885
|
+
}
|
|
2886
|
+
return labels.get(obj.endianness, obj.get_endianness_display())
|
|
2887
|
+
|
|
2998
2888
|
def scan_rfids(self, request, queryset):
|
|
2999
2889
|
return redirect("admin:core_rfid_scan")
|
|
3000
2890
|
|
|
@@ -3690,6 +3580,7 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
|
|
|
3690
3580
|
"toggle_url": toggle_url,
|
|
3691
3581
|
"toggle_label": toggle_label,
|
|
3692
3582
|
"public_view_url": public_view_url,
|
|
3583
|
+
"deep_read_url": reverse("rfid-scan-deep"),
|
|
3693
3584
|
}
|
|
3694
3585
|
)
|
|
3695
3586
|
context["title"] = _("Scan RFIDs")
|
|
@@ -3947,9 +3838,9 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
3947
3838
|
self.message_user(request, str(exc), messages.ERROR)
|
|
3948
3839
|
return
|
|
3949
3840
|
releases = resp.json().get("releases", {})
|
|
3950
|
-
created = 0
|
|
3951
3841
|
updated = 0
|
|
3952
3842
|
restored = 0
|
|
3843
|
+
missing: list[str] = []
|
|
3953
3844
|
|
|
3954
3845
|
for version, files in releases.items():
|
|
3955
3846
|
release_on = self._release_on_from_files(files)
|
|
@@ -3974,23 +3865,11 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
3974
3865
|
if update_fields:
|
|
3975
3866
|
release.save(update_fields=update_fields)
|
|
3976
3867
|
continue
|
|
3977
|
-
|
|
3978
|
-
package=package,
|
|
3979
|
-
release_manager=package.release_manager,
|
|
3980
|
-
version=version,
|
|
3981
|
-
revision="",
|
|
3982
|
-
pypi_url=f"https://pypi.org/project/{package.name}/{version}/",
|
|
3983
|
-
release_on=release_on,
|
|
3984
|
-
)
|
|
3985
|
-
created += 1
|
|
3868
|
+
missing.append(version)
|
|
3986
3869
|
|
|
3987
|
-
if
|
|
3870
|
+
if updated or restored:
|
|
3988
3871
|
PackageRelease.dump_fixture()
|
|
3989
3872
|
message_parts = []
|
|
3990
|
-
if created:
|
|
3991
|
-
message_parts.append(
|
|
3992
|
-
f"Created {created} release{'s' if created != 1 else ''} from PyPI"
|
|
3993
|
-
)
|
|
3994
3873
|
if updated:
|
|
3995
3874
|
message_parts.append(
|
|
3996
3875
|
f"Updated release date for {updated} release"
|
|
@@ -4001,8 +3880,17 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
4001
3880
|
f"Restored {restored} release{'s' if restored != 1 else ''}"
|
|
4002
3881
|
)
|
|
4003
3882
|
self.message_user(request, "; ".join(message_parts), messages.SUCCESS)
|
|
4004
|
-
|
|
4005
|
-
self.message_user(request, "No
|
|
3883
|
+
elif not missing:
|
|
3884
|
+
self.message_user(request, "No matching releases found", messages.INFO)
|
|
3885
|
+
|
|
3886
|
+
if missing:
|
|
3887
|
+
versions = ", ".join(sorted(missing))
|
|
3888
|
+
count = len(missing)
|
|
3889
|
+
message = (
|
|
3890
|
+
"Manual creation required for "
|
|
3891
|
+
f"{count} release{'s' if count != 1 else ''}: {versions}"
|
|
3892
|
+
)
|
|
3893
|
+
self.message_user(request, message, messages.WARNING)
|
|
4006
3894
|
|
|
4007
3895
|
refresh_from_pypi.label = "Refresh from PyPI"
|
|
4008
3896
|
refresh_from_pypi.short_description = "Refresh from PyPI"
|
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")
|