예제 #1
0
    def _build_requested_prefetches(self, prefetches, requirements, model,
                                    fields, filters, is_root_level):
        """Build a prefetch dictionary based on request requirements."""
        meta = Meta(model)
        for name, field in six.iteritems(fields):
            original_field = field
            if isinstance(field, dfields.DynamicRelationField):
                field = field.serializer
            if isinstance(field, serializers.ListSerializer):
                field = field.child
            if not isinstance(field, serializers.ModelSerializer):
                continue

            source = field.source or name
            if '.' in source:
                raise ValidationError('Nested relationship values '
                                      'are not supported')
            if source == '*':
                # ignore custom getter/setter
                continue

            if source in prefetches:
                # ignore duplicated sources
                continue

            related_queryset = getattr(original_field, 'queryset', None)
            if callable(related_queryset):
                related_queryset = related_queryset(field)

            is_id_only = getattr(field, 'id_only', lambda: False)()
            is_remote = meta.is_field_remote(source)
            is_gui_root = self.view.get_format() == 'admin' and is_root_level
            if (related_queryset is None and is_id_only and not is_remote
                    and not is_gui_root):
                # full representation and remote fields
                # should all trigger prefetching
                continue

            # Popping the source here (during explicit prefetch construction)
            # guarantees that implicitly required prefetches that follow will
            # not conflict.
            required = requirements.pop(source, None)

            query_name = Meta.get_query_name(original_field.model_field)
            prefetch_queryset = self._build_queryset(serializer=field,
                                                     filters=filters.get(
                                                         query_name, {}),
                                                     queryset=related_queryset,
                                                     requirements=required)

            # There can only be one prefetch per source, even
            # though there can be multiple fields pointing to
            # the same source. This could break in some cases,
            # but is mostly an issue on writes when we use all
            # fields by default.
            prefetches[source] = Prefetch(source, queryset=prefetch_queryset)

        return prefetches
예제 #2
0
    def _build_queryset(self,
                        serializer=None,
                        filters=None,
                        queryset=None,
                        requirements=None):
        """Build a queryset that pulls in all data required by this request.

        Handles nested prefetching of related data and deferring fields
        at the queryset level.

        Arguments:
            serializer: An optional serializer to use a base for the queryset.
                If no serializer is passed, the `get_serializer` method will
                be used to initialize the base serializer for the viewset.
            filters: An optional TreeMap of nested filters.
            queryset: An optional base queryset.
            requirements: An optional TreeMap of nested requirements.
        """

        is_root_level = False
        if serializer:
            if queryset is None:
                queryset = serializer.Meta.model.objects
        else:
            serializer = self.view.get_serializer()
            is_root_level = True

        model = serializer.get_model()

        if not model:
            return queryset

        meta = Meta(model)

        prefetches = {}

        # build a nested Prefetch queryset
        # based on request parameters and serializer fields
        fields = serializer.fields

        if requirements is None:
            requirements = TreeMap()

        self._get_implicit_requirements(fields, requirements)

        if filters is None:
            filters = self._get_requested_filters()

        # build nested Prefetch queryset
        self._build_requested_prefetches(prefetches, requirements, model,
                                         fields, filters, is_root_level)

        # build remaining prefetches out of internal requirements
        # that are not already covered by request requirements
        self._build_implicit_prefetches(model, prefetches, requirements)

        # use requirements at this level to limit fields selected
        # only do this for GET requests where we are not requesting the
        # entire fieldset
        is_gui = self.view.get_format() == 'admin'
        if ('*' not in requirements and not self.view.is_update()
                and not self.view.is_delete() and not is_gui):
            id_fields = getattr(serializer, 'get_id_fields', lambda: [])()
            # only include local model fields
            only = [
                field for field in set(id_fields + list(requirements.keys()))
                if meta.is_field(field) and not meta.is_field_remote(field)
            ]
            queryset = queryset.only(*only)

        # add request filters
        query = self._filters_to_query(filters)

        if query:
            # Convert internal django ValidationError to
            # APIException-based one in order to resolve validation error
            # from 500 status code to 400.
            try:
                queryset = queryset.filter(query)
            except InternalValidationError as e:
                raise ValidationError(
                    dict(e) if hasattr(e, 'error_dict') else list(e))
            except Exception as e:
                # Some other Django error in parsing the filter.
                # Very likely a bad query, so throw a ValidationError.
                err_msg = getattr(e, 'message', '')
                raise ValidationError(err_msg)

        # A serializer can have this optional function
        # to dynamically apply additional filters on
        # any queries that will use that serializer
        # You could use this to have (for example) different
        # serializers for different subsets of a model or to
        # implement permissions which work even in sideloads
        if hasattr(serializer, 'filter_queryset'):
            queryset = serializer.filter_queryset(queryset)

        # add prefetches and remove duplicates if necessary
        prefetch = prefetches.values()
        queryset = queryset.prefetch_related(*prefetch)
        if has_joins(queryset) or not is_root_level:
            queryset = queryset.distinct()

        if self.DEBUG:
            queryset._using_prefetches = prefetches
        return queryset