arthexis 0.1.19__py3-none-any.whl → 0.1.21__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.
- {arthexis-0.1.19.dist-info → arthexis-0.1.21.dist-info}/METADATA +5 -6
- {arthexis-0.1.19.dist-info → arthexis-0.1.21.dist-info}/RECORD +42 -44
- config/asgi.py +1 -15
- config/settings.py +0 -26
- config/urls.py +0 -1
- core/admin.py +143 -234
- core/apps.py +0 -6
- core/backends.py +8 -2
- core/environment.py +240 -4
- core/models.py +132 -102
- core/notifications.py +1 -1
- core/reference_utils.py +10 -11
- core/sigil_builder.py +2 -2
- core/tasks.py +24 -1
- core/tests.py +2 -7
- core/views.py +70 -132
- nodes/admin.py +162 -7
- nodes/models.py +294 -48
- nodes/rfid_sync.py +1 -1
- nodes/tasks.py +100 -2
- nodes/tests.py +581 -15
- nodes/urls.py +4 -0
- nodes/views.py +560 -96
- ocpp/admin.py +144 -4
- ocpp/consumers.py +106 -9
- ocpp/models.py +131 -1
- ocpp/tasks.py +4 -0
- ocpp/test_export_import.py +1 -0
- ocpp/test_rfid.py +3 -1
- ocpp/tests.py +183 -9
- ocpp/transactions_io.py +9 -1
- ocpp/urls.py +3 -3
- ocpp/views.py +186 -31
- pages/context_processors.py +15 -21
- pages/defaults.py +1 -1
- pages/module_defaults.py +5 -5
- pages/tests.py +110 -79
- pages/urls.py +1 -1
- pages/views.py +108 -13
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- {arthexis-0.1.19.dist-info → arthexis-0.1.21.dist-info}/WHEEL +0 -0
- {arthexis-0.1.19.dist-info → arthexis-0.1.21.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.19.dist-info → arthexis-0.1.21.dist-info}/top_level.txt +0 -0
pages/views.py
CHANGED
|
@@ -439,7 +439,7 @@ def admin_model_graph(request, app_label: str):
|
|
|
439
439
|
return response
|
|
440
440
|
|
|
441
441
|
|
|
442
|
-
def
|
|
442
|
+
def _locate_readme_document(role, doc: str | None, lang: str) -> SimpleNamespace:
|
|
443
443
|
app = (
|
|
444
444
|
Module.objects.filter(node_role=role, is_default=True)
|
|
445
445
|
.select_related("application")
|
|
@@ -448,9 +448,8 @@ def _render_readme(request, role, doc: str | None = None):
|
|
|
448
448
|
app_slug = app.path.strip("/") if app else ""
|
|
449
449
|
root_base = Path(settings.BASE_DIR).resolve()
|
|
450
450
|
readme_base = (root_base / app_slug).resolve() if app_slug else root_base
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
candidates = []
|
|
451
|
+
candidates: list[Path] = []
|
|
452
|
+
|
|
454
453
|
if doc:
|
|
455
454
|
normalized = doc.strip().replace("\\", "/")
|
|
456
455
|
while normalized.startswith("./"):
|
|
@@ -501,23 +500,72 @@ def _render_readme(request, role, doc: str | None = None):
|
|
|
501
500
|
if short != lang:
|
|
502
501
|
candidates.append(root_base / f"README.{short}.md")
|
|
503
502
|
candidates.append(root_base / "README.md")
|
|
503
|
+
|
|
504
504
|
readme_file = next((p for p in candidates if p.exists()), None)
|
|
505
505
|
if readme_file is None:
|
|
506
506
|
raise Http404("Document not found")
|
|
507
|
-
|
|
508
|
-
html, toc_html = _render_markdown_with_toc(text)
|
|
507
|
+
|
|
509
508
|
title = "README" if readme_file.name.startswith("README") else readme_file.stem
|
|
509
|
+
return SimpleNamespace(
|
|
510
|
+
file=readme_file,
|
|
511
|
+
title=title,
|
|
512
|
+
root_base=root_base,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _relative_readme_path(readme_file: Path, root_base: Path) -> str | None:
|
|
517
|
+
try:
|
|
518
|
+
return readme_file.relative_to(root_base).as_posix()
|
|
519
|
+
except ValueError:
|
|
520
|
+
return None
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _render_readme(request, role, doc: str | None = None):
|
|
524
|
+
lang = getattr(request, "LANGUAGE_CODE", "")
|
|
525
|
+
lang = lang.replace("_", "-").lower()
|
|
526
|
+
document = _locate_readme_document(role, doc, lang)
|
|
527
|
+
text = document.file.read_text(encoding="utf-8")
|
|
528
|
+
html, toc_html = _render_markdown_with_toc(text)
|
|
529
|
+
relative_path = _relative_readme_path(document.file, document.root_base)
|
|
530
|
+
user = getattr(request, "user", None)
|
|
531
|
+
can_edit = bool(
|
|
532
|
+
relative_path
|
|
533
|
+
and user
|
|
534
|
+
and user.is_authenticated
|
|
535
|
+
and user.is_superuser
|
|
536
|
+
)
|
|
537
|
+
edit_url = None
|
|
538
|
+
if can_edit:
|
|
539
|
+
try:
|
|
540
|
+
edit_url = reverse("pages:readme-edit", kwargs={"doc": relative_path})
|
|
541
|
+
except NoReverseMatch:
|
|
542
|
+
edit_url = None
|
|
510
543
|
context = {
|
|
511
544
|
"content": html,
|
|
512
|
-
"title": title,
|
|
545
|
+
"title": document.title,
|
|
513
546
|
"toc": toc_html,
|
|
514
547
|
"page_url": request.build_absolute_uri(),
|
|
548
|
+
"edit_url": edit_url,
|
|
515
549
|
}
|
|
516
550
|
response = render(request, "pages/readme.html", context)
|
|
517
551
|
patch_vary_headers(response, ["Accept-Language", "Cookie"])
|
|
518
552
|
return response
|
|
519
553
|
|
|
520
554
|
|
|
555
|
+
class MarkdownDocumentForm(forms.Form):
|
|
556
|
+
content = forms.CharField(
|
|
557
|
+
widget=forms.Textarea(
|
|
558
|
+
attrs={
|
|
559
|
+
"class": "form-control",
|
|
560
|
+
"rows": 24,
|
|
561
|
+
"spellcheck": "false",
|
|
562
|
+
}
|
|
563
|
+
),
|
|
564
|
+
required=False,
|
|
565
|
+
strip=False,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
521
569
|
@landing("Home")
|
|
522
570
|
@never_cache
|
|
523
571
|
def index(request):
|
|
@@ -573,6 +621,57 @@ def readme(request, doc=None):
|
|
|
573
621
|
return _render_readme(request, role, doc)
|
|
574
622
|
|
|
575
623
|
|
|
624
|
+
def readme_edit(request, doc):
|
|
625
|
+
user = getattr(request, "user", None)
|
|
626
|
+
if not (user and user.is_authenticated and user.is_superuser):
|
|
627
|
+
raise PermissionDenied
|
|
628
|
+
|
|
629
|
+
node = Node.get_local()
|
|
630
|
+
role = node.role if node else None
|
|
631
|
+
lang = getattr(request, "LANGUAGE_CODE", "")
|
|
632
|
+
lang = lang.replace("_", "-").lower()
|
|
633
|
+
document = _locate_readme_document(role, doc, lang)
|
|
634
|
+
relative_path = _relative_readme_path(document.file, document.root_base)
|
|
635
|
+
if relative_path:
|
|
636
|
+
read_url = reverse("pages:readme-document", kwargs={"doc": relative_path})
|
|
637
|
+
else:
|
|
638
|
+
read_url = reverse("pages:readme")
|
|
639
|
+
|
|
640
|
+
if request.method == "POST":
|
|
641
|
+
form = MarkdownDocumentForm(request.POST)
|
|
642
|
+
if form.is_valid():
|
|
643
|
+
content = form.cleaned_data["content"]
|
|
644
|
+
try:
|
|
645
|
+
document.file.write_text(content, encoding="utf-8")
|
|
646
|
+
except OSError:
|
|
647
|
+
logger.exception("Failed to update markdown document %s", document.file)
|
|
648
|
+
messages.error(
|
|
649
|
+
request,
|
|
650
|
+
_("Unable to save changes. Please try again."),
|
|
651
|
+
)
|
|
652
|
+
else:
|
|
653
|
+
messages.success(request, _("Document saved successfully."))
|
|
654
|
+
if relative_path:
|
|
655
|
+
return redirect("pages:readme-edit", doc=relative_path)
|
|
656
|
+
return redirect("pages:readme")
|
|
657
|
+
else:
|
|
658
|
+
try:
|
|
659
|
+
initial_text = document.file.read_text(encoding="utf-8")
|
|
660
|
+
except OSError:
|
|
661
|
+
logger.exception("Failed to read markdown document %s", document.file)
|
|
662
|
+
messages.error(request, _("Unable to load the document for editing."))
|
|
663
|
+
return redirect("pages:readme")
|
|
664
|
+
form = MarkdownDocumentForm(initial={"content": initial_text})
|
|
665
|
+
|
|
666
|
+
context = {
|
|
667
|
+
"form": form,
|
|
668
|
+
"title": document.title,
|
|
669
|
+
"relative_path": relative_path,
|
|
670
|
+
"read_url": read_url,
|
|
671
|
+
}
|
|
672
|
+
return render(request, "pages/readme_edit.html", context)
|
|
673
|
+
|
|
674
|
+
|
|
576
675
|
def sitemap(request):
|
|
577
676
|
site = get_site(request)
|
|
578
677
|
node = Node.get_local()
|
|
@@ -611,11 +710,6 @@ def release_checklist(request):
|
|
|
611
710
|
return response
|
|
612
711
|
|
|
613
712
|
|
|
614
|
-
@csrf_exempt
|
|
615
|
-
def datasette_auth(request):
|
|
616
|
-
if request.user.is_authenticated:
|
|
617
|
-
return HttpResponse("OK")
|
|
618
|
-
return HttpResponse(status=401)
|
|
619
713
|
|
|
620
714
|
|
|
621
715
|
class CustomLoginView(LoginView):
|
|
@@ -1005,13 +1099,14 @@ class ClientReportForm(forms.Form):
|
|
|
1005
1099
|
label=_("Month"),
|
|
1006
1100
|
required=False,
|
|
1007
1101
|
widget=forms.DateInput(attrs={"type": "month"}),
|
|
1102
|
+
input_formats=["%Y-%m"],
|
|
1008
1103
|
help_text=_("Generates the report for the calendar month that you select."),
|
|
1009
1104
|
)
|
|
1010
1105
|
owner = forms.ModelChoiceField(
|
|
1011
1106
|
queryset=get_user_model().objects.all(),
|
|
1012
1107
|
required=False,
|
|
1013
1108
|
help_text=_(
|
|
1014
|
-
"Sets who owns the report schedule and is listed as the
|
|
1109
|
+
"Sets who owns the report schedule and is listed as the requester."
|
|
1015
1110
|
),
|
|
1016
1111
|
)
|
|
1017
1112
|
destinations = forms.CharField(
|
core/workgroup_urls.py
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"""URL routes for assistant profile endpoints."""
|
|
2
|
-
|
|
3
|
-
from django.urls import path
|
|
4
|
-
|
|
5
|
-
from . import workgroup_views as views
|
|
6
|
-
|
|
7
|
-
app_name = "workgroup"
|
|
8
|
-
|
|
9
|
-
urlpatterns = [
|
|
10
|
-
path(
|
|
11
|
-
"assistant-profiles/<int:user_id>/",
|
|
12
|
-
views.issue_key,
|
|
13
|
-
name="assistantprofile-issue",
|
|
14
|
-
),
|
|
15
|
-
path("assistant/test/", views.assistant_test, name="assistant-test"),
|
|
16
|
-
path("chat/", views.chat, name="chat"),
|
|
17
|
-
]
|
core/workgroup_views.py
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
"""REST endpoints for AssistantProfile issuance and authentication."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from functools import wraps
|
|
6
|
-
|
|
7
|
-
from django.apps import apps
|
|
8
|
-
from django.contrib.auth import get_user_model
|
|
9
|
-
from django.forms.models import model_to_dict
|
|
10
|
-
from django.http import HttpResponse, JsonResponse
|
|
11
|
-
from django.views.decorators.csrf import csrf_exempt
|
|
12
|
-
from django.views.decorators.http import require_GET, require_POST
|
|
13
|
-
|
|
14
|
-
from .models import AssistantProfile, hash_key
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@csrf_exempt
|
|
18
|
-
@require_POST
|
|
19
|
-
def issue_key(request, user_id: int) -> JsonResponse:
|
|
20
|
-
"""Issue a new ``user_key`` for ``user_id``.
|
|
21
|
-
|
|
22
|
-
The response reveals the plain key once. Store only the hash server-side.
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
user = get_user_model().objects.get(pk=user_id)
|
|
26
|
-
profile, key = AssistantProfile.issue_key(user)
|
|
27
|
-
return JsonResponse({"user_id": user_id, "user_key": key})
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def authenticate(view_func):
|
|
31
|
-
"""View decorator that validates the ``Authorization`` header."""
|
|
32
|
-
|
|
33
|
-
@wraps(view_func)
|
|
34
|
-
def wrapper(request, *args, **kwargs):
|
|
35
|
-
header = request.META.get("HTTP_AUTHORIZATION", "")
|
|
36
|
-
if not header.startswith("Bearer "):
|
|
37
|
-
return HttpResponse(status=401)
|
|
38
|
-
|
|
39
|
-
key_hash = hash_key(header.split(" ", 1)[1])
|
|
40
|
-
try:
|
|
41
|
-
profile = AssistantProfile.objects.get(
|
|
42
|
-
user_key_hash=key_hash, is_active=True
|
|
43
|
-
)
|
|
44
|
-
except AssistantProfile.DoesNotExist:
|
|
45
|
-
return HttpResponse(status=401)
|
|
46
|
-
|
|
47
|
-
profile.touch()
|
|
48
|
-
request.assistant_profile = profile
|
|
49
|
-
request.chat_profile = profile
|
|
50
|
-
return view_func(request, *args, **kwargs)
|
|
51
|
-
|
|
52
|
-
return wrapper
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@require_GET
|
|
56
|
-
@authenticate
|
|
57
|
-
def assistant_test(request):
|
|
58
|
-
"""Return a simple greeting to confirm authentication."""
|
|
59
|
-
|
|
60
|
-
profile = getattr(request, "assistant_profile", None)
|
|
61
|
-
user_id = profile.user_id if profile else None
|
|
62
|
-
return JsonResponse({"message": f"Hello from user {user_id}"})
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
@require_GET
|
|
66
|
-
@authenticate
|
|
67
|
-
def chat(request):
|
|
68
|
-
"""Return serialized data from any model.
|
|
69
|
-
|
|
70
|
-
Clients must provide ``model`` as ``app_label.ModelName`` and may include a
|
|
71
|
-
``pk`` to fetch a specific record. When ``pk`` is omitted, the view returns
|
|
72
|
-
up to 100 records.
|
|
73
|
-
"""
|
|
74
|
-
|
|
75
|
-
model_label = request.GET.get("model")
|
|
76
|
-
if not model_label:
|
|
77
|
-
return JsonResponse({"error": "model parameter required"}, status=400)
|
|
78
|
-
try:
|
|
79
|
-
model = apps.get_model(model_label)
|
|
80
|
-
except LookupError:
|
|
81
|
-
return JsonResponse({"error": "unknown model"}, status=400)
|
|
82
|
-
|
|
83
|
-
qs = model.objects.all()
|
|
84
|
-
pk = request.GET.get("pk")
|
|
85
|
-
if pk is not None:
|
|
86
|
-
try:
|
|
87
|
-
obj = qs.get(pk=pk)
|
|
88
|
-
except model.DoesNotExist:
|
|
89
|
-
return JsonResponse({"error": "object not found"}, status=404)
|
|
90
|
-
data = model_to_dict(obj)
|
|
91
|
-
else:
|
|
92
|
-
data = [model_to_dict(o) for o in qs[:100]]
|
|
93
|
-
|
|
94
|
-
return JsonResponse({"data": data})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|