arthexis 0.1.9__py3-none-any.whl → 0.1.11__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.9.dist-info → arthexis-0.1.11.dist-info}/METADATA +76 -23
- arthexis-0.1.11.dist-info/RECORD +99 -0
- config/context_processors.py +1 -0
- config/settings.py +245 -26
- config/urls.py +11 -4
- core/admin.py +585 -57
- core/apps.py +29 -1
- core/auto_upgrade.py +57 -0
- core/backends.py +115 -3
- core/environment.py +23 -5
- core/fields.py +93 -0
- core/mailer.py +3 -1
- core/models.py +482 -38
- core/reference_utils.py +108 -0
- core/sigil_builder.py +23 -5
- core/sigil_resolver.py +35 -4
- core/system.py +400 -140
- core/tasks.py +151 -8
- core/temp_passwords.py +181 -0
- core/test_system_info.py +97 -1
- core/tests.py +393 -15
- core/user_data.py +154 -16
- core/views.py +499 -20
- nodes/admin.py +149 -6
- nodes/backends.py +125 -18
- nodes/dns.py +203 -0
- nodes/models.py +498 -9
- nodes/tests.py +682 -3
- nodes/views.py +154 -7
- ocpp/admin.py +63 -3
- ocpp/consumers.py +255 -41
- ocpp/evcs.py +6 -3
- ocpp/models.py +52 -7
- ocpp/reference_utils.py +42 -0
- ocpp/simulator.py +62 -5
- ocpp/store.py +30 -0
- ocpp/test_rfid.py +169 -7
- ocpp/tests.py +414 -8
- ocpp/views.py +109 -76
- pages/admin.py +9 -1
- pages/context_processors.py +24 -4
- pages/defaults.py +14 -0
- pages/forms.py +131 -0
- pages/models.py +53 -14
- pages/tests.py +450 -14
- pages/urls.py +4 -0
- pages/views.py +419 -110
- arthexis-0.1.9.dist-info/RECORD +0 -92
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/WHEEL +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/top_level.txt +0 -0
core/admin.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from django import forms
|
|
2
2
|
from django.contrib import admin
|
|
3
3
|
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
|
|
4
|
-
from django.urls import path, reverse
|
|
4
|
+
from django.urls import NoReverseMatch, path, reverse
|
|
5
|
+
from urllib.parse import urlencode
|
|
5
6
|
from django.shortcuts import redirect, render
|
|
6
7
|
from django.http import JsonResponse, HttpResponseBase, HttpResponseRedirect
|
|
7
8
|
from django.template.response import TemplateResponse
|
|
@@ -20,6 +21,8 @@ from import_export.admin import ImportExportModelAdmin
|
|
|
20
21
|
from import_export.widgets import ForeignKeyWidget
|
|
21
22
|
from django.contrib.auth.models import Group
|
|
22
23
|
from django.templatetags.static import static
|
|
24
|
+
from django.utils import timezone
|
|
25
|
+
from django.utils.dateparse import parse_datetime
|
|
23
26
|
from django.utils.html import format_html
|
|
24
27
|
from django.utils.translation import gettext_lazy as _
|
|
25
28
|
from django.forms.models import BaseInlineFormSet
|
|
@@ -50,6 +53,7 @@ from .models import (
|
|
|
50
53
|
Reference,
|
|
51
54
|
OdooProfile,
|
|
52
55
|
EmailInbox,
|
|
56
|
+
SocialProfile,
|
|
53
57
|
EmailCollector,
|
|
54
58
|
Package,
|
|
55
59
|
PackageRelease,
|
|
@@ -61,7 +65,15 @@ from .models import (
|
|
|
61
65
|
Todo,
|
|
62
66
|
hash_key,
|
|
63
67
|
)
|
|
64
|
-
from .user_data import
|
|
68
|
+
from .user_data import (
|
|
69
|
+
EntityModelAdmin,
|
|
70
|
+
UserDatumAdminMixin,
|
|
71
|
+
delete_user_fixture,
|
|
72
|
+
dump_user_fixture,
|
|
73
|
+
_fixture_path,
|
|
74
|
+
_resolve_fixture_user,
|
|
75
|
+
_user_allows_user_data,
|
|
76
|
+
)
|
|
65
77
|
from .widgets import OdooProductWidget
|
|
66
78
|
from .mcp import process as mcp_process
|
|
67
79
|
|
|
@@ -161,11 +173,98 @@ class SaveBeforeChangeAction(DjangoObjectActions):
|
|
|
161
173
|
return super().response_change(request, obj)
|
|
162
174
|
|
|
163
175
|
|
|
176
|
+
class ProfileAdminMixin:
|
|
177
|
+
"""Reusable actions for profile-bound admin classes."""
|
|
178
|
+
|
|
179
|
+
def _resolve_my_profile_target(self, request):
|
|
180
|
+
opts = self.model._meta
|
|
181
|
+
changelist_url = reverse(
|
|
182
|
+
f"admin:{opts.app_label}_{opts.model_name}_changelist"
|
|
183
|
+
)
|
|
184
|
+
user = getattr(request, "user", None)
|
|
185
|
+
if not getattr(user, "is_authenticated", False):
|
|
186
|
+
return (
|
|
187
|
+
changelist_url,
|
|
188
|
+
_("You must be logged in to manage your profile."),
|
|
189
|
+
messages.ERROR,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
profile = self.model._default_manager.filter(user=user).first()
|
|
193
|
+
if profile is not None:
|
|
194
|
+
permission_check = getattr(self, "has_view_or_change_permission", None)
|
|
195
|
+
has_permission = (
|
|
196
|
+
permission_check(request, obj=profile)
|
|
197
|
+
if callable(permission_check)
|
|
198
|
+
else self.has_change_permission(request, obj=profile)
|
|
199
|
+
)
|
|
200
|
+
if has_permission:
|
|
201
|
+
change_url = reverse(
|
|
202
|
+
f"admin:{opts.app_label}_{opts.model_name}_change",
|
|
203
|
+
args=[profile.pk],
|
|
204
|
+
)
|
|
205
|
+
return change_url, None, None
|
|
206
|
+
return (
|
|
207
|
+
changelist_url,
|
|
208
|
+
_("You do not have permission to view this profile."),
|
|
209
|
+
messages.ERROR,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if self.has_add_permission(request):
|
|
213
|
+
add_url = reverse(f"admin:{opts.app_label}_{opts.model_name}_add")
|
|
214
|
+
params = {}
|
|
215
|
+
user_id = getattr(user, "pk", None)
|
|
216
|
+
if user_id:
|
|
217
|
+
params["user"] = user_id
|
|
218
|
+
if params:
|
|
219
|
+
add_url = f"{add_url}?{urlencode(params)}"
|
|
220
|
+
return add_url, None, None
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
changelist_url,
|
|
224
|
+
_("You do not have permission to create this profile."),
|
|
225
|
+
messages.ERROR,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def get_my_profile_url(self, request):
|
|
229
|
+
url, _message, _level = self._resolve_my_profile_target(request)
|
|
230
|
+
return url
|
|
231
|
+
|
|
232
|
+
def _redirect_to_my_profile(self, request):
|
|
233
|
+
target_url, message, level = self._resolve_my_profile_target(request)
|
|
234
|
+
if message:
|
|
235
|
+
self.message_user(request, message, level=level)
|
|
236
|
+
return HttpResponseRedirect(target_url)
|
|
237
|
+
|
|
238
|
+
@admin.action(description=_("Active Profile"))
|
|
239
|
+
def my_profile(self, request, queryset=None):
|
|
240
|
+
return self._redirect_to_my_profile(request)
|
|
241
|
+
|
|
242
|
+
def my_profile_action(self, request, obj=None):
|
|
243
|
+
return self._redirect_to_my_profile(request)
|
|
244
|
+
|
|
245
|
+
my_profile_action.label = _("Active Profile")
|
|
246
|
+
my_profile_action.short_description = _("Active Profile")
|
|
247
|
+
|
|
248
|
+
def get_actions(self, request):
|
|
249
|
+
actions = super().get_actions(request)
|
|
250
|
+
if "my_profile" not in actions:
|
|
251
|
+
action = getattr(self, "my_profile", None)
|
|
252
|
+
if action is not None:
|
|
253
|
+
actions["my_profile"] = (
|
|
254
|
+
action,
|
|
255
|
+
"my_profile",
|
|
256
|
+
getattr(action, "short_description", _("Active Profile")),
|
|
257
|
+
)
|
|
258
|
+
return actions
|
|
259
|
+
|
|
260
|
+
|
|
164
261
|
@admin.register(ExperienceReference)
|
|
165
262
|
class ReferenceAdmin(EntityModelAdmin):
|
|
166
263
|
list_display = (
|
|
167
264
|
"alt_text",
|
|
168
265
|
"content_type",
|
|
266
|
+
"link",
|
|
267
|
+
"header",
|
|
169
268
|
"footer",
|
|
170
269
|
"visibility",
|
|
171
270
|
"author",
|
|
@@ -182,6 +281,7 @@ class ReferenceAdmin(EntityModelAdmin):
|
|
|
182
281
|
"features",
|
|
183
282
|
"sites",
|
|
184
283
|
"include_in_footer",
|
|
284
|
+
"show_in_header",
|
|
185
285
|
"footer_visibility",
|
|
186
286
|
"transaction_uuid",
|
|
187
287
|
"author",
|
|
@@ -200,10 +300,23 @@ class ReferenceAdmin(EntityModelAdmin):
|
|
|
200
300
|
def footer(self, obj):
|
|
201
301
|
return obj.include_in_footer
|
|
202
302
|
|
|
303
|
+
@admin.display(description="Header", boolean=True, ordering="show_in_header")
|
|
304
|
+
def header(self, obj):
|
|
305
|
+
return obj.show_in_header
|
|
306
|
+
|
|
203
307
|
@admin.display(description="Visibility", ordering="footer_visibility")
|
|
204
308
|
def visibility(self, obj):
|
|
205
309
|
return obj.get_footer_visibility_display()
|
|
206
310
|
|
|
311
|
+
@admin.display(description="LINK")
|
|
312
|
+
def link(self, obj):
|
|
313
|
+
if obj.value:
|
|
314
|
+
return format_html(
|
|
315
|
+
'<a href="{}" target="_blank" rel="noopener noreferrer">open</a>',
|
|
316
|
+
obj.value,
|
|
317
|
+
)
|
|
318
|
+
return ""
|
|
319
|
+
|
|
207
320
|
def get_urls(self):
|
|
208
321
|
urls = super().get_urls()
|
|
209
322
|
custom = [
|
|
@@ -260,11 +373,12 @@ class ReleaseManagerAdminForm(forms.ModelForm):
|
|
|
260
373
|
|
|
261
374
|
|
|
262
375
|
@admin.register(ReleaseManager)
|
|
263
|
-
class ReleaseManagerAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
376
|
+
class ReleaseManagerAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin):
|
|
264
377
|
form = ReleaseManagerAdminForm
|
|
265
378
|
list_display = ("owner", "pypi_username", "pypi_url")
|
|
266
379
|
actions = ["test_credentials"]
|
|
267
|
-
change_actions = ["test_credentials_action"]
|
|
380
|
+
change_actions = ["test_credentials_action", "my_profile_action"]
|
|
381
|
+
changelist_actions = ["my_profile"]
|
|
268
382
|
fieldsets = (
|
|
269
383
|
("Owner", {"fields": ("user", "group")}),
|
|
270
384
|
(
|
|
@@ -325,9 +439,6 @@ class ReleaseManagerAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
325
439
|
request, f"{manager} credentials check failed: {exc}", messages.ERROR
|
|
326
440
|
)
|
|
327
441
|
|
|
328
|
-
def get_model_perms(self, request):
|
|
329
|
-
return {}
|
|
330
|
-
|
|
331
442
|
|
|
332
443
|
@admin.register(Package)
|
|
333
444
|
class PackageAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
@@ -338,7 +449,6 @@ class PackageAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
338
449
|
"release_manager",
|
|
339
450
|
"is_active",
|
|
340
451
|
)
|
|
341
|
-
actions = ["prepare_next_release"]
|
|
342
452
|
change_actions = ["prepare_next_release_action"]
|
|
343
453
|
|
|
344
454
|
def _prepare(self, request, package):
|
|
@@ -395,13 +505,6 @@ class PackageAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
395
505
|
return redirect("admin:core_package_changelist")
|
|
396
506
|
return self._prepare(request, package)
|
|
397
507
|
|
|
398
|
-
@admin.action(description="Prepare next Release")
|
|
399
|
-
def prepare_next_release(self, request, queryset):
|
|
400
|
-
if queryset.count() != 1:
|
|
401
|
-
self.message_user(request, "Select exactly one package", messages.ERROR)
|
|
402
|
-
return
|
|
403
|
-
return self._prepare(request, queryset.first())
|
|
404
|
-
|
|
405
508
|
def prepare_next_release_action(self, request, obj):
|
|
406
509
|
return self._prepare(request, obj)
|
|
407
510
|
|
|
@@ -442,7 +545,14 @@ class SecurityGroupAdmin(DjangoGroupAdmin):
|
|
|
442
545
|
|
|
443
546
|
|
|
444
547
|
class InviteLeadAdmin(EntityModelAdmin):
|
|
445
|
-
list_display = (
|
|
548
|
+
list_display = (
|
|
549
|
+
"email",
|
|
550
|
+
"mac_address",
|
|
551
|
+
"created_on",
|
|
552
|
+
"sent_on",
|
|
553
|
+
"sent_via_outbox",
|
|
554
|
+
"short_error",
|
|
555
|
+
)
|
|
446
556
|
search_fields = ("email", "comment")
|
|
447
557
|
readonly_fields = (
|
|
448
558
|
"created_on",
|
|
@@ -453,6 +563,7 @@ class InviteLeadAdmin(EntityModelAdmin):
|
|
|
453
563
|
"ip_address",
|
|
454
564
|
"mac_address",
|
|
455
565
|
"sent_on",
|
|
566
|
+
"sent_via_outbox",
|
|
456
567
|
"error",
|
|
457
568
|
)
|
|
458
569
|
|
|
@@ -617,6 +728,17 @@ class EmailInboxAdminForm(forms.ModelForm):
|
|
|
617
728
|
class ProfileInlineFormSet(BaseInlineFormSet):
|
|
618
729
|
"""Hide deletion controls and allow implicit removal when empty."""
|
|
619
730
|
|
|
731
|
+
@classmethod
|
|
732
|
+
def get_default_prefix(cls):
|
|
733
|
+
prefix = super().get_default_prefix()
|
|
734
|
+
if prefix:
|
|
735
|
+
return prefix
|
|
736
|
+
model_name = cls.model._meta.model_name
|
|
737
|
+
remote_field = getattr(cls.fk, "remote_field", None)
|
|
738
|
+
if remote_field is not None and getattr(remote_field, "one_to_one", False):
|
|
739
|
+
return model_name
|
|
740
|
+
return f"{model_name}_set"
|
|
741
|
+
|
|
620
742
|
def add_fields(self, form, index):
|
|
621
743
|
super().add_fields(form, index)
|
|
622
744
|
if "DELETE" in form.fields:
|
|
@@ -750,6 +872,14 @@ class EmailInboxInlineForm(ProfileFormMixin, EmailInboxAdminForm):
|
|
|
750
872
|
exclude = ("user", "group")
|
|
751
873
|
|
|
752
874
|
|
|
875
|
+
class SocialProfileInlineForm(ProfileFormMixin, forms.ModelForm):
|
|
876
|
+
profile_fields = SocialProfile.profile_fields
|
|
877
|
+
|
|
878
|
+
class Meta:
|
|
879
|
+
model = SocialProfile
|
|
880
|
+
fields = ("network", "handle", "domain", "did")
|
|
881
|
+
|
|
882
|
+
|
|
753
883
|
class EmailOutboxInlineForm(ProfileFormMixin, forms.ModelForm):
|
|
754
884
|
profile_fields = EmailOutbox.profile_fields
|
|
755
885
|
password = forms.CharField(
|
|
@@ -768,6 +898,7 @@ class EmailOutboxInlineForm(ProfileFormMixin, forms.ModelForm):
|
|
|
768
898
|
"use_tls",
|
|
769
899
|
"use_ssl",
|
|
770
900
|
"from_email",
|
|
901
|
+
"is_enabled",
|
|
771
902
|
)
|
|
772
903
|
|
|
773
904
|
def __init__(self, *args, **kwargs):
|
|
@@ -897,6 +1028,22 @@ PROFILE_INLINE_CONFIG = {
|
|
|
897
1028
|
"from_email",
|
|
898
1029
|
),
|
|
899
1030
|
},
|
|
1031
|
+
SocialProfile: {
|
|
1032
|
+
"form": SocialProfileInlineForm,
|
|
1033
|
+
"fieldsets": (
|
|
1034
|
+
(
|
|
1035
|
+
_("Configuration: Bluesky"),
|
|
1036
|
+
{
|
|
1037
|
+
"fields": ("network", "handle", "domain", "did"),
|
|
1038
|
+
"description": _(
|
|
1039
|
+
"1. Set your Bluesky handle to the domain managed by Arthexis. "
|
|
1040
|
+
"2. Publish a _atproto TXT record or /.well-known/atproto-did file pointing to the DID below. "
|
|
1041
|
+
"3. Save once Bluesky confirms the domain matches the DID."
|
|
1042
|
+
),
|
|
1043
|
+
},
|
|
1044
|
+
),
|
|
1045
|
+
),
|
|
1046
|
+
},
|
|
900
1047
|
ReleaseManager: {
|
|
901
1048
|
"form": ReleaseManagerInlineForm,
|
|
902
1049
|
"fields": (
|
|
@@ -955,6 +1102,7 @@ PROFILE_MODELS = (
|
|
|
955
1102
|
OdooProfile,
|
|
956
1103
|
EmailInbox,
|
|
957
1104
|
EmailOutbox,
|
|
1105
|
+
SocialProfile,
|
|
958
1106
|
ReleaseManager,
|
|
959
1107
|
AssistantProfile,
|
|
960
1108
|
)
|
|
@@ -974,21 +1122,61 @@ class UserPhoneNumberInline(admin.TabularInline):
|
|
|
974
1122
|
fields = ("number", "priority")
|
|
975
1123
|
|
|
976
1124
|
|
|
977
|
-
class UserAdmin(DjangoUserAdmin):
|
|
1125
|
+
class UserAdmin(UserDatumAdminMixin, DjangoUserAdmin):
|
|
978
1126
|
fieldsets = _append_operate_as(DjangoUserAdmin.fieldsets)
|
|
979
1127
|
add_fieldsets = _append_operate_as(DjangoUserAdmin.add_fieldsets)
|
|
980
1128
|
inlines = USER_PROFILE_INLINES + [UserPhoneNumberInline]
|
|
981
1129
|
change_form_template = "admin/user_profile_change_form.html"
|
|
1130
|
+
_skip_entity_user_datum = True
|
|
1131
|
+
|
|
1132
|
+
def _get_operate_as_profile_template(self):
|
|
1133
|
+
opts = self.model._meta
|
|
1134
|
+
try:
|
|
1135
|
+
return reverse(
|
|
1136
|
+
f"{self.admin_site.name}:{opts.app_label}_{opts.model_name}_change",
|
|
1137
|
+
args=["__ID__"],
|
|
1138
|
+
)
|
|
1139
|
+
except NoReverseMatch:
|
|
1140
|
+
user_opts = User._meta
|
|
1141
|
+
try:
|
|
1142
|
+
return reverse(
|
|
1143
|
+
f"{self.admin_site.name}:{user_opts.app_label}_{user_opts.model_name}_change",
|
|
1144
|
+
args=["__ID__"],
|
|
1145
|
+
)
|
|
1146
|
+
except NoReverseMatch:
|
|
1147
|
+
return None
|
|
982
1148
|
|
|
983
1149
|
def render_change_form(
|
|
984
1150
|
self, request, context, add=False, change=False, form_url="", obj=None
|
|
985
1151
|
):
|
|
986
|
-
|
|
1152
|
+
response = super().render_change_form(
|
|
987
1153
|
request, context, add=add, change=change, form_url=form_url, obj=obj
|
|
988
1154
|
)
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1155
|
+
if isinstance(response, dict):
|
|
1156
|
+
context_data = response
|
|
1157
|
+
else:
|
|
1158
|
+
context_data = getattr(response, "context_data", None)
|
|
1159
|
+
if context_data is not None:
|
|
1160
|
+
context_data["show_user_datum"] = False
|
|
1161
|
+
context_data["show_seed_datum"] = False
|
|
1162
|
+
context_data["show_save_as_copy"] = False
|
|
1163
|
+
operate_as_user = None
|
|
1164
|
+
operate_as_template = self._get_operate_as_profile_template()
|
|
1165
|
+
operate_as_url = None
|
|
1166
|
+
if obj and getattr(obj, "operate_as_id", None):
|
|
1167
|
+
try:
|
|
1168
|
+
operate_as_user = obj.operate_as
|
|
1169
|
+
except User.DoesNotExist:
|
|
1170
|
+
operate_as_user = None
|
|
1171
|
+
if operate_as_user and operate_as_template:
|
|
1172
|
+
operate_as_url = operate_as_template.replace(
|
|
1173
|
+
"__ID__", str(operate_as_user.pk)
|
|
1174
|
+
)
|
|
1175
|
+
if context_data is not None:
|
|
1176
|
+
context_data["operate_as_user"] = operate_as_user
|
|
1177
|
+
context_data["operate_as_profile_url_template"] = operate_as_template
|
|
1178
|
+
context_data["operate_as_profile_url"] = operate_as_url
|
|
1179
|
+
return response
|
|
992
1180
|
|
|
993
1181
|
def get_inline_instances(self, request, obj=None):
|
|
994
1182
|
inline_instances = super().get_inline_instances(request, obj)
|
|
@@ -1035,6 +1223,25 @@ class UserAdmin(DjangoUserAdmin):
|
|
|
1035
1223
|
should_store = bool(inline_form.cleaned_data.get("user_datum"))
|
|
1036
1224
|
self._update_profile_fixture(instance, owner_user, store=should_store)
|
|
1037
1225
|
|
|
1226
|
+
def save_model(self, request, obj, form, change):
|
|
1227
|
+
super().save_model(request, obj, form, change)
|
|
1228
|
+
if not getattr(obj, "pk", None):
|
|
1229
|
+
return
|
|
1230
|
+
target_user = _resolve_fixture_user(obj, obj)
|
|
1231
|
+
allow_user_data = _user_allows_user_data(target_user)
|
|
1232
|
+
if request.POST.get("_user_datum") == "on":
|
|
1233
|
+
type(obj).all_objects.filter(pk=obj.pk).update(is_user_data=False)
|
|
1234
|
+
obj.is_user_data = False
|
|
1235
|
+
delete_user_fixture(obj, target_user)
|
|
1236
|
+
self.message_user(
|
|
1237
|
+
request,
|
|
1238
|
+
_("User data for user accounts is managed through the profile sections."),
|
|
1239
|
+
)
|
|
1240
|
+
elif obj.is_user_data:
|
|
1241
|
+
type(obj).all_objects.filter(pk=obj.pk).update(is_user_data=False)
|
|
1242
|
+
obj.is_user_data = False
|
|
1243
|
+
delete_user_fixture(obj, target_user)
|
|
1244
|
+
|
|
1038
1245
|
|
|
1039
1246
|
class EmailCollectorInline(admin.TabularInline):
|
|
1040
1247
|
model = EmailCollector
|
|
@@ -1042,21 +1249,85 @@ class EmailCollectorInline(admin.TabularInline):
|
|
|
1042
1249
|
|
|
1043
1250
|
|
|
1044
1251
|
class EmailCollectorAdmin(EntityModelAdmin):
|
|
1045
|
-
list_display = ("inbox", "subject", "sender", "body", "fragment")
|
|
1046
|
-
search_fields = ("subject", "sender", "body", "fragment")
|
|
1252
|
+
list_display = ("name", "inbox", "subject", "sender", "body", "fragment")
|
|
1253
|
+
search_fields = ("name", "subject", "sender", "body", "fragment")
|
|
1254
|
+
actions = ["preview_messages"]
|
|
1255
|
+
|
|
1256
|
+
@admin.action(description=_("Preview matches"))
|
|
1257
|
+
def preview_messages(self, request, queryset):
|
|
1258
|
+
results = []
|
|
1259
|
+
for collector in queryset.select_related("inbox"):
|
|
1260
|
+
try:
|
|
1261
|
+
messages = collector.search_messages(limit=5)
|
|
1262
|
+
error = None
|
|
1263
|
+
except ValidationError as exc:
|
|
1264
|
+
messages = []
|
|
1265
|
+
error = str(exc)
|
|
1266
|
+
except Exception as exc: # pragma: no cover - admin feedback
|
|
1267
|
+
messages = []
|
|
1268
|
+
error = str(exc)
|
|
1269
|
+
results.append(
|
|
1270
|
+
{
|
|
1271
|
+
"collector": collector,
|
|
1272
|
+
"messages": messages,
|
|
1273
|
+
"error": error,
|
|
1274
|
+
}
|
|
1275
|
+
)
|
|
1276
|
+
context = {
|
|
1277
|
+
"title": _("Preview Email Collectors"),
|
|
1278
|
+
"results": results,
|
|
1279
|
+
"opts": self.model._meta,
|
|
1280
|
+
"queryset": queryset,
|
|
1281
|
+
}
|
|
1282
|
+
return TemplateResponse(
|
|
1283
|
+
request, "admin/core/emailcollector/preview.html", context
|
|
1284
|
+
)
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
@admin.register(SocialProfile)
|
|
1288
|
+
class SocialProfileAdmin(
|
|
1289
|
+
ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin
|
|
1290
|
+
):
|
|
1291
|
+
list_display = ("owner", "network", "handle", "domain")
|
|
1292
|
+
list_filter = ("network",)
|
|
1293
|
+
search_fields = ("handle", "domain", "did")
|
|
1294
|
+
changelist_actions = ["my_profile"]
|
|
1295
|
+
change_actions = ["my_profile_action"]
|
|
1296
|
+
fieldsets = (
|
|
1297
|
+
(_("Owner"), {"fields": ("user", "group")}),
|
|
1298
|
+
(
|
|
1299
|
+
_("Configuration: Bluesky"),
|
|
1300
|
+
{
|
|
1301
|
+
"fields": ("network", "handle", "domain", "did"),
|
|
1302
|
+
"description": _(
|
|
1303
|
+
"Link Arthexis to Bluesky by using a verified domain handle. "
|
|
1304
|
+
"Publish a _atproto TXT record or /.well-known/atproto-did file "
|
|
1305
|
+
"that returns the DID stored here before saving."
|
|
1306
|
+
),
|
|
1307
|
+
},
|
|
1308
|
+
),
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
@admin.display(description=_("Owner"))
|
|
1312
|
+
def owner(self, obj):
|
|
1313
|
+
return obj.owner_display()
|
|
1047
1314
|
|
|
1048
1315
|
|
|
1049
1316
|
@admin.register(OdooProfile)
|
|
1050
|
-
class OdooProfileAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
1317
|
+
class OdooProfileAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin):
|
|
1051
1318
|
change_form_template = "django_object_actions/change_form.html"
|
|
1052
1319
|
form = OdooProfileAdminForm
|
|
1053
1320
|
list_display = ("owner", "host", "database", "verified_on")
|
|
1054
1321
|
readonly_fields = ("verified_on", "odoo_uid", "name", "email")
|
|
1055
1322
|
actions = ["verify_credentials"]
|
|
1056
|
-
change_actions = ["verify_credentials_action"]
|
|
1323
|
+
change_actions = ["verify_credentials_action", "my_profile_action"]
|
|
1324
|
+
changelist_actions = ["my_profile"]
|
|
1057
1325
|
fieldsets = (
|
|
1058
1326
|
("Owner", {"fields": ("user", "group")}),
|
|
1059
|
-
(
|
|
1327
|
+
(
|
|
1328
|
+
"Configuration",
|
|
1329
|
+
{"fields": ("host", "database", "username", "password")},
|
|
1330
|
+
),
|
|
1060
1331
|
(
|
|
1061
1332
|
"Odoo Employee",
|
|
1062
1333
|
{"fields": ("verified_on", "odoo_uid", "name", "email")},
|
|
@@ -1088,9 +1359,6 @@ class OdooProfileAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
1088
1359
|
verify_credentials_action.label = "Test credentials"
|
|
1089
1360
|
verify_credentials_action.short_description = "Test credentials"
|
|
1090
1361
|
|
|
1091
|
-
def get_model_perms(self, request):
|
|
1092
|
-
return {}
|
|
1093
|
-
|
|
1094
1362
|
|
|
1095
1363
|
class EmailSearchForm(forms.Form):
|
|
1096
1364
|
subject = forms.CharField(
|
|
@@ -1107,11 +1375,12 @@ class EmailSearchForm(forms.Form):
|
|
|
1107
1375
|
)
|
|
1108
1376
|
|
|
1109
1377
|
|
|
1110
|
-
class EmailInboxAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
1378
|
+
class EmailInboxAdmin(ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin):
|
|
1111
1379
|
form = EmailInboxAdminForm
|
|
1112
1380
|
list_display = ("owner_label", "username", "host", "protocol")
|
|
1113
1381
|
actions = ["test_connection", "search_inbox", "test_collectors"]
|
|
1114
|
-
change_actions = ["test_collectors_action"]
|
|
1382
|
+
change_actions = ["test_collectors_action", "my_profile_action"]
|
|
1383
|
+
changelist_actions = ["my_profile"]
|
|
1115
1384
|
change_form_template = "admin/core/emailinbox/change_form.html"
|
|
1116
1385
|
inlines = [EmailCollectorInline]
|
|
1117
1386
|
|
|
@@ -1221,6 +1490,7 @@ class EmailInboxAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
1221
1490
|
subject=form.cleaned_data["subject"],
|
|
1222
1491
|
from_address=form.cleaned_data["from_address"],
|
|
1223
1492
|
body=form.cleaned_data["body"],
|
|
1493
|
+
use_regular_expressions=False,
|
|
1224
1494
|
)
|
|
1225
1495
|
context = {
|
|
1226
1496
|
"form": form,
|
|
@@ -1244,12 +1514,16 @@ class EmailInboxAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
1244
1514
|
|
|
1245
1515
|
|
|
1246
1516
|
@admin.register(AssistantProfile)
|
|
1247
|
-
class AssistantProfileAdmin(
|
|
1517
|
+
class AssistantProfileAdmin(
|
|
1518
|
+
ProfileAdminMixin, SaveBeforeChangeAction, EntityModelAdmin
|
|
1519
|
+
):
|
|
1248
1520
|
list_display = ("owner", "created_at", "last_used_at", "is_active")
|
|
1249
1521
|
readonly_fields = ("user_key_hash", "created_at", "last_used_at")
|
|
1250
1522
|
|
|
1251
1523
|
change_form_template = "admin/workgroupassistantprofile_change_form.html"
|
|
1252
1524
|
change_list_template = "admin/assistantprofile_change_list.html"
|
|
1525
|
+
change_actions = ["my_profile_action"]
|
|
1526
|
+
changelist_actions = ["my_profile"]
|
|
1253
1527
|
fieldsets = (
|
|
1254
1528
|
("Owner", {"fields": ("user", "group")}),
|
|
1255
1529
|
(
|
|
@@ -1406,9 +1680,6 @@ class AssistantProfileAdmin(EntityModelAdmin):
|
|
|
1406
1680
|
self.message_user(request, msg, level=level)
|
|
1407
1681
|
return self._redirect_to_changelist()
|
|
1408
1682
|
|
|
1409
|
-
def get_model_perms(self, request):
|
|
1410
|
-
return {}
|
|
1411
|
-
|
|
1412
1683
|
|
|
1413
1684
|
class EnergyCreditInline(admin.TabularInline):
|
|
1414
1685
|
model = EnergyCredit
|
|
@@ -1614,9 +1885,192 @@ class ProductAdminForm(forms.ModelForm):
|
|
|
1614
1885
|
widgets = {"odoo_product": OdooProductWidget}
|
|
1615
1886
|
|
|
1616
1887
|
|
|
1888
|
+
class ProductFetchWizardForm(forms.Form):
|
|
1889
|
+
name = forms.CharField(label="Name", required=False)
|
|
1890
|
+
default_code = forms.CharField(label="Internal reference", required=False)
|
|
1891
|
+
barcode = forms.CharField(label="Barcode", required=False)
|
|
1892
|
+
renewal_period = forms.IntegerField(
|
|
1893
|
+
label="Renewal period (days)", min_value=1, initial=30
|
|
1894
|
+
)
|
|
1895
|
+
|
|
1896
|
+
def __init__(self, *args, require_search_terms=True, **kwargs):
|
|
1897
|
+
self.require_search_terms = require_search_terms
|
|
1898
|
+
super().__init__(*args, **kwargs)
|
|
1899
|
+
|
|
1900
|
+
def clean(self):
|
|
1901
|
+
cleaned = super().clean()
|
|
1902
|
+
if self.require_search_terms:
|
|
1903
|
+
if not any(
|
|
1904
|
+
cleaned.get(field) for field in ("name", "default_code", "barcode")
|
|
1905
|
+
):
|
|
1906
|
+
raise forms.ValidationError(
|
|
1907
|
+
_("Enter at least one field to search for a product.")
|
|
1908
|
+
)
|
|
1909
|
+
return cleaned
|
|
1910
|
+
|
|
1911
|
+
def build_domain(self):
|
|
1912
|
+
domain = []
|
|
1913
|
+
if self.cleaned_data.get("name"):
|
|
1914
|
+
domain.append(("name", "ilike", self.cleaned_data["name"]))
|
|
1915
|
+
if self.cleaned_data.get("default_code"):
|
|
1916
|
+
domain.append(("default_code", "ilike", self.cleaned_data["default_code"]))
|
|
1917
|
+
if self.cleaned_data.get("barcode"):
|
|
1918
|
+
domain.append(("barcode", "ilike", self.cleaned_data["barcode"]))
|
|
1919
|
+
return domain
|
|
1920
|
+
|
|
1921
|
+
|
|
1617
1922
|
@admin.register(Product)
|
|
1618
1923
|
class ProductAdmin(EntityModelAdmin):
|
|
1619
1924
|
form = ProductAdminForm
|
|
1925
|
+
actions = ["fetch_odoo_product"]
|
|
1926
|
+
|
|
1927
|
+
def _odoo_profile_admin(self):
|
|
1928
|
+
return self.admin_site._registry.get(OdooProfile)
|
|
1929
|
+
|
|
1930
|
+
def _search_odoo_products(self, profile, form):
|
|
1931
|
+
domain = form.build_domain()
|
|
1932
|
+
return profile.execute(
|
|
1933
|
+
"product.product",
|
|
1934
|
+
"search_read",
|
|
1935
|
+
domain,
|
|
1936
|
+
{
|
|
1937
|
+
"fields": [
|
|
1938
|
+
"name",
|
|
1939
|
+
"default_code",
|
|
1940
|
+
"barcode",
|
|
1941
|
+
"description_sale",
|
|
1942
|
+
],
|
|
1943
|
+
"limit": 50,
|
|
1944
|
+
},
|
|
1945
|
+
)
|
|
1946
|
+
|
|
1947
|
+
@admin.action(description="Fetch Odoo Product")
|
|
1948
|
+
def fetch_odoo_product(self, request, queryset):
|
|
1949
|
+
profile = getattr(request.user, "odoo_profile", None)
|
|
1950
|
+
has_credentials = bool(profile and profile.is_verified)
|
|
1951
|
+
profile_admin = self._odoo_profile_admin()
|
|
1952
|
+
profile_url = None
|
|
1953
|
+
if profile_admin is not None:
|
|
1954
|
+
profile_url = profile_admin.get_my_profile_url(request)
|
|
1955
|
+
|
|
1956
|
+
context = {
|
|
1957
|
+
"opts": self.model._meta,
|
|
1958
|
+
"queryset": queryset,
|
|
1959
|
+
"action": "fetch_odoo_product",
|
|
1960
|
+
"has_credentials": has_credentials,
|
|
1961
|
+
"profile_url": profile_url,
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
if not has_credentials:
|
|
1965
|
+
context["credential_error"] = _(
|
|
1966
|
+
"Configure your Odoo employee credentials before fetching products."
|
|
1967
|
+
)
|
|
1968
|
+
return TemplateResponse(
|
|
1969
|
+
request, "admin/core/product/fetch_odoo.html", context
|
|
1970
|
+
)
|
|
1971
|
+
|
|
1972
|
+
is_import = "import" in request.POST
|
|
1973
|
+
form_kwargs = {"require_search_terms": not is_import}
|
|
1974
|
+
if request.method == "POST":
|
|
1975
|
+
form = ProductFetchWizardForm(request.POST, **form_kwargs)
|
|
1976
|
+
else:
|
|
1977
|
+
form = ProductFetchWizardForm()
|
|
1978
|
+
|
|
1979
|
+
results = None
|
|
1980
|
+
selected_product_id = request.POST.get("product_id", "")
|
|
1981
|
+
|
|
1982
|
+
if request.method == "POST" and form.is_valid():
|
|
1983
|
+
try:
|
|
1984
|
+
results = self._search_odoo_products(profile, form)
|
|
1985
|
+
except Exception:
|
|
1986
|
+
form.add_error(None, _("Unable to fetch products from Odoo."))
|
|
1987
|
+
results = []
|
|
1988
|
+
else:
|
|
1989
|
+
if is_import:
|
|
1990
|
+
if not self.has_add_permission(request):
|
|
1991
|
+
form.add_error(
|
|
1992
|
+
None, _("You do not have permission to add products.")
|
|
1993
|
+
)
|
|
1994
|
+
else:
|
|
1995
|
+
product_id = request.POST.get("product_id")
|
|
1996
|
+
if not product_id:
|
|
1997
|
+
form.add_error(None, _("Select a product to import."))
|
|
1998
|
+
else:
|
|
1999
|
+
try:
|
|
2000
|
+
odoo_id = int(product_id)
|
|
2001
|
+
except (TypeError, ValueError):
|
|
2002
|
+
form.add_error(None, _("Invalid product selection."))
|
|
2003
|
+
else:
|
|
2004
|
+
match = next(
|
|
2005
|
+
(item for item in results if item.get("id") == odoo_id),
|
|
2006
|
+
None,
|
|
2007
|
+
)
|
|
2008
|
+
if not match:
|
|
2009
|
+
form.add_error(
|
|
2010
|
+
None,
|
|
2011
|
+
_(
|
|
2012
|
+
"The selected product was not found. Run the search again."
|
|
2013
|
+
),
|
|
2014
|
+
)
|
|
2015
|
+
else:
|
|
2016
|
+
existing = self.model.objects.filter(
|
|
2017
|
+
odoo_product__id=odoo_id
|
|
2018
|
+
).first()
|
|
2019
|
+
if existing:
|
|
2020
|
+
self.message_user(
|
|
2021
|
+
request,
|
|
2022
|
+
_(
|
|
2023
|
+
"Product %(name)s already imported; opening existing record."
|
|
2024
|
+
)
|
|
2025
|
+
% {"name": existing.name},
|
|
2026
|
+
level=messages.WARNING,
|
|
2027
|
+
)
|
|
2028
|
+
return HttpResponseRedirect(
|
|
2029
|
+
reverse(
|
|
2030
|
+
"admin:%s_%s_change"
|
|
2031
|
+
% (
|
|
2032
|
+
existing._meta.app_label,
|
|
2033
|
+
existing._meta.model_name,
|
|
2034
|
+
),
|
|
2035
|
+
args=[existing.pk],
|
|
2036
|
+
)
|
|
2037
|
+
)
|
|
2038
|
+
product = self.model.objects.create(
|
|
2039
|
+
name=match.get("name") or f"Odoo Product {odoo_id}",
|
|
2040
|
+
description=match.get("description_sale", "") or "",
|
|
2041
|
+
renewal_period=form.cleaned_data["renewal_period"],
|
|
2042
|
+
odoo_product={
|
|
2043
|
+
"id": odoo_id,
|
|
2044
|
+
"name": match.get("name", ""),
|
|
2045
|
+
},
|
|
2046
|
+
)
|
|
2047
|
+
self.log_addition(
|
|
2048
|
+
request, product, "Imported product from Odoo"
|
|
2049
|
+
)
|
|
2050
|
+
self.message_user(
|
|
2051
|
+
request,
|
|
2052
|
+
_("Imported %(name)s from Odoo.")
|
|
2053
|
+
% {"name": product.name},
|
|
2054
|
+
)
|
|
2055
|
+
return HttpResponseRedirect(
|
|
2056
|
+
reverse(
|
|
2057
|
+
"admin:%s_%s_change"
|
|
2058
|
+
% (
|
|
2059
|
+
product._meta.app_label,
|
|
2060
|
+
product._meta.model_name,
|
|
2061
|
+
),
|
|
2062
|
+
args=[product.pk],
|
|
2063
|
+
)
|
|
2064
|
+
)
|
|
2065
|
+
context.update(
|
|
2066
|
+
{
|
|
2067
|
+
"form": form,
|
|
2068
|
+
"results": results,
|
|
2069
|
+
"selected_product_id": selected_product_id,
|
|
2070
|
+
}
|
|
2071
|
+
)
|
|
2072
|
+
context["media"] = self.media + form.media
|
|
2073
|
+
return TemplateResponse(request, "admin/core/product/fetch_odoo.html", context)
|
|
1620
2074
|
|
|
1621
2075
|
|
|
1622
2076
|
class RFIDResource(resources.ModelResource):
|
|
@@ -1756,30 +2210,39 @@ class ClientReportAdmin(EntityModelAdmin):
|
|
|
1756
2210
|
]
|
|
1757
2211
|
RECURRENCE_CHOICES = ClientReportSchedule.PERIODICITY_CHOICES
|
|
1758
2212
|
period = forms.ChoiceField(
|
|
1759
|
-
choices=PERIOD_CHOICES,
|
|
2213
|
+
choices=PERIOD_CHOICES,
|
|
2214
|
+
widget=forms.RadioSelect,
|
|
2215
|
+
initial="range",
|
|
2216
|
+
help_text="Choose how the reporting window will be calculated.",
|
|
1760
2217
|
)
|
|
1761
2218
|
start = forms.DateField(
|
|
1762
2219
|
label="Start date",
|
|
1763
2220
|
required=False,
|
|
1764
2221
|
widget=forms.DateInput(attrs={"type": "date"}),
|
|
2222
|
+
help_text="First day included when using a custom date range.",
|
|
1765
2223
|
)
|
|
1766
2224
|
end = forms.DateField(
|
|
1767
2225
|
label="End date",
|
|
1768
2226
|
required=False,
|
|
1769
2227
|
widget=forms.DateInput(attrs={"type": "date"}),
|
|
2228
|
+
help_text="Last day included when using a custom date range.",
|
|
1770
2229
|
)
|
|
1771
2230
|
week = forms.CharField(
|
|
1772
2231
|
label="Week",
|
|
1773
2232
|
required=False,
|
|
1774
2233
|
widget=forms.TextInput(attrs={"type": "week"}),
|
|
2234
|
+
help_text="Generates the report for the ISO week that you select.",
|
|
1775
2235
|
)
|
|
1776
2236
|
month = forms.DateField(
|
|
1777
2237
|
label="Month",
|
|
1778
2238
|
required=False,
|
|
1779
2239
|
widget=forms.DateInput(attrs={"type": "month"}),
|
|
2240
|
+
help_text="Generates the report for the calendar month that you select.",
|
|
1780
2241
|
)
|
|
1781
2242
|
owner = forms.ModelChoiceField(
|
|
1782
|
-
queryset=get_user_model().objects.all(),
|
|
2243
|
+
queryset=get_user_model().objects.all(),
|
|
2244
|
+
required=False,
|
|
2245
|
+
help_text="Sets who owns the report schedule and is listed as the requestor.",
|
|
1783
2246
|
)
|
|
1784
2247
|
destinations = forms.CharField(
|
|
1785
2248
|
label="Email destinations",
|
|
@@ -1791,6 +2254,7 @@ class ClientReportAdmin(EntityModelAdmin):
|
|
|
1791
2254
|
label="Recurrency",
|
|
1792
2255
|
choices=RECURRENCE_CHOICES,
|
|
1793
2256
|
initial=ClientReportSchedule.PERIODICITY_NONE,
|
|
2257
|
+
help_text="Defines how often the report should be generated automatically.",
|
|
1794
2258
|
)
|
|
1795
2259
|
disable_emails = forms.BooleanField(
|
|
1796
2260
|
label="Disable email delivery",
|
|
@@ -1909,14 +2373,15 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
1909
2373
|
"package_link",
|
|
1910
2374
|
"is_current",
|
|
1911
2375
|
"pypi_url",
|
|
2376
|
+
"release_on",
|
|
1912
2377
|
"revision_short",
|
|
1913
2378
|
"published_status",
|
|
1914
2379
|
)
|
|
1915
2380
|
list_display_links = ("version",)
|
|
1916
2381
|
actions = ["publish_release", "validate_releases"]
|
|
1917
2382
|
change_actions = ["publish_release_action"]
|
|
1918
|
-
changelist_actions = ["refresh_from_pypi"]
|
|
1919
|
-
readonly_fields = ("pypi_url", "is_current", "revision")
|
|
2383
|
+
changelist_actions = ["refresh_from_pypi", "prepare_next_release"]
|
|
2384
|
+
readonly_fields = ("pypi_url", "release_on", "is_current", "revision")
|
|
1920
2385
|
fields = (
|
|
1921
2386
|
"package",
|
|
1922
2387
|
"release_manager",
|
|
@@ -1924,6 +2389,7 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
1924
2389
|
"revision",
|
|
1925
2390
|
"is_current",
|
|
1926
2391
|
"pypi_url",
|
|
2392
|
+
"release_on",
|
|
1927
2393
|
)
|
|
1928
2394
|
|
|
1929
2395
|
@admin.display(description="package", ordering="package")
|
|
@@ -1951,32 +2417,94 @@ class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
1951
2417
|
return
|
|
1952
2418
|
releases = resp.json().get("releases", {})
|
|
1953
2419
|
created = 0
|
|
1954
|
-
|
|
1955
|
-
|
|
2420
|
+
updated = 0
|
|
2421
|
+
restored = 0
|
|
2422
|
+
|
|
2423
|
+
for version, files in releases.items():
|
|
2424
|
+
release_on = self._release_on_from_files(files)
|
|
2425
|
+
release = PackageRelease.all_objects.filter(
|
|
1956
2426
|
package=package, version=version
|
|
1957
|
-
).
|
|
1958
|
-
if
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2427
|
+
).first()
|
|
2428
|
+
if release:
|
|
2429
|
+
update_fields = []
|
|
2430
|
+
if release.is_deleted:
|
|
2431
|
+
release.is_deleted = False
|
|
2432
|
+
update_fields.append("is_deleted")
|
|
2433
|
+
restored += 1
|
|
2434
|
+
if not release.pypi_url:
|
|
2435
|
+
release.pypi_url = (
|
|
2436
|
+
f"https://pypi.org/project/{package.name}/{version}/"
|
|
2437
|
+
)
|
|
2438
|
+
update_fields.append("pypi_url")
|
|
2439
|
+
if release_on and release.release_on != release_on:
|
|
2440
|
+
release.release_on = release_on
|
|
2441
|
+
update_fields.append("release_on")
|
|
2442
|
+
updated += 1
|
|
2443
|
+
if update_fields:
|
|
2444
|
+
release.save(update_fields=update_fields)
|
|
2445
|
+
continue
|
|
2446
|
+
PackageRelease.objects.create(
|
|
2447
|
+
package=package,
|
|
2448
|
+
release_manager=package.release_manager,
|
|
2449
|
+
version=version,
|
|
2450
|
+
revision="",
|
|
2451
|
+
pypi_url=f"https://pypi.org/project/{package.name}/{version}/",
|
|
2452
|
+
release_on=release_on,
|
|
1973
2453
|
)
|
|
2454
|
+
created += 1
|
|
2455
|
+
|
|
2456
|
+
if created or updated or restored:
|
|
2457
|
+
PackageRelease.dump_fixture()
|
|
2458
|
+
message_parts = []
|
|
2459
|
+
if created:
|
|
2460
|
+
message_parts.append(
|
|
2461
|
+
f"Created {created} release{'s' if created != 1 else ''} from PyPI"
|
|
2462
|
+
)
|
|
2463
|
+
if updated:
|
|
2464
|
+
message_parts.append(
|
|
2465
|
+
f"Updated release date for {updated} release"
|
|
2466
|
+
f"{'s' if updated != 1 else ''}"
|
|
2467
|
+
)
|
|
2468
|
+
if restored:
|
|
2469
|
+
message_parts.append(
|
|
2470
|
+
f"Restored {restored} release{'s' if restored != 1 else ''}"
|
|
2471
|
+
)
|
|
2472
|
+
self.message_user(request, "; ".join(message_parts), messages.SUCCESS)
|
|
1974
2473
|
else:
|
|
1975
2474
|
self.message_user(request, "No new releases found", messages.INFO)
|
|
1976
2475
|
|
|
1977
2476
|
refresh_from_pypi.label = "Refresh from PyPI"
|
|
1978
2477
|
refresh_from_pypi.short_description = "Refresh from PyPI"
|
|
1979
2478
|
|
|
2479
|
+
@staticmethod
|
|
2480
|
+
def _release_on_from_files(files):
|
|
2481
|
+
if not files:
|
|
2482
|
+
return None
|
|
2483
|
+
candidates = []
|
|
2484
|
+
for item in files:
|
|
2485
|
+
stamp = item.get("upload_time_iso_8601") or item.get("upload_time")
|
|
2486
|
+
if not stamp:
|
|
2487
|
+
continue
|
|
2488
|
+
when = parse_datetime(stamp)
|
|
2489
|
+
if when is None:
|
|
2490
|
+
continue
|
|
2491
|
+
if timezone.is_naive(when):
|
|
2492
|
+
when = timezone.make_aware(when, datetime.timezone.utc)
|
|
2493
|
+
candidates.append(when.astimezone(datetime.timezone.utc))
|
|
2494
|
+
if not candidates:
|
|
2495
|
+
return None
|
|
2496
|
+
return min(candidates)
|
|
2497
|
+
|
|
2498
|
+
def prepare_next_release(self, request, queryset):
|
|
2499
|
+
package = Package.objects.filter(is_active=True).first()
|
|
2500
|
+
if not package:
|
|
2501
|
+
self.message_user(request, "No active package", messages.ERROR)
|
|
2502
|
+
return redirect("admin:core_packagerelease_changelist")
|
|
2503
|
+
return PackageAdmin._prepare(self, request, package)
|
|
2504
|
+
|
|
2505
|
+
prepare_next_release.label = "Prepare next Release"
|
|
2506
|
+
prepare_next_release.short_description = "Prepare next release"
|
|
2507
|
+
|
|
1980
2508
|
def _publish_release(self, request, release):
|
|
1981
2509
|
try:
|
|
1982
2510
|
release.full_clean()
|