Project

General

Profile

Revision 5a845c93

ID5a845c934e3801e4ae1cf896cdc3a3fe5d2cf26f
Parent ac065134
Child 00ece25a

Added by Vincent QUEMENER over 10 years ago

Tri des événements dans VigiBoard (#1186)

Ajoute la possibilité de trier sur les colonnes de VigiBoard.

Change-Id: Ibd7e5c71dfd6a9d81be3846942b19d7dc067b397
Refs: #1186
Reviewed-on: https://vigilo-dev.si.c-s.fr/review/1282
Tested-by: Build system <>
Reviewed-by: Thomas BURGUIERE <>

View differences:

vigiboard/controllers/plugins/__init__.py
84 84
    def get_search_fields(self):
85 85
        return []
86 86

  
87
    def get_sort_criterion(self, query, column):
88
        """
89
        Cette méthode renvoie le critère à utiliser par SQLAlchemy pour trier
90
        la requête alimentant le tableau des événements.
91

  
92
        Cette méthode DEVRAIT être surchargée dans les classes dérivées
93
        si le plugin en question implémente un tri.
94

  
95
        @param query: La requête VigiBoard servant à alimenter le tableau des
96
            événements.
97
        @type  query: L{VigiboardRequest}
98
        @param column: La colonne sur laquelle l'utilisateur souhaite opérer le
99
            tri.
100
        @type column: C{str}
101
        """
102
        pass
103

  
87 104
    def handle_search_fields(self, query, search, state, subqueries):
88 105
        pass
vigiboard/controllers/plugins/date.py
105 105
            'date': event[0].cause.timestamp,
106 106
            'duration': duration,
107 107
        }
108

  
109
    def get_sort_criterion(self, query, column):
110
        if column == 'date':
111
            return tables.Event.timestamp
112
        return None
113

  
vigiboard/controllers/plugins/hls.py
100 100
        @rtype:  C{dict}
101 101
        """
102 102

  
103
        if not events_ids:
104
            return {}
105

  
103 106
        imp_hls1 = aliased(tables.ImpactedHLS)
104 107
        imp_hls2 = aliased(tables.ImpactedHLS)
105 108

  
vigiboard/controllers/plugins/hostname.py
52 52
        return {
53 53
            'hostname': event.hostname,
54 54
        }
55

  
56
    def get_sort_criterion(self, query, column):
57
        if column == 'hostname':
58
            return query.items.c.hostname
59
        return None
60

  
vigiboard/controllers/plugins/occurrences.py
22 22
Un plugin pour VigiBoard qui ajoute une colonne avec le nombre
23 23
d'occurrences d'un événement corrélé donné.
24 24
"""
25
from vigilo.models.tables import StateName
25
from vigilo.models.tables import StateName, CorrEvent
26 26
from vigiboard.controllers.plugins import VigiboardRequestPlugin
27 27

  
28 28
class PluginOccurrences(VigiboardRequestPlugin):
......
38 38
            'state': state,
39 39
            'occurrences': event[0].occurrence,
40 40
        }
41

  
42
    def get_sort_criterion(self, query, column):
43
        if column == 'occurrences':
44
            return CorrEvent.occurrence
45
        return None
46

  
vigiboard/controllers/plugins/output.py
52 52
        return {
53 53
            'output': event[0].cause.message,
54 54
        }
55

  
56
    def get_sort_criterion(self, query, column):
57
        if column == 'output':
58
            return Event.message
59
        return None
60

  
vigiboard/controllers/plugins/priority.py
137 137
            'state': state,
138 138
            'priority': event[0].priority,
139 139
        }
140

  
141
    def get_sort_criterion(self, query, column):
142
        if column == 'priority':
143
            return CorrEvent.priority
144
        return None
145

  
vigiboard/controllers/plugins/servicename.py
55 55
        return {
56 56
            'servicename': event.servicename,
57 57
        }
58

  
59
    def get_sort_criterion(self, query, column):
60
        if column == 'servicename':
61
            return query.items.c.servicename
62
        return None
63

  
vigiboard/controllers/plugins/status.py
116 116
            'id': event[0].idcorrevent,
117 117
            'ack': ack,
118 118
        }
