From ab311e82bc2fc3d7535836013c8f3a1aa186375d Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Sun, 16 Feb 2020 18:35:46 +0000 Subject: [PATCH 01/18] Add ability to search by building+room --- search/views.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/search/views.py b/search/views.py index 3eb9deae..b07f079d 100644 --- a/search/views.py +++ b/search/views.py @@ -121,6 +121,12 @@ def search_single_word(word, filters, user, start, end, user_state, aff): ) filter_users = (filter_clubs | Q(name__icontains=word)) + if len(word.split(" ")) >= 2: + # Assume the room is in 1 word, and the building may be in multiple words + building = " ".join(word.split(" ")[:-1]) + room = word.split(" ")[-1] + filter_users |= (Q(room__name__icontains=room) & Q(room__building__name__icontains=building)) + if not User.can_view_all(user)[0]: filter_clubs &= Q(id=user.id) filter_users &= Q(id=user.id) @@ -204,6 +210,13 @@ def search_single_word(word, filters, user, start, end, user_state, aff): filter_rooms = ( Q(details__icontains=word) | Q(name__icontains=word) | Q(port__details=word) ) + + if len(word.split(" ")) >= 2: + # Assume the room is in 1 word, and the building may be in multiple words + building = " ".join(word.split(" ")[:-1]) + room = word.split(" ")[-1] + filter_rooms |= (Q(name__icontains=room) & Q(building__name__icontains=building)) + filters["rooms"] |= filter_rooms # Switch ports @@ -326,6 +339,10 @@ def get_words(query): # If we are between two ", ignore separators words[i] += char continue + if char == "+": + # If we encouter a + outside of ", we replace it with a space + words[i] += " " + continue if char == " " or char == ",": # If we encouter a separator outside of ", we create a new word if words[i] is not "": From 7ba5ff8be645a3d1faf44207093e17eb2b4eaf8d Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Tue, 18 Feb 2020 12:02:08 +0100 Subject: [PATCH 02/18] Handle queries with "+" operators --- search/views.py | 143 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 98 insertions(+), 45 deletions(-) diff --git a/search/views.py b/search/views.py index b07f079d..4ea4814e 100644 --- a/search/views.py +++ b/search/views.py @@ -62,11 +62,22 @@ def is_int(variable): return True +def filter_fields(): + """Return the list of fields the search applies to""" + return ["users", "clubs", "machines", "factures", "bans", "whitelists", "rooms", "ports", "switches"] + + +def empty_filters(): + """Build empty filters used by Django""" + filters = [Q() for f in filter_fields()] + + def finish_results(request, results, col, order): """Sort the results by applying filters and then limit them to the number of max results. Finally add the info of the nmax number of results to the dict""" + results["users"] += results["clubs"] results["users"] = SortTable.sort( results["users"], col, order, SortTable.USERS_INDEX ) @@ -121,12 +132,6 @@ def search_single_word(word, filters, user, start, end, user_state, aff): ) filter_users = (filter_clubs | Q(name__icontains=word)) - if len(word.split(" ")) >= 2: - # Assume the room is in 1 word, and the building may be in multiple words - building = " ".join(word.split(" ")[:-1]) - room = word.split(" ")[-1] - filter_users |= (Q(room__name__icontains=room) & Q(room__building__name__icontains=building)) - if not User.can_view_all(user)[0]: filter_clubs &= Q(id=user.id) filter_users &= Q(id=user.id) @@ -211,12 +216,6 @@ def search_single_word(word, filters, user, start, end, user_state, aff): Q(details__icontains=word) | Q(name__icontains=word) | Q(port__details=word) ) - if len(word.split(" ")) >= 2: - # Assume the room is in 1 word, and the building may be in multiple words - building = " ".join(word.split(" ")[:-1]) - room = word.split(" ")[-1] - filter_rooms |= (Q(name__icontains=room) & Q(building__name__icontains=building)) - filters["rooms"] |= filter_rooms # Switch ports @@ -252,7 +251,7 @@ def search_single_word(word, filters, user, start, end, user_state, aff): def apply_filters(filters, user, aff): - """ Apply the filters constructed by search_single_word. + """ Apply the filters constructed by search_single_query. It also takes into account the visual filters defined during the search query. """ @@ -305,53 +304,117 @@ def apply_filters(filters, user, aff): return results -def get_words(query): - """Function used to split the uery in different words to look for. - The rules are simple : +def search_single_query(query, filters, user, start, end, user_state, aff): + """ Handle different queries an construct the correct filters using + search_single_word""" + if query["operator"] == "+": + # Special queries with "+" operators should use & rather than | + for q in query["subqueries"]: + # Construct an independent filter for each subquery + subfilters = empty_filters() + subfilters = search_single_word(q, subfilters, user, start, end, user_state, aff) + + # Apply the new filter + for field in filter_fields(): + filters[field] &= subfilters[field] + + return filters + + # Handle standard queries + q = query["text"] + return search_single_word(q, filters, user, start, end, user_state, aff) + + +def create_queries(query): + """Function used to split the query in different words to look for. + The rules are the following : - anti-slash ('\\') is used to escape characters - anything between quotation marks ('"') is kept intact (not interpreted as separators) excepts anti-slashes used to escape + Values in between quotation marks are not searched accross + multiple field in the database (contrary to +) - spaces (' ') and commas (',') are used to separated words + - "+" signs are used as "and" operators """ + # A dict representing the different queries extracted from the user's text + queries = [] - words = [] - i = 0 + # Format: { + # "text": "", # Content of the query + # "operator": None, # Whether a special char ("+") was used + # "subqueries": None # When splitting the query in subparts (ex when using "+") + # } + current_query = None + + # Whether the query is between " keep_intact = False + + # Whether the previous char was a \ escaping_char = False + for char in query: - if i >= len(words): + if current_query is None: # We are starting a new word - words.append("") + current_query = { "text": "", "operator": None, "subqueries": None } + if escaping_char: # The last char war a \ so we escape this char escaping_char = False - words[i] += char + current_query["text"] += char continue + if char == "\\": # We need to escape the next char escaping_char = True continue + if char == '"': # Toogle the keep_intact state, if true, we are between two " keep_intact = not keep_intact continue + if keep_intact: # If we are between two ", ignore separators - words[i] += char + current_query["text"] += char continue + if char == "+": - # If we encouter a + outside of ", we replace it with a space - words[i] += " " + # Can't sart a query with a "+", consider it escaped + if len(current_query["text"]) == 0: + current_query["text"] = char + continue + + # Build a slightly more complicate data structure + # This is need for queries like '"A B"+C' + if current_query["operator"] is None: + current_query["operator"] = "+" + current_query["subqueries"] = [] + + current_query["subqueries"].append(current_query["text"]) + current_query["text"] = "" continue + if char == " " or char == ",": # If we encouter a separator outside of ", we create a new word - if words[i] is not "": - i += 1 - continue - # If we haven't encountered any special case, add the char to the word - words[i] += char - return words + if len(current_query["text"]) == 0: + # Discard empty queries + continue + + if current_query["operator"] is not None: + # If we were building a special structure, finish building it + current_query["subqueries"].append(current_query["text"]) + current_query["text"] = "" + + # Save the query and start a new one + queries.append(current_query) + current_query = None + continue + + # If we haven't encountered any special case, add the char to the word + current_query["text"].append(char) + + return queries def get_results(query, request, params): @@ -365,22 +428,12 @@ def get_results(query, request, params): user_state = params.get("u", initial_choices(CHOICES_USER)) aff = params.get("a", initial_choices(CHOICES_AFF)) - filters = { - "users": Q(), - "clubs": Q(), - "machines": Q(), - "factures": Q(), - "bans": Q(), - "whitelists": Q(), - "rooms": Q(), - "ports": Q(), - "switches": Q(), - } + filters = empty_filters() - words = get_words(query) - for word in words: - filters = search_single_word( - word, filters, request.user, start, end, user_state, aff + queries = create_queries(query) + for query in queries: + filters = search_single_query( + query, filters, request.user, start, end, user_state, aff ) results = apply_filters(filters, request.user, aff) From e36a7c7f1fedf09eddbc2dec02d34e2977a4fb95 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Tue, 18 Feb 2020 14:29:47 +0100 Subject: [PATCH 03/18] Create Query class --- search/views.py | 99 ++++++++++++++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 38 deletions(-) diff --git a/search/views.py b/search/views.py index 4ea4814e..30ef9bb0 100644 --- a/search/views.py +++ b/search/views.py @@ -51,6 +51,25 @@ from re2o.base import SortTable, re2o_paginator from re2o.acl import can_view_all +class Query: + def __init__(self, text=""): + self.text = text # Content of the query + self.operator = None # Whether a special char (ex "+") was used + self.subqueries = None # When splitting the query in subparts (ex when using "+") + + def add_char(self, char): + self.text += char + + def add_operator(self, operator): + self.operator = operator + + if self.subqueries is None: + self.subqueries = [] + + self.subqueries.append(Query(self.text)) + self.text = "" + + def is_int(variable): """ Check if the variable can be casted to an integer """ @@ -69,7 +88,7 @@ def filter_fields(): def empty_filters(): """Build empty filters used by Django""" - filters = [Q() for f in filter_fields()] + return {f: Q() for f in filter_fields()} def finish_results(request, results, col, order): @@ -77,10 +96,12 @@ def finish_results(request, results, col, order): number of max results. Finally add the info of the nmax number of results to the dict""" - results["users"] += results["clubs"] results["users"] = SortTable.sort( results["users"], col, order, SortTable.USERS_INDEX ) + results["clubs"] = SortTable.sort( + results["clubs"], col, order, SortTable.USERS_INDEX + ) results["machines"] = SortTable.sort( results["machines"], col, order, SortTable.MACHINES_INDEX ) @@ -129,6 +150,8 @@ def search_single_word(word, filters, user, start, end, user_state, aff): | Q(room__name__icontains=word) | Q(email__icontains=word) | Q(telephone__icontains=word) + | Q(room__name__icontains=word) + | Q(room__building__name__icontains=word) ) filter_users = (filter_clubs | Q(name__icontains=word)) @@ -213,8 +236,9 @@ def search_single_word(word, filters, user, start, end, user_state, aff): # Rooms if "5" in aff and Room.can_view_all(user): filter_rooms = ( - Q(details__icontains=word) | Q(name__icontains=word) | Q(port__details=word) + Q(details__icontains=word) | Q(name__icontains=word) | Q(port__details=word) | Q(building__name__icontains=building) ) + filter_rooms |= (Q(name__icontains=room) & Q(building__name__icontains=building)) filters["rooms"] |= filter_rooms @@ -307,22 +331,25 @@ def apply_filters(filters, user, aff): def search_single_query(query, filters, user, start, end, user_state, aff): """ Handle different queries an construct the correct filters using search_single_word""" - if query["operator"] == "+": + if query.operator == "+": # Special queries with "+" operators should use & rather than | - for q in query["subqueries"]: + newfilters = empty_filters() + for q in query.subqueries: # Construct an independent filter for each subquery - subfilters = empty_filters() - subfilters = search_single_word(q, subfilters, user, start, end, user_state, aff) + subfilters = search_single_query(q, empty_filters(), user, start, end, user_state, aff) - # Apply the new filter + # Apply the subfilter for field in filter_fields(): - filters[field] &= subfilters[field] + newfilters[field] &= subfilters[field] + + # Add these filters to the existing ones + for field in filter_fields(): + filters[field] |= newfilters[field] return filters # Handle standard queries - q = query["text"] - return search_single_word(q, filters, user, start, end, user_state, aff) + return search_single_word(query.text, filters, user, start, end, user_state, aff) def create_queries(query): @@ -338,12 +365,6 @@ def create_queries(query): """ # A dict representing the different queries extracted from the user's text queries = [] - - # Format: { - # "text": "", # Content of the query - # "operator": None, # Whether a special char ("+") was used - # "subqueries": None # When splitting the query in subparts (ex when using "+") - # } current_query = None # Whether the query is between " @@ -355,12 +376,12 @@ def create_queries(query): for char in query: if current_query is None: # We are starting a new word - current_query = { "text": "", "operator": None, "subqueries": None } + current_query = Query() if escaping_char: # The last char war a \ so we escape this char escaping_char = False - current_query["text"] += char + current_query.add_char(char) continue if char == "\\": @@ -375,36 +396,28 @@ def create_queries(query): if keep_intact: # If we are between two ", ignore separators - current_query["text"] += char + current_query.add_char(char) continue if char == "+": - # Can't sart a query with a "+", consider it escaped - if len(current_query["text"]) == 0: - current_query["text"] = char + if len(current_query.text) == 0: + # Can't sart a query with a "+", consider it escaped + current_query.add_char(char) continue - # Build a slightly more complicate data structure - # This is need for queries like '"A B"+C' - if current_query["operator"] is None: - current_query["operator"] = "+" - current_query["subqueries"] = [] - - current_query["subqueries"].append(current_query["text"]) - current_query["text"] = "" + current_query.add_operator("+") continue if char == " " or char == ",": # If we encouter a separator outside of ", we create a new word - if len(current_query["text"]) == 0: + if len(current_query.text) == 0: # Discard empty queries continue - if current_query["operator"] is not None: + if current_query.operator is not None: # If we were building a special structure, finish building it - current_query["subqueries"].append(current_query["text"]) - current_query["text"] = "" + current_query.add_operator(current_query.operator) # Save the query and start a new one queries.append(current_query) @@ -412,7 +425,17 @@ def create_queries(query): continue # If we haven't encountered any special case, add the char to the word - current_query["text"].append(char) + current_query.add_char(char) + + # Save the current working query if necessary + if current_query is not None: + if current_query.operator is not None: + # There was an operator supposed to split multiple words + if len(current_query.text) > 0: + # Finish the current search + current_query.add_operator(current_query.operator) + + queries.append(current_query) return queries @@ -431,9 +454,9 @@ def get_results(query, request, params): filters = empty_filters() queries = create_queries(query) - for query in queries: + for q in queries: filters = search_single_query( - query, filters, request.user, start, end, user_state, aff + q, filters, request.user, start, end, user_state, aff ) results = apply_filters(filters, request.user, aff) From 66183f9288ea9dab0e06fddf7ed5960cfc300ea8 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Tue, 18 Feb 2020 14:01:48 +0000 Subject: [PATCH 04/18] Add plaintext representation for Query --- search/views.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/search/views.py b/search/views.py index 30ef9bb0..c2e9338f 100644 --- a/search/views.py +++ b/search/views.py @@ -69,6 +69,13 @@ class Query: self.subqueries.append(Query(self.text)) self.text = "" + @property + def plaintext(self): + if self.operator is not None: + return self.operator.join([q.plaintext for q in self.subqueries]) + + return self.text + def is_int(variable): """ Check if the variable can be casted to an integer """ @@ -236,9 +243,8 @@ def search_single_word(word, filters, user, start, end, user_state, aff): # Rooms if "5" in aff and Room.can_view_all(user): filter_rooms = ( - Q(details__icontains=word) | Q(name__icontains=word) | Q(port__details=word) | Q(building__name__icontains=building) + Q(details__icontains=word) | Q(name__icontains=word) | Q(port__details=word) | Q(building__name__icontains=word) ) - filter_rooms |= (Q(name__icontains=room) & Q(building__name__icontains=building)) filters["rooms"] |= filter_rooms From 89d2d6a1d2945aca14a6daf03cc4fa50965aabb6 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Tue, 18 Feb 2020 16:04:54 +0100 Subject: [PATCH 05/18] Update description for advanced search --- search/forms.py | 4 ++-- search/locale/fr/LC_MESSAGES/django.po | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/search/forms.py b/search/forms.py index 259f739c..1cfd02cb 100644 --- a/search/forms.py +++ b/search/forms.py @@ -63,7 +63,7 @@ class SearchForm(Form): help_text=( _( 'Use « » and «,» to specify distinct words, «"query"» for' - " an exact search and «\\» to escape a character." + " an exact search, «\\» to escape a character and «+» to combine keywors." ) ), max_length=100, @@ -78,7 +78,7 @@ class SearchFormPlus(Form): help_text=( _( 'Use « » and «,» to specify distinct words, «"query"» for' - " an exact search and «\\» to escape a character." + " an exact search, «\\» to escape a character and «+» to combine keywors." ) ), max_length=100, diff --git a/search/locale/fr/LC_MESSAGES/django.po b/search/locale/fr/LC_MESSAGES/django.po index 6c317169..efb33ebe 100644 --- a/search/locale/fr/LC_MESSAGES/django.po +++ b/search/locale/fr/LC_MESSAGES/django.po @@ -90,10 +90,10 @@ msgstr "Rechercher" #: search/forms.py:65 search/forms.py:80 msgid "" "Use « » and «,» to specify distinct words, «\"query\"» for an exact search " -"and «\\» to escape a character." +"an exact search, «\\» to escape a character and «+» to combine keywors." msgstr "" -"Utilisez « » et «,» pour spécifier différents mots, «\"query\"» pour une " -"recherche exacte et «\\» pour échapper un caractère." +"Utilisez « » et «,» pour spécifier différents mots, «\"mot\"» pour une " +"recherche exacte, «\\» pour échapper un caractère et «+» pour combiner des mots clés." #: search/forms.py:88 msgid "Users filter" From 8323bc46da52256ea915d1628ae8b2d07c56c23a Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Tue, 18 Feb 2020 18:16:08 +0100 Subject: [PATCH 06/18] Split search/views into 2 files --- search/engine.py | 448 +++++++++++++++++++++++++++++++++++++++++++++++ search/views.py | 406 +----------------------------------------- 2 files changed, 451 insertions(+), 403 deletions(-) create mode 100644 search/engine.py diff --git a/search/engine.py b/search/engine.py new file mode 100644 index 00000000..716cacd6 --- /dev/null +++ b/search/engine.py @@ -0,0 +1,448 @@ +# Re2o est un logiciel d'administration développé initiallement au rezometz. Il +# se veut agnostique au réseau considéré, de manière à être installable en +# quelques clics. +# +# Copyright © 2017 Gabriel Détraz +# Copyright © 2017 Lara Kermarec +# Copyright © 2017 Augustin Lemesle +# Copyright © 2019 Jean-Romain Garnier +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +"""The views for the search app, responsible for finding the matches +Augustin lemesle, Gabriel Détraz, Lara Kermarec, Maël Kervella, +Jean-Romain Garnier +Gplv2""" + +from __future__ import unicode_literals + +from netaddr import EUI, AddrFormatError + +from django.db.models import Q +from users.models import User, Adherent, Club, Ban, Whitelist +from machines.models import Machine +from topologie.models import Port, Switch, Room +from cotisations.models import Facture +from preferences.models import GeneralOption +from re2o.base import SortTable, re2o_paginator + + +class Query: + """Class representing a query. + It can contain the user-entered text, the operator for the query, + and a list of subqueries""" + def __init__(self, text=""): + self.text = text # Content of the query + self.operator = None # Whether a special char (ex "+") was used + self.subqueries = None # When splitting the query in subparts + + def add_char(self, char): + """Add the given char to the query's text""" + self.text += char + + def add_operator(self, operator): + """Consider a new operator was entered, and that it must be processed. + The query's current text is moved to self.subqueries in the form + of a plain Query object""" + self.operator = operator + + if self.subqueries is None: + self.subqueries = [] + + self.subqueries.append(Query(self.text)) + self.text = "" + + @property + def plaintext(self): + """Returns a textual representation of the query's content""" + if self.operator is not None: + return self.operator.join([q.plaintext for q in self.subqueries]) + + return self.text + + +def filter_fields(): + """Return the list of fields the search applies to""" + return ["users", + "clubs", + "machines", + "factures", + "bans", + "whitelists", + "rooms", + "ports", + "switches"] + + +def empty_filters(): + """Build empty filters used by Django""" + return {f: Q() for f in filter_fields()} + + +def is_int(variable): + """ Check if the variable can be casted to an integer """ + try: + int(variable) + except ValueError: + return False + else: + return True + + +def finish_results(request, results, col, order): + """Sort the results by applying filters and then limit them to the + number of max results. Finally add the info of the nmax number of results + to the dict""" + results["users"] = SortTable.sort( + results["users"], col, order, SortTable.USERS_INDEX + ) + results["clubs"] = SortTable.sort( + results["clubs"], col, order, SortTable.USERS_INDEX + ) + results["machines"] = SortTable.sort( + results["machines"], col, order, SortTable.MACHINES_INDEX + ) + results["factures"] = SortTable.sort( + results["factures"], col, order, SortTable.COTISATIONS_INDEX + ) + results["bans"] = SortTable.sort( + results["bans"], col, order, SortTable.USERS_INDEX_BAN + ) + results["whitelists"] = SortTable.sort( + results["whitelists"], col, order, SortTable.USERS_INDEX_WHITE + ) + results["rooms"] = SortTable.sort( + results["rooms"], col, order, SortTable.TOPOLOGIE_INDEX_ROOM + ) + results["ports"] = SortTable.sort( + results["ports"], col, order, SortTable.TOPOLOGIE_INDEX_PORT + ) + results["switches"] = SortTable.sort( + results["switches"], col, order, SortTable.TOPOLOGIE_INDEX + ) + + max_result = GeneralOption.get_cached_value("search_display_page") + for name, val in results.items(): + page_arg = name + "_page" + results[name] = re2o_paginator(request, val.distinct(), max_result, page_arg=page_arg) + + results.update({"max_result": max_result}) + + return results + + +def search_single_word(word, filters, user, start, end, user_state, aff): + """ Construct the correct filters to match differents fields of some models + with the given query according to the given filters. + The match field are either CharField or IntegerField that will be displayed + on the results page (else, one might not see why a result has matched the + query). IntegerField are matched against the query only if it can be casted + to an int.""" + + # Users + if "0" in aff: + filter_clubs = ( + Q(surname__icontains=word) + | Q(pseudo__icontains=word) + | Q(room__name__icontains=word) + | Q(email__icontains=word) + | Q(telephone__icontains=word) + | Q(room__name__icontains=word) + | Q(room__building__name__icontains=word) + ) + filter_users = (filter_clubs | Q(name__icontains=word)) + + if not User.can_view_all(user)[0]: + filter_clubs &= Q(id=user.id) + filter_users &= Q(id=user.id) + + filter_clubs &= Q(state__in=user_state) + filter_users &= Q(state__in=user_state) + + filters["users"] |= filter_users + filters["clubs"] |= filter_clubs + + # Machines + if "1" in aff: + filter_machines = ( + Q(name__icontains=word) + | (Q(user__pseudo__icontains=word) & Q(user__state__in=user_state)) + | Q(interface__domain__name__icontains=word) + | Q(interface__domain__related_domain__name__icontains=word) + | Q(interface__mac_address__icontains=word) + | Q(interface__ipv4__ipv4__icontains=word) + ) + try: + _mac_addr = EUI(word, 48) + filter_machines |= Q(interface__mac_address=word) + except AddrFormatError: + pass + if not Machine.can_view_all(user)[0]: + filter_machines &= Q(user__id=user.id) + filters["machines"] |= filter_machines + + # Factures + if "2" in aff: + filter_factures = Q(user__pseudo__icontains=word) & Q( + user__state__in=user_state + ) + if start is not None: + filter_factures &= Q(date__gte=start) + if end is not None: + filter_factures &= Q(date__lte=end) + filters["factures"] |= filter_factures + + # Bans + if "3" in aff: + filter_bans = ( + Q(user__pseudo__icontains=word) & Q(user__state__in=user_state) + ) | Q(raison__icontains=word) + if start is not None: + filter_bans &= ( + (Q(date_start__gte=start) & Q(date_end__gte=start)) + | (Q(date_start__lte=start) & Q(date_end__gte=start)) + | (Q(date_start__gte=start) & Q(date_end__lte=start)) + ) + if end is not None: + filter_bans &= ( + (Q(date_start__lte=end) & Q(date_end__lte=end)) + | (Q(date_start__lte=end) & Q(date_end__gte=end)) + | (Q(date_start__gte=end) & Q(date_end__lte=end)) + ) + filters["bans"] |= filter_bans + + # Whitelists + if "4" in aff: + filter_whitelists = ( + Q(user__pseudo__icontains=word) & Q(user__state__in=user_state) + ) | Q(raison__icontains=word) + if start is not None: + filter_whitelists &= ( + (Q(date_start__gte=start) & Q(date_end__gte=start)) + | (Q(date_start__lte=start) & Q(date_end__gte=start)) + | (Q(date_start__gte=start) & Q(date_end__lte=start)) + ) + if end is not None: + filter_whitelists &= ( + (Q(date_start__lte=end) & Q(date_end__lte=end)) + | (Q(date_start__lte=end) & Q(date_end__gte=end)) + | (Q(date_start__gte=end) & Q(date_end__lte=end)) + ) + filters["whitelists"] |= filter_whitelists + + # Rooms + if "5" in aff and Room.can_view_all(user): + filter_rooms = ( + Q(details__icontains=word) | Q(name__icontains=word) | Q(port__details=word) | Q(building__name__icontains=word) + ) + + filters["rooms"] |= filter_rooms + + # Switch ports + if "6" in aff and User.can_view_all(user): + filter_ports = ( + Q(room__name__icontains=word) + | Q(machine_interface__domain__name__icontains=word) + | Q(related__switch__interface__domain__name__icontains=word) + | Q(custom_profile__name__icontains=word) + | Q(custom_profile__profil_default__icontains=word) + | Q(details__icontains=word) + ) + if is_int(word): + filter_ports |= Q(port=word) + filters["ports"] |= filter_ports + + # Switches + if "7" in aff and Switch.can_view_all(user): + filter_switches = ( + Q(interface__domain__name__icontains=word) + | Q(interface__ipv4__ipv4__icontains=word) + | Q(switchbay__building__name__icontains=word) + | Q(stack__name__icontains=word) + | Q(model__reference__icontains=word) + | Q(model__constructor__name__icontains=word) + | Q(interface__details__icontains=word) + ) + if is_int(word): + filter_switches |= Q(number=word) | Q(stack_member_id=word) + filters["switches"] |= filter_switches + + return filters + + +def apply_filters(filters, user, aff): + """ Apply the filters constructed by search_single_query. + It also takes into account the visual filters defined during + the search query. + """ + # Results are later filled-in depending on the display filter + results = { + "users": Adherent.objects.none(), + "clubs": Club.objects.none(), + "machines": Machine.objects.none(), + "factures": Facture.objects.none(), + "bans": Ban.objects.none(), + "whitelists": Whitelist.objects.none(), + "rooms": Room.objects.none(), + "ports": Port.objects.none(), + "switches": Switch.objects.none(), + } + + # Users and clubs + if "0" in aff: + results["users"] = Adherent.objects.filter(filters["users"]) + results["clubs"] = Club.objects.filter(filters["clubs"]) + + # Machines + if "1" in aff: + results["machines"] = Machine.objects.filter(filters["machines"]) + + # Factures + if "2" in aff: + results["factures"] = Facture.objects.filter(filters["factures"]) + + # Bans + if "3" in aff: + results["bans"] = Ban.objects.filter(filters["bans"]) + + # Whitelists + if "4" in aff: + results["whitelists"] = Whitelist.objects.filter(filters["whitelists"]) + + # Rooms + if "5" in aff and Room.can_view_all(user): + results["rooms"] = Room.objects.filter(filters["rooms"]) + + # Switch ports + if "6" in aff and User.can_view_all(user): + results["ports"] = Port.objects.filter(filters["ports"]) + + # Switches + if "7" in aff and Switch.can_view_all(user): + results["switches"] = Switch.objects.filter(filters["switches"]) + + return results + + +def search_single_query(query, filters, user, start, end, user_state, aff): + """ Handle different queries an construct the correct filters using + search_single_word""" + if query.operator == "+": + # Special queries with "+" operators should use & rather than | + newfilters = empty_filters() + for q in query.subqueries: + # Construct an independent filter for each subquery + subfilters = search_single_query(q, empty_filters(), user, start, end, user_state, aff) + + # Apply the subfilter + for field in filter_fields(): + newfilters[field] &= subfilters[field] + + # Add these filters to the existing ones + for field in filter_fields(): + filters[field] |= newfilters[field] + + return filters + + # Handle standard queries + return search_single_word(query.text, filters, user, start, end, user_state, aff) + + +def create_queries(query): + """Function used to split the query in different words to look for. + The rules are the following : + - anti-slash ('\\') is used to escape characters + - anything between quotation marks ('"') is kept intact (not + interpreted as separators) excepts anti-slashes used to escape + Values in between quotation marks are not searched accross + multiple field in the database (contrary to +) + - spaces (' ') and commas (',') are used to separated words + - "+" signs are used as "and" operators + """ + # A dict representing the different queries extracted from the user's text + queries = [] + current_query = None + + # Whether the query is between " + keep_intact = False + + # Whether the previous char was a \ + escaping_char = False + + for char in query: + if current_query is None: + # We are starting a new word + current_query = Query() + + if escaping_char: + # The last char war a \ so we escape this char + escaping_char = False + current_query.add_char(char) + continue + + if char == "\\": + # We need to escape the next char + escaping_char = True + continue + + if char == '"': + # Toogle the keep_intact state, if true, we are between two " + keep_intact = not keep_intact + continue + + if keep_intact: + # If we are between two ", ignore separators + current_query.add_char(char) + continue + + if char == "+": + if len(current_query.text) == 0: + # Can't sart a query with a "+", consider it escaped + current_query.add_char(char) + continue + + current_query.add_operator("+") + continue + + if char == " " or char == ",": + # If we encouter a separator outside of ", we create a new word + + if len(current_query.text) == 0: + # Discard empty queries + continue + + if current_query.operator is not None: + # If we were building a special structure, finish building it + current_query.add_operator(current_query.operator) + + # Save the query and start a new one + queries.append(current_query) + current_query = None + continue + + # If we haven't encountered any special case, add the char to the word + current_query.add_char(char) + + # Save the current working query if necessary + if current_query is not None: + if current_query.operator is not None: + # There was an operator supposed to split multiple words + if len(current_query.text) > 0: + # Finish the current search + current_query.add_operator(current_query.operator) + + queries.append(current_query) + + return queries diff --git a/search/views.py b/search/views.py index c2e9338f..cf0a5248 100644 --- a/search/views.py +++ b/search/views.py @@ -28,18 +28,12 @@ Gplv2""" from __future__ import unicode_literals -from netaddr import EUI, AddrFormatError - from django.shortcuts import render from django.contrib.auth.decorators import login_required -from django.db.models import Q -from users.models import User, Adherent, Club, Ban, Whitelist -from machines.models import Machine +from users.models import User from cotisations.models import Cotisation -from topologie.models import Port, Switch, Room -from cotisations.models import Facture -from preferences.models import GeneralOption +from machines.models import Machine from search.forms import ( SearchForm, SearchFormPlus, @@ -47,403 +41,9 @@ from search.forms import ( CHOICES_AFF, initial_choices, ) -from re2o.base import SortTable, re2o_paginator from re2o.acl import can_view_all - -class Query: - def __init__(self, text=""): - self.text = text # Content of the query - self.operator = None # Whether a special char (ex "+") was used - self.subqueries = None # When splitting the query in subparts (ex when using "+") - - def add_char(self, char): - self.text += char - - def add_operator(self, operator): - self.operator = operator - - if self.subqueries is None: - self.subqueries = [] - - self.subqueries.append(Query(self.text)) - self.text = "" - - @property - def plaintext(self): - if self.operator is not None: - return self.operator.join([q.plaintext for q in self.subqueries]) - - return self.text - - -def is_int(variable): - """ Check if the variable can be casted to an integer """ - - try: - int(variable) - except ValueError: - return False - else: - return True - - -def filter_fields(): - """Return the list of fields the search applies to""" - return ["users", "clubs", "machines", "factures", "bans", "whitelists", "rooms", "ports", "switches"] - - -def empty_filters(): - """Build empty filters used by Django""" - return {f: Q() for f in filter_fields()} - - -def finish_results(request, results, col, order): - """Sort the results by applying filters and then limit them to the - number of max results. Finally add the info of the nmax number of results - to the dict""" - - results["users"] = SortTable.sort( - results["users"], col, order, SortTable.USERS_INDEX - ) - results["clubs"] = SortTable.sort( - results["clubs"], col, order, SortTable.USERS_INDEX - ) - results["machines"] = SortTable.sort( - results["machines"], col, order, SortTable.MACHINES_INDEX - ) - results["factures"] = SortTable.sort( - results["factures"], col, order, SortTable.COTISATIONS_INDEX - ) - results["bans"] = SortTable.sort( - results["bans"], col, order, SortTable.USERS_INDEX_BAN - ) - results["whitelists"] = SortTable.sort( - results["whitelists"], col, order, SortTable.USERS_INDEX_WHITE - ) - results["rooms"] = SortTable.sort( - results["rooms"], col, order, SortTable.TOPOLOGIE_INDEX_ROOM - ) - results["ports"] = SortTable.sort( - results["ports"], col, order, SortTable.TOPOLOGIE_INDEX_PORT - ) - results["switches"] = SortTable.sort( - results["switches"], col, order, SortTable.TOPOLOGIE_INDEX - ) - - max_result = GeneralOption.get_cached_value("search_display_page") - for name, val in results.items(): - page_arg = name + "_page" - results[name] = re2o_paginator(request, val.distinct(), max_result, page_arg=page_arg) - - results.update({"max_result": max_result}) - - return results - - -def search_single_word(word, filters, user, start, end, user_state, aff): - """ Construct the correct filters to match differents fields of some models - with the given query according to the given filters. - The match field are either CharField or IntegerField that will be displayed - on the results page (else, one might not see why a result has matched the - query). IntegerField are matched against the query only if it can be casted - to an int.""" - - # Users - if "0" in aff: - filter_clubs = ( - Q(surname__icontains=word) - | Q(pseudo__icontains=word) - | Q(room__name__icontains=word) - | Q(email__icontains=word) - | Q(telephone__icontains=word) - | Q(room__name__icontains=word) - | Q(room__building__name__icontains=word) - ) - filter_users = (filter_clubs | Q(name__icontains=word)) - - if not User.can_view_all(user)[0]: - filter_clubs &= Q(id=user.id) - filter_users &= Q(id=user.id) - - filter_clubs &= Q(state__in=user_state) - filter_users &= Q(state__in=user_state) - - filters["users"] |= filter_users - filters["clubs"] |= filter_clubs - - # Machines - if "1" in aff: - filter_machines = ( - Q(name__icontains=word) - | (Q(user__pseudo__icontains=word) & Q(user__state__in=user_state)) - | Q(interface__domain__name__icontains=word) - | Q(interface__domain__related_domain__name__icontains=word) - | Q(interface__mac_address__icontains=word) - | Q(interface__ipv4__ipv4__icontains=word) - ) - try: - _mac_addr = EUI(word, 48) - filter_machines |= Q(interface__mac_address=word) - except AddrFormatError: - pass - if not Machine.can_view_all(user)[0]: - filter_machines &= Q(user__id=user.id) - filters["machines"] |= filter_machines - - # Factures - if "2" in aff: - filter_factures = Q(user__pseudo__icontains=word) & Q( - user__state__in=user_state - ) - if start is not None: - filter_factures &= Q(date__gte=start) - if end is not None: - filter_factures &= Q(date__lte=end) - filters["factures"] |= filter_factures - - # Bans - if "3" in aff: - filter_bans = ( - Q(user__pseudo__icontains=word) & Q(user__state__in=user_state) - ) | Q(raison__icontains=word) - if start is not None: - filter_bans &= ( - (Q(date_start__gte=start) & Q(date_end__gte=start)) - | (Q(date_start__lte=start) & Q(date_end__gte=start)) - | (Q(date_start__gte=start) & Q(date_end__lte=start)) - ) - if end is not None: - filter_bans &= ( - (Q(date_start__lte=end) & Q(date_end__lte=end)) - | (Q(date_start__lte=end) & Q(date_end__gte=end)) - | (Q(date_start__gte=end) & Q(date_end__lte=end)) - ) - filters["bans"] |= filter_bans - - # Whitelists - if "4" in aff: - filter_whitelists = ( - Q(user__pseudo__icontains=word) & Q(user__state__in=user_state) - ) | Q(raison__icontains=word) - if start is not None: - filter_whitelists &= ( - (Q(date_start__gte=start) & Q(date_end__gte=start)) - | (Q(date_start__lte=start) & Q(date_end__gte=start)) - | (Q(date_start__gte=start) & Q(date_end__lte=start)) - ) - if end is not None: - filter_whitelists &= ( - (Q(date_start__lte=end) & Q(date_end__lte=end)) - | (Q(date_start__lte=end) & Q(date_end__gte=end)) - | (Q(date_start__gte=end) & Q(date_end__lte=end)) - ) - filters["whitelists"] |= filter_whitelists - - # Rooms - if "5" in aff and Room.can_view_all(user): - filter_rooms = ( - Q(details__icontains=word) | Q(name__icontains=word) | Q(port__details=word) | Q(building__name__icontains=word) - ) - - filters["rooms"] |= filter_rooms - - # Switch ports - if "6" in aff and User.can_view_all(user): - filter_ports = ( - Q(room__name__icontains=word) - | Q(machine_interface__domain__name__icontains=word) - | Q(related__switch__interface__domain__name__icontains=word) - | Q(custom_profile__name__icontains=word) - | Q(custom_profile__profil_default__icontains=word) - | Q(details__icontains=word) - ) - if is_int(word): - filter_ports |= Q(port=word) - filters["ports"] |= filter_ports - - # Switches - if "7" in aff and Switch.can_view_all(user): - filter_switches = ( - Q(interface__domain__name__icontains=word) - | Q(interface__ipv4__ipv4__icontains=word) - | Q(switchbay__building__name__icontains=word) - | Q(stack__name__icontains=word) - | Q(model__reference__icontains=word) - | Q(model__constructor__name__icontains=word) - | Q(interface__details__icontains=word) - ) - if is_int(word): - filter_switches |= Q(number=word) | Q(stack_member_id=word) - filters["switches"] |= filter_switches - - return filters - - -def apply_filters(filters, user, aff): - """ Apply the filters constructed by search_single_query. - It also takes into account the visual filters defined during - the search query. - """ - # Results are later filled-in depending on the display filter - results = { - "users": Adherent.objects.none(), - "clubs": Club.objects.none(), - "machines": Machine.objects.none(), - "factures": Facture.objects.none(), - "bans": Ban.objects.none(), - "whitelists": Whitelist.objects.none(), - "rooms": Room.objects.none(), - "ports": Port.objects.none(), - "switches": Switch.objects.none(), - } - - # Users and clubs - if "0" in aff: - results["users"] = Adherent.objects.filter(filters["users"]) - results["clubs"] = Club.objects.filter(filters["clubs"]) - - # Machines - if "1" in aff: - results["machines"] = Machine.objects.filter(filters["machines"]) - - # Factures - if "2" in aff: - results["factures"] = Facture.objects.filter(filters["factures"]) - - # Bans - if "3" in aff: - results["bans"] = Ban.objects.filter(filters["bans"]) - - # Whitelists - if "4" in aff: - results["whitelists"] = Whitelist.objects.filter(filters["whitelists"]) - - # Rooms - if "5" in aff and Room.can_view_all(user): - results["rooms"] = Room.objects.filter(filters["rooms"]) - - # Switch ports - if "6" in aff and User.can_view_all(user): - results["ports"] = Port.objects.filter(filters["ports"]) - - # Switches - if "7" in aff and Switch.can_view_all(user): - results["switches"] = Switch.objects.filter(filters["switches"]) - - return results - - -def search_single_query(query, filters, user, start, end, user_state, aff): - """ Handle different queries an construct the correct filters using - search_single_word""" - if query.operator == "+": - # Special queries with "+" operators should use & rather than | - newfilters = empty_filters() - for q in query.subqueries: - # Construct an independent filter for each subquery - subfilters = search_single_query(q, empty_filters(), user, start, end, user_state, aff) - - # Apply the subfilter - for field in filter_fields(): - newfilters[field] &= subfilters[field] - - # Add these filters to the existing ones - for field in filter_fields(): - filters[field] |= newfilters[field] - - return filters - - # Handle standard queries - return search_single_word(query.text, filters, user, start, end, user_state, aff) - - -def create_queries(query): - """Function used to split the query in different words to look for. - The rules are the following : - - anti-slash ('\\') is used to escape characters - - anything between quotation marks ('"') is kept intact (not - interpreted as separators) excepts anti-slashes used to escape - Values in between quotation marks are not searched accross - multiple field in the database (contrary to +) - - spaces (' ') and commas (',') are used to separated words - - "+" signs are used as "and" operators - """ - # A dict representing the different queries extracted from the user's text - queries = [] - current_query = None - - # Whether the query is between " - keep_intact = False - - # Whether the previous char was a \ - escaping_char = False - - for char in query: - if current_query is None: - # We are starting a new word - current_query = Query() - - if escaping_char: - # The last char war a \ so we escape this char - escaping_char = False - current_query.add_char(char) - continue - - if char == "\\": - # We need to escape the next char - escaping_char = True - continue - - if char == '"': - # Toogle the keep_intact state, if true, we are between two " - keep_intact = not keep_intact - continue - - if keep_intact: - # If we are between two ", ignore separators - current_query.add_char(char) - continue - - if char == "+": - if len(current_query.text) == 0: - # Can't sart a query with a "+", consider it escaped - current_query.add_char(char) - continue - - current_query.add_operator("+") - continue - - if char == " " or char == ",": - # If we encouter a separator outside of ", we create a new word - - if len(current_query.text) == 0: - # Discard empty queries - continue - - if current_query.operator is not None: - # If we were building a special structure, finish building it - current_query.add_operator(current_query.operator) - - # Save the query and start a new one - queries.append(current_query) - current_query = None - continue - - # If we haven't encountered any special case, add the char to the word - current_query.add_char(char) - - # Save the current working query if necessary - if current_query is not None: - if current_query.operator is not None: - # There was an operator supposed to split multiple words - if len(current_query.text) > 0: - # Finish the current search - current_query.add_operator(current_query.operator) - - queries.append(current_query) - - return queries +from engine import * def get_results(query, request, params): From c49beb02ee3832da823dbf6f858e1d1b5d2bd2b8 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Tue, 18 Feb 2020 17:18:44 +0000 Subject: [PATCH 07/18] Fix import error --- search/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/search/views.py b/search/views.py index cf0a5248..60d0a92f 100644 --- a/search/views.py +++ b/search/views.py @@ -43,7 +43,7 @@ from search.forms import ( ) from re2o.acl import can_view_all -from engine import * +from .engine import * def get_results(query, request, params): From 8a9c8eaaaaac94e73adef6430e02699252b2b56c Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Tue, 18 Feb 2020 18:52:50 +0100 Subject: [PATCH 08/18] Add ability to make queries case sensitive --- search/engine.py | 101 +++++++++++++++++++++++++++++------------------ 1 file changed, 62 insertions(+), 39 deletions(-) diff --git a/search/engine.py b/search/engine.py index 716cacd6..7d4d8934 100644 --- a/search/engine.py +++ b/search/engine.py @@ -43,10 +43,11 @@ class Query: """Class representing a query. It can contain the user-entered text, the operator for the query, and a list of subqueries""" - def __init__(self, text=""): + def __init__(self, text="", case_sensitive=False): self.text = text # Content of the query self.operator = None # Whether a special char (ex "+") was used self.subqueries = None # When splitting the query in subparts + self.case_sensitive = case_sensitive def add_char(self, char): """Add the given char to the query's text""" @@ -61,8 +62,9 @@ class Query: if self.subqueries is None: self.subqueries = [] - self.subqueries.append(Query(self.text)) + self.subqueries.append(Query(self.text, self.case_sensitive)) self.text = "" + self.case_sensitive = False @property def plaintext(self): @@ -70,6 +72,9 @@ class Query: if self.operator is not None: return self.operator.join([q.plaintext for q in self.subqueries]) + if self.case_sensitive: + return "\"{}\"".format(self.text) + return self.text @@ -143,7 +148,18 @@ def finish_results(request, results, col, order): return results -def search_single_word(word, filters, user, start, end, user_state, aff): +def contains_filter(attribute, word, case_sensitive=False): + """Create a django model filtering whether the given attribute + contains the specified value.""" + if case_sensitive: + attr = "{}__{}".format(attribute, "contains") + else: + attr = "{}__{}".format(attribute, "icontains") + + return Q(**{attr: word}) + + +def search_single_word(word, filters, user, start, end, user_state, aff, case_sensitive=False): """ Construct the correct filters to match differents fields of some models with the given query according to the given filters. The match field are either CharField or IntegerField that will be displayed @@ -154,15 +170,14 @@ def search_single_word(word, filters, user, start, end, user_state, aff): # Users if "0" in aff: filter_clubs = ( - Q(surname__icontains=word) - | Q(pseudo__icontains=word) - | Q(room__name__icontains=word) - | Q(email__icontains=word) - | Q(telephone__icontains=word) - | Q(room__name__icontains=word) - | Q(room__building__name__icontains=word) + contains_filter("surname", word, case_sensitive) + | contains_filter("pseudo", word, case_sensitive) + | contains_filter("email", word, case_sensitive) + | contains_filter("telephone", word, case_sensitive) + | contains_filter("room__name", word, case_sensitive) + | contains_filter("room__building__name", word, case_sensitive) ) - filter_users = (filter_clubs | Q(name__icontains=word)) + filter_users = (filter_clubs | contains_filter("name", word, case_sensitive)) if not User.can_view_all(user)[0]: filter_clubs &= Q(id=user.id) @@ -177,12 +192,12 @@ def search_single_word(word, filters, user, start, end, user_state, aff): # Machines if "1" in aff: filter_machines = ( - Q(name__icontains=word) - | (Q(user__pseudo__icontains=word) & Q(user__state__in=user_state)) - | Q(interface__domain__name__icontains=word) - | Q(interface__domain__related_domain__name__icontains=word) - | Q(interface__mac_address__icontains=word) - | Q(interface__ipv4__ipv4__icontains=word) + contains_filter("name", word, case_sensitive) + | contains_filter("user__pseudo", word, case_sensitive) & Q(user__state__in=user_state) + | contains_filter("interface__domain__name", word, case_sensitive) + | contains_filter("interface__domain__related_domain__name", word, case_sensitive) + | contains_filter("interface__mac_address", word, case_sensitive) + | contains_filter("interface__ipv4__ipv4", word, case_sensitive) ) try: _mac_addr = EUI(word, 48) @@ -195,9 +210,8 @@ def search_single_word(word, filters, user, start, end, user_state, aff): # Factures if "2" in aff: - filter_factures = Q(user__pseudo__icontains=word) & Q( - user__state__in=user_state - ) + filter_factures = contains_filter("user__pseudo", word, case_sensitive) + & Q(user__state__in=user_state) if start is not None: filter_factures &= Q(date__gte=start) if end is not None: @@ -207,8 +221,9 @@ def search_single_word(word, filters, user, start, end, user_state, aff): # Bans if "3" in aff: filter_bans = ( - Q(user__pseudo__icontains=word) & Q(user__state__in=user_state) - ) | Q(raison__icontains=word) + contains_filter("user__pseudo", word, case_sensitive) + & Q(user__state__in=user_state) + ) | contains_filter("raison", word, case_sensitive) if start is not None: filter_bans &= ( (Q(date_start__gte=start) & Q(date_end__gte=start)) @@ -226,8 +241,9 @@ def search_single_word(word, filters, user, start, end, user_state, aff): # Whitelists if "4" in aff: filter_whitelists = ( - Q(user__pseudo__icontains=word) & Q(user__state__in=user_state) - ) | Q(raison__icontains=word) + contains_filter("user__pseudo", word, case_sensitive) + & Q(user__state__in=user_state) + ) | contains_filter("raison", word, case_sensitive) if start is not None: filter_whitelists &= ( (Q(date_start__gte=start) & Q(date_end__gte=start)) @@ -245,7 +261,10 @@ def search_single_word(word, filters, user, start, end, user_state, aff): # Rooms if "5" in aff and Room.can_view_all(user): filter_rooms = ( - Q(details__icontains=word) | Q(name__icontains=word) | Q(port__details=word) | Q(building__name__icontains=word) + contains_filter("details", word, case_sensitive) + | contains_filter("name", word, case_sensitive) + | contains_filter("building__name", word, case_sensitive) + | Q(port__details=word) ) filters["rooms"] |= filter_rooms @@ -253,12 +272,12 @@ def search_single_word(word, filters, user, start, end, user_state, aff): # Switch ports if "6" in aff and User.can_view_all(user): filter_ports = ( - Q(room__name__icontains=word) - | Q(machine_interface__domain__name__icontains=word) - | Q(related__switch__interface__domain__name__icontains=word) - | Q(custom_profile__name__icontains=word) - | Q(custom_profile__profil_default__icontains=word) - | Q(details__icontains=word) + contains_filter("room__name", word, case_sensitive) + | contains_filter("machine_interface__domain__name", word, case_sensitive) + | contains_filter("related__switch__interface__domain__name", word, case_sensitive) + | contains_filter("custom_profile__name", word, case_sensitive) + | contains_filter("custom_profile__profil_default", word, case_sensitive) + | contains_filter("details", word, case_sensitive) ) if is_int(word): filter_ports |= Q(port=word) @@ -267,13 +286,13 @@ def search_single_word(word, filters, user, start, end, user_state, aff): # Switches if "7" in aff and Switch.can_view_all(user): filter_switches = ( - Q(interface__domain__name__icontains=word) - | Q(interface__ipv4__ipv4__icontains=word) - | Q(switchbay__building__name__icontains=word) - | Q(stack__name__icontains=word) - | Q(model__reference__icontains=word) - | Q(model__constructor__name__icontains=word) - | Q(interface__details__icontains=word) + contains_filter("interface__domain__name", word, case_sensitive) + | contains_filter("interface__ipv4__ipv4", word, case_sensitive) + | contains_filter("switchbay__building__name", word, case_sensitive) + | contains_filter("stack__name", word, case_sensitive) + | contains_filter("model__reference", word, case_sensitive) + | contains_filter("model__constructor__name", word, case_sensitive) + | contains_filter("interface__details", word, case_sensitive) ) if is_int(word): filter_switches |= Q(number=word) | Q(stack_member_id=word) @@ -357,7 +376,7 @@ def search_single_query(query, filters, user, start, end, user_state, aff): return filters # Handle standard queries - return search_single_word(query.text, filters, user, start, end, user_state, aff) + return search_single_word(query.text, filters, user, start, end, user_state, aff, q.case_sensitive) def create_queries(query): @@ -400,6 +419,10 @@ def create_queries(query): if char == '"': # Toogle the keep_intact state, if true, we are between two " keep_intact = not keep_intact + + if keep_intact: + current_query.case_sensitive = True + continue if keep_intact: From 0347086ca4c7f8d726fbb4c5c2c6d53a9a418a45 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Tue, 18 Feb 2020 18:01:39 +0000 Subject: [PATCH 09/18] Fix identation error --- search/engine.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/search/engine.py b/search/engine.py index 7d4d8934..f8e19585 100644 --- a/search/engine.py +++ b/search/engine.py @@ -210,8 +210,10 @@ def search_single_word(word, filters, user, start, end, user_state, aff, case_se # Factures if "2" in aff: - filter_factures = contains_filter("user__pseudo", word, case_sensitive) - & Q(user__state__in=user_state) + filter_factures = ( + contains_filter("user__pseudo", word, case_sensitive) + & Q(user__state__in=user_state) + ) if start is not None: filter_factures &= Q(date__gte=start) if end is not None: @@ -266,7 +268,6 @@ def search_single_word(word, filters, user, start, end, user_state, aff, case_se | contains_filter("building__name", word, case_sensitive) | Q(port__details=word) ) - filters["rooms"] |= filter_rooms # Switch ports @@ -376,7 +377,7 @@ def search_single_query(query, filters, user, start, end, user_state, aff): return filters # Handle standard queries - return search_single_word(query.text, filters, user, start, end, user_state, aff, q.case_sensitive) + return search_single_word(query.text, filters, user, start, end, user_state, aff, query.case_sensitive) def create_queries(query): From 1a49c1a7446e7c2100054b3168dd08a1f7646db7 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Tue, 18 Feb 2020 21:45:34 +0000 Subject: [PATCH 10/18] Improve behavior when searching for rooms --- search/engine.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/search/engine.py b/search/engine.py index f8e19585..36d320e5 100644 --- a/search/engine.py +++ b/search/engine.py @@ -31,6 +31,9 @@ from __future__ import unicode_literals from netaddr import EUI, AddrFormatError from django.db.models import Q +from django.db.models import Value +from django.db.models.functions import Concat + from users.models import User, Adherent, Club, Ban, Whitelist from machines.models import Machine from topologie.models import Port, Switch, Room @@ -174,9 +177,10 @@ def search_single_word(word, filters, user, start, end, user_state, aff, case_se | contains_filter("pseudo", word, case_sensitive) | contains_filter("email", word, case_sensitive) | contains_filter("telephone", word, case_sensitive) - | contains_filter("room__name", word, case_sensitive) - | contains_filter("room__building__name", word, case_sensitive) + | contains_filter("room_full_name", word, case_sensitive) # Added through annotate ) + + # Users have a name whereas clubs only have a surname filter_users = (filter_clubs | contains_filter("name", word, case_sensitive)) if not User.can_view_all(user)[0]: @@ -264,8 +268,7 @@ def search_single_word(word, filters, user, start, end, user_state, aff, case_se if "5" in aff and Room.can_view_all(user): filter_rooms = ( contains_filter("details", word, case_sensitive) - | contains_filter("name", word, case_sensitive) - | contains_filter("building__name", word, case_sensitive) + | contains_filter("full_name", word, case_sensitive) # Added through annotate | Q(port__details=word) ) filters["rooms"] |= filter_rooms @@ -273,7 +276,7 @@ def search_single_word(word, filters, user, start, end, user_state, aff, case_se # Switch ports if "6" in aff and User.can_view_all(user): filter_ports = ( - contains_filter("room__name", word, case_sensitive) + contains_filter("room_full_name", word, case_sensitive) # Added through annotate | contains_filter("machine_interface__domain__name", word, case_sensitive) | contains_filter("related__switch__interface__domain__name", word, case_sensitive) | contains_filter("custom_profile__name", word, case_sensitive) @@ -308,6 +311,10 @@ def apply_filters(filters, user, aff): the search query. """ # Results are later filled-in depending on the display filter + # In some cases, annotations are used to match what is displayed in the results + # For example, the displayed room is actually "room__building__name room__name" + # So queries wouldn't match what the user expects if we just kept the + # database's format results = { "users": Adherent.objects.none(), "clubs": Club.objects.none(), @@ -322,8 +329,12 @@ def apply_filters(filters, user, aff): # Users and clubs if "0" in aff: - results["users"] = Adherent.objects.filter(filters["users"]) - results["clubs"] = Club.objects.filter(filters["clubs"]) + results["users"] = Adherent.objects.annotate( + room_full_name=Concat("room__building__name", Value(" "), "room__name"), + ).filter(filters["users"]) + results["clubs"] = Club.objects.annotate( + room_full_name=Concat("room__building__name", Value(" "), "room__name"), + ).filter(filters["clubs"]) # Machines if "1" in aff: @@ -343,11 +354,15 @@ def apply_filters(filters, user, aff): # Rooms if "5" in aff and Room.can_view_all(user): - results["rooms"] = Room.objects.filter(filters["rooms"]) + results["rooms"] = Room.objects.annotate( + full_name=Concat("building__name", Value(" "), "name"), + ).filter(filters["rooms"]) # Switch ports if "6" in aff and User.can_view_all(user): - results["ports"] = Port.objects.filter(filters["ports"]) + results["ports"] = Port.objects.annotate( + room_full_name=Concat("room__building__name", Value(" "), "room__name"), + ).filter(filters["ports"]) # Switches if "7" in aff and Switch.can_view_all(user): From a6be269eece74c4272b4c46f6000ad042d02b608 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Tue, 18 Feb 2020 23:04:38 +0100 Subject: [PATCH 11/18] Fix some translations --- search/locale/fr/LC_MESSAGES/django.po | 6 +++--- users/locale/fr/LC_MESSAGES/django.po | 2 +- users/templates/users/aff_users.html | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/search/locale/fr/LC_MESSAGES/django.po b/search/locale/fr/LC_MESSAGES/django.po index efb33ebe..6f798188 100644 --- a/search/locale/fr/LC_MESSAGES/django.po +++ b/search/locale/fr/LC_MESSAGES/django.po @@ -89,10 +89,10 @@ msgstr "Rechercher" #: search/forms.py:65 search/forms.py:80 msgid "" -"Use « » and «,» to specify distinct words, «\"query\"» for an exact search " -"an exact search, «\\» to escape a character and «+» to combine keywors." +'Use « » and «,» to specify distinct words, «"query"» for' +" an exact search, «\\» to escape a character and «+» to combine keywors." msgstr "" -"Utilisez « » et «,» pour spécifier différents mots, «\"mot\"» pour une " +'Utilisez « » et «,» pour spécifier différents mots, «"mot"» pour une ' "recherche exacte, «\\» pour échapper un caractère et «+» pour combiner des mots clés." #: search/forms.py:88 diff --git a/users/locale/fr/LC_MESSAGES/django.po b/users/locale/fr/LC_MESSAGES/django.po index 55780636..12580ba1 100644 --- a/users/locale/fr/LC_MESSAGES/django.po +++ b/users/locale/fr/LC_MESSAGES/django.po @@ -826,7 +826,7 @@ msgid "Shell" msgstr "Interface en ligne de commande" #: users/templates/users/aff_users.html:35 -msgid "Firt name" +msgid "First name" msgstr "Prénom" #: users/templates/users/delete.html:29 diff --git a/users/templates/users/aff_users.html b/users/templates/users/aff_users.html index 8365e259..d0704167 100644 --- a/users/templates/users/aff_users.html +++ b/users/templates/users/aff_users.html @@ -32,7 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc., - {% trans "Firt name" as tr_name %} + {% trans "First name" as tr_name %} {% trans "Surname" as tr_surname %} From 615f25146fefd2e9396fd44f2e4b0936bd444272 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Tue, 18 Feb 2020 23:37:37 +0100 Subject: [PATCH 12/18] Fix wrongfuly "No result" in search --- search/templates/search/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/search/templates/search/index.html b/search/templates/search/index.html index a93547cc..e560ccaa 100644 --- a/search/templates/search/index.html +++ b/search/templates/search/index.html @@ -119,7 +119,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,

