from django.db.models.query import Q, QNot, QAnd, QOr, LOOKUP_SEPARATOR def build_query_filter_from_spec(spec, field_mapping=None): """ Assemble a django "Q" query filter object from a specification that consists of a possibly-nested list of query filter descriptions. These descriptions themselves specify Django primitive query filters, along with boolean "and", "or", and "not" operators. This format can be serialized and deserialized, allowing django queries to be composed client-side and sent across the wire using JSON. Each filter description is a list. The first element of the list is always the filter operator name. This name is one of either django's filter operators, "eq" (a synonym for "exact"), or the boolean operators "and", "or", and "not". Primitive query filters have three elements: [filteroperator, fieldname, queryarg] "filteroperator" is a string name like "in", "range", "icontains", etc. "fieldname" is the django field being queried. Any name that django accepts is allowed, including references to fields in foreign keys using the "__" syntax described in the django API reference. "queryarg" is the argument you'd pass to the `filter()` method in the Django database API. "and" and "or" query filters are lists that begin with the appropriate operator name, and include subfilters as additional list elements: ['or', [subfilter], ...] ['and', [subfilter], ...] "not" query filters consist of exactly two elements: ['not', [subfilter]] As a special case, the empty list "[]" or None return all elements. If field_mapping is specified, the field name provided in the spec is looked up in the field_mapping dictionary. If there's a match, the result is subsitituted. Otherwise, the field name is used unchanged to form the query. This feature allows client-side programs to use "nice" names that can be mapped to more complex django names. If you decide to use this feature, you'll probably want to do a similar mapping on the field names being returned to the client. This function returns a Q object that can be used anywhere you'd like in the django query machinery. This function raises ValueError in case the query is malformed, or perhaps other errors from the underlying DB code. Example queries: ['and', ['contains', 'name', 'Django'], ['range', 'apps', [1, 4]]] ['not', ['in', 'tags', ['colors', 'shapes', 'animals']]] ['or', ['eq', 'id', 2], ['icontains', 'city', 'Boston']] """ if spec == None or len(spec) == 0: return Q() cmd = spec[0] if cmd == 'and' or cmd == 'or': # ["or", [filter],[filter],[filter],...] # ["and", [filter],[filter],[filter],...] if len(spec) < 2: raise ValueError,'"and" or "or" filters must have at least one subfilter' if cmd == 'and': qop = QAnd else: qop = QOr resultq = None for arg in spec[1:]: q = build_query_filter_from_spec(arg) if q != None: if resultq == None: resultq = q else: resultq = qop(resultq, q) elif cmd == 'not': # ["not", [query]] if len(spec) != 2: raise ValueError,'"not" filters must have exactly one subfilter' q = build_query_filter_from_spec(spec[1]) if q != None: resultq = QNot(q) else: # some other query, will be validated in the query machinery # ["cmd", "fieldname", "arg"] # provide an intuitive alias for exact field equality if cmd == 'eq': cmd = 'exact' if len(spec) != 3: raise ValueError,'primitive filters must have two arguments (fieldname and query arg)' field_name = spec[1] if field_mapping: # see if the mapping contains an entry for the field_name # (for example, if you're mapping an external database name # to an internal django one). If not, use the existing name. field_name = field_mapping.get(field_name, field_name) kwname = str("%s%s%s" % (field_name, LOOKUP_SEPARATOR, cmd)) kwdict = {kwname : spec[2]} resultq = Q(**kwdict) return resultq