arthexis 0.1.10__py3-none-any.whl → 0.1.11__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/views.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import base64
2
2
  import logging
3
3
  from pathlib import Path
4
+ from types import SimpleNamespace
4
5
  import datetime
5
6
  import calendar
6
7
  import io
@@ -19,9 +20,10 @@ from django import forms
19
20
  from django.apps import apps as django_apps
20
21
  from utils.sites import get_site
21
22
  from django.http import Http404, HttpResponse
22
- from django.shortcuts import redirect, render
23
+ from django.shortcuts import get_object_or_404, redirect, render
23
24
  from nodes.models import Node
24
25
  from django.template.response import TemplateResponse
26
+ from django.test import RequestFactory
25
27
  from django.urls import NoReverseMatch, reverse
26
28
  from django.utils import timezone
27
29
  from django.utils.encoding import force_bytes, force_str
@@ -47,13 +49,37 @@ except ImportError: # pragma: no cover - handled gracefully in views
47
49
  CalledProcessError = ExecutableNotFound = None
48
50
 
49
51
  import markdown
52
+
53
+
54
+ MARKDOWN_EXTENSIONS = ["toc", "tables", "mdx_truly_sane_lists"]
55
+
56
+
57
+ def _render_markdown_with_toc(text: str) -> tuple[str, str]:
58
+ """Render ``text`` to HTML and return the HTML and stripped TOC."""
59
+
60
+ md = markdown.Markdown(extensions=MARKDOWN_EXTENSIONS)
61
+ html = md.convert(text)
62
+ toc_html = md.toc
63
+ toc_html = _strip_toc_wrapper(toc_html)
64
+ return html, toc_html
65
+
66
+
67
+ def _strip_toc_wrapper(toc_html: str) -> str:
68
+ """Normalize ``markdown``'s TOC output by removing the wrapper ``div``."""
69
+
70
+ toc_html = toc_html.strip()
71
+ if toc_html.startswith('<div class="toc">'):
72
+ toc_html = toc_html[len('<div class="toc">') :]
73
+ if toc_html.endswith("</div>"):
74
+ toc_html = toc_html[: -len("</div>")]
75
+ return toc_html.strip()
50
76
  from pages.utils import landing
51
77
  from core.liveupdate import live_update
52
78
  from django_otp import login as otp_login
53
79
  from django_otp.plugins.otp_totp.models import TOTPDevice
54
80
  import qrcode
55
81
  from .forms import AuthenticatorEnrollmentForm, AuthenticatorLoginForm
56
- from .models import Module
82
+ from .models import Module, UserManual
57
83
 
58
84
 
59
85
  logger = logging.getLogger(__name__)
@@ -407,14 +433,7 @@ def index(request):
407
433
  candidates.append(root_base / "README.md")
408
434
  readme_file = next((p for p in candidates if p.exists()), root_base / "README.md")
409
435
  text = readme_file.read_text(encoding="utf-8")
410
- md = markdown.Markdown(extensions=["toc", "tables"])
411
- html = md.convert(text)
412
- toc_html = md.toc
413
- if toc_html.strip().startswith('<div class="toc">'):
414
- toc_html = toc_html.strip()[len('<div class="toc">') :]
415
- if toc_html.endswith("</div>"):
416
- toc_html = toc_html[: -len("</div>")]
417
- toc_html = toc_html.strip()
436
+ html, toc_html = _render_markdown_with_toc(text)
418
437
  title = "README" if readme_file.name.startswith("README") else readme_file.stem
419
438
  context = {"content": html, "title": title, "toc": toc_html}
420
439
  response = render(request, "pages/readme.html", context)
@@ -447,14 +466,7 @@ def release_checklist(request):
447
466
  if not file_path.exists():
448
467
  raise Http404("Release checklist not found")
449
468
  text = file_path.read_text(encoding="utf-8")
450
- md = markdown.Markdown(extensions=["toc", "tables"])
451
- html = md.convert(text)
452
- toc_html = md.toc
453
- if toc_html.strip().startswith('<div class="toc">'):
454
- toc_html = toc_html.strip()[len('<div class="toc">') :]
455
- if toc_html.endswith("</div>"):
456
- toc_html = toc_html[: -len("</div>")]
457
- toc_html = toc_html.strip()
469
+ html, toc_html = _render_markdown_with_toc(text)
458
470
  context = {"content": html, "title": "Release Checklist", "toc": toc_html}
