Django get_next_by_FIELD, использующий сложный поиск Q

При создании внешнего интерфейса для модуля Django я столкнулся со следующей проблемой внутри ядра Django:

Чтобы отобразить ссылку на следующий/предыдущий объект из запроса модели, мы можем использовать дополнительные-экземпляры-методы экземпляра модели: get_next_by_FIELD() или get_previous_by_FIELD(). Где FIELD — поле модели типа DateField или DateTimeField.

Давайте объясним это на примере

from django.db import models

class Shoe(models.Model):
    created = models.DateTimeField(auto_now_add=True, null=False)
    size = models.IntegerField()

Представление для отображения списка обуви, за исключением тех, где размер равен 4:

def list_shoes(request):
    shoes = Shoe.objects.exclude(size=4)

    return render_to_response(request, {
        'shoes': shoes
    })

И пусть следующее будет представлением для отображения одной обуви и соответствующей ссылки на предыдущую и следующую обувь.

def show_shoe(request, shoe_id):
    shoe = Shoe.objects.get(pk=shoe_id)

    prev_shoe = shoe.get_previous_by_created()
    next_shoe = shoe.get_next_by_created()

    return render_to_response('show_shoe.html', {
        'shoe': shoe,
        'prev_shoe': prev_shoe,
        'next_shoe': next_shoe
    })

Теперь у меня ситуация, что представление show_shoe отображает ссылку на предыдущую/следующую независимо от размера обуви. Но на самом деле я хотел просто обувь, размер которой не равен 4. Поэтому я попытался использовать аргумент **kwargs методов get_(previous|next)_by_created(), чтобы отфильтровать ненужную обувь, как указано по документации:

Оба этих метода будут выполнять свои запросы, используя диспетчер по умолчанию для модели. Если вам нужно эмулировать фильтрацию, используемую пользовательским менеджером, или вы хотите выполнить одноразовую пользовательскую фильтрацию, оба метода также принимают необязательные аргументы ключевого слова, которые должны быть в формате, описанном в разделе Поиск полей.

Редактировать: следите за словом "следует", потому что тогда также (size_ne=4) должно работать, но это не так.

Актуальная проблема

Фильтрация с использованием поиска size__ne ...

def show_shoe(request, shoe_id):
    ...
    prev_shoe = shoe.get_previous_by_created(size__ne=4)
    next_shoe = shoe.get_next_by_created(size__ne=4)
    ...

... не сработало, выдает FieldError: Не удается преобразовать ключевое слово 'size_ne' в поле.

Затем я попытался использовать отрицательный комплекс поиск с использованием объектов Q:

from django.db.models import Q

def show_shoe(request, shoe_id):
    ...
    prev_shoe = shoe.get_previous_by_created(~Q(size=4))
    next_shoe = shoe.get_next_by_created(~Q(size=4))
    ...

... тоже не работает, выдает TypeError: _get_next_or_previous_by_FIELD() получил несколько значений для аргумента 'field'

Поскольку методы get_(previous|next)_by_created принимают только **kwargs.

Фактическое решение

Поскольку эти методы экземпляра используют _get_next_or_previous_by_FIELD(self, field , is_next, **kwargs) я изменил его, чтобы он принимал позиционные аргументы с использованием *args и передал их фильтру, как **kwargs.

def my_get_next_or_previous_by_FIELD(self, field, is_next, *args, **kwargs):
    """
    Workaround to call get_next_or_previous_by_FIELD by using complext lookup queries using
    Djangos Q Class. The only difference between this version and original version is that
    positional arguments are also passed to the filter function.
    """
    if not self.pk:
        raise ValueError("get_next/get_previous cannot be used on unsaved objects.")
    op = 'gt' if is_next else 'lt'
    order = '' if is_next else '-'
    param = force_text(getattr(self, field.attname))
    q = Q(**{'%s__%s' % (field.name, op): param})
    q = q | Q(**{field.name: param, 'pk__%s' % op: self.pk})
    qs = self.__class__._default_manager.using(self._state.db).filter(*args, **kwargs).filter(q).order_by('%s%s' % (order, field.name), '%spk' % order)
    try:
        return qs[0]
    except IndexError:
        raise self.DoesNotExist("%s matching query does not exist." % self.__class__._meta.object_name)

