Guide visibility is now scoped by PORTAL first and role second: - the buyer portal lists shared and buyer guides, even for an admin, because it is the buyer portal; admin guides never surface there - the ops portal lists shared and ops guides, plus admin guides for an admin/superuser - the detail view names its context object, so guide pages get a real browser title instead of a blank one Link rewriting, so one page can serve both the wiki and the in-app help centre: - absolute /en/user-guides/... links become the in-app help path - other absolute /en/... links point at the wiki itself; the app hosts serve no /en/ routes, so leaving them untouched was a 404 for whoever clicked - relative links keep their existing behaviour Tests cover the new rewriting and the portal scoping rules. Also adds docs/USER_GUIDES_VIEWER_SPEC.md, the spec this viewer was built to.
66 lines
2.4 KiB
Python
66 lines
2.4 KiB
Python
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
from django.http import Http404
|
|
from django.shortcuts import get_object_or_404
|
|
from django.views.generic import DetailView, ListView
|
|
|
|
from .markdown import render_markdown
|
|
from .models import GuidePage, allowed_audiences
|
|
|
|
|
|
class GuideAccessMixin:
|
|
"""Scope the help centre by PORTAL first, then by role.
|
|
|
|
Being an admin does not mean seeing every guide in every portal: the buyer
|
|
portal lists buyer guides (the audience of its readers), the ops portal
|
|
lists ops guides, and the admin guides only appear in the ops portal for
|
|
admin/superuser accounts. See apps.guides.models.allowed_audiences.
|
|
"""
|
|
|
|
def get_portal(self):
|
|
return getattr(self.request.resolver_match, "namespace", None) or "portal"
|
|
|
|
def get_allowed_audiences(self):
|
|
user = self.request.user
|
|
return allowed_audiences(
|
|
self.get_portal(),
|
|
role=getattr(user, "role", None),
|
|
is_superuser=user.is_superuser,
|
|
)
|
|
|
|
def get_help_root(self):
|
|
return f"/{self.get_portal()}/help/"
|
|
|
|
|
|
class GuideIndexView(LoginRequiredMixin, GuideAccessMixin, ListView):
|
|
model = GuidePage
|
|
context_object_name = "guides"
|
|
|
|
def get_queryset(self):
|
|
return GuidePage.objects.filter(audience__in=self.get_allowed_audiences())
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
ctx["help_root"] = self.get_help_root()
|
|
return ctx
|
|
|
|
|
|
class GuideDetailView(LoginRequiredMixin, GuideAccessMixin, DetailView):
|
|
model = GuidePage
|
|
# the templates read {{ guide.title }}; without this the object arrives as
|
|
# "object" and every guide detail page renders a blank browser title
|
|
context_object_name = "guide"
|
|
|
|
def get_object(self, queryset=None):
|
|
path = f"user-guides/{self.kwargs['page_path']}"
|
|
page = get_object_or_404(GuidePage, path=path)
|
|
if page.audience not in self.get_allowed_audiences():
|
|
raise Http404("This guide is not available in this portal.")
|
|
return page
|
|
|
|
def get_context_data(self, **kwargs):
|
|
ctx = super().get_context_data(**kwargs)
|
|
help_root = self.get_help_root()
|
|
ctx["help_root"] = help_root
|
|
ctx["guide_html"] = render_markdown(self.object.content, self.object.path, help_root)
|
|
ctx["guides"] = GuidePage.objects.filter(audience__in=self.get_allowed_audiences())
|
|
return ctx
|