Ei kuvausta

views.py 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. from django.shortcuts import render, redirect, get_object_or_404
  2. from django.contrib.admin.views.decorators import staff_member_required
  3. from django.views.decorators.http import require_POST
  4. from django.contrib import messages
  5. from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
  6. from django.urls import reverse, get_resolver, URLPattern, URLResolver, NoReverseMatch
  7. from functools import wraps
  8. from typing import Any, Dict, List, Optional, Tuple
  9. from admin_frontend.templatetags.public_urls import public_route
  10. from admin_frontend.nav import _nav_items
  11. from .models import Post, PostCategory
  12. from .forms import PostForm, PostCategoryForm
  13. from .filters import PostFilter
  14. # --- Lightweight breadcrumbs decorator -------------------------------------
  15. def _iter_patterns(resolver: URLResolver, ns_path: str = ""):
  16. for entry in resolver.url_patterns:
  17. if isinstance(entry, URLResolver):
  18. new_ns = ns_path
  19. if entry.namespace:
  20. new_ns = f"{ns_path}:{entry.namespace}" if ns_path else entry.namespace
  21. yield from _iter_patterns(entry, new_ns)
  22. elif isinstance(entry, URLPattern):
  23. full_name = entry.name
  24. if full_name and ns_path:
  25. full_name = f"{ns_path}:{full_name}"
  26. yield entry, full_name
  27. def _find_pattern_by_breadcrumb_name(name: str) -> Tuple[Optional[URLPattern], Optional[str]]:
  28. resolver = get_resolver()
  29. for pattern, full_name in _iter_patterns(resolver):
  30. if not full_name:
  31. continue
  32. view = pattern.callback
  33. # unwrap to find metadata on inner functions
  34. meta = None
  35. cur = view
  36. while cur is not None and meta is None:
  37. meta = getattr(cur, "_breadcrumbs_meta", None)
  38. cur = getattr(cur, "__wrapped__", None)
  39. if meta and meta.get("name") == name:
  40. return pattern, full_name
  41. return None, None
  42. def _build_breadcrumb_chain(meta: Dict[str, Any]) -> List[Tuple[str, Optional[str]]]:
  43. chain: List[Tuple[str, Optional[str]]] = []
  44. # Walk parent chain (by name) outward
  45. parent_name = meta.get("parent")
  46. while parent_name:
  47. pattern, full_name = _find_pattern_by_breadcrumb_name(parent_name)
  48. if not pattern:
  49. break
  50. view = pattern.callback
  51. vmeta = None
  52. cur = view
  53. while cur is not None and vmeta is None:
  54. vmeta = getattr(cur, "_breadcrumbs_meta", None)
  55. cur = getattr(cur, "__wrapped__", None)
  56. vmeta = vmeta or {}
  57. label = vmeta.get("label") or parent_name
  58. # Try to reverse parent URL (skip if args required)
  59. url: Optional[str] = None
  60. if full_name:
  61. try:
  62. url = reverse(full_name)
  63. except NoReverseMatch:
  64. url = None
  65. chain.insert(0, (label, url))
  66. parent_name = vmeta.get("parent")
  67. # Add current at the end (no URL)
  68. chain.append((meta.get("label") or "", None))
  69. return chain
  70. def breadcrumbs(*, label: str, name: Optional[str] = None, parent: Optional[str] = None):
  71. """Decorator to attach breadcrumb metadata and expose a computed trail.
  72. Usage:
  73. @breadcrumbs(label="Posts", name="posts")
  74. def posts_list(...): ...
  75. @breadcrumbs(label="Post Edit", parent="posts")
  76. def post_edit(...): ...
  77. """
  78. def _decorator(view_func):
  79. meta = {"label": label, "name": name, "parent": parent}
  80. @wraps(view_func)
  81. def _wrapped(request, *args, **kwargs):
  82. # Expose breadcrumbs via request for templates/tags to consume
  83. request._breadcrumbs = _build_breadcrumb_chain(meta)
  84. return view_func(request, *args, **kwargs)
  85. # Make metadata discoverable on the resolved callback
  86. setattr(_wrapped, "_breadcrumbs_meta", meta)
  87. return _wrapped
  88. return _decorator
  89. @staff_member_required
  90. @breadcrumbs(label="Posts", name="posts")
  91. @public_route(label="CMS Posts", order=70, icon="document-text")
  92. def posts_list(request):
  93. if request.method == "POST":
  94. form = PostForm(request.POST, request.FILES)
  95. if form.is_valid():
  96. obj = form.save(commit=False)
  97. # Scope to current organization if available
  98. org = getattr(request, "org", None)
  99. if org is not None:
  100. obj.organization = org
  101. obj.save()
  102. messages.success(request, "Post created.")
  103. return redirect("cms:posts_list")
  104. else:
  105. messages.error(request, "Please correct the errors below.")
  106. else:
  107. form = PostForm()
  108. # Restrict category choices to current organization
  109. org = getattr(request, "org", None)
  110. if org is not None:
  111. try:
  112. form.fields["category"].queryset = PostCategory.objects.filter(organization=org)
  113. except Exception:
  114. pass
  115. qs = Post.objects.select_related("category", "author").all().order_by("-published_at", "-created_at")
  116. if org is not None:
  117. qs = qs.filter(organization=org)
  118. post_filter = PostFilter(request.GET, queryset=qs)
  119. # Also scope the filter's category queryset
  120. if org is not None:
  121. try:
  122. post_filter.form.fields["category"].queryset = PostCategory.objects.filter(organization=org)
  123. except Exception:
  124. pass
  125. paginator = Paginator(post_filter.qs, 10)
  126. page = request.GET.get("page")
  127. try:
  128. page_obj = paginator.page(page)
  129. except PageNotAnInteger:
  130. page_obj = paginator.page(1)
  131. except EmptyPage:
  132. page_obj = paginator.page(paginator.num_pages)
  133. context = {
  134. "nav": _nav_items(),
  135. "posts": page_obj.object_list,
  136. "page_obj": page_obj,
  137. "form": form,
  138. "filter": post_filter,
  139. }
  140. return render(request, "cms/posts_list.html", context)
  141. @staff_member_required
  142. @breadcrumbs(label="Post Edit", parent="posts")
  143. def post_edit(request, pk: int):
  144. org = getattr(request, "org", None)
  145. base_qs = Post.objects.all()
  146. if org is not None:
  147. base_qs = base_qs.filter(organization=org)
  148. item = get_object_or_404(base_qs, pk=pk)
  149. if request.method == "POST":
  150. form = PostForm(request.POST, request.FILES, instance=item)
  151. if form.is_valid():
  152. obj = form.save(commit=False)
  153. org = getattr(request, "org", None)
  154. if org is not None:
  155. obj.organization = org
  156. obj.save()
  157. messages.success(request, "Post updated.")
  158. return redirect("cms:post_edit", pk=item.pk)
  159. else:
  160. form = PostForm(instance=item)
  161. # Restrict category choices
  162. org = getattr(request, "org", None)
  163. if org is not None:
  164. try:
  165. form.fields["category"].queryset = PostCategory.objects.filter(organization=org)
  166. except Exception:
  167. pass
  168. context = {
  169. "nav": _nav_items(),
  170. "item": item,
  171. "form": form,
  172. "cancel_url": reverse("cms:posts_list"),
  173. }
  174. return render(request, "cms/post_form.html", context)
  175. @staff_member_required
  176. @require_POST
  177. def post_delete(request, pk: int):
  178. org = getattr(request, "org", None)
  179. base_qs = Post.objects.all()
  180. if org is not None:
  181. base_qs = base_qs.filter(organization=org)
  182. item = get_object_or_404(base_qs, pk=pk)
  183. item.delete()
  184. messages.success(request, "Post deleted.")
  185. return redirect("cms:posts_list")
  186. @staff_member_required
  187. @public_route(label="CMS Categories", order=75, icon="tag")
  188. def categories_list(request):
  189. if request.method == "POST":
  190. form = PostCategoryForm(request.POST)
  191. if form.is_valid():
  192. obj = form.save(commit=False)
  193. org = getattr(request, "org", None)
  194. if org is not None:
  195. obj.organization = org
  196. obj.save()
  197. messages.success(request, "Category created.")
  198. return redirect("cms:categories_list")
  199. else:
  200. messages.error(request, "Please correct the errors below.")
  201. else:
  202. form = PostCategoryForm()
  203. q = (request.GET.get("q") or "").strip()
  204. qs = PostCategory.objects.all().order_by("name")
  205. org = getattr(request, "org", None)
  206. if org is not None:
  207. qs = qs.filter(organization=org)
  208. if q:
  209. qs = qs.filter(name__icontains=q)
  210. paginator = Paginator(qs, 15)
  211. page = request.GET.get("page")
  212. try:
  213. page_obj = paginator.page(page)
  214. except PageNotAnInteger:
  215. page_obj = paginator.page(1)
  216. except EmptyPage:
  217. page_obj = paginator.page(paginator.num_pages)
  218. context = {
  219. "nav": _nav_items(),
  220. "categories": qs,
  221. "page_obj": page_obj,
  222. "form": form,
  223. "query": q,
  224. }
  225. return render(request, "cms/categories_list.html", context)
  226. @staff_member_required
  227. def category_edit(request, pk: int):
  228. org = getattr(request, "org", None)
  229. base_qs = PostCategory.objects.all()
  230. if org is not None:
  231. base_qs = base_qs.filter(organization=org)
  232. item = get_object_or_404(base_qs, pk=pk)
  233. if request.method == "POST":
  234. form = PostCategoryForm(request.POST, instance=item)
  235. if form.is_valid():
  236. obj = form.save(commit=False)
  237. org = getattr(request, "org", None)
  238. if org is not None:
  239. obj.organization = org
  240. obj.save()
  241. messages.success(request, "Category updated.")
  242. return redirect("cms:category_edit", pk=item.pk)
  243. else:
  244. form = PostCategoryForm(instance=item)
  245. # Restrict parent choices
  246. org = getattr(request, "org", None)
  247. if org is not None:
  248. try:
  249. form.fields["parent"].queryset = PostCategory.objects.filter(organization=org)
  250. except Exception:
  251. pass
  252. context = {
  253. "nav": _nav_items(),
  254. "item": item,
  255. "form": form,
  256. "cancel_url": reverse("cms:categories_list"),
  257. }
  258. return render(request, "cms/category_form.html", context)
  259. @staff_member_required
  260. @require_POST
  261. def category_delete(request, pk: int):
  262. org = getattr(request, "org", None)
  263. base_qs = PostCategory.objects.all()
  264. if org is not None:
  265. base_qs = base_qs.filter(organization=org)
  266. item = get_object_or_404(base_qs, pk=pk)
  267. item.delete()
  268. messages.success(request, "Category deleted.")
  269. return redirect("cms:categories_list")