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
pages/tests.py
CHANGED
|
@@ -109,6 +109,7 @@ from nodes.models import (
|
|
|
109
109
|
NodeRole,
|
|
110
110
|
NodeFeature,
|
|
111
111
|
NodeFeatureAssignment,
|
|
112
|
+
NetMessage,
|
|
112
113
|
)
|
|
113
114
|
from django.contrib.auth.models import AnonymousUser
|
|
114
115
|
|
|
@@ -721,18 +722,36 @@ class AdminDashboardAppListTests(TestCase):
|
|
|
721
722
|
|
|
722
723
|
def test_horologia_hidden_without_celery_feature(self):
|
|
723
724
|
resp = self.client.get(reverse("admin:index"))
|
|
724
|
-
self.assertNotContains(resp, "5. Horologia
|
|
725
|
+
self.assertNotContains(resp, "5. Horologia</a>")
|
|
725
726
|
|
|
726
727
|
def test_horologia_visible_with_celery_feature(self):
|
|
727
728
|
feature = NodeFeature.objects.create(slug="celery-queue", display="Celery Queue")
|
|
728
729
|
NodeFeatureAssignment.objects.create(node=self.node, feature=feature)
|
|
729
730
|
resp = self.client.get(reverse("admin:index"))
|
|
730
|
-
self.assertContains(resp, "5. Horologia
|
|
731
|
+
self.assertContains(resp, "5. Horologia</a>")
|
|
731
732
|
|
|
732
733
|
def test_horologia_visible_with_celery_lock(self):
|
|
733
734
|
self.celery_lock.write_text("")
|
|
734
735
|
resp = self.client.get(reverse("admin:index"))
|
|
735
|
-
self.assertContains(resp, "5. Horologia
|
|
736
|
+
self.assertContains(resp, "5. Horologia</a>")
|
|
737
|
+
|
|
738
|
+
def test_dashboard_shows_last_net_message(self):
|
|
739
|
+
NetMessage.objects.all().delete()
|
|
740
|
+
NetMessage.objects.create(subject="Older", body="First body")
|
|
741
|
+
NetMessage.objects.create(subject="Latest", body="Signal ready")
|
|
742
|
+
|
|
743
|
+
resp = self.client.get(reverse("admin:index"))
|
|
744
|
+
|
|
745
|
+
self.assertContains(resp, gettext("Net message"))
|
|
746
|
+
self.assertContains(resp, "Latest — Signal ready")
|
|
747
|
+
self.assertNotContains(resp, gettext("No net messages available"))
|
|
748
|
+
|
|
749
|
+
def test_dashboard_shows_placeholder_without_net_message(self):
|
|
750
|
+
NetMessage.objects.all().delete()
|
|
751
|
+
|
|
752
|
+
resp = self.client.get(reverse("admin:index"))
|
|
753
|
+
|
|
754
|
+
self.assertContains(resp, gettext("No net messages available"))
|
|
736
755
|
|
|
737
756
|
class AdminSidebarTests(TestCase):
|
|
738
757
|
def setUp(self):
|
|
@@ -2083,7 +2102,7 @@ class ControlNavTests(TestCase):
|
|
|
2083
2102
|
|
|
2084
2103
|
def test_readme_pill_visible(self):
|
|
2085
2104
|
resp = self.client.get(reverse("pages:readme"))
|
|
2086
|
-
self.assertContains(resp, 'href="/read/"')
|
|
2105
|
+
self.assertContains(resp, 'href="/read/docs/cookbooks/install-start-stop-upgrade-uninstall"')
|
|
2087
2106
|
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
|
|
2088
2107
|
|
|
2089
2108
|
def test_cookbook_pill_has_no_dropdown(self):
|
|
@@ -2099,7 +2118,7 @@ class ControlNavTests(TestCase):
|
|
|
2099
2118
|
|
|
2100
2119
|
self.assertContains(
|
|
2101
2120
|
resp,
|
|
2102
|
-
'<a class="nav-link" href="/read/"><span class="badge rounded-pill text-bg-secondary">COOKBOOKS</span></a>',
|
|
2121
|
+
'<a class="nav-link" href="/read/docs/cookbooks/install-start-stop-upgrade-uninstall"><span class="badge rounded-pill text-bg-secondary">COOKBOOKS</span></a>',
|
|
2103
2122
|
html=True,
|
|
2104
2123
|
)
|
|
2105
2124
|
self.assertNotContains(resp, 'dropdown-item" href="/man/"')
|
|
@@ -2205,7 +2224,7 @@ class SatelliteNavTests(TestCase):
|
|
|
2205
2224
|
|
|
2206
2225
|
def test_readme_pill_visible(self):
|
|
2207
2226
|
resp = self.client.get(reverse("pages:readme"))
|
|
2208
|
-
self.assertContains(resp, 'href="/read/"')
|
|
2227
|
+
self.assertContains(resp, 'href="/read/docs/cookbooks/install-start-stop-upgrade-uninstall"')
|
|
2209
2228
|
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
|
|
2210
2229
|
|
|
2211
2230
|
|
pages/urls.py
CHANGED
|
@@ -17,6 +17,11 @@ urlpatterns = [
|
|
|
17
17
|
path("sitemap.xml", views.sitemap, name="pages-sitemap"),
|
|
18
18
|
path("release/", views.release_admin_redirect, name="release-admin"),
|
|
19
19
|
path("client-report/", views.client_report, name="client-report"),
|
|
20
|
+
path(
|
|
21
|
+
"client-report/download/<int:report_id>/",
|
|
22
|
+
views.client_report_download,
|
|
23
|
+
name="client-report-download",
|
|
24
|
+
),
|
|
20
25
|
path("release-checklist", views.release_checklist, name="release-checklist"),
|
|
21
26
|
path("login/", views.login_view, name="login"),
|
|
22
27
|
path("authenticator/setup/", views.authenticator_setup, name="authenticator-setup"),
|
pages/views.py
CHANGED
|
@@ -16,6 +16,7 @@ from django.contrib import admin
|
|
|
16
16
|
from django.contrib import messages
|
|
17
17
|
from django.contrib.admin.views.decorators import staff_member_required
|
|
18
18
|
from django.contrib.auth import get_user_model, login
|
|
19
|
+
from django.contrib.auth.decorators import login_required
|
|
19
20
|
from django.contrib.auth.tokens import default_token_generator
|
|
20
21
|
from django.contrib.auth.views import LoginView
|
|
21
22
|
from django import forms
|
|
@@ -23,7 +24,14 @@ from django.apps import apps as django_apps
|
|
|
23
24
|
from utils.decorators import security_group_required
|
|
24
25
|
from utils.sites import get_site
|
|
25
26
|
from django.contrib.staticfiles import finders
|
|
26
|
-
from django.http import
|
|
27
|
+
from django.http import (
|
|
28
|
+
FileResponse,
|
|
29
|
+
Http404,
|
|
30
|
+
HttpResponse,
|
|
31
|
+
HttpResponseForbidden,
|
|
32
|
+
HttpResponseRedirect,
|
|
33
|
+
JsonResponse,
|
|
34
|
+
)
|
|
27
35
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
28
36
|
from nodes.models import Node
|
|
29
37
|
from nodes.utils import capture_screenshot, save_screenshot
|
|
@@ -58,6 +66,7 @@ from core.models import (
|
|
|
58
66
|
SecurityGroup,
|
|
59
67
|
Todo,
|
|
60
68
|
)
|
|
69
|
+
from ocpp.models import Charger
|
|
61
70
|
|
|
62
71
|
try: # pragma: no cover - optional dependency guard
|
|
63
72
|
from graphviz import Digraph
|
|
@@ -1261,6 +1270,25 @@ class ClientReportForm(forms.Form):
|
|
|
1261
1270
|
input_formats=["%Y-%m"],
|
|
1262
1271
|
help_text=_("Generates the report for the calendar month that you select."),
|
|
1263
1272
|
)
|
|
1273
|
+
language = forms.ChoiceField(
|
|
1274
|
+
label=_("Report language"),
|
|
1275
|
+
choices=settings.LANGUAGES,
|
|
1276
|
+
help_text=_("Choose the language used for the generated report."),
|
|
1277
|
+
)
|
|
1278
|
+
title = forms.CharField(
|
|
1279
|
+
label=_("Report title"),
|
|
1280
|
+
required=False,
|
|
1281
|
+
max_length=200,
|
|
1282
|
+
help_text=_("Optional heading that replaces the default report title."),
|
|
1283
|
+
)
|
|
1284
|
+
chargers = forms.ModelMultipleChoiceField(
|
|
1285
|
+
label=_("Charge points"),
|
|
1286
|
+
queryset=Charger.objects.filter(connector_id__isnull=True)
|
|
1287
|
+
.order_by("display_name", "charger_id"),
|
|
1288
|
+
required=False,
|
|
1289
|
+
widget=forms.CheckboxSelectMultiple,
|
|
1290
|
+
help_text=_("Choose which charge points are included in the report."),
|
|
1291
|
+
)
|
|
1264
1292
|
owner = forms.ModelChoiceField(
|
|
1265
1293
|
queryset=get_user_model().objects.all(),
|
|
1266
1294
|
required=False,
|
|
@@ -1280,10 +1308,10 @@ class ClientReportForm(forms.Form):
|
|
|
1280
1308
|
initial=ClientReportSchedule.PERIODICITY_NONE,
|
|
1281
1309
|
help_text=_("Defines how often the report should be generated automatically."),
|
|
1282
1310
|
)
|
|
1283
|
-
|
|
1284
|
-
label=_("
|
|
1311
|
+
enable_emails = forms.BooleanField(
|
|
1312
|
+
label=_("Enable email delivery"),
|
|
1285
1313
|
required=False,
|
|
1286
|
-
help_text=_("
|
|
1314
|
+
help_text=_("Send the report via email to the recipients listed above."),
|
|
1287
1315
|
)
|
|
1288
1316
|
|
|
1289
1317
|
def __init__(self, *args, request=None, **kwargs):
|
|
@@ -1291,6 +1319,13 @@ class ClientReportForm(forms.Form):
|
|
|
1291
1319
|
super().__init__(*args, **kwargs)
|
|
1292
1320
|
if request and getattr(request, "user", None) and request.user.is_authenticated:
|
|
1293
1321
|
self.fields["owner"].initial = request.user.pk
|
|
1322
|
+
self.fields["chargers"].widget.attrs["class"] = "charger-options"
|
|
1323
|
+
language_initial = ClientReport.default_language()
|
|
1324
|
+
if request:
|
|
1325
|
+
language_initial = ClientReport.normalize_language(
|
|
1326
|
+
getattr(request, "LANGUAGE_CODE", language_initial)
|
|
1327
|
+
)
|
|
1328
|
+
self.fields["language"].initial = language_initial
|
|
1294
1329
|
|
|
1295
1330
|
def clean(self):
|
|
1296
1331
|
cleaned = super().clean()
|
|
@@ -1340,6 +1375,10 @@ class ClientReportForm(forms.Form):
|
|
|
1340
1375
|
emails.append(candidate)
|
|
1341
1376
|
return emails
|
|
1342
1377
|
|
|
1378
|
+
def clean_title(self):
|
|
1379
|
+
title = self.cleaned_data.get("title")
|
|
1380
|
+
return ClientReport.normalize_title(title)
|
|
1381
|
+
|
|
1343
1382
|
|
|
1344
1383
|
@live_update()
|
|
1345
1384
|
def client_report(request):
|
|
@@ -1350,7 +1389,7 @@ def client_report(request):
|
|
|
1350
1389
|
if not request.user.is_authenticated:
|
|
1351
1390
|
form.is_valid() # Run validation to surface field errors alongside auth error.
|
|
1352
1391
|
form.add_error(
|
|
1353
|
-
None, _("You must log in to generate
|
|
1392
|
+
None, _("You must log in to generate consumer reports."),
|
|
1354
1393
|
)
|
|
1355
1394
|
elif form.is_valid():
|
|
1356
1395
|
throttle_seconds = getattr(settings, "CLIENT_REPORT_THROTTLE_SECONDS", 60)
|
|
@@ -1379,38 +1418,90 @@ def client_report(request):
|
|
|
1379
1418
|
form.add_error(
|
|
1380
1419
|
None,
|
|
1381
1420
|
_(
|
|
1382
|
-
"
|
|
1421
|
+
"Consumer reports can only be generated periodically. Please wait before trying again."
|
|
1383
1422
|
),
|
|
1384
1423
|
)
|
|
1385
1424
|
else:
|
|
1386
1425
|
owner = form.cleaned_data.get("owner")
|
|
1387
1426
|
if not owner and request.user.is_authenticated:
|
|
1388
1427
|
owner = request.user
|
|
1428
|
+
enable_emails = form.cleaned_data.get("enable_emails", False)
|
|
1429
|
+
disable_emails = not enable_emails
|
|
1430
|
+
recipients = (
|
|
1431
|
+
form.cleaned_data.get("destinations") if enable_emails else []
|
|
1432
|
+
)
|
|
1433
|
+
chargers = list(form.cleaned_data.get("chargers") or [])
|
|
1434
|
+
language = form.cleaned_data.get("language")
|
|
1435
|
+
title = form.cleaned_data.get("title")
|
|
1389
1436
|
report = ClientReport.generate(
|
|
1390
1437
|
form.cleaned_data["start"],
|
|
1391
1438
|
form.cleaned_data["end"],
|
|
1392
1439
|
owner=owner,
|
|
1393
|
-
recipients=
|
|
1394
|
-
disable_emails=
|
|
1440
|
+
recipients=recipients,
|
|
1441
|
+
disable_emails=disable_emails,
|
|
1442
|
+
chargers=chargers,
|
|
1443
|
+
language=language,
|
|
1444
|
+
title=title,
|
|
1395
1445
|
)
|
|
1396
1446
|
report.store_local_copy()
|
|
1447
|
+
if chargers:
|
|
1448
|
+
report.chargers.set(chargers)
|
|
1449
|
+
if enable_emails and recipients:
|
|
1450
|
+
delivered = report.send_delivery(
|
|
1451
|
+
to=recipients,
|
|
1452
|
+
cc=[],
|
|
1453
|
+
outbox=ClientReport.resolve_outbox_for_owner(owner),
|
|
1454
|
+
reply_to=ClientReport.resolve_reply_to_for_owner(owner),
|
|
1455
|
+
)
|
|
1456
|
+
if delivered:
|
|
1457
|
+
report.recipients = delivered
|
|
1458
|
+
report.save(update_fields=["recipients"])
|
|
1459
|
+
messages.success(
|
|
1460
|
+
request,
|
|
1461
|
+
_("Consumer report emailed to the selected recipients."),
|
|
1462
|
+
)
|
|
1397
1463
|
recurrence = form.cleaned_data.get("recurrence")
|
|
1398
1464
|
if recurrence and recurrence != ClientReportSchedule.PERIODICITY_NONE:
|
|
1399
1465
|
schedule = ClientReportSchedule.objects.create(
|
|
1400
1466
|
owner=owner,
|
|
1401
1467
|
created_by=request.user if request.user.is_authenticated else None,
|
|
1402
1468
|
periodicity=recurrence,
|
|
1403
|
-
email_recipients=
|
|
1404
|
-
disable_emails=
|
|
1469
|
+
email_recipients=recipients,
|
|
1470
|
+
disable_emails=disable_emails,
|
|
1471
|
+
language=language,
|
|
1472
|
+
title=title,
|
|
1405
1473
|
)
|
|
1474
|
+
if chargers:
|
|
1475
|
+
schedule.chargers.set(chargers)
|
|
1406
1476
|
report.schedule = schedule
|
|
1407
1477
|
report.save(update_fields=["schedule"])
|
|
1408
1478
|
messages.success(
|
|
1409
1479
|
request,
|
|
1410
1480
|
_(
|
|
1411
|
-
"
|
|
1481
|
+
"Consumer report schedule created; future reports will be generated automatically."
|
|
1412
1482
|
),
|
|
1413
1483
|
)
|
|
1484
|
+
if disable_emails:
|
|
1485
|
+
messages.success(
|
|
1486
|
+
request,
|
|
1487
|
+
_(
|
|
1488
|
+
"Consumer report generated. The download will begin automatically."
|
|
1489
|
+
),
|
|
1490
|
+
)
|
|
1491
|
+
redirect_url = f"{reverse('pages:client-report')}?download={report.pk}"
|
|
1492
|
+
return HttpResponseRedirect(redirect_url)
|
|
1493
|
+
download_url = None
|
|
1494
|
+
download_param = request.GET.get("download")
|
|
1495
|
+
if download_param and request.user.is_authenticated:
|
|
1496
|
+
try:
|
|
1497
|
+
download_id = int(download_param)
|
|
1498
|
+
except (TypeError, ValueError):
|
|
1499
|
+
download_id = None
|
|
1500
|
+
if download_id:
|
|
1501
|
+
download_url = reverse(
|
|
1502
|
+
"pages:client-report-download", args=[download_id]
|
|
1503
|
+
)
|
|
1504
|
+
|
|
1414
1505
|
try:
|
|
1415
1506
|
login_url = reverse("pages:login")
|
|
1416
1507
|
except NoReverseMatch:
|
|
@@ -1424,10 +1515,25 @@ def client_report(request):
|
|
|
1424
1515
|
"report": report,
|
|
1425
1516
|
"schedule": schedule,
|
|
1426
1517
|
"login_url": login_url,
|
|
1518
|
+
"download_url": download_url,
|
|
1427
1519
|
}
|
|
1428
1520
|
return render(request, "pages/client_report.html", context)
|
|
1429
1521
|
|
|
1430
1522
|
|
|
1523
|
+
@login_required
|
|
1524
|
+
def client_report_download(request, report_id: int):
|
|
1525
|
+
report = get_object_or_404(ClientReport, pk=report_id)
|
|
1526
|
+
if not request.user.is_staff and report.owner_id != request.user.pk:
|
|
1527
|
+
return HttpResponseForbidden(
|
|
1528
|
+
_("You do not have permission to download this report.")
|
|
1529
|
+
)
|
|
1530
|
+
pdf_path = report.ensure_pdf()
|
|
1531
|
+
if not pdf_path.exists():
|
|
1532
|
+
raise Http404(_("Report file unavailable."))
|
|
1533
|
+
filename = f"consumer-report-{report.start_date}-{report.end_date}.pdf"
|
|
1534
|
+
response = FileResponse(pdf_path.open("rb"), content_type="application/pdf")
|
|
1535
|
+
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
|
1536
|
+
return response
|
|
1431
1537
|
def _get_request_language_code(request) -> str:
|
|
1432
1538
|
language_code = ""
|
|
1433
1539
|
if hasattr(request, "session"):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|