arthexis 0.1.18__py3-none-any.whl → 0.1.20__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,43 +439,133 @@ def admin_model_graph(request, app_label: str):
439
439
  return response
440
440
 
441
441
 
442
- def _render_readme(request, role):
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")
446
446
  .first()
447
447
  )
448
448
  app_slug = app.path.strip("/") if app else ""
449
- readme_base = (
450
- Path(settings.BASE_DIR) / app_slug if app_slug else Path(settings.BASE_DIR)
451
- )
452
- lang = getattr(request, "LANGUAGE_CODE", "")
453
- lang = lang.replace("_", "-").lower()
454
- root_base = Path(settings.BASE_DIR)
455
- candidates = []
456
- if lang:
457
- candidates.append(readme_base / f"README.{lang}.md")
458
- short = lang.split("-")[0]
459
- if short != lang:
460
- candidates.append(readme_base / f"README.{short}.md")
461
- candidates.append(readme_base / "README.md")
462
- if readme_base != root_base:
449
+ root_base = Path(settings.BASE_DIR).resolve()
450
+ readme_base = (root_base / app_slug).resolve() if app_slug else root_base
451
+ candidates: list[Path] = []
452
+
453
+ if doc:
454
+ normalized = doc.strip().replace("\\", "/")
455
+ while normalized.startswith("./"):
456
+ normalized = normalized[2:]
457
+ normalized = normalized.lstrip("/")
458
+ if not normalized:
459
+ raise Http404("Document not found")
460
+ doc_path = Path(normalized)
461
+ if doc_path.is_absolute() or any(part == ".." for part in doc_path.parts):
462
+ raise Http404("Document not found")
463
+
464
+ relative_candidates: list[Path] = []
465
+
466
+ def add_candidate(path: Path) -> None:
467
+ if path not in relative_candidates:
468
+ relative_candidates.append(path)
469
+
470
+ add_candidate(doc_path)
471
+ if doc_path.suffix.lower() != ".md" or doc_path.suffix != ".md":
472
+ add_candidate(doc_path.with_suffix(".md"))
473
+ if doc_path.suffix.lower() != ".md":
474
+ add_candidate(doc_path / "README.md")
475
+
476
+ search_roots = [readme_base]
477
+ if readme_base != root_base:
478
+ search_roots.append(root_base)
479
+
480
+ for relative in relative_candidates:
481
+ for base in search_roots:
482
+ base_resolved = base.resolve()
483
+ candidate = (base_resolved / relative).resolve(strict=False)
484
+ try:
485
+ candidate.relative_to(base_resolved)
486
+ except ValueError:
487
+ continue
488
+ candidates.append(candidate)
489
+ else:
463
490
  if lang:
464
- candidates.append(root_base / f"README.{lang}.md")
491
+ candidates.append(readme_base / f"README.{lang}.md")
465
492
  short = lang.split("-")[0]
466
493
  if short != lang:
467
- candidates.append(root_base / f"README.{short}.md")
468
- candidates.append(root_base / "README.md")
469
- readme_file = next((p for p in candidates if p.exists()), root_base / "README.md")
470
- text = readme_file.read_text(encoding="utf-8")
471
- html, toc_html = _render_markdown_with_toc(text)
494
+ candidates.append(readme_base / f"README.{short}.md")
495
+ candidates.append(readme_base / "README.md")
496
+ if readme_base != root_base:
497
+ if lang:
498
+ candidates.append(root_base / f"README.{lang}.md")
499
+ short = lang.split("-")[0]
500
+ if short != lang:
501
+ candidates.append(root_base / f"README.{short}.md")
502
+ candidates.append(root_base / "README.md")
503
+
504
+ readme_file = next((p for p in candidates if p.exists()), None)
505
+ if readme_file is None:
506
+ raise Http404("Document not found")
507
+
472
508
  title = "README" if readme_file.name.startswith("README") else readme_file.stem
473
- context = {"content": html, "title": title, "toc": toc_html}
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
543
+ context = {
544
+ "content": html,
545
+ "title": document.title,
546
+ "toc": toc_html,
547
+ "page_url": request.build_absolute_uri(),
548
+ "edit_url": edit_url,
549
+ }
474
550
  response = render(request, "pages/readme.html", context)
475
551
  patch_vary_headers(response, ["Accept-Language", "Cookie"])
476
552
  return response
477
553
 
478
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
+
479
569
  @landing("Home")
480
570
  @never_cache
481
571
  def index(request):
@@ -525,10 +615,61 @@ def index(request):
525
615
 
526
616
 
527
617
  @never_cache
528
- def readme(request):
618
+ def readme(request, doc=None):
529
619
  node = Node.get_local()
530
620
  role = node.role if node else None
531
- return _render_readme(request, role)
621
+ return _render_readme(request, role, doc)
622
+
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)
532
673
 
533
674
 
534
675
  def sitemap(request):
@@ -963,23 +1104,24 @@ class ClientReportForm(forms.Form):
963
1104
  label=_("Month"),
964
1105
  required=False,
965
1106
  widget=forms.DateInput(attrs={"type": "month"}),
1107
+ input_formats=["%Y-%m"],
966
1108
  help_text=_("Generates the report for the calendar month that you select."),
967
1109
  )
968
1110
  owner = forms.ModelChoiceField(
969
1111
  queryset=get_user_model().objects.all(),
970
1112
  required=False,
971
1113
  help_text=_(
972
- "Sets who owns the report schedule and is listed as the requestor."
1114
+ "Sets who owns the report schedule and is listed as the requester."
973
1115
  ),
974
1116
  )
975
1117
  destinations = forms.CharField(
976
1118
  label=_("Email destinations"),
977
1119
  required=False,
978
1120
  widget=forms.Textarea(attrs={"rows": 2}),
979
- help_text=_("Separate addresses with commas or new lines."),
1121
+ help_text=_("Separate addresses with commas, whitespace, or new lines."),
980
1122
  )
981
1123
  recurrence = forms.ChoiceField(
982
- label=_("Recurrency"),
1124
+ label=_("Recurrence"),
983
1125
  choices=RECURRENCE_CHOICES,
984
1126
  initial=ClientReportSchedule.PERIODICITY_NONE,
985
1127
  help_text=_("Defines how often the report should be generated automatically."),
@@ -1006,8 +1148,13 @@ class ClientReportForm(forms.Form):
1006
1148
  week_str = cleaned.get("week")
1007
1149
  if not week_str:
1008
1150
  raise forms.ValidationError(_("Please select a week."))
1009
- year, week_num = week_str.split("-W")
1010
- start = datetime.date.fromisocalendar(int(year), int(week_num), 1)
1151
+ try:
1152
+ year_str, week_num_str = week_str.split("-W", 1)
1153
+ start = datetime.date.fromisocalendar(
1154
+ int(year_str), int(week_num_str), 1
1155
+ )
1156
+ except (TypeError, ValueError):
1157
+ raise forms.ValidationError(_("Please select a week."))
1011
1158
  cleaned["start"] = start
1012
1159
  cleaned["end"] = start + datetime.timedelta(days=6)
1013
1160
  elif period == "month":