arthexis 0.1.10__py3-none-any.whl → 0.1.12__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.10.dist-info → arthexis-0.1.12.dist-info}/METADATA +36 -26
- arthexis-0.1.12.dist-info/RECORD +102 -0
- config/context_processors.py +1 -0
- config/settings.py +31 -5
- config/urls.py +5 -4
- core/admin.py +430 -90
- core/apps.py +48 -2
- core/backends.py +38 -0
- core/environment.py +23 -5
- core/mailer.py +3 -1
- core/models.py +303 -31
- core/reference_utils.py +20 -9
- core/release.py +4 -0
- core/sigil_builder.py +7 -2
- core/sigil_resolver.py +35 -4
- core/system.py +250 -1
- core/tasks.py +92 -40
- core/temp_passwords.py +181 -0
- core/test_system_info.py +62 -2
- core/tests.py +169 -3
- core/user_data.py +51 -8
- core/views.py +371 -20
- nodes/admin.py +453 -8
- nodes/backends.py +21 -6
- nodes/dns.py +203 -0
- nodes/feature_checks.py +133 -0
- nodes/models.py +374 -31
- nodes/reports.py +411 -0
- nodes/tests.py +677 -38
- nodes/utils.py +32 -0
- nodes/views.py +14 -0
- ocpp/admin.py +278 -15
- ocpp/consumers.py +517 -16
- ocpp/evcs_discovery.py +158 -0
- ocpp/models.py +237 -4
- ocpp/reference_utils.py +42 -0
- ocpp/simulator.py +321 -22
- ocpp/store.py +110 -2
- ocpp/test_rfid.py +169 -7
- ocpp/tests.py +819 -6
- ocpp/transactions_io.py +17 -3
- ocpp/views.py +233 -19
- pages/admin.py +144 -4
- pages/context_processors.py +21 -7
- pages/defaults.py +13 -0
- pages/forms.py +38 -0
- pages/models.py +189 -15
- pages/tests.py +281 -8
- pages/urls.py +4 -0
- pages/views.py +137 -21
- arthexis-0.1.10.dist-info/RECORD +0 -95
- {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/WHEEL +0 -0
- {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/top_level.txt +0 -0
core/admin.py
CHANGED
|
@@ -21,6 +21,8 @@ from import_export.admin import ImportExportModelAdmin
|
|
|
21
21
|
from import_export.widgets import ForeignKeyWidget
|
|
22
22
|
from django.contrib.auth.models import Group
|
|
23
23
|
from django.templatetags.static import static
|
|
24
|
+
from django.utils import timezone
|
|
25
|
+
from django.utils.dateparse import parse_datetime
|
|
24
26
|
from django.utils.html import format_html
|
|
25
27
|
from django.utils.translation import gettext_lazy as _
|
|
26
28
|
from django.forms.models import BaseInlineFormSet
|
|
@@ -51,6 +53,7 @@ from .models import (
|
|
|
51
53
|
Reference,
|
|
52
54
|
OdooProfile,
|
|
53
55
|
EmailInbox,
|
|
56
|
+
SocialProfile,
|
|
54
57
|
EmailCollector,
|
|
55
58
|
Package,
|
|
56
59
|
PackageRelease,
|
|
@@ -73,6 +76,7 @@ from .user_data import (
|
|
|
73
76
|
)
|
|
74
77
|
from .widgets import OdooProductWidget
|
|
75
78
|
from .mcp import process as mcp_process
|
|
79
|
+
from .mcp.server import resolve_base_urls
|
|
76
80
|
|
|
77
81
|
|
|
78
82
|
admin.site.unregister(Group)
|
|
@@ -260,6 +264,7 @@ class ReferenceAdmin(EntityModelAdmin):
|
|
|
260
264
|
list_display = (
|
|
261
265
|
"alt_text",
|
|
262
266
|
"content_type",
|
|
267
|
+
"link",
|
|
263
268
|
"header",
|
|
264
269
|
"footer",
|
|
265
270
|
"visibility",
|
|
@@ -304,6 +309,15 @@ class ReferenceAdmin(EntityModelAdmin):
|
|
|
304
309
|
def visibility(self, obj):
|
|
305
310
|
return obj.get_footer_visibility_display()
|
|
306
311
|
|
|
312
|
+
@admin.display(description="LINK")
|
|
313
|
+
def link(self, obj):
|
|
314
|
+
if obj.value:
|
|
315
|
+
return format_html(
|
|
316
|
+
'<a href="{}" target="_blank" rel="noopener noreferrer">open</a>',
|
|
317
|
+
obj.value,
|
|
318
|
+
)
|
|
319
|
+
return ""
|
|
320
|
+
|
|
307
321
|
def get_urls(self):
|
|
308
322
|
urls = super().get_urls()
|
|
309
323
|
custom = [
|
|
@@ -358,6 +372,29 @@ class ReleaseManagerAdminForm(forms.ModelForm):
|
|
|
358
372
|
"github_token": forms.Textarea(attrs={"rows": 3, "style": "width: 40em;"}),
|
|
359
373
|
}
|
|
360
374
|
|
|
375
|
+
def __init__(self, *args, **kwargs):
|
|
376
|
+
super().__init__(*args, **kwargs)
|
|
377
|
+
self.fields["pypi_token"].help_text = format_html(
|
|
378
|
+
"{} <a href=\"{}\" target=\"_blank\" rel=\"noopener noreferrer\">{}</a>{}",
|
|
379
|
+
"Generate an API token from your PyPI account settings.",
|
|
380
|
+
"https://pypi.org/manage/account/token/",
|
|
381
|
+
"pypi.org/manage/account/token/",
|
|
382
|
+
(
|
|
383
|
+
" by clicking “Add API token”, optionally scoping it to the package, "
|
|
384
|
+
"and paste the full `pypi-***` value here."
|
|
385
|
+
),
|
|
386
|
+
)
|
|
387
|
+
self.fields["github_token"].help_text = format_html(
|
|
388
|
+
"{} <a href=\"{}\" target=\"_blank\" rel=\"noopener noreferrer\">{}</a>{}",
|
|
389
|
+
"Create a personal access token at GitHub → Settings → Developer settings →",
|
|
390
|
+
"https://github.com/settings/tokens",
|
|
391
|
+
"github.com/settings/tokens",
|
|
392
|
+
(
|
|
393
|
+
" with the repository access needed for releases (repo scope for classic tokens "
|
|
394
|
+
"or an equivalent fine-grained token) and paste it here."
|
|
395
|
+
),
|
|
396
|
+
)
|
|
397
|
+
|
|
361
398
|
|
|
362
399
|
@admin.register(ReleaseManager)
|
|
363
400
|
class ReleaseManagerAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin):
|
|
@@ -532,7 +569,14 @@ class SecurityGroupAdmin(DjangoGroupAdmin):
|
|
|
532
569
|
|
|
533
570
|
|
|
534
571
|
class InviteLeadAdmin(EntityModelAdmin):
|
|
535
|
-
list_display = (
|
|
572
|
+
list_display = (
|
|
573
|
+
"email",
|
|
574
|
+
"mac_address",
|
|
575
|
+
"created_on",
|
|
576
|
+
"sent_on",
|
|
577
|
+
"sent_via_outbox",
|
|
578
|
+
"short_error",
|
|
579
|
+
)
|
|
536
580
|
search_fields = ("email", "comment")
|
|
537
581
|
readonly_fields = (
|
|
538
582
|
"created_on",
|
|
@@ -543,6 +587,7 @@ class InviteLeadAdmin(EntityModelAdmin):
|
|
|
543
587
|
"ip_address",
|
|
544
588
|
"mac_address",
|
|
545
589
|
"sent_on",
|
|
590
|
+
"sent_via_outbox",
|
|
546
591
|
"error",
|
|
547
592
|
)
|
|
548
593
|
|
|
@@ -669,28 +714,30 @@ class OdooProfileAdminForm(forms.ModelForm):
|
|
|
669
714
|
)
|
|
670
715
|
|
|
671
716
|
|
|
672
|
-
class
|
|
673
|
-
"""
|
|
674
|
-
|
|
675
|
-
password = forms.CharField(
|
|
676
|
-
widget=forms.PasswordInput(render_value=True),
|
|
677
|
-
required=False,
|
|
678
|
-
help_text="Leave blank to keep the current password.",
|
|
679
|
-
)
|
|
717
|
+
class MaskedPasswordFormMixin:
|
|
718
|
+
"""Mixin that hides stored passwords while allowing updates."""
|
|
680
719
|
|
|
681
|
-
|
|
682
|
-
model = EmailInbox
|
|
683
|
-
fields = "__all__"
|
|
720
|
+
password_sigil_fields: tuple[str, ...] = ()
|
|
684
721
|
|
|
685
722
|
def __init__(self, *args, **kwargs):
|
|
686
723
|
super().__init__(*args, **kwargs)
|
|
724
|
+
field = self.fields.get("password")
|
|
725
|
+
if field is None:
|
|
726
|
+
return
|
|
727
|
+
if not isinstance(field.widget, forms.PasswordInput):
|
|
728
|
+
field.widget = forms.PasswordInput()
|
|
729
|
+
field.widget.attrs.setdefault("autocomplete", "new-password")
|
|
730
|
+
field.help_text = field.help_text or "Leave blank to keep the current password."
|
|
687
731
|
if self.instance.pk:
|
|
688
|
-
|
|
732
|
+
field.initial = ""
|
|
689
733
|
self.initial["password"] = ""
|
|
690
734
|
else:
|
|
691
|
-
|
|
735
|
+
field.required = True
|
|
692
736
|
|
|
693
737
|
def clean_password(self):
|
|
738
|
+
field = self.fields.get("password")
|
|
739
|
+
if field is None:
|
|
740
|
+
return self.cleaned_data.get("password")
|
|
694
741
|
pwd = self.cleaned_data.get("password")
|
|
695
742
|
if not pwd and self.instance.pk:
|
|
696
743
|
return keep_existing("password")
|
|
@@ -698,10 +745,23 @@ class EmailInboxAdminForm(forms.ModelForm):
|
|
|
698
745
|
|
|
699
746
|
def _post_clean(self):
|
|
700
747
|
super()._post_clean()
|
|
701
|
-
|
|
702
|
-
self,
|
|
703
|
-
|
|
704
|
-
|
|
748
|
+
if self.password_sigil_fields:
|
|
749
|
+
_restore_sigil_values(self, self.password_sigil_fields)
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
class EmailInboxAdminForm(MaskedPasswordFormMixin, forms.ModelForm):
|
|
753
|
+
"""Admin form for :class:`core.models.EmailInbox` with hidden password."""
|
|
754
|
+
|
|
755
|
+
password = forms.CharField(
|
|
756
|
+
widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
|
|
757
|
+
required=False,
|
|
758
|
+
help_text="Leave blank to keep the current password.",
|
|
759
|
+
)
|
|
760
|
+
password_sigil_fields = ("username", "host", "password", "protocol")
|
|
761
|
+
|
|
762
|
+
class Meta:
|
|
763
|
+
model = EmailInbox
|
|
764
|
+
fields = "__all__"
|
|
705
765
|
|
|
706
766
|
|
|
707
767
|
class ProfileInlineFormSet(BaseInlineFormSet):
|
|
@@ -851,16 +911,33 @@ class EmailInboxInlineForm(ProfileFormMixin, EmailInboxAdminForm):
|
|
|
851
911
|
exclude = ("user", "group")
|
|
852
912
|
|
|
853
913
|
|
|
854
|
-
class
|
|
855
|
-
profile_fields =
|
|
914
|
+
class SocialProfileInlineForm(ProfileFormMixin, forms.ModelForm):
|
|
915
|
+
profile_fields = SocialProfile.profile_fields
|
|
916
|
+
|
|
917
|
+
class Meta:
|
|
918
|
+
model = SocialProfile
|
|
919
|
+
fields = ("network", "handle", "domain", "did")
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
class EmailOutboxAdminForm(MaskedPasswordFormMixin, forms.ModelForm):
|
|
923
|
+
"""Admin form for :class:`nodes.models.EmailOutbox` with hidden password."""
|
|
924
|
+
|
|
856
925
|
password = forms.CharField(
|
|
857
|
-
widget=forms.PasswordInput(
|
|
926
|
+
widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
|
|
858
927
|
required=False,
|
|
859
928
|
help_text="Leave blank to keep the current password.",
|
|
860
929
|
)
|
|
930
|
+
password_sigil_fields = ("password", "host", "username", "from_email")
|
|
861
931
|
|
|
862
932
|
class Meta:
|
|
863
933
|
model = EmailOutbox
|
|
934
|
+
fields = "__all__"
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
class EmailOutboxInlineForm(ProfileFormMixin, EmailOutboxAdminForm):
|
|
938
|
+
profile_fields = EmailOutbox.profile_fields
|
|
939
|
+
|
|
940
|
+
class Meta(EmailOutboxAdminForm.Meta):
|
|
864
941
|
fields = (
|
|
865
942
|
"password",
|
|
866
943
|
"host",
|
|
@@ -869,27 +946,7 @@ class EmailOutboxInlineForm(ProfileFormMixin, forms.ModelForm):
|
|
|
869
946
|
"use_tls",
|
|
870
947
|
"use_ssl",
|
|
871
948
|
"from_email",
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
def __init__(self, *args, **kwargs):
|
|
875
|
-
super().__init__(*args, **kwargs)
|
|
876
|
-
if self.instance.pk:
|
|
877
|
-
self.fields["password"].initial = ""
|
|
878
|
-
self.initial["password"] = ""
|
|
879
|
-
else:
|
|
880
|
-
self.fields["password"].required = True
|
|
881
|
-
|
|
882
|
-
def clean_password(self):
|
|
883
|
-
pwd = self.cleaned_data.get("password")
|
|
884
|
-
if not pwd and self.instance.pk:
|
|
885
|
-
return keep_existing("password")
|
|
886
|
-
return pwd
|
|
887
|
-
|
|
888
|
-
def _post_clean(self):
|
|
889
|
-
super()._post_clean()
|
|
890
|
-
_restore_sigil_values(
|
|
891
|
-
self,
|
|
892
|
-
["password", "host", "username", "from_email"],
|
|
949
|
+
"is_enabled",
|
|
893
950
|
)
|
|
894
951
|
|
|
895
952
|
|
|
@@ -998,6 +1055,22 @@ PROFILE_INLINE_CONFIG = {
|
|
|
998
1055
|
"from_email",
|
|
999
1056
|
),
|
|
1000
1057
|
},
|
|
1058
|
+
SocialProfile: {
|
|
1059
|
+
"form": SocialProfileInlineForm,
|
|
1060
|
+
"fieldsets": (
|
|
1061
|
+
(
|
|
1062
|
+
_("Configuration: Bluesky"),
|
|
1063
|
+
{
|
|
1064
|
+
"fields": ("network", "handle", "domain", "did"),
|
|
1065
|
+
"description": _(
|
|
1066
|
+
"1. Set your Bluesky handle to the domain managed by Arthexis. "
|
|
1067
|
+
"2. Publish a _atproto TXT record or /.well-known/atproto-did file pointing to the DID below. "
|
|
1068
|
+
"3. Save once Bluesky confirms the domain matches the DID."
|
|
1069
|
+
),
|
|
1070
|
+
},
|
|
1071
|
+
),
|
|
1072
|
+
),
|
|
1073
|
+
},
|
|
1001
1074
|
ReleaseManager: {
|
|
1002
1075
|
"form": ReleaseManagerInlineForm,
|
|
1003
1076
|
"fields": (
|
|
@@ -1056,6 +1129,7 @@ PROFILE_MODELS = (
|
|
|
1056
1129
|
OdooProfile,
|
|
1057
1130
|
EmailInbox,
|
|
1058
1131
|
EmailOutbox,
|
|
1132
|
+
SocialProfile,
|
|
1059
1133
|
ReleaseManager,
|
|
1060
1134
|
AssistantProfile,
|
|
1061
1135
|
)
|
|
@@ -1202,8 +1276,68 @@ class EmailCollectorInline(admin.TabularInline):
|
|
|
1202
1276
|
|
|
1203
1277
|
|
|
1204
1278
|
class EmailCollectorAdmin(EntityModelAdmin):
|
|
1205
|
-
list_display = ("inbox", "subject", "sender", "body", "fragment")
|
|
1206
|
-
search_fields = ("subject", "sender", "body", "fragment")
|
|
1279
|
+
list_display = ("name", "inbox", "subject", "sender", "body", "fragment")
|
|
1280
|
+
search_fields = ("name", "subject", "sender", "body", "fragment")
|
|
1281
|
+
actions = ["preview_messages"]
|
|
1282
|
+
|
|
1283
|
+
@admin.action(description=_("Preview matches"))
|
|
1284
|
+
def preview_messages(self, request, queryset):
|
|
1285
|
+
results = []
|
|
1286
|
+
for collector in queryset.select_related("inbox"):
|
|
1287
|
+
try:
|
|
1288
|
+
messages = collector.search_messages(limit=5)
|
|
1289
|
+
error = None
|
|
1290
|
+
except ValidationError as exc:
|
|
1291
|
+
messages = []
|
|
1292
|
+
error = str(exc)
|
|
1293
|
+
except Exception as exc: # pragma: no cover - admin feedback
|
|
1294
|
+
messages = []
|
|
1295
|
+
error = str(exc)
|
|
1296
|
+
results.append(
|
|
1297
|
+
{
|
|
1298
|
+
"collector": collector,
|
|
1299
|
+
"messages": messages,
|
|
1300
|
+
"error": error,
|
|
1301
|
+
}
|
|
1302
|
+
)
|
|
1303
|
+
context = {
|
|
1304
|
+
"title": _("Preview Email Collectors"),
|
|
1305
|
+
"results": results,
|
|
1306
|
+
"opts": self.model._meta,
|
|
1307
|
+
"queryset": queryset,
|
|
1308
|
+
}
|
|
1309
|
+
return TemplateResponse(
|
|
1310
|
+
request, "admin/core/emailcollector/preview.html", context
|
|
1311
|
+
)
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
@admin.register(SocialProfile)
|
|
1315
|
+
class SocialProfileAdmin(
|
|
1316
|
+
ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin
|
|
1317
|
+
):
|
|
1318
|
+
list_display = ("owner", "network", "handle", "domain")
|
|
1319
|
+
list_filter = ("network",)
|
|
1320
|
+
search_fields = ("handle", "domain", "did")
|
|
1321
|
+
changelist_actions = ["my_profile"]
|
|
1322
|
+
change_actions = ["my_profile_action"]
|
|
1323
|
+
fieldsets = (
|
|
1324
|
+
(_("Owner"), {"fields": ("user", "group")}),
|
|
1325
|
+
(
|
|
1326
|
+
_("Configuration: Bluesky"),
|
|
1327
|
+
{
|
|
1328
|
+
"fields": ("network", "handle", "domain", "did"),
|
|
1329
|
+
"description": _(
|
|
1330
|
+
"Link Arthexis to Bluesky by using a verified domain handle. "
|
|
1331
|
+
"Publish a _atproto TXT record or /.well-known/atproto-did file "
|
|
1332
|
+
"that returns the DID stored here before saving."
|
|
1333
|
+
),
|
|
1334
|
+
},
|
|
1335
|
+
),
|
|
1336
|
+
)
|
|
1337
|
+
|
|
1338
|
+
@admin.display(description=_("Owner"))
|
|
1339
|
+
def owner(self, obj):
|
|
1340
|
+
return obj.owner_display()
|
|
1207
1341
|
|
|
1208
1342
|
|
|
1209
1343
|
@admin.register(OdooProfile)
|
|
@@ -1217,10 +1351,8 @@ class OdooProfileAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdm
|
|
|
1217
1351
|
changelist_actions = ["my_profile"]
|
|
1218
1352
|
fieldsets = (
|
|
1219
1353
|
("Owner", {"fields": ("user", "group")}),
|
|
1220
|
-
(
|
|
1221
|
-
|
|
1222
|
-
{"fields": ("host", "database", "username", "password")},
|
|
1223
|
-
),
|
|
1354
|
+
("Configuration", {"fields": ("host", "database")}),
|
|
1355
|
+
("Credentials", {"fields": ("username", "password")}),
|
|
1224
1356
|
(
|
|
1225
1357
|
"Odoo Employee",
|
|
1226
1358
|
{"fields": ("verified_on", "odoo_uid", "name", "email")},
|
|
@@ -1310,18 +1442,10 @@ class EmailInboxAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmi
|
|
|
1310
1442
|
|
|
1311
1443
|
fieldsets = (
|
|
1312
1444
|
("Owner", {"fields": ("user", "group")}),
|
|
1445
|
+
("Credentials", {"fields": ("username", "password")}),
|
|
1313
1446
|
(
|
|
1314
|
-
|
|
1315
|
-
{
|
|
1316
|
-
"fields": (
|
|
1317
|
-
"username",
|
|
1318
|
-
"host",
|
|
1319
|
-
"port",
|
|
1320
|
-
"password",
|
|
1321
|
-
"protocol",
|
|
1322
|
-
"use_ssl",
|
|
1323
|
-
)
|
|
1324
|
-
},
|
|
1447
|
+
"Configuration",
|
|
1448
|
+
{"fields": ("host", "port", "protocol", "use_ssl")},
|
|
1325
1449
|
),
|
|
1326
1450
|
)
|
|
1327
1451
|
|
|
@@ -1383,6 +1507,7 @@ class EmailInboxAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmi
|
|
|
1383
1507
|
subject=form.cleaned_data["subject"],
|
|
1384
1508
|
from_address=form.cleaned_data["from_address"],
|
|
1385
1509
|
body=form.cleaned_data["body"],
|
|
1510
|
+
use_regular_expressions=False,
|
|
1386
1511
|
)
|
|
1387
1512
|
context = {
|
|
1388
1513
|
"form": form,
|
|
@@ -1418,17 +1543,10 @@ class AssistantProfileAdmin(
|
|
|
1418
1543
|
changelist_actions = ["my_profile"]
|
|
1419
1544
|
fieldsets = (
|
|
1420
1545
|
("Owner", {"fields": ("user", "group")}),
|
|
1546
|
+
("Credentials", {"fields": ("user_key_hash",)}),
|
|
1421
1547
|
(
|
|
1422
|
-
|
|
1423
|
-
{
|
|
1424
|
-
"fields": (
|
|
1425
|
-
"scopes",
|
|
1426
|
-
"is_active",
|
|
1427
|
-
"user_key_hash",
|
|
1428
|
-
"created_at",
|
|
1429
|
-
"last_used_at",
|
|
1430
|
-
)
|
|
1431
|
-
},
|
|
1548
|
+
"Configuration",
|
|
1549
|
+
{"fields": ("scopes", "is_active", "created_at", "last_used_at")},
|
|
1432
1550
|
),
|
|
1433
1551
|
)
|
|
1434
1552
|
|
|
@@ -1519,14 +1637,19 @@ class AssistantProfileAdmin(
|
|
|
1519
1637
|
config = dict(getattr(settings, "MCP_SIGIL_SERVER", {}))
|
|
1520
1638
|
host = config.get("host") or "127.0.0.1"
|
|
1521
1639
|
port = config.get("port", 8800)
|
|
1640
|
+
base_url, issuer_url = resolve_base_urls(config)
|
|
1522
1641
|
if isinstance(response, dict):
|
|
1523
1642
|
response.setdefault("mcp_server_host", host)
|
|
1524
1643
|
response.setdefault("mcp_server_port", port)
|
|
1644
|
+
response.setdefault("mcp_server_base_url", base_url)
|
|
1645
|
+
response.setdefault("mcp_server_issuer_url", issuer_url)
|
|
1525
1646
|
else:
|
|
1526
1647
|
context_data = getattr(response, "context_data", None)
|
|
1527
1648
|
if context_data is not None:
|
|
1528
1649
|
context_data.setdefault("mcp_server_host", host)
|
|
1529
1650
|
context_data.setdefault("mcp_server_port", port)
|
|
1651
|
+
context_data.setdefault("mcp_server_base_url", base_url)
|
|
1652
|
+
context_data.setdefault("mcp_server_issuer_url", issuer_url)
|
|
1530
1653
|
return response
|
|
1531
1654
|
|
|
1532
1655
|
def start_server(self, request):
|
|
@@ -1814,7 +1937,7 @@ class ProductFetchWizardForm(forms.Form):
|
|
|
1814
1937
|
@admin.register(Product)
|
|
1815
1938
|
class ProductAdmin(EntityModelAdmin):
|
|
1816
1939
|
form = ProductAdminForm
|
|
1817
|
-
actions = ["fetch_odoo_product"]
|
|
1940
|
+
actions = ["fetch_odoo_product", "register_from_odoo"]
|
|
1818
1941
|
|
|
1819
1942
|
def _odoo_profile_admin(self):
|
|
1820
1943
|
return self.admin_site._registry.get(OdooProfile)
|
|
@@ -1824,7 +1947,7 @@ class ProductAdmin(EntityModelAdmin):
|
|
|
1824
1947
|
return profile.execute(
|
|
1825
1948
|
"product.product",
|
|
1826
1949
|
"search_read",
|
|
1827
|
-
domain,
|
|
1950
|
+
[domain],
|
|
1828
1951
|
{
|
|
1829
1952
|
"fields": [
|
|
1830
1953
|
"name",
|
|
@@ -1964,6 +2087,169 @@ class ProductAdmin(EntityModelAdmin):
|
|
|
1964
2087
|
context["media"] = self.media + form.media
|
|
1965
2088
|
return TemplateResponse(request, "admin/core/product/fetch_odoo.html", context)
|
|
1966
2089
|
|
|
2090
|
+
def get_urls(self):
|
|
2091
|
+
urls = super().get_urls()
|
|
2092
|
+
custom = [
|
|
2093
|
+
path(
|
|
2094
|
+
"register-from-odoo/",
|
|
2095
|
+
self.admin_site.admin_view(self.register_from_odoo_view),
|
|
2096
|
+
name=f"{self.opts.app_label}_{self.opts.model_name}_register_from_odoo",
|
|
2097
|
+
)
|
|
2098
|
+
]
|
|
2099
|
+
return custom + urls
|
|
2100
|
+
|
|
2101
|
+
@admin.action(description="Register from Odoo")
|
|
2102
|
+
def register_from_odoo(self, request, queryset=None): # pragma: no cover - simple redirect
|
|
2103
|
+
return HttpResponseRedirect(
|
|
2104
|
+
reverse(
|
|
2105
|
+
f"admin:{self.opts.app_label}_{self.opts.model_name}_register_from_odoo"
|
|
2106
|
+
)
|
|
2107
|
+
)
|
|
2108
|
+
|
|
2109
|
+
def _build_register_context(self, request):
|
|
2110
|
+
opts = self.model._meta
|
|
2111
|
+
context = self.admin_site.each_context(request)
|
|
2112
|
+
context.update(
|
|
2113
|
+
{
|
|
2114
|
+
"opts": opts,
|
|
2115
|
+
"title": _("Register from Odoo"),
|
|
2116
|
+
"has_credentials": False,
|
|
2117
|
+
"profile_url": None,
|
|
2118
|
+
"products": [],
|
|
2119
|
+
"selected_product_id": request.POST.get("product_id", ""),
|
|
2120
|
+
}
|
|
2121
|
+
)
|
|
2122
|
+
|
|
2123
|
+
profile_admin = self._odoo_profile_admin()
|
|
2124
|
+
if profile_admin is not None:
|
|
2125
|
+
context["profile_url"] = profile_admin.get_my_profile_url(request)
|
|
2126
|
+
|
|
2127
|
+
profile = getattr(request.user, "odoo_profile", None)
|
|
2128
|
+
if not profile or not profile.is_verified:
|
|
2129
|
+
context["credential_error"] = _(
|
|
2130
|
+
"Configure your Odoo employee credentials before registering products."
|
|
2131
|
+
)
|
|
2132
|
+
return context, None
|
|
2133
|
+
|
|
2134
|
+
try:
|
|
2135
|
+
products = profile.execute(
|
|
2136
|
+
"product.product",
|
|
2137
|
+
"search_read",
|
|
2138
|
+
[[]],
|
|
2139
|
+
{
|
|
2140
|
+
"fields": [
|
|
2141
|
+
"name",
|
|
2142
|
+
"description_sale",
|
|
2143
|
+
"list_price",
|
|
2144
|
+
"standard_price",
|
|
2145
|
+
],
|
|
2146
|
+
"limit": 0,
|
|
2147
|
+
},
|
|
2148
|
+
)
|
|
2149
|
+
except Exception:
|
|
2150
|
+
context["error"] = _("Unable to fetch products from Odoo.")
|
|
2151
|
+
return context, []
|
|
2152
|
+
|
|
2153
|
+
context["has_credentials"] = True
|
|
2154
|
+
simplified = []
|
|
2155
|
+
for product in products:
|
|
2156
|
+
simplified.append(
|
|
2157
|
+
{
|
|
2158
|
+
"id": product.get("id"),
|
|
2159
|
+
"name": product.get("name", ""),
|
|
2160
|
+
"description_sale": product.get("description_sale", ""),
|
|
2161
|
+
"list_price": product.get("list_price"),
|
|
2162
|
+
"standard_price": product.get("standard_price"),
|
|
2163
|
+
}
|
|
2164
|
+
)
|
|
2165
|
+
context["products"] = simplified
|
|
2166
|
+
return context, simplified
|
|
2167
|
+
|
|
2168
|
+
def register_from_odoo_view(self, request):
|
|
2169
|
+
context, products = self._build_register_context(request)
|
|
2170
|
+
if products is None:
|
|
2171
|
+
return TemplateResponse(
|
|
2172
|
+
request, "admin/core/product/register_from_odoo.html", context
|
|
2173
|
+
)
|
|
2174
|
+
|
|
2175
|
+
if request.method == "POST" and context.get("has_credentials"):
|
|
2176
|
+
if not self.has_add_permission(request):
|
|
2177
|
+
context["form_error"] = _(
|
|
2178
|
+
"You do not have permission to add products."
|
|
2179
|
+
)
|
|
2180
|
+
else:
|
|
2181
|
+
product_id = request.POST.get("product_id")
|
|
2182
|
+
if not product_id:
|
|
2183
|
+
context["form_error"] = _("Select a product to register.")
|
|
2184
|
+
else:
|
|
2185
|
+
try:
|
|
2186
|
+
odoo_id = int(product_id)
|
|
2187
|
+
except (TypeError, ValueError):
|
|
2188
|
+
context["form_error"] = _("Invalid product selection.")
|
|
2189
|
+
else:
|
|
2190
|
+
match = next(
|
|
2191
|
+
(item for item in products if item.get("id") == odoo_id),
|
|
2192
|
+
None,
|
|
2193
|
+
)
|
|
2194
|
+
if not match:
|
|
2195
|
+
context["form_error"] = _(
|
|
2196
|
+
"The selected product was not found. Reload the page and try again."
|
|
2197
|
+
)
|
|
2198
|
+
else:
|
|
2199
|
+
existing = self.model.objects.filter(
|
|
2200
|
+
odoo_product__id=odoo_id
|
|
2201
|
+
).first()
|
|
2202
|
+
if existing:
|
|
2203
|
+
self.message_user(
|
|
2204
|
+
request,
|
|
2205
|
+
_(
|
|
2206
|
+
"Product %(name)s already imported; opening existing record."
|
|
2207
|
+
)
|
|
2208
|
+
% {"name": existing.name},
|
|
2209
|
+
level=messages.WARNING,
|
|
2210
|
+
)
|
|
2211
|
+
return HttpResponseRedirect(
|
|
2212
|
+
reverse(
|
|
2213
|
+
"admin:%s_%s_change"
|
|
2214
|
+
% (
|
|
2215
|
+
existing._meta.app_label,
|
|
2216
|
+
existing._meta.model_name,
|
|
2217
|
+
),
|
|
2218
|
+
args=[existing.pk],
|
|
2219
|
+
)
|
|
2220
|
+
)
|
|
2221
|
+
product = self.model.objects.create(
|
|
2222
|
+
name=match.get("name") or f"Odoo Product {odoo_id}",
|
|
2223
|
+
description=match.get("description_sale", "") or "",
|
|
2224
|
+
renewal_period=30,
|
|
2225
|
+
odoo_product={
|
|
2226
|
+
"id": odoo_id,
|
|
2227
|
+
"name": match.get("name", ""),
|
|
2228
|
+
},
|
|
2229
|
+
)
|
|
2230
|
+
self.log_addition(
|
|
2231
|
+
request, product, "Registered product from Odoo"
|
|
2232
|
+
)
|
|
2233
|
+
self.message_user(
|
|
2234
|
+
request,
|
|
2235
|
+
_("Imported %(name)s from Odoo.")
|
|
2236
|
+
% {"name": product.name},
|
|
2237
|
+
)
|
|
2238
|
+
return HttpResponseRedirect(
|
|
2239
|
+
reverse(
|
|
2240
|
+
"admin:%s_%s_change"
|
|
2241
|
+
% (
|
|
2242
|
+
product._meta.app_label,
|
|
2243
|
+
product._meta.model_name,
|
|
2244
|
+
),
|
|
2245
|
+
args=[product.pk],
|
|
2246
|
+
)
|
|
2247
|
+
)
|
|
2248
|
+
|
|
2249
|
+
return TemplateResponse(
|
|
2250
|
+
request, "admin/core/product/register_from_odoo.html", context
|
|
2251
|
+
)
|
|
2252
|
+
|
|
1967
2253
|
|
|
1968
2254
|
class RFIDResource(resources.ModelResource):
|
|
1969
2255
|
reference = fields.Field(
|
|
@@ -2265,6 +2551,7 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
2265
2551
|
"package_link",
|
|
2266
2552
|
"is_current",
|
|
2267
2553
|
"pypi_url",
|
|
2554
|
+
"release_on",
|
|
2268
2555
|
"revision_short",
|
|
2269
2556
|
"published_status",
|
|
2270
2557
|
)
|
|
@@ -2272,7 +2559,7 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
2272
2559
|
actions = ["publish_release", "validate_releases"]
|
|
2273
2560
|
change_actions = ["publish_release_action"]
|
|
2274
2561
|
changelist_actions = ["refresh_from_pypi", "prepare_next_release"]
|
|
2275
|
-
readonly_fields = ("pypi_url", "is_current", "revision")
|
|
2562
|
+
readonly_fields = ("pypi_url", "release_on", "is_current", "revision")
|
|
2276
2563
|
fields = (
|
|
2277
2564
|
"package",
|
|
2278
2565
|
"release_manager",
|
|
@@ -2280,6 +2567,7 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
2280
2567
|
"revision",
|
|
2281
2568
|
"is_current",
|
|
2282
2569
|
"pypi_url",
|
|
2570
|
+
"release_on",
|
|
2283
2571
|
)
|
|
2284
2572
|
|
|
2285
2573
|
@admin.display(description="package", ordering="package")
|
|
@@ -2307,32 +2595,84 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
2307
2595
|
return
|
|
2308
2596
|
releases = resp.json().get("releases", {})
|
|
2309
2597
|
created = 0
|
|
2310
|
-
|
|
2311
|
-
|
|
2598
|
+
updated = 0
|
|
2599
|
+
restored = 0
|
|
2600
|
+
|
|
2601
|
+
for version, files in releases.items():
|
|
2602
|
+
release_on = self._release_on_from_files(files)
|
|
2603
|
+
release = PackageRelease.all_objects.filter(
|
|
2312
2604
|
package=package, version=version
|
|
2313
|
-
).
|
|
2314
|
-
if
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2605
|
+
).first()
|
|
2606
|
+
if release:
|
|
2607
|
+
update_fields = []
|
|
2608
|
+
if release.is_deleted:
|
|
2609
|
+
release.is_deleted = False
|
|
2610
|
+
update_fields.append("is_deleted")
|
|
2611
|
+
restored += 1
|
|
2612
|
+
if not release.pypi_url:
|
|
2613
|
+
release.pypi_url = (
|
|
2614
|
+
f"https://pypi.org/project/{package.name}/{version}/"
|
|
2615
|
+
)
|
|
2616
|
+
update_fields.append("pypi_url")
|
|
2617
|
+
if release_on and release.release_on != release_on:
|
|
2618
|
+
release.release_on = release_on
|
|
2619
|
+
update_fields.append("release_on")
|
|
2620
|
+
updated += 1
|
|
2621
|
+
if update_fields:
|
|
2622
|
+
release.save(update_fields=update_fields)
|
|
2623
|
+
continue
|
|
2624
|
+
PackageRelease.objects.create(
|
|
2625
|
+
package=package,
|
|
2626
|
+
release_manager=package.release_manager,
|
|
2627
|
+
version=version,
|
|
2628
|
+
revision="",
|
|
2629
|
+
pypi_url=f"https://pypi.org/project/{package.name}/{version}/",
|
|
2630
|
+
release_on=release_on,
|
|
2329
2631
|
)
|
|
2632
|
+
created += 1
|
|
2633
|
+
|
|
2634
|
+
if created or updated or restored:
|
|
2635
|
+
PackageRelease.dump_fixture()
|
|
2636
|
+
message_parts = []
|
|
2637
|
+
if created:
|
|
2638
|
+
message_parts.append(
|
|
2639
|
+
f"Created {created} release{'s' if created != 1 else ''} from PyPI"
|
|
2640
|
+
)
|
|
2641
|
+
if updated:
|
|
2642
|
+
message_parts.append(
|
|
2643
|
+
f"Updated release date for {updated} release"
|
|
2644
|
+
f"{'s' if updated != 1 else ''}"
|
|
2645
|
+
)
|
|
2646
|
+
if restored:
|
|
2647
|
+
message_parts.append(
|
|
2648
|
+
f"Restored {restored} release{'s' if restored != 1 else ''}"
|
|
2649
|
+
)
|
|
2650
|
+
self.message_user(request, "; ".join(message_parts), messages.SUCCESS)
|
|
2330
2651
|
else:
|
|
2331
2652
|
self.message_user(request, "No new releases found", messages.INFO)
|
|
2332
2653
|
|
|
2333
2654
|
refresh_from_pypi.label = "Refresh from PyPI"
|
|
2334
2655
|
refresh_from_pypi.short_description = "Refresh from PyPI"
|
|
2335
2656
|
|
|
2657
|
+
@staticmethod
|
|
2658
|
+
def _release_on_from_files(files):
|
|
2659
|
+
if not files:
|
|
2660
|
+
return None
|
|
2661
|
+
candidates = []
|
|
2662
|
+
for item in files:
|
|
2663
|
+
stamp = item.get("upload_time_iso_8601") or item.get("upload_time")
|
|
2664
|
+
if not stamp:
|
|
2665
|
+
continue
|
|
2666
|
+
when = parse_datetime(stamp)
|
|
2667
|
+
if when is None:
|
|
2668
|
+
continue
|
|
2669
|
+
if timezone.is_naive(when):
|
|
2670
|
+
when = timezone.make_aware(when, datetime.timezone.utc)
|
|
2671
|
+
candidates.append(when.astimezone(datetime.timezone.utc))
|
|
2672
|
+
if not candidates:
|
|
2673
|
+
return None
|
|
2674
|
+
return min(candidates)
|
|
2675
|
+
|
|
2336
2676
|
def prepare_next_release(self, request, queryset):
|
|
2337
2677
|
package = Package.objects.filter(is_active=True).first()
|
|
2338
2678
|
if not package:
|