digitalfabrik/integreat-cms

View on GitHub
integreat_cms/cms/views/pages/page_form_view.py

Summary

Maintainability
A
0 mins
Test Coverage
C
71%
from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from django.conf import settings
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView

from ...constants import status, text_directions
from ...decorators import permission_required
from ...forms import PageForm, PageTranslationForm
from ...models import PageTranslation
from ...utils.translation_utils import gettext_many_lazy as __
from ...utils.translation_utils import translate_link
from ..media.media_context_mixin import MediaContextMixin
from ..mixins import ContentEditLockMixin
from .page_context_mixin import PageContextMixin

if TYPE_CHECKING:
    from typing import Any

    from django.http import HttpRequest, HttpResponse, HttpResponseRedirect

    from integreat_cms.cms.models.languages.language import Language
    from integreat_cms.cms.models.pages.page import Page
    from integreat_cms.cms.models.regions.region import Region

logger = logging.getLogger(__name__)


@method_decorator(permission_required("cms.view_page"), name="dispatch")
class PageFormView(
    TemplateView, PageContextMixin, MediaContextMixin, ContentEditLockMixin
):
    """
    View for the page form and page translation form
    """

    #: The template to render (see :class:`~django.views.generic.base.TemplateResponseMixin`)
    template_name = "pages/page_form.html"
    #: The context dict passed to the template (see :class:`~django.views.generic.base.ContextMixin`)
    extra_context = {
        "current_menu_item": "new_page",
    }
    #: The url name of the view to show if the user decides to go back (see :class:`~integreat_cms.cms.views.mixins.ContentEditLockMixin`)
    back_url_name: str | None = "pages"

    # pylint: disable=too-many-locals, too-many-branches
    def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
        r"""
        Render :class:`~integreat_cms.cms.forms.pages.page_form.PageForm` and :class:`~integreat_cms.cms.forms.pages.page_translation_form.PageTranslationForm`

        :param request: The current request
        :param \*args: The supplied arguments
        :param \**kwargs: The supplied keyword arguments
        :raises ~django.core.exceptions.PermissionDenied: If user does not have the permission to edit the specific page

        :return: The rendered template response
        """

        region = request.region
        language = region.get_language_or_404(
            kwargs.get("language_slug"), only_active=True
        )

        # Get page and translation objects if they exist
        page = (
            region.pages.filter(id=kwargs.get("page_id"))
            .prefetch_translations()
            .prefetch_public_translations()
            .first()
        )
        page_translation = page.get_translation(language.slug) if page else None

        disabled = False
        if page:
            # Make form disabled if page is archived
            if page.explicitly_archived:
                disabled = True
                messages.warning(
                    request, _("You cannot edit this page because it is archived.")
                )
            elif page.implicitly_archived:
                disabled = True
                messages.warning(
                    request,
                    _(
                        "You cannot edit this page, because one of its parent pages is archived and therefore, this page is archived as well."
                    ),
                )
            # If the page is not public, check whether we need additional messages to prevent confusion
            if page_translation and page_translation.status != status.PUBLIC:
                # Indicate autosaves and changes pending review
                if page_translation.status in [status.AUTO_SAVE, status.REVIEW]:
                    revision_url = reverse(
                        "page_versions",
                        kwargs={
                            "region_slug": region.slug,
                            "language_slug": language.slug,
                            "page_id": page.id,
                        },
                    )
                    if page_translation.status == status.AUTO_SAVE:
                        status_message = _("The last changes were saved automatically.")
                        action = _("discard")
                    else:
                        status_message = _("The latest changes are pending approval.")
                        action = _("reject")
                    # If the user has the permission to reject/discard, show another message
                    if request.user.has_perm("cms.publish_page_object", page):
                        message = __(
                            status_message,
                            _(
                                "You can {action} these changes in the <a>version overview</a>."
                            ).format(action=action),
                        )
                        status_message = translate_link(
                            message,
                            attributes={
                                "href": revision_url,
                                "class": "underline hover:no-underline",
                            },
                        )
                    messages.warning(request, status_message)
                # Show information if a public translation exists even if the latest version is not public
                if public_translation := page.get_public_translation(language.slug):
                    if page_translation.status == status.DRAFT:
                        messages.warning(
                            request,
                            _("The latest changes have only been saved as a draft."),
                        )
                    revision_url = reverse(
                        "page_versions",
                        kwargs={
                            "region_slug": region.slug,
                            "language_slug": language.slug,
                            "page_id": page.id,
                            "selected_version": public_translation.version,
                        },
                    )
                    message = _(
                        "Currently, <a>version {}</a> of this page is displayed in the app."
                    ).format(public_translation.version)
                    messages.info(
                        request,
                        translate_link(
                            message,
                            attributes={
                                "href": revision_url,
                                "class": "underline hover:no-underline",
                            },
                        ),
                    )

        # Make form disabled if user has no permission to edit the page
        if not request.user.has_perm("cms.change_page_object", page):
            disabled = True
            messages.warning(
                request,
                _("You don't have the permission to edit this page."),
            )
        # Show warning if user has no permission to publish the page
        elif not request.user.has_perm("cms.publish_page_object", page):
            messages.warning(
                request,
                _(
                    "You don't have the permission to publish this page, but you can propose changes and submit them for review instead."
                ),
            )

        page_form = PageForm(
            instance=page,
            disabled=disabled,
            additional_instance_attributes={
                "region": region,
            },
        )
        # Pass language to mirrored page widget to render the preview urls
        page_form.fields["mirrored_page"].widget.language_slug = language.slug

        if not request.user.expert_mode:
            del page_form.fields["api_token"]

        page_translation_form = PageTranslationForm(
            request=request,
            language=language,
            instance=page_translation,
            disabled=disabled,
        )

        # Pass side by side language options
        side_by_side_language_options = self.get_side_by_side_language_options(
            region, language, page
        )

        # Pass siblings to template to enable rendering of page order table
        siblings = (
            (
                page.region_siblings.prefetch_translations()
                .prefetch_public_translations()
                .filter(explicitly_archived=page.explicitly_archived)
            )
            if page
            else (
                region.get_root_pages()
                .prefetch_translations()
                .prefetch_public_translations()
                .filter(explicitly_archived=False)
            )
        )

        return render(
            request,
            self.template_name,
            {
                **self.get_context_data(**kwargs),
                "page_form": page_form,
                "page_translation_form": page_translation_form,
                "page": page,
                "siblings": siblings,
                "language": language,
                # Languages for tab view
                "languages": region.active_languages if page else [language],
                "side_by_side_language_options": side_by_side_language_options,
                "right_to_left": (
                    language.text_direction == text_directions.RIGHT_TO_LEFT
                ),
                "translation_states": page.translation_states if page else [],
                "minimum_hix": settings.HIX_REQUIRED_FOR_MT,
            },
        )

    # pylint: disable=too-many-statements
    @transaction.atomic
    def post(
        self, request: HttpRequest, *args: Any, **kwargs: Any
    ) -> HttpResponseRedirect:
        r"""
        Submit :class:`~integreat_cms.cms.forms.pages.page_form.PageForm` and
        :class:`~integreat_cms.cms.forms.pages.page_translation_form.PageTranslationForm` and save :class:`~integreat_cms.cms.models.pages.page.Page`
        and :class:`~integreat_cms.cms.models.pages.page_translation.PageTranslation` objects.
        Forms containing images/files need to be additionally instantiated with the FILES attribute of request objects,
        see :doc:`django:topics/http/file-uploads`

        :param request: The current request
        :param \*args: The supplied arguments
        :param \**kwargs: The supplied keyword arguments
        :raises ~django.core.exceptions.PermissionDenied: If user does not have the permission to edit the specific page

        :return: The rendered template response
        """

        region = request.region
        language = region.get_language_or_404(
            kwargs.get("language_slug"), only_active=True
        )

        page_instance = region.pages.filter(id=kwargs.get("page_id")).first()

        if not request.user.has_perm("cms.change_page_object", page_instance):
            raise PermissionDenied(
                f"{request.user!r} does not have the permission to edit {page_instance!r}"
            )

        page_translation_instance = PageTranslation.objects.filter(
            page=page_instance,
            language=language,
        ).first()

        page_form = PageForm(
            data=request.POST,
            files=request.FILES,
            instance=page_instance,
            additional_instance_attributes={
                "region": region,
            },
        )
        # Pass language to mirrored page widget to render the preview urls
        page_form.fields["mirrored_page"].widget.language_slug = language.slug

        if not request.user.expert_mode:
            del page_form.fields["api_token"]

        page_translation_form = PageTranslationForm(
            request=request,
            language=language,
            data=request.POST,
            instance=page_translation_instance,
            additional_instance_attributes={
                "creator": request.user,
                "language": language,
                "page": page_form.instance,
            },
            changed_by_user=request.user,
        )
        user_slug = page_translation_form.data.get("slug")

        if not page_form.is_valid() or not page_translation_form.is_valid():
            # Add error messages
            page_form.add_error_messages(request)
            page_translation_form.add_error_messages(request)
        elif not request.user.has_perm(
            "cms.publish_page_object", page_form.instance
        ) and (
            page_translation_form.cleaned_data.get("status")
            in [status.DRAFT, status.PUBLIC]
        ):
            # Raise PermissionDenied if user wants to publish page but doesn't have the permission
            raise PermissionDenied(
                f"{request.user!r} does not have the permission to publish {page_form.instance!r}"
            )
        elif (
            page_translation_form.instance.status == status.AUTO_SAVE
            and not page_form.has_changed()
            and not page_translation_form.has_changed()
        ):
            messages.info(request, _("No changes detected, autosave skipped"))

        else:
            # Only save page form if page does not yet exist or if translation is no auto save
            if (
                not page_instance
                or page_translation_form.instance.status != status.AUTO_SAVE
            ):
                page_translation_form.instance.page = page_form.save()
            # Save page translation form
            page_translation_instance = page_translation_form.save(
                foreign_form_changed=page_form.has_changed()
            )

            # Show a message that the slug was changed if it was not unique
            if user_slug and user_slug != page_translation_form.cleaned_data["slug"]:
                other_translation = PageTranslation.objects.filter(
                    page__region=region, slug=user_slug, language=language
                ).first()
                other_translation_link = reverse(
                    "page_versions",
                    kwargs={
                        "page_id": other_translation.page.id,
                        "language_slug": language.slug,
                        "region_slug": region.slug,
                        "selected_version": other_translation.version,
                    },
                )
                message = _(
                    "The slug was changed from '{user_slug}' to '{slug}', "
                    "because '{user_slug}' is already used by <a>{translation}</a>.",
                ).format(
                    user_slug=user_slug,
                    slug=page_translation_form.cleaned_data["slug"],
                    translation=other_translation,
                )
                messages.warning(
                    request,
                    translate_link(
                        message,
                        attributes={
                            "href": other_translation_link,
                            "class": "underline hover:no-underline",
                        },
                    ),
                )

            # If any source translation changes to draft, set all depending translations/versions to draft
            if page_translation_form.instance.status == status.DRAFT:
                language_tree_node = region.language_node_by_slug.get(language.slug)
                languages = [language] + [
                    node.language for node in language_tree_node.get_descendants()
                ]
                page_translation_form.instance.page.translations.filter(
                    language__in=languages, status=status.PUBLIC
                ).update(status=status.DRAFT)
            # If this is the first version and the minor edit checkbox is checked, remove it
            if (
                page_translation_form.instance.version == 1
                and page_translation_form.instance.minor_edit
            ):
                page_translation_form.instance.minor_edit = False
                page_translation_form.instance.save()
                messages.info(
                    request,
                    _(
                        'The "minor edit" option was disabled because the first version can never be a minor edit.'
                    ),
                )
            # If this is not an autosave, clean up previous auto saves
            if page_translation_form.instance.status != status.AUTO_SAVE:
                page_translation_form.instance.cleanup_autosaves()

            # Add the success message and redirect to the edit page
            if not page_instance:
                if page_translation_form.instance.status == status.REVIEW:
                    messages.success(
                        request,
                        _(
                            'Page "{}" was successfully created and submitted for approval'
                        ).format(page_translation_form.instance.title),
                    )
                else:
                    messages.success(
                        request,
                        _('Page "{}" was successfully created').format(
                            page_translation_form.instance.title
                        ),
                    )
            elif (
                not page_form.has_changed() and not page_translation_form.has_changed()
            ):
                messages.info(request, _("No changes detected, but date refreshed"))
            else:
                # Add the success message
                page_translation_form.add_success_message(request)

            return redirect(
                "edit_page",
                **{
                    "page_id": page_form.instance.id,
                    "region_slug": region.slug,
                    "language_slug": language.slug,
                },
            )

        # Pass siblings to template to enable rendering of page order table
        if page_translation_form.instance.id:
            siblings = page_translation_form.instance.page.region_siblings
        elif page_form.instance.id:
            siblings = page_form.instance.region_siblings
        else:
            siblings = region.get_root_pages()
        siblings = siblings.filter(
            explicitly_archived=page_form.instance.explicitly_archived
        )

        return render(
            request,
            self.template_name,
            {
                **self.get_context_data(**kwargs),
                "page_form": page_form,
                "page_translation_form": page_translation_form,
                "page": page_instance,
                "siblings": siblings,
                "language": language,
                # Languages for tab view
                "languages": region.active_languages if page_instance else [language],
                "side_by_side_language_options": self.get_side_by_side_language_options(
                    region, language, page_instance
                ),
                "right_to_left": (
                    language.text_direction == text_directions.RIGHT_TO_LEFT
                ),
                "translation_states": (
                    page_instance.translation_states if page_instance else []
                ),
            },
        )

    @staticmethod
    def get_side_by_side_language_options(
        region: Region, language: Language, page: Page | None
    ) -> list[dict[str, Any]]:
        """
        This is a helper function to generate the side-by-side language options for both the get and post requests.

        :param region: The current region
        :param language: The current language
        :param page: The current page
        :return: The list of language options, each represented by a dict
        """

        side_by_side_language_options = []
        for language_node in filter(lambda n: n.active, region.language_tree):
            if not language_node.is_root():
                source_language = region.language_node_by_id.get(
                    language_node.parent_id
                ).language
                side_by_side_language_options.append(
                    {
                        "value": language_node.slug,
                        "label": _("{source_language} to {target_language}").format(
                            source_language=source_language.translated_name,
                            target_language=language_node.translated_name,
                        ),
                        "selected": language_node.language == language,
                        "disabled": not page or source_language not in page.languages,
                    }
                )
        return side_by_side_language_options