From a3f10da96f582c9c0bb513b2dbe1b9ffe2fde11b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Tue, 7 Nov 2017 00:46:45 +0000 Subject: [PATCH 1/3] Fix #32 : Permet de search sur plusieurs mots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La syntaxe est classique : '\' pour escape un caractère, '""' pour spécifier de ne pas interpréter et ' ' et ',' pour séparer les mots --- search/templates/search/index.html | 34 +-- search/views.py | 369 ++++++++++++++++++----------- 2 files changed, 248 insertions(+), 155 deletions(-) diff --git a/search/templates/search/index.html b/search/templates/search/index.html index c043a22b..98658bfb 100644 --- a/search/templates/search/index.html +++ b/search/templates/search/index.html @@ -28,39 +28,39 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}Résultats de la recherche{% endblock %} {% block content %} - {% if users_list %} + {% if users %}

Résultats dans les utilisateurs

- {% include "users/aff_users.html" with users_list=users_list %} + {% include "users/aff_users.html" with users_list=users %} {% endif%} - {% if machines_list %} + {% if machines %}

Résultats dans les machines :

- {% include "machines/aff_machines.html" with machines_list=machines_list %} + {% include "machines/aff_machines.html" with machines_list=machines %} {% endif %} - {% if factures_list %} + {% if factures %}

Résultats dans les factures :

- {% include "cotisations/aff_cotisations.html" with facture_list=factures_list %} + {% include "cotisations/aff_cotisations.html" with facture_list=factures %} {% endif %} - {% if whitelists_list %} + {% if whitelists %}

Résultats dans les accès à titre gracieux :

- {% include "users/aff_whitelists.html" with white_list=whitelists_list %} + {% include "users/aff_whitelists.html" with white_list=whitelists %} {% endif %} - {% if bans_list %} + {% if bans %}

Résultats dans les banissements :

- {% include "users/aff_bans.html" with ban_list=bans_list %} + {% include "users/aff_bans.html" with ban_list=bans %} {% endif %} - {% if rooms_list %} + {% if rooms %}

Résultats dans les chambres :

- {% include "topologie/aff_chambres.html" with room_list=rooms_list %} + {% include "topologie/aff_chambres.html" with room_list=rooms %} {% endif %} - {% if switch_ports_list %} + {% if ports %}

Résultats dans les ports :

- {% include "topologie/aff_port.html" with port_list=switch_ports_list %} + {% include "topologie/aff_port.html" with port_list=ports %} {% endif %} - {% if switches_list %} + {% if switches %}

Résultats dans les switchs :

- {% include "topologie/aff_switch.html" with switch_list=switches_list %} + {% include "topologie/aff_switch.html" with switch_list=switches %} {% endif %} - {% if not users_list and not machines_list and not factures_list and not whitelists_list and not bans_list and not rooms_list and not switch_ports_list and not switches_list %} + {% if not users and not machines and not factures and not whitelists and not bans and not rooms and not ports and not switches %}

Aucun résultat

