arthexis 0.1.22__py3-none-any.whl → 0.1.23__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arthexis
3
- Version: 0.1.22
3
+ Version: 0.1.23
4
4
  Summary: Power & Energy Infrastructure
5
5
  Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
6
6
  License-Expression: GPL-3.0-only
@@ -203,6 +203,7 @@ Terminal nodes can start directly with the scripts below without installing; Con
203
203
  - `--constellation` – enables the multi-user orchestration stack.
204
204
  - Use `./install.sh --help` to list every available flag if you need to customize the node beyond the role defaults.
205
205
  - Upgrade with [`./upgrade.sh`](upgrade.sh).
206
+ - Consult the [Install & Lifecycle Scripts Manual](docs/development/install-lifecycle-scripts-manual.md) for complete flag descriptions and operational notes.
206
207
 
207
208
  - **Windows:**
208
209
  - Run [`install.bat`](install.bat) to install (Terminal role) and [`upgrade.bat`](upgrade.bat) to upgrade.
@@ -1,4 +1,4 @@
1
- arthexis-0.1.22.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
1
+ arthexis-0.1.23.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
2
2
  config/__init__.py,sha256=AwpOX7il-DAOmkdJ5dVfVJ3CWWebn1lHyQNmkw1EkDw,103
3
3
  config/active_app.py,sha256=KJqYh-o91nPQjVXPEdbiJHzsI6cN9IZsBZ9O3iZ6Hyc,373
4
4
  config/asgi.py,sha256=Z2HjWrxOxVU9BXcqS7dMEfOGJC48H-WPwFwokRdermY,774
@@ -15,7 +15,7 @@ config/settings_helpers.py,sha256=0BdBciUHIkwsWa0vV_RKAd4wDuEzgE7G-42XYiES4YQ,31
15
15
  config/urls.py,sha256=979WyL05v7nb-Lz_3Qf6Z2QzGtWpXADbIKZ28Zm12ts,5535
16
16
  config/wsgi.py,sha256=zU_mKlya6hejQ21PxKacTui3dUWd4ca_-YJNSYAoMX0,433
17
17
  core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- core/admin.py,sha256=dzM1-jibV1bzpcXGvydoR_8_WMclKYS4ohLreHgipvU,146524
18
+ core/admin.py,sha256=DhDHDYQM2Za6FbgWoJs31wsVi6zUo0GS0CawOgONOPw,149468
19
19
  core/admin_history.py,sha256=XZ4b0ryufIka-xcwboK3DzmOL-INSx5Y2fJO-aJdV70,1783
20
20
  core/admindocs.py,sha256=ycD0bJ_VE6rTGf9ebXTiKdYkD8Y8hD2oQ4HxxoBURCM,6756
21
21
  core/apps.py,sha256=S6fySxtxUzfvz8FI9dii0KI4wSyLhh5API_oeERLIsc,14084
@@ -34,11 +34,11 @@ core/liveupdate.py,sha256=22m0ueQ10-6b-9pQJHY0_5WRYA98fysXKEXOWzIr550,691
34
34
  core/log_paths.py,sha256=lxvgXPgJtVNZ-kYrqV8VFle4GFQrSxG-yRTglqvclmU,3318
35
35
  core/mailer.py,sha256=JpW0RnD9uZ4O-wvlqeW7CMw95IFeCSkdvbankJDwHq0,2886
36
36
  core/middleware.py,sha256=j19K9SX-Emkv7BDDtAacR9g6RWsxhKHwCc8w23JFvMM,3388
37
- core/models.py,sha256=9cwmW1onK7tAg7NQyhSavm2-eXV4nqDewyJUIHuMjZc,143176
37
+ core/models.py,sha256=A2QOFW6mDp2aDDOkZh9GiAGxGfjzrWD4gdDEaM60fCU,159883
38
38
  core/notifications.py,sha256=jNLSuSCrhb8x5cDu_APeDlkrmbMejufk5eJOhssAC4I,3917