119

  
120
    def get_sort_criterion(self, query, column):
121
        criteria = {
122
            'ticket': CorrEvent.trouble_ticket,
123
            'ack': CorrEvent.ack,
124
        }
125
        return criteria.get(column)
126

  
vigiboard/controllers/root.py
131 131
        """Schéma de validation de la méthode index."""
132 132
        # Si on ne passe pas le paramètre "page" ou qu'on passe une valeur
133 133
        # invalide ou pas de valeur du tout, alors on affiche la 1ère page.
134
        page = validators.Int(min=1, if_missing=1, if_invalid=1, not_empty=True)
134
        page = validators.Int(
135
            min=1,
136
            if_missing=1,
137
            if_invalid=1,
138
            not_empty=True
139
        )
140

  
141
        # Paramètres de tri
142
        sort = validators.String(if_missing=None)
143
        order = validators.OneOf(['asc', 'desc'], if_missing='asc')
135 144

  
136 145
        # Nécessaire pour que les critères de recherche soient conservés.
137 146
        allow_extra_fields = True
......
146 155
    @expose('events_table.html')
147 156
    @expose('events_table.html', content_type='text/csv')
148 157
    @require(access_restriction)
149
    def index(self, page, **search):
158
    def index(self, page, sort=None, order=None, **search):
150 159
        """
151 160
        Page d'accueil de Vigiboard. Elle affiche, suivant la page demandée
152 161
        (page 1 par defaut), la liste des événements, rangés par ordre de prise
......
155 164

  
156 165
        @param page: Numéro de la page souhaitée, commence à 1
157 166
        @type page: C{int}
167
        @param sort: Colonne de tri
168
        @type sort: C{str} or C{None}
169
        @param order: Ordre du tri (asc ou desc)
170
        @type order: C{str} or C{None}
158 171
        @param search: Dictionnaire contenant les critères de recherche.
159 172
        @type search: C{dict}
160 173

  
......
168 181
        self.get_failures()
169 182

  
170 183
        user = get_current_user()
171
        aggregates = VigiboardRequest(user, search=search)
184
        aggregates = VigiboardRequest(user, search=search, sort=sort, order=order)
172 185

  
173 186
        aggregates.add_table(
174 187
            CorrEvent,
......
249 262
            servicename = None,
250 263
            plugins_data = plugins_data,
251 264
            page = page,
265
            sort = sort,
266
            order = order,
252 267
            event_edit_status_options = edit_event_status_options,
253 268
            search_form = create_search_form,
254 269
            search = search,
......
310 325
        Affichage de la liste des événements bruts masqués d'un événement
311 326
        corrélé (événements agrégés dans l'événement corrélé).
312 327

  
328
        @param page: numéro de la page à afficher.
329
        @type  page: C{int}
313 330
        @param idcorrevent: identifiant de l'événement corrélé souhaité.
314
        @type idcorrevent: C{int}
331
        @type  idcorrevent: C{int}
315 332
        """
316 333

  
317 334
        # Auto-supervision
......
448 465

  
449 466
    class ItemSchema(schema.Schema):
450 467
        """Schéma de validation de la méthode item."""
468
        # Si on ne passe pas le paramètre "page" ou qu'on passe une valeur
469
        # invalide ou pas de valeur du tout, alors on affiche la 1ère page.
451 470
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
471

  
472
        # Paramètres de tri
473
        sort = validators.String(if_missing=None)
474
        order = validators.OneOf(['asc', 'desc'], if_missing='asc')
475

  
476
        # L'hôte / service dont on doit afficher les évènements
452 477
        host = validators.String(not_empty=True)
453 478
        service = validators.String(if_missing=None)
454 479

  
......
457 482
        error_handler = process_form_errors)
458 483
    @expose('events_table.html')
459 484
    @require(access_restriction)
460
    def item(self, page, host, service):
485
    def item(self, page, host, service, sort=None, order=None):
461 486
        """
462 487
        Affichage de l'historique de l'ensemble des événements corrélés
463 488
        jamais ouverts sur l'hôte / service demandé.
464 489
        Pour accéder à cette page, l'utilisateur doit être authentifié.
465 490

  
466 491
        @param page: Numéro de la page à afficher.
492
        @type: C{int}
467 493
        @param host: Nom de l'hôte souhaité.
494
        @type: C{str}
468 495
        @param service: Nom du service souhaité
496
        @type: C{str}
497
        @param sort: Colonne de tri
498
        @type: C{str} or C{None}
499
        @param order: Ordre du tri (asc ou desc)
500
        @type: C{str} or C{None}
469 501

  
470 502
        Cette méthode permet de satisfaire l'exigence
471 503
        VIGILO_EXIG_VIGILO_BAC_0080.
......
480 512
            redirect('/')
481 513

  
482 514
        user = get_current_user()
483
        aggregates = VigiboardRequest(user, False)
515
        aggregates = VigiboardRequest(user, False, sort=sort, order=order)
484 516
        aggregates.add_table(
485 517
            CorrEvent,
486 518
            aggregates.items.c.hostname,
......
520 552
            servicename = service,
521 553
            plugins_data = plugins_data,
522 554
            page = page,
555
            sort = sort,
556
            order = order,
523 557
            event_edit_status_options = edit_event_status_options,
524 558
            search_form = create_search_form,
525 559
            search = {},
......
993 1027
    opérée sur l'un des événements dont l'identifiant
994 1028
    fait partie de la liste passée en paramètre.
995 1029
    """
996
    last_modification_timestamp = DBSession.query(
1030
    if not event_id_list:
1031
        last_modification_timestamp = None
1032
    else:
1033
        last_modification_timestamp = DBSession.query(
997 1034
                                func.max(EventHistory.timestamp),
998 1035
                         ).filter(EventHistory.idevent.in_(event_id_list)
999 1036
                         ).scalar()
1037

  
1000 1038
    if not last_modification_timestamp:
1001 1039
        if not value_if_none:
1002 1040
            return None
vigiboard/controllers/vigiboardrequest.py
43 43
    le préformatage des événements et celui des historiques
44 44
    """
45 45

  
46
    def __init__(self, user, mask_closed_events=True, search=None):
46
    def __init__(self, user, mask_closed_events=True, search=None, sort=None, order=None):
47 47
        """
48 48
        Initialisation de l'objet qui effectue les requêtes de VigiBoard
49 49
        sur la base de données.
50 50
        Cet objet est responsable de la vérification des droits de
51 51
        l'utilisateur sur les données manipulées.
52

  
53
        @param user: Nom de l'utilisateur cherchant à afficher les événements.
54
        @type  user: C{str}
55
        @param mask_closed_events: Booléen indiquant si l'on souhaite masquer les
56
            événements fermés ou non.
57
        @type  mask_closed_events: C{boolean}
58
        @param search: Dictionnaire contenant les critères de recherche.
59
        @type  search: C{dict}
60
        @param sort: Colonne de tri; vide en l'absence de tri.
61
        @type  sort: C{unicode}
62
        @param order: Ordre du tri ("asc" ou "desc"); vide en l'absence de tri.
63
        @type  order: C{unicode}
64

  
52 65
        """
53 66

  
54 67
        # Permet s'appliquer des filtres de recherche aux sous-requêtes.
......
82 95
            StateName.statename,
83 96
        ]
84 97

  
85
        # Permet de définir le sens de tri pour la priorité.
86
        if config['vigiboard_priority_order'] == 'asc':
87
            priority_order = asc(CorrEvent.priority)
88
        else:
89
            priority_order = desc(CorrEvent.priority)
90

  
91
        # Tris (ORDER BY)
92
        # Permet de répondre aux exigences suivantes :
93
        # - VIGILO_EXIG_VIGILO_BAC_0050
94
        # - VIGILO_EXIG_VIGILO_BAC_0060
95
        self.orderby = [
96
            asc(CorrEvent.ack),                             # État acquittement
97
            asc(StateName.statename.in_([u'OK', u'UP'])),   # Vert / Pas vert
98
            priority_order,                                 # Priorité ITIL
99
        ]
100

  
101
        if asbool(config.get('state_first', True)):
102
            self.orderby.extend([
103
                desc(StateName.order),                      # Etat courant
104
                desc(Event.timestamp),                      # Timestamp
105
            ])
106
        else:
107
            self.orderby.extend([
108
                desc(Event.timestamp),                      # Timestamp
109
                desc(StateName.order),                      # Etat courant
110
            ])
111 98

  
112 99
        self.req = DBSession
113 100
        self.plugin = []
......
176 163
            # Permet d'avoir le même format que pour l'autre requête.
177 164
            self.items = items.subquery()
178 165

  
166
        # Tris (ORDER BY)
167
        # Permet de répondre aux exigences suivantes :
168
        # - VIGILO_EXIG_VIGILO_BAC_0050
169
        # - VIGILO_EXIG_VIGILO_BAC_0060
170
        self.orderby = []
171
        if sort:
172
            for _plugin, instance in config.get('columns_plugins', []):
173
                criterion = instance.get_sort_criterion(self, sort)
174
                if criterion is not None:
175
                    if order == 'asc':
176
                        self.orderby.append(asc(criterion))
177
                    else:
178
                        self.orderby.append(desc(criterion))
179

  
180
        # Permet de définir le sens de tri pour la priorité.
181
        if config['vigiboard_priority_order'] == 'asc':
182
            priority_order = asc(CorrEvent.priority)
183
        else:
184
            priority_order = desc(CorrEvent.priority)
185

  
186
        self.orderby.extend([
187
            asc(CorrEvent.ack),                             # État acquittement
188
            asc(StateName.statename.in_([u'OK', u'UP'])),   # Vert / Pas vert
189
            priority_order,                                 # Priorité ITIL
190
        ])
191

  
192
        if asbool(config.get('state_first', True)):
193
            self.orderby.extend([
194
                desc(StateName.order),                      # Etat courant
195
                desc(Event.timestamp),                      # Timestamp
196
            ])
197
        else:
198
            self.orderby.extend([
199
                desc(Event.timestamp),                      # Timestamp
200
                desc(StateName.order),                      # Etat courant
201
            ])
202

  
179 203
        if search is not None:
180 204
            # 2nde passe pour les filtres : self.items est désormais défini.
181 205
            for _plugin, instance in config.get('columns_plugins', []):
vigiboard/tests/functional/test_sorting.py
1
# -*- coding: utf-8 -*-
2
# vim:set expandtab tabstop=4 shiftwidth=4:
3
# Copyright (C) 2006-2013 CS-SI
4
# License: GNU GPL v2 <http://www.gnu.org/licenses/gpl-2.0.html>
5

  
6
"""
7
Test du tri de Vigiboard
8
"""
9

  
10
from __future__ import absolute_import
11

  
12
from nose.tools import assert_true, assert_equal
13

  
14
import transaction
15
from vigilo.models.demo import functions
16
from vigilo.models.session import DBSession
17
from vigilo.models import tables
18

  
19
from vigiboard.tests import TestController
20
from tg import config
21

  
22
def populate_DB():
23
    """ Peuple la base de données en vue des tests. """
24

  
25
    # On crée deux hôtes de test.
26
    host1 = functions.add_host(u'host1')
27
    host2 = functions.add_host(u'host2')
28
    DBSession.flush()
29

  
30
    # On ajoute un service sur chaque hôte.
31
    service1 = functions.add_lowlevelservice(
32
                        host2, u'service1')
33
    service2 = functions.add_lowlevelservice(
34
                        host1, u'service2')
35
    DBSession.flush()
36

  
37
    # On ajoute un événement brut sur chaque service.
38
    event1 = functions.add_event(service1, u'WARNING', u'foo')
39
    event2 = functions.add_event(service2, u'CRITICAL', u'foo')
40
    DBSession.flush()
41

  
42
    # On ajoute un événement corrélé pour chaque événement brut.
43
    functions.add_correvent([event1])
44
    functions.add_correvent([event2])
45
    DBSession.flush()
46
    transaction.commit()
47

  
48
class TestSorting(TestController):
49
    """
50
    Test du tri de Vigiboard
51
    """
52
    def setUp(self):
53
        super(TestSorting, self).setUp()
54
        populate_DB()
55

  
56
    def test_ascending_order(self):
57
        """ Tri dans l'ordre croissant """
58

  
59
        # On affiche la page principale de VigiBoard
60
        # triée sur le nom d'hôte par ordre croissant
61
        environ = {'REMOTE_USER': 'manager'}
62
        response = self.app.get(
63
            '/?sort=hostname&order=asc', extra_environ=environ)
64

  
65
        # Il doit y avoir 2 lignes de résultats :
66
        # - la 1ère concerne 'service2' sur 'host1' ;
67
        # - la 2nde concerne 'service1' sur 'host2'.
68
        # Il doit y avoir plusieurs colonnes dans la ligne de résultats.
69
        hostnames = response.lxml.xpath(
70
            '//table[@class="vigitable"]/tbody/tr/' \
71
            'td[@class="plugin_hostname"]/text()')
72
        assert_equal(hostnames, ['host1', 'host2'])
73
        servicenames = response.lxml.xpath(
74
            '//table[@class="vigitable"]/tbody/tr/' \
75
            'td[@class="plugin_servicename"]/text()')
76
        assert_equal(servicenames, ['service2', 'service1'])
77

  
78
    def test_descending_order(self):
79
        """ Tri dans l'ordre décroissant """
80

  
81
        # On affiche la page principale de VigiBoard
82
        # triée sur le nom de service par ordre décroissant
83
        environ = {'REMOTE_USER': 'manager'}
84
        response = self.app.get(
85
            '/?sort=servicename&order=desc', extra_environ=environ)
86

  
87
        # Il doit y avoir 2 lignes de résultats :
88
        # - la 1ère concerne 'service2' sur 'host1' ;
89
        # - la 2nde concerne 'service1' sur 'host2'.
90
        # Il doit y avoir plusieurs colonnes dans la ligne de résultats.
91
        hostnames = response.lxml.xpath(
92
            '//table[@class="vigitable"]/tbody/tr/' \
93
            'td[@class="plugin_hostname"]/text()')
94
        assert_equal(hostnames, ['host1', 'host2'])
95
        servicenames = response.lxml.xpath(
96
            '//table[@class="vigitable"]/tbody/tr/' \
97
            'td[@class="plugin_servicename"]/text()')
98
        assert_equal(servicenames, ['service2', 'service1'])
99

  
100
    def test_pagination(self):
101
        """ Pagination du tri """
102

  
103
        # On crée autant d'événements qu'on peut en afficher par page + 1,
104
        # afin d'avoir 2 pages dans le bac à événements.
105
        host3 = functions.add_host(u'host3')
106
        service3 = functions.add_lowlevelservice(
107
                            host3, u'service3')
108
        DBSession.flush()
109
        items_per_page = int(config['vigiboard_items_per_page'])
110
        for i in xrange(items_per_page - 1):
111
            event = functions.add_event(service3, u'WARNING', u'foo')
112
            functions.add_correvent([event])
113
            DBSession.flush()
114
        transaction.commit()
115

  
116
        # On affiche la seconde page de VigiBoard avec
117
        # un tri par ordre décroissant sur le nom d'hôte
118
        environ = {'REMOTE_USER': 'manager'}
119
        response = self.app.get(
120
            '/?page=2&sort=hostname&order=desc', extra_environ=environ)
121

  
122
        # Il ne doit y avoir qu'une seule ligne de
123
        # résultats concernant "service2" sur "host1"
124
        hostnames = response.lxml.xpath(
125
            '//table[@class="vigitable"]/tbody/tr/' \
126
            'td[@class="plugin_hostname"]/text()')
127
        assert_equal(hostnames, ['host1'])
128
        servicenames = response.lxml.xpath(
129
            '//table[@class="vigitable"]/tbody/tr/' \
130
            'td[@class="plugin_servicename"]/text()')
131
        assert_equal(servicenames, ['service2'])
132

  
133

  
vigiboard/widgets/search_form.py
45 45
    style = 'display: none'
46 46

  
47 47
    fields = [
48
        twf.HiddenField('page')
48
        twf.HiddenField('page'),
49
        twf.HiddenField('sort'),
50
        twf.HiddenField('order')
49 51
    ]
50 52
    for plugin, instance in tg.config.get('columns_plugins', []):
51 53
        fields.extend(instance.get_search_fields())

Also available in: Unified diff