class ApiViewSet(viewsets.ViewSet): def list(self, request): return Response("Este endpoint utiliza o verbo GET.") def create(self, request): return Response(f"Este endpoint utiliza o verbo POST.") action(detail=False, methods=['put']) def put(self, request): return Response("Este endpoint utiliza o verbo PUT.") action(detail=False, methods=['post']) def post_dado(self, request, dado): request.data['dado'] return Response(f"Este endpoint recebe o nome {dado} via URL.") action(detail=False, methods=['put']) def put_dado(self, request, *args, **kwargs): return Response(f"Este endpoint recebe o nome {dado} via URL.") action(detail=False, methods=['delete']) def delete(self, request): return Response(f"Este endpoint utiliza o verbo DELETE.")
def project_action(viewset_method=None, serializer_class=serializers.Serializer): """ Decorator that turns a project viewset method into an action that receives the project as its only argument. """ # If no viewset method is given, return a decorator function if viewset_method is None: return partial(project_action, serializer_class=serializer_class) # Define the wrapper that processes the incoming data and finds the project @wraps(viewset_method) def wrapper(viewset, request, pk=None): # Get the project first - this will also process permissions project = viewset.get_object() # Then process the input data according to the serializer serializer = viewset.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) # Then call the view function return viewset_method(viewset, project, serializer.data) # Wrap the viewset method in the action decorator action_decorator = action(detail=True, methods=['POST'], serializer_class=serializer_class) return action_decorator(wrapper)
def fetches_route(): @transaction.atomic def fetches(self, request, *args, **kwargs): obj = self.get_object() if request.method == "GET": queryset = models.Fetch.objects.get_for_object(obj).select_related( "actor") queryset = queryset.order_by("-creation_date") filterset = filters.FetchFilter(request.GET, queryset=queryset) page = self.paginate_queryset(filterset.qs) if page is not None: serializer = api_serializers.FetchSerializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = api_serializers.FetchSerializer(queryset, many=True) return response.Response(serializer.data) if request.method == "POST": if utils.is_local(obj.fid): return response.Response( {"detail": "Cannot fetch a local object"}, status=400) fetch = models.Fetch.objects.create(url=obj.fid, actor=request.user.actor, object=obj) common_utils.on_commit(tasks.fetch.delay, fetch_id=fetch.pk) serializer = api_serializers.FetchSerializer(fetch) return response.Response(serializer.data, status=status.HTTP_201_CREATED) return decorators.action( methods=["get", "post"], detail=True, permission_classes=[permissions.IsAuthenticated], )(fetches)
class ActorViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): queryset = models.Actor.objects.select_related("user", "channel", "summary_obj", "attachment_icon") permission_classes = [ConditionalAuthentication] serializer_class = api_serializers.FullActorSerializer lookup_field = "full_username" lookup_value_regex = r"([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)" def get_object(self): queryset = self.get_queryset() username, domain = self.kwargs["full_username"].split("@", 1) return queryset.get(preferred_username=username, domain_id=domain) def get_queryset(self): qs = super().get_queryset() qs = qs.exclude( domain__instance_policy__is_active=True, domain__instance_policy__block_all=True, ) if preferences.get("moderation__allow_list_enabled"): query = Q(domain_id=settings.FUNKWHALE_HOSTNAME) | Q( domain__allowed=True) qs = qs.filter(query) return qs libraries = decorators.action(methods=[ "get" ], detail=True)(music_views.get_libraries( filter_uploads=lambda o, uploads: uploads.filter(library__actor=o)))
def decorator(func: _t.Callable): # pylint: disable=used-before-assignment def wrapper(view: NestedViewMixin, request: drf_request.Request, *args, **kwargs): # Nested name view.nested_name = name # Allow append to nested or only create view.nested_allow_append = allow_append # ID name of nested object view.nested_arg = request_arg view.nested_append_arg = append_arg view.nested_id = kwargs.get(view.nested_arg, None) view.nested_view_object = None view._nested_filter_class = _nested_filter_class return func(view, request, *args) wrapper.__name__ = func.__name__ kwargs['methods'] = methods kwargs['detail'] = True kwargs['url_path'] = path kwargs['url_name'] = kwargs.pop('url_name', name) view: NestedViewMixin = action(*args, **kwargs)(wrapper) # type: ignore view._nested_args = _nested_args view._nested_manager = manager_name or name view._nested_filter_class = _nested_filter_class if arg: view._nested_args[name] = request_arg return view
def decorator(func): def wrapper(view, request, *args, **kwargs): # Nested name view.nested_name = name # Nested parent object view.nested_parent_object = view.get_object() # Allow append to nested or only create view.nested_allow_append = allow_append # ID name of nested object view.nested_arg = request_arg view.nested_append_arg = append_arg view.nested_id = kwargs.get(view.nested_arg, None) view.nested_manager = getattr( view.nested_parent_object, manager_name or name, None ) view.nested_view_object = None view._nested_filter_class = _nested_filter_class return func(view, request, *args) wrapper.__name__ = func.__name__ kwargs['methods'] = methods kwargs['detail'] = True kwargs['url_path'] = path kwargs['url_name'] = kwargs.pop('url_name', name) view = action(*args, **kwargs)(wrapper) view._nested_args = _nested_args view._nested_manager = manager_name or name view._nested_filter_class = _nested_filter_class if arg: view._nested_args[name] = request_arg return view
def decorator(func): func = action(methods=[method], detail=True, **kwargs)(func) func.action_type = 'custom' func.action_kwargs = action_kwargs(icon_class, btn_class, text, func, kwargs) func.kwargs = {} return func
def decorator(func): func = action(methods=[method], detail=False, **kwargs)(func) func.action_type = 'bulk' func.action_kwargs = action_kwargs(icon_class, btn_class, text, func, kwargs) func.action_kwargs['atOnce'] = func.action_kwargs.get('atOnce', True) func.kwargs = {} return func
def decorator(func): func_object = action(*args, **kwargs)(func) override_kw = dict() if response_code: override_kw['responses'] = {response_code: response_serializer()} if operation_description: override_kw['operation_description'] = operation_description else: override_kw['operation_description'] = str(func.__doc__ or '').strip() return swagger_auto_schema(**override_kw)(func_object)
def decorator(func): if action is not None: func = action(methods=[method], detail=True, **kwargs)(func) else: func.detail = True func.bind_to_methods = [method] func.action_type = 'custom' func.action_kwargs = action_kwargs(icon_class, btn_class, text, func, kwargs) func.kwargs = {} return func
def test_view_name_kwargs(self): """ 'name' and 'suffix' are mutually exclusive kwargs used for generating a view's display name. """ # by default, generate name from method @action(detail=True) def test_action(request): raise NotImplementedError assert test_action.kwargs == { 'description': None, 'name': 'Test action', } # name kwarg supersedes name generation @action(detail=True, name='test name') def test_action(request): raise NotImplementedError assert test_action.kwargs == { 'description': None, 'name': 'test name', } # suffix kwarg supersedes name generation @action(detail=True, suffix='Suffix') def test_action(request): raise NotImplementedError assert test_action.kwargs == { 'description': None, 'suffix': 'Suffix', } # name + suffix is a conflict. with pytest.raises(TypeError) as excinfo: action(detail=True, name='test name', suffix='Suffix') assert str(excinfo.value ) == "`name` and `suffix` are mutually exclusive arguments."
def get_extra_actions(cls): extra_actions = super().get_extra_actions() # type: ignore if cls.time_bucket_serializer_class is not None: extra_actions.append( action( detail=False, methods=["get"], serializer_class=cls.time_bucket_serializer_class, )(cls.stats)) return extra_actions
def decorator(func): if action is not None: func = action(methods=[method], detail=False, **kwargs)(func) else: func.bind_to_methods = [method, ] func.detail = False func.action_type = 'bulk' func.action_kwargs = action_kwargs(icon_class, btn_class, text, func, kwargs) func.action_kwargs['atOnce'] = func.action_kwargs.get('atOnce', True) func.kwargs = {} return func
def test_view_name_kwargs(self): """ 'name' and 'suffix' are mutually exclusive kwargs used for generating a view's display name. """ # by default, generate name from method @action(detail=True) def test_action(request): raise NotImplementedError assert test_action.kwargs == { 'description': None, 'name': 'Test action', } # name kwarg supersedes name generation @action(detail=True, name='test name') def test_action(request): raise NotImplementedError assert test_action.kwargs == { 'description': None, 'name': 'test name', } # suffix kwarg supersedes name generation @action(detail=True, suffix='Suffix') def test_action(request): raise NotImplementedError assert test_action.kwargs == { 'description': None, 'suffix': 'Suffix', } # name + suffix is a conflict. with pytest.raises(TypeError) as excinfo: action(detail=True, name='test name', suffix='Suffix') assert str(excinfo.value) == "`name` and `suffix` are mutually exclusive arguments."
def __new__(cls, name, bases, dct): _class = super().__new__(cls, name, bases, dct) # The class needs the field FSM_MODELFIELDS to know which transitions it needs to add if hasattr(_class, "get_model"): model = _class.get_model() if model: setattr(_class, "FSM_BUTTONS", getattr(_class, "FSM_BUTTONS", set())) # The model potentially has multiple FSMFields, which needs to be iterated over for field in filter(lambda f: isinstance(f, FSMField), model._meta.fields): # Get all transitions, by calling the partialmethod defined by django-fsm transitions = getattr( model, f"get_all_{field.name}_transitions")(model()) # Since the method above can potentially return a transition multiple times # i.e. when a transitions has multiple sources, we need to filter out those transitions _discovered_transitions = list() for transition in transitions: if transition.name in _discovered_transitions: continue else: _discovered_transitions.append(transition.name) # Get the Transition Button and add it to the front of the instance buttons button = transition.custom.get("_transition_button") _class.FSM_BUTTONS.add(button) # Create a method that calls fsm_route with the request and the action name method = get_method(transition) # We need to manually change the method name, otherwise django-fsm won't # Add this method to the URLs # Wrap the above defined method in the action decorator # IMPORTANT: This needs to happen after we changed the method name # therefore we cannot use the proper decorator method.__name__ = transition.name method.__doc__ = transition.method.__doc__ wrapped_method = action(detail=True, methods=["GET", "PATCH"])(method) # Set the method as a attribute of the class that implements this # metaclass setattr(_class, transition.name, wrapped_method) return _class
def action( methods: List[str], *, detail: bool, ordering: Optional[str] = None, ordering_fields: Optional[List[str]] = None, filterset_class: Optional[Type[FilterSet]] = None, **kwargs: Any, ): """ Custom action decorator that wraps rest_framework.decorators.action. Use this decorator to reset view class configurations e.g. serializer_class, ordering, etc. :param methods: List of http methods e.g. ["get", "post"] :param detail: Boolean: Switch between a detail route (True) and a list route (False). :param ordering: Optional list of strings Default ordering on the queryset. Default: None :param ordering_fields: Optional list of strings. List options for ordering. Default: None :param filterset_class: Option FilterSet class. Default: None :param kwargs: Optional keyword arguments e.g. serializer_class, filter_backends, etc. :return: Called rest_framework.decorators.action Usage: from drft.views import action class SomeViewSet(...): @action(["POST", "GET"], detail=False) def some_view(self, request: Request) -> Response: ... @action(["POST", "GET"], detail=True) def some_other_view( self, request: Request, pk: Optional[str] = None ) -> Response: ... """ defaults = dict( detail=detail, ordering=ordering, ordering_fields=ordering_fields, filterset_class=filterset_class, ) defaults.update(kwargs) return decorators.action(methods, **defaults)
def relationship_route(*args, **kwargs): """ Convenience decorator to support a JSON API relationship link Instead of using a DRF action decorator when constructing views that would resolve "Relationship Links" according to the JSON API spec, you should use this decorator. :spec: jsonapi.org/format/#fetching-relationships """ kwargs['filter_backends'] = kwargs.pop('filter_backends', ()) kwargs['url_path'] = 'relationships/%s' % kwargs.pop('relation') return action(*args, detail=True, **kwargs)
class ArtistViewSet( HandleInvalidSearch, common_views.SkipFilterForGetObject, viewsets.ReadOnlyModelViewSet, ): queryset = (models.Artist.objects.all().prefetch_related( "attributed_to", "attachment_cover").prefetch_related( "channel__actor", Prefetch( "tracks", queryset=models.Track.objects.all(), to_attr="_prefetched_tracks", ), ).order_by("-id")) serializer_class = serializers.ArtistWithAlbumsSerializer permission_classes = [oauth_permissions.ScopePermission] required_scope = "libraries" anonymous_policy = "setting" filterset_class = filters.ArtistFilter fetches = federation_decorators.fetches_route() mutations = common_decorators.mutations_route(types=["update"]) def get_object(self): obj = super().get_object() if (self.action == "retrieve" and self.request.GET.get("refresh", "").lower() == "true"): obj = refetch_obj(obj, self.get_queryset()) return obj def get_serializer_context(self): context = super().get_serializer_context() context["description"] = self.action in [ "retrieve", "create", "update" ] return context def get_queryset(self): queryset = super().get_queryset() albums = (models.Album.objects.with_tracks_count().select_related( "attachment_cover").prefetch_related("tracks")) albums = albums.annotate_playable_by_actor( utils.get_actor_from_request(self.request)) return queryset.prefetch_related(Prefetch("albums", queryset=albums), TAG_PREFETCH) libraries = action(methods=["get"], detail=True)( get_libraries(filter_uploads=lambda o, uploads: uploads.filter( Q(track__artist=o) | Q(track__album__artist=o))))
def _state_change_action(name, state: Thesis.State): def _action_method(self: 'ThesisViewSet', request: Request, *args, **kwargs): thesis = self.get_object() # type: Thesis self.get_serializer() serializer = self.get_serializer(instance=thesis, data=dict(state=state), partial=True) serializer.is_valid(raise_exception=True) serializer.save() return Response(data=serializer.data) _action_method.__name__ = name return transaction.atomic( action(methods=['patch'], detail=True)(_action_method) )
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)
def decorator(func): def wizard_func(self, request, *args, **kwargs): Serializer = serializer serializer_instance = Serializer(data=request.data) if not serializer_instance.is_valid(): return Response(serializer_instance.errors, status=400) request.validated_data = serializer_instance.validated_data return func(self, request, *args, **kwargs) wizard_func.__name__ = func.__name__ if meta_type == 'custom': detail = True else: detail = False if action is not None: wizard_func = action(methods=[kwargs.pop('method', 'post')], detail=detail, **kwargs)(wizard_func) # NoQA wizard_func.__name__ = func.__name__ else: wizard_func.bind_to_methods = [ kwargs.pop('method', 'POST'), ] wizard_func.detail = detail wizard_func.action_type = meta_type wizard_func.wizard = True wizard_func.action_kwargs = action_kwargs(icon_class, btn_class, text, wizard_func, kwargs) wizard_func.kwargs = {} if target_model is not None: wizard_func.action_kwargs['params']['model'] = '{}/{}/{}'.format( target_model._meta.app_label.lower().replace('_', '-'), inflector.pluralize(target_model._meta.model_name.lower()), wizard_func.__name__) wizard_func.serializer = serializer return Adapter.adapt_wizard(wizard_func)
def decorator(func: _t.Callable): func_object = action(*args, **kwargs)(func) override_kw: _t.Dict = { 'methods': tuple(func_object.mapping.keys()) or None } # type: ignore if response_code: override_kw['responses'] = {response_code: response_serializer()} if operation_description: override_kw['operation_description'] = operation_description else: override_kw['operation_description'] = str( func.__doc__ or '').strip() # type: ignore override_kw['x-multiaction'] = bool(is_mul) override_kw['x-require-confirmation'] = bool(require_confirmation) return swagger_auto_schema(**override_kw)(func_object) # type: ignore
def build_form(clz, form_name, form_def): def form_method(self, request, pk, *args, **kwargs): viewset = form_def["viewset"]() viewset.request = request viewset.format_kwarg = self.format_kwarg link = form_def["link"] if isinstance(link, str): serializer = viewset.get_serializer() fld = serializer.fields[link] if isinstance(fld, PrimaryKeyRelatedField): request.data[link] = self.get_object().pk else: request.data[link] = self.get_serializer( instance=self.get_object()).data method = form_def["method"] if "link_id" in form_def: link_id = request.GET.get( form_def["link_id"]) or request.data.get( form_def["link_id"]) viewset.lookup_url_kwarg = form_def["link_id"] viewset.kwargs = {viewset.lookup_url_kwarg: link_id} else: link_id = None if request._request.method == "GET": method = "retrieve" elif not method: method = "create" if not link_id else "update" return getattr(viewset, method)(request, *args, **kwargs) form_method.__name__ = form_name.replace("-", "_") return ( form_name.replace("-", "_"), action(methods=["get", "post", "patch"], detail=True, url_path=form_name)(form_method), )
def get_drf_fsm_mixin(Model, fieldname='state'): """ Find all transitions defined on `Model`, then create a corresponding viewset action method for each and apply it to `Mixin`. Finally, return `Mixin` """ class Mixin(object): save_after_transition = True @action(methods=['GET'], detail=True, url_name='possible-transitions', url_path='possible-transitions') def possible_transitions(self, request, *args, **kwargs): instance = self.get_object() return Response( { 'transitions': [ trans.name.replace('_', '-') for trans in getattr( instance, 'get_available_{}_transitions'.format( fieldname))() if trans.has_perm(instance, request.user) ] }, ) transitions = getattr(Model(), 'get_all_{}_transitions'.format(fieldname))() transition_names = set(x.name for x in transitions) for transition_name in transition_names: url = transition_name.replace('_', '-') setattr( Mixin, transition_name, action(methods=['POST'], detail=True, url_name=url, url_path=url)( get_transition_viewset_method(transition_name)), ) return Mixin
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)
def build_form(clz, form_name, form_def): def form_method(self, request, pk, *args, **kwargs): viewset = form_def['viewset']() viewset.request = request viewset.format_kwarg = self.format_kwarg link = form_def['link'] if isinstance(link, str): serializer = viewset.get_serializer() fld = serializer.fields[link] if isinstance(fld, PrimaryKeyRelatedField): request.data[link] = self.get_object().pk else: request.data[link] = self.get_serializer( instance=self.get_object()).data method = form_def['method'] if 'link_id' in form_def: link_id = request.GET.get( form_def['link_id']) or request.data.get( form_def['link_id']) viewset.lookup_url_kwarg = form_def['link_id'] viewset.kwargs = {viewset.lookup_url_kwarg: link_id} else: link_id = None if request._request.method == 'GET': method = 'retrieve' elif not method: method = 'create' if not link_id else 'update' return getattr(viewset, method)(request, *args, **kwargs) form_method.__name__ = form_name.replace('-', '_') return (form_name.replace('-', '_'), action(methods=['get', 'post', 'patch'], detail=True, url_path=form_name)(form_method))
def related_route(*args, **kwargs): """ Convenience decorator to support JSON API related resource link Instead of using a DRF detail_route decorator when constructing views that would resolve "Related Resource Links" according to the JSON API spec, you should use this decorator. It simply wraps the detail_route but skips the processing of filter_backends on the primary data since all query params provided are for the RELATED data & not the primary data. For example, `/actors/1/movies?sort=title` would ensure the sort query param would be processed on the movies resource & NOT the actors resource. :spec: jsonapi.org/format/#document-resource-object-related-resource-links """ kwargs['filter_backends'] = kwargs.pop('filter_backends', ()) kwargs['url_name'] = kwargs.get('url_name', kwargs['url_path']) return action(*args, detail=True, **kwargs)
def detail_route(methods, suffix, url_path=None): return action(detail=True, methods=methods, suffix=suffix)
NonDefaultIdSerializer, get_artists, get_albums, get_tracks, get_non_default_ids, QuerySet, Artist, Album, Track, NonDefaultId, ) try: from rest_framework.decorators import action action_route = action(detail=True, methods=["get"]) except ImportError: from rest_framework.decorators import detail_route action_route = detail_route(methods=["get"]) class BaseViewSet(viewsets.ModelViewSet): """Base view set for our "models".""" parser_classes = (JSONAPIParser, ) permission_classes = (AllowAny, ) renderer_classes = (JSONAPIRenderer, ) content_negotiation_class = JSONAPIContentNegotiation def get_object(self) -> Any:
def detail_route(methods, suffix): return action(detail=True, methods=methods, suffix=suffix)
def list_route(methods=None, **kwargs): return action(detail=False, **kwargs)