Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / root.py @ 6f89d8f8

History | View | Annotate | Download (31.3 KB)

1
# -*- coding: utf-8 -*-
2
# vim:set expandtab tabstop=4 shiftwidth=4: 
3
"""VigiBoard Controller"""
4

    
5
from datetime import datetime
6
from time import mktime
7
import math
8

    
9
from tg.exceptions import HTTPNotFound, HTTPInternalServerError
10
from tg import expose, validate, require, flash, \
11
    tmpl_context, request, config, session, redirect
12
from tw.forms import validators
13
from pylons.i18n import ugettext as _
14
from pylons.i18n import lazy_ugettext as l_
15
from sqlalchemy import asc
16
from sqlalchemy.sql import func
17
from repoze.what.predicates import Any, All, in_group, \
18
                                    has_permission, not_anonymous
19
from formencode import schema
20

    
21
from vigilo.models.session import DBSession
22
from vigilo.models.tables import Event, EventHistory, CorrEvent, Host, \
23
                                    SupItem, SupItemGroup, LowLevelService, \
24
                                    StateName
25
from vigilo.models.tables.grouphierarchy import GroupHierarchy
26
from vigilo.models.functions import sql_escape_like
27
from vigilo.models.tables.secondary_tables import EVENTSAGGREGATE_TABLE
28

    
29
from vigilo.turbogears.controllers.autocomplete import AutoCompleteController
30
from vigilo.turbogears.controllers.proxy import ProxyController
31
from vigilo.turbogears.helpers import get_current_user
32

    
33
from vigiboard.controllers.vigiboardrequest import VigiboardRequest
34
from vigiboard.controllers.vigiboard_controller import VigiboardRootController
35

    
36
from vigiboard.widgets.edit_event import edit_event_status_options
37
from vigiboard.widgets.search_form import create_search_form, get_calendar_lang
38

    
39
__all__ = ('RootController', 'get_last_modification_timestamp', 
40
           'date_to_timestamp')
41

    
42
# pylint: disable-msg=R0201
43
class RootController(VigiboardRootController):
44
    """
45
    Le controller général de vigiboard
46
    """
47
    autocomplete = AutoCompleteController()
48
    nagios = ProxyController('nagios', '/nagios/',
49
        not_anonymous(l_('You need to be authenticated')))
50

    
51

    
52
    # Prédicat pour la restriction de l'accès aux interfaces.
53
    # L'utilisateur doit avoir la permission "vigiboard-access"
54
    # ou appartenir au groupe "managers" pour accéder à VigiBoard.
55
    access_restriction = All(
56
        not_anonymous(msg=l_("You need to be authenticated")),
57
        Any(in_group('managers'),
58
            has_permission('vigiboard-access'),
59
            msg=l_("You don't have access to VigiBoard"))
60
    )
61

    
62
    def process_form_errors(self, *argv, **kwargv):
63
        """
64
        Gestion des erreurs de validation : On affiche les erreurs
65
        puis on redirige vers la dernière page accédée.
66
        """
67
        for k in tmpl_context.form_errors:
68
            flash("'%s': %s" % (k, tmpl_context.form_errors[k]), 'error')
69
        redirect(request.environ.get('HTTP_REFERER', '/'))
70

    
71
    @expose('json')
72
    def handle_validation_errors_json(self, *args, **kwargs):
73
        kwargs['errors'] = tmpl_context.form_errors
74
        return dict(kwargs)
75
    
76
    class DefaultSchema(schema.Schema):
77
        """Schéma de validation de la méthode default."""
78
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
79
        supitemgroup = validators.Int(if_missing=None, if_invalid=None)
80
        host = validators.String(if_missing=None)
81
        service = validators.String(if_missing=None)
82
        output = validators.String(if_missing=None)
83
        trouble_ticket = validators.String(if_missing=None)
84
        from_date = validators.String(if_missing=None)
85
        to_date = validators.String(if_missing=None)
86

    
87
    @validate(
88
        validators=DefaultSchema(),
89
        error_handler = process_form_errors)
90
    @expose('events_table.html')