39
39
  core/public_wifi.py,sha256=yydLgxOo9DmJJbM4X_23wGR3gxL3YzHno54v9GssuFA,7213
40
40
  core/reference_utils.py,sha256=tffCoyE1w4_SmYzXVWOsW8aR_ZVVTSPzrGhBq8K2xzA,3631
41
- core/release.py,sha256=y5NRs0XwB7RQVvMEZoNWYjTBxuG68dOMizUXLRx7-x8,31561
41
+ core/release.py,sha256=tEMcM7qubmFGmER3TD_MG5j4RceHcwtBjczArZWXEWE,31357
42
42
  core/rfid_import_export.py,sha256=petyhPvL0WUpehc6uGUDUhjYQ9AVvc6O49zuhDs6YFw,3516
43
43
  core/sigil_builder.py,sha256=nMuhYlw3j3LosrK85Q0pYsMcfGWCmrmdnv8UG7GTq_o,4856
44
44
  core/sigil_context.py,sha256=GCzjfM6fcVvBtSbVNfmE6sx3HU8QnxnXrCIytnNpQzM,439
@@ -47,35 +47,35 @@ core/system.py,sha256=6ndxYDPswKkC3ySTwbgXzH0CdQYCZJytfA-99smyv_Q,42249
47
47
  core/tasks.py,sha256=d0MQP5fmn5pA2VCFGxDMEX0xppDIIh8IAzPfGdnk8J4,12340
48
48
  core/temp_passwords.py,sha256=FieUnIUeQHmA1DoXvfJ5U6-Ayv3oDz-hSln5s_vNbA4,5271
49
49
  core/test_system_info.py,sha256=IMPz21KEs6OC5YbL7YaIBdmJVLjRY6MgPuZpldJB5OI,6935
50
- core/tests.py,sha256=eKKoUkF-zI4pCXpjMCuIKCXnv_KgLO8q3_9W0f2lcws,104199
50
+ core/tests.py,sha256=PuxoarDS4reHNV4EDIyVRW7xIOFxZJYou1K_LI9ZNHY,105265
51
51
  core/tests_liveupdate.py,sha256=IquU8ztk6zbzC1bQu3Nrr3RzGzuujtPwDkANJHbxg98,510
52
52
  core/urls.py,sha256=YPippON1MAP2KeZZ8jHpcLO6mvbnKn1q7fdMv5Vm9dY,425
53
- core/user_data.py,sha256=02CfvxayELWSWZJCxWpv1Yz7EGg08yEu5MM31Khsi0U,21083
54
- core/views.py,sha256=oq3_INVD6VwgeRfZ3-UXV0aCUYKj4Kh7Hjx1B9eHVbA,87819
53
+ core/user_data.py,sha256=4pheHB5RqLJtmWMql30CLaCpuVqSyShXb7Sy-crRk_4,22400
54
+ core/views.py,sha256=QIYcBDjzn3YQzP53ub99wVR79d8SCNXRfSX_ENW3snE,88310
55
55
  core/widgets.py,sha256=vlR9PlFfZGlkHm5X2cqNXuEBZSj8gmWaR6MO1mMy6kg,6904
56
56
  nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
- nodes/admin.py,sha256=GDptKIbz5JTu57UxEFiKIq0aqDBFSXilg_zYxc4Tgew,72336
57
+ nodes/admin.py,sha256=2lkeWBCgKuHQQs0olHeE10MvOqhdS8eO9M82EDWhmFc,73679
58
58
  nodes/apps.py,sha256=oi_M2Ya8CAR8N_MoYU68u7_9u-9SlIMelzLOgYM9tDs,3059
59
59
  nodes/backends.py,sha256=dmmbS0X2YIlCDz2KjoDf_L62dy--nuqZF1rEDoi2JHM,5921
60
60
  nodes/dns.py,sha256=D5smXD7Rkh6E4MdL6TBL2WY8GgJg7Rx9z88LZrcMbTw,7048
61
61
  nodes/feature_checks.py,sha256=27e4PCkZ8BGWnJCOwMcY2Bo9z7LoeZWiTZuISWGnrzk,3996
