Source code for feincms3.root.middleware

"""
Page middleware (``feincms3.root.middleware``)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The guide recommends using a middleware for the feincms3 pages app. This module
offers helpers and utilities to reduce the amount of code you have to write.
The reason why this module is called ``root`` is that the page app's mountpoint
has to be the Python app's mountpoint when using this since the middleware uses
the value of ``request.path_info`` to find a page for the current request.

If you have other requirements you may want to write your own
:ref:`urls-and-views` or investigate if modifying ``request.path_info`` works;
the latter isn't recommended though since it's not officially supported by
Django.

Example code for using this module (e.g. ``app.pages.middleware``):

.. code-block:: python

    from django.shortcuts import render
    from feincms3.root.middleware import add_redirect_handler, create_page_if_404_middleware

    from app.pages.models import Page
    from app.pages.utils import page_context

    # The page handler receives the request and the page.
    # ``add_redirect_handler`` wraps the handler function with support for the
    # RedirectMixin.
    @add_redirect_handler
    def handler(request, page):
        return render(request, page.type.template_name, page_context(request, page=page))

    # This is the middleware which you want to add to ``MIDDLEWARE`` as
    # ``app.pages.middleware.page_if_404_middleware``. The middleware should be
    # added in the last position except if you have a very good reason not to
    # do this.
    page_if_404_middleware = create_page_if_404_middleware(
        # queryset=Page.objects.active() works too (if .active() doesn't use
        # get_language or anything similar)
        queryset=lambda request: Page.objects.active(),

        handler=handler,
    )

Building a preview functionality
--------------------------------

If you wish that staff users can view a page which isn't active yet, here's how
to do that. It's basically the same as above except for the fact that the
queryset function depends on the value of ``request.user.is_staff``.

If you're using feincms3 applications, it may happen that you want to preview
an application site. This doesn't work without overriding the list of apps
passed to :func:`~feincms3.applications.apps_urlconf`. The easier way to check
if the preview should be rendered is to use the fact that most feincms3
application page types won't have ``template_name`` set. Returning a 404 error
in this case is a good alternative to crashing with an ``IsADirectoryError``
because the Django templates backend tries to load the ``templates`` directory
itself instead of a template file.

.. code-block:: python

    def pages(request):
        if request.user.is_staff:
            return Page.objects.all()
        return Page.objects.active()

    @add_redirect_handler
    def handler(request, page):
        if not page.type.template_name:
            raise Http404(
                f"The page type {page.type.key!r} doesn't have a template name."
            )
        return render(
            request,
            page.type.template_name,
            page_context(request, page=page),
        )

    page_if_404_middleware = create_page_if_404_middleware(
        queryset=pages,
        handler=handler,
    )
"""

from functools import wraps

from django.conf import settings
from django.http import (
    HttpResponseNotFound,
    HttpResponsePermanentRedirect,
    HttpResponseRedirect,
)


[docs] class UseRootMiddlewareResponse(HttpResponseNotFound): """ Used by feincms3.root.passthru to tell the middleware to do its thing You can return this response from your own views as well if some view cannot handle a request and you want to allow the root middleware to try handling the current request. If you just raise ``Http404`` or return a stock ``HttpResponseNotFound`` response the root middleware will do nothing. As an example, of ``get_object_or_404(Thing, pk=pk)`` you could do the following if you want to fall back to the root middleware: .. code-block:: python from feincms3.root.middleware import UseRootMiddlewareResponse def view(request, pk): if thing := Thing.objects.filter(pk=pk).first(): return render(...) return UseRootMiddlewareResponse() This makes the root middleware check the pages for a match, and still return the 404 if no page could be found. """
[docs] def create_page_if_404_middleware(*, queryset, handler, language_code_redirect=False): """ Create a middleware for handling pages This utility is there for your convenience, you do not have to use it. The returned middleware already handles returning non-404 responses as-is, fetching a page instance from the database and calling a user-defined handler on success. It optionally also supports redirecting requests to the root of the app to a language-specific landing page. Required arguments: - ``queryset``: A page queryset or a callable accepting the request and returning a page queryset. - ``handler``: A callable accepting the request and a page and returning a response. Optional arguments: - ``language_code_redirect`` (``False``): Redirect visitor to the language code prefix (e.g. ``/en/``, ``/de-ch/``) if request path equals the script prefix (generally ``/``), no active page for ``/`` exists and the prefixed version exists. """ def outer(get_response): def inner(request): response = get_response(request) if response.status_code != 404 or ( request.resolver_match and not isinstance(response, UseRootMiddlewareResponse) ): # Response is not a 404 OR the 404 comes from a resolved view # which also didn't return a UseRootMiddlewareResponse. return response qs = queryset(request) if callable(queryset) else queryset._clone() if page := qs.filter(path=request.path_info).first(): return handler(request, page) if language_code_redirect and request.path_info == "/": target = f"/{request.LANGUAGE_CODE}/" if qs.filter(path=target).exists(): return HttpResponseRedirect(target) if settings.APPEND_SLASH and not request.path_info.endswith("/"): target = request.path_info + "/" if qs.filter(path=target).exists(): return HttpResponsePermanentRedirect(target) return response return inner return outer
[docs] def add_redirect_handler(handler): """ Wrap the page handler in a redirect mixin handler """ @wraps(handler) def inner(request, page): if redirect_to := page.get_redirect_url(): return HttpResponseRedirect(redirect_to) return handler(request, page) return inner