Example #1
0
class GrantStaffObjectsView(djarg.views.SuccessMessageMixin,
                            djarg.views.ObjectsFormView):
    model = User
    func = arg.s(
        arg.contexts(error_collector=raise_trapped_errors),
        arg.parametrize(user=arg.val('objects')),
        arg.contexts(trap_errors),
    )(grant_staff_access)
    template_name = 'tests/grant_staff_access.html'
    form_class = GrantAccessObjectForm
    success_url = '.'
    success_message = '{granter} successfully granted staff access to users.'

    def get_default_args(self):
        return {**super().get_default_args(), **{'granter': self.request.user}}
 def wrapper(cls):
     return arg.s(
         arg.contexts(trapped_errors=daf.contrib.raise_trapped_errors),
         arg.defaults(
             objects=arg.first(
                 'objects',
                 daf.contrib.single_list('object'),
                 daf.contrib.single_list(cls.object_arg),
             )
         ),
         arg.defaults(objects=djarg.qset('objects', qset=cls.queryset)),
         arg.parametrize(**{cls.object_arg: arg.val('objects')}),
         arg.contexts(daf.contrib.trap_errors),
         super().wrapper,
     )
Example #3
0
    def wrapper(cls):
        arg_decs = []
        if cls.select_for_update is not None:  # pragma: no branch
            arg_decs = [arg.contexts(transaction.atomic)]

        arg_decs += [
            arg.defaults(
                **{
                    cls.objects_arg:
                    arg.first(
                        'objects',
                        daf.contrib.single_list('object'),
                        cls.objects_arg,
                    )
                }),
            arg.defaults(
                **{
                    cls.objects_arg:
                    djarg.qset(
                        cls.objects_arg,
                        qset=cls.queryset,
                        select_for_update=cls.select_for_update,
                    )
                }),
            super().wrapper,
        ]

        return arg.s(*arg_decs)
Example #4
0
    def wrapper(cls):
        arg_decs = []
        if cls.select_for_update is not None:  # pragma: no branch
            arg_decs = [arg.contexts(transaction.atomic)]

        arg_decs += [
            arg.contexts(trapped_errors=daf.contrib.raise_trapped_errors),
            arg.defaults(objects=arg.first(
                'objects',
                daf.contrib.single_list('object'),
                daf.contrib.single_list(cls.object_arg),
            )),
            arg.defaults(objects=djarg.qset(
                'objects',
                qset=cls.queryset,
                select_for_update=cls.select_for_update,
            )),
            arg.parametrize(**{cls.object_arg: arg.val('objects')}),
            arg.contexts(daf.contrib.trap_errors),
            super().wrapper,
        ]

        return arg.s(*arg_decs)
Example #5
0
class ViewInterface(daf.interfaces.Interface, djarg.views.SuccessMessageMixin):
    """
    Base interface all action views must inherit.

    Automatically wraps the action with
    ``daf.contrib.raise_contextualized_error``, which will raise errors
    contextualized based on various factors.
    """

    type = 'view'
    wrapper = arg.contexts(daf.contrib.raise_contextualized_error)

    @classmethod
    def build_interface(cls, **kwargs):
        view = cls.as_view(**kwargs)
        if cls.permission_required:
            view = permission_required(cls.permission_uri,
                                       raise_exception=True)(view)

        return view
