3

I don't know if the title of the question is formed well enough. But essentially I would like to be able to do something like this from front end :

(name="abc" OR name="xyz") AND (status="active" OR (status="available" AND age=30))

I want to the user to send this string. I will parse it in backend and form a query.

I have looked at this answer and this but couldn't figure out how to solve the parenthesis here.

I am thinking about using a stack (the way we solve infix expressions) to do this, but don't want to go that long route unless I am sure there isn't another/ready solution available. If someone can do this with that method, would be great too.

Mazhar Ali
  • 328
  • 1
  • 3
  • 13
  • Similar question here: https://stackoverflow.com/questions/62149580/how-to-add-parentheses-to-build-a-complicated-dynamic-django-filter-with-q – caram Apr 18 '22 at 08:30

2 Answers2

1

So I got it done using by changing the 'infix expression solver' a bit.

Here's the code:

def complex_filter_by_string(mystr):
"""
Filters string by using Q objects and infix expression solver.
The input string should be of the type ( name = abc OR name = xyz ) AND ( other = fgh ) Or ( other_one = xyz )
Notice the spaces after each 'word'. We are splitting the string by space, so space is necessary.
"""
tokens = mystr.split()

# stack to store integer values.
values = []

# stack to store operators.
ops = []
i = 0
while i < len(tokens):

    # Current token is a whitespace,
    # skip it.
    if tokens[i] == ' ':
        i += 1
        continue

    # Current token is an opening
    # brace, push it to 'ops'
    elif tokens[i] == '(':
        ops.append(tokens[i])

    # Current token is =, convert it to a Q object and push
    # it to stack for q objects.
    elif tokens[i] == "=":

        # if token equals '=' . It means we will have tokens[i-1]=attribute and tokens[i+1]=value (this is a constraint on string)
        obj = {}
        key = tokens[i-1]
        value = tokens[i+1]
        if isinstance(value,str):
            i+=1
            while i+1 < len(tokens) and (tokens[i+1] != 'OR' and tokens[i+1] != 'AND' and tokens[i+1] != ')'):
                value= value + " " + tokens[i+1]
                i+=1
            value = value.rstrip()
        obj[key]=value
        val = Q(**obj)
        values.append(val)
    
        
    # Closing brace encountered,
    # solve entire brace.
    elif tokens[i] == ')':
    
        while len(ops) != 0 and ops[-1] != '(':
        
            val2 = values.pop()
            val1 = values.pop()
            op = ops.pop()
            
            values.append(applyOp(val1, val2, op))
        
        # pop opening brace.
        ops.pop()
    
    # Current token is an operator.
    elif tokens[i] == "OR" or tokens[i] == "AND":
    
        # While top of 'ops' has same or
        # greater precedence to current
        # token, which is an operator.
        # Apply operator on top of 'ops'
        # to top two elements in values stack.
        while (len(ops) != 0 and
            precedence(ops[-1]) >=
            precedence(tokens[i])):
                    
            val2 = values.pop()
            val1 = values.pop()
            op = ops.pop()
            
            values.append(applyOp(val1, val2, op))
        
        # Push current token to 'ops'.
        ops.append(tokens[i])
    
    i += 1

# Entire expression has been parsed
# at this point, apply remaining ops
# to remaining values.
while len(ops) != 0:
    
    val2 = values.pop()
    val1 = values.pop()
    op = ops.pop()
            
    values.append(applyOp(val1, val2, op))

# Top of 'values' contains result,
# return it.
return values[-1]

This will return a Q object which you can pass onto your model lie MyModel.objects.filter(q_obj)

There are a few restrictions on the string though. The main restriction is 'there should be a whitespace after each keyword'. A keyword can be ' OR , AND , attribute , value , = , open_parenthesis '.

So the string that i posted in question should be : ( name = abc OR name = xyz ) AND (status = active OR ( status = available AND age = 30 ))

Mazhar Ali
  • 328
  • 1
  • 3
  • 13
  • What an over complicated way of solving the problem, when you can do it with Q objects in a relatively simple fashion. – wobbily_col Dec 14 '21 at 09:59
  • 1
    @wobbily_col I don't think you understood the question. I am filtering based on the USER string. If you think it can be done in a simpler way, would be happy to see that. But as far as i know, you need some mechanism to parse the user string and convert into Q objects. – Mazhar Ali Dec 14 '21 at 12:11
  • I agree, I don't think you have made it clear what you are trying to achieve or what exactly the USER string you want to filter on is. – wobbily_col Jan 05 '22 at 11:23
  • Here’s a reference to the original code and definition of the applyOp and precedence functions: https://www.geeksforgeeks.org/expression-evaluation/ – caram Apr 18 '22 at 08:32
0

I think it will be something like this.

YourModel.objects.filter(
    Q(Q(name="abc") | Q(name="xyz")) 
    & 
    Q( Q(status="active") 
       OR 
       Q(Q(status="available") 
         & 
         Q(age=30)
       )
   )
)
wobbily_col
  • 11,390
  • 12
  • 62
  • 86