arthexis 0.1.12__py3-none-any.whl → 0.1.13__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.12.dist-info → arthexis-0.1.13.dist-info}/METADATA +2 -2
- {arthexis-0.1.12.dist-info → arthexis-0.1.13.dist-info}/RECORD +37 -34
- config/asgi.py +15 -1
- config/celery.py +8 -1
- config/settings.py +42 -76
- config/settings_helpers.py +109 -0
- core/admin.py +47 -10
- core/auto_upgrade.py +2 -2
- core/form_fields.py +75 -0
- core/models.py +182 -59
- core/release.py +38 -20
- core/tests.py +11 -1
- core/views.py +47 -12
- core/widgets.py +43 -0
- nodes/admin.py +277 -14
- nodes/apps.py +15 -0
- nodes/models.py +224 -43
- nodes/tests.py +629 -10
- nodes/urls.py +1 -0
- nodes/views.py +173 -5
- ocpp/admin.py +146 -2
- ocpp/consumers.py +125 -8
- ocpp/evcs.py +7 -94
- ocpp/models.py +2 -0
- ocpp/routing.py +4 -2
- ocpp/simulator.py +29 -8
- ocpp/status_display.py +26 -0
- ocpp/tests.py +625 -16
- ocpp/transactions_io.py +10 -0
- ocpp/views.py +122 -22
- pages/admin.py +3 -0
- pages/forms.py +30 -1
- pages/tests.py +118 -1
- pages/views.py +12 -4
- {arthexis-0.1.12.dist-info → arthexis-0.1.13.dist-info}/WHEEL +0 -0
- {arthexis-0.1.12.dist-info → arthexis-0.1.13.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.12.dist-info → arthexis-0.1.13.dist-info}/top_level.txt +0 -0
ocpp/transactions_io.py
CHANGED
|
@@ -61,6 +61,12 @@ def export_transactions(
|
|
|
61
61
|
"soc_stop": tx.soc_stop,
|
|
62
62
|
"start_time": tx.start_time.isoformat(),
|
|
63
63
|
"stop_time": tx.stop_time.isoformat() if tx.stop_time else None,
|
|
64
|
+
"received_start_time": tx.received_start_time.isoformat()
|
|
65
|
+
if tx.received_start_time
|
|
66
|
+
else None,
|
|
67
|
+
"received_stop_time": tx.received_stop_time.isoformat()
|
|
68
|
+
if tx.received_stop_time
|
|
69
|
+
else None,
|
|
64
70
|
"meter_values": [
|
|
65
71
|
{
|
|
66
72
|
"connector_id": mv.connector_id,
|
|
@@ -157,6 +163,10 @@ def import_transactions(data: dict) -> int:
|
|
|
157
163
|
soc_stop=tx.get("soc_stop"),
|
|
158
164
|
start_time=_parse_dt(tx.get("start_time")),
|
|
159
165
|
stop_time=_parse_dt(tx.get("stop_time")),
|
|
166
|
+
received_start_time=_parse_dt(tx.get("received_start_time"))
|
|
167
|
+
or _parse_dt(tx.get("start_time")),
|
|
168
|
+
received_stop_time=_parse_dt(tx.get("received_stop_time"))
|
|
169
|
+
or _parse_dt(tx.get("stop_time")),
|
|
160
170
|
)
|
|
161
171
|
for mv in tx.get("meter_values", []):
|
|
162
172
|
connector_id = mv.get("connector_id")
|
ocpp/views.py
CHANGED
|
@@ -26,13 +26,14 @@ from pages.utils import landing
|
|
|
26
26
|
from core.liveupdate import live_update
|
|
27
27
|
|
|
28
28
|
from . import store
|
|
29
|
-
from .models import Transaction, Charger, DataTransferMessage
|
|
29
|
+
from .models import Transaction, Charger, DataTransferMessage, RFID
|
|
30
30
|
from .evcs import (
|
|
31
31
|
_start_simulator,
|
|
32
32
|
_stop_simulator,
|
|
33
33
|
get_simulator_state,
|
|
34
34
|
_simulator_status_json,
|
|
35
35
|
)
|
|
36
|
+
from .status_display import STATUS_BADGE_MAP, ERROR_OK_VALUES
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
def _normalize_connector_slug(slug: str | None) -> tuple[int | None, str]:
|
|
@@ -99,7 +100,41 @@ def _ensure_charger_access(user, charger: Charger):
|
|
|
99
100
|
raise Http404("Charger not found")
|
|
100
101
|
|
|
101
102
|
|
|
102
|
-
def
|
|
103
|
+
def _transaction_rfid_details(
|
|
104
|
+
tx_obj, *, cache: dict[str, dict[str, str | None]] | None = None
|
|
105
|
+
) -> dict[str, str | None] | None:
|
|
106
|
+
"""Return normalized RFID metadata for a transaction-like object."""
|
|
107
|
+
|
|
108
|
+
if not tx_obj:
|
|
109
|
+
return None
|
|
110
|
+
rfid_value = getattr(tx_obj, "rfid", None)
|
|
111
|
+
if not rfid_value:
|
|
112
|
+
return None
|
|
113
|
+
normalized = str(rfid_value).strip()
|
|
114
|
+
if not normalized:
|
|
115
|
+
return None
|
|
116
|
+
normalized = normalized.upper()
|
|
117
|
+
if cache is not None and normalized in cache:
|
|
118
|
+
return cache[normalized]
|
|
119
|
+
tag = RFID.objects.filter(rfid=normalized).only("pk").first()
|
|
120
|
+
rfid_url = None
|
|
121
|
+
if tag:
|
|
122
|
+
try:
|
|
123
|
+
rfid_url = reverse("admin:core_rfid_change", args=[tag.pk])
|
|
124
|
+
except NoReverseMatch: # pragma: no cover - admin may be disabled
|
|
125
|
+
rfid_url = None
|
|
126
|
+
details = {"value": normalized, "url": rfid_url}
|
|
127
|
+
if cache is not None:
|
|
128
|
+
cache[normalized] = details
|
|
129
|
+
return details
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _connector_overview(
|
|
133
|
+
charger: Charger,
|
|
134
|
+
user=None,
|
|
135
|
+
*,
|
|
136
|
+
rfid_cache: dict[str, dict[str, str | None]] | None = None,
|
|
137
|
+
) -> list[dict]:
|
|
103
138
|
"""Return connector metadata used for navigation and summaries."""
|
|
104
139
|
|
|
105
140
|
overview: list[dict] = []
|
|
@@ -123,6 +158,9 @@ def _connector_overview(charger: Charger, user=None) -> list[dict]:
|
|
|
123
158
|
"last_status_timestamp": sibling.last_status_timestamp,
|
|
124
159
|
"last_status_vendor_info": sibling.last_status_vendor_info,
|
|
125
160
|
"tx": tx_obj,
|
|
161
|
+
"rfid_details": _transaction_rfid_details(
|
|
162
|
+
tx_obj, cache=rfid_cache
|
|
163
|
+
),
|
|
126
164
|
"connected": store.is_connected(
|
|
127
165
|
sibling.charger_id, sibling.connector_id
|
|
128
166
|
),
|
|
@@ -166,6 +204,7 @@ def _landing_page_translations() -> dict[str, dict[str, str]]:
|
|
|
166
204
|
"charging_label": gettext("Charging"),
|
|
167
205
|
"energy_label": gettext("Energy"),
|
|
168
206
|
"started_label": gettext("Started"),
|
|
207
|
+
"rfid_label": gettext("RFID"),
|
|
169
208
|
"instruction_text": gettext(
|
|
170
209
|
"Plug in your vehicle and slide your RFID card over the reader to begin charging."
|
|
171
210
|
),
|
|
@@ -190,32 +229,26 @@ def _landing_page_translations() -> dict[str, dict[str, str]]:
|
|
|
190
229
|
return catalog
|
|
191
230
|
|
|
192
231
|
|
|
193
|
-
STATUS_BADGE_MAP: dict[str, tuple[str, str]] = {
|
|
194
|
-
"available": (_("Available"), "#0d6efd"),
|
|
195
|
-
"preparing": (_("Preparing"), "#0d6efd"),
|
|
196
|
-
"charging": (_("Charging"), "#198754"),
|
|
197
|
-
"suspendedevse": (_("Suspended (EVSE)"), "#fd7e14"),
|
|
198
|
-
"suspendedev": (_("Suspended (EV)"), "#fd7e14"),
|
|
199
|
-
"finishing": (_("Finishing"), "#20c997"),
|
|
200
|
-
"faulted": (_("Faulted"), "#dc3545"),
|
|
201
|
-
"unavailable": (_("Unavailable"), "#6c757d"),
|
|
202
|
-
"reserved": (_("Reserved"), "#6f42c1"),
|
|
203
|
-
"occupied": (_("Occupied"), "#0dcaf0"),
|
|
204
|
-
"outofservice": (_("Out of Service"), "#6c757d"),
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
_ERROR_OK_VALUES = {"", "noerror", "no_error"}
|
|
208
|
-
|
|
209
|
-
|
|
210
232
|
def _charger_state(charger: Charger, tx_obj: Transaction | list | None):
|
|
211
233
|
"""Return human readable state and color for a charger."""
|
|
212
234
|
|
|
213
235
|
status_value = (charger.last_status or "").strip()
|
|
236
|
+
has_session = bool(tx_obj)
|
|
214
237
|
if status_value:
|
|
215
238
|
key = status_value.lower()
|
|
216
239
|
label, color = STATUS_BADGE_MAP.get(key, (status_value, "#0d6efd"))
|
|
217
240
|
error_code = (charger.last_error_code or "").strip()
|
|
218
|
-
|
|
241
|
+
error_code_lower = error_code.lower()
|
|
242
|
+
if (
|
|
243
|
+
has_session
|
|
244
|
+
and error_code_lower in ERROR_OK_VALUES
|
|
245
|
+
and (key not in STATUS_BADGE_MAP or key == "available")
|
|
246
|
+
):
|
|
247
|
+
# Some stations continue reporting "Available" (or an unknown status)
|
|
248
|
+
# while a session is active. Override the badge so the user can see
|
|
249
|
+
# the charger is actually busy.
|
|
250
|
+
label, color = STATUS_BADGE_MAP.get("charging", (_("Charging"), "#198754"))
|
|
251
|
+
elif error_code and error_code_lower not in ERROR_OK_VALUES:
|
|
219
252
|
label = _("%(status)s (%(error)s)") % {
|
|
220
253
|
"status": label,
|
|
221
254
|
"error": error_code,
|
|
@@ -225,7 +258,6 @@ def _charger_state(charger: Charger, tx_obj: Transaction | list | None):
|
|
|
225
258
|
|
|
226
259
|
cid = charger.charger_id
|
|
227
260
|
connected = store.is_connected(cid, charger.connector_id)
|
|
228
|
-
has_session = bool(tx_obj)
|
|
229
261
|
if connected and has_session:
|
|
230
262
|
return _("Charging"), "green"
|
|
231
263
|
if connected:
|
|
@@ -575,7 +607,10 @@ def charger_page(request, cid, connector=None):
|
|
|
575
607
|
"""Public landing page for a charger displaying usage guidance or progress."""
|
|
576
608
|
charger, connector_slug = _get_charger(cid, connector)
|
|
577
609
|
_ensure_charger_access(request.user, charger)
|
|
578
|
-
|
|
610
|
+
rfid_cache: dict[str, dict[str, str | None]] = {}
|
|
611
|
+
overview = _connector_overview(
|
|
612
|
+
charger, request.user, rfid_cache=rfid_cache
|
|
613
|
+
)
|
|
579
614
|
sessions = _live_sessions(charger)
|
|
580
615
|
tx = None
|
|
581
616
|
active_connector_count = 0
|
|
@@ -621,12 +656,14 @@ def charger_page(request, cid, connector=None):
|
|
|
621
656
|
item for item in overview if item["charger"].connector_id is not None
|
|
622
657
|
]
|
|
623
658
|
status_url = _reverse_connector_url("charger-status", cid, connector_slug)
|
|
659
|
+
tx_rfid_details = _transaction_rfid_details(tx, cache=rfid_cache)
|
|
624
660
|
return render(
|
|
625
661
|
request,
|
|
626
662
|
"ocpp/charger_page.html",
|
|
627
663
|
{
|
|
628
664
|
"charger": charger,
|
|
629
665
|
"tx": tx,
|
|
666
|
+
"tx_rfid_details": tx_rfid_details,
|
|
630
667
|
"connector_slug": connector_slug,
|
|
631
668
|
"connector_links": connector_links,
|
|
632
669
|
"connector_overview": connector_overview,
|
|
@@ -680,7 +717,39 @@ def charger_status(request, cid, connector=None):
|
|
|
680
717
|
paginator = Paginator(transactions_qs, 10)
|
|
681
718
|
page_obj = paginator.get_page(request.GET.get("page"))
|
|
682
719
|
transactions = page_obj.object_list
|
|
720
|
+
date_view = request.GET.get("dates", "charger").lower()
|
|
721
|
+
if date_view not in {"charger", "received"}:
|
|
722
|
+
date_view = "charger"
|
|
723
|
+
|
|
724
|
+
def _date_query(mode: str) -> str:
|
|
725
|
+
params = request.GET.copy()
|
|
726
|
+
params["dates"] = mode
|
|
727
|
+
query = params.urlencode()
|
|
728
|
+
return f"?{query}" if query else ""
|
|
729
|
+
|
|
730
|
+
date_view_options = {
|
|
731
|
+
"charger": _("Charger timestamps"),
|
|
732
|
+
"received": _("Received timestamps"),
|
|
733
|
+
}
|
|
734
|
+
date_toggle_links = [
|
|
735
|
+
{
|
|
736
|
+
"mode": mode,
|
|
737
|
+
"label": label,
|
|
738
|
+
"url": _date_query(mode),
|
|
739
|
+
"active": mode == date_view,
|
|
740
|
+
}
|
|
741
|
+
for mode, label in date_view_options.items()
|
|
742
|
+
]
|
|
683
743
|
chart_data = {"labels": [], "datasets": []}
|
|
744
|
+
pagination_params = request.GET.copy()
|
|
745
|
+
pagination_params["dates"] = date_view
|
|
746
|
+
pagination_params.pop("page", None)
|
|
747
|
+
pagination_query = pagination_params.urlencode()
|
|
748
|
+
session_params = request.GET.copy()
|
|
749
|
+
session_params["dates"] = date_view
|
|
750
|
+
session_params.pop("session", None)
|
|
751
|
+
session_params.pop("page", None)
|
|
752
|
+
session_query = session_params.urlencode()
|
|
684
753
|
|
|
685
754
|
def _series_from_transaction(tx):
|
|
686
755
|
points: list[tuple[str, float]] = []
|
|
@@ -785,6 +854,8 @@ def charger_status(request, cid, connector=None):
|
|
|
785
854
|
"error": str(_("Unable to send remote start request.")),
|
|
786
855
|
}
|
|
787
856
|
action_url = _reverse_connector_url("charger-action", cid, connector_slug)
|
|
857
|
+
chart_should_animate = bool(has_active_session and not past_session)
|
|
858
|
+
|
|
788
859
|
return render(
|
|
789
860
|
request,
|
|
790
861
|
"ocpp/charger_status.html",
|
|
@@ -815,6 +886,11 @@ def charger_status(request, cid, connector=None):
|
|
|
815
886
|
for dataset in chart_data["datasets"]
|
|
816
887
|
)
|
|
817
888
|
),
|
|
889
|
+
"date_view": date_view,
|
|
890
|
+
"date_toggle_links": date_toggle_links,
|
|
891
|
+
"pagination_query": pagination_query,
|
|
892
|
+
"session_query": session_query,
|
|
893
|
+
"chart_should_animate": chart_should_animate,
|
|
818
894
|
},
|
|
819
895
|
)
|
|
820
896
|
|
|
@@ -824,6 +900,28 @@ def charger_session_search(request, cid, connector=None):
|
|
|
824
900
|
charger, connector_slug = _get_charger(cid, connector)
|
|
825
901
|
_ensure_charger_access(request.user, charger)
|
|
826
902
|
date_str = request.GET.get("date")
|
|
903
|
+
date_view = request.GET.get("dates", "charger").lower()
|
|
904
|
+
if date_view not in {"charger", "received"}:
|
|
905
|
+
date_view = "charger"
|
|
906
|
+
|
|
907
|
+
def _date_query(mode: str) -> str:
|
|
908
|
+
params = request.GET.copy()
|
|
909
|
+
params["dates"] = mode
|
|
910
|
+
query = params.urlencode()
|
|
911
|
+
return f"?{query}" if query else ""
|
|
912
|
+
|
|
913
|
+
date_toggle_links = [
|
|
914
|
+
{
|
|
915
|
+
"mode": mode,
|
|
916
|
+
"label": label,
|
|
917
|
+
"url": _date_query(mode),
|
|
918
|
+
"active": mode == date_view,
|
|
919
|
+
}
|
|
920
|
+
for mode, label in {
|
|
921
|
+
"charger": _("Charger timestamps"),
|
|
922
|
+
"received": _("Received timestamps"),
|
|
923
|
+
}.items()
|
|
924
|
+
]
|
|
827
925
|
transactions = None
|
|
828
926
|
if date_str:
|
|
829
927
|
try:
|
|
@@ -861,6 +959,8 @@ def charger_session_search(request, cid, connector=None):
|
|
|
861
959
|
"connector_slug": connector_slug,
|
|
862
960
|
"connector_links": connector_links,
|
|
863
961
|
"status_url": status_url,
|
|
962
|
+
"date_view": date_view,
|
|
963
|
+
"date_toggle_links": date_toggle_links,
|
|
864
964
|
},
|
|
865
965
|
)
|
|
866
966
|
|
pages/admin.py
CHANGED
|
@@ -23,6 +23,8 @@ from django.utils.translation import gettext_lazy as _, ngettext
|
|
|
23
23
|
from nodes.models import Node
|
|
24
24
|
from nodes.utils import capture_screenshot, save_screenshot
|
|
25
25
|
|
|
26
|
+
from .forms import UserManualAdminForm
|
|
27
|
+
|
|
26
28
|
from .models import (
|
|
27
29
|
SiteBadge,
|
|
28
30
|
Application,
|
|
@@ -190,6 +192,7 @@ class ModuleAdmin(EntityModelAdmin):
|
|
|
190
192
|
|
|
191
193
|
@admin.register(UserManual)
|
|
192
194
|
class UserManualAdmin(EntityModelAdmin):
|
|
195
|
+
form = UserManualAdminForm
|
|
193
196
|
list_display = ("title", "slug", "languages", "is_seed_data", "is_user_data")
|
|
194
197
|
search_fields = ("title", "slug", "description")
|
|
195
198
|
list_filter = ("is_seed_data", "is_user_data")
|
pages/forms.py
CHANGED
|
@@ -9,7 +9,9 @@ from django.core.exceptions import ValidationError
|
|
|
9
9
|
from django.utils.translation import gettext_lazy as _
|
|
10
10
|
from django.views.decorators.debug import sensitive_variables
|
|
11
11
|
|
|
12
|
-
from .
|
|
12
|
+
from core.form_fields import Base64FileField
|
|
13
|
+
|
|
14
|
+
from .models import UserManual, UserStory
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class AuthenticatorLoginForm(AuthenticationForm):
|
|
@@ -133,6 +135,33 @@ class AuthenticatorEnrollmentForm(forms.Form):
|
|
|
133
135
|
return self.device
|
|
134
136
|
|
|
135
137
|
|
|
138
|
+
_manual_pdf_field = UserManual._meta.get_field("content_pdf")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class UserManualAdminForm(forms.ModelForm):
|
|
142
|
+
content_pdf = Base64FileField(
|
|
143
|
+
label=_manual_pdf_field.verbose_name,
|
|
144
|
+
help_text=_manual_pdf_field.help_text,
|
|
145
|
+
required=not _manual_pdf_field.blank,
|
|
146
|
+
content_type="application/pdf",
|
|
147
|
+
download_name="manual.pdf",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
class Meta:
|
|
151
|
+
model = UserManual
|
|
152
|
+
fields = "__all__"
|
|
153
|
+
|
|
154
|
+
def __init__(self, *args, **kwargs):
|
|
155
|
+
super().__init__(*args, **kwargs)
|
|
156
|
+
instance = getattr(self, "instance", None)
|
|
157
|
+
slug = getattr(instance, "slug", "")
|
|
158
|
+
if slug:
|
|
159
|
+
self.fields["content_pdf"].widget.download_name = f"{slug}.pdf"
|
|
160
|
+
self.fields["content_pdf"].widget.attrs.setdefault(
|
|
161
|
+
"accept", "application/pdf"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
136
165
|
class UserStoryForm(forms.ModelForm):
|
|
137
166
|
class Meta:
|
|
138
167
|
model = UserStory
|
pages/tests.py
CHANGED
|
@@ -22,9 +22,15 @@ from pages.models import (
|
|
|
22
22
|
SiteBadge,
|
|
23
23
|
Favorite,
|
|
24
24
|
ViewHistory,
|
|
25
|
+
UserManual,
|
|
25
26
|
UserStory,
|
|
26
27
|
)
|
|
27
|
-
from pages.admin import
|
|
28
|
+
from pages.admin import (
|
|
29
|
+
ApplicationAdmin,
|
|
30
|
+
UserManualAdmin,
|
|
31
|
+
UserStoryAdmin,
|
|
32
|
+
ViewHistoryAdmin,
|
|
33
|
+
)
|
|
28
34
|
from pages.screenshot_specs import (
|
|
29
35
|
ScreenshotSpec,
|
|
30
36
|
ScreenshotSpecRunner,
|
|
@@ -1069,6 +1075,65 @@ class ConstellationNavTests(TestCase):
|
|
|
1069
1075
|
resp = self.client.get(reverse("pages:index"))
|
|
1070
1076
|
self.assertContains(resp, 'href="/ocpp/"')
|
|
1071
1077
|
|
|
1078
|
+
class ControlNavTests(TestCase):
|
|
1079
|
+
def setUp(self):
|
|
1080
|
+
self.client = Client()
|
|
1081
|
+
role, _ = NodeRole.objects.get_or_create(name="Control")
|
|
1082
|
+
Node.objects.update_or_create(
|
|
1083
|
+
mac_address=Node.get_current_mac(),
|
|
1084
|
+
defaults={
|
|
1085
|
+
"hostname": "localhost",
|
|
1086
|
+
"address": "127.0.0.1",
|
|
1087
|
+
"role": role,
|
|
1088
|
+
},
|
|
1089
|
+
)
|
|
1090
|
+
Site.objects.update_or_create(
|
|
1091
|
+
id=1, defaults={"domain": "testserver", "name": ""}
|
|
1092
|
+
)
|
|
1093
|
+
fixtures = [
|
|
1094
|
+
Path(
|
|
1095
|
+
settings.BASE_DIR,
|
|
1096
|
+
"pages",
|
|
1097
|
+
"fixtures",
|
|
1098
|
+
"control__application_ocpp.json",
|
|
1099
|
+
),
|
|
1100
|
+
Path(
|
|
1101
|
+
settings.BASE_DIR,
|
|
1102
|
+
"pages",
|
|
1103
|
+
"fixtures",
|
|
1104
|
+
"control__module_ocpp.json",
|
|
1105
|
+
),
|
|
1106
|
+
Path(
|
|
1107
|
+
settings.BASE_DIR,
|
|
1108
|
+
"pages",
|
|
1109
|
+
"fixtures",
|
|
1110
|
+
"control__landing_ocpp_dashboard.json",
|
|
1111
|
+
),
|
|
1112
|
+
Path(
|
|
1113
|
+
settings.BASE_DIR,
|
|
1114
|
+
"pages",
|
|
1115
|
+
"fixtures",
|
|
1116
|
+
"control__landing_ocpp_cp_simulator.json",
|
|
1117
|
+
),
|
|
1118
|
+
Path(
|
|
1119
|
+
settings.BASE_DIR,
|
|
1120
|
+
"pages",
|
|
1121
|
+
"fixtures",
|
|
1122
|
+
"control__landing_ocpp_rfid.json",
|
|
1123
|
+
),
|
|
1124
|
+
]
|
|
1125
|
+
call_command("loaddata", *map(str, fixtures))
|
|
1126
|
+
|
|
1127
|
+
def test_ocpp_dashboard_visible(self):
|
|
1128
|
+
user = get_user_model().objects.create_user("control", password="pw")
|
|
1129
|
+
self.client.force_login(user)
|
|
1130
|
+
resp = self.client.get(reverse("pages:index"))
|
|
1131
|
+
self.assertEqual(resp.status_code, 200)
|
|
1132
|
+
self.assertContains(resp, 'href="/ocpp/"')
|
|
1133
|
+
self.assertContains(
|
|
1134
|
+
resp, 'badge rounded-pill text-bg-secondary">CHARGERS'
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1072
1137
|
def test_header_links_visible_when_defined(self):
|
|
1073
1138
|
Reference.objects.create(
|
|
1074
1139
|
alt_text="Console",
|
|
@@ -1242,6 +1307,58 @@ class ApplicationAdminDisplayTests(TestCase):
|
|
|
1242
1307
|
self.assertContains(resp, "Power, Energy and Cost calculations.")
|
|
1243
1308
|
|
|
1244
1309
|
|
|
1310
|
+
class UserManualAdminFormTests(TestCase):
|
|
1311
|
+
def setUp(self):
|
|
1312
|
+
self.manual = UserManual.objects.create(
|
|
1313
|
+
slug="manual-one",
|
|
1314
|
+
title="Manual One",
|
|
1315
|
+
description="Test manual",
|
|
1316
|
+
languages="en",
|
|
1317
|
+
content_html="<p>Manual</p>",
|
|
1318
|
+
content_pdf=base64.b64encode(b"initial").decode("ascii"),
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1321
|
+
def test_widget_uses_slug_for_download(self):
|
|
1322
|
+
admin_instance = UserManualAdmin(UserManual, admin.site)
|
|
1323
|
+
form_class = admin_instance.get_form(request=None, obj=self.manual)
|
|
1324
|
+
form = form_class(instance=self.manual)
|
|
1325
|
+
field = form.fields["content_pdf"]
|
|
1326
|
+
self.assertEqual(field.widget.download_name, f"{self.manual.slug}.pdf")
|
|
1327
|
+
self.assertEqual(field.widget.content_type, "application/pdf")
|
|
1328
|
+
|
|
1329
|
+
def test_upload_encodes_content_pdf(self):
|
|
1330
|
+
admin_instance = UserManualAdmin(UserManual, admin.site)
|
|
1331
|
+
form_class = admin_instance.get_form(request=None, obj=self.manual)
|
|
1332
|
+
payload = {
|
|
1333
|
+
"slug": self.manual.slug,
|
|
1334
|
+
"title": self.manual.title,
|
|
1335
|
+
"description": self.manual.description,
|
|
1336
|
+
"languages": self.manual.languages,
|
|
1337
|
+
"content_html": self.manual.content_html,
|
|
1338
|
+
}
|
|
1339
|
+
upload = SimpleUploadedFile("manual.pdf", b"PDF data")
|
|
1340
|
+
form = form_class(data=payload, files={"content_pdf": upload}, instance=self.manual)
|
|
1341
|
+
self.assertTrue(form.is_valid(), form.errors.as_json())
|
|
1342
|
+
self.assertEqual(
|
|
1343
|
+
form.cleaned_data["content_pdf"],
|
|
1344
|
+
base64.b64encode(b"PDF data").decode("ascii"),
|
|
1345
|
+
)
|
|
1346
|
+
|
|
1347
|
+
def test_initial_base64_preserved_without_upload(self):
|
|
1348
|
+
admin_instance = UserManualAdmin(UserManual, admin.site)
|
|
1349
|
+
form_class = admin_instance.get_form(request=None, obj=self.manual)
|
|
1350
|
+
payload = {
|
|
1351
|
+
"slug": self.manual.slug,
|
|
1352
|
+
"title": self.manual.title,
|
|
1353
|
+
"description": self.manual.description,
|
|
1354
|
+
"languages": self.manual.languages,
|
|
1355
|
+
"content_html": self.manual.content_html,
|
|
1356
|
+
}
|
|
1357
|
+
form = form_class(data=payload, files={}, instance=self.manual)
|
|
1358
|
+
self.assertTrue(form.is_valid(), form.errors.as_json())
|
|
1359
|
+
self.assertEqual(form.cleaned_data["content_pdf"], self.manual.content_pdf)
|
|
1360
|
+
|
|
1361
|
+
|
|
1245
1362
|
class LandingCreationTests(TestCase):
|
|
1246
1363
|
def setUp(self):
|
|
1247
1364
|
role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
pages/views.py
CHANGED
|
@@ -169,7 +169,7 @@ def _build_model_graph(models):
|
|
|
169
169
|
raise RuntimeError("Graphviz is not installed")
|
|
170
170
|
|
|
171
171
|
graph = Digraph(
|
|
172
|
-
"admin_app_models",
|
|
172
|
+
name="admin_app_models",
|
|
173
173
|
graph_attr={
|
|
174
174
|
"rankdir": "LR",
|
|
175
175
|
"splines": "ortho",
|
|
@@ -229,7 +229,7 @@ def _build_model_graph(models):
|
|
|
229
229
|
label = '<\n <table BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4">\n '
|
|
230
230
|
label += "\n ".join(rows)
|
|
231
231
|
label += "\n </table>\n>"
|
|
232
|
-
graph.node(node_id, label=label)
|
|
232
|
+
graph.node(name=node_id, label=label)
|
|
233
233
|
|
|
234
234
|
edges = set()
|
|
235
235
|
for model in models:
|
|
@@ -244,7 +244,11 @@ def _build_model_graph(models):
|
|
|
244
244
|
key = (source_id, node_ids[related], tuple(sorted(attrs.items())))
|
|
245
245
|
if key not in edges:
|
|
246
246
|
edges.add(key)
|
|
247
|
-
graph.edge(
|
|
247
|
+
graph.edge(
|
|
248
|
+
tail_name=source_id,
|
|
249
|
+
head_name=node_ids[related],
|
|
250
|
+
**attrs,
|
|
251
|
+
)
|
|
248
252
|
|
|
249
253
|
for field in model._meta.local_many_to_many:
|
|
250
254
|
related = _resolve_related_model(field, model._meta.app_label)
|
|
@@ -259,7 +263,11 @@ def _build_model_graph(models):
|
|
|
259
263
|
key = (source_id, node_ids[related], tuple(sorted(attrs.items())))
|
|
260
264
|
if key not in edges:
|
|
261
265
|
edges.add(key)
|
|
262
|
-
graph.edge(
|
|
266
|
+
graph.edge(
|
|
267
|
+
tail_name=source_id,
|
|
268
|
+
head_name=node_ids[related],
|
|
269
|
+
**attrs,
|
|
270
|
+
)
|
|
263
271
|
|
|
264
272
|
return graph
|
|
265
273
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|