8
0
Fork 0
mirror of https://gitlab.federez.net/re2o/re2o synced 2024-07-06 22:24:06 +00:00
re2o/search/views.py

505 lines
17 KiB
Python
Raw Normal View History

2017-01-15 23:01:18 +00:00
# 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
2017-01-15 23:01:18 +00:00
# Copyright © 2017 Augustin Lemesle
# Copyright © 2019 Jean-Romain Garnier
2017-01-15 23:01:18 +00:00
#
# 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.
2017-11-02 19:58:20 +00:00
"""The views for the search app, responsible for finding the matches
Augustin lemesle, Gabriel Détraz, Lara Kermarec, Maël Kervella
2017-11-02 19:58:20 +00:00
Gplv2"""
from __future__ import unicode_literals
from netaddr import EUI, AddrFormatError
2016-07-02 22:27:22 +00:00
from django.shortcuts import render
2016-07-08 10:35:53 +00:00
from django.contrib.auth.decorators import login_required
2016-07-02 22:27:22 +00:00
from django.db.models import Q
from users.models import User, Adherent, Club, Ban, Whitelist
2017-11-02 19:58:20 +00:00
from machines.models import Machine
from cotisations.models import Cotisation
2017-11-02 16:06:44 +00:00
from topologie.models import Port, Switch, Room
2016-07-02 22:27:22 +00:00
from cotisations.models import Facture
from preferences.models import GeneralOption
from search.forms import (
SearchForm,
SearchFormPlus,
CHOICES_USER,
CHOICES_AFF,
initial_choices,
)
2019-12-06 00:01:13 +00:00
from re2o.base import SortTable, re2o_paginator
from re2o.acl import can_view_all
2020-02-18 13:29:47 +00:00
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 = ""
2020-02-18 14:01:48 +00:00
@property
def plaintext(self):
if self.operator is not None:
return self.operator.join([q.plaintext for q in self.subqueries])
return self.text
2020-02-18 13:29:47 +00:00
def is_int(variable):
""" Check if the variable can be casted to an integer """
try:
int(variable)
except ValueError:
return False
else:
return True
2017-11-07 13:02:19 +00:00
2020-02-18 11:02:08 +00:00
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"""
2020-02-18 13:29:47 +00:00
return {f: Q() for f in filter_fields()}
2020-02-18 11:02:08 +00:00
2019-12-06 00:01:13 +00:00
def finish_results(request, results, col, order):
2017-11-07 13:02:19 +00:00
"""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
)
2020-02-18 13:29:47 +00:00
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():
2019-12-06 00:01:13 +00:00
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)
2020-02-18 13:29:47 +00:00
| Q(room__name__icontains=word)
| Q(room__building__name__icontains=word)
)
filter_users = (filter_clubs | Q(name__icontains=word))
2017-12-30 01:17:45 +00:00
if not User.can_view_all(user)[0]:
filter_clubs &= Q(id=user.id)
2017-12-30 01:17:45 +00:00
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
2017-12-30 01:17:45 +00:00
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
)
2017-11-02 19:58:20 +00:00
if start is not None:
filter_factures &= Q(date__gte=start)
2017-11-02 19:58:20 +00:00
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)
2017-11-02 19:58:20 +00:00
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))
)
2017-11-02 19:58:20 +00:00
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)
2017-11-02 19:58:20 +00:00
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))
)
2017-11-02 19:58:20 +00:00
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
2017-11-02 16:06:44 +00:00
# Rooms
if "5" in aff and Room.can_view_all(user):
filter_rooms = (
2020-02-18 14:01:48 +00:00
Q(details__icontains=word) | Q(name__icontains=word) | Q(port__details=word) | Q(building__name__icontains=word)
2017-11-02 16:06:44 +00:00
)
2020-02-16 18:35:46 +00:00
filters["rooms"] |= filter_rooms
2017-11-02 16:06:44 +00:00
# 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):
2020-02-18 11:02:08 +00:00
""" 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
2020-02-18 11:02:08 +00:00
def search_single_query(query, filters, user, start, end, user_state, aff):
""" Handle different queries an construct the correct filters using
search_single_word"""
2020-02-18 13:29:47 +00:00
if query.operator == "+":
2020-02-18 11:02:08 +00:00
# Special queries with "+" operators should use & rather than |
2020-02-18 13:29:47 +00:00
newfilters = empty_filters()
for q in query.subqueries:
2020-02-18 11:02:08 +00:00
# Construct an independent filter for each subquery
2020-02-18 13:29:47 +00:00
subfilters = search_single_query(q, empty_filters(), user, start, end, user_state, aff)
2020-02-18 11:02:08 +00:00
2020-02-18 13:29:47 +00:00
# Apply the subfilter
2020-02-18 11:02:08 +00:00
for field in filter_fields():
2020-02-18 13:29:47 +00:00
newfilters[field] &= subfilters[field]
# Add these filters to the existing ones
for field in filter_fields():
filters[field] |= newfilters[field]
2020-02-18 11:02:08 +00:00
return filters
# Handle standard queries
2020-02-18 13:29:47 +00:00
return search_single_word(query.text, filters, user, start, end, user_state, aff)
2020-02-18 11:02:08 +00:00
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
2020-02-18 11:02:08 +00:00
Values in between quotation marks are not searched accross
multiple field in the database (contrary to +)
- spaces (' ') and commas (',') are used to separated words
2020-02-18 11:02:08 +00:00
- "+" signs are used as "and" operators
"""
2020-02-18 11:02:08 +00:00
# A dict representing the different queries extracted from the user's text
queries = []
current_query = None
2020-02-18 11:02:08 +00:00
# Whether the query is between "
keep_intact = False
2020-02-18 11:02:08 +00:00
# Whether the previous char was a \
escaping_char = False
2020-02-18 11:02:08 +00:00
for char in query:
2020-02-18 11:02:08 +00:00
if current_query is None:
# We are starting a new word
2020-02-18 13:29:47 +00:00
current_query = Query()
2020-02-18 11:02:08 +00:00
if escaping_char:
# The last char war a \ so we escape this char
escaping_char = False
2020-02-18 13:29:47 +00:00
current_query.add_char(char)
continue
2020-02-18 11:02:08 +00:00
if char == "\\":
# We need to escape the next char
escaping_char = True
continue
2020-02-18 11:02:08 +00:00
if char == '"':
# Toogle the keep_intact state, if true, we are between two "
keep_intact = not keep_intact
continue
2020-02-18 11:02:08 +00:00
if keep_intact:
# If we are between two ", ignore separators
2020-02-18 13:29:47 +00:00
current_query.add_char(char)
continue
2020-02-18 11:02:08 +00:00
2020-02-16 18:35:46 +00:00
if char == "+":
2020-02-18 13:29:47 +00:00
if len(current_query.text) == 0:
# Can't sart a query with a "+", consider it escaped
current_query.add_char(char)
2020-02-18 11:02:08 +00:00
continue
2020-02-18 13:29:47 +00:00
current_query.add_operator("+")
2020-02-16 18:35:46 +00:00
continue
2020-02-18 11:02:08 +00:00
if char == " " or char == ",":
# If we encouter a separator outside of ", we create a new word
2020-02-18 11:02:08 +00:00
2020-02-18 13:29:47 +00:00
if len(current_query.text) == 0:
2020-02-18 11:02:08 +00:00
# Discard empty queries
continue
2020-02-18 13:29:47 +00:00
if current_query.operator is not None:
2020-02-18 11:02:08 +00:00
# If we were building a special structure, finish building it
2020-02-18 13:29:47 +00:00
current_query.add_operator(current_query.operator)
2020-02-18 11:02:08 +00:00
# Save the query and start a new one
queries.append(current_query)
current_query = None
continue
2020-02-18 11:02:08 +00:00
# If we haven't encountered any special case, add the char to the word
2020-02-18 13:29:47 +00:00
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)
2017-11-07 13:02:19 +00:00
2020-02-18 11:02:08 +00:00
return queries
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))
2020-02-18 11:02:08 +00:00
filters = empty_filters()
2020-02-18 11:02:08 +00:00
queries = create_queries(query)
2020-02-18 13:29:47 +00:00
for q in queries:
2020-02-18 11:02:08 +00:00
filters = search_single_query(
2020-02-18 13:29:47 +00:00
q, filters, request.user, start, end, user_state, aff
)
results = apply_filters(filters, request.user, aff)
2019-12-06 00:01:13 +00:00
results = finish_results(request, results, request.GET.get("col"), request.GET.get("order"))
results.update({"search_term": query})
return results
2016-07-04 21:56:51 +00:00
2017-11-02 19:58:20 +00:00
2016-07-08 10:35:53 +00:00
@login_required
@can_view_all(User, Machine, Cotisation)
2016-07-04 21:56:51 +00:00
def search(request):
2017-11-02 19:58:20 +00:00
""" La page de recherche standard """
search_form = SearchForm(request.GET or None)
if search_form.is_valid():
return render(
request,
"search/index.html",
get_results(
search_form.cleaned_data.get("q", ""), request, search_form.cleaned_data
),
)
return render(request, "search/search.html", {"search_form": search_form})
2017-11-02 19:58:20 +00:00
2016-07-04 21:56:51 +00:00
2016-07-08 10:35:53 +00:00
@login_required
@can_view_all(User, Machine, Cotisation)
2016-07-04 21:56:51 +00:00
def searchp(request):
2017-11-02 19:58:20 +00:00
""" La page de recherche avancée """
search_form = SearchFormPlus(request.GET or None)
if search_form.is_valid():
return render(
request,
"search/index.html",
get_results(
search_form.cleaned_data.get("q", ""), request, search_form.cleaned_data
),
)
return render(request, "search/search.html", {"search_form": search_form})