diff options
author | Étienne Loks <etienne.loks@iggdrasil.net> | 2018-07-03 13:06:01 +0200 |
---|---|---|
committer | Étienne Loks <etienne.loks@iggdrasil.net> | 2018-08-13 18:26:03 +0200 |
commit | 71432db3cbf135225fdb432c380db402a3eeffe7 (patch) | |
tree | eca25ee9bd6dd44ae9613845d22773a358950581 /ishtar_common | |
parent | 94f1e5957436a128c1fe8b543f3b2d1ae65093e4 (diff) | |
download | Ishtar-71432db3cbf135225fdb432c380db402a3eeffe7.tar.bz2 Ishtar-71432db3cbf135225fdb432c380db402a3eeffe7.zip |
Search: manage exclude - refactor search (refs #4180)
Diffstat (limited to 'ishtar_common')
-rw-r--r-- | ishtar_common/views_item.py | 499 |
1 files changed, 282 insertions, 217 deletions
diff --git a/ishtar_common/views_item.py b/ishtar_common/views_item.py index 675c36575..d19cbf7ff 100644 --- a/ishtar_common/views_item.py +++ b/ishtar_common/views_item.py @@ -312,16 +312,26 @@ FORBIDDEN_CHAR = [u":"] RESERVED_CHAR = [u"|", u"&"] -def _parse_query_string(string, request_keys, current_dct): +def _parse_query_string(string, request_keys, current_dct, exc_dct): string = string.strip().lower() if u"=" in string: splited = string.split(u"=") - if len(splited) == 2 and splited[0] in request_keys: + if len(splited) == 2: term, query = splited - term = request_keys[term] - current_dct[term] = query - return "" + excluded = term.startswith(u"-") + if excluded: + term = term[1:] + if term in request_keys: + term = request_keys[term] + dct = current_dct + if excluded: + dct = exc_dct + if term in dct: + dct[term] += u";" + query + else: + dct[term] = query + return "" for reserved_char in FORBIDDEN_CHAR: string = string.replace(reserved_char, u"") if len(string) != 1: @@ -330,21 +340,27 @@ def _parse_query_string(string, request_keys, current_dct): # like search if string.endswith(u'*'): string = string[:-1] + u':*' + if string.startswith(u'-'): + string = u"!" + string[1:] return string -def _parse_parentheses_groups(groups, request_keys, current_dct=None): +def _parse_parentheses_groups(groups, request_keys, current_dct=None, + exc_dct=None): """ Transform parentheses groups to query :param groups: groups to transform (list) :param request_keys: request keys for facet search - :param current_dct: - :return: query string, query dict + :param current_dct: query dict + :param exc_dct: exclude query dict + :return: query string, query dict, excluded query dict """ if not current_dct: current_dct = {} + if not exc_dct: + exc_dct = {} if type(groups) is not list: string = groups.strip() # split into many groups if spaces @@ -369,17 +385,18 @@ def _parse_parentheses_groups(groups, request_keys, current_dct=None): string_groups = [gp.replace(SEP, u" ") for gp in string.split(u" ")] if len(string_groups) == 1: - return _parse_query_string(string_groups[0], request_keys, - current_dct), current_dct + return _parse_query_string( + string_groups[0], request_keys, current_dct, exc_dct), \ + current_dct, exc_dct return _parse_parentheses_groups(string_groups, - request_keys, current_dct) + request_keys, current_dct, exc_dct) if not groups: # empty list - return "", current_dct + return "", current_dct, exc_dct query = u"(" previous_sep, has_item = None, False for item in groups: - q, current_dct = _parse_parentheses_groups(item, request_keys, - current_dct) + q, current_dct, exc_dct = _parse_parentheses_groups( + item, request_keys, current_dct, exc_dct) q = q.strip() if not q: continue @@ -399,24 +416,234 @@ def _parse_parentheses_groups(groups, request_keys, current_dct=None): query += u")" if query == u"()": query = u"" - return unidecode(query), current_dct + return unidecode(query), current_dct, exc_dct -def _search_manage_search_vector(dct, request_keys): +def _search_manage_search_vector(dct, exc_dct, request_keys): if 'search_vector' not in dct: - return dct + return dct, exc_dct parentheses_groups = _parse_parentheses(dct['search_vector'].strip()) - search_query, extra_dct = _parse_parentheses_groups(parentheses_groups, - request_keys) + search_query, extra_dct, extra_exc_dct = _parse_parentheses_groups( + parentheses_groups, request_keys) dct.update(extra_dct) + exc_dct.update(extra_exc_dct) if search_query: dct['extras'].append( {'where': ["search_vector @@ (to_tsquery(%s, %s)) = true"], 'params': [settings.ISHTAR_SEARCH_LANGUAGE, search_query]} ) - return dct + return dct, exc_dct + + +def _manage_bool_fields(model, bool_fields, reversed_bool_fields, dct, or_reqs): + bool_fields = list(bool_fields) + list(reversed_bool_fields) + for k in bool_fields: + if k in dct: + if dct[k] == u"1": + dct.pop(k) + else: + dct[k] = dct[k] == u"2" and True or False + if k in reversed_bool_fields: + dct[k] = not dct[k] + # check also for empty value with image field + field_name = k.split('__')[0] + # TODO: can be improved in later version of Django + try: + c_field = model._meta.get_field(field_name) + if k.endswith('__isnull') and \ + isinstance(c_field, ImageField): + if dct[k]: + or_reqs.append( + (k, {k.split('__')[0] + '__exact': ''})) + else: + dct[k.split('__')[0] + '__regex'] = '.{1}.*' + except FieldDoesNotExist: + pass + + +def _manage_dated_fields(dated_fields, dct): + for k in dated_fields: + if k in dct: + if not dct[k]: + dct.pop(k) + try: + items = dct[k].split('/') + assert len(items) == 3 + dct[k] = datetime.date(*map(lambda x: int(x), + reversed(items))) \ + .strftime('%Y-%m-%d') + except AssertionError: + dct.pop(k) + + +def _manage_facet_search(model, dct, and_reqs): + general_types = model.general_types() + for base_k in general_types: + if base_k in HIERARCHIC_FIELDS: # already managed + continue + k = base_k + "__pk" + if k not in dct or not dct[k].startswith(u'"') \ + or not dct[k].startswith(u'"'): + continue + val = dct.pop(k) + if u";" in val: + # OR request + values = val.split(u";") + else: + values = [val] + reqs = None + for val in values: + if not val.endswith(u'"') or not val.startswith(u""): + continue + suffix = "__label__icontains" if u"%" in val else \ + "__label__iexact" + query = val[1:-1].replace(u'*', u"") + if not reqs: + reqs = Q(**{base_k + suffix: query}) + else: + reqs |= Q(**{base_k + suffix: query}) + if reqs: + and_reqs.append(reqs) + + +def _manage_hierarchic_fields(dct, and_reqs): + for req in dct.copy(): + if req.endswith('areas__pk'): + val = dct.pop(req) + reqs = Q(**{req: val}) + base_req = req[:-2] + '__' + req = base_req[:] + for idx in range(HIERARCHIC_LEVELS): + req = req[:-2] + 'parent__pk' + q = Q(**{req: val}) + reqs |= q + req = base_req[:] + for idx in range(HIERARCHIC_LEVELS): + req = req[:-2] + 'children__pk' + q = Q(**{req: val}) + reqs |= q + and_reqs.append(reqs) + continue + + if req.endswith('town__pk') or req.endswith('towns__pk'): + val = dct.pop(req) + reqs = Q(**{req: val}) + base_req = req[:-2] + '__' + req = base_req[:] + for idx in range(HIERARCHIC_LEVELS): + req = req[:-2] + 'parents__pk' + q = Q(**{req: val}) + reqs |= q + req = base_req[:] + for idx in range(HIERARCHIC_LEVELS): + req = req[:-2] + 'children__pk' + q = Q(**{req: val}) + reqs |= q + and_reqs.append(reqs) + continue + + for k_hr in HIERARCHIC_FIELDS: + if type(req) in (list, tuple): + val = dct.pop(req) + q = None + for idx, r in enumerate(req): + if not idx: + q = Q(**{r: val}) + else: + q |= Q(**{r: val}) + and_reqs.append(q) + break + elif req.endswith(k_hr + '__pk'): + val = dct.pop(req) + + if u";" in val: + # OR request + values = val.split(u";") + else: + values = [val] + base_req = req[:] + reqs = None + for val in values: + suffix = "pk" + req = base_req[:] + + if val.startswith(u'"') and val.startswith(u'"'): + # manage search text by label + if u"*" in val: + suffix = "label__icontains" + val = val.replace(u'*', u"") + else: + suffix = "label__iexact" + val = val[1:-1] + req = req[:-2] + suffix + + if not reqs: + reqs = Q(**{req: val}) + else: + reqs |= Q(**{req: val}) + for idx in range(HIERARCHIC_LEVELS): + req = req[:-(len(suffix))] + 'parent__' + suffix + q = Q(**{req: val}) + reqs |= q + if reqs: + and_reqs.append(reqs) + break + + +def _manage_relation_types(relation_types, dct, query, or_reqs): + for rtype_prefix in relation_types: + vals = list(relation_types[rtype_prefix]) + if not vals: + continue + alt_dct = { + rtype_prefix + 'right_relations__relation_type__pk__in': vals} + for k in dct: + val = dct[k] + if rtype_prefix: + # only get conditions related to the object + if rtype_prefix not in k: + continue + # tricky: reconstruct the key to make sense - remove the + # prefix from the key + k = k[0:k.index(rtype_prefix)] + \ + k[k.index(rtype_prefix) + len(rtype_prefix):] + if k.endswith('year'): + k += '__exact' + alt_dct[rtype_prefix + 'right_relations__right_record__' + k] = \ + val + if not dct: + # fake condition to trick Django (1.4): without it only the + # alt_dct is managed + query &= Q(pk__isnull=False) + query |= Q(**alt_dct) + for k, or_req in or_reqs: + altor_dct = alt_dct.copy() + altor_dct.pop(k) + for j in or_req: + val = or_req[j] + if j == 'year': + j = 'year__exact' + altor_dct[ + rtype_prefix + 'right_relations__right_record__' + j] = \ + val + query |= Q(**altor_dct) + return query + + +def _contruct_query(relation_types, dct, or_reqs, and_reqs): + query = Q(**dct) + for k, or_req in or_reqs: + alt_dct = dct.copy() + alt_dct.pop(k) + alt_dct.update(or_req) + query |= Q(**alt_dct) + query = _manage_relation_types(relation_types, dct, query, or_reqs) + + for and_req in and_reqs: + query = query & and_req + return query def _format_val(val): @@ -486,6 +713,13 @@ def get_item(model, func_name, default_name, extra_request_keys=[], request.session['SHORTCUT_SEARCH'] == 'own': own = True + query_own = None + if own: + q = models.IshtarUser.objects.filter(user_ptr=request.user) + if not q.count(): + return HttpResponse(EMPTY, content_type='text/plain') + query_own = model.get_query_owns(q.all()[0]) + # get defaults from model if not extra_request_keys and hasattr(model, 'EXTRA_REQUEST_KEYS'): my_extra_request_keys = copy(model.EXTRA_REQUEST_KEYS) @@ -524,8 +758,6 @@ def get_item(model, func_name, default_name, extra_request_keys=[], else: my_relation_types_prefix = copy(relation_types_prefix) - general_types = model.general_types() - fields = [model._meta.get_field(k) for k in get_all_field_names(model)] @@ -569,9 +801,13 @@ def get_item(model, func_name, default_name, extra_request_keys=[], request_items = dct_request_items dct = my_base_request + excluded_dct = {} + and_reqs, or_reqs = [], [] + exc_and_reqs, exc_or_reqs = [], [] + if full == 'shortcut': dct['cached_label__icontains'] = request.GET.get('term', None) - and_reqs, or_reqs = [], [] + try: old = 'old' in request_items and int(request_items['old']) except ValueError: @@ -649,211 +885,36 @@ def get_item(model, func_name, default_name, extra_request_keys=[], request.session[func_name] = dct dct['extras'] = [] - dct = _search_manage_search_vector(dct, request_keys) + dct, excluded_dct = _search_manage_search_vector(dct, excluded_dct, + request_keys) search_vector = "" if 'search_vector' in dct: search_vector = dct.pop('search_vector') - for k in (list(my_bool_fields) + list(my_reversed_bool_fields)): - if k in dct: - if dct[k] == u"1": - dct.pop(k) - else: - dct[k] = dct[k] == u"2" and True or False - if k in my_reversed_bool_fields: - dct[k] = not dct[k] - # check also for empty value with image field - field_name = k.split('__')[0] - # TODO: can be improved in later version of Django - try: - c_field = model._meta.get_field(field_name) - if k.endswith('__isnull') and \ - isinstance(c_field, ImageField): - if dct[k]: - or_reqs.append( - (k, {k.split('__')[0] + '__exact': ''})) - else: - dct[k.split('__')[0] + '__regex'] = '.{1}.*' - except FieldDoesNotExist: - pass - for k in my_dated_fields: - if k in dct: - if not dct[k]: - dct.pop(k) - try: - items = dct[k].split('/') - assert len(items) == 3 - dct[k] = datetime.date(*map(lambda x: int(x), - reversed(items))) \ - .strftime('%Y-%m-%d') - except AssertionError: - dct.pop(k) - # manage hierarchic conditions - for req in dct.copy(): - if req.endswith('areas__pk'): - val = dct.pop(req) - reqs = Q(**{req: val}) - base_req = req[:-2] + '__' - req = base_req[:] - for idx in range(HIERARCHIC_LEVELS): - req = req[:-2] + 'parent__pk' - q = Q(**{req: val}) - reqs |= q - req = base_req[:] - for idx in range(HIERARCHIC_LEVELS): - req = req[:-2] + 'children__pk' - q = Q(**{req: val}) - reqs |= q - and_reqs.append(reqs) - continue - - if req.endswith('town__pk') or req.endswith('towns__pk'): - val = dct.pop(req) - reqs = Q(**{req: val}) - base_req = req[:-2] + '__' - req = base_req[:] - for idx in range(HIERARCHIC_LEVELS): - req = req[:-2] + 'parents__pk' - q = Q(**{req: val}) - reqs |= q - req = base_req[:] - for idx in range(HIERARCHIC_LEVELS): - req = req[:-2] + 'children__pk' - q = Q(**{req: val}) - reqs |= q - and_reqs.append(reqs) - continue - - for k_hr in HIERARCHIC_FIELDS: - if type(req) in (list, tuple): - val = dct.pop(req) - q = None - for idx, r in enumerate(req): - if not idx: - q = Q(**{r: val}) - else: - q |= Q(**{r: val}) - and_reqs.append(q) - break - elif req.endswith(k_hr + '__pk'): - val = dct.pop(req) + _manage_bool_fields(model, my_bool_fields, my_reversed_bool_fields, + dct, or_reqs) + _manage_bool_fields(model, my_bool_fields, my_reversed_bool_fields, + excluded_dct, exc_or_reqs) - if u";" in val: - # OR request - values = val.split(u";") - else: - values = [val] - base_req = req[:] - reqs = None - for val in values: - suffix = "pk" - req = base_req[:] - - if val.startswith(u'"') and val.startswith(u'"'): - # manage search text by label - if u"*" in val: - suffix = "label__icontains" - val = val.replace(u'*', u"") - else: - suffix = "label__iexact" - val = val[1:-1] - req = req[:-2] + suffix + _manage_dated_fields(my_dated_fields, dct) + _manage_dated_fields(my_dated_fields, excluded_dct) - if not reqs: - reqs = Q(**{req: val}) - else: - reqs |= Q(**{req: val}) - for idx in range(HIERARCHIC_LEVELS): - req = req[:-(len(suffix))] + 'parent__' + suffix - q = Q(**{req: val}) - reqs |= q - if reqs: - and_reqs.append(reqs) - break + _manage_hierarchic_fields(dct, and_reqs) + _manage_hierarchic_fields(excluded_dct, exc_and_reqs) - # manage search text by label - for base_k in general_types: - if base_k in HIERARCHIC_FIELDS: - continue - k = base_k + "__pk" - if k not in dct or not dct[k].startswith(u'"') \ - or not dct[k].startswith(u'"'): - continue - val = dct.pop(k) - if u";" in val: - # OR request - values = val.split(u";") - else: - values = [val] - reqs = None - for val in values: - if not val.endswith(u'"') or not val.startswith(u""): - continue - suffix = "__label__icontains" if u"%" in val else \ - "__label__iexact" - query = val[1:-1].replace(u'*', u"") - if not reqs: - reqs = Q(**{base_k + suffix: query}) - else: - reqs |= Q(**{base_k + suffix: query}) - if reqs: - and_reqs.append(reqs) + _manage_facet_search(model, dct, and_reqs) + _manage_facet_search(model, excluded_dct, exc_and_reqs) extras = dct.pop('extras') - query = Q(**dct) - for k, or_req in or_reqs: - alt_dct = dct.copy() - alt_dct.pop(k) - alt_dct.update(or_req) - query |= Q(**alt_dct) - - for rtype_prefix in relation_types: - vals = list(relation_types[rtype_prefix]) - if not vals: - continue - alt_dct = { - rtype_prefix + 'right_relations__relation_type__pk__in': vals} - for k in dct: - val = dct[k] - if rtype_prefix: - # only get conditions related to the object - if rtype_prefix not in k: - continue - # tricky: reconstruct the key to make sense - remove the - # prefix from the key - k = k[0:k.index(rtype_prefix)] + k[ - k.index(rtype_prefix) + len(rtype_prefix):] - if k.endswith('year'): - k += '__exact' - alt_dct[rtype_prefix + 'right_relations__right_record__' + k] = \ - val - if not dct: - # fake condition to trick Django (1.4): without it only the - # alt_dct is managed - query &= Q(pk__isnull=False) - query |= Q(**alt_dct) - for k, or_req in or_reqs: - altor_dct = alt_dct.copy() - altor_dct.pop(k) - for j in or_req: - val = or_req[j] - if j == 'year': - j = 'year__exact' - altor_dct[ - rtype_prefix + 'right_relations__right_record__' + j] = \ - val - query |= Q(**altor_dct) - - if own: - q = models.IshtarUser.objects.filter(user_ptr=request.user) - if q.count(): - query = query & model.get_query_owns(q.all()[0]) - else: - return HttpResponse(EMPTY, content_type='text/plain') + query = _contruct_query(relation_types, dct, or_reqs, and_reqs) + exc_query = None + if excluded_dct or exc_and_reqs or exc_or_reqs: + exc_query = _contruct_query( + relation_types, excluded_dct, exc_or_reqs, exc_and_reqs) - for and_req in and_reqs: - query = query & and_req + if query_own: + query = query & query_own # manage hierarchic in shortcut menu if full == 'shortcut': @@ -872,8 +933,12 @@ def get_item(model, func_name, default_name, extra_request_keys=[], query &= Q(**dct) items = model.objects.filter(query) + if exc_query: + items = items.exclude(exc_query) + for extra in extras: items = items.extra(**extra) + items = items.distinct() # print(items.query) |