class ApplicationType(Type):
    """
    Application page type

    Example usage:

    .. code-block:: python

        from feincms3.applications import PageTypeMixin, TemplateType
        from feincms3.pages import AbstractPage
        from content_editor.models import Region

        class Page(AbstractPage, PageTypeMixin)
            TYPES = [
                TemplateType(
                    # Required arguments
                    key="standard",
                    title="Standard page",
                    urlconf="path.to.urlconf.module",

                    # Optional arguments
                    template_name="pages/standard.html",
                    regions=[
                        Region(key="main", title="Main"),
                    ],
                    app_namespace=lambda page: ...,

                    # You may pass other arguments here, they will be available
                    # on ``page.type`` as-is.
                ),
            ]
    """

    _REQUIRED = {"key", "title", "urlconf", "app_namespace"}

    def __init__(self, **kwargs):
        kwargs.setdefault("template_name", "")
        kwargs.setdefault("regions", [])
        kwargs.setdefault("app_namespace", lambda instance: instance.page_type)
        super().__init__(**kwargs) class PageTypeMixin(models.Model):
    """
    The page class should inherit this mixin. It adds a ``page_type`` field
    containing the selected page type, and an ``app_namespace`` field which
    contains the instance namespace of the application, if the type of the page
    is an application type. The field is empty e.g. for template page types.
    Note that currently the :class:`~feincms3.mixins.LanguageMixin` is a
    required dependency of :mod:`feincms3.applications`.

    ``TYPES`` contains a list of page type instances, either
    :class:`~feincms3.applications.TemplateType` or
    :class:`~feincms3.applications.ApplicationType` and maybe others in the
    future. The configuration values are specific to each type, common to all
    of them are a key (stored in the ``page_type`` field) and a user-visible
    title.

    Template types additionally require a ``template_name`` and a ``regions``
    value.

    Application types require a ``urlconf`` value and support the following
    options:

    - ``urlconf``: The path to the URLconf module for the application. Besides
      the ``urlpatterns`` list the module should probably also specify a
      ``app_name``.
    - ``required_fields``: A list of page class fields which must be non-empty
      for the application to work. The values are checked in
      ``PageTypeMixin.clean_fields``.
    - ``app_namespace``: A callable which receives the page instance
      as its only argument and returns a string suitable for use as an
      instance namespace.

    Usage::

        from content_editor.models import Region
        from django.utils.translation import gettext_lazy as _
        from feincms3.applications import PageTypeMixin
        from feincms3.mixins import LanguageMixin
        from feincms3.pages import AbstractPage

        class Page(AbstractPage, PageTypeMixin, LanguageMixin):
            TYPES = [
                # It is recommended to always put a TemplateType type first
                # because it will be the default type:
                TemplateType(
                    key="standard",
                    title=_("Standard"),
                    template_name="pages/standard.html",
                    regions=[Region(key="main", title=_("Main"))],
                ),
                ApplicationType(
                    key="publications",
                    title=_("publications"),
                    urlconf="app.articles.urls",
                ),
                ApplicationType(
                    key="blog",
                    title=_("blog"),
                    urlconf="app.articles.urls",
                ),
                ApplicationType(
                    key="contact",
                    title=_("contact form"),
                    urlconf="app.forms.contact_urls",
                ),
                ApplicationType(
                    key="teams",
                    title=_("teams"),
                    urlconf="app.teams.urls",
                    app_namespace=lambda page: f"{page.page_type}-{page.team_id}",
                    required_fields=["team"],
                ),
            ]
    """

    #: Override this to set a different name for the outer namespace.
    LANGUAGE_CODES_NAMESPACE = "apps"

    page_type = ChoicesCharField(_("page type"), max_length=100)
    app_namespace = models.CharField(
        ("app instance namespace"), max_length=100, blank=True, editable=False
    )

    class Meta:
        abstract = True

    @property
    def type(self):
        """
        Returns the appropriate page type instance, either the selected type or
        the first type in the list of ``TYPES`` if no type is selected or if
        the type does not exist anymore.
        """
        return self.TYPES_DICT.get(self.page_type, self.TYPES[0])

    @property
    def regions(self):
        return self.type.regions

    def save(self, *args, **kwargs):
        """
        Updates ``app_namespace``.
        """
        self.app_namespace = self.type.app_namespace(self)
        super().save(*args, **kwargs)

    save.alters_data = True

    def clean_fields(self, exclude=None):
        """
        Checks that required fields are given and that an app namespace only
        exists once per site and language.
        """
        exclude = [] if exclude is None else exclude
        super().clean_fields(exclude)

        type = self.type
        if type and type.get("required_fields"):
            missing = [
                field for field in type["required_fields"] if not getattr(self, field)
            ]
            if missing:
                error = _('This field is required for the page type "%s".') % (
                    self.get_page_type_display(),
                )
                errors = {}
                for field in missing:
                    if field in exclude:
                        errors.setdefault("__all__", []).append(f"{field}: {error}")
                    else:
                        errors[field] = error

                raise ValidationError(errors)

        if type and type.app_namespace(self):
            if self.__class__._default_manager.filter(
                Q(app_namespace=type.app_namespace(self)),
                Q(language_code=self.language_code),
                ~Q(pk=self.pk),
            ).exists():
                fields = ["__all__", "page_type"]
                fields.extend(type.get("required_fields", ()))
                raise ValidationError(
                    {
                        field: _("This exact app already exists.")
                        for field in fields
                        if field not in exclude
                    }
                )

    @staticmethod
    def fill_page_type_choices(sender, **kwargs):
        """
        Fills in the choices for ``page_type`` from the ``TYPES``
        class variable. This method is a receiver of Django's
        ``class_prepared`` signal.
        """
        if issubclass(sender, PageTypeMixin) and not sender._meta.abstract:
            field = sender._meta.get_field("page_type")
            field.choices = [(app.key, app.title) for app in sender.TYPES]
            field.default = sender.TYPES[0].key
            sender.TYPES_DICT = {app.key: app for app in sender.TYPES}
            global _APPS_MODEL
            _APPS_MODEL = sender

    @classmethod
    def check(cls, **kwargs):
        errors = super().check(**kwargs)
        errors.extend(cls._check_feincms3_appsmixin_templatemixin_clash(**kwargs))
        errors.extend(cls._check_feincms3_applications(**kwargs))
        return errors

    @classmethod
    def _check_feincms3_appsmixin_templatemixin_clash(cls, **kwargs):
        from feincms3.mixins import TemplateMixin

        if not cls._meta.abstract and issubclass(cls, TemplateMixin):
            return [
                Warning(
                    f"The model {cls._meta.label} extends both"
                    " PageTypeMixin and TemplateMixin. The new PageTypeMixin includes"
                    " the functionality of the TemplateMixin, please remove"
                    " the latter, fill in ``page_type`` fields either from"
                    " ``application`` (if non-empty) or from ``template_key``,"
                    " and rename ``app_instance_namespace`` to ``app_namespace``.",
                    obj=cls,
                    id="feincms3.W002",
                )
            ]
        return []

    @classmethod
    def _check_feincms3_applications(cls, **kwargs):
        for type in cls.TYPES:
            if isinstance(type, ApplicationType):
                try:
                    module = import_module(type.urlconf)
                except ModuleNotFoundError as exc:
                    yield Error(
                        f"The application type {type.key!r} has an unimportable"
                        f" URLconf value {type.urlconf!r}: {exc}",
                        obj=cls,
                        id="feincms3.E003",
                    )
                else:
                    app_name = getattr(module, "app_name", None)
                    if type.key != app_name and not getattr(
                        module, "ignore_app_name_mismatch", False
                    ):
                        yield Info(
                            f"The URLconf module of the application type {type.key!r}"
                            f" has app_name = {app_name!r} which doesn't match"
                            " the application key.",
                            obj=cls,
                            id="feincms3.I004",
                            hint=(
                                "Silence this warning by adding"
                                " 'ignore_app_name_mismatch = True'"
                                f" to the {type.urlconf!r} module if this is expected."
                            ),
                        ) class TemplateType(Type):
    """
    Template page type

    Example usage:

    .. code-block:: python

        from feincms3.applications import PageTypeMixin, TemplateType
        from feincms3.pages import AbstractPage
        from content_editor.models import Region

        class Page(AbstractPage, PageTypeMixin)
            TYPES = [
                TemplateType(
                    # Required arguments
                    key="standard",
                    title="Standard page",
                    template_name="pages/standard.html",
                    regions=[
                        Region(key="main", title="Main"),
                    ],

                    # You may pass other arguments here, they will be available
                    # on ``page.type`` as-is.
                ),
            ]
    """

    _REQUIRED = {"key", "title", "template_name", "regions", "app_namespace"}

    def __init__(self, **kwargs):
        # Always setting app_namespace makes it easier for PageTypeMixin.save
        kwargs.setdefault("app_namespace", lambda instance: "")
        super().__init__(**kwargs) def reverse_app(namespaces, viewname, *args, languages=None, **kwargs):
    """
    Reverse app URLs, preferring the active language.

    ``reverse_app`` first generates a list of viewnames and passes them on
    to ``reverse_any``.

    Assuming that we're trying to reverse the URL of an article detail view,
    that the project is configured with german, english and french as available
    languages, french as active language and that the current article is a
    publication, the viewnames are:

    - ``apps-fr.publications.article-detail``
    - ``apps-fr.articles.article-detail``
    - ``apps-de.publications.article-detail``
    - ``apps-de.articles.article-detail``
    - ``apps-en.publications.article-detail``
    - ``apps-en.articles.article-detail``

    reverse_app tries harder returning an URL in the correct language than
    returning an URL for the correct instance namespace. The ``fallback``
    keyword argument is supported too.

    Example:

    .. code-block:: python

        url = reverse_app(
            ("category-1", "blog"),
            "post-detail",
            kwargs={"year": 2016, "slug": "my-cat"},
        )
    """

    if languages is None:
        current = get_language()
        languages = sorted(
            (row[0] for row in settings.LANGUAGES), key=lambda lang: lang != current
        )
    viewnames = [
        ":".join(r)
        for r in itertools.product(
            (
                f"{_APPS_MODEL.LANGUAGE_CODES_NAMESPACE}-{language}"
                for language in languages
            ),
            (namespaces if isinstance(namespaces, (list, tuple)) else (namespaces,)),
            (viewname,),
        )
    ]
    return reverse_any(viewnames, *args, **kwargs) class LanguageAndTranslationOfMixin(LanguageMixin):
    """
    This object not only has a language, it may also be a translation of
    another object.
    """

    translation_of = TreeNodeForeignKey(
        "self",
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        related_name="+",
        verbose_name=_("translation of"),
        limit_choices_to={"language_code": settings.LANGUAGES[0][0]},
    )

    class Meta:
        abstract = True

    @classmethod
    def check(cls, **kwargs):
        errors = super().check(**kwargs)
        errors.extend(cls._check_feincms3_language_and_translation_of_mixin(**kwargs))
        return errors

    @classmethod
    def _check_feincms3_language_and_translation_of_mixin(cls, **kwargs):
        unique_together = [set(fields) for fields in cls._meta.unique_together]
        if {"language_code", "translation_of"} not in unique_together:
            yield Warning(
                "Models using the LanguageAndTranslationOfMixin should ensure"
                " that only one translation can be added per language.",
                obj=cls,
                id="feincms3.W003",
                hint='Add ("language_code", "translation_of") to unique_together.',
            )

    def translations(self):
        """
        Return a queryset containing all translations of this object

        This method can be called on any object if translations have been
        defined at all, you do not have to fetch the object in the primary
        language first.
        """

        primary = (
            self.pk
            if self.language_code == settings.LANGUAGES[0][0]
            else self.translation_of_id
        )
        queryset = self.__class__._default_manager
        return (
            queryset.filter(Q(id=primary) | Q(translation_of=primary))
            if primary
            else queryset.none()
        )

    def clean_fields(self, exclude=None):
        """
        Implement the following validation rules:

        - Objects in the primary language cannot be the translation of another object
        - Objects in other languages can only reference objects in the primary
          language (this is automatically verified by Django because we're
          using ``limit_choices_to``)
        """

        super().clean_fields(exclude)

        if self.language_code == settings.LANGUAGES[0][0] and self.translation_of:
            raise validation_error(
                _(
                    "Objects in the primary language cannot be"
                    " the translation of another object."
                ),
                field="translation_of",
                exclude=exclude,
            ) class MenuMixin(models.Model):
    """
    The ``MenuMixin`` is most useful on pages where there are menus with
    differing content on a single page, for example the main navigation
    and a meta navigation (containing contact, imprint etc.)
    """

    menu = ChoicesCharField(_("menu"), max_length=100, blank=True)

    class Meta:
        abstract = True

    @staticmethod
    def fill_menu_choices(sender, **kwargs):
        """
        Fills in the choices for ``menu`` from the ``MENUS`` class variable.
        This method is a receiver of Django's ``class_prepared`` signal.
        """
        if issubclass(sender, MenuMixin) and not sender._meta.abstract:
            field = sender._meta.get_field("menu")
            field.choices = sender.MENUS
            field.default = field.choices[0][0]

    @classmethod
    def check(cls, **kwargs):
        errors = super().check(**kwargs)
        errors.extend(cls._check_feincms3_menu_mixin(**kwargs))
        return errors

    @classmethod
    def _check_feincms3_menu_mixin(cls, **kwargs):
        if invalid := [
            value
            for value, label in cls.MENUS
            if value and (value.startswith("_") or not value.isidentifier())
        ]:
            yield Warning(
                "MenuMixin menus should only use valid public Python identifiers"
                f" as keys. Invalid: {sorted(invalid)!r}.",
                obj=cls,
                id="feincms3.W005",
            ) class RedirectMixin(models.Model):
    """
    The ``RedirectMixin`` allows adding redirects in the page tree.
    """

    redirect_to_url = models.CharField(
        _("Redirect to URL"), max_length=1000, blank=True
    )
    redirect_to_page = TreeNodeForeignKey(
        "self",
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        related_name="+",
        verbose_name=_("Redirect to page"),
    )

    class Meta:
        abstract = True

    def get_redirect_url(self):
        """
        Return the URL for the redirect, if a redirect is configured
        """
        if self.redirect_to_url:
            return self.redirect_to_url
        elif self.redirect_to_page:
            return self.redirect_to_page.get_absolute_url()

    def clean_fields(self, exclude=None):
        """
        Ensure that redirects are configured properly.
        """
        super().clean_fields(exclude)

        if self.redirect_to_url and self.redirect_to_page_id:
            raise validation_error(
                _("Only set one redirect value."),
                field="redirect_to_url",
                exclude=exclude,
            )

        if self.redirect_to_page_id:
            if self.redirect_to_page_id == self.pk:
                raise validation_error(
                    _("Cannot redirect to self."),
                    field="redirect_to_page",
                    exclude=exclude,
                )

            if (
                self.redirect_to_page.redirect_to_url
                or self.redirect_to_page.redirect_to_page
            ):
                raise validation_error(
                    _(
                        'Do not chain redirects. The selected page "%(title)s"'
                        ' redirects to "%(path)s".'
                    )
                    % {
                        "title": self.redirect_to_page,
                        "path": self.redirect_to_page.get_redirect_url(),
                    },
                    field="redirect_to_page",
                    exclude=exclude,
                )

        if self.pk and (self.redirect_to_url or self.redirect_to_page_id):
            # Any page redirects to this page?
            other = self.__class__._default_manager.filter(redirect_to_page=self)
            if other:
                raise validation_error(
                    _(
                        "Do not chain redirects. The following pages already"
                        " redirect to this page: %(pages)s"
                    )
                    % {
                        "pages": ", ".join(
                            f"{page} ({page.get_absolute_url()})" for page in other
                        )
                    },
                    field="redirect_to_page",
                    exclude=exclude,
                ) class AbstractPage(TreeNode):
    """
    Short version: If you want to build a CMS with a hierarchical page
    structure, use this base class.

    It comes with the following fields:

    - ``parent``: (a nullable tree foreign key) and a ``position`` field for
      relatively ordering pages. While it is technically possible for
      ``position`` to be 0, e.g., data bulk imported from another CMS, it is not
      recommended, as the ``save()`` method will override values of 0 if you
      manipulate pages using the ORM.
    - ``is_active``: Boolean field. The ``save()`` method ensures that inactive
      pages never have any active descendants.
    - ``title`` and ``slug``
    - ``path``: The complete path to the page, starting and ending with a
      slash. The maximum length of path (1000) should be enough for everyone
      (tm, famous last words). This field also has a unique index, which means
      that MySQL with its low limit on unique indexes will not work with this
      base class. Sorry.
    - ``static_path``: A boolean which, when ``True``, allows you to fill in
      the ``path`` field all by yourself. By default, ``save()`` ensures that
      the ``path`` fields are always composed of a concatenation of the
      parent's ``path`` with the page's own ``slug`` (with slashes of course).
      This is especially useful for root pages (set ``path`` to ``/``) or,
      when building a multilingual site, for language root pages (i.e.
      ``/en/``, ``/de/``, ``/pt-br/`` etc.)
    """

    is_active = models.BooleanField(_("is active"), default=True)
    title = models.CharField(_("title"), max_length=200)
    slug = models.SlugField(_("slug"))
    position = models.PositiveIntegerField(
        db_index=True,
        editable=False,
        validators=[
            MinValueValidator(
                limit_value=1,
                message=_("Position is expected to be greater than zero."),
            )
        ],
    )

    # Who even cares about MySQL
    path = models.CharField(
        _("path"),
        max_length=1000,
        blank=True,
        unique=True,
        help_text=_(
            "Automatically generated by concatenating the parent's path and"
            " the slug unless the path is defined manually."
        ),
        validators=[
            RegexValidator(
                regex=r"^/(|.+/)$",
                message=_("Path must start and end with a slash (/)."),
            )
        ],
    )
    static_path = models.BooleanField(_("manually define the path"), default=False)

    objects = AbstractPageQuerySet.as_manager(with_tree_fields=True)

    class Meta:
        abstract = True
        ordering = ("position",)
        verbose_name = _("page")
        verbose_name_plural = _("pages")

    def __str__(self):
        return self.title

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Go through self.__dict__ to avoid triggering deferred field loading
        self._save_descendants_cache = (
            self.__dict__.get("is_active"),
            self.__dict__.get("path"),
        )

    def _branch_for_update(self):
        nodes = OrderedDict({self.pk: self})
        for node in self.descendants():
            # Assign already-updated instance:
            node.parent = nodes[node.parent_id]
            if not node.static_path:
                node.path = f"{node.parent.path}{node.slug}/"

            # Descendants of inactive nodes cannot be active themselves:
            if not node.parent.is_active:
                node.is_active = False
            nodes[node.id] = node
        return nodes

    def _path_clash_candidates(self):
        # Hook used in feincms3-sites
        return self.__class__._default_manager.exclude(
            Q(pk__in=self.descendants(), static_path=False) | Q(pk=self.pk)
        )

    def clean_fields(self, exclude=None):
        """
        Check for path uniqueness problems.
        """
        super().clean_fields(exclude)

        if self.static_path:
            if not self.path:
                raise validation_error(
                    _("Static paths cannot be empty. Did you mean '/'?"),
                    field="path",
                    exclude=exclude,
                )
        else:
            self.path = "{}{}/".format(
                self.parent.path if self.parent else "/", self.slug
            )

        super().clean()

        # Skip if we don't exist yet.
        if not self.pk:
            return

        clash_candidates = dict(self._path_clash_candidates().values_list("path", "id"))
        for pk, node in self._branch_for_update().items():
            if (
                node.path in clash_candidates
                and not clash_candidates[node.path] == node.pk
            ):
                raise validation_error(
                    _("The page %(page)s's new path %(path)s would not be unique.")
                    % {"page": node, "path": node.path},
                    field="path",
                    exclude=exclude,
                )

    def save(self, *args, **kwargs):
        """save(self, ..., save_descendants=None)
        Saves the page instance, and traverses all descendants to update their
        ``path`` fields and ensure that inactive pages (``is_active=False``)
        never have any descendants with ``is_active=True``.

        By default, descendants are only updated when any of ``is_active`` and
        ``path`` change. This can be overridden by either forcing updates using
        ``save_descendants=True`` or skipping them using
        ``save_descendants=False``.
        """
        save_descendants = kwargs.pop("save_descendants", None)

        if not self.static_path:
            self.path = "{}{}/".format(
                self.parent.path if self.parent else "/", self.slug
            )

        if not self.position:
            self.position = 10 + (
                self.__class__._default_manager.filter(parent_id=self.parent_id)
                .order_by()
                .aggregate(p=Max("position"))["p"]
                or 0
            )

        super().save(*args, **kwargs)

        if save_descendants is True or (
            save_descendants is None
            and (self.is_active, self.path) != self._save_descendants_cache
        ):
            for pk, node in self._branch_for_update().items():
                if pk == self.pk:
                    continue
                node.save(save_descendants=False)

    save.alters_data = True

    def get_absolute_url(self):
        """
        Return the page's absolute URL

        If path is ``/``, reverses ``pages:root`` without any arguments,
        alternatively reverses ``pages:page`` with an argument of ``path``.
        Note that this ``path`` is not the same as ``self.path`` -- slashes
        are stripped from the beginning and the end of the string to make
        building an URLconf more straightforward.

        If any ``reverse()`` call fails it falls back to returning
        ``self.path`` prefixed with the script prefix which is ``/`` in the
        standard case.
        """
        try:
            if self.path == "/":
                return reverse("pages:root")
            return reverse("pages:page", kwargs={"path": self.path.strip("/")})
        except NoReverseMatch:
            return path_with_script_prefix(self.path)

    @classmethod
    def check(cls, **kwargs):
        errors = super().check(**kwargs)
        errors.extend(cls._check_feincms3_pages_default_ordering(**kwargs))
        return errors

    @classmethod
    def _check_feincms3_pages_default_ordering(cls, **kwargs):
        if not tuple(cls._meta.ordering) == ("position",):
            return [
                Warning(
                    "The page subclass isn't ordered by `position`.",
                    hint=(
                        'Define `ordering = ("position",)` when defining your own'
                        " `class Meta` on subclassed pages."
                    ),
                    obj=cls,
                    id="feincms3.W001",
                ),
            ]
        return [] def oembed_json(url, *, cache_failures=True, force_refresh=False, params=None):
