<filename>permissionedforms/forms.py
from django import forms


class Options:
    """
    An object that serves as a container for configuration options. When a class is defined using
    OptionCollectingMetaclass as its metaclass, any attributes defined on an inner `class Meta`
    will be copied to an Options instance which will then be accessible as the class attribute
    `_meta`.

    The base Options class has no functionality of its own, but exists so that specific
    configuration options can be defined as mixins and collectively merged in to either Options or
    another base class with the same interface such as django.forms.models.ModelFormOptions,
    to arrive at a final class that recognises the desired set of options.
    """

    def __init__(self, options=None):
        pass


class OptionCollectingMetaclass(type):
    """
    Metaclass that handles inner `class Meta` definitions. When a class using
    OptionCollectingMetaclass defines an inner Meta class and an `options_class` attribute
    specifying an Options class, an Options object will be created from it and set as the class
    attribute `_meta`.
    """

    options_class = None

    def __new__(mcs, name, bases, attrs):
        new_class = super().__new__(mcs, name, bases, attrs)
        if mcs.options_class:
            new_class._meta = mcs.options_class(getattr(new_class, "Meta", None))
        return new_class


class PermissionedFormOptionsMixin:
    """Handles the field_permissions option for PermissionedForm"""

    def __init__(self, options=None):
        super().__init__(options)
        self.field_permissions = getattr(options, "field_permissions", None)


class PermissionedFormOptions(PermissionedFormOptionsMixin, Options):
    """Options class for PermissionedForm"""


FormMetaclass = type(forms.Form)


class PermissionedFormMetaclass(OptionCollectingMetaclass, FormMetaclass):
    """
    Extends the django.forms.Form metaclass with support for an inner `class Meta` that accepts
    a `field_permissions` configuration option
    """

    options_class = PermissionedFormOptions


class PermissionedForm(forms.Form, metaclass=PermissionedFormMetaclass):
    """
    An extension to `django.forms.Form` to accept an optional `for_user` keyword argument
    indicating the user the form will be presented to.

    Any fields named in the `field_permissions` dict in Meta will apply a permission test on the
    named permission using `User.has_perm`; if the user lacks that permission, the field will be
    omitted from the form.
    """

    def __init__(self, *args, for_user=None, **kwargs):
        super().__init__(*args, **kwargs)
        if for_user:
            field_perms = self._meta.field_permissions or {}
            for field_name, perm in field_perms.items():
                if not for_user.has_perm(perm):
                    del self.fields[field_name]


class PermissionedModelFormOptions(
    PermissionedFormOptionsMixin, forms.models.ModelFormOptions
):
    """
    Options class for PermissionedModelForm; extends ModelForm's options to accept
    `field_permissions`
    """


class PermissionedModelFormMetaclass(
    PermissionedFormMetaclass, forms.models.ModelFormMetaclass
):
    """
    Metaclass for PermissionedModelForm; extends the ModelForm metaclass to use
    PermissionedModelFormOptions in place of ModelFormOptions and thus accept the
    `field_permissions` option.

    Note that because ModelForm does not participate in the OptionCollectingMetaclass logic, this
    has the slightly hacky effect of letting ModelFormMetaclass construct a ModelFormOptions object
    for the lifetime of ModelFormMetaclass.__new__, which we promptly throw out and recreate as a
    PermissionedModelFormOptions object.
    """

    options_class = PermissionedModelFormOptions


class PermissionedModelForm(
    PermissionedForm, forms.ModelForm, metaclass=PermissionedModelFormMetaclass
):
    """A ModelForm that implements the `for_user` keyword argument from PermissionedForm"""

<filename>permissionedforms/forms.py
from django import forms


class Options:
    """
    An object that serves as a container for configuration options. When a class is defined using
    OptionCollectingMetaclass as its metaclass, any attributes defined on an inner `class Meta`
    will be copied to an Options instance which will then be accessible as the class attribute
    `_meta`.

    The base Options class has no functionality of its own, but exists so that specific
    configuration options can be defined as mixins and collectively merged in to either Options or
    another base class with the same interface such as django.forms.models.ModelFormOptions,
    to arrive at a final class that recognises the desired set of options.
    """

    def __init__(self, options=None):
        pass


class OptionCollectingMetaclass(type):
    """
    Metaclass that handles inner `class Meta` definitions. When a class using
    OptionCollectingMetaclass defines an inner Meta class and an `options_class` attribute
    specifying an Options class, an Options object will be created from it and set as the class
    attribute `_meta`.
    """

    options_class = None

    def __new__(mcs, name, bases, attrs):
        new_class = super().__new__(mcs, name, bases, attrs)
        if mcs.options_class:
            new_class._meta = mcs.options_class(getattr(new_class, "Meta", None))
        return new_class