459
471
  response = render(request, "pages/readme.html", context)
460
472
  patch_vary_headers(response, ["Accept-Language", "Cookie"])
@@ -709,11 +721,14 @@ def request_invite(request):
709
721
  try:
710
722
  node_error = None
711
723
  node = Node.get_local()
724
+ outbox = getattr(node, "email_outbox", None) if node else None
712
725
  if node:
713
726
  try:
714
727
  result = node.send_mail(subject, body, [email])
728
+ lead.sent_via_outbox = outbox
715
729
  except Exception as exc:
716
730
  node_error = exc
731
+ lead.sent_via_outbox = None
717
732
  logger.exception(
718
733
  "Node send_mail failed, falling back to default backend"
719
734
  )
@@ -724,6 +739,7 @@ def request_invite(request):
724
739
  result = mailer.send(
725
740
  subject, body, [email], settings.DEFAULT_FROM_EMAIL
726
741
  )
742
+ lead.sent_via_outbox = None
727
743
  lead.sent_on = timezone.now()
728
744
  if node_error:
729
745
  lead.error = (
@@ -738,9 +754,10 @@ def request_invite(request):
738
754
  )
739
755
  except Exception as exc:
740
756
  lead.error = f"{exc}. Ensure the email service is reachable and settings are correct."
757
+ lead.sent_via_outbox = None
741
758
  logger.exception("Failed to send invitation email to %s", email)
742
759
  if lead.sent_on or lead.error:
743
- lead.save(update_fields=["sent_on", "error"])
760
+ lead.save(update_fields=["sent_on", "error", "sent_via_outbox"])
744
761
  sent = True
745
762
  return render(request, "pages/request_invite.html", {"form": form, "sent": sent})
746
763
 
@@ -1002,3 +1019,53 @@ def csrf_failure(request, reason=""):
1002
1019
  """Custom CSRF failure view with a friendly message."""
1003
1020
  logger.warning("CSRF failure on %s: %s", request.path, reason)
1004
1021
  return render(request, "pages/csrf_failure.html", status=403)
1022
+
1023
+
1024
+ def _admin_context(request):
1025
+ context = admin.site.each_context(request)
1026
+ if not context.get("has_permission"):
1027
+ rf = RequestFactory()
1028
+ mock_request = rf.get(request.path)
1029
+ mock_request.user = SimpleNamespace(
1030
+ is_active=True,
1031
+ is_staff=True,
1032
+ is_superuser=True,
1033
+ has_perm=lambda perm, obj=None: True,
1034
+ has_module_perms=lambda app_label: True,
1035
+ )
1036
+ context["available_apps"] = admin.site.get_app_list(mock_request)
1037
+ context["has_permission"] = True
1038
+ return context
1039
+
1040
+
1041
+ def admin_manual_list(request):
1042
+ manuals = UserManual.objects.order_by("title")
1043
+ context = _admin_context(request)
1044
+ context["manuals"] = manuals
1045
+ return render(request, "admin_doc/manuals.html", context)
1046
+
1047
+
1048
+ def admin_manual_detail(request, slug):
1049
+ manual = get_object_or_404(UserManual, slug=slug)
1050
+ context = _admin_context(request)
1051
+ context["manual"] = manual
1052
+ return render(request, "admin_doc/manual_detail.html", context)
1053
+
1054
+
1055
+ def manual_pdf(request, slug):
1056
+ manual = get_object_or_404(UserManual, slug=slug)
1057
+ pdf_data = base64.b64decode(manual.content_pdf)
1058
+ response = HttpResponse(pdf_data, content_type="application/pdf")
1059
+ response["Content-Disposition"] = f'attachment; filename="{manual.slug}.pdf"'
1060
+ return response
1061
+
1062
+
1063
+ @landing(_("Manuals"))
1064
+ def manual_list(request):
1065
+ manuals = UserManual.objects.order_by("title")
1066
+ return render(request, "pages/manual_list.html", {"manuals": manuals})
1067
+
1068
+
1069
+ def manual_detail(request, slug):
1070
+ manual = get_object_or_404(UserManual, slug=slug)
1071
+ return render(request, "pages/manual_detail.html", {"manual": manual})