def oembed_html(url, **kwargs):
def render_external(plugin, **kwargs):
    def __str__(self):
    def clean(self):
class External(models.Model):
    class Meta:
class NoembedValidationForm(forms.ModelForm):
class ExternalInline(ContentEditorInline): class HTML(models.Model):
    class Meta:
class HTMLInline(ContentEditorInline):
    def __str__(self):
def render_html(plugin, **kwargs): class Image(models.Model):
    class Meta:
class ImageInline(ContentEditorInline):
    def __str__(self):
def render_image(plugin, **kwargs): class RichText(models.Model):
    class Meta:
class RichTextInline(ContentEditorInline):
    def __str__(self):
def render_richtext(plugin, **kwargs): def render_snippet(plugin, **kwargs):
    def __str__(self):
    def fill_template_name_choices(sender, **kwargs):
    def register_with(cls, renderer):
class Snippet(models.Model):
    class Meta:
class SnippetInline(ContentEditorInline):
from content_editor.models import Region, create_plugin_base
from django.db import models
from django.utils.translation import gettext_lazy as _, override
from feincms3.applications import (
    ApplicationType,
    PageTypeMixin,
    TemplateType,
    reverse_app,
)
from feincms3.mixins import LanguageAndTranslationOfMixin, MenuMixin, RedirectMixin
from feincms3.pages import AbstractPage
from feincms3.plugins import external, html, image, richtext, snippet



class Page(
    # Add this first so that AbstractPage.Meta's properties are active:
    AbstractPage,
    # For adding the articles app to pages through the CMS:
    PageTypeMixin,
    # We have a main and a footer navigation (meta):
    MenuMixin,
    # We're building a multilingual CMS. (Also, feincms3.applications depends on
    # LanguageMixin currently):
    LanguageAndTranslationOfMixin,
    # Allow redirecting pages to other pages and/or arbitrary URLs:
    RedirectMixin,
):

    # MenuMixin
    MENUS = [("main", _("main")), ("footer", _("footer"))]

    # PageTypeMixin. We have two templates and four apps.
    TYPES = [

        TemplateType(