views.py 12.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# 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  Goulven Kermarec
# 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.

Maël Kervella's avatar
Maël Kervella committed
23 24 25 26
"""The views for the search app, responsible for finding the matches
Augustin lemesle, Gabriel Détraz, Goulven Kermarec, Maël Kervella
Gplv2"""

27 28 29

from __future__ import unicode_literals

30
from django.shortcuts import render
chirac's avatar
chirac committed
31
from django.contrib.auth.decorators import login_required
32 33

from django.db.models import Q
34
from users.models import User, Adherent, Club, Ban, Whitelist
Maël Kervella's avatar
Maël Kervella committed
35
from machines.models import Machine
36
from topologie.models import Port, Switch, Room
37
from cotisations.models import Facture
38
from preferences.models import GeneralOption
39 40 41 42 43 44 45
from search.forms import (
    SearchForm,
    SearchFormPlus,
    CHOICES_USER,
    CHOICES_AFF,
    initial_choices
)
46
from re2o.utils import SortTable
47

48 49 50 51 52 53 54 55 56 57 58

def is_int(variable):
    """ Check if the variable can be casted to an integer """

    try:
        int(variable)
    except ValueError:
        return False
    else:
        return True

Maël Kervella's avatar
Maël Kervella committed
59

60
def finish_results(results, col, order):
Maël Kervella's avatar
Maël Kervella committed
61
    """Sort the results by applying filters and then limit them to the
62 63
    number of max results. Finally add the info of the nmax number of results
    to the dict"""
64

65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
    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
    )

114
    max_result = GeneralOption.get_cached_value('search_display_page')
115 116 117 118 119 120 121
    for name, val in results.items():
        results[name] = val.distinct()[:max_result]
    results.update({'max_result': max_result})

    return results


LEVY-FALK Hugo's avatar
LEVY-FALK Hugo committed
122
def search_single_word(word, filters, user,
Maël Kervella's avatar
Maël Kervella committed
123
                       start, end, user_state, aff):
124 125 126 127 128 129 130
    """ 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."""

131 132
    # Users
    if '0' in aff:
133
        filter_users = (
134
            Q(
135
                surname__icontains=word
136
            ) | Q(
137
                pseudo__icontains=word
138
            ) | Q(
139
                room__name__icontains=word
140
            ) | Q(
141
                room__name__icontains=word
142 143
            )
        ) & Q(state__in=user_state)
LEVY-FALK Hugo's avatar
LEVY-FALK Hugo committed
144 145
        if not User.can_view_all(user)[0]:
            filter_users &= Q(id=user.id)
146
        filter_clubs = filter_users
147
        filter_users |= Q(name__icontains=word)
148
        filters['users'] |= filter_users
149
        filters['clubs'] |= filter_clubs
150 151 152

    # Machines
    if '1' in aff:
153 154
        filter_machines = Q(
            name__icontains=word
155 156
        ) | (
            Q(
157
                user__pseudo__icontains=word
158 159 160
            ) & Q(
                user__state__in=user_state
            )
161
        ) | Q(
162
            interface__domain__name__icontains=word
163
        ) | Q(
164
            interface__domain__related_domain__name__icontains=word
165
        ) | Q(
166
            interface__mac_address__icontains=word
167
        ) | Q(
168
            interface__ipv4__ipv4__icontains=word
169
        )
LEVY-FALK Hugo's avatar
LEVY-FALK Hugo committed
170 171
        if not Machine.can_view_all(user)[0]:
            filter_machines &= Q(user__id=user.id)
172
        filters['machines'] |= filter_machines
173 174 175

    # Factures
    if '2' in aff:
176 177
        filter_factures = Q(
            user__pseudo__icontains=word
178 179
        ) & Q(
            user__state__in=user_state
180
        )
Maël Kervella's avatar
Maël Kervella committed
181
        if start is not None:
182
            filter_factures &= Q(date__gte=start)
Maël Kervella's avatar
Maël Kervella committed
183
        if end is not None:
184 185
            filter_factures &= Q(date__lte=end)
        filters['factures'] |= filter_factures
186 187 188

    # Bans
    if '3' in aff:
189
        filter_bans = (
190
            Q(
191
                user__pseudo__icontains=word
192 193 194
            ) & Q(
                user__state__in=user_state
            )
195
        ) | Q(
196
            raison__icontains=word
197
        )
Maël Kervella's avatar
Maël Kervella committed
198
        if start is not None:
199
            filter_bans &= (
200 201 202 203 204 205
                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)
            )
Maël Kervella's avatar
Maël Kervella committed
206
        if end is not None:
207
            filter_bans &= (
208 209 210 211 212 213
                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)
            )
214
        filters['bans'] |= filter_bans
215 216 217

    # Whitelists
    if '4' in aff:
218
        filter_whitelists = (
219
            Q(
220
                user__pseudo__icontains=word
221 222 223
            ) & Q(
                user__state__in=user_state
            )
224
        ) | Q(
225
            raison__icontains=word
226
        )
Maël Kervella's avatar
Maël Kervella committed
227
        if start is not None:
228
            filter_whitelists &= (
229 230 231 232 233 234
                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)
            )
Maël Kervella's avatar
Maël Kervella committed
235
        if end is not None:
236
            filter_whitelists &= (
237 238 239 240 241 242
                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)
            )
243
        filters['whitelists'] |= filter_whitelists
244

245
    # Rooms
LEVY-FALK Hugo's avatar
LEVY-FALK Hugo committed
246
    if '5' in aff and Room.can_view_all(user):
247 248
        filter_rooms = Q(
            details__icontains=word
249
        ) | Q(
250
            name__icontains=word
251
        ) | Q(
252
            port__details=word
253
        )
254
        filters['rooms'] |= filter_rooms
255 256

    # Switch ports
LEVY-FALK Hugo's avatar
LEVY-FALK Hugo committed
257
    if '6' in aff and User.can_view_all(user):
258 259
        filter_ports = Q(
            room__name__icontains=word
260
        ) | Q(
261
            machine_interface__domain__name__icontains=word
262
        ) | Q(
Gabriel Detraz's avatar
Gabriel Detraz committed
263
            related__switch__interface__domain__name__icontains=word
264
        ) | Q(
265
            radius__icontains=word
266
        ) | Q(
267
            vlan_force__name__icontains=word
268
        ) | Q(
269
            details__icontains=word
270
        )
271 272 273
        if is_int(word):
            filter_ports |= Q(
                port=word
274
            )
275
        filters['ports'] |= filter_ports
276

277
    # Switches
LEVY-FALK Hugo's avatar
LEVY-FALK Hugo committed
278
    if '7' in aff and Switch.can_view_all(user):
279
        filter_switches = Q(
Gabriel Detraz's avatar
Gabriel Detraz committed
280
            interface__domain__name__icontains=word
281
        ) | Q(
Gabriel Detraz's avatar
Gabriel Detraz committed
282
            interface__ipv4__ipv4__icontains=word
283
        ) | Q(
284
            switchbay__building__name__icontains=word
285
        ) | Q(
286
            stack__name__icontains=word
287
        ) | Q(
288
            model__reference__icontains=word
289
        ) | Q(
290
            model__constructor__name__icontains=word
291
        ) | Q(
Gabriel Detraz's avatar
Gabriel Detraz committed
292
            interface__details__icontains=word
293
        )
294 295 296
        if is_int(word):
            filter_switches |= Q(
                number=word
297
            ) | Q(
298
                stack_member_id=word
299
            )
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
        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
            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
Maël Kervella's avatar
Maël Kervella committed
339
        if char == ' ' or char == ',':
340 341 342 343 344 345
            # 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
Maël Kervella's avatar
Maël Kervella committed
346

347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
    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(),
363
        'clubs': Q(),
364 365 366 367 368 369 370 371 372 373 374 375 376 377
        'machines': Q(),
        'factures': Q(),
        'bans': Q(),
        'whitelists': Q(),
        'rooms': Q(),
        'ports': Q(),
        'switches': Q()
    }

    words = get_words(query)
    for word in words:
        filters = search_single_word(
            word,
            filters,
LEVY-FALK Hugo's avatar
LEVY-FALK Hugo committed
378
            request.user,
379 380 381 382
            start,
            end,
            user_state,
            aff
383 384
        )

385
    results = {
386 387
        'users': Adherent.objects.filter(filters['users']),
        'clubs': Club.objects.filter(filters['clubs']),
388 389 390 391 392 393 394 395
        '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'])
    }
396

397 398 399 400 401
    results = finish_results(
        results,
        request.GET.get('col'),
        request.GET.get('order')
    )
402
    results.update({'search_term': query})
403

404
    return results
Dalahro's avatar
Dalahro committed
405

Maël Kervella's avatar
Maël Kervella committed
406

chirac's avatar
chirac committed
407
@login_required
Dalahro's avatar
Dalahro committed
408
def search(request):
Maël Kervella's avatar
Maël Kervella committed
409
    """ La page de recherche standard """
410 411
    search_form = SearchForm(request.GET or None)
    if search_form.is_valid():
412 413 414 415 416 417 418 419 420
        return render(
            request,
            'search/index.html',
            get_results(
                search_form.cleaned_data.get('q', ''),
                request,
                search_form.cleaned_data
            )
        )
Maël Kervella's avatar
Maël Kervella committed
421 422
    return render(request, 'search/search.html', {'search_form': search_form})

Dalahro's avatar
Dalahro committed
423

chirac's avatar
chirac committed
424
@login_required
Dalahro's avatar
Dalahro committed
425
def searchp(request):
Maël Kervella's avatar
Maël Kervella committed
426
    """ La page de recherche avancée """
427 428
    search_form = SearchFormPlus(request.GET or None)
    if search_form.is_valid():
429 430 431 432 433 434 435 436 437
        return render(
            request,
            'search/index.html',
            get_results(
                search_form.cleaned_data.get('q', ''),
                request,
                search_form.cleaned_data
            )
        )
Maël Kervella's avatar
Maël Kervella committed
438
    return render(request, 'search/search.html', {'search_form': search_form})