62
62
  nodes/lcd.py,sha256=iKA8Wmq85KZD52aTzAU8ZmS144_gbdGMOXcE8yuECps,5758
63
- nodes/models.py,sha256=3ym5od7ykGokkxOrvXhU8h439e7EaOGsIbG89x7BYz8,72598
63
+ nodes/models.py,sha256=zIeEfUGduaRJ6CYsIiHlba55wgT6pCtyhiP_2w2ssGE,72890
64
64
  nodes/reports.py,sha256=NRYh3Y0SlZFhx31Zh2K03yO12ZrpxEHEY6T-dODA6WE,12059
65
65
  nodes/rfid_sync.py,sha256=oeblawcp6xeLApdIuhsJS83OAk58Eu7pVVmgpAc0Nt8,6953
66
66
  nodes/signals.py,sha256=PtOKdQfb08mV1LgSZvn7ZAcfOyy2c3Xkq4AOpBQyUdE,622
67
67
  nodes/tasks.py,sha256=7m9pKO-iI6JDdfPQ-GWRGown4mdyKrcroOnhbiWN7dY,5246
68
- nodes/tests.py,sha256=5aP2byahZXzKtUY87XtNc9Zv0P9ex2WUrkpNPK0BtJI,186035
68
+ nodes/tests.py,sha256=22fCi_o6lDf-hZmAgQuCNa8i2u4kjZUvN90unCbKi_E,188926
69
69
  nodes/urls.py,sha256=-o9_pLo6XHerKMQwL0TW80wm6wmtVZqyNWcUhpdq9vk,915
70
70
  nodes/utils.py,sha256=wt7UuSXGuq79A-g-B6EW3kK49QWJBb7zhhkw4pun4k8,4474
71
- nodes/views.py,sha256=EByqVc8Nj1nGdLgu-2a8AcxcSqbUtXyuLCF_tRSDTSk,38564
71
+ nodes/views.py,sha256=Z-bFzCLaP4w1l2NzqknmwsBMwuLSWRtzsbroNfA2vJU,39794
72
72
  ocpp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
73
- ocpp/admin.py,sha256=FWYLzZ51-ArcIOZlvI8vwk0ohdPVTAXwurlCOhRhWnc,40502
73
+ ocpp/admin.py,sha256=O5dqd_bwK8k612ltr4Tt9xR3rs3VxxJXCTxarx0mW0w,40970
74
74
  ocpp/apps.py,sha256=i3NqrmIamNEQBT33CIqh7HOSOPmJXCMKrZ-DUd3whqg,842
75
75
  ocpp/consumers.py,sha256=7PYlOkSlHSlIz2FUcBjui4uLFEIHOBWIHfnYpvITrMY,71719
76
76
  ocpp/evcs.py,sha256=q1mZrCVSZxXTrtYsDqH6lkeEcJ6tfSC7p9YxkDmpSCw,28883
77
77
  ocpp/evcs_discovery.py,sha256=OmrzgaOHwveDRJs8AIhrM3apX8_k2PPXh_oYaYpNW3c,3876
78
- ocpp/models.py,sha256=XMTKNu8K03_GId5Z9ucHUxMKlIw8crFnn4aTUHZYW8s,36763
78
+ ocpp/models.py,sha256=1m3kVeuyGoJXLeaR__t_DgcVhAK3MHbCdnv3hF0WZMw,37587
79
79
  ocpp/reference_utils.py,sha256=_UR82GfE93kv4766mHyVIfdhhyYvrT59660r3H6W55M,1072
80
80
  ocpp/routing.py,sha256=3kQya-MdJ00778xDmX0esQLBP05P200V45asg-CGNoo,438
81
81
  ocpp/simulator.py,sha256=vnyd59QffT79AaPhmfM_jipni_nqfG57X5tXyx1rBoc,28016
