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.
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 _render_readme(request, role, doc: str | None = None):
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
- lang = getattr(request, "LANGUAGE_CODE", "")
452
- lang = lang.replace("_", "-").lower()
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
- text = readme_file.read_text(encoding="utf-8")
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 requestor."
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})