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, )
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)
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)
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)
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)