@@ -84,10 +84,10 @@ ocpp/store.py,sha256=gLCSaP9KKF7li2ALlE3O3RW5eVJtoe-_YHfKhdf0VOM,18943
84
84
  ocpp/tasks.py,sha256=VSna6Mi1jp3zjYTo4y0c5N-n-4efIq5FRKu3z2y8Oio,8950
85
85
  ocpp/test_export_import.py,sha256=ouQbTCp4mxfqoK6gondlu3PPcyrT9jSbWAX5gqqgaNk,4561
86
86
  ocpp/test_rfid.py,sha256=IhFSlvsI8A8D3S32sRE298nYfrmqxbv7GfVErtNU3DQ,39137
87
- ocpp/tests.py,sha256=5PWaNY3gUPxQmkoEkkjU5AXlryzyeWkDL6MUfsydEyg,203144
87
+ ocpp/tests.py,sha256=0AsSR7UKZQUhWFPEwCE4FJHH1Ykl2UpooSGlBIPJOeU,207815
88
88
  ocpp/transactions_io.py,sha256=p2aUsKlCDYnZ4ZBrOM7pxXoW_w3Tbm-tvRFSjnR3x24,7738
89
89
  ocpp/urls.py,sha256=5ZomUtznJe3kfs8E-DtVp12eFva5jUuJdpTEczIsQ5w,1730
90
- ocpp/views.py,sha256=AsdHwLe_6gdLy6DQcwH5ioSbFATqmlpdKkesGoWD7QI,76346
90
+ ocpp/views.py,sha256=iwm2oEMN_d9mmjaD6Hh9axqBIpqw_qSWpMSzyHICrE4,77091
91
91
  pages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
92
92
  pages/admin.py,sha256=VbxkwgjrFx7lXVhwbZuPSvCkS7EC-flBpwOztHejWtE,35834
93
93
  pages/apps.py,sha256=0qcTFKVX9_QgqexJtGeph1sHRqq7khJf4x5ZtkWwblg,1424
@@ -100,11 +100,11 @@ pages/models.py,sha256=9LdIoIK2Epp3YDUk8LUWyhLW5pJ-NiuYTzO_-xKjg0c,23636
100
100
  pages/module_defaults.py,sha256=rCAY8aTyxYNL0M5zDr393rX-Gi-svXqKtuLXm0rILrQ,5444
101
101
  pages/site_config.py,sha256=f1Me0GFdHeGbIeyMlQNzD2e6hym59YHqbz92U_ppffY,4057
102
102
  pages/tasks.py,sha256=ivcba_3wSQ1-cku0oDplzw6vLeQ9hBq3R4TG-LmR5gs,1913
103
- pages/tests.py,sha256=OFyYVFeAfsnHXL27zL3ma821pE4cXGVtOrtIO2YGjsk,153460
104
- pages/urls.py,sha256=2PVF8--cKi3h230CZl7VQsoVaQrxvWg3GPAKiQowQB0,1354
103
+ pages/tests.py,sha256=akLS7p62PeUCaSXTAIsjAfDyv4KWOMJ2MkAGjPuX7AE,154350
104
+ pages/urls.py,sha256=Oe88tm67iVHRFcGJLSBidZ0rkRQPRZ_vRt6ahxNqPek,1499
105
105
  pages/utils.py,sha256=CR4D1debgJLGgXsw9kap2ggpe7fIpSoWS_ivbgMNp2k,564
106
- pages/views.py,sha256=7F3zvXUMBH320rY-UtnxOiUz8HFWTRrGkxwdjZlOdbg,60863
107
- arthexis-0.1.22.dist-info/METADATA,sha256=72h6hoADl-yJFWfojMeYpb2qo3cW56E9a5UyMdSdxn4,11724
108
- arthexis-0.1.22.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
109
- arthexis-0.1.22.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
110
- arthexis-0.1.22.dist-info/RECORD,,
106
+ pages/views.py,sha256=48I-XlW5Dop5ZEZPpm8iZmkRlGM20XP9-wEC4HtC3pg,63633
107
+ arthexis-0.1.23.dist-info/METADATA,sha256=riViVTMwAbUo-CDK3doWMDCV_wyznoApGZE_AZJxEhc,11886
108
+ arthexis-0.1.23.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
109
+ arthexis-0.1.23.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
110
+ arthexis-0.1.23.dist-info/RECORD,,
core/admin.py CHANGED
@@ -9,10 +9,13 @@ from django.urls import NoReverseMatch, path, reverse
9
9
  from urllib.parse import urlencode, urlparse