class PermissionedFormOptionsMixin:
    """Handles the field_permissions option for PermissionedForm"""

    def __init__(self, options=None):
        super().__init__(options)
        self.field_permissions = getattr(options, "field_permissions", None)


class PermissionedFormOptions(PermissionedFormOptionsMixin, Options):
    """Options class for PermissionedForm"""


FormMetaclass = type(forms.Form)


class PermissionedFormMetaclass(OptionCollectingMetaclass, FormMetaclass):
    """
    Extends the django.forms.Form metaclass with support for an inner `class Meta` that accepts
    a `field_permissions` configuration option
    """

    options_class = PermissionedFormOptions


class PermissionedForm(forms.Form, metaclass=PermissionedFormMetaclass):
    """
    An extension to `django.forms.Form` to accept an optional `for_user` keyword argument
    indicating the user the form will be presented to.

    Any fields named in the `field_permissions` dict in Meta will apply a permission test on the
    named permission using `User.has_perm`; if the user lacks that permission, the field will be
    omitted from the form.
    """

    def __init__(self, *args, for_user=None, **kwargs):
        super().__init__(*args, **kwargs)
        if for_user:
            field_perms = self._meta.field_permissions or {}
            for field_name, perm in field_perms.items():
                if not for_user.has_perm(perm):
                    del self.fields[field_name]


class PermissionedModelFormOptions(
    PermissionedFormOptionsMixin, forms.models.ModelFormOptions
):
    """
    Options class for PermissionedModelForm; extends ModelForm's options to accept
    `field_permissions`
    """


class PermissionedModelFormMetaclass(
    PermissionedFormMetaclass, forms.models.ModelFormMetaclass
):
    """
    Metaclass for PermissionedModelForm; extends the ModelForm metaclass to use
    PermissionedModelFormOptions in place of ModelFormOptions and thus accept the
    `field_permissions` option.

    Note that because ModelForm does not participate in the OptionCollectingMetaclass logic, this
    has the slightly hacky effect of letting ModelFormMetaclass construct a ModelFormOptions object
    for the lifetime of ModelFormMetaclass.__new__, which we promptly throw out and recreate as a
    PermissionedModelFormOptions object.
    """

    options_class = PermissionedModelFormOptions


class PermissionedModelForm(
    PermissionedForm, forms.ModelForm, metaclass=PermissionedModelFormMetaclass
):
    """A ModelForm that implements the `for_user` keyword argument from PermissionedForm"""

An object that serves as a container for configuration options. When a class is defined using     OptionCollectingMetaclass as its metaclass, any attributes defined on an inner `class Meta`     will be copied to an Options instance which will then be accessible as the class attribute     `_meta`.      The base Options class has no functionality of its own, but exists so that specific     configuration options can be defined as mixins and collectively merged in to either Options or     another base class with the same interface such as django.forms.models.ModelFormOptions,     to arrive at a final class that recognises the desired set of options. Metaclass that handles inner `class Meta` definitions. When a class using     OptionCollectingMetaclass defines an inner Meta class and an `options_class` attribute     specifying an Options class, an Options object will be created from it and set as the class     attribute `_meta`. Handles the field_permissions option for PermissionedForm Options class for PermissionedForm Extends the django.forms.Form metaclass with support for an inner `class Meta` that accepts     a `field_permissions` configuration option An extension to `django.forms.Form` to accept an optional `for_user` keyword argument     indicating the user the form will be presented to.      Any fields named in the `field_permissions` dict in Meta will apply a permission test on the     named permission using `User.has_perm`; if the user lacks that permission, the field will be     omitted from the form. Options class for PermissionedModelForm; extends ModelForm's options to accept     `field_permissions` Metaclass for PermissionedModelForm; extends the ModelForm metaclass to use     PermissionedModelFormOptions in place of ModelFormOptions and thus accept the     `field_permissions` option.      Note that because ModelForm does not participate in the OptionCollectingMetaclass logic, this     has the slightly hacky effect of letting ModelFormMetaclass construct a ModelFormOptions object     for the lifetime of ModelFormMetaclass.__new__, which we promptly throw out and recreate as a     PermissionedModelFormOptions object. A ModelForm that implements the `for_user` keyword argument from PermissionedForm