Строку в объект Q

Моя строка содержит скобки для определения старшинства операций. Операции: and, or, eq (равно), ne (не равно), gt (больше чем), lt (меньше чем)

пример строки: "(date eq '2016-05-01') AND ((number_of_calories gt 20) OR (number_of_calories lt 10))"

>

Как я могу фильтровать модели django с заданными учетными данными? какой способ лучше?

В настоящее время у меня есть следующее решение, но это не очень хороший подход, так как он уязвим для SQL Injection:

q_string = "(date eq '2016-05-01') AND ((number_of_calories gt 20) OR (number_of_calories lt 10))"

query = convert_string(q_string)
# query = "(date = '2016-05-01') AND ((number_of_calories > 20) OR (number_of_calories < 10))"

Users.objects.raw('SELECT * FROM Users WHERE ' + query)

Итак, я думаю о преобразовании строки в объект Q, что-то вроде:

q_object = Q(q_string)
Users.models.filter(q_object)

как я могу преобразовать строку в объект Q?

В настоящее время я активно использую объект Q в проекте, который использует параметры get пользователей для фильтрации результатов поиска.

Вот фрагмент

some_initial_query_object = Model.objects.all()

qs_result_dates = []

qs_result_dates.append(
    Q(
        event_date__start_date_time__gte='2021-08-01',
        event_date__start_date_time__lt='2021-09-01' + datetime.timedelta(days=1)
      )
)

some_initial_query_object = some_initial_query_object.filter(qs_result_dates)

В вашем сценарии вы можете использовать | для OR и & для AND

Q(date='2016-05-01')
&
Q(number_of_calories__gt=20, number_of_calories__lt=10)

Для разбора строки запроса типа:

string = "((num_of_pages gt 20) OR (num_of_pages lt 10)) AND (date gt '2016-05-01')"

вы можете использовать пакет pyparsing (не эксперт, но очень мощная библиотека) с объектами django Q:

  1. разбор кода:
import pyparsing as pp
import operator as op

from django.db.models import Q

word = pp.Word(pp.alphas, pp.alphanums + "_-*'")
operator = pp.oneOf('lt gt eq').setResultsName('operator')
number = pp.pyparsing_common.number()
quoted = pp.quotedString().setParseAction(pp.removeQuotes)
term = (word | number | quoted)

key = term.setResultsName('key')
value = term.setResultsName('value')

group = pp.Group(key + operator + value)

def q_item(item):
    """Helper for create django Q() object"""
    k = f'{item.key}__{item.operator}'
    v = item.value
    return Q(**{k: v})


class BaseBinary:

    def __init__(self, tokens):
        self.args = tokens[0][0::2]

    def __repr__(self):
        return f'{self.__class__.__name__}({self.symbol}):{self.args}'

    def evaluate(self):
        a = q_item(self.args[0]) if not isinstance(self.args[0], BaseBinary) else self.args[0].evaluate()
        b = q_item(self.args[1]) if not isinstance(self.args[1], BaseBinary) else self.args[1].evaluate()
        return self.op(a, b)


class BoolNotOp(BaseBinary):
    symbol = 'NOT'
    op = op.not_

    def __init__(self, tokens):
        super().__init__(tokens)
        self.args = tokens[0][1]

    def evaluate(self):
        a = q_item(self.args) if not isinstance(self.args, BaseBinary) else self.args.evaluate()
        return ~a


class BoolAndOp(BaseBinary):
    symbol = 'AND'
    op = op.and_


class BoolOrOp(BaseBinary):
    symbol = 'OR'
    op = op.or_


expr = pp.infixNotation(group,
                        [('NOT', 1, pp.opAssoc.RIGHT, BoolNotOp),
                         ('AND', 2, pp.opAssoc.LEFT, BoolAndOp),
                         ('OR', 2, pp.opAssoc.LEFT, BoolOrOp)])

Теперь дана строка вида:

string = "(date gt '2016-05-01') AND ((num_of_pages gt 20) OR (num_of_pages lt 10))"

к синтаксическому анализатору:

parser = expr.parseString(string)[0]
print(parser.evaluate())

дайте нам наши Q-объекты:

(AND: ('date__gt', '2016-05-01'), (OR: ('num_of_pages__gt', 20), ('num_of_pages__lt', 10)))

готов к фильтрации

class Book(models.Model):
    title = models.CharField(max_length=200)
    counter = models.PositiveIntegerField(default=0)
    date = models.DateField(auto_now=True)
    num_of_pages = models.PositiveIntegerField(default=0)
    
qs = Book.objects.filter(parser.evaluate())
print(qs.query)
SELECT "core_book"."id", "core_book"."title", "core_book"."counter", "core_book"."date", "core_book"."num_of_pages" FROM "core_book" WHERE ("core_book"."date" > 2016-05-01 AND ("core_book"."num_of_pages" > 20 OR "core_book"."num_of_pages" < 10))

P.S не полностью протестировано.

Вот пример динамической фильтрации с использованием DRF путем переопределения метода get_queryset на ModelViewSet, который я использую во всех своих проектах. Используя этот метод, я могу использовать всю мощь фреймворка Django-ORM из фронтенда.

views.py

def BaseAPIView(...):

    ''' base view for other views to inherit '''

    def get_queryset(self):

        queryset = self.queryset

        # get filter request from client:
        filter_string = self.request.query_params.get('filter')

        # apply filters if they are passed in:
        if filters: 
            filter_dictionary = json.loads(filter_string)
            queryset = queryset.filter(**filter_dictionary)

        return queryset

Теперь url запроса будет выглядеть, например, так: my_website.com/api/users?filter={"first_name":"John"}

Который может быть построен следующим образом:

script.js

// using ajax as an example:
var filter = JSON.stringify({
  "first_name" : "John"
});

$.ajax({
   "url" : "my_website.com/api/users?filter=" + filter,
   "type" : "GET",
   ...
});

Некоторые преимущества:

  • не нужно указывать, какие поля можно фильтровать в каждом классе представления
  • напишите его один раз, используйте везде
  • фильтрация на переднем крае выглядит точно так же, как фильтрация в django
  • можно делать то же самое с exclude

Некоторые недостатки:

  • потенциальные риски безопасности, если вы хотите, чтобы некоторые поля не подлежали фильтрации
  • менее интуитивный внешний код для запроса таблицы

В целом, этот подход оказался для меня гораздо более полезным, чем все существующие пакеты.

Вернуться на верх