10
10
  from django.shortcuts import get_object_or_404, redirect, render
11
11
  from django.http import (
12
+ FileResponse,
13
+ Http404,
12
14
  HttpResponse,
13
15
  JsonResponse,
14
16
  HttpResponseBase,
15
17
  HttpResponseRedirect,
18
+ HttpResponseNotAllowed,
16
19
  )
17
20
  from django.template.response import TemplateResponse
18
21
  from django.conf import settings
@@ -606,15 +609,18 @@ class PackageAdmin(SaveBeforeChangeAction, EntityModelAdmin):
606
609
  change_actions = ["create_repository_action", "prepare_next_release_action"]
607
610
 
608
611
  def _prepare(self, request, package):
612
+ if request.method not in {"POST", "GET"}:
613
+ return HttpResponseNotAllowed(["GET", "POST"])
609
614
  from pathlib import Path
610
615
  from packaging.version import Version
611
616
 
612
617
  ver_file = Path("VERSION")
613
- repo_version = (
614
- Version(ver_file.read_text().strip())
615
- if ver_file.exists()
616
- else Version("0.0.0")
617
- )
618
+ if ver_file.exists():
619
+ raw_version = ver_file.read_text().strip()
620
+ cleaned_version = raw_version.rstrip("+") or "0.0.0"
621
+ repo_version = Version(cleaned_version)
622
+ else:
623
+ repo_version = Version("0.0.0")
618
624
 
619
625
  pypi_latest = Version("0.0.0")
620
626
  try:
@@ -3670,10 +3676,10 @@ class ClientReportAdmin(EntityModelAdmin):
3670
3676
  initial=ClientReportSchedule.PERIODICITY_NONE,
3671
3677
  help_text="Defines how often the report should be generated automatically.",
3672
3678
  )
3673
- disable_emails = forms.BooleanField(
3674
- label="Disable email delivery",
3679
+ enable_emails = forms.BooleanField(
3680
+ label="Enable email delivery",
3675
3681
  required=False,
3676
- help_text="Generate files without sending emails.",
3682
+ help_text="Send the report via email to the recipients listed above.",
3677
3683
  )
3678
3684
 
3679
3685
  def __init__(self, *args, request=None, **kwargs):
@@ -3737,6 +3743,11 @@ class ClientReportAdmin(EntityModelAdmin):
3737
3743
  self.admin_site.admin_view(self.generate_view),
3738
3744
  name="core_clientreport_generate",
3739
3745
  ),
3746
+ path(
3747
+ "download/<int:report_id>/",
3748
+ self.admin_site.admin_view(self.download_view),
3749
+ name="core_clientreport_download",
3750
+ ),
3740
3751
  ]
3741
3752
  return custom + urls
3742
3753
 
@@ -3744,16 +3755,20 @@ class ClientReportAdmin(EntityModelAdmin):
3744
3755
  form = self.ClientReportForm(request.POST or None, request=request)
3745
3756
  report = None
3746
3757
  schedule = None
3758
+ download_url = None
3747
3759
  if request.method == "POST" and form.is_valid():
3748
3760
  owner = form.cleaned_data.get("owner")
3749
3761
  if not owner and request.user.is_authenticated:
3750
3762
  owner = request.user
3763
+ enable_emails = form.cleaned_data.get("enable_emails", False)
3764
+ disable_emails = not enable_emails
3765
+ recipients = form.cleaned_data.get("destinations") if enable_emails else []
3751
3766
  report = ClientReport.generate(
3752
3767
  form.cleaned_data["start"],
3753
3768
  form.cleaned_data["end"],
3754
3769
  owner=owner,
3755
- recipients=form.cleaned_data.get("destinations"),
3756
- disable_emails=form.cleaned_data.get("disable_emails", False),
3770
+ recipients=recipients,
3771
+ disable_emails=disable_emails,
3757
3772
  )