И называя это так:

...
prev_shoe = shoe.my_get_next_or_previous_by_FIELD(Shoe._meta.get_field('created'), False, ~Q(state=4))
next_shoe = shoe.my_get_next_or_previous_by_FIELD(Shoe._meta.get_field('created'), True, ~Q(state=4))
...

наконец сделал это.

Теперь вопрос к вам

Есть ли более простой способ справиться с этим? Должен ли shoe.get_previous_by_created(size__ne=4) работать должным образом, или мне следует сообщить об этой проблеме ребятам из Django в надежде, что они примут мое исправление _get_next_or_previous_by_FIELD()?

Среда: Django 1.7, еще не тестировал на 1.9, но код для _get_next_or_previous_by_FIELD() остался прежним.

Редактировать: Это правда, что сложные поиски с использованием объекта Q не являются частью «поиска полей», вместо этого они скорее являются частью функций filter() и exclude(). И я, вероятно, ошибаюсь, когда полагаю, что get_next_by_FIELD также должен принимать объекты Q. Но поскольку вносимые изменения минимальны, а преимущества использования объекта Q велики, я думаю, что эти изменения должны получить распространение.

теги: django, сложный поиск, запрос, get_next_by_FIELD, get_previous_by_FIELD

(список тегов здесь, потому что у меня недостаточно репутации.)


person Florin Hillebrand    schedule 24.02.2016    source источник
comment
Поскольку это мой первый вопрос, какие-нибудь подсказки по его улучшению?   -  person Florin Hillebrand    schedule 24.02.2016


Ответы (2)


Вы можете создать пользовательский поиск ne. и используйте его:

.get_next_by_created(size__ne=4)
person vsd    schedule 24.02.2016
comment
Создание собственного поиска работает! Но почему нельзя использовать size__ne из коробки? Так как он «должен» вести себя как поиск других полей. - person Florin Hillebrand; 25.02.2016
comment
Потому что по умолчанию нет поиска ne. - person vsd; 25.02.2016

Я подозреваю, что метод, который вы попробовали первым, принимает только аргумент поиска для поля, на котором вы основываете get_next. Это означает, что вы не сможете получить доступ к полю размера, например, из метода get_next_by_created().

Редактировать: ваш метод намного эффективнее, но, чтобы ответить на ваш вопрос о проблеме Django, я думаю, что все работает так, как должно. Вы могли бы предложить дополнительный метод, такой как ваш, но существующий get_next_by_FIELD работает, как описано в документах.

Вам удалось обойти это с помощью рабочего метода, что, я думаю, нормально, но если вы хотите уменьшить накладные расходы, вы можете попробовать простой цикл:

def get_next_by_field_filtered(obj, field=None, **kwargs):

    next_obj = getattr(obj, 'get_next_by_{}'.format(field))()

    for key in kwargs:
        if not getattr(next_obj, str(key)) == kwargs[str(key)]:
            return get_next_by_field_filtered(next_obj, field=field, **kwargs)

    return next_obj

Это не очень эффективно, но это один из способов сделать то, что вы хотите.

Надеюсь это поможет !

С уважением,

person Ambroise    schedule 24.02.2016
comment
Да, зацикливание тоже сработает, но да, мне это не очень нравится, но ваше решение выглядит лучше. - person Florin Hillebrand; 25.02.2016
comment
И я не совсем уверен, что накладные расходы на вычисления/БД при циклическом обходе ниже, чем при использовании модифицированного my_get_next_or_previous_by_FIELD, потому что для каждого вызова get_next_by_FIELD потребуется доступ к БД. Вот почему я хотел решить эту проблему с помощью объектов size_ne или Q, чтобы всего один доступ к БД давал желаемый результат. - person Florin Hillebrand; 25.02.2016
comment
Да, вы абсолютно правы, ваш метод работает очень хорошо :) - person Ambroise; 25.02.2016