digitalfabrik/integreat-cms

View on GitHub
integreat_cms/cms/forms/users/user_form.py

Summary

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

import logging
from typing import TYPE_CHECKING

from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.password_validation import (
    password_validators_help_texts,
    validate_password,
)
from django.utils.translation import gettext_lazy as _

if TYPE_CHECKING:
    from typing import Any

    from ...models import User

from ...models import Role
from ...utils.translation_utils import gettext_many_lazy as __
from ..custom_model_form import CustomModelForm
from .organization_field import OrganizationField

logger = logging.getLogger(__name__)


class UserForm(CustomModelForm):
    """
    Form for creating and modifying user objects
    """

    role = forms.ModelChoiceField(
        queryset=Role.objects.filter(staff_role=False),
        required=False,
        label=_("Role"),
        help_text=_("This grants the user all permissions of the selected role"),
    )
    staff_role = forms.ModelChoiceField(
        queryset=Role.objects.filter(staff_role=True),
        required=False,
        label=_("Team"),
        help_text=_("This grants the user all permissions of the selected team"),
    )
    password = forms.CharField(
        widget=forms.PasswordInput,
        validators=[validate_password],
        help_text=password_validators_help_texts,
        required=False,
    )
    send_activation_link = forms.BooleanField(
        initial=True,
        required=False,
        label=_("Send activation link"),
        help_text=__(
            _(
                "Select this option to create an inactive user account and send an activation link per email to the user."
            ),
            _(
                "This link allows the user to choose a password and activates the account after confirmation."
            ),
        ),
    )

    class Meta:
        """
        This class contains additional meta configuration of the form class, see the :class:`django.forms.ModelForm`
        for more information.
        """

        #: The model of this :class:`django.forms.ModelForm`
        model = get_user_model()
        #: The fields of the model which should be handled by this form
        fields = [
            "username",
            "first_name",
            "last_name",
            "email",
            "is_staff",
            "is_active",
            "is_superuser",
            "organization",
            "expert_mode",
            "regions",
            "role",
            "send_activation_link",
            "passwordless_authentication_enabled",
        ]
        field_classes = {"organization": OrganizationField}

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

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

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

        # check if user instance already exists
        if self.instance.id:
            # set initial role data
            if self.instance.is_staff:
                self.fields["staff_role"].initial = self.instance.role
            else:
                self.fields["role"].initial = self.instance.role
            # don't require password if user already exists
            self.fields["password"].required = False
            # adapt placeholder of password input field
            self.fields["password"].widget.attrs.update(
                {"placeholder": _("Leave empty to keep unchanged")}
            )
        else:
            self.fields["is_active"].initial = False

        # fix labels
        self.fields["password"].label = _("Password")
        if "is_staff" in self.fields:
            self.fields["is_staff"].label = _("Integreat team member")
        self.fields["email"].required = True

        # Check if passwordless authentication is possible for the user
        if "passwordless_authentication_enabled" in self.fields and (
            not self.instance.pk or not self.instance.fido_keys.exists()
        ):
            self.fields["passwordless_authentication_enabled"].disabled = True

    def save(self, commit: bool = True) -> User:
        """
        This method extends the default ``save()``-method of the base :class:`~django.forms.ModelForm` to set attributes
        which are not directly determined by input fields.

        :param commit: Whether or not the changes should be written to the database
        :return: The saved user object
        """

        # Save CustomModelForm
        user = super().save(commit=commit)

        # check if password field was changed
        if self.cleaned_data["password"]:
            # change password
            user.set_password(self.cleaned_data["password"])
            user.save()

        role = (
            self.cleaned_data["staff_role"]
            if user.is_staff
            else self.cleaned_data["role"]
        )

        for removed_group in user.groups.exclude(id=role.id):
            # Remove unselected roles
            removed_group.user_set.remove(user)
            logger.info(
                "%r was removed from %r",
                removed_group.role,
                user,
            )
        if role.group not in user.groups.all():
            # Assign the selected role
            role.group.user_set.add(user)
            logger.info("%r was assigned to %r", role, user)

        return user

    def clean_email(self) -> str:
        """
        Make the email lower case (see :ref:`overriding-modelform-clean-method`)

        :return: The email in lower case
        """
        if email := self.cleaned_data.get("email"):
            email = email.lower()
        return email

    def clean(self) -> dict[str, Any]:
        """
        Validate form fields which depend on each other, see :meth:`django.forms.Form.clean`

        :return: The cleaned form data
        """
        cleaned_data = super().clean()

        if cleaned_data.get("is_superuser") and not cleaned_data.get("is_staff"):
            logger.warning("Superuser %r is not a staff member", self.instance)
            self.add_error(
                "is_superuser",
                forms.ValidationError(
                    _("Only staff members can be superusers"),
                    code="invalid",
                ),
            )

        if cleaned_data.get("is_staff"):
            if cleaned_data.get("role"):
                logger.warning(
                    "Staff member %r can only have staff roles", self.instance
                )
                self.add_error(
                    "staff_role",
                    forms.ValidationError(
                        _("Staff members can only have staff roles"),
                        code="invalid",
                    ),
                )
            if not cleaned_data.get("staff_role"):
                logger.warning("Staff member %r needs a staff role", self.instance)
                self.add_error(
                    "staff_role",
                    forms.ValidationError(
                        _("Staff members have to be member of a team"),
                        code="required",
                    ),
                )
        else:
            if cleaned_data.get("staff_role"):
                logger.warning(
                    "Non-staff member %r cannot have staff roles", self.instance
                )
                self.add_error(
                    "role",
                    forms.ValidationError(
                        _("Only staff members can be member of a team"),
                        code="invalid",
                    ),
                )
            if not cleaned_data.get("role"):
                logger.warning("Non-staff member %r needs a role", self.instance)
                self.add_error(
                    "role",
                    forms.ValidationError(
                        _("Please select a role"),
                        code="required",
                    ),
                )

        if not (
            cleaned_data.get("send_activation_link")
            or cleaned_data.get("password")
            or self.instance
        ):
            self.add_error(
                "send_activation_link",
                forms.ValidationError(
                    _(
                        "Please choose either to send an activation link or set a password."
                    ),
                    code="required",
                ),
            )

        # If this is the global user form, validate the given organization
        # (In the region user form, this is already ensured by the field's choices)
        if "regions" in self.fields and cleaned_data.get("organization"):
            if cleaned_data.get("is_superuser") or cleaned_data.get("is_staff"):
                logger.warning(
                    "Staff member %r cannot be member of an organization", self.instance
                )
                self.add_error(
                    "organization",
                    forms.ValidationError(
                        _("Staff members cannot be member of an organization"),
                        code="invalid",
                    ),
                )
            elif cleaned_data["organization"].region not in cleaned_data.get(
                "regions", []
            ):
                logger.warning(
                    "User %r cannot be member of organization %r of other region",
                    self.instance,
                    cleaned_data["organization"],
                )
                self.add_error(
                    "organization",
                    forms.ValidationError(
                        _(
                            "Users can only be members of organizations in regions they have access to"
                        ),
                        code="invalid",
                    ),
                )

        logger.debug("UserForm validated [2] with cleaned data %r", cleaned_data)
        return cleaned_data