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.

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 MODELS")
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 MODELS")
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 MODELS")
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 FileResponse, Http404, HttpResponse, JsonResponse
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
- disable_emails = forms.BooleanField(
1284
- label=_("Disable email delivery"),
1311
+ enable_emails = forms.BooleanField(
1312
+ label=_("Enable email delivery"),
1285
1313
  required=False,
1286
- help_text=_("Generate files without sending emails."),
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 client reports."),
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
- "Client reports can only be generated periodically. Please wait before trying again."
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=form.cleaned_data.get("destinations"),
1394
- disable_emails=form.cleaned_data.get("disable_emails", False),
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=form.cleaned_data.get("destinations", []),
1404
- disable_emails=form.cleaned_data.get("disable_emails", False),
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
- "Client report schedule created; future reports will be generated automatically."
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"):