91
    @require(access_restriction)
92
    def default(self, page, supitemgroup, host, service,
93
                output, trouble_ticket, from_date, to_date):
94
        """
95
        Page d'accueil de Vigiboard. Elle affiche, suivant la page demandée
96
        (page 1 par defaut), la liste des événements, rangés par ordre de prise
97
        en compte, puis de sévérité.
98
        Pour accéder à cette page, l'utilisateur doit être authentifié.
99

100
        @param page: Numéro de la page souhaitée, commence à 1
101
        @param host: Si l'utilisateur souhaite sélectionner seulement certains
102
                     événements suivant leur hôte, il peut placer une expression
103
                     ici en suivant la structure du LIKE en SQL
104
        @param service: Idem que host mais sur les services
105
        @param output: Idem que host mais sur le text explicatif
106
        @param trouble_ticket: Idem que host mais sur les tickets attribués
107

108
        Cette méthode permet de satisfaire les exigences suivantes : 
109
            - VIGILO_EXIG_VIGILO_BAC_0040, 
110
            - VIGILO_EXIG_VIGILO_BAC_0070,
111
            - VIGILO_EXIG_VIGILO_BAC_0100,
112
        """
113
        user = get_current_user()
114
        aggregates = VigiboardRequest(user)
115
        aggregates.add_table(
116
            CorrEvent,
117
            aggregates.items.c.hostname,
118
            aggregates.items.c.servicename
119
        )
120
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
121
        aggregates.add_join((aggregates.items, 
122
            Event.idsupitem == aggregates.items.c.idsupitem))
123
        aggregates.add_order_by(asc(aggregates.items.c.hostname))
124
        
125
        search = {}
126

    
127
        # Application des filtres si nécessaire
128
        if supitemgroup:
129
            search['supitemgroup'] = supitemgroup
130
            aggregates.add_filter(aggregates.items.c.idsupitemgroup == \
131
                supitemgroup)
132

    
133
        if host:
134
            search['host'] = host
135
            host = sql_escape_like(host)
136
            aggregates.add_filter(aggregates.items.c.hostname.ilike(
137
                '%s' % host))
138

    
139
        if service:
140
            search['service'] = service
141
            service = sql_escape_like(service)
142
            aggregates.add_filter(aggregates.items.c.servicename.ilike(
143
                '%s' % service))
144

    
145
        if output:
146
            search['output'] = output
147
            output = sql_escape_like(output)
148
            aggregates.add_filter(Event.message.ilike('%s' % output))
149

    
150
        if trouble_ticket:
151
            search['tt'] = trouble_ticket
152
            trouble_ticket = sql_escape_like(trouble_ticket)
153
            aggregates.add_filter(CorrEvent.trouble_ticket.ilike(
154
                '%s' % trouble_ticket))
155

    
156
        if from_date:
157
            search['from_date'] = from_date.lower()
158
            try:
159
                # TRANSLATORS: Format de date et heure Python/JavaScript.
160
                # TRANSLATORS: http://www.dynarch.com/static/jscalendar-1.0/doc/html/reference.html#node_sec_5.3.5
161
                # TRANSLATORS: http://docs.python.org/release/2.5/lib/module-time.html
162
                from_date = datetime.strptime(
163
                    from_date, _('%Y-%m-%d %I:%M:%S %p'))
164
            except ValueError:
165
                # On ignore silencieusement la date invalide reçue.
166
                pass
167
            else:
168
                aggregates.add_filter(CorrEvent.timestamp_active >= from_date)
169

    
170
        if to_date:
171
            search['to_date'] = to_date.lower()
172
            try:
173
                # TRANSLATORS: Format de date et heure.
174
                # TRANSLATORS: http://www.dynarch.com/static/jscalendar-1.0/doc/html/reference.html#node_sec_5.3.5
175
                # TRANSLATORS: http://docs.python.org/release/2.5/lib/module-time.html
176
                to_date = datetime.strptime(
177
                    to_date, _('%Y-%m-%d %I:%M:%S %p'))
178
            except ValueError:
179
                # On ignore silencieusement la date invalide reçue.
180
                pass
181
            else:
182
                aggregates.add_filter(CorrEvent.timestamp_active <= to_date)
183

    
184
        # Calcul des éléments à afficher et du nombre de pages possibles
185
        total_rows = aggregates.num_rows()
186
        items_per_page = int(config['vigiboard_items_per_page'])
187

    
188
        # Si le numéro de page dépasse le nombre de pages existantes,
189
        # on redirige automatiquement vers la 1ère page.
190
        if total_rows and items_per_page * (page-1) > total_rows:
191
            redirect('/', page=1, **search)
192

    
193
        id_first_row = items_per_page * (page-1)
194
        id_last_row = min(id_first_row + items_per_page, total_rows)
195

    
196
        aggregates.format_events(id_first_row, id_last_row)
197
        aggregates.generate_tmpl_context()
198

    
199
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
200
        if not total_rows:
201
            id_first_row = 0
202
        else:
203
            id_first_row += 1
204

    
205
        return dict(
206
            hostname = None,
207
            servicename = None,
208
            events = aggregates.events,
209
            plugins = get_plugins_instances(),
210
            rows_info = {
211
                'id_first_row': id_first_row,
212
                'id_last_row': id_last_row,
213
                'total_rows': total_rows,
214
            },
215
            nb_pages = nb_pages,
216
            page = page,
217
            event_edit_status_options = edit_event_status_options,
218
            search_form = create_search_form,
219
            search = search,
220
            get_calendar_lang = get_calendar_lang,
221
        )
222

    
223

    
224
    class MaskedEventsSchema(schema.Schema):
225
        """Schéma de validation de la méthode masked_events."""
226
        idcorrevent = validators.Int(not_empty=True)
227
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
228

    
229
    @validate(
230
        validators=MaskedEventsSchema(),
231
        error_handler = process_form_errors)
232
    @expose('raw_events_table.html')
233
    @require(access_restriction)
234
    def masked_events(self, idcorrevent, page):
235
        """
236
        Affichage de la liste des événements bruts masqués d'un événement
237
        corrélé (événements agrégés dans l'événement corrélé).
238

239
        @param idcorrevent: identifiant de l'événement corrélé souhaité.
240
        @type idcorrevent: C{int}
241
        """
242
        user = get_current_user()
243

    
244
        # Récupère la liste des événements masqués de l'événement
245
        # corrélé donné par idcorrevent.
246
        events = VigiboardRequest(user, False)
247
        events.add_table(
248
            Event,
249
            events.items.c.hostname,
250
            events.items.c.servicename,
251
        )