class Action(metaclass=ActionMeta):
    """
    The core Action class.

    Given an ``app_label`` and ``callable``, the Action class automatically
    generates attributes that can be overridden by a user. These attributes
    influence every interface built directly from the Action. Change attributes
    on the Action object to affect every interface.
    """

    ###
    # Static action properties.
    #
    # Static action properties can only be set directly on the class.
    # These properties are all queryable in the action registry.
    ###

    @daf.utils.classproperty
    def name(cls):
        """The identifying name of the action"""
        return arg.s()(cls.func).func.__name__

    #: The app to which the action belongs.
    app_label = ''

    @daf.utils.classproperty
    def uri(cls):
        """The URI is the unique identifier for the action."""
        return f'{cls.app_label}.{cls.name}'

    @daf.utils.classproperty
    def url_name(cls):
        """The default URL name for URL-based interfaces"""
        return f'{cls.app_label}_{cls.name}'

    @daf.utils.classproperty
    def url_path(cls):
        """The default URL name for URL-based interfaces"""
        return os.path.join(
            cls.app_label.replace('_', '-'), cls.name.replace('_', '-')
        )

    @daf.utils.classproperty
    def permission_codename(cls):
        """
        Returns the name of the permission associate with the action
        """
        return f'{cls.app_label}_{cls.name}_action'

    @daf.utils.classproperty
    def permission_uri(cls):
        """
        The full permission URI, which includes the "daf" app label
        under which all DAF permissions are saved
        """
        return f'daf.{cls.permission_codename}'

    ###
    # Dynamic action properties
    #
    # Dynamic action properties can be set on the class or dynamically
    # determined with an associated get_{property_name} function.
    # Some dynamic properties will take different arguments depending on
    # the context of how they are called. For example, the success URL
    # is only obtained after a successful action run, so it contains
    # all returned values.
    ###

    @daf.utils.classproperty
    def display_name(cls):
        """The display name is used to render UI headings and other elements"""
        return cls.name.replace('_', ' ').title()

    @daf.utils.classproperty
    def success_message(cls):
        """The success message displayed after successful action runs"""
        return f'Successfully performed "{cls.display_name.lower()}"'

    @classmethod
    def get_success_message(cls, args, results):
        """Obtains a success message based on callable args and results"""
        return cls.success_message

    #: The URL one goes to after a successful action
    success_url = '.'

    @classmethod
    def get_success_url(cls, args, results):
        """Obtain a success url based on callable args and results"""
        return cls.success_url

    ###
    # Action running.
    #
    # The wrapper around the action function in constructed, and the
    # action itself can be executed with __call__.
    ###

    #: The main action callable
    callable = None

    #: The wrapper around the callable. Attach exception metadata
    #: by default for interoperability with other tools
    wrapper = arg.contexts(daf.contrib.attach_error_metadata)

    @classmethod
    def get_wrapper(cls):
        # A utility so that instance methods can safely access
        # the class wrapper variable. self.wrapper() will use
        # "self" as an argument when calling
        return cls.wrapper

    @daf.utils.classproperty
    def func(cls):
        """The function called by the action"""
        return cls.get_wrapper()(cls.callable)

    def __call__(self, *args, **kwargs):
        """
        A utility for calling the main action. Note that this is not
        used
        """
        return self.func(*args, **kwargs)

    ###
    # Action interfaces.
    #
    # These properties are not meant to be overridden. They are
    # determined as interface classes are created for an action.
    ###

    # The interfaces registered to the action
    interfaces = {}

    ###
    # Abstract properties.
    #
    # These properties help in creating abstract actions. Abstract
    # actions are not registered and are used to build other actions.
    ###

    # True if the class is abstract. Note this property must be
    # overridden in each child class to declare it as abstract.
    abstract = True

    @daf.utils.classproperty
    def is_abstract(cls):
        """
        True if the action is an abstract action, False otherwise
        Do not override this helper, otherwise actual abstract
        actions could appear as concrete
        """
        return cls.__dict__.get('abstract', False)

    # True if the action should not populate the registry
    unregistered = False

    ###
    # Action class checkers.
    #
    # When actions are registered, class definitions are checked to ensure
    # actions are set up correctly.
    ###

    @classmethod
    def definition_error(cls, msg):
        raise AttributeError(f'{cls.__name__} - {msg}')

    @classmethod
    def check_class_definition(cls):
        """
        Verifies all properties have been filled out properly for the action
        class. Called by the metaclass only on concrete actions
        """
        if not cls.callable:
            cls.definition_error('Must provide "callable" attribute.')

        if not re.match(r'\w+', cls.name):
            cls.definition_error('Must provide alphanumeric "name" attribute.')

        if not re.match(r'\w+', cls.app_label):
            cls.definition_error(
                'Must provide alphanumeric "app_label" attribute.'
            )

        if len(cls.permission_codename) > 100:
            cls.definition_error(
                f'The permission_codename "{cls.permission_codename}"'
                ' exceeds 100 characters. Try making a shorter action name'
                ' or manually overridding the permission_codename attribute.'
            )
class DetailAction(daf.interfaces.Interface):
    """
    The interface for constructing detail actions in rest framework
    viewsets.
    """

    namespace = 'rest_framework'
    type = 'detail_action'
    exception_class = APIException
    wrapper = arg.contexts(
        functools.partial(raise_drf_error, exception_class=exception_class),
        daf.contrib.raise_contextualized_error,
    )

    #: Define a form class to parse POST parameters through a Django form
    form_class = forms.Form

    #: True if objects should be re-fetched before they are serialized
    #: and returned as a response
    refetch_for_serialization = True

    #: Methods for the action. Defaults to ["post"] if None
    methods = None

    def __init__(self, viewset, request, pk):
        self.viewset = viewset
        self.request = request
        self.pk = pk

    @daf.utils.classproperty
    def url_name(cls):
        return cls.action.name.replace('_', '-') + '-detail-action'

    @daf.utils.classproperty
    def url_path(cls):
        return cls.action.name.replace('_', '-')

    def get_object(self):
        return self.viewset.get_object()

    def get_default_args(self):
        return {'object': self.get_object(), 'request': self.request}

    def run(self):
        request_args = self.request.data
        form = self.form_class(request_args)
        form.full_clean()
        self.args = {
            **self.get_default_args(),
            **request_args,
            **form.cleaned_data,
        }

        def _validate_form():
            if not form.is_valid():
                raise exceptions.ValidationError(form.errors)

        wrapper = arg.s(self.get_wrapper(), arg.validators(_validate_form))
        self.result = wrapper(self.action.func)(**self.args)

        object_to_serialize = self.result

        # Object actions may be parametrized and return a list by default.
        # Return only one object if this is the case
        if (isinstance(object_to_serialize, list)
                and len(object_to_serialize) == 1):
            object_to_serialize = object_to_serialize[0]

        if self.refetch_for_serialization:
            object_to_serialize = self.get_object()

        serializer = self.viewset.get_serializer(
            object_to_serialize, context={'request': self.request})
        return Response(serializer.data)

    @classmethod
    def as_interface(cls,
                     url_name=None,
                     url_path=None,
                     methods=None,
                     **kwargs):
        """
        Creates a DRF action from a the interface.

        Args:
            url_name (str, default=cls.url_name): The url_name
                argument that is passed to the DRF @action decorator.
            url_path (str, default=cls.url_path): The url_path
                argument that is passed to the DRF @action decorator.
            methods (list, default=[POST]): The list of methods over
                which the action will be available.
            **kwargs: Any additional argument accepted by the drf.action
                decorator.
        """
        def _drf_detail_action(viewset, request, pk, **kwargs):
            """
            The code that is executed in the DRF viewset
            """
            return cls(viewset, request, pk).run()

        url_name = url_name or cls.url_name
        url_path = url_path or cls.url_path
        methods = methods or cls.methods or ['post']

        func = _drf_detail_action
        func.__name__ = 'detail_' + cls.action.name
        func.__doc__ = cls.__doc__

        return drf_decorators.action(
            methods=methods,
            detail=True,
            url_path=url_path,
            url_name=url_name,
            **kwargs,
        )(func)