3758
3773
  report.store_local_copy()
3759
3774
  recurrence = form.cleaned_data.get("recurrence")
@@ -3762,8 +3777,8 @@ class ClientReportAdmin(EntityModelAdmin):
3762
3777
  owner=owner,
3763
3778
  created_by=request.user if request.user.is_authenticated else None,
3764
3779
  periodicity=recurrence,
3765
- email_recipients=form.cleaned_data.get("destinations", []),
3766
- disable_emails=form.cleaned_data.get("disable_emails", False),
3780
+ email_recipients=recipients,
3781
+ disable_emails=disable_emails,
3767
3782
  )
3768
3783
  report.schedule = schedule
3769
3784
  report.save(update_fields=["schedule"])
@@ -3772,12 +3787,69 @@ class ClientReportAdmin(EntityModelAdmin):
3772
3787
  "Client report schedule created; future reports will be generated automatically.",
3773
3788
  messages.SUCCESS,
3774
3789
  )
3790
+ if disable_emails:
3791
+ self.message_user(
3792
+ request,
3793
+ "Consumer report generated. The download will begin automatically.",
3794
+ messages.SUCCESS,
3795
+ )
3796
+ redirect_url = f"{reverse('admin:core_clientreport_generate')}?download={report.pk}"
3797
+ return HttpResponseRedirect(redirect_url)
3798
+ download_param = request.GET.get("download")
3799
+ if download_param:
3800
+ try:
3801
+ download_report = ClientReport.objects.get(pk=download_param)
3802
+ except ClientReport.DoesNotExist:
3803
+ pass
3804
+ else:
3805
+ download_url = reverse(
3806
+ "admin:core_clientreport_download", args=[download_report.pk]
3807
+ )
3775
3808
  context = self.admin_site.each_context(request)
3776
- context.update({"form": form, "report": report, "schedule": schedule})
3809
+ context.update(
3810
+ {
3811
+ "form": form,
3812
+ "report": report,
3813
+ "schedule": schedule,
3814
+ "download_url": download_url,
3815
+ "previous_reports": self._build_report_history(request),
3816
+ }
3817
+ )
3777
3818
  return TemplateResponse(
3778
3819
  request, "admin/core/clientreport/generate.html", context
3779
3820
  )
3780
3821
 
3822
+ def download_view(self, request, report_id: int):
3823
+ report = get_object_or_404(ClientReport, pk=report_id)
3824
+ pdf_path = report.ensure_pdf()
3825
+ if not pdf_path.exists():
3826
+ raise Http404("Report file unavailable")
3827
+ filename = f"consumer-report-{report.start_date}-{report.end_date}.pdf"
3828
+ response = FileResponse(pdf_path.open("rb"), content_type="application/pdf")
3829
+ response["Content-Disposition"] = f'attachment; filename="{filename}"'
3830
+ return response
3831
+
3832
+ def _build_report_history(self, request):
3833
+ queryset = ClientReport.objects.order_by("-created_on")[:20]
3834
+ history = []
3835
+ for item in queryset:
3836
+ totals = item.rows_for_display.get("totals", {})
3837
+ history.append(
3838
+ {
3839
+ "instance": item,
3840
+ "download_url": reverse(
3841
+ "admin:core_clientreport_download", args=[item.pk]
3842
+ ),
3843
+ "email_enabled": not item.disable_emails,
3844
+ "recipients": item.recipients or [],
3845
+ "totals": {
3846
+ "total_kw": totals.get("total_kw", 0.0),
3847
+ "total_kw_period": totals.get("total_kw_period", 0.0),
3848
+ },
3849
+ }
3850
+ )
3851
+ return history
3852
+
3781
3853
 
3782
3854
  @admin.register(PackageRelease)
3783
3855
  class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):