Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow use with non-Page models #43

Open
dustinblanchard opened this issue Sep 14, 2022 · 4 comments
Open

Allow use with non-Page models #43

dustinblanchard opened this issue Sep 14, 2022 · 4 comments

Comments

@dustinblanchard
Copy link

Our application has a mix of Page and other types of models. Could we add an additional Mixin that is not dependent on Page that allows us to manually specify which fields to use?

DylannCordel added a commit to DylannCordel/wagtail-seo that referenced this issue Jul 16, 2024
@DylannCordel
Copy link

Code available here: #67

DylannCordel added a commit to DylannCordel/wagtail-seo that referenced this issue Jul 16, 2024
@vsalvino
Copy link
Contributor

This is kind of a controversial change - it goes against wagtail's philosophy by using snippets as a "page". On one hand, I do not want to encourage this kind of practice as it creates unmaintainable code and goes against best practices. On the other hand, I understand that sometimes you are stuck in a situation (legacy code, etc.) where you need to make it work.

Can you all provide more context as to why you need/want this change? Having some case studies / use-cases would be really good to know about. Generally speaking we get zero feedback about how people are using this package, despite having thousands of pip installs!

I'll take a look at the pull request, but please note this may take some time as there are a couple other major refactors in progress that also need to be reviewed.

@DylannCordel
Copy link

DylannCordel commented Jul 18, 2024

Hi @vsalvino