Example #8
0
class DetailAction(daf.interfaces.Interface):
    """
    The interface for constructing detail actions in rest framework
    viewsets.
    """

    namespace = 'rest_framework'
    type = 'detail_action'
    exception_class = APIException
    wrapper = arg.contexts(
        functools.partial(raise_drf_error, exception_class=exception_class),
        daf.contrib.raise_contextualized_error,
    )

    #: Define a form class to parse POST parameters through a Django form
    form_class = forms.Form

    #: True if objects should be re-fetched before they are serialized
    #: and returned as a response
    refetch_for_serialization = True

    #: Methods for the action. Defaults to ["post"] if None
    methods = None

    def __init__(self, viewset, request, pk):
        self.viewset = viewset
        self.request = request
        self.pk = pk

    @daf.utils.classproperty
    def url_name(cls):
        return cls.action.name.replace('_', '-') + '-detail-action'

    @daf.utils.classproperty
    def url_path(cls):
        return cls.action.name.replace('_', '-')

    def get_object(self):
        return self.viewset.get_object()

    def get_default_args(self):
        return {'object': self.get_object(), 'request': self.request}

    def run(self):
        request_args = self.request.data
        form = self.form_class(request_args)
        default_args = {**self.get_default_args(), **request_args}
        form = djarg.forms.adapt(
            form, self.action.func, default_args, clean=False
        )
        form.full_clean()

        self.args = {**default_args, **form.cleaned_data}

        def _validate_form():
            if not form.is_valid():
                raise exceptions.ValidationError(form.errors)

        wrapper = arg.s(self.get_wrapper(), arg.validators(_validate_form))
        self.result = wrapper(self.action.func)(**self.args)

        object_to_serialize = self.result

        # Object actions may be parametrized and return a list by default.
        # Return only one object if this is the case
        if (
            isinstance(object_to_serialize, list)
            and len(object_to_serialize) == 1
        ):
            object_to_serialize = object_to_serialize[0]

        if self.refetch_for_serialization:
            object_to_serialize = self.get_object()

        serializer = self.viewset.get_serializer(
            object_to_serialize, context={'request': self.request}
        )
        return Response(serializer.data)

    @classmethod
    def as_interface(
        cls, url_name=None, url_path=None, methods=None, **kwargs
    ):
        """
        Creates a DRF action from a the interface.

        Args:
            url_name (str, default=cls.url_name): The url_name
                argument that is passed to the DRF @action decorator.
            url_path (str, default=cls.url_path): The url_path
                argument that is passed to the DRF @action decorator.
            methods (list, default=[POST]): The list of methods over
                which the action will be available.
            **kwargs: Any additional argument accepted by the drf.action
                decorator.
        """
        # NOTE(@tomage): Moving this import in here, as if it is on module top-
        # level, it results in an error if `daf.rest_framework` is imported
        # prematurely in another process (e.g. before Django has loaded up the
        # settings module).
        # It is generally discouraged that libraries do this (see django docs)
        # and this issue has been reported to DRF in particular (see
        # here: https://github.com/encode/django-rest-framework/issues/6030).
        import rest_framework.decorators as drf_decorators

        def _drf_detail_action(viewset, request, pk, **kwargs):
            """
            The code that is executed in the DRF viewset
            """
            return cls(viewset, request, pk).run()

        url_name = url_name or cls.url_name
        url_path = url_path or cls.url_path
        methods = methods or cls.methods or ['post']

        func = _drf_detail_action
        func.__name__ = 'detail_' + cls.action.name
        func.__doc__ = cls.__doc__

        return drf_decorators.action(
            methods=methods,
            detail=True,
            url_path=url_path,
            url_name=url_name,
            **kwargs,
        )(func)