digitalfabrik/integreat-cms

View on GitHub
integreat_cms/cms/forms/custom_tree_node_form.py

Summary

Maintainability
A
0 mins
Test Coverage
A
93%
from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from django import forms
from treebeard.forms import MoveNodeForm

if TYPE_CHECKING:
    from typing import Any

    from django.db.models.base import ModelBase

    from ..models import LanguageTreeNode, Page

logger = logging.getLogger(__name__)


class CustomTreeNodeForm(MoveNodeForm):
    """
    Form for creating and modifying tree node objects
    """

    def __init__(self, **kwargs: Any) -> None:
        r"""
        Initialize custom tree node form

        :param \**kwargs: The supplied keyword arguments
        """

        # Instantiate MoveNodeForm
        super().__init__(**kwargs)

        # Hide tree node inputs
        self.fields["_ref_node_id"].widget = forms.HiddenInput()
        self.fields["_position"].widget = forms.HiddenInput()

    def _clean_cleaned_data(self) -> tuple[str, int]:
        """
        Delete auxiliary fields not belonging to node model and include instance attributes in cleaned_data

        :return: The initial data for _ref_node_id and _position fields
        """
        # This workaround is required because the MoveNodeForm does not take
        # instance attribute into account which are not included in cleaned_data
        self.cleaned_data["region"] = self.instance.region
        return super()._clean_cleaned_data()

    def _get_position_ref_node(
        self, instance: LanguageTreeNode | Page
    ) -> dict[str, str]:
        """
        Get the initial values for the referenced node and the position

        :param instance: The node instance
        :return: A dictionary containing the initial values
        """
        prev_sibling = instance.get_prev_sibling()
        # If the previous sibling is of another region, use a different node as reference
        if prev_sibling and prev_sibling.region != instance.region:
            logger.debug(
                "Node %r was referenced to node %r of another region",
                instance,
                prev_sibling,
            )
            next_sibling = instance.get_next_sibling()
            # If the next sibling exists and is of this region, reference this instance to the left of the next sibling
            if next_sibling and next_sibling.region == instance.region:
                logger.debug(
                    "Node %r is now referenced left to node %r", instance, next_sibling
                )
                return {"_ref_node_id": str(next_sibling.id), "_position": "left"}
            # If the page is the only root page of this region, do not reference other nodes
            logger.debug(
                "Node %r is the only root node of its region and now referenced to no other node",
                instance,
            )
            return {"_ref_node_id": "", "_position": "first-child"}
        # Convert initial data to string to fix the change detection
        initial_data = super()._get_position_ref_node(instance)
        return {key: str(value) for key, value in initial_data.items()}

    @classmethod
    def mk_dropdown_tree(
        cls, model: ModelBase, for_node: None | (LanguageTreeNode | Page) = None
    ) -> list:
        """
        Creates a tree-like list of choices. Overwrites the parent method because the field is hidden anyway and
        additional queries to render the node titles should be avoided.

        :param model: ~integreat_cms.cms.models.abstract_tree_node.AbstractTreeNode
        :param for_node: The instance of this form
        :return: A list of select options
        """
        # No need to calculate anything here, because we set self.fields["_ref_node_id"].choices manually
        return []