Thanks for your reply. I understand your reluctance. I'll try to explain why we often use our own models for some « Detail » page and how we code it:

  • we (Webu) build technical website where the CMS part is a small visible part of the iceberg : the main part of the code is via some specific interfaces where the daily tasks of main users are our main focus. So, our code is mainly written as « Django code », not « Wagtail code ».
  • As a direct cause of the previous point, wagtail is just another brick in the wall and we want to be able to change it if needed (we used django-cms before, with the same approach, and I'm so glad of this code of conduct today because we are migrating some big websites from django-cms to wagtail : this migration is simpler due to this choice to keep code as « Django Code » and not « Django CMS code »)
  • Due to the first point too, data architecture is important and we want models which are independent (I mean, which don't require a OneToOne to get their own information of title or status for eg to avoid more complex queries)
  • the choice of how I18n is coded in wagtail is not (IMHO) a good choice: django-parler approach is a better one (still IMHO) : using our own models allow us to avoid two translations behaviour for a same model (one for the "page" part, and one for the "specific part"). I code a module to be able to use django-parler into wagtail admin : wagtail-parler
  • Wagtail allow us to have class Page which can handle sub-urls, so for me, it's also in the wagtail philosophy to have "final page" which are not managed by wagtail. I used this approach to have some mixins which allow a wagtail Page to serve standard django-views as suburls. Here are the code used to do it:
# extract from webu_wagtail/models/pages.py

from copy import deepcopy
from typing import Callable
from typing import Tuple
from typing import Type

from django.http import HttpRequest
from django.http import HttpResponse
from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as _
from django.views import View

from wagtail.contrib.routable_page.models import RoutablePageMixin
from wagtail.contrib.routable_page.models import re_path
from wagtail.coreutils import camelcase_to_underscore
from wagtail.models import Page
from wagtail.models import PageBase

__all__ = ["BasePage"]


def build_serve_sub_view(name) -> Callable:
    """
    Attention, cette fonction doit rester à l'extérieur du __new__ de MultiViewPageBase
    sinon pour une raison obscure de construction dynamique de fonction et de référence,
    "name" vaudra toujours le dernier de la boucle for.
    Alors qu'en gardant un appel externe, le name est résolu à l'appel et reste bien
    inchangé lorsque "serve_sub_view" sera réellement appelé.
    """

    def serve_sub_view(self, request: HttpRequest, **kwargs) -> HttpResponse:
        return self._serve_specific_view(request, name, **kwargs)

    serve_sub_view.__name__ = f"serve_{name}_view"
    return serve_sub_view


class MultiViewPageBase(PageBase):
    """
    Fabrication automatique des méthodes et cie pour faciliter l'utilisation
    conjointe de page Wagtail et de vue django grace à la clase `ViewsMeta`
    ex d'utilisation:

        class BlogPage(BasePage):
            class ViewsMeta:
                urls = {
                    "article_list": {
                            "path": "",
                            "class": ArticleListView,
                            "init_kwargs": {"some_arg": "bar"},
                        }
                    ],
                    "article_detail": {
                        "path": r"^(?P<slug>[\w-]+)/$",
                        "class": "myapp.views.ArticleDetailView",
                    }
                }
    """

    def __new__(cls, cls_name:str, cls_bases, cls_attrs):
        tmp_views_meta = cls_attrs.pop("ViewsMeta", None)
        views_meta_kwargs = {
            "classes": {},
            "classes_initkwargs": {},
        }
        cls_attrs["_views_meta"] = type("ViewsMeta", (), views_meta_kwargs)
        sub_views = getattr(tmp_views_meta, "sub_views", None) if tmp_views_meta else None
        if not sub_views:
            return super().__new__(cls, cls_name, cls_bases, cls_attrs)

        for name, view_conf in sub_views.items():
            assert (
                name.isidentifier() and name == name.lower()
            ), f"`{name}` n'est pas un nom valide pour pouvoir générer une méthode"

            method_name = f"serve_{name}_view"
            if method_name in cls_attrs:
                continue

            pattern = view_conf.get("path", None)
            # assert issubclass(
            #     view_conf["class"], WagtailPageViewMixin
            # ), f"{view_conf["class"]} must extend WagtailPageViewMixin"
            views_meta_kwargs["classes"][name] = view_conf["class"]
            serve_sub_view = build_serve_sub_view(name)
            cls_attrs[method_name] = re_path(pattern=pattern, name=name)(serve_sub_view)
            _initkwargs = view_conf.get("classes_initkwargs")
            if _initkwargs:
                views_meta_kwargs["classes_initkwargs"][name] = _initkwargs
        return super().__new__(cls, cls_name, cls_bases, cls_attrs)


class BasePage(RoutablePageMixin, Page, metaclass=MultiViewPageBase):
    """
    Mixin permettant de lier une vue à une page wagtail pour que ce soit lui qui gère les URLs
    """

    class Meta:
        abstract = True

    def get_template(self, request: HttpRequest, *args, **kwargs) -> str:
        name = self.__class__.__name__
        if name.endswith("Page"):
            name = name[0:-4]
        name = camelcase_to_underscore(name)
        if request.headers.get("x-requested-with") == "XMLHttpRequest":
            name += ".ajax"
        return "%s/pages/%s.html" % (self._meta.app_label, name)

    def get_view_class(self, name:str="default") -> View:
        view_cls = self._views_meta.classes.get(name, None)
        if isinstance(view_cls, str):
            view_cls = import_string(view_cls)
        return view_cls

    def get_view_class_initkwargs(self, name:str="default", **kwargs) -> dict:
        base_kwargs = deepcopy(self._views_meta.classes_initkwargs.get(name, None) or {})
        base_kwargs.update(kwargs)
        return base_kwargs

    def get_view_func(self, name:str="default") -> Callable:
        view_class = self.get_view_class(name)
        return view_class.as_view(**(self.get_view_class_initkwargs(name)))

    def _serve_specific_view(self, request: HttpRequest, name:str, **kwargs) -> HttpResponse:
        view_class = self.get_view_class(name)
        if not view_class:
            return super().serve(request)
        request.is_preview = getattr(request, "is_preview", False)
        wagtail_context = self.get_context(request)
        view = self.get_view_func(name)
        return view(request, wagtail_context=wagtail_context, **kwargs)
# webu_wagtail/views/_bases.py

class WagtailPageViewMixin:
    """
    Permet d'utiliser une page wagtail comme une vue django
    """

    context_object_name = "page"

    def get_template_names(self):
        return [self.wagtail_context["page"].get_template(self.request)]

    def setup(self, request, *args, **kwargs):
        self.wagtail_context = kwargs.pop("wagtail_context", {})
        return super().setup(request, *args, **kwargs)

    def get_context_data(self, **context):
        context = {**self.wagtail_context, **context}
        context.pop("self", None)
        return super().get_context_data(**context)

Now, we can use our own "pure django" ListView and DetailView but keeping the strong of wagtail: display it under a page tree, allowing the website editor to move it if he wants.

Here is an exemple of models used on tracedirecte.com, a travel agency:

  • td_agencies.agency
  • td_agencies.agencydestinationtext
  • td_blogs.blogarticle
  • td_blogs.blogcategory
  • td_communications.autonotification
  • td_communications.message
  • td_communications.messagetemplate
  • td_communications.reminder
  • td_contents.experience
  • td_contents.experiencecategory
  • td_contents.mainfaq
  • td_contents.mainfaqcategory
  • td_contents.meetlocalpeople
  • td_contents.travelguide
  • td_contents.travelguidearticle
  • td_contracts.administrativedoc
  • td_contracts.insurance
  • td_geo.continent
  • td_geo.continentfaq
  • td_geo.continentfaqcategory
  • td_geo.destination
  • td_geo.destinationrtogfaq
  • td_geo.destinationrtogfaqcategory
  • td_geo.destinationseofaq
  • td_geo.destinationseofaqcategory
  • td_medias.diaporamaimage
  • td_medias.messagedocument
  • td_medias.protecteddocument
  • td_medias.publicdocument
  • td_medias.travelparticipantdocument
  • td_medias.wagtailimage
  • td_medias.wagtailrendition
  • td_orders.accountingline
  • td_orders.convocation
  • td_orders.registration
  • td_orders.travel
  • td_orders.travelparticipant
  • td_payments.transaction
  • td_products.interestpoint
  • td_products.theme
  • td_products.themefaq
  • td_products.themefaqcategory
  • td_products.tour
  • td_products.tourpage
  • td_products.tourpagefaq
  • td_products.tourpagefaqcategory
  • td_products.tourscheduledstep
  • td_products.tourscheduledstepinterestpoint
  • td_reviews.review
  • td_reviews.reviewmessage
  • td_stats.report
  • td_stats.travellifeforgoogledata
  • td_users.advisor
  • td_users.authtoken
  • td_users.contactbloctext
  • td_users.customer
  • td_users.profile
  • td_users.scout
  • td_users.user
  • tdfront.apropospage: is real wagtail Page subclass
  • tdfront.contactformpage: is real wagtail Page subclass
  • tdfront.formpage: is real wagtail Page subclass
  • tdfront.homepage: is real wagtail Page subclass
  • tdfront.requiredstandardpage: is real wagtail Page subclass
  • tdfront.standardpage: is real wagtail Page subclass
  • tdfront.storypage: is real wagtail Page subclass
  • tdfront.teampage: is real wagtail Page subclass
  • tdfront.travelguidespage: is real wagtail Page subclass

As you can see, only the tdfront module is the "standard cms" part of this project. But there are some other Models wich have Detail page (for eg, a Destination, an Agency, a TravelGuide...), but there are also some specific logic behind those models (which are not front related). Furthermore, we have custom logic of display, permissions, etc. and have a really small part of "cms" (for eg. a Destination has a StreamField for it's content).

That's why we only use wagtail Page for real front Pages without specific "backend" behaviors :). I hope it's not so fuzzy as explanation :-D

@vsalvino
Copy link
Contributor

Thanks for the explanation, that is really good detail to know. I will also pass this on to the Wagtail core team :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants