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
|
2019-09-29 14:02:28 +00:00
|
|
|
# Copyright © 2017 Lara Kermarec
|
2017-01-15 23:01:18 +00:00
|
|
|
# Copyright © 2017 Augustin Lemesle
|
|
|
|
#
|
|
|
|
# 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
|
2019-09-29 14:02:28 +00:00
|
|
|
Augustin lemesle, Gabriel Détraz, Lara Kermarec, Maël Kervella
|
2017-11-02 19:58:20 +00:00
|
|
|
Gplv2"""
|
|
|
|
|
2017-09-10 23:29:24 +00:00
|
|
|
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
|
2018-10-14 21:31:36 +00:00
|
|
|
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
|
2018-03-18 00:50:51 +00:00
|
|
|
from users.models import User, Adherent, Club, Ban, Whitelist
|
2017-11-02 19:58:20 +00:00
|
|
|
from machines.models import Machine
|
2018-08-10 23:53:46 +00:00
|
|
|
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
|
2017-06-25 23:29:34 +00:00
|
|
|
from preferences.models import GeneralOption
|
2017-11-01 23:19:41 +00:00
|
|
|
from search.forms import (
|
|
|
|
SearchForm,
|
|
|
|
SearchFormPlus,
|
|
|
|
CHOICES_USER,
|
|
|
|
CHOICES_AFF,
|
2019-11-04 16:55:03 +00:00
|
|
|
initial_choices,
|
2017-11-01 23:19:41 +00:00
|
|
|
)
|
2019-12-06 00:01:13 +00:00
|
|
|
from re2o.base import SortTable, re2o_paginator
|
2018-08-10 23:53:46 +00:00
|
|
|
from re2o.acl import can_view_all
|
2016-10-12 20:58:41 +00:00
|
|
|
|
2017-11-02 19:26:20 +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
|
|
|
|
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
|
2017-11-07 00:46:45 +00:00
|
|
|
number of max results. Finally add the info of the nmax number of results
|
|
|
|
to the dict"""
|
2017-11-02 19:26:20 +00:00
|
|
|
|
2019-11-04 16:55:03 +00:00
|
|
|
results["users"] = SortTable.sort(
|
|
|
|
results["users"], col, order, SortTable.USERS_INDEX
|
2017-11-07 00:46:45 +00:00
|
|
|
)
|
2019-11-04 16:55:03 +00:00
|
|
|
results["machines"] = SortTable.sort(
|
|
|
|
results["machines"], col, order, SortTable.MACHINES_INDEX
|
2017-11-07 00:46:45 +00:00
|
|
|
)
|
2019-11-04 16:55:03 +00:00
|
|
|
results["factures"] = SortTable.sort(
|
|
|
|
results["factures"], col, order, SortTable.COTISATIONS_INDEX
|
2017-11-07 00:46:45 +00:00
|
|
|
)
|
2019-11-04 16:55:03 +00:00
|
|
|
results["bans"] = SortTable.sort(
|
|
|
|
results["bans"], col, order, SortTable.USERS_INDEX_BAN
|
2017-11-07 00:46:45 +00:00
|
|
|
)
|
2019-11-04 16:55:03 +00:00
|
|
|
results["whitelists"] = SortTable.sort(
|
|
|
|
results["whitelists"], col, order, SortTable.USERS_INDEX_WHITE
|
2017-11-07 00:46:45 +00:00
|
|
|
)
|
2019-11-04 16:55:03 +00:00
|
|
|
results["rooms"] = SortTable.sort(
|
|
|
|
results["rooms"], col, order, SortTable.TOPOLOGIE_INDEX_ROOM
|
2017-11-07 00:46:45 +00:00
|
|
|
)
|
2019-11-04 16:55:03 +00:00
|
|
|
results["ports"] = SortTable.sort(
|
|
|
|
results["ports"], col, order, SortTable.TOPOLOGIE_INDEX_PORT
|
2017-11-07 00:46:45 +00:00
|
|
|
)
|
2019-11-04 16:55:03 +00:00
|
|
|
results["switches"] = SortTable.sort(
|
|
|
|
results["switches"], col, order, SortTable.TOPOLOGIE_INDEX
|
2017-11-07 00:46:45 +00:00
|
|
|
)
|
|
|
|
|
2019-11-04 16:55:03 +00:00
|
|
|
max_result = GeneralOption.get_cached_value("search_display_page")
|
2017-11-07 00:46:45 +00:00
|
|
|
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)
|
|
|
|
|
2019-11-04 16:55:03 +00:00
|
|
|
results.update({"max_result": max_result})
|
2017-11-07 00:46:45 +00:00
|
|
|
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
2019-11-04 16:55:03 +00:00
|
|
|
def search_single_word(word, filters, user, start, end, user_state, aff):
|
2017-11-02 19:26:20 +00:00
|
|
|
""" 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."""
|
|
|
|
|
2017-11-01 23:19:41 +00:00
|
|
|
# Users
|
2019-11-04 16:55:03 +00:00
|
|
|
if "0" in aff:
|
2017-11-07 00:46:45 +00:00
|
|
|
filter_users = (
|
2019-11-04 16:55:03 +00:00
|
|
|
Q(surname__icontains=word)
|
|
|
|
| Q(pseudo__icontains=word)
|
|
|
|
| Q(room__name__icontains=word)
|
|
|
|
| Q(email__icontains=word)
|
|
|
|
| Q(telephone__icontains=word)
|
2017-11-02 20:06:19 +00:00
|
|
|
) & Q(state__in=user_state)
|
2017-12-30 01:17:45 +00:00
|
|
|
if not User.can_view_all(user)[0]:
|
|
|
|
filter_users &= Q(id=user.id)
|
2018-03-18 00:50:51 +00:00
|
|
|
filter_clubs = filter_users
|
2018-04-14 00:20:44 +00:00
|
|
|
filter_users |= Q(name__icontains=word)
|
2019-11-04 16:55:03 +00:00
|
|
|
filters["users"] |= filter_users
|
|
|
|
filters["clubs"] |= filter_clubs
|
2017-11-01 23:19:41 +00:00
|
|
|
|
|
|
|
# Machines
|
2019-11-04 16:55:03 +00:00
|
|
|
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)
|
2017-11-02 00:25:24 +00:00
|
|
|
)
|
2018-10-14 21:31:36 +00:00
|
|
|
try:
|
2018-11-03 18:24:46 +00:00
|
|
|
_mac_addr = EUI(word, 48)
|
2018-10-14 21:31:36 +00:00
|
|
|
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)
|
2019-11-04 16:55:03 +00:00
|
|
|
filters["machines"] |= filter_machines
|
2017-11-01 23:19:41 +00:00
|
|
|
|
|
|
|
# Factures
|
2019-11-04 16:55:03 +00:00
|
|
|
if "2" in aff:
|
|
|
|
filter_factures = Q(user__pseudo__icontains=word) & Q(
|
2017-11-02 20:06:19 +00:00
|
|
|
user__state__in=user_state
|
2017-11-02 19:26:20 +00:00
|
|
|
)
|
2017-11-02 19:58:20 +00:00
|
|
|
if start is not None:
|
2017-11-07 00:46:45 +00:00
|
|
|
filter_factures &= Q(date__gte=start)
|
2017-11-02 19:58:20 +00:00
|
|
|
if end is not None:
|
2017-11-07 00:46:45 +00:00
|
|
|
filter_factures &= Q(date__lte=end)
|
2019-11-04 16:55:03 +00:00
|
|
|
filters["factures"] |= filter_factures
|
2017-11-01 23:19:41 +00:00
|
|
|
|
|
|
|
# Bans
|
2019-11-04 16:55:03 +00:00
|
|
|
if "3" in aff:
|
2017-11-07 00:46:45 +00:00
|
|
|
filter_bans = (
|
2019-11-04 16:55:03 +00:00
|
|
|
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:
|
2017-11-07 00:46:45 +00:00
|
|
|
filter_bans &= (
|
2019-11-04 16:55:03 +00:00
|
|
|
(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-01 23:19:41 +00:00
|
|
|
)
|
2017-11-02 19:58:20 +00:00
|
|
|
if end is not None:
|
2017-11-07 00:46:45 +00:00
|
|
|
filter_bans &= (
|
2019-11-04 16:55:03 +00:00
|
|
|
(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))
|
2017-11-01 23:19:41 +00:00
|
|
|
)
|
2019-11-04 16:55:03 +00:00
|
|
|
filters["bans"] |= filter_bans
|
2017-11-01 23:19:41 +00:00
|
|
|
|
|
|
|
# Whitelists
|
2019-11-04 16:55:03 +00:00
|
|
|
if "4" in aff:
|
2017-11-07 00:46:45 +00:00
|
|
|
filter_whitelists = (
|
2019-11-04 16:55:03 +00:00
|
|
|
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:
|
2017-11-07 00:46:45 +00:00
|
|
|
filter_whitelists &= (
|
2019-11-04 16:55:03 +00:00
|
|
|
(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-01 23:19:41 +00:00
|
|
|
)
|
2017-11-02 19:58:20 +00:00
|
|
|
if end is not None:
|
2017-11-07 00:46:45 +00:00
|
|
|
filter_whitelists &= (
|
2019-11-04 16:55:03 +00:00
|
|
|
(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))
|
2017-11-01 23:19:41 +00:00
|
|
|
)
|
2019-11-04 16:55:03 +00:00
|
|
|
filters["whitelists"] |= filter_whitelists
|
2017-11-01 23:19:41 +00:00
|
|
|
|
2017-11-02 16:06:44 +00:00
|
|
|
# Rooms
|
2019-11-04 16:55:03 +00:00
|
|
|
if "5" in aff and Room.can_view_all(user):
|
|
|
|
filter_rooms = (
|
|
|
|
Q(details__icontains=word) | Q(name__icontains=word) | Q(port__details=word)
|
2017-11-02 16:06:44 +00:00
|
|
|
)
|
2019-11-04 16:55:03 +00:00
|
|
|
filters["rooms"] |= filter_rooms
|
2017-11-02 16:06:44 +00:00
|
|
|
|
|
|
|
# Switch ports
|
2019-11-04 16:55:03 +00:00
|
|
|
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)
|
2017-11-02 16:54:06 +00:00
|
|
|
)
|
2017-11-07 00:46:45 +00:00
|
|
|
if is_int(word):
|
2019-11-04 16:55:03 +00:00
|
|
|
filter_ports |= Q(port=word)
|
|
|
|
filters["ports"] |= filter_ports
|
2016-11-01 14:29:12 +00:00
|
|
|
|
2017-11-01 23:19:41 +00:00
|
|
|
# Switches
|
2019-11-04 16:55:03 +00:00
|
|
|
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)
|
2017-11-02 16:54:06 +00:00
|
|
|
)
|
2017-11-07 00:46:45 +00:00
|
|
|
if is_int(word):
|
2019-11-04 16:55:03 +00:00
|
|
|
filter_switches |= Q(number=word) | Q(stack_member_id=word)
|
|
|
|
filters["switches"] |= filter_switches
|
2017-11-07 00:46:45 +00:00
|
|
|
|
|
|
|
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
|
2019-11-04 16:55:03 +00:00
|
|
|
words.append("")
|
2017-11-07 00:46:45 +00:00
|
|
|
if escaping_char:
|
|
|
|
# The last char war a \ so we escape this char
|
|
|
|
escaping_char = False
|
|
|
|
words[i] += char
|
|
|
|
continue
|
2019-11-04 16:55:03 +00:00
|
|
|
if char == "\\":
|
2017-11-07 00:46:45 +00:00
|
|
|
# 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
|
2019-11-04 16:55:03 +00:00
|
|
|
if char == " " or char == ",":
|
2017-11-07 00:46:45 +00:00
|
|
|
# If we encouter a separator outside of ", we create a new word
|
2019-11-04 16:55:03 +00:00
|
|
|
if words[i] is not "":
|
2017-11-07 00:46:45 +00:00
|
|
|
i += 1
|
|
|
|
continue
|
|
|
|
# If we haven't encountered any special case, add the char to the word
|
|
|
|
words[i] += char
|
2017-11-07 13:02:19 +00:00
|
|
|
|
2017-11-07 00:46:45 +00:00
|
|
|
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"""
|
|
|
|
|
2019-11-04 16:55:03 +00:00
|
|
|
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))
|
2017-11-07 00:46:45 +00:00
|
|
|
|
|
|
|
filters = {
|
2019-11-04 16:55:03 +00:00
|
|
|
"users": Q(),
|
|
|
|
"clubs": Q(),
|
|
|
|
"machines": Q(),
|
|
|
|
"factures": Q(),
|
|
|
|
"bans": Q(),
|
|
|
|
"whitelists": Q(),
|
|
|
|
"rooms": Q(),
|
|
|
|
"ports": Q(),
|
|
|
|
"switches": Q(),
|
2017-11-07 00:46:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
words = get_words(query)
|
|
|
|
for word in words:
|
|
|
|
filters = search_single_word(
|
2019-11-04 16:55:03 +00:00
|
|
|
word, filters, request.user, start, end, user_state, aff
|
2017-11-02 00:25:24 +00:00
|
|
|
)
|
|
|
|
|
2017-11-07 00:46:45 +00:00
|
|
|
results = {
|
2019-11-04 16:55:03 +00:00
|
|
|
"users": Adherent.objects.filter(filters["users"]),
|
|
|
|
"clubs": Club.objects.filter(filters["clubs"]),
|
|
|
|
"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"]),
|
2017-11-07 00:46:45 +00:00
|
|
|
}
|
2016-11-01 14:29:12 +00:00
|
|
|
|
2019-12-06 00:01:13 +00:00
|
|
|
results = finish_results(request, results, request.GET.get("col"), request.GET.get("order"))
|
2019-11-04 16:55:03 +00:00
|
|
|
results.update({"search_term": query})
|
2016-11-01 14:29:12 +00:00
|
|
|
|
2017-11-01 23:19:41 +00:00
|
|
|
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
|
2018-08-11 00:54:50 +00:00
|
|
|
@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 """
|
2017-11-01 16:01:10 +00:00
|
|
|
search_form = SearchForm(request.GET or None)
|
|
|
|
if search_form.is_valid():
|
2017-11-01 23:19:41 +00:00
|
|
|
return render(
|
|
|
|
request,
|
2019-11-04 16:55:03 +00:00
|
|
|
"search/index.html",
|
2017-11-01 23:19:41 +00:00
|
|
|
get_results(
|
2019-11-04 16:55:03 +00:00
|
|
|
search_form.cleaned_data.get("q", ""), request, search_form.cleaned_data
|
|
|
|
),
|
2017-11-01 23:19:41 +00:00
|
|
|
)
|
2019-11-04 16:55:03 +00:00
|
|
|
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
|
2018-08-11 00:54:50 +00:00
|
|
|
@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 """
|
2017-11-01 16:01:10 +00:00
|
|
|
search_form = SearchFormPlus(request.GET or None)
|
|
|
|
if search_form.is_valid():
|
2017-11-01 23:19:41 +00:00
|
|
|
return render(
|
|
|
|
request,
|
2019-11-04 16:55:03 +00:00
|
|
|
"search/index.html",
|
2017-11-01 23:19:41 +00:00
|
|
|
get_results(
|
2019-11-04 16:55:03 +00:00
|
|
|
search_form.cleaned_data.get("q", ""), request, search_form.cleaned_data
|
|
|
|
),
|
2017-11-01 23:19:41 +00:00
|
|
|
)
|
2019-11-04 16:55:03 +00:00
|
|
|
return render(request, "search/search.html", {"search_form": search_form})
|