mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-11-22 11:23:10 +00:00
Merge branch 'room_building_search' into 'dev'
Improve search functionality See merge request federez/re2o!488
This commit is contained in:
commit
d058fe5b59
7 changed files with 548 additions and 324 deletions
515
search/engine.py
Normal file
515
search/engine.py
Normal file
|
@ -0,0 +1,515 @@
|
||||||
|
# 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 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
|
||||||
|
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="", 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"""
|
||||||
|
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.case_sensitive))
|
||||||
|
self.text = ""
|
||||||
|
self.case_sensitive = False
|
||||||
|
|
||||||
|
@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])
|
||||||
|
|
||||||
|
if self.case_sensitive:
|
||||||
|
return "\"{}\"".format(self.text)
|
||||||
|
|
||||||
|
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 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
|
||||||
|
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 = (
|
||||||
|
contains_filter("surname", word, case_sensitive)
|
||||||
|
| contains_filter("pseudo", word, case_sensitive)
|
||||||
|
| contains_filter("email", word, case_sensitive)
|
||||||
|
| contains_filter("telephone", word, case_sensitive)
|
||||||
|
# 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)
|
||||||
|
)
|
||||||
|
|
||||||
|
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 = (
|
||||||
|
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:
|
||||||
|
_ = 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 = (
|
||||||
|
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:
|
||||||
|
filter_factures &= Q(date__lte=end)
|
||||||
|
filters["factures"] |= filter_factures
|
||||||
|
|
||||||
|
# Bans
|
||||||
|
if "3" in aff:
|
||||||
|
filter_bans = (
|
||||||
|
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))
|
||||||
|
| (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 = (
|
||||||
|
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))
|
||||||
|
| (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 = (
|
||||||
|
contains_filter("details", word, case_sensitive)
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Switch ports
|
||||||
|
if "6" in aff and User.can_view_all(user):
|
||||||
|
filter_ports = (
|
||||||
|
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)
|
||||||
|
# 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)
|
||||||
|
filters["ports"] |= filter_ports
|
||||||
|
|
||||||
|
# Switches
|
||||||
|
if "7" in aff and Switch.can_view_all(user):
|
||||||
|
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("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)
|
||||||
|
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
|
||||||
|
# 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(),
|
||||||
|
"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.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
|
||||||
|
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.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
|
||||||
|
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, query.case_sensitive)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if keep_intact:
|
||||||
|
current_query.case_sensitive = True
|
||||||
|
|
||||||
|
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
|
|
@ -63,7 +63,8 @@ class SearchForm(Form):
|
||||||
help_text=(
|
help_text=(
|
||||||
_(
|
_(
|
||||||
'Use « » and «,» to specify distinct words, «"query"» for'
|
'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 keywords."
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
|
@ -78,7 +79,8 @@ class SearchFormPlus(Form):
|
||||||
help_text=(
|
help_text=(
|
||||||
_(
|
_(
|
||||||
'Use « » and «,» to specify distinct words, «"query"» for'
|
'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 keywords."
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
|
|
|
@ -89,11 +89,13 @@ msgstr "Rechercher"
|
||||||
|
|
||||||
#: search/forms.py:65 search/forms.py:80
|
#: search/forms.py:65 search/forms.py:80
|
||||||
msgid ""
|
msgid ""
|
||||||
"Use « » and «,» to specify distinct words, «\"query\"» for an exact search "
|
"Use « » and «,» to specify distinct words, «\"query\"» for"
|
||||||
"and «\\» to escape a character."
|
" an exact search, «\\» to escape a character and «+» to"
|
||||||
|
" combine keywords."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Utilisez « » et «,» pour spécifier différents mots, «\"query\"» pour une "
|
"Utilisez « » et «,» pour spécifier différents mots, «\"recherche\"» pour"
|
||||||
"recherche exacte et «\\» pour échapper un caractère."
|
" une recherche exacte, «\\» pour échapper un caractère et «+» pour"
|
||||||
|
" combiner des mots clés."
|
||||||
|
|
||||||
#: search/forms.py:88
|
#: search/forms.py:88
|
||||||
msgid "Users filter"
|
msgid "Users filter"
|
||||||
|
|
|
@ -119,7 +119,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<hr/>
|
<hr/>
|
||||||
<br/>
|
<br/>
|
||||||
{% endif %}
|
{% 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 %}
|
||||||
<h3>{% trans "No result" %}</h3>
|
<h3>{% trans "No result" %}</h3>
|
||||||
{% else %}
|
{% else %}
|
||||||
<h6>{% blocktrans %}Only the first {{ max_result }} results are displayed in each category.{% endblocktrans %}</h6>
|
<h6>{% blocktrans %}Only the first {{ max_result }} results are displayed in each category.{% endblocktrans %}</h6>
|
||||||
|
|
333
search/views.py
333
search/views.py
|
@ -28,18 +28,12 @@ Gplv2"""
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from netaddr import EUI, AddrFormatError
|
|
||||||
|
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
from django.db.models import Q
|
from users.models import User
|
||||||
from users.models import User, Adherent, Club, Ban, Whitelist
|
|
||||||
from machines.models import Machine
|
|
||||||
from cotisations.models import Cotisation
|
from cotisations.models import Cotisation
|
||||||
from topologie.models import Port, Switch, Room
|
from machines.models import Machine
|
||||||
from cotisations.models import Facture
|
|
||||||
from preferences.models import GeneralOption
|
|
||||||
from search.forms import (
|
from search.forms import (
|
||||||
SearchForm,
|
SearchForm,
|
||||||
SearchFormPlus,
|
SearchFormPlus,
|
||||||
|
@ -47,294 +41,10 @@ from search.forms import (
|
||||||
CHOICES_AFF,
|
CHOICES_AFF,
|
||||||
initial_choices,
|
initial_choices,
|
||||||
)
|
)
|
||||||
from re2o.base import SortTable, re2o_paginator
|
|
||||||
from re2o.acl import can_view_all
|
from re2o.acl import can_view_all
|
||||||
|
|
||||||
|
from .engine import empty_filters, create_queries, search_single_query
|
||||||
def is_int(variable):
|
from .engine import apply_filters, finish_results
|
||||||
""" 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["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)
|
|
||||||
)
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
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_word.
|
|
||||||
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 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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def get_results(query, request, params):
|
def get_results(query, request, params):
|
||||||
|
@ -348,26 +58,21 @@ def get_results(query, request, params):
|
||||||
user_state = params.get("u", initial_choices(CHOICES_USER))
|
user_state = params.get("u", initial_choices(CHOICES_USER))
|
||||||
aff = params.get("a", initial_choices(CHOICES_AFF))
|
aff = params.get("a", initial_choices(CHOICES_AFF))
|
||||||
|
|
||||||
filters = {
|
filters = empty_filters()
|
||||||
"users": Q(),
|
|
||||||
"clubs": Q(),
|
|
||||||
"machines": Q(),
|
|
||||||
"factures": Q(),
|
|
||||||
"bans": Q(),
|
|
||||||
"whitelists": Q(),
|
|
||||||
"rooms": Q(),
|
|
||||||
"ports": Q(),
|
|
||||||
"switches": Q(),
|
|
||||||
}
|
|
||||||
|
|
||||||
words = get_words(query)
|
queries = create_queries(query)
|
||||||
for word in words:
|
for q in queries:
|
||||||
filters = search_single_word(
|
filters = search_single_query(
|
||||||
word, filters, request.user, start, end, user_state, aff
|
q, filters, request.user, start, end, user_state, aff
|
||||||
)
|
)
|
||||||
|
|
||||||
results = apply_filters(filters, request.user, aff)
|
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})
|
results.update({"search_term": query})
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
@ -383,7 +88,9 @@ def search(request):
|
||||||
request,
|
request,
|
||||||
"search/index.html",
|
"search/index.html",
|
||||||
get_results(
|
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})
|
return render(request, "search/search.html", {"search_form": search_form})
|
||||||
|
@ -399,7 +106,9 @@ def searchp(request):
|
||||||
request,
|
request,
|
||||||
"search/index.html",
|
"search/index.html",
|
||||||
get_results(
|
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})
|
return render(request, "search/search.html", {"search_form": search_form})
|
||||||
|
|
|
@ -825,10 +825,6 @@ msgstr "Group d'accès"
|
||||||
msgid "Shell"
|
msgid "Shell"
|
||||||
msgstr "Interface en ligne de commande"
|
msgstr "Interface en ligne de commande"
|
||||||
|
|
||||||
#: users/templates/users/aff_users.html:35
|
|
||||||
msgid "Firt name"
|
|
||||||
msgstr "Prénom"
|
|
||||||
|
|
||||||
#: users/templates/users/delete.html:29
|
#: users/templates/users/delete.html:29
|
||||||
msgid "Deletion of users"
|
msgid "Deletion of users"
|
||||||
msgstr "Suppression d'utilisateurs"
|
msgstr "Suppression d'utilisateurs"
|
||||||
|
|
|
@ -32,7 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{% trans "Firt name" as tr_name %}
|
{% trans "First name" as tr_name %}
|
||||||
<th>{% include 'buttons/sort.html' with prefix='user' col="name" text=tr_name %}</th>
|
<th>{% include 'buttons/sort.html' with prefix='user' col="name" text=tr_name %}</th>
|
||||||
{% trans "Surname" as tr_surname %}
|
{% trans "Surname" as tr_surname %}
|
||||||
<th>{% include 'buttons/sort.html' with prefix='user' col="surname" text=tr_surname %}</th>
|
<th>{% include 'buttons/sort.html' with prefix='user' col="surname" text=tr_surname %}</th>
|
||||||
|
|
Loading…
Reference in a new issue