DRF - фильтр набора запросов, использующий поиск полей в SlugRelatedField
Я пытаюсь выяснить, как запустить фильтр набора запросов, используя "field__contains" в SlugRelatedField. У меня есть простая модель Book и Tag, которая выглядит следующим образом:
class Book(models.Model):
title = models.CharField(max_length=100)
authors = models.ManyToManyField(Author)
publisher = models.ForeignKey(Publisher)
publication_date = models.DateField()
class MetaTag(models.Model):
book = models.ManyToManyField('Book', related_name='meta_tags',
help_text='The book this meta tag belongs to')
value = models.CharField(max_length=400, unique=True, help_text='Meta tag value')
class BookSerializer(serializers.HyperlinkedModelSerializer):
class BookHyperlink(serializers.HyperlinkedIdentityField):
"""A Hyperlink field for book details"""
def get_url(self, obj, view_name, request, format):
url_kwargs = {
'pk': obj.id,
}
return reverse(view_name, kwargs=url_kwargs, request=request, format=format)
url = BookHyperlink(view_name='book-detail')
meta_tags = CreatableSlugRelatedField(many=True, slug_field='value', queryset=MetaTag.objects.all())
class Meta:
model = Book
fields = (
'id',
'title',
'publisher',
'publication_date',
'meta_tags',
'url'
)
class MetaTagSerializer(serializers.ModelSerializer):
class Meta:
model = MetaTag
fields = ('id', 'book', 'value',)
class CreatableSlugRelatedField(serializers.SlugRelatedField):
def to_internal_value(self, data):
try:
return self.get_queryset().get_or_create(**{self.slug_field: data})[0]
except ObjectDoesNotExist:
self.fail('does_not_exist', slug_name=self.slug_field, value=smart_text(data))
except (TypeError, ValueError):
self.fail('invalid')
class Meta:
model = MetaTag
fields = ('id', 'book', 'value', )
Теперь в моем BooksView я хочу иметь возможность фильтровать набор запросов по значению meta_tags. Я пробовал следующее с "__contains" поиск поля:
class Books(viewsets.ModelViewSet):
"""Default view for Book."""
queryset = Book.objects.all()
serializer_class = BookSerializer
permission_classes = (IsAuthenticated, )
filter_backends = (DjangoFilterBackend,)
filter_fields = tuple(f.name for f in Book._meta.get_fields())
def get_queryset(self):
search_pattern = self.request.query_params.get('search', None)
if search_pattern is not None and search_pattern is not '':
self.queryset = self.queryset.filter(meta_tags__contains = search_pattern)
return self.queryset
def get_object(self):
if self.kwargs.get('pk'):
return Book.objects.get(pk=self.kwargs.get('pk'))
Но я получаю следующую ошибку от django:
Файл "~MyProject/venv/lib/python3.6/site-packages/django/db/models/sql/query.py", строка 1076, в build_lookup повысить FieldError('Связанное поле получило недопустимый поиск: {}'. Format(lookup_name)) django.core.exceptions.FieldError: Связанное поле получило недопустимый поиск: содержит
Что, как я понимаю, означает, что, поскольку "meta_tags" не является обычным массивом или текстовым полем, поиск содержимого поля не может быть применен к этому полю.
Как лучше всего фильтровать набор запросов в этом случае по значению meta_tags?
1 ответ
Эксперт по django, с которым я консультировался по этой проблеме, предложил попробовать добавить поле "slug_field" (в данном случае "__value") к поиску поля "__contains" при использовании с внешней моделью.
Он не был задокументирован нигде или даже в официальной документации django по адресу https://docs.djangoproject.com/en/2.0/ref/contrib/postgres/fields/, поэтому у меня не было возможности узнать, как это работает, но это решение на самом деле работает:
queryset = queryset.filter(meta_tags__value__contains=search_pattern)
Это на самом деле имеет смысл, когда вы смотрите глубже на модель MetaTag, так как "value" - это внутреннее поле модели meta_tags:
class MetaTag(models.Model):
book = models.ManyToManyField('Book', related_name='meta_tags',
help_text='The book this meta tag belongs to')
value = models.CharField(max_length=400, unique=True, help_text='Meta tag value')
def __str__(self):
return '%s > %s' % (self.channel, self.value)
Причина, по которой было не так очевидно добавлять __value в первую очередь, заключается в том, что массив meta_tags (массив объектов) выравнивается с помощью сериализатора SlugRelatedField, где проецируется только поле slug_field, а остальные поля опускаются. Итак, окончательный вывод массива meta_tags плоский:
meta_tags: ['tag1','tag2']
вместо:
meta_tags: [{book: 'a', value: 'tag1'},{book: 'a', value: 'tag2'}]
Но поскольку сериализация на DRF django выполняется на поздней стадии (после завершения набора запросов), следует рассмотреть исходную схему полей.
Надеюсь, это когда-нибудь спасет чью-то головную боль.