252
        events.add_join((EVENTSAGGREGATE_TABLE, \
253
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
254
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
255
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
256
        events.add_join((events.items, 
257
            Event.idsupitem == events.items.c.idsupitem))
258
        events.add_filter(Event.idevent != CorrEvent.idcause)
259
        events.add_filter(CorrEvent.idcorrevent == idcorrevent)
260

    
261
        # Récupère l'instance de SupItem associé à la cause de
262
        # l'événement corrélé. Cette instance est utilisé pour
263
        # obtenir le nom d'hôte/service auquel la cause est
264
        # rattachée (afin de fournir un contexte à l'utilisateur).
265
        hostname = None
266
        servicename = None
267
        cause_supitem = DBSession.query(
268
                SupItem,
269
            ).join(
270
                (Event, Event.idsupitem == SupItem.idsupitem),
271
                (EVENTSAGGREGATE_TABLE, EVENTSAGGREGATE_TABLE.c.idevent ==
272
                    Event.idevent),
273
                (CorrEvent, CorrEvent.idcorrevent ==
274
                    EVENTSAGGREGATE_TABLE.c.idcorrevent),
275
            ).filter(CorrEvent.idcorrevent == idcorrevent
276
            ).filter(Event.idevent == CorrEvent.idcause
277
            ).one()
278

    
279
        if isinstance(cause_supitem, LowLevelService):
280
            hostname = cause_supitem.host.name
281
            servicename = cause_supitem.servicename
282
        elif isinstance(cause_supitem, Host):
283
            hostname = cause_supitem.name
284

    
285
        # Vérification que l'événement existe
286
        total_rows = events.num_rows()
287
        if total_rows < 1:
288
            flash(_('No masked event or access denied'), 'error')
289
            redirect('/')
290

    
291
        # Calcul des éléments à afficher et du nombre de pages possibles
292
        items_per_page = int(config['vigiboard_items_per_page'])
293

    
294
        id_first_row = items_per_page * (page-1)
295
        id_last_row = min(id_first_row + items_per_page, total_rows)
296

    
297
        events.format_events(id_first_row, id_last_row)
298
        events.generate_tmpl_context()
299

    
300
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
301
        if not total_rows:
302
            id_first_row = 0
303
        else:
304
            id_first_row += 1
305

    
306
        return dict(
307
            idcorrevent = idcorrevent,
308
            hostname = hostname,
309
            servicename = servicename,
310
            events = events.events,
311
            plugins = get_plugins_instances(),
312
            rows_info = {
313
                'id_first_row': id_first_row,
314
                'id_last_row': id_last_row,
315
                'total_rows': total_rows,
316
            },
317
            nb_pages = nb_pages,
318
            page = page,
319
            search_form = create_search_form,
320
            search = {},
321
            get_calendar_lang = get_calendar_lang,
322
        )
323

    
324

    
325
    class EventSchema(schema.Schema):
326
        """Schéma de validation de la méthode event."""
327
        idevent = validators.Int(not_empty=True)
328
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
329

    
330
    @validate(
331
        validators=EventSchema(),
332
        error_handler = process_form_errors)
333
    @expose('history_table.html')
334
    @require(access_restriction)
335
    def event(self, idevent, page):
336
        """
337
        Affichage de l'historique d'un événement brut.
338
        Pour accéder à cette page, l'utilisateur doit être authentifié.
339

340
        @param idevent: identifiant de l'événement brut souhaité.
341
        @type idevent: C{int}
342
        @param page: numéro de la page à afficher.
343
        @type page: C{int}
344

345
        Cette méthode permet de satisfaire l'exigence
346
        VIGILO_EXIG_VIGILO_BAC_0080.
347
        """
348
        user = get_current_user()
349
        events = VigiboardRequest(user, False)
350
        events.add_table(
351
            Event,
352
            events.items.c.hostname.label('hostname'),
353
            events.items.c.servicename.label('servicename'),
354
        )
355
        events.add_join((EVENTSAGGREGATE_TABLE, \
356
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
357
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
358
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
359
        events.add_join((events.items, 
360
            Event.idsupitem == events.items.c.idsupitem))
361
        events.add_filter(Event.idevent == idevent)
362

    
363
        if events.num_rows() != 1:
364
            flash(_('No such event or access denied'), 'error')
365
            redirect('/')
366

    
367
        events.format_events(0, 1)
368
        events.generate_tmpl_context()
369
        history = events.format_history()
370

    
371
        total_rows = history.count()
372
        items_per_page = int(config['vigiboard_items_per_page'])
373

    
374
        id_first_row = items_per_page * (page-1)
375
        id_last_row = min(id_first_row + items_per_page, total_rows)
376

    
377
        history_entries = history[id_first_row : id_last_row]
378

    
379
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
380
        if not total_rows:
381
            id_first_row = 0
382
        else:
383
            id_first_row += 1
384

    
385
        event = events.req[0]
386

    
387
        return dict(
388
            idevent = idevent,
389
            hostname = event.hostname,
390
            servicename = event.servicename,
391
            plugins = get_plugins_instances(),
392
            rows_info = {
393
                'id_first_row': id_first_row,
394
                'id_last_row': id_last_row,
395
                'total_rows': total_rows,
396
            },
397
            nb_pages = nb_pages,
398
            page = page,
399
            history = history_entries,
400
            search_form = create_search_form,
401
            search = {},
402
            get_calendar_lang = get_calendar_lang,
403
        )
404

    
405

    
406
    class ItemSchema(schema.Schema):
407
        """Schéma de validation de la méthode item."""
408
        page = validators.Int(min=1, if_missing=1, if_invalid=1)
409
        host = validators.String(not_empty=True)
410
        service = validators.String(if_missing=None)
411

    
412
    @validate(
413
        validators=ItemSchema(),
414
        error_handler = process_form_errors)
415
    @expose('events_table.html')
416
    @require(access_restriction)
417
    def item(self, page, host, service):
418
        """
419
        Affichage de l'historique de l'ensemble des événements corrélés
420
        jamais ouverts sur l'hôte / service demandé.
421
        Pour accéder à cette page, l'utilisateur doit être authentifié.
422

423
        @param page: Numéro de la page à afficher.
424
        @param host: Nom de l'hôte souhaité.
425
        @param service: Nom du service souhaité
426

427
        Cette méthode permet de satisfaire l'exigence
428
        VIGILO_EXIG_VIGILO_BAC_0080.
429
        """
430
        idsupitem = SupItem.get_supitem(host, service)
431
        if not idsupitem:
432
            flash(_('No such host/service'), 'error')
433
            redirect('/')
434

    
435
        user = get_current_user()
436
        aggregates = VigiboardRequest(user, False)
437
        aggregates.add_table(
438
            CorrEvent,
439
            aggregates.items.c.hostname,
440
            aggregates.items.c.servicename,
441
        )
442
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
443
        aggregates.add_join((aggregates.items, 
444
            Event.idsupitem == aggregates.items.c.idsupitem))
445
        aggregates.add_filter(aggregates.items.c.idsupitem == idsupitem)
446

    
447
        # Vérification qu'il y a au moins 1 événement qui correspond
448
        total_rows = aggregates.num_rows()
449
        if not total_rows:
450
            flash(_('No access to this host/service or no event yet'), 'error')
451
            redirect('/')
452

    
453
        items_per_page = int(config['vigiboard_items_per_page'])
454

    
455
        id_first_row = items_per_page * (page-1)
456
        id_last_row = min(id_first_row + items_per_page, total_rows)
457

    
458
        aggregates.format_events(id_first_row, id_last_row)
459
        aggregates.generate_tmpl_context()
460

    
461
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
462
        if not total_rows:
463
            id_first_row = 0
464
        else:
465
            id_first_row += 1
466
        
467
        return dict(
468
            hostname = host,
469
            servicename = service,
470
            events = aggregates.events,
471
            plugins = get_plugins_instances(),
472
            rows_info = {
473
                'id_first_row': id_first_row,
474
                'id_last_row': id_last_row,
475
                'total_rows': total_rows,
476
            },
477
            nb_pages = nb_pages,
478
            page = page,
479
            event_edit_status_options = edit_event_status_options,
480
            search_form = create_search_form,
481
            search = {},
482
            get_calendar_lang = get_calendar_lang,
483
        )
484

    
485

    
486
    class UpdateSchema(schema.Schema):
487
        """Schéma de validation de la méthode update."""
488
        id = validators.Regex(r'^[0-9]+(,[0-9]+)*,?$')
489
        last_modification = validators.Number(not_empty=True)
490
        trouble_ticket = validators.String(if_missing='')
491
        ack = validators.OneOf(
492
            [unicode(s[0]) for s in edit_event_status_options],
493
            not_empty=True)
494

    
495
    @validate(
496
        validators=UpdateSchema(),
497
        error_handler = process_form_errors)
498
    @require(
499
        All(
500
            not_anonymous(msg=l_("You need to be authenticated")),
501
            Any(in_group('managers'),
502
                has_permission('vigiboard-update'),
503
                msg=l_("You don't have write access to VigiBoard"))
504
        ))
505
    @expose()
506
    def update(self, id, last_modification, trouble_ticket, ack):
507
        """
508
        Mise à jour d'un événement suivant les arguments passés.
509
        Cela peut être un changement de ticket ou un changement de statut.
510
        
511
        @param id: Le ou les identifiants des événements à traiter
512
        @param last_modification: La date de la dernière modification
513
            dont l'utilisateur est au courant.
514
        @param trouble_ticket: Nouveau numéro du ticket associé.
515
        @param ack: Nouvel état d'acquittement des événements sélectionnés.
516

517
        Cette méthode permet de satisfaire les exigences suivantes : 
518
            - VIGILO_EXIG_VIGILO_BAC_0020,
519
            - VIGILO_EXIG_VIGILO_BAC_0060,
520
            - VIGILO_EXIG_VIGILO_BAC_0110.
521
        """
522

    
523
        # On vérifie que des identifiants ont bien été transmis via
524
        # le formulaire, et on informe l'utilisateur le cas échéant.
525
        if id is None:
526
            flash(_('No event has been selected'), 'warning')
527
            raise redirect(request.environ.get('HTTP_REFERER', '/'))
528

    
529
        # On récupère la liste de tous les identifiants des événements
530
        # à mettre à jour.
531
        ids = map(int, id.strip(',').split(','))
532

    
533
        user = get_current_user()
534
        events = VigiboardRequest(user)
535
        events.add_table(CorrEvent)
536
        events.add_join((Event, CorrEvent.idcause == Event.idevent))
537
        events.add_join((events.items, 
538
            Event.idsupitem == events.items.c.idsupitem))
539
        events.add_filter(CorrEvent.idcorrevent.in_(ids))
540
        
541
        events.generate_request()
542
        idevents = [cause.idcause for cause in events.req]
543

    
544
        # Si des changements sont survenus depuis que la 
545
        # page est affichée, on en informe l'utilisateur.
546
        last_modification = datetime.fromtimestamp(last_modification)
547
        cur_last_modification = get_last_modification_timestamp(idevents, None)
548
        if cur_last_modification and last_modification < cur_last_modification:
549
            flash(_('Changes have occurred since the page was last displayed, '
550
                    'your changes HAVE NOT been saved.'), 'warning')
551
            raise redirect(request.environ.get('HTTP_REFERER', '/'))
552

    
553
        # Vérification que au moins un des identifiants existe et est éditable
554
        if not events.num_rows():
555
            flash(_('No access to this event'), 'error')
556
            redirect('/')
557

    
558
        if ack == u'Forced':
559
            condition = Any(
560
                in_group('managers'),
561
                has_permission('vigiboard-admin'),
562
                msg=l_("You don't have administrative access "
563
                        "to VigiBoard"))
564
            try:
565
                condition.check_authorization(request.environ)
566
            except NotAuthorizedError, e:
567
                reason = unicode(e)
568
                flash(reason, 'error')
569
                raise redirect(request.environ.get('HTTP_REFERER', '/'))
570

    
571
        # Modification des événements et création d'un historique
572
        # chaque fois que cela est nécessaire.
573
        for event in events.req:
574
            if trouble_ticket and trouble_ticket != event.trouble_ticket:
575
                history = EventHistory(
576
                        type_action="Ticket change",
577
                        idevent=event.idcause,
578
                        value=unicode(trouble_ticket),
579
                        text="Changed trouble ticket from '%(from)s' "
580
                             "to '%(to)s'" % {
581
                            'from': event.trouble_ticket,
582
                            'to': trouble_ticket,
583
                        },
584
                        username=user.user_name,
585
                        timestamp=datetime.now(),
586
                    )
587
                DBSession.add(history)   
588
                event.trouble_ticket = trouble_ticket
589

    
590
            # Changement du statut d'acquittement.
591
            if ack != u'NoChange':
592
                changed_ack = ack
593
                # Pour forcer l'acquittement d'un événement,
594
                # il faut en plus avoir la permission
595
                # "vigiboard-admin".
596
                if ack == u'Forced':
597
                    changed_ack = u'AAClosed'
598
                    # On met systématiquement l'état à "OK", même s'il
599
                    # s'agit d'un hôte. Techniquement, c'est incorrect,
600
                    # mais comme on fait ça pour masquer l'événement...
601
                    event.cause.current_state = \
602
                        StateName.statename_to_value(u'OK')
603

    
604
                    history = EventHistory(
605
                            type_action="Forced change state",
606
                            idevent=event.idcause,
607
                            value=u'OK',
608
                            text="Forced state to 'OK'",
609
                            username=user.user_name,
610
                            timestamp=datetime.now(),
611
                        )
612
                    DBSession.add(history)
613

    
614
                history = EventHistory(
615
                        type_action="Acknowledgement change state",
616
                        idevent=event.idcause,
617
                        value=ack,
618
                        text="Changed acknowledgement status "
619
                            "from '%s' to '%s'" % (
620
                            event.status, changed_ack
621
                        ),
622
                        username=user.user_name,
623
                        timestamp=datetime.now(),
624
                    )
625
                DBSession.add(history)
626
                event.status = changed_ack
627

    
628
        DBSession.flush()
629
        flash(_('Updated successfully'))
630
        redirect(request.environ.get('HTTP_REFERER', '/'))
631

    
632

    
633
    class GetPluginValueSchema(schema.Schema):
634
        """Schéma de validation de la méthode get_plugin_value."""
635
        idcorrevent = validators.Int(not_empty=True)
636
        plugin_name = validators.OneOf(
637
            [unicode(i[0]) for i in config.get('vigiboard_plugins', [])],
638
            not_empty=True, hideList=True)
639
        # Permet de passer des paramètres supplémentaires au plugin.
640
        allow_extra_fields = True
641

    
642
    @validate(
643
        validators=GetPluginValueSchema(),
644
        error_handler = handle_validation_errors_json)
645
    @expose('json')
646
    @require(access_restriction)
647
    def get_plugin_value(self, idcorrevent, plugin_name, *arg, **krgv):
648
        """
649
        Permet de récupérer la valeur d'un plugin associée à un CorrEvent
650
        donné via JSON.
651
        """
652
        plugins = config.get('vigiboard_plugins', {})
653
        # Permet de vérifier si l'utilisateur a bien les permissions
654
        # pour accéder à cet événement et si l'événement existe.
655
        user = get_current_user()
656
        events = VigiboardRequest(user, False)
657
        events.add_table(CorrEvent.idcorrevent)
658
        events.add_join((Event, CorrEvent.idcause == Event.idevent))
659
        events.add_join((events.items, 
660
            Event.idsupitem == events.items.c.idsupitem))
661
        events.add_filter(CorrEvent.idcorrevent == idcorrevent)
662

    
663
        # Pas d'événement ou permission refusée. On ne distingue pas
664
        # les 2 cas afin d'éviter la divulgation d'informations.
665
        if not events.num_rows():
666
            raise HTTPNotFound(_('No such incident or insufficient '
667
                                'permissions'))
668

    
669
        plugin_class = [p[1] for p in plugins if p[0] == plugin_name]
670
        if not plugin_class:
671
            raise HTTPNotFound(_('No such plugin'))
672

    
673
        plugin_class = plugin_class[0]
674
        try:
675
            mypac = __import__(
676
                'vigiboard.controllers.plugins.' + plugin_name,
677
                globals(), locals(), [plugin_class], -1)
678
            plugin = getattr(mypac, plugin_class)
679
            if callable(plugin):
680
                return plugin().get_value(idcorrevent, *arg, **krgv)
681
            raise HTTPInternalServerError(_('Not a valid plugin'))
682
        except ImportError:
683
            raise HTTPInternalServerError(_('Plugin could not be loaded'))
684

    
685
    @validate(validators={
686
        "fontsize": validators.Regex(
687
            r'[0-9]+(pt|px|em|%)',
688
            regexOps = ('I',)
689
        )}, error_handler = handle_validation_errors_json)
690
    @expose('json')
691
    def set_fontsize(self, fontsize):
692
        """Enregistre la taille de la police dans les préférences."""
693
        session['fontsize'] = fontsize
694
        session.save()
695
        return dict()
696

    
697
    @validate(validators={"refresh": validators.Int()},
698
            error_handler = handle_validation_errors_json)
699
    @expose('json')
700
    def set_refresh(self, refresh):
701
        """Enregistre le temps de rafraichissement dans les préférences."""
702
        session['refresh'] = refresh
703
        session.save()
704
        return dict()
705

    
706
    @expose('json')
707
    def set_theme(self, theme):
708
        """Enregistre le thème à utiliser dans les préférences."""
709
        # On sauvegarde l'ID du thème sans vérifications
710
        # car les thèmes (styles CSS) sont définies dans
711
        # les packages de thèmes (ex: vigilo-themes-default).
712
        # La vérification de la valeur est faite dans les templates.
713
        session['theme'] = theme
714
        session.save()
715
        return dict()
716

    
717
    @require(access_restriction)
718
    @expose('json')
719
    def get_groups(self):
720
        hierarchy = []
721
        user = get_current_user()
722
        groups = DBSession.query(
723
                    SupItemGroup.idgroup,
724
                    SupItemGroup.name,
725
                    GroupHierarchy.idparent,
726
                ).join(
727
                    (GroupHierarchy, GroupHierarchy.idchild == \
728
                        SupItemGroup.idgroup),
729
                ).filter(GroupHierarchy.hops <= 1
730
                ).order_by(GroupHierarchy.hops.asc()
731
                ).order_by(SupItemGroup.name.asc())
732

    
733
        is_manager = in_group('managers').is_met(request.environ)
734
        if not is_manager:
735
            user_groups = [ug[0] for ug in user.supitemgroups() if ug[1]]
736
            groups = groups.filter(SupItemGroup.idgroup.in_(user_groups))
737

    
738
        def find_parent(idgroup):
739
            def __find_parent(hier, idgroup):
740
                for g in hier:
741
                    if g['idgroup'] == idgroup:
742
                        return g['children']
743
                for g in hier:
744
                    res = __find_parent(g['children'], idgroup)
745
                    if res:
746
                        return res
747
                return None
748
            parent = __find_parent(hierarchy, idgroup)
749
            if parent is None:
750
                return hierarchy
751
            return parent
752

    
753
        for g in groups.all():
754
            parent = find_parent(g.idparent)
755
            for item in hierarchy:
756
                if item['idgroup'] == g.idgroup:
757
                    parent.append(item)
758
                    hierarchy.remove(item)
759
                    break
760
            else:
761
                parent.append({
762
                    'idgroup': g.idgroup,
763
                    'name': g.name,
764
                    'children': [],
765
                })
766

    
767
        return dict(groups=hierarchy)
768

    
769
def get_last_modification_timestamp(event_id_list, 
770
                                    value_if_none=datetime.now()):
771
    """
772
    Récupère le timestamp de la dernière modification 
773
    opérée sur l'un des événements dont l'identifiant
774
    fait partie de la liste passée en paramètre.
775
    """
776
    last_modification_timestamp = DBSession.query(
777
                                func.max(EventHistory.timestamp),
778
                         ).filter(EventHistory.idevent.in_(event_id_list)
779
                         ).scalar()
780
    if not last_modification_timestamp:
781
        if not value_if_none:
782
            return None
783
        else:
784
            last_modification_timestamp = value_if_none
785
    return datetime.fromtimestamp(mktime(
786
        last_modification_timestamp.timetuple()))
787

    
788
def get_plugins_instances():
789
    """
790
    Renvoie une liste d'instances de plugins pour VigiBoard.
791

792
    @return: Liste de tuples contenant le nom du plugin
793
        et l'instance associée.
794
    @rtype: C{list} of C{tuple}
795
    """
796
    plugins = config.get('vigiboard_plugins', [])
797
    plugins_instances = []
798
    for (plugin_name, plugin_class) in plugins:
799
        try:
800
            mypac = __import__(
801
                'vigiboard.controllers.plugins.' + plugin_name,
802
                globals(), locals(), [plugin_class], -1)
803
            plugin = getattr(mypac, plugin_class)
804
            if callable(plugin):
805
                plugins_instances.append((plugin_name, plugin()))
806
        except ImportError:
807
            pass
808
    return plugins_instances
809