{% else %}
(Seulement les {{ max_result }} premiers résultats sont affichés dans chaque catégorie)
diff --git a/search/views.py b/search/views.py index 4c28de63..75bfe45b 100644 --- a/search/views.py +++ b/search/views.py @@ -56,8 +56,70 @@ def is_int(variable): else: return True +def finish_results(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""" -def get_results(query, request, filters={}): + results['users'] = SortTable.sort( + results['users'], + 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 + ) + + options, _ = GeneralOption.objects.get_or_create() + max_result = options.search_display_page + for name, val in results.items(): + results[name] = val.distinct()[:max_result] + results.update({'max_result': max_result}) + + return results + + +def search_single_word(word, filters, is_cableur, 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 @@ -65,111 +127,74 @@ def get_results(query, request, filters={}): query). IntegerField are matched against the query only if it can be casted to an int.""" - start = filters.get('s', None) - end = filters.get('e', None) - user_state = filters.get('u', initial_choices(CHOICES_USER)) - aff = filters.get('a', initial_choices(CHOICES_AFF)) - - options, _ = GeneralOption.objects.get_or_create() - max_result = options.search_display_page - - results = { - 'users_list': User.objects.none(), - 'machines_list': Machine.objects.none(), - 'factures_list': Facture.objects.none(), - 'bans_list': Ban.objects.none(), - 'whitelists_list': Whitelist.objects.none(), - 'rooms_list': Room.objects.none(), - 'switch_ports_list': Port.objects.none(), - 'switches_list': Switch.objects.none() - } - # Users if '0' in aff: - filter_user_list = ( + filter_users = ( Q( - surname__icontains=query + surname__icontains=word ) | Q( - adherent__name__icontains=query + adherent__name__icontains=word ) | Q( - pseudo__icontains=query + pseudo__icontains=word ) | Q( - club__room__name__icontains=query + club__room__name__icontains=word ) | Q( - adherent__room__name__icontains=query + adherent__room__name__icontains=word ) ) & Q(state__in=user_state) - if not request.user.has_perms(('cableur',)): - filter_user_list &= Q(id=request.user.id) - results['users_list'] = User.objects.filter(filter_user_list) - results['users_list'] = SortTable.sort( - results['users_list'], - request.GET.get('col'), - request.GET.get('order'), - SortTable.USERS_INDEX - ) + if not is_cableur: + filter_users &= Q(id=request.user.id) + filters['users'] |= filter_users # Machines if '1' in aff: - filter_machine_list = Q( - name__icontains=query + filter_machines = Q( + name__icontains=word ) | ( Q( - user__pseudo__icontains=query + user__pseudo__icontains=word ) & Q( user__state__in=user_state ) ) | Q( - interface__domain__name__icontains=query + interface__domain__name__icontains=word ) | Q( - interface__domain__related_domain__name__icontains=query + interface__domain__related_domain__name__icontains=word ) | Q( - interface__mac_address__icontains=query + interface__mac_address__icontains=word ) | Q( - interface__ipv4__ipv4__icontains=query - ) - if not request.user.has_perms(('cableur',)): - filter_machine_list &= Q(user__id=request.user.id) - results['machines_list'] = Machine.objects.filter(filter_machine_list) - results['machines_list'] = SortTable.sort( - results['machines_list'], - request.GET.get('col'), - request.GET.get('order'), - SortTable.MACHINES_INDEX + interface__ipv4__ipv4__icontains=word ) + if not is_cableur: + filter_machines &= Q(user__id=request.user.id) + filters['machines'] |= filter_machines # Factures if '2' in aff: - filter_facture_list = Q( - user__pseudo__icontains=query + filter_factures = Q( + user__pseudo__icontains=word ) & Q( user__state__in=user_state ) if start is not None: - filter_facture_list &= Q(date__gte=start) + filter_factures &= Q(date__gte=start) if end is not None: - filter_facture_list &= Q(date__lte=end) - results['factures_list'] = Facture.objects.filter(filter_facture_list) - results['factures_list'] = SortTable.sort( - results['factures_list'], - request.GET.get('col'), - request.GET.get('order'), - SortTable.COTISATIONS_INDEX - ) + filter_factures &= Q(date__lte=end) + filters['factures'] |= filter_factures # Bans if '3' in aff: - date_filter = ( + filter_bans = ( Q( - user__pseudo__icontains=query + user__pseudo__icontains=word ) & Q( user__state__in=user_state ) ) | Q( - raison__icontains=query + raison__icontains=word ) if start is not None: - date_filter &= ( + filter_bans &= ( Q(date_start__gte=start) & Q(date_end__gte=start) ) | ( Q(date_start__lte=start) & Q(date_end__gte=start) @@ -177,34 +202,28 @@ def get_results(query, request, filters={}): Q(date_start__gte=start) & Q(date_end__lte=start) ) if end is not None: - date_filter &= ( + 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) ) - results['bans_list'] = Ban.objects.filter(date_filter) - results['bans_list'] = SortTable.sort( - results['bans_list'], - request.GET.get('col'), - request.GET.get('order'), - SortTable.USERS_INDEX_BAN - ) + filters['bans'] |= filter_bans # Whitelists if '4' in aff: - date_filter = ( + filter_whitelists = ( Q( - user__pseudo__icontains=query + user__pseudo__icontains=word ) & Q( user__state__in=user_state ) ) | Q( - raison__icontains=query + raison__icontains=word ) if start is not None: - date_filter &= ( + filter_whitelists &= ( Q(date_start__gte=start) & Q(date_end__gte=start) ) | ( Q(date_start__lte=start) & Q(date_end__gte=start) @@ -212,100 +231,174 @@ def get_results(query, request, filters={}): Q(date_start__gte=start) & Q(date_end__lte=start) ) if end is not None: - date_filter &= ( + 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) ) - results['whitelists_list'] = Whitelist.objects.filter(date_filter) - results['whitelists_list'] = SortTable.sort( - results['whitelists_list'], - request.GET.get('col'), - request.GET.get('order'), - SortTable.USERS_INDEX_WHITE - ) + filters['whitelists'] |= filter_whitelists # Rooms - if '5' in aff and request.user.has_perms(('cableur',)): - filter_rooms_list = Q( - details__icontains=query + if '5' in aff and is_cableur: + filter_rooms = Q( + details__icontains=word ) | Q( - name__icontains=query + name__icontains=word ) | Q( - port__details=query - ) - results['rooms_list'] = Room.objects.filter(filter_rooms_list) - results['rooms_list'] = SortTable.sort( - results['rooms_list'], - request.GET.get('col'), - request.GET.get('order'), - SortTable.TOPOLOGIE_INDEX_ROOM + port__details=word ) + filters['rooms'] |= filter_rooms # Switch ports - if '6' in aff and request.user.has_perms(('cableur',)): - filter_ports_list = Q( - room__name__icontains=query + if '6' in aff and is_cableur: + filter_ports = Q( + room__name__icontains=word ) | Q( - machine_interface__domain__name__icontains=query + machine_interface__domain__name__icontains=word ) | Q( - related__switch__switch_interface__domain__name__icontains=query + related__switch__switch_interface__domain__name__icontains=word ) | Q( - radius__icontains=query + radius__icontains=word ) | Q( - vlan_force__name__icontains=query + vlan_force__name__icontains=word ) | Q( - details__icontains=query + details__icontains=word ) - if is_int(query): - filter_ports_list |= Q( - port=query + if is_int(word): + filter_ports |= Q( + port=word ) - results['switch_ports_list'] = Port.objects.filter(filter_ports_list) - results['switch_ports_list'] = SortTable.sort( - results['switch_ports_list'], - request.GET.get('col'), - request.GET.get('order'), - SortTable.TOPOLOGIE_INDEX_PORT - ) + filters['ports'] |= filter_ports # Switches - if '7' in aff and request.user.has_perms(('cableur',)): - filter_switches_list = Q( - switch_interface__domain__name__icontains=query + if '7' in aff and is_cableur: + filter_switches = Q( + switch_interface__domain__name__icontains=word ) | Q( - switch_interface__ipv4__ipv4__icontains=query + switch_interface__ipv4__ipv4__icontains=word ) | Q( - location__icontains=query + location__icontains=word ) | Q( - stack__name__icontains=query + stack__name__icontains=word ) | Q( - model__reference__icontains=query + model__reference__icontains=word ) | Q( - model__constructor__name__icontains=query + model__constructor__name__icontains=word ) | Q( - details__icontains=query + details__icontains=word ) - if is_int(query): - filter_switches_list |= Q( - number=query + if is_int(word): + filter_switches |= Q( + number=word ) | Q( - stack_member_id=query + stack_member_id=word ) - results['switches_list'] = Switch.objects.filter(filter_switches_list) - results['switches_list'] = SortTable.sort( - results['switches_list'], - request.GET.get('col'), - request.GET.get('order'), - SortTable.TOPOLOGIE_INDEX + filters['switches'] |= filter_switches + + return filters + + +def get_words(query): + """Function used to split the uery in different words to look for. + The rules are simple : + - anti-slash ('\\') is used to escape characters + - anything between quotation marks ('"') is kept intact (not + interpreted as separators) excepts anti-slashes used to escape + - spaces (' ') and commas (',') are used to separated words + """ + + words = [] + i = 0 + keep_intact = False + escaping_char = False + for char in query: + if i >= len(words): + # We are starting a new word + words.append('') + if escaping_char: + # The last char war a \ so we escape this char + escaping_char = False + words[i] += char + print( 'escaped '+char+' -> '+words[i] ) + continue + if char == '\\': + # We need to escape the next char + print( 'escaping '+char+' -> '+words[i] ) + 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 + 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 + print(words) + words[i] += char + + return words + + +def get_results(query, request, params): + """The main function of the search procedure. It gather the filters for + each of the different words of the query and concatenate them into a + single filter. Then it calls 'finish_results' and return the queryset of + objects to display as results""" + + start = params.get('s', None) + end = params.get('e', None) + user_state = params.get('u', initial_choices(CHOICES_USER)) + aff = params.get('a', initial_choices(CHOICES_AFF)) + + filters = { + 'users': Q(), + 'machines': Q(), + 'factures': Q(), + 'bans': Q(), + 'whitelists': Q(), + 'rooms': Q(), + 'ports': Q(), + 'switches': Q() + } + + words = get_words(query) + print( words ) + for word in words: + filters = search_single_word( + word, + filters, + request.user.has_perms(('cableur',)), + start, + end, + user_state, + aff ) - for name, val in results.items(): - results[name] = val.distinct()[:max_result] + results = { + 'users': User.objects.filter(filters['users']), + 'machines': Machine.objects.filter(filters['machines']), + 'factures': Facture.objects.filter(filters['factures']), + 'bans': Ban.objects.filter(filters['bans']), + 'whitelists': Whitelist.objects.filter(filters['whitelists']), + 'rooms': Room.objects.filter(filters['rooms']), + 'ports': Port.objects.filter(filters['ports']), + 'switches': Switch.objects.filter(filters['switches']) + } - results.update({'max_result': max_result}) + results = finish_results( + results, + request.GET.get('col'), + request.GET.get('order') + ) results.update({'search_term': query}) return results From 536081da101176c8aeed9dc589ea34d537934ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Tue, 7 Nov 2017 13:02:19 +0000 Subject: [PATCH 2/3] Pep, Pylint & Fix oubli --- search/views.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/search/views.py b/search/views.py index 75bfe45b..a561dd11 100644 --- a/search/views.py +++ b/search/views.py @@ -56,8 +56,9 @@ def is_int(variable): else: return True + def finish_results(results, col, order): - """Sort the results by applying filters and then limit them to the + """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""" @@ -119,7 +120,8 @@ def finish_results(results, col, order): return results -def search_single_word(word, filters, is_cableur, start, end, user_state, aff): +def search_single_word(word, filters, is_cableur, user_id, + 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 @@ -143,7 +145,7 @@ def search_single_word(word, filters, is_cableur, start, end, user_state, aff): ) ) & Q(state__in=user_state) if not is_cableur: - filter_users &= Q(id=request.user.id) + filter_users &= Q(id=user_id) filters['users'] |= filter_users # Machines @@ -166,7 +168,7 @@ def search_single_word(word, filters, is_cableur, start, end, user_state, aff): interface__ipv4__ipv4__icontains=word ) if not is_cableur: - filter_machines &= Q(user__id=request.user.id) + filter_machines &= Q(user__id=user_id) filters['machines'] |= filter_machines # Factures @@ -321,11 +323,9 @@ def get_words(query): # The last char war a \ so we escape this char escaping_char = False words[i] += char - print( 'escaped '+char+' -> '+words[i] ) continue if char == '\\': # We need to escape the next char - print( 'escaping '+char+' -> '+words[i] ) escaping_char = True continue if char == '"': @@ -336,15 +336,14 @@ def get_words(query): # If we are between two ", ignore separators words[i] += char continue - if char == ' ' or char == ',' : + 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 - print(words) words[i] += char - + return words @@ -371,12 +370,12 @@ def get_results(query, request, params): } words = get_words(query) - print( words ) for word in words: filters = search_single_word( word, filters, request.user.has_perms(('cableur',)), + request.user.id, start, end, user_state, From 57021ad573ec95fd159cc4147649827e741d1055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Kervella?= Date: Tue, 7 Nov 2017 22:16:59 +0000 Subject: [PATCH 3/3] Texte d'aide sur l'utilisation de termes de recherche exacts --- search/forms.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/search/forms.py b/search/forms.py index 90f7407f..b0668ef9 100644 --- a/search/forms.py +++ b/search/forms.py @@ -45,21 +45,32 @@ CHOICES_AFF = ( ) -def initial_choices(c): +def initial_choices(choice_set): """Return the choices that should be activated by default for a given set of choices""" - return [i[0] for i in c] + return [i[0] for i in choice_set] class SearchForm(Form): """The form for a simple search""" - q = forms.CharField(label='Search', max_length=100) + q = forms.CharField( + label='Recherche', + help_text=( + 'Utilisez « » et «,» pour spécifier différents mots, «"query"» ' + 'pour une recherche exacte et «\\» pour échapper un caractère.' + ), + max_length=100 + ) class SearchFormPlus(Form): """The form for an advanced search (with filters)""" q = forms.CharField( - label='Search', + label='Recherche', + help_text=( + 'Utilisez « » et «,» pour spécifier différents mots, «"query"» ' + 'pour une recherche exacte et «\\» pour échapper un caractère.' + ), max_length=100, required=False )