diff options
| -rw-r--r-- | archaeological_operations/tests.py | 65 | ||||
| -rw-r--r-- | ishtar_common/views_item.py | 117 | 
2 files changed, 170 insertions, 12 deletions
| diff --git a/archaeological_operations/tests.py b/archaeological_operations/tests.py index 9f08f1e48..381efd070 100644 --- a/archaeological_operations/tests.py +++ b/archaeological_operations/tests.py @@ -1456,8 +1456,11 @@ class OperationSearchTest(TestCase, OperationInitTest):          profile.areas.add(area)          self.orgas = self.create_orgas(self.user) -        self.operations = self.create_operation(self.user, self.orgas[0]) -        self.operations += self.create_operation(self.alt_user, self.orgas[0]) +        self.create_operation(self.user, self.orgas[0]) +        self.create_operation(self.alt_user, self.orgas[0]) +        self.operations = self.create_operation(self.alt_user, self.orgas[0]) +        self.operations[2].year = 2018 +        self.operations[2].save()          self.item = self.operations[0]      def test_base_search(self): @@ -1471,11 +1474,11 @@ class OperationSearchTest(TestCase, OperationInitTest):          response = c.get(reverse('get-operation'),                           {'operator': self.orgas[0].pk})          result = json.loads(response.content) -        self.assertEqual(result['recordsTotal'], 2) +        self.assertEqual(result['recordsTotal'], 3) -    def test_search_vector(self): +    def test_base_search_vector(self):          c = Client() -        response = c.get(reverse('get-operation'), {'year': '2010'}) +        response = c.get(reverse('get-operation'), {'search_vector': 'chaTEAU'})          # no result when no authentication          self.assertTrue(not json.loads(response.content))          c.login(username=self.username, password=self.password) @@ -1490,6 +1493,56 @@ class OperationSearchTest(TestCase, OperationInitTest):          result = json.loads(response.content)          self.assertEqual(result['recordsTotal'], 1) +    def test_complex_search_vector(self): +        c = Client() +        c.login(username=self.username, password=self.password) +        operation_1 = models.Operation.objects.get(pk=self.operations[0].pk) +        operation_2 = models.Operation.objects.get(pk=self.operations[1].pk) +        operation_3 = models.Operation.objects.get(pk=self.operations[2].pk) +        operation_1.common_name = u"Opération : Château de Fougères" +        operation_1.save() +        operation_2.common_name = u"Opération : Fougère filicophyta et " \ +                                  u"herbe à chat" +        operation_2.save() +        operation_3.common_name = u"Opération : Château Filicophyta" +        operation_3.save() + +        # simple separation +        response = c.get(reverse('get-operation'), +                         {'search_vector': 'chaTEAU fougere'}) +        result = json.loads(response.content) +        self.assertEqual(result['recordsTotal'], 1) + +        # explicit AND +        response = c.get(reverse('get-operation'), +                         {'search_vector': 'chaTEAU & fougere'}) +        result = json.loads(response.content) +        self.assertEqual(result['recordsTotal'], 1) + +        # explicit OR +        response = c.get(reverse('get-operation'), +                         {'search_vector': 'chaTEAU | fougere'}) +        result = json.loads(response.content) +        self.assertEqual(result['recordsTotal'], 3) + +        # query with parenthesis +        response = c.get(reverse('get-operation'), +                         {'search_vector': '2010 & (fougere | filicophyta)'}) +        result = json.loads(response.content) +        self.assertEqual(result['recordsTotal'], 2) + +        # query with mistmatch parenthesis +        response = c.get(reverse('get-operation'), +                         {'search_vector': '))   2010 &) ((chaTEAU | fougere)'}) +        result = json.loads(response.content) +        self.assertEqual(result['recordsTotal'], 2) + +        # open search +        response = c.get(reverse('get-operation'), +                         {'search_vector': 'cha*'}) +        result = json.loads(response.content) +        self.assertEqual(result['recordsTotal'], 3) +      def create_relations(self):          rel1 = models.RelationType.objects.create(              symmetrical=True, label='Include', txt_idx='include') @@ -1513,7 +1566,7 @@ class OperationSearchTest(TestCase, OperationInitTest):          self.assertTrue(not json.loads(response.content))          c.login(username=self.username, password=self.password)          response = c.get(reverse('get-operation'), search) -        self.assertTrue(json.loads(response.content)['recordsTotal'] == 2) +        self.assertEqual(json.loads(response.content)['recordsTotal'], 2)      def testHierarchicSearch(self):          ope = self.operations[1] diff --git a/ishtar_common/views_item.py b/ishtar_common/views_item.py index 513035903..f5e47a832 100644 --- a/ishtar_common/views_item.py +++ b/ishtar_common/views_item.py @@ -268,11 +268,107 @@ def _get_values(request, val):      return new_vals +def _push_to_list(obj, current_group, depth): +    """ +    parse_parentheses helper function +    """ +    try: +        while depth > 0: +            current_group = current_group[-1] +            depth -= 1 +    except IndexError: +        # tolerant to parentheses mismatch +        pass +    if current_group and type(obj) in (unicode, str) and \ +            type(current_group[-1]) in (unicode, str): +        current_group[-1] += obj +    else: +        current_group.append(obj) + + +def _parse_parentheses(s): +    """ +    Parse parentheses into list. +    (OA01 & (pierre | ciseau)) -> ["0A01 &", ["pierre | ciseau"]] +    """ + +    groups = [] +    depth = 0 + +    for char in s: +        if char == u'(': +            _push_to_list([], groups, depth) +            depth += 1 +        elif char == ')': +            if depth > 0: +                depth -= 1 +        else: +            _push_to_list(char, groups, depth) +    # for non tolerant to parentheses mismatch check depth is equal to 0 +    return groups + + +FORBIDDEN_CHAR = [u":"] +RESERVED_CHAR = [u"|", u"&"] + + +def _parse_query_string(string): +    string = string.strip().lower() +    for reserved_char in FORBIDDEN_CHAR: +        string = string.replace(reserved_char, u"") +    if len(string) != 1: +        for reserved_char in RESERVED_CHAR: +            string = string.replace(reserved_char, u"") +    # like search +    if string.endswith(u'*'): +        string = string[:-1] + u':*' + +    return string + + +def _parse_parentheses_groups(groups): +    """ +    Transform parentheses groups to query +    """ +    if type(groups) is not list: +        string = groups.strip() +        # split into many groups if spaces +        if ' ' not in string: +            return _parse_query_string(groups) +        return _parse_parentheses_groups(string.split(u" ")) +    if not groups:  # empty list +        return "" +    query = u"(" +    previous_sep, has_item = None, False +    for item in groups: +        q = _parse_parentheses_groups(item).strip() +        if not q: +            continue +        if q in (u"|", u"&"): +            if previous_sep or not has_item: +                continue  # multiple sep is not relevant +            previous_sep = q +            continue +        if has_item: +            if previous_sep: +                query += previous_sep +            else: +                query += u" & " +        query += q +        has_item = True +        previous_sep = None +    query += u")" +    return unidecode(query) + +  def _search_manage_search_vector(dct):      if 'search_vector' in dct: -        dct['search_vector'] = SearchQuery( -            unidecode(dct['search_vector']), -            config=settings.ISHTAR_SEARCH_LANGUAGE +        parentheses_groups = _parse_parentheses(dct['search_vector'].strip()) +        query = _parse_parentheses_groups(parentheses_groups) +        dct['extras'].append( +            {'where': ["search_vector @@ (to_tsquery(%s, %s)) = true"], +             'params': [settings.ISHTAR_SEARCH_LANGUAGE, +                        query]}          )      return dct @@ -594,7 +690,13 @@ def get_item(model, func_name, default_name, extra_request_keys=[],                          reqs |= q                      and_reqs.append(reqs)                      break +        dct['extras'] = []          dct = _search_manage_search_vector(dct) +        search_vector = "" +        if 'search_vector' in dct: +            search_vector = dct.pop('search_vector') +        extras = dct.pop('extras') +          query = Q(**dct)          for k, or_req in or_reqs:              alt_dct = dct.copy() @@ -665,11 +767,14 @@ def get_item(model, func_name, default_name, extra_request_keys=[],                      dct = {upper_key: current}                      query &= Q(**dct) -        items = model.objects.filter(query).distinct() +        items = model.objects.filter(query) +        for extra in extras: +            items = items.extra(**extra) +        items = items.distinct()          # print(items.query) -        if 'search_vector' in dct:  # for serialization -            dct['search_vector'] = dct['search_vector'].value +        if search_vector:  # for serialization +            dct['search_vector'] = search_vector          # table cols          if own_table_cols: | 
