arthexis 0.1.22__py3-none-any.whl → 0.1.24__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.22.dist-info → arthexis-0.1.24.dist-info}/METADATA +6 -5
- {arthexis-0.1.22.dist-info → arthexis-0.1.24.dist-info}/RECORD +26 -26
- config/settings.py +4 -0
- core/admin.py +200 -16
- core/models.py +878 -118
- core/release.py +0 -5
- core/tasks.py +25 -0
- core/tests.py +29 -1
- core/user_data.py +42 -2
- core/views.py +33 -26
- nodes/admin.py +153 -132
- nodes/models.py +9 -1
- nodes/tests.py +106 -81
- nodes/urls.py +6 -0
- nodes/views.py +620 -48
- ocpp/admin.py +543 -166
- ocpp/models.py +57 -2
- ocpp/tasks.py +336 -1
- ocpp/tests.py +123 -0
- ocpp/views.py +19 -3
- pages/tests.py +25 -6
- pages/urls.py +5 -0
- pages/views.py +117 -11
- {arthexis-0.1.22.dist-info → arthexis-0.1.24.dist-info}/WHEEL +0 -0
- {arthexis-0.1.22.dist-info → arthexis-0.1.24.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.22.dist-info → arthexis-0.1.24.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arthexis
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.24
|
|
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
|
|
@@ -15,7 +15,7 @@ Requires-Dist: amqp==5.3.1
|
|
|
15
15
|
Requires-Dist: annotated-types==0.7.0
|
|
16
16
|
Requires-Dist: anyio==4.9.0
|
|
17
17
|
Requires-Dist: asgiref==3.10.0
|
|
18
|
-
Requires-Dist: atproto
|
|
18
|
+
Requires-Dist: atproto<0.1.0,>=0.0.63
|
|
19
19
|
Requires-Dist: attrs==25.3.0
|
|
20
20
|
Requires-Dist: autobahn==24.4.2
|
|
21
21
|
Requires-Dist: Automat==25.4.16
|
|
@@ -70,8 +70,8 @@ Requires-Dist: psycopg-binary==3.2.12
|
|
|
70
70
|
Requires-Dist: pyasn1==0.6.1
|
|
71
71
|
Requires-Dist: pyasn1_modules==0.4.2
|
|
72
72
|
Requires-Dist: pycparser==2.22
|
|
73
|
-
Requires-Dist: pydantic==2.
|
|
74
|
-
Requires-Dist: pydantic_core==2.
|
|
73
|
+
Requires-Dist: pydantic==2.12.3
|
|
74
|
+
Requires-Dist: pydantic_core==2.41.4
|
|
75
75
|
Requires-Dist: pyOpenSSL==25.1.0
|
|
76
76
|
Requires-Dist: pyperclip==1.11.0
|
|
77
77
|
Requires-Dist: PySocks==1.7.1
|
|
@@ -106,7 +106,7 @@ Requires-Dist: vine==5.1.0
|
|
|
106
106
|
Requires-Dist: wcwidth==0.2.14
|
|
107
107
|
Requires-Dist: webencodings==0.5.1
|
|
108
108
|
Requires-Dist: websocket-client==1.8.0
|
|
109
|
-
Requires-Dist: websockets==
|
|
109
|
+
Requires-Dist: websockets==15.0.1
|
|
110
110
|
Requires-Dist: whitenoise==6.11.0
|
|
111
111
|
Requires-Dist: plyer==2.1.0; sys_platform == "win32"
|
|
112
112
|
Requires-Dist: wsproto==1.2.0
|
|
@@ -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.
|
|
1
|
+
arthexis-0.1.24.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
|
|
@@ -10,12 +10,12 @@ config/loadenv.py,sha256=CjXx-wBaTt1wixub4GJ5CMSMFqtiK5JURc7cPXpqO7s,287
|
|
|
10
10
|
config/logging.py,sha256=1cIbPgRshHuMKnVEEH0jKpRAlJSpewvLFbYDz7sCBG4,2104
|
|
11
11
|
config/middleware.py,sha256=zF8Cma0n5G8NNdh2LVeNJi7Hgl1G4mF9msRE2eRi1RU,2328
|
|
12
12
|
config/offline.py,sha256=X-yDcyoI4C44Y27lpkUwszY_09GwwFfazEsthKJpQ70,1382
|
|
13
|
-
config/settings.py,sha256=
|
|
13
|
+
config/settings.py,sha256=tFf0C4PO1whB4a_U7KcVlYnp2_gNC16t-cce3nNoStc,20914
|
|
14
14
|
config/settings_helpers.py,sha256=0BdBciUHIkwsWa0vV_RKAd4wDuEzgE7G-42XYiES4YQ,3127
|
|
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=
|
|
18
|
+
core/admin.py,sha256=N_x_4t7lSBIg1f13lN5qW54l29giXcGAy3_GWRVn4C0,153652
|
|
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,60 +34,60 @@ 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=
|
|
37
|
+
core/models.py,sha256=jwr_LL0CmtLLybiFjeRFMPY1aNRLaE_IOPvQuihwnJ0,171669
|
|
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=
|
|
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
|
|
45
45
|
core/sigil_resolver.py,sha256=rCsypuX-0oWNfKyM1T9ZLWHY0Ezwhtk4VmI0L3krnsE,11098
|
|
46
46
|
core/system.py,sha256=6ndxYDPswKkC3ySTwbgXzH0CdQYCZJytfA-99smyv_Q,42249
|
|
47
|
-
core/tasks.py,sha256=
|
|
47
|
+
core/tasks.py,sha256=ptO44VTBAoTwf7Y3pI6TnniIs4lTUgN4MKCgNAUjhm4,13135
|
|
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=
|
|
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=
|
|
54
|
-
core/views.py,sha256=
|
|
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=
|
|
57
|
+
nodes/admin.py,sha256=oOPXFsqQXGUKpgH0B9SIdcPJziBqZRrgFEwo-yEH-O4,72895
|
|
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=
|
|
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=
|
|
69
|
-
nodes/urls.py,sha256
|
|
68
|
+
nodes/tests.py,sha256=IikaUCBOUo7r0VHJmqcnk-CsXbbBDT8687p0BmVVJOM,186523
|
|
69
|
+
nodes/urls.py,sha256=c1C-4rROmp51HbVf3KTERuFYvTRXwD5LlApoX4SIwBg,1135
|
|
70
70
|
nodes/utils.py,sha256=wt7UuSXGuq79A-g-B6EW3kK49QWJBb7zhhkw4pun4k8,4474
|
|
71
|
-
nodes/views.py,sha256=
|
|
71
|
+
nodes/views.py,sha256=b7g2TD4pHO01SK6e7uDf78zKbnQVVw3v0N7S2VBvdKQ,57780
|
|
72
72
|
ocpp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
73
|
-
ocpp/admin.py,sha256=
|
|
73
|
+
ocpp/admin.py,sha256=tRB7F9a4nQxcM-xV1AAeGRlEKQxm8i235yIq57xDU90,54379
|
|
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=
|
|
78
|
+
ocpp/models.py,sha256=VmkNPEACRks2kbyUa-2qt3xjrtBaipBonlgOk8xFTXg,38481
|
|
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
|
|
82
82
|
ocpp/status_display.py,sha256=YGFosd5HJETA0DcLdsjvx6EfhZSnI8Aa3cMnHG2WsBE,939
|
|
83
83
|
ocpp/store.py,sha256=gLCSaP9KKF7li2ALlE3O3RW5eVJtoe-_YHfKhdf0VOM,18943
|
|
84
|
-
ocpp/tasks.py,sha256=
|
|
84
|
+
ocpp/tasks.py,sha256=Hv_YUzT0dIq8OZE0yHIKzViGU7fEPKknXcXuNkJqC-g,20287
|
|
85
85
|
ocpp/test_export_import.py,sha256=ouQbTCp4mxfqoK6gondlu3PPcyrT9jSbWAX5gqqgaNk,4561
|
|
86
86
|
ocpp/test_rfid.py,sha256=IhFSlvsI8A8D3S32sRE298nYfrmqxbv7GfVErtNU3DQ,39137
|
|
87
|
-
ocpp/tests.py,sha256=
|
|
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=
|
|
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=
|
|
104
|
-
pages/urls.py,sha256=
|
|
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=
|
|
107
|
-
arthexis-0.1.
|
|
108
|
-
arthexis-0.1.
|
|
109
|
-
arthexis-0.1.
|
|
110
|
-
arthexis-0.1.
|
|
106
|
+
pages/views.py,sha256=7-10W-GYDzlef98Warr2JIs3oQkTwKoOMo1aFGglj14,65214
|
|
107
|
+
arthexis-0.1.24.dist-info/METADATA,sha256=9xkmegLGMFbSSMo1b3Fk8lGrksWPvJN57AeSDRWJ7so,11895
|
|
108
|
+
arthexis-0.1.24.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
109
|
+
arthexis-0.1.24.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
|
|
110
|
+
arthexis-0.1.24.dist-info/RECORD,,
|
config/settings.py
CHANGED
|
@@ -666,4 +666,8 @@ CELERY_BEAT_SCHEDULE = {
|
|
|
666
666
|
"task": "ocpp.tasks.schedule_daily_charge_point_configuration_checks",
|
|
667
667
|
"schedule": crontab(minute=0, hour=0),
|
|
668
668
|
},
|
|
669
|
+
"ocpp_remote_sync": {
|
|
670
|
+
"task": "ocpp.tasks.sync_remote_chargers",
|
|
671
|
+
"schedule": crontab(minute="*"),
|
|
672
|
+
},
|
|
669
673
|
}
|
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
|
|
@@ -47,6 +50,7 @@ import uuid
|
|
|
47
50
|
import requests
|
|
48
51
|
import datetime
|
|
49
52
|
from django.db import IntegrityError, transaction
|
|
53
|
+
from django.db.models import Q
|
|
50
54
|
import calendar
|
|
51
55
|
import re
|
|
52
56
|
from django_object_actions import DjangoObjectActions
|
|
@@ -60,7 +64,7 @@ from reportlab.graphics.barcode import qr
|
|
|
60
64
|
from reportlab.graphics.shapes import Drawing
|
|
61
65
|
from reportlab.lib.styles import getSampleStyleSheet
|
|
62
66
|
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
|
|
63
|
-
from ocpp.models import Transaction
|
|
67
|
+
from ocpp.models import Charger, Transaction
|
|
64
68
|
from ocpp.rfid.utils import build_mode_toggle
|
|
65
69
|
from nodes.models import EmailOutbox
|
|
66
70
|
from .github_helper import GitHubRepositoryError, create_repository_for_package
|
|
@@ -606,15 +610,18 @@ class PackageAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|
|
|
606
610
|
change_actions = ["create_repository_action", "prepare_next_release_action"]
|
|
607
611
|
|
|
608
612
|
def _prepare(self, request, package):
|
|
613
|
+
if request.method not in {"POST", "GET"}:
|
|
614
|
+
return HttpResponseNotAllowed(["GET", "POST"])
|
|
609
615
|
from pathlib import Path
|
|
610
616
|
from packaging.version import Version
|
|
611
617
|
|
|
612
618
|
ver_file = Path("VERSION")
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
619
|
+
if ver_file.exists():
|
|
620
|
+
raw_version = ver_file.read_text().strip()
|
|
621
|
+
cleaned_version = raw_version.rstrip("+") or "0.0.0"
|
|
622
|
+
repo_version = Version(cleaned_version)
|
|
623
|
+
else:
|
|
624
|
+
repo_version = Version("0.0.0")
|
|
618
625
|
|
|
619
626
|
pypi_latest = Version("0.0.0")
|
|
620
627
|
try:
|
|
@@ -3609,13 +3616,62 @@ class RFIDAdmin(EntityModelAdmin, ImportExportModelAdmin):
|
|
|
3609
3616
|
return JsonResponse(result, status=status)
|
|
3610
3617
|
|
|
3611
3618
|
|
|
3619
|
+
class ClientReportRecurrencyFilter(admin.SimpleListFilter):
|
|
3620
|
+
title = "Recurrency"
|
|
3621
|
+
parameter_name = "recurrency"
|
|
3622
|
+
|
|
3623
|
+
def lookups(self, request, model_admin):
|
|
3624
|
+
for value, label in ClientReportSchedule.PERIODICITY_CHOICES:
|
|
3625
|
+
yield (value, label)
|
|
3626
|
+
|
|
3627
|
+
def queryset(self, request, queryset):
|
|
3628
|
+
value = self.value()
|
|
3629
|
+
if not value:
|
|
3630
|
+
return queryset
|
|
3631
|
+
if value == ClientReportSchedule.PERIODICITY_NONE:
|
|
3632
|
+
return queryset.filter(
|
|
3633
|
+
Q(schedule__isnull=True) | Q(schedule__periodicity=value)
|
|
3634
|
+
)
|
|
3635
|
+
return queryset.filter(schedule__periodicity=value)
|
|
3636
|
+
|
|
3637
|
+
|
|
3612
3638
|
@admin.register(ClientReport)
|
|
3613
3639
|
class ClientReportAdmin(EntityModelAdmin):
|
|
3614
|
-
list_display = (
|
|
3640
|
+
list_display = (
|
|
3641
|
+
"created_on",
|
|
3642
|
+
"period_range",
|
|
3643
|
+
"owner",
|
|
3644
|
+
"recurrency_display",
|
|
3645
|
+
"total_kw_period_display",
|
|
3646
|
+
"download_link",
|
|
3647
|
+
)
|
|
3648
|
+
list_select_related = ("schedule", "owner")
|
|
3649
|
+
list_filter = ("owner", ClientReportRecurrencyFilter)
|
|
3615
3650
|
readonly_fields = ("created_on", "data")
|
|
3616
3651
|
|
|
3617
3652
|
change_list_template = "admin/core/clientreport/change_list.html"
|
|
3618
3653
|
|
|
3654
|
+
def period_range(self, obj):
|
|
3655
|
+
return str(obj)
|
|
3656
|
+
|
|
3657
|
+
period_range.short_description = "Period"
|
|
3658
|
+
|
|
3659
|
+
def recurrency_display(self, obj):
|
|
3660
|
+
return obj.periodicity_label
|
|
3661
|
+
|
|
3662
|
+
recurrency_display.short_description = "Recurrency"
|
|
3663
|
+
|
|
3664
|
+
def total_kw_period_display(self, obj):
|
|
3665
|
+
return f"{obj.total_kw_period:.2f}"
|
|
3666
|
+
|
|
3667
|
+
total_kw_period_display.short_description = "Total kW (period)"
|
|
3668
|
+
|
|
3669
|
+
def download_link(self, obj):
|
|
3670
|
+
url = reverse("admin:core_clientreport_download", args=[obj.pk])
|
|
3671
|
+
return format_html('<a href="{}">Download</a>', url)
|
|
3672
|
+
|
|
3673
|
+
download_link.short_description = "Download"
|
|
3674
|
+
|
|
3619
3675
|
class ClientReportForm(forms.Form):
|
|
3620
3676
|
PERIOD_CHOICES = [
|
|
3621
3677
|
("range", "Date range"),
|
|
@@ -3651,8 +3707,28 @@ class ClientReportAdmin(EntityModelAdmin):
|
|
|
3651
3707
|
label="Month",
|
|
3652
3708
|
required=False,
|
|
3653
3709
|
widget=forms.DateInput(attrs={"type": "month"}),
|
|
3710
|
+
input_formats=["%Y-%m"],
|
|
3654
3711
|
help_text="Generates the report for the calendar month that you select.",
|
|
3655
3712
|
)
|
|
3713
|
+
language = forms.ChoiceField(
|
|
3714
|
+
label="Report language",
|
|
3715
|
+
choices=settings.LANGUAGES,
|
|
3716
|
+
help_text="Choose the language used for the generated report.",
|
|
3717
|
+
)
|
|
3718
|
+
title = forms.CharField(
|
|
3719
|
+
label="Report title",
|
|
3720
|
+
required=False,
|
|
3721
|
+
max_length=200,
|
|
3722
|
+
help_text="Optional heading that replaces the default report title.",
|
|
3723
|
+
)
|
|
3724
|
+
chargers = forms.ModelMultipleChoiceField(
|
|
3725
|
+
label="Charge points",
|
|
3726
|
+
queryset=Charger.objects.filter(connector_id__isnull=True)
|
|
3727
|
+
.order_by("display_name", "charger_id"),
|
|
3728
|
+
required=False,
|
|
3729
|
+
widget=forms.CheckboxSelectMultiple,
|
|
3730
|
+
help_text="Choose which charge points are included in the report.",
|
|
3731
|
+
)
|
|
3656
3732
|
owner = forms.ModelChoiceField(
|
|
3657
3733
|
queryset=get_user_model().objects.all(),
|
|
3658
3734
|
required=False,
|
|
@@ -3670,10 +3746,10 @@ class ClientReportAdmin(EntityModelAdmin):
|
|
|
3670
3746
|
initial=ClientReportSchedule.PERIODICITY_NONE,
|
|
3671
3747
|
help_text="Defines how often the report should be generated automatically.",
|
|
3672
3748
|
)
|
|
3673
|
-
|
|
3674
|
-
label="
|
|
3749
|
+
enable_emails = forms.BooleanField(
|
|
3750
|
+
label="Enable email delivery",
|
|
3675
3751
|
required=False,
|
|
3676
|
-
help_text="
|
|
3752
|
+
help_text="Send the report via email to the recipients listed above.",
|
|
3677
3753
|
)
|
|
3678
3754
|
|
|
3679
3755
|
def __init__(self, *args, request=None, **kwargs):
|
|
@@ -3685,6 +3761,13 @@ class ClientReportAdmin(EntityModelAdmin):
|
|
|
3685
3761
|
and request.user.is_authenticated
|
|
3686
3762
|
):
|
|
3687
3763
|
self.fields["owner"].initial = request.user.pk
|
|
3764
|
+
self.fields["chargers"].widget.attrs["class"] = "charger-options"
|
|
3765
|
+
language_initial = ClientReport.default_language()
|
|
3766
|
+
if request:
|
|
3767
|
+
language_initial = ClientReport.normalize_language(
|
|
3768
|
+
getattr(request, "LANGUAGE_CODE", language_initial)
|
|
3769
|
+
)
|
|
3770
|
+
self.fields["language"].initial = language_initial
|
|
3688
3771
|
|
|
3689
3772
|
def clean(self):
|
|
3690
3773
|
cleaned = super().clean()
|
|
@@ -3729,6 +3812,10 @@ class ClientReportAdmin(EntityModelAdmin):
|
|
|
3729
3812
|
emails.append(candidate)
|
|
3730
3813
|
return emails
|
|
3731
3814
|
|
|
3815
|
+
def clean_title(self):
|
|
3816
|
+
title = self.cleaned_data.get("title")
|
|
3817
|
+
return ClientReport.normalize_title(title)
|
|
3818
|
+
|
|
3732
3819
|
def get_urls(self):
|
|
3733
3820
|
urls = super().get_urls()
|
|
3734
3821
|
custom = [
|
|
@@ -3737,6 +3824,16 @@ class ClientReportAdmin(EntityModelAdmin):
|
|
|
3737
3824
|
self.admin_site.admin_view(self.generate_view),
|
|
3738
3825
|
name="core_clientreport_generate",
|
|
3739
3826
|
),
|
|
3827
|
+
path(
|
|
3828
|
+
"generate/action/",
|
|
3829
|
+
self.admin_site.admin_view(self.generate_report),
|
|
3830
|
+
name="core_clientreport_generate_report",
|
|
3831
|
+
),
|
|
3832
|
+
path(
|
|
3833
|
+
"download/<int:report_id>/",
|
|
3834
|
+
self.admin_site.admin_view(self.download_view),
|
|
3835
|
+
name="core_clientreport_download",
|
|
3836
|
+
),
|
|
3740
3837
|
]
|
|
3741
3838
|
return custom + urls
|
|
3742
3839
|
|
|
@@ -3744,40 +3841,127 @@ class ClientReportAdmin(EntityModelAdmin):
|
|
|
3744
3841
|
form = self.ClientReportForm(request.POST or None, request=request)
|
|
3745
3842
|
report = None
|
|
3746
3843
|
schedule = None
|
|
3844
|
+
download_url = None
|
|
3747
3845
|
if request.method == "POST" and form.is_valid():
|
|
3748
3846
|
owner = form.cleaned_data.get("owner")
|
|
3749
3847
|
if not owner and request.user.is_authenticated:
|
|
3750
3848
|
owner = request.user
|
|
3849
|
+
enable_emails = form.cleaned_data.get("enable_emails", False)
|
|
3850
|
+
disable_emails = not enable_emails
|
|
3851
|
+
recipients = form.cleaned_data.get("destinations") if enable_emails else []
|
|
3852
|
+
chargers = list(form.cleaned_data.get("chargers") or [])
|
|
3853
|
+
language = form.cleaned_data.get("language")
|
|
3854
|
+
title = form.cleaned_data.get("title")
|
|
3751
3855
|
report = ClientReport.generate(
|
|
3752
3856
|
form.cleaned_data["start"],
|
|
3753
3857
|
form.cleaned_data["end"],
|
|
3754
3858
|
owner=owner,
|
|
3755
|
-
recipients=
|
|
3756
|
-
disable_emails=
|
|
3859
|
+
recipients=recipients,
|
|
3860
|
+
disable_emails=disable_emails,
|
|
3861
|
+
chargers=chargers,
|
|
3862
|
+
language=language,
|
|
3863
|
+
title=title,
|
|
3757
3864
|
)
|
|
3758
3865
|
report.store_local_copy()
|
|
3866
|
+
if chargers:
|
|
3867
|
+
report.chargers.set(chargers)
|
|
3868
|
+
if enable_emails and recipients:
|
|
3869
|
+
delivered = report.send_delivery(
|
|
3870
|
+
to=recipients,
|
|
3871
|
+
cc=[],
|
|
3872
|
+
outbox=ClientReport.resolve_outbox_for_owner(owner),
|
|
3873
|
+
reply_to=ClientReport.resolve_reply_to_for_owner(owner),
|
|
3874
|
+
)
|
|
3875
|
+
if delivered:
|
|
3876
|
+
report.recipients = delivered
|
|
3877
|
+
report.save(update_fields=["recipients"])
|
|
3878
|
+
self.message_user(
|
|
3879
|
+
request,
|
|
3880
|
+
"Consumer report emailed to the selected recipients.",
|
|
3881
|
+
messages.SUCCESS,
|
|
3882
|
+
)
|
|
3759
3883
|
recurrence = form.cleaned_data.get("recurrence")
|
|
3760
3884
|
if recurrence and recurrence != ClientReportSchedule.PERIODICITY_NONE:
|
|
3761
3885
|
schedule = ClientReportSchedule.objects.create(
|
|
3762
3886
|
owner=owner,
|
|
3763
3887
|
created_by=request.user if request.user.is_authenticated else None,
|
|
3764
3888
|
periodicity=recurrence,
|
|
3765
|
-
email_recipients=
|
|
3766
|
-
disable_emails=
|
|
3889
|
+
email_recipients=recipients,
|
|
3890
|
+
disable_emails=disable_emails,
|
|
3891
|
+
language=language,
|
|
3892
|
+
title=title,
|
|
3767
3893
|
)
|
|
3894
|
+
if chargers:
|
|
3895
|
+
schedule.chargers.set(chargers)
|
|
3768
3896
|
report.schedule = schedule
|
|
3769
3897
|
report.save(update_fields=["schedule"])
|
|
3770
3898
|
self.message_user(
|
|
3771
3899
|
request,
|
|
3772
|
-
"
|
|
3900
|
+
"Consumer report schedule created; future reports will be generated automatically.",
|
|
3901
|
+
messages.SUCCESS,
|
|
3902
|
+
)
|
|
3903
|
+
if disable_emails:
|
|
3904
|
+
self.message_user(
|
|
3905
|
+
request,
|
|
3906
|
+
"Consumer report generated. The download will begin automatically.",
|
|
3773
3907
|
messages.SUCCESS,
|
|
3774
3908
|
)
|
|
3909
|
+
redirect_url = f"{reverse('admin:core_clientreport_generate')}?download={report.pk}"
|
|
3910
|
+
return HttpResponseRedirect(redirect_url)
|
|
3911
|
+
download_param = request.GET.get("download")
|
|
3912
|
+
if download_param:
|
|
3913
|
+
try:
|
|
3914
|
+
download_report = ClientReport.objects.get(pk=download_param)
|
|
3915
|
+
except ClientReport.DoesNotExist:
|
|
3916
|
+
pass
|
|
3917
|
+
else:
|
|
3918
|
+
download_url = reverse(
|
|
3919
|
+
"admin:core_clientreport_download", args=[download_report.pk]
|
|
3920
|
+
)
|
|
3775
3921
|
context = self.admin_site.each_context(request)
|
|
3776
|
-
context.update(
|
|
3922
|
+
context.update(
|
|
3923
|
+
{
|
|
3924
|
+
"form": form,
|
|
3925
|
+
"report": report,
|
|
3926
|
+
"schedule": schedule,
|
|
3927
|
+
"download_url": download_url,
|
|
3928
|
+
"opts": self.model._meta,
|
|
3929
|
+
}
|
|
3930
|
+
)
|
|
3777
3931
|
return TemplateResponse(
|
|
3778
3932
|
request, "admin/core/clientreport/generate.html", context
|
|
3779
3933
|
)
|
|
3780
3934
|
|
|
3935
|
+
def get_changelist_actions(self, request):
|
|
3936
|
+
parent = getattr(super(), "get_changelist_actions", None)
|
|
3937
|
+
actions: list[str] = []
|
|
3938
|
+
if callable(parent):
|
|
3939
|
+
parent_actions = parent(request)
|
|
3940
|
+
if parent_actions:
|
|
3941
|
+
actions.extend(parent_actions)
|
|
3942
|
+
if "generate_report" not in actions:
|
|
3943
|
+
actions.append("generate_report")
|
|
3944
|
+
return actions
|
|
3945
|
+
|
|
3946
|
+
def generate_report(self, request):
|
|
3947
|
+
return HttpResponseRedirect(reverse("admin:core_clientreport_generate"))
|
|
3948
|
+
|
|
3949
|
+
generate_report.label = _("Generate report")
|
|
3950
|
+
|
|
3951
|
+
def download_view(self, request, report_id: int):
|
|
3952
|
+
report = get_object_or_404(ClientReport, pk=report_id)
|
|
3953
|
+
pdf_path = report.ensure_pdf()
|
|
3954
|
+
if not pdf_path.exists():
|
|
3955
|
+
raise Http404("Report file unavailable")
|
|
3956
|
+
end_date = report.end_date
|
|
3957
|
+
if hasattr(end_date, "isoformat"):
|
|
3958
|
+
end_date_str = end_date.isoformat()
|
|
3959
|
+
else: # pragma: no cover - fallback for unexpected values
|
|
3960
|
+
end_date_str = str(end_date)
|
|
3961
|
+
filename = f"consumer-report-{end_date_str}.pdf"
|
|
3962
|
+
response = FileResponse(pdf_path.open("rb"), content_type="application/pdf")
|
|
3963
|
+
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
|
3964
|
+
return response
|
|
3781
3965
|
|
|
3782
3966
|
@admin.register(PackageRelease)
|
|
3783
3967
|
class PackageReleaseAdmin(SaveBeforeChangeAction, EntityModelAdmin):
|