{% endif %} - {% if not users and not machines and not factures and not whitelists and not bans and not rooms and not ports and not switches %} + {% if not users and not clubs and not machines and not factures and not whitelists and not bans and not rooms and not ports and not switches %}

{% trans "No result" %}

{% else %}
{% blocktrans %}Only the first {{ max_result }} results are displayed in each category.{% endblocktrans %}
From 562973efc7836917aeb43bec30940e92d0c9cbc9 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Tue, 18 Feb 2020 23:41:15 +0100 Subject: [PATCH 13/18] Fix typo in translation --- search/forms.py | 4 ++-- search/locale/fr/LC_MESSAGES/django.po | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/search/forms.py b/search/forms.py index 1cfd02cb..1a29ce2d 100644 --- a/search/forms.py +++ b/search/forms.py @@ -63,7 +63,7 @@ class SearchForm(Form): help_text=( _( 'Use « » and «,» to specify distinct words, «"query"» for' - " an exact search, «\\» to escape a character and «+» to combine keywors." + " an exact search, «\\» to escape a character and «+» to combine keywords." ) ), max_length=100, @@ -78,7 +78,7 @@ class SearchFormPlus(Form): help_text=( _( 'Use « » and «,» to specify distinct words, «"query"» for' - " an exact search, «\\» to escape a character and «+» to combine keywors." + " an exact search, «\\» to escape a character and «+» to combine keywords." ) ), max_length=100, diff --git a/search/locale/fr/LC_MESSAGES/django.po b/search/locale/fr/LC_MESSAGES/django.po index 6f798188..356c1ae4 100644 --- a/search/locale/fr/LC_MESSAGES/django.po +++ b/search/locale/fr/LC_MESSAGES/django.po @@ -89,10 +89,10 @@ msgstr "Rechercher" #: search/forms.py:65 search/forms.py:80 msgid "" -'Use « » and «,» to specify distinct words, «"query"» for' -" an exact search, «\\» to escape a character and «+» to combine keywors." +"Use « » and «,» to specify distinct words, «\"query\"» for" +" an exact search, «\\» to escape a character and «+» to combine keywords." msgstr "" -'Utilisez « » et «,» pour spécifier différents mots, «"mot"» pour une ' +"Utilisez « » et «,» pour spécifier différents mots, «\"recherche\"» pour une " "recherche exacte, «\\» pour échapper un caractère et «+» pour combiner des mots clés." #: search/forms.py:88 From 27b4bee02c7930ba423eef3dd89ca4d6a86a6004 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Tue, 18 Feb 2020 23:43:35 +0100 Subject: [PATCH 14/18] Remove duplicate translation --- users/locale/fr/LC_MESSAGES/django.po | 4 ---- 1 file changed, 4 deletions(-) diff --git a/users/locale/fr/LC_MESSAGES/django.po b/users/locale/fr/LC_MESSAGES/django.po index 12580ba1..7d630850 100644 --- a/users/locale/fr/LC_MESSAGES/django.po +++ b/users/locale/fr/LC_MESSAGES/django.po @@ -825,10 +825,6 @@ msgstr "Group d'accès" msgid "Shell" msgstr "Interface en ligne de commande" -#: users/templates/users/aff_users.html:35 -msgid "First name" -msgstr "Prénom" - #: users/templates/users/delete.html:29 msgid "Deletion of users" msgstr "Suppression d'utilisateurs" From 870523b43925aff9fcdb9f0959316e3e6531f6b1 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Wed, 19 Feb 2020 09:50:16 +0000 Subject: [PATCH 15/18] Add ability to search by building and room name without space --- search/engine.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/search/engine.py b/search/engine.py index 36d320e5..62ca5e53 100644 --- a/search/engine.py +++ b/search/engine.py @@ -178,6 +178,7 @@ def search_single_word(word, filters, user, start, end, user_state, aff, case_se | contains_filter("email", word, case_sensitive) | contains_filter("telephone", word, case_sensitive) | contains_filter("room_full_name", word, case_sensitive) # Added through annotate + | contains_filter("room_full_name_stuck", word, case_sensitive) # Added through annotate ) # Users have a name whereas clubs only have a surname @@ -269,6 +270,7 @@ def search_single_word(word, filters, user, start, end, user_state, aff, case_se filter_rooms = ( contains_filter("details", word, case_sensitive) | contains_filter("full_name", word, case_sensitive) # Added through annotate + | contains_filter("full_name_stuck", word, case_sensitive) # Added through annotate | Q(port__details=word) ) filters["rooms"] |= filter_rooms @@ -277,6 +279,7 @@ def search_single_word(word, filters, user, start, end, user_state, aff, case_se if "6" in aff and User.can_view_all(user): filter_ports = ( contains_filter("room_full_name", word, case_sensitive) # Added through annotate + | contains_filter("room_full_name_stuck", word, case_sensitive) # Added through annotate | contains_filter("machine_interface__domain__name", word, case_sensitive) | contains_filter("related__switch__interface__domain__name", word, case_sensitive) | contains_filter("custom_profile__name", word, case_sensitive) @@ -331,9 +334,11 @@ def apply_filters(filters, user, aff): if "0" in aff: results["users"] = Adherent.objects.annotate( room_full_name=Concat("room__building__name", Value(" "), "room__name"), + room_full_name_stuck=Concat("room__building__name", "room__name"), ).filter(filters["users"]) results["clubs"] = Club.objects.annotate( room_full_name=Concat("room__building__name", Value(" "), "room__name"), + room_full_name_stuck=Concat("room__building__name", "room__name"), ).filter(filters["clubs"]) # Machines @@ -356,12 +361,14 @@ def apply_filters(filters, user, aff): if "5" in aff and Room.can_view_all(user): results["rooms"] = Room.objects.annotate( full_name=Concat("building__name", Value(" "), "name"), + full_name_stuck=Concat("building__name", "name"), ).filter(filters["rooms"]) # Switch ports if "6" in aff and User.can_view_all(user): results["ports"] = Port.objects.annotate( room_full_name=Concat("room__building__name", Value(" "), "room__name"), + room_full_name_stuck=Concat("room__building__name", "room__name"), ).filter(filters["ports"]) # Switches From a708fe2dda1398fe8c99ad5c59cb6f85dcbe178a Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Wed, 19 Feb 2020 11:06:57 +0100 Subject: [PATCH 16/18] Fix linting issues --- search/engine.py | 71 +++++++++++++++++--------- search/forms.py | 6 ++- search/locale/fr/LC_MESSAGES/django.po | 10 ++-- search/views.py | 18 +++++-- 4 files changed, 70 insertions(+), 35 deletions(-) diff --git a/search/engine.py b/search/engine.py index 62ca5e53..3dfd0680 100644 --- a/search/engine.py +++ b/search/engine.py @@ -144,7 +144,10 @@ def finish_results(request, results, col, order): max_result = GeneralOption.get_cached_value("search_display_page") for name, val in results.items(): page_arg = name + "_page" - results[name] = re2o_paginator(request, val.distinct(), max_result, page_arg=page_arg) + results[name] = re2o_paginator(request, + val.distinct(), + max_result, + page_arg=page_arg) results.update({"max_result": max_result}) @@ -162,7 +165,8 @@ def contains_filter(attribute, word, case_sensitive=False): return Q(**{attr: word}) -def search_single_word(word, filters, user, start, end, user_state, aff, case_sensitive=False): +def search_single_word(word, filters, user, start, end, + user_state, aff, case_sensitive=False): """ Construct the correct filters to match differents fields of some models with the given query according to the given filters. The match field are either CharField or IntegerField that will be displayed @@ -177,12 +181,16 @@ def search_single_word(word, filters, user, start, end, user_state, aff, case_se | contains_filter("pseudo", word, case_sensitive) | contains_filter("email", word, case_sensitive) | contains_filter("telephone", word, case_sensitive) - | contains_filter("room_full_name", word, case_sensitive) # Added through annotate - | contains_filter("room_full_name_stuck", word, case_sensitive) # Added through annotate + # Added through annotate + | contains_filter("room_full_name", word, case_sensitive) + | contains_filter("room_full_name_stuck", word, case_sensitive) ) # Users have a name whereas clubs only have a surname - filter_users = (filter_clubs | contains_filter("name", word, case_sensitive)) + filter_users = ( + filter_clubs + | contains_filter("name", word, case_sensitive) + ) if not User.can_view_all(user)[0]: filter_clubs &= Q(id=user.id) @@ -198,14 +206,16 @@ def search_single_word(word, filters, user, start, end, user_state, aff, case_se if "1" in aff: filter_machines = ( contains_filter("name", word, case_sensitive) - | contains_filter("user__pseudo", word, case_sensitive) & Q(user__state__in=user_state) + | (contains_filter("user__pseudo", word, case_sensitive) + & Q(user__state__in=user_state)) | contains_filter("interface__domain__name", word, case_sensitive) - | contains_filter("interface__domain__related_domain__name", word, case_sensitive) + | contains_filter("interface__domain__related_domain__name", + word, case_sensitive) | contains_filter("interface__mac_address", word, case_sensitive) | contains_filter("interface__ipv4__ipv4", word, case_sensitive) ) try: - _mac_addr = EUI(word, 48) + _ = EUI(word, 48) filter_machines |= Q(interface__mac_address=word) except AddrFormatError: pass @@ -269,8 +279,9 @@ def search_single_word(word, filters, user, start, end, user_state, aff, case_se if "5" in aff and Room.can_view_all(user): filter_rooms = ( contains_filter("details", word, case_sensitive) - | contains_filter("full_name", word, case_sensitive) # Added through annotate - | contains_filter("full_name_stuck", word, case_sensitive) # Added through annotate + # Added through annotate + | contains_filter("full_name", word, case_sensitive) + | contains_filter("full_name_stuck", word, case_sensitive) | Q(port__details=word) ) filters["rooms"] |= filter_rooms @@ -278,13 +289,17 @@ def search_single_word(word, filters, user, start, end, user_state, aff, case_se # Switch ports if "6" in aff and User.can_view_all(user): filter_ports = ( - contains_filter("room_full_name", word, case_sensitive) # Added through annotate - | contains_filter("room_full_name_stuck", word, case_sensitive) # Added through annotate - | contains_filter("machine_interface__domain__name", word, case_sensitive) - | contains_filter("related__switch__interface__domain__name", word, case_sensitive) + contains_filter("machine_interface__domain__name", + word, case_sensitive) + | contains_filter("related__switch__interface__domain__name", + word, case_sensitive) | contains_filter("custom_profile__name", word, case_sensitive) - | contains_filter("custom_profile__profil_default", word, case_sensitive) + | contains_filter("custom_profile__profil_default", + word, case_sensitive) | contains_filter("details", word, case_sensitive) + # Added through annotate + | contains_filter("room_full_name", word, case_sensitive) + | contains_filter("room_full_name_stuck", word, case_sensitive) ) if is_int(word): filter_ports |= Q(port=word) @@ -295,7 +310,8 @@ def search_single_word(word, filters, user, start, end, user_state, aff, case_se filter_switches = ( contains_filter("interface__domain__name", word, case_sensitive) | contains_filter("interface__ipv4__ipv4", word, case_sensitive) - | contains_filter("switchbay__building__name", word, case_sensitive) + | contains_filter("switchbay__building__name", + word, case_sensitive) | contains_filter("stack__name", word, case_sensitive) | contains_filter("model__reference", word, case_sensitive) | contains_filter("model__constructor__name", word, case_sensitive) @@ -314,10 +330,10 @@ def apply_filters(filters, user, aff): the search query. """ # Results are later filled-in depending on the display filter - # In some cases, annotations are used to match what is displayed in the results - # For example, the displayed room is actually "room__building__name room__name" - # So queries wouldn't match what the user expects if we just kept the - # database's format + # In some cases, annotations are used to match what is displayed in the + # results. For example, the displayed room is actually + # "room__building__name room__name", so queries wouldn't match what the + # user expects if we just kept the database's format results = { "users": Adherent.objects.none(), "clubs": Club.objects.none(), @@ -333,11 +349,13 @@ def apply_filters(filters, user, aff): # Users and clubs if "0" in aff: results["users"] = Adherent.objects.annotate( - room_full_name=Concat("room__building__name", Value(" "), "room__name"), + room_full_name=Concat("room__building__name", + Value(" "), "room__name"), room_full_name_stuck=Concat("room__building__name", "room__name"), ).filter(filters["users"]) results["clubs"] = Club.objects.annotate( - room_full_name=Concat("room__building__name", Value(" "), "room__name"), + room_full_name=Concat("room__building__name", + Value(" "), "room__name"), room_full_name_stuck=Concat("room__building__name", "room__name"), ).filter(filters["clubs"]) @@ -367,7 +385,8 @@ def apply_filters(filters, user, aff): # Switch ports if "6" in aff and User.can_view_all(user): results["ports"] = Port.objects.annotate( - room_full_name=Concat("room__building__name", Value(" "), "room__name"), + room_full_name=Concat("room__building__name", + Value(" "), "room__name"), room_full_name_stuck=Concat("room__building__name", "room__name"), ).filter(filters["ports"]) @@ -386,7 +405,8 @@ def search_single_query(query, filters, user, start, end, user_state, aff): newfilters = empty_filters() for q in query.subqueries: # Construct an independent filter for each subquery - subfilters = search_single_query(q, empty_filters(), user, start, end, user_state, aff) + subfilters = search_single_query(q, empty_filters(), user, + start, end, user_state, aff) # Apply the subfilter for field in filter_fields(): @@ -399,7 +419,8 @@ def search_single_query(query, filters, user, start, end, user_state, aff): return filters # Handle standard queries - return search_single_word(query.text, filters, user, start, end, user_state, aff, query.case_sensitive) + return search_single_word(query.text, filters, user, start, end, + user_state, aff, query.case_sensitive) def create_queries(query): diff --git a/search/forms.py b/search/forms.py index 1a29ce2d..f6e90cd7 100644 --- a/search/forms.py +++ b/search/forms.py @@ -63,7 +63,8 @@ class SearchForm(Form): help_text=( _( 'Use « » and «,» to specify distinct words, «"query"» for' - " an exact search, «\\» to escape a character and «+» to combine keywords." + " search, «\\» to escape a character and «+» to" + " combine keywords." ) ), max_length=100, @@ -78,7 +79,8 @@ class SearchFormPlus(Form): help_text=( _( 'Use « » and «,» to specify distinct words, «"query"» for' - " an exact search, «\\» to escape a character and «+» to combine keywords." + " an exact search, «\\» to escape a character and «+» to" + " combine keywords." ) ), max_length=100, diff --git a/search/locale/fr/LC_MESSAGES/django.po b/search/locale/fr/LC_MESSAGES/django.po index 356c1ae4..5afe48c4 100644 --- a/search/locale/fr/LC_MESSAGES/django.po +++ b/search/locale/fr/LC_MESSAGES/django.po @@ -89,11 +89,13 @@ msgstr "Rechercher" #: search/forms.py:65 search/forms.py:80 msgid "" -"Use « » and «,» to specify distinct words, «\"query\"» for" -" an exact search, «\\» to escape a character and «+» to combine keywords." +"Use « » and «,» to specify distinct words, «"query"» for" +" search, «\\» to escape a character and «+» to" +" combine keywords." msgstr "" -"Utilisez « » et «,» pour spécifier différents mots, «\"recherche\"» pour une " -"recherche exacte, «\\» pour échapper un caractère et «+» pour combiner des mots clés." +"Utilisez « » et «,» pour spécifier différents mots, «\"recherche\"» pour" +" une recherche exacte, «\\» pour échapper un caractère et «+» pour" +" combiner des mots clés." #: search/forms.py:88 msgid "Users filter" diff --git a/search/views.py b/search/views.py index 60d0a92f..a994240f 100644 --- a/search/views.py +++ b/search/views.py @@ -43,7 +43,8 @@ from search.forms import ( ) from re2o.acl import can_view_all -from .engine import * +from .engine import empty_filters, create_queries, search_single_query +from .engine import apply_filters, finish_results def get_results(query, request, params): @@ -66,7 +67,12 @@ def get_results(query, request, params): ) results = apply_filters(filters, request.user, aff) - results = finish_results(request, results, request.GET.get("col"), request.GET.get("order")) + results = finish_results( + request, + results, + request.GET.get("col"), + request.GET.get("order") + ) results.update({"search_term": query}) return results @@ -82,7 +88,9 @@ def search(request): request, "search/index.html", get_results( - search_form.cleaned_data.get("q", ""), request, search_form.cleaned_data + search_form.cleaned_data.get("q", ""), + request, + search_form.cleaned_data ), ) return render(request, "search/search.html", {"search_form": search_form}) @@ -98,7 +106,9 @@ def searchp(request): request, "search/index.html", get_results( - search_form.cleaned_data.get("q", ""), request, search_form.cleaned_data + search_form.cleaned_data.get("q", ""), + request, + search_form.cleaned_data ), ) return render(request, "search/search.html", {"search_form": search_form}) From d6316bbc0b4546f6a1a3f1e7ec2cb4af4258c716 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Wed, 19 Feb 2020 11:07:53 +0100 Subject: [PATCH 17/18] Fix translation --- search/locale/fr/LC_MESSAGES/django.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/search/locale/fr/LC_MESSAGES/django.po b/search/locale/fr/LC_MESSAGES/django.po index 5afe48c4..749f4184 100644 --- a/search/locale/fr/LC_MESSAGES/django.po +++ b/search/locale/fr/LC_MESSAGES/django.po @@ -89,7 +89,7 @@ msgstr "Rechercher" #: search/forms.py:65 search/forms.py:80 msgid "" -"Use « » and «,» to specify distinct words, «"query"» for" +"Use « » and «,» to specify distinct words, «\"query\"» for" " search, «\\» to escape a character and «+» to" " combine keywords." msgstr "" From 57631f368dea4a046a3f7cbcdd98d75518de1b18 Mon Sep 17 00:00:00 2001 From: Jean-Romain Garnier Date: Wed, 19 Feb 2020 11:10:07 +0100 Subject: [PATCH 18/18] Fix wrong translation in advanced search --- search/forms.py | 2 +- search/locale/fr/LC_MESSAGES/django.po | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/search/forms.py b/search/forms.py index f6e90cd7..9f2ff82a 100644 --- a/search/forms.py +++ b/search/forms.py @@ -63,7 +63,7 @@ class SearchForm(Form): help_text=( _( 'Use « » and «,» to specify distinct words, «"query"» for' - " search, «\\» to escape a character and «+» to" + " an exact search, «\\» to escape a character and «+» to" " combine keywords." ) ), diff --git a/search/locale/fr/LC_MESSAGES/django.po b/search/locale/fr/LC_MESSAGES/django.po index 749f4184..ca0507ae 100644 --- a/search/locale/fr/LC_MESSAGES/django.po +++ b/search/locale/fr/LC_MESSAGES/django.po @@ -90,7 +90,7 @@ msgstr "Rechercher" #: search/forms.py:65 search/forms.py:80 msgid "" "Use « » and «,» to specify distinct words, «\"query\"» for" -" search, «\\» to escape a character and «+» to" +" an exact search, «\\» to escape a character and «+» to" " combine keywords." msgstr "" "Utilisez « » et «,» pour spécifier différents mots, «\"recherche\"» pour"
{% include 'buttons/sort.html' with prefix='user' col="name" text=tr_name %}{% include 'buttons/sort.html' with prefix='user' col="surname" text=tr_surname %}