class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet): """ A simple ViewSet for viewing and editing accounts. """ queryset = models.Track.objects.all().for_nested_serialization() serializer_class = serializers.TrackSerializer permission_classes = [common_permissions.ConditionalAuthentication] filter_class = filters.TrackFilter ordering_fields = ( "creation_date", "title", "album__release_date", "size", "artist__name", ) def get_queryset(self): queryset = super().get_queryset() filter_favorites = self.request.GET.get("favorites", None) user = self.request.user if user.is_authenticated and filter_favorites == "true": queryset = queryset.filter(track_favorites__user=user) queryset = queryset.annotate_playable_by_actor( utils.get_actor_from_request(self.request)).annotate_duration() if (hasattr(self, "kwargs") and self.kwargs and self.request.method.lower() == "get"): # we are detailing a single track, so we can add the overhead # to fetch additional data queryset = queryset.annotate_file_data() return queryset.distinct() @detail_route(methods=["get"]) @transaction.non_atomic_requests def lyrics(self, request, *args, **kwargs): try: track = models.Track.objects.get(pk=kwargs["pk"]) except models.Track.DoesNotExist: return Response(status=404) work = track.work if not work: work = track.get_work() if not work: return Response({"error": "unavailable work "}, status=404) lyrics = work.fetch_lyrics() try: if not lyrics.content: tasks.fetch_content(lyrics_id=lyrics.pk) lyrics.refresh_from_db() except AttributeError: return Response({"error": "unavailable lyrics"}, status=404) serializer = serializers.LyricsSerializer(lyrics) return Response(serializer.data) libraries = detail_route(methods=["get"])(get_libraries( filter_uploads=lambda o, uploads: uploads.filter(track=o)))
def decorator(func): if detail: decorated = detail_route(methods, **kwargs)(func) else: decorated = list_route(methods, **kwargs)(func) decorated.url_path = kwargs.pop('url_path', None) or func.__name__ decorated.url_name = kwargs.pop('url_name', None) or func.__name__.replace('_', '-') decorated.mapping = MethodMapper(func, methods) decorated.kwargs = kwargs return decorated
def relationship_route(*args, **kwargs): """ Convenience decorator to support a JSON API relationship link Instead of using a DRF detail_route 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 detail_route(*args, **kwargs)
def async_action(wrapped_action): def step(it): #TODO check if time is as expected when task is actually launched, issue a warning if it isn't try: time_delta = next(it) except StopIteration: return tornado.ioloop.IOLoop.instance().add_timeout(timedelta(seconds=time_delta), step, it) def wrapper(model, request, *args, **kwargs): it = wrapped_action(model, request, *args, **kwargs) step(it) return Response() return detail_route(methods=['post'])(wrapper)
def add_transition_actions(Klass): Model = Klass.queryset.model fsm_fields = [ f.name for f in Model._meta.fields if isinstance(f, FSMFieldMixin) ] if len(fsm_fields) == 0: raise ImproperlyConfigured( "There is no FSM field at {0}".format(Model)) if len(fsm_fields) > 1: raise ImproperlyConfigured( "There is more than one FSM field at {0}".format(Model)) method_name = "get_all_{0}_transitions".format(fsm_fields[0]) for f in getattr(Model, method_name)(Model()): def get_fn(name_): name = copy(name_) def fn(self, request, **kwargs): args = self.get_serializer(data=request.data) args.is_valid(raise_exception=True) obj = self.get_object() transition = getattr(obj, name) if can_proceed(transition) and\ has_transition_perm(transition, request.user): try: transition(**args.validated_data) except TransitionNotAllowed as err: raise ValidationError(str(err)) obj.save() return Response(status=204) else: raise ValidationError( "You cant perform '{}' transition".format(name)) return fn serializer_class = f.custom.get('args_serializer') or Serializer setattr( Klass, f.name, detail_route(methods=['post'], serializer_class=serializer_class)(get_fn(f.name))) return Klass
class ArtistViewSet(viewsets.ReadOnlyModelViewSet): queryset = models.Artist.objects.all() serializer_class = serializers.ArtistWithAlbumsSerializer permission_classes = [common_permissions.ConditionalAuthentication] filter_class = filters.ArtistFilter ordering_fields = ("id", "name", "creation_date") def get_queryset(self): queryset = super().get_queryset() albums = models.Album.objects.with_tracks_count() albums = albums.annotate_playable_by_actor( utils.get_actor_from_request(self.request)) return queryset.prefetch_related(Prefetch("albums", queryset=albums)).distinct() libraries = detail_route(methods=["get"])( get_libraries(filter_uploads=lambda o, uploads: uploads.filter( Q(track__artist=o) | Q(track__album__artist=o))))
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', ()) return detail_route(*args, **kwargs)
def decorator(Klass): Model = Klass.queryset.model fsm_fields = [ f.name for f in Model._meta.fields if isinstance(f, FSMFieldMixin) ] if len(fsm_fields) == 0: raise ImproperlyConfigured( "There is no FSM field at '{0}'".format(Model)) if len(fsm_fields) > 1: raise ImproperlyConfigured( "There is more than one FSM field at '{0}'".format(Model)) method_name = "get_all_{0}_transitions".format(fsm_fields[0]) for f in getattr(Model, method_name)(Model()): # Skip methods which explicitly excluded via using `viewset=False` # in custom attr if not f.custom.get('viewset', True): continue serializer_class = serializers.get(f.name, None) check_args(f, serializer_class) if not serializer_class: # Use an empty serializer for the transition # without data argument serializer_class = Serializer setattr( Klass, f.name, detail_route( methods=['post'], # Skip permissions because transition has owns permission_classes=(), serializer_class=serializer_class)(get_view_fn(f.name))) return Klass
class AlbumViewSet(viewsets.ReadOnlyModelViewSet): queryset = (models.Album.objects.all().order_by( "artist", "release_date").select_related()) serializer_class = serializers.AlbumSerializer permission_classes = [common_permissions.ConditionalAuthentication] ordering_fields = ("creation_date", "release_date", "title") filter_class = filters.AlbumFilter def get_queryset(self): queryset = super().get_queryset() tracks = models.Track.objects.annotate_playable_by_actor( utils.get_actor_from_request( self.request)).select_related("artist") if (hasattr(self, "kwargs") and self.kwargs and self.request.method.lower() == "get"): # we are detailing a single album, so we can add the overhead # to fetch additional data tracks = tracks.annotate_duration() qs = queryset.prefetch_related(Prefetch("tracks", queryset=tracks)) return qs.distinct() libraries = detail_route(methods=["get"])(get_libraries( filter_uploads=lambda o, uploads: uploads.filter(track__album=o)))
def link(): return lambda func: detail_route()(func)
def action(): return lambda func: detail_route(methods=['post'])(func)
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: """Get an object using our faked queryset.""" lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field try: return self.get_queryset()[self.kwargs[lookup_url_kwarg]]
def link(): return lambda func: decorators.detail_route()(func)
class BiobrickViewSet(UserPaginationMixin, BaseBrickViewSet, BrickLookupMixin, viewsets.ReadOnlyModelViewSet): renderer_classes = (JSONRenderer, ) lookup_value_regex = brick_lookup_regex def get_object(self): if hasattr(self, '_object'): return self._object self._object = self.get_brick_object() return self._object def parse_query(self): queryset = SearchQuerySet() statements = self.request.query_params.get('q', '').split() keywords = [] types = [] names = [] orderings = [] authors = [] highlight = False for statement in statements: if statement.startswith('o:'): orderings.append(statement[2:]) if statement.startswith('n:'): names.append(statement[2:]) elif statement.startswith('t:'): types.append(statement[2:]) elif statement.startswith('h:'): highlight = True elif statement.startswith('a:'): authors.append(statement[2:]) else: keywords.append(statement) condition = None for field, items in (('text', keywords), ('part_name', names), ('part_type', types), ('author', authors)): if items: clause = reduce( or_, map(lambda kw: SQ(**{field + '__contains': kw}), items)) if condition is not None: condition &= clause else: condition = clause if condition is not None: queryset = queryset.filter(condition) if not orderings: orderings = ['-weight', '-creation_date'] orderings = unique(orderings) queryset = queryset.order_by(*orderings) if highlight: queryset = queryset.highlight( pre_tags=['<span class="highlight">'], post_tags=['</span>']) return queryset def list(self, request, *args, **kwargs): context = OrderedDict() queryset = self.parse_query() queryset.load_all() page = self.paginate_queryset(queryset.order_by('-weight')) if page is not None: serializer = self.get_serializer(page, many=True) response = self.get_paginated_response(serializer.data) else: serializer = self.get_serializer(queryset, many=True) response = Response(serializer.data) response.data.update(context) return response @list_route(methods=['GET']) def popular(self, request, *args, **kwargs): try: size = int(request.query_params.get('size', 4)) except ValueError: size = 4 return Response(brick_views_manager.get_by_random(size)) @detail_route(methods=['GET']) def stats(self, request, *args, **kwargs): user = request.user if not user.is_authenticated(): return Response({}) try: brick_meta = BiobrickMeta.objects.only('part_name').get( **self.get_brick_lookup_options()) except BiobrickMeta.DoesNotExist: return Response({}) result = {} for name, model in (('watched', WatchingUser), ('rated', RatedUser), ('starred', StarredUser)): try: obj = model.objects.get(user=user, brick=brick_meta) result[name] = True if name == 'rated': result['score'] = obj.score except model.DoesNotExist: result[name] = False return Response(result) def retrieve(self, request, *args, **kwargs): brick = self.get_object() brick_views_manager.handle_request( request, brick.part_name) # Process views count if not request.query_params.get('nofetch', '') and brick.should_fetch: try: brick.fetch() except SpiderError as e: logging.error(str(e)) serializer = BiobrickSerializer(brick, context=dict(request=request)) return Response(serializer.data) # User-Biobrick m2m retrieving for view_name, attribute in (('users_watching', ) * 2, ('users_rated', ) * 2, ('users_starred', ) * 2): locals()[view_name] = detail_route(methods=['GET'])( # use closure to generate a scope ( lambda attribute: # view lambda self, *args, **kwargs: self.paginate_user_queryset( getattr(self.get_object(), attribute).all()))(attribute)) def assert_brick_action(self, action, success='OK', fail='Fail'): """ This is a helper function to reduce code duplication. `action` should be a callable accepting a Brick object and returning a bool. `success` and `fail` are optional response payload. """ if action(self.get_object()): return Response(success) else: raise ValidationError(fail) # Extra actions for view_name, action in (('watch', lambda self, b: b.watch(self.request.user)), ('unwatch', lambda self, b: b.unwatch(self.request.user)), ('star', lambda self, b: b.star(self.request.user)), ('unstar', lambda self, b: b.unstar(self.request.user))): locals()[view_name] = detail_route( methods=['POST'], permission_classes=(permissions.IsAuthenticated, ))( # use closure to generate a scope ( lambda action: # view lambda self, *args, **kwargs: self.assert_brick_action( lambda b: action(self, b)))(action)) @detail_route(methods=['POST'], permission_classes=(permissions.IsAuthenticated, )) def rate(self, request, *args, **kwargs): serializer = RateSerializer(data=request.data, context={ 'user': request.user, 'brick': self.get_object() }) if serializer.is_valid(raise_exception=True): result = serializer.save() return self.assert_brick_action(lambda b: result) @detail_route(methods=['GET']) def related(self, request, *args, **kwargs): part_name = self.get_brick_lookup_options()['part_name'] return Response(brick_getter.get_related_bricks(part_name))
def action(detail=True, methods=None): return detail_route(methods)