| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309 |
- from django.shortcuts import render, redirect, get_object_or_404
- from django.contrib.admin.views.decorators import staff_member_required
- from django.views.decorators.http import require_POST
- from django.contrib import messages
- from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
- from django.urls import reverse, get_resolver, URLPattern, URLResolver, NoReverseMatch
- from functools import wraps
- from typing import Any, Dict, List, Optional, Tuple
- from admin_frontend.templatetags.public_urls import public_route
- from admin_frontend.nav import _nav_items
- from .models import Post, PostCategory
- from .forms import PostForm, PostCategoryForm
- from .filters import PostFilter
- # --- Lightweight breadcrumbs decorator -------------------------------------
- def _iter_patterns(resolver: URLResolver, ns_path: str = ""):
- for entry in resolver.url_patterns:
- if isinstance(entry, URLResolver):
- new_ns = ns_path
- if entry.namespace:
- new_ns = f"{ns_path}:{entry.namespace}" if ns_path else entry.namespace
- yield from _iter_patterns(entry, new_ns)
- elif isinstance(entry, URLPattern):
- full_name = entry.name
- if full_name and ns_path:
- full_name = f"{ns_path}:{full_name}"
- yield entry, full_name
- def _find_pattern_by_breadcrumb_name(name: str) -> Tuple[Optional[URLPattern], Optional[str]]:
- resolver = get_resolver()
- for pattern, full_name in _iter_patterns(resolver):
- if not full_name:
- continue
- view = pattern.callback
- # unwrap to find metadata on inner functions
- meta = None
- cur = view
- while cur is not None and meta is None:
- meta = getattr(cur, "_breadcrumbs_meta", None)
- cur = getattr(cur, "__wrapped__", None)
- if meta and meta.get("name") == name:
- return pattern, full_name
- return None, None
- def _build_breadcrumb_chain(meta: Dict[str, Any]) -> List[Tuple[str, Optional[str]]]:
- chain: List[Tuple[str, Optional[str]]] = []
- # Walk parent chain (by name) outward
- parent_name = meta.get("parent")
- while parent_name:
- pattern, full_name = _find_pattern_by_breadcrumb_name(parent_name)
- if not pattern:
- break
- view = pattern.callback
- vmeta = None
- cur = view
- while cur is not None and vmeta is None:
- vmeta = getattr(cur, "_breadcrumbs_meta", None)
- cur = getattr(cur, "__wrapped__", None)
- vmeta = vmeta or {}
- label = vmeta.get("label") or parent_name
- # Try to reverse parent URL (skip if args required)
- url: Optional[str] = None
- if full_name:
- try:
- url = reverse(full_name)
- except NoReverseMatch:
- url = None
- chain.insert(0, (label, url))
- parent_name = vmeta.get("parent")
- # Add current at the end (no URL)
- chain.append((meta.get("label") or "", None))
- return chain
- def breadcrumbs(*, label: str, name: Optional[str] = None, parent: Optional[str] = None):
- """Decorator to attach breadcrumb metadata and expose a computed trail.
- Usage:
- @breadcrumbs(label="Posts", name="posts")
- def posts_list(...): ...
- @breadcrumbs(label="Post Edit", parent="posts")
- def post_edit(...): ...
- """
- def _decorator(view_func):
- meta = {"label": label, "name": name, "parent": parent}
- @wraps(view_func)
- def _wrapped(request, *args, **kwargs):
- # Expose breadcrumbs via request for templates/tags to consume
- request._breadcrumbs = _build_breadcrumb_chain(meta)
- return view_func(request, *args, **kwargs)
- # Make metadata discoverable on the resolved callback
- setattr(_wrapped, "_breadcrumbs_meta", meta)
- return _wrapped
- return _decorator
- @staff_member_required
- @breadcrumbs(label="Posts", name="posts")
- @public_route(label="CMS Posts", order=70, icon="document-text")
- def posts_list(request):
- if request.method == "POST":
- form = PostForm(request.POST, request.FILES)
- if form.is_valid():
- obj = form.save(commit=False)
- # Scope to current organization if available
- org = getattr(request, "org", None)
- if org is not None:
- obj.organization = org
- obj.save()
- messages.success(request, "Post created.")
- return redirect("cms:posts_list")
- else:
- messages.error(request, "Please correct the errors below.")
- else:
- form = PostForm()
- # Restrict category choices to current organization
- org = getattr(request, "org", None)
- if org is not None:
- try:
- form.fields["category"].queryset = PostCategory.objects.filter(organization=org)
- except Exception:
- pass
- qs = Post.objects.select_related("category", "author").all().order_by("-published_at", "-created_at")
- if org is not None:
- qs = qs.filter(organization=org)
- post_filter = PostFilter(request.GET, queryset=qs)
- # Also scope the filter's category queryset
- if org is not None:
- try:
- post_filter.form.fields["category"].queryset = PostCategory.objects.filter(organization=org)
- except Exception:
- pass
- paginator = Paginator(post_filter.qs, 10)
- page = request.GET.get("page")
- try:
- page_obj = paginator.page(page)
- except PageNotAnInteger:
- page_obj = paginator.page(1)
- except EmptyPage:
- page_obj = paginator.page(paginator.num_pages)
- context = {
- "nav": _nav_items(),
- "posts": page_obj.object_list,
- "page_obj": page_obj,
- "form": form,
- "filter": post_filter,
- }
- return render(request, "cms/posts_list.html", context)
- @staff_member_required
- @breadcrumbs(label="Post Edit", parent="posts")
- def post_edit(request, pk: int):
- org = getattr(request, "org", None)
- base_qs = Post.objects.all()
- if org is not None:
- base_qs = base_qs.filter(organization=org)
- item = get_object_or_404(base_qs, pk=pk)
- if request.method == "POST":
- form = PostForm(request.POST, request.FILES, instance=item)
- if form.is_valid():
- obj = form.save(commit=False)
- org = getattr(request, "org", None)
- if org is not None:
- obj.organization = org
- obj.save()
- messages.success(request, "Post updated.")
- return redirect("cms:post_edit", pk=item.pk)
- else:
- form = PostForm(instance=item)
- # Restrict category choices
- org = getattr(request, "org", None)
- if org is not None:
- try:
- form.fields["category"].queryset = PostCategory.objects.filter(organization=org)
- except Exception:
- pass
- context = {
- "nav": _nav_items(),
- "item": item,
- "form": form,
- "cancel_url": reverse("cms:posts_list"),
- }
- return render(request, "cms/post_form.html", context)
- @staff_member_required
- @require_POST
- def post_delete(request, pk: int):
- org = getattr(request, "org", None)
- base_qs = Post.objects.all()
- if org is not None:
- base_qs = base_qs.filter(organization=org)
- item = get_object_or_404(base_qs, pk=pk)
- item.delete()
- messages.success(request, "Post deleted.")
- return redirect("cms:posts_list")
- @staff_member_required
- @public_route(label="CMS Categories", order=75, icon="tag")
- def categories_list(request):
- if request.method == "POST":
- form = PostCategoryForm(request.POST)
- if form.is_valid():
- obj = form.save(commit=False)
- org = getattr(request, "org", None)
- if org is not None:
- obj.organization = org
- obj.save()
- messages.success(request, "Category created.")
- return redirect("cms:categories_list")
- else:
- messages.error(request, "Please correct the errors below.")
- else:
- form = PostCategoryForm()
- q = (request.GET.get("q") or "").strip()
- qs = PostCategory.objects.all().order_by("name")
- org = getattr(request, "org", None)
- if org is not None:
- qs = qs.filter(organization=org)
- if q:
- qs = qs.filter(name__icontains=q)
- paginator = Paginator(qs, 15)
- page = request.GET.get("page")
- try:
- page_obj = paginator.page(page)
- except PageNotAnInteger:
- page_obj = paginator.page(1)
- except EmptyPage:
- page_obj = paginator.page(paginator.num_pages)
- context = {
- "nav": _nav_items(),
- "categories": qs,
- "page_obj": page_obj,
- "form": form,
- "query": q,
- }
- return render(request, "cms/categories_list.html", context)
- @staff_member_required
- def category_edit(request, pk: int):
- org = getattr(request, "org", None)
- base_qs = PostCategory.objects.all()
- if org is not None:
- base_qs = base_qs.filter(organization=org)
- item = get_object_or_404(base_qs, pk=pk)
- if request.method == "POST":
- form = PostCategoryForm(request.POST, instance=item)
- if form.is_valid():
- obj = form.save(commit=False)
- org = getattr(request, "org", None)
- if org is not None:
- obj.organization = org
- obj.save()
- messages.success(request, "Category updated.")
- return redirect("cms:category_edit", pk=item.pk)
- else:
- form = PostCategoryForm(instance=item)
- # Restrict parent choices
- org = getattr(request, "org", None)
- if org is not None:
- try:
- form.fields["parent"].queryset = PostCategory.objects.filter(organization=org)
- except Exception:
- pass
- context = {
- "nav": _nav_items(),
- "item": item,
- "form": form,
- "cancel_url": reverse("cms:categories_list"),
- }
- return render(request, "cms/category_form.html", context)
- @staff_member_required
- @require_POST
- def category_delete(request, pk: int):
- org = getattr(request, "org", None)
- base_qs = PostCategory.objects.all()
- if org is not None:
- base_qs = base_qs.filter(organization=org)
- item = get_object_or_404(base_qs, pk=pk)
- item.delete()
- messages.success(request, "Category deleted.")
- return redirect("cms:categories_list")
|