Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

vigiboard / vigiboard / controllers / root.py @ 7c30dabe

History | View | Annotate | Download (25.1 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
import urllib
9

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

    
21
from vigilo.models.configure import DBSession
22
from vigilo.models import Event, EventHistory, CorrEvent, SupItem, \
23
                            HostGroup, ServiceGroup, StateName, User
24
from vigilo.models.functions import sql_escape_like
25
from vigilo.models.secondary_tables import EVENTSAGGREGATE_TABLE
26

    
27
from vigilo.turbogears.controllers.autocomplete \
28
    import make_autocomplete_controller
29
from vigiboard.controllers.vigiboardrequest import VigiboardRequest
30
from vigiboard.controllers.vigiboard_controller import VigiboardRootController
31
from vigiboard.widgets.edit_event import edit_event_status_options
32
from vigiboard.lib.base import BaseController
33

    
34
__all__ = ('RootController', 'get_last_modification_timestamp', 
35
           'date_to_timestamp')
36

    
37
# pylint: disable-msg=R0201
38
class RootController(VigiboardRootController):
39
    """
40
    Le controller général de vigiboard
41
    """
42
    autocomplete = make_autocomplete_controller(BaseController)
43

    
44
    def process_form_errors(self, *argv, **kwargv):
45
        """
46
        Gestion des erreurs de validation : On affiche les erreurs
47
        puis on redirige vers la dernière page accédée.
48
        """
49
        for k in tmpl_context.form_errors:
50
            flash("'%s': %s" % (k, tmpl_context.form_errors[k]), 'error')
51
        if request.environ.get('HTTP_REFERER') :
52
            redirect(request.environ.get('HTTP_REFERER'
53
                ).split(request.environ.get('HTTP_HOST'))[1])
54
        else :
55
            redirect('/')
56

    
57
    @validate(validators={
58
            'page': validators.Int(min=1),
59
        }, error_handler=process_form_errors)
60
    @expose('events_table.html')
61
    @require(Any(not_anonymous(), msg=l_("You need to be authenticated")))
62
    def default(self, page=1, hostgroup=None, servicegroup=None,
63
            host=None, service=None, output=None, trouble_ticket=None,
64
            from_date=None, to_date=None, *argv, **krgv):
65
        """
66
        Page d'accueil de Vigiboard. Elle affiche, suivant la page demandée
67
        (page 1 par defaut), la liste des événements, rangés par ordre de prise
68
        en compte, puis de sévérité.
69
        Pour accéder à cette page, l'utilisateur doit être authentifié.
70

71
        @param page: Numéro de la page souhaitée, commence à 1
72
        @param host: Si l'utilisateur souhaite sélectionner seulement certains
73
                     événements suivant leur hôte, il peut placer une expression
74
                     ici en suivant la structure du LIKE en SQL
75
        @param service: Idem que host mais sur les services
76
        @param output: Idem que host mais sur le text explicatif
77
        @param trouble_ticket: Idem que host mais sur les tickets attribués
78
        """
79
        if not page:
80
            page = 1
81

    
82
        username = request.environ['repoze.who.identity']['repoze.who.userid']
83
        user = User.by_user_name(username)
84

    
85
        aggregates = VigiboardRequest(user)
86
        aggregates.add_table(
87
            CorrEvent,
88
            aggregates.items.c.hostname,
89
            aggregates.items.c.servicename
90
        )
91
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
92
        aggregates.add_join((aggregates.items, 
93
            Event.idsupitem == aggregates.items.c.idsupitem))
94
        aggregates.add_order_by(asc(aggregates.items.c.hostname))
95
        
96
        search = {
97
            'host': '',
98
            'service': '',
99
            'output': '',
100
            'tt': '',
101
            'from_date': '',
102
            'to_date': '',
103
            'hostgroup': '',
104
            'servicegroup': '',
105
        }
106

    
107
        # Application des filtres si nécessaire
108
        if hostgroup:
109
            search['hostgroup'] = hostgroup
110
            hostgroup = sql_escape_like(hostgroup)
111
            aggregates.add_join((HostGroup, HostGroup.idgroup == \
112
                aggregates.items.c.idhostgroup))
113
            aggregates.add_filter(HostGroup.name.ilike('%%%s%%' % hostgroup))
114

    
115
        if servicegroup:
116
            search['servicegroup'] = servicegroup
117
            servicegroup = sql_escape_like(servicegroup)
118
            aggregates.add_join((ServiceGroup, ServiceGroup.idgroup == \
119
                aggregates.items.c.idservicegroup))
120
            aggregates.add_filter(
121
                ServiceGroup.name.ilike('%%%s%%' % servicegroup))
122

    
123
        if host:
124
            search['host'] = host
125
            host = sql_escape_like(host)
126
            aggregates.add_filter(aggregates.items.c.hostname.ilike(
127
                '%%%s%%' % host))
128

    
129
        if service:
130
            search['service'] = service
131
            service = sql_escape_like(service)
132
            aggregates.add_filter(aggregates.items.c.servicename.ilike(
133
                '%%%s%%' % service))
134

    
135
        if output:
136
            search['output'] = output
137
            output = sql_escape_like(output)
138
            aggregates.add_filter(Event.message.ilike('%%%s%%' % output))
139

    
140
        if trouble_ticket:
141
            search['tt'] = trouble_ticket
142
            trouble_ticket = sql_escape_like(trouble_ticket)
143
            aggregates.add_filter(CorrEvent.trouble_ticket.ilike(
144
                '%%%s%%' % trouble_ticket))
145

    
146
        if from_date:
147
            search['from_date'] = from_date
148
            try:
149
                # TRANSLATORS: Format de date et heure.
150
                from_date = datetime.strptime(
151
                    from_date, _('%Y-%m-%d %I:%M:%S %p'))
152
            except ValueError:
153
                # On ignore silencieusement la date invalide reçue.
154
                pass
155
            else:
156
                aggregates.add_filter(CorrEvent.timestamp_active >= from_date)
157

    
158
        if to_date:
159
            search['to_date'] = to_date
160
            try:
161
                # TRANSLATORS: Format de date et heure.
162
                to_date = datetime.strptime(
163
                    to_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 <= to_date)
169

    
170
        # Calcul des éléments à afficher et du nombre de pages possibles
171
        total_rows = aggregates.num_rows()
172
        items_per_page = int(config['vigiboard_items_per_page'])
173

    
174
        # Si le numéro de page dépasse le nombre de pages existantes,
175
        # on redirige automatiquement vers la 1ère page.
176
        if total_rows and items_per_page * (page-1) > total_rows:
177
            redirect('/', page=1, **search)
178

    
179
        id_first_row = items_per_page * (page-1)
180
        id_last_row = min(id_first_row + items_per_page, total_rows)
181

    
182
        aggregates.format_events(id_first_row, id_last_row)
183
        aggregates.generate_tmpl_context()
184

    
185
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
186
        if not total_rows:
187
            id_first_row = 0
188
        else:
189
            id_first_row += 1
190

    
191
        return dict(
192
            hostname = None,
193
            servicename = None,
194
            events = aggregates.events,
195
            plugins = get_plugins_instances(),
196
            rows_info = {
197
                'id_first_row': id_first_row,
198
                'id_last_row': id_last_row,
199
                'total_rows': total_rows,
200
            },
201
            nb_pages = nb_pages,
202
            page = page,
203
            event_edit_status_options = edit_event_status_options,
204
            search = search,
205
            refresh_times = config['vigiboard_refresh_times'],
206
        )
207

    
208
    @validate(validators={
209
            'idcorrevent': validators.Int(not_empty=True),
210
            'page': validators.Int(min=1),
211
        }, error_handler=process_form_errors)
212
    @expose('raw_events_table.html')
213
    @require(Any(not_anonymous(), msg=l_("You need to be authenticated")))
214
    def masked_events(self, idcorrevent, page=1):
215
        """
216
        Affichage de la liste des événements bruts masqués dans un
217
        événement corrélé (agrégés).
218

219
        @param idevent: identifiant de l'événement souhaité
220
        """
221
        if not page:
222
            page = 1
223

    
224
        username = request.environ['repoze.who.identity']['repoze.who.userid']
225
        events = VigiboardRequest(User.by_user_name(username), False)
226
        events.add_table(
227
            Event,
228
            events.items.c.hostname,
229
            events.items.c.servicename,
230
        )
231
        events.add_join((EVENTSAGGREGATE_TABLE, \
232
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
233
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
234
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
235
        events.add_join((events.items, 
236
            Event.idsupitem == events.items.c.idsupitem))
237
        events.add_filter(Event.idevent != CorrEvent.idcause)
238
        events.add_filter(CorrEvent.idcorrevent == idcorrevent)
239

    
240
        # Vérification que l'événement existe
241
        total_rows = events.num_rows()
242
        if total_rows < 1:
243
            flash(_('No masked event or access denied'), 'error')
244
            redirect('/')
245

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

    
249
        id_first_row = items_per_page * (page-1)
250
        id_last_row = min(id_first_row + items_per_page, total_rows)
251

    
252
        events.format_events(id_first_row, id_last_row)
253
        events.generate_tmpl_context()
254

    
255
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
256
        if not total_rows:
257
            id_first_row = 0
258
        else:
259
            id_first_row += 1
260

    
261
        return dict(
262
            idcorrevent = idcorrevent,
263
            hostname = None,
264
            servicename = None,
265
            events = events.events,
266
            plugins = get_plugins_instances(),
267
            rows_info = {
268
                'id_first_row': id_first_row,
269
                'id_last_row': id_last_row,
270
                'total_rows': total_rows,
271
            },
272
            nb_pages = nb_pages,
273
            page = page,
274
            search = {
275
                'host': '',
276
                'service': '',
277
                'output': '',
278
                'tt': '',
279
                'from_date': '',
280
                'to_date': '',
281
                'hostgroup': '',
282
                'servicegroup': '',
283
            },
284
           refresh_times=config['vigiboard_refresh_times'],
285
        )
286

    
287
    @validate(validators={
288
            'idevent': validators.Int(not_empty=True),
289
            'page': validators.Int(min=1),
290
        }, error_handler=process_form_errors)
291
    @expose('history_table.html')
292
    @require(Any(not_anonymous(), msg=l_("You need to be authenticated")))
293
    def event(self, idevent, page=1):
294
        """
295
        Affichage de l'historique d'un événement brut.
296
        Pour accéder à cette page, l'utilisateur doit être authentifié.
297

298
        @param idevent: identifiant de l'événement brut souhaité.
299
        @type idevent: C{int}
300
        """
301
        if not page:
302
            page = 1
303

    
304
        username = request.environ['repoze.who.identity']['repoze.who.userid']
305
        events = VigiboardRequest(User.by_user_name(username), False)
306
        events.add_table(
307
            Event,
308
            events.items.c.hostname,
309
            events.items.c.servicename,
310
        )
311
        events.add_join((EVENTSAGGREGATE_TABLE, \
312
            EVENTSAGGREGATE_TABLE.c.idevent == Event.idevent))
313
        events.add_join((CorrEvent, CorrEvent.idcorrevent == \
314
            EVENTSAGGREGATE_TABLE.c.idcorrevent))
315
        events.add_join((events.items, 
316
            Event.idsupitem == events.items.c.idsupitem))
317
        events.add_filter(Event.idevent == idevent)
318

    
319
        if events.num_rows() != 1:
320
            flash(_('No such event or access denied'), 'error')
321
            redirect('/')
322

    
323
        events.format_events(0, 1)
324
        events.generate_tmpl_context()
325
        history = events.format_history()
326

    
327
        total_rows = history.count()
328
        items_per_page = int(config['vigiboard_items_per_page'])
329

    
330
        id_first_row = items_per_page * (page-1)
331
        id_last_row = min(id_first_row + items_per_page, total_rows)
332

    
333
        history_entries = history[id_first_row : id_last_row]
334

    
335
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
336
        if not total_rows:
337
            id_first_row = 0
338
        else:
339
            id_first_row += 1
340

    
341
        return dict(
342
            idevent = idevent,
343
            plugins = get_plugins_instances(),
344
            rows_info = {
345
                'id_first_row': id_first_row,
346
                'id_last_row': id_last_row,
347
                'total_rows': total_rows,
348
            },
349
            nb_pages = nb_pages,
350
            page = page,
351
            history = history_entries,
352
            search = {
353
                'host': '',
354
                'service': '',
355
                'output': '',
356
                'tt': '',
357
                'from_date': '',
358
                'to_date': '',
359
                'hostgroup': '',
360
                'servicegroup': '',
361
            },
362
           refresh_times=config['vigiboard_refresh_times'],
363
        )
364

    
365
    @validate(
366
        validators={
367
            'host': validators.NotEmpty(),
368
#            'service': validators.NotEmpty(),
369
            'page': validators.Int(min=1),
370
        }, 
371
        error_handler = process_form_errors)
372
    @expose('events_table.html')
373
    @require(Any(not_anonymous(), msg=l_("You need to be authenticated")))
374
    def item(self, page, host, service=None):
375
        """
376
        Affichage de l'historique de l'ensemble des événements corrélés
377
        jamais ouverts sur l'hôte / service demandé.
378
        Pour accéder à cette page, l'utilisateur doit être authentifié.
379

380
        @param host: Nom de l'hôte souhaité.
381
        @param service: Nom du service souhaité
382
        """
383
        idsupitem = SupItem.get_supitem(host, service)
384

    
385
        username = request.environ['repoze.who.identity']['repoze.who.userid']
386
        aggregates = VigiboardRequest(User.by_user_name(username), False)
387
        aggregates.add_table(
388
            CorrEvent,
389
            aggregates.items.c.hostname,
390
            aggregates.items.c.servicename,
391
        )
392
        aggregates.add_join((Event, CorrEvent.idcause == Event.idevent))
393
        aggregates.add_join((aggregates.items, 
394
            Event.idsupitem == aggregates.items.c.idsupitem))
395
        aggregates.add_filter(aggregates.items.c.idsupitem == idsupitem)
396

    
397
        # Vérification qu'il y a au moins 1 événement qui correspond
398
        total_rows = aggregates.num_rows()
399
        if not total_rows:
400
            flash(_('No access to this host/service or no event yet'), 'error')
401
            redirect('/')
402

    
403
        items_per_page = int(config['vigiboard_items_per_page'])
404

    
405
        id_first_row = items_per_page * (page-1)
406
        id_last_row = min(id_first_row + items_per_page, total_rows)
407

    
408
        aggregates.format_events(id_first_row, id_last_row)
409
        aggregates.generate_tmpl_context()
410

    
411
        nb_pages = int(math.ceil(total_rows / (items_per_page + 0.0)))
412
        if not total_rows:
413
            id_first_row = 0
414
        else:
415
            id_first_row += 1
416
        
417
        return dict(
418
            hostname = host,
419
            servicename = service,
420
            events = aggregates.events,
421
            plugins = get_plugins_instances(),
422
            rows_info = {
423
                'id_first_row': id_first_row,
424
                'id_last_row': id_last_row,
425
                'total_rows': total_rows,
426
            },
427
            nb_pages = nb_pages,
428
            page = page,
429
            event_edit_status_options = edit_event_status_options,
430
            search = {
431
                'host': '',
432
                'service': '',
433
                'output': '',
434
                'tt': '',
435
                'from_date': '',
436
                'to_date': '',
437
                'hostgroup': '',
438
                'servicegroup': '',
439
            },
440
            refresh_times=config['vigiboard_refresh_times'],
441
        )
442

    
443
    @validate(validators={
444
        "id": validators.Regex(r'^[0-9]+(,[0-9]+)*,?$'),
445
#        "trouble_ticket": validators.Regex(r'^[0-9]*$'),
446
        "ack": validators.OneOf([
447
            u'NoChange',
448
            u'None',
449
            u'Acknowledged',
450
            u'AAClosed'
451
        ])}, error_handler=process_form_errors)
452
    @require(Any(not_anonymous(), msg=l_("You need to be authenticated")))
453
    def update(self, **krgv):
454
        """
455
        Mise à jour d'un événement suivant les arguments passés.
456
        Cela peut être un changement de ticket ou un changement de statut.
457
        
458
        @param krgv['id']: Le ou les identifiants des événements à traiter
459
        @param krgv['last_modification']: La date de la dernière modification
460
        dont l'utilisateur est au courant.
461
        @param krgv['tt']: Nouveau numéro du ticket associé.
462
        @param krgv['status']: Nouveau status de/des événements.
463
        """
464

    
465
        # On vérifie que des identifiants ont bien été transmis via
466
        # le formulaire, et on informe l'utilisateur le cas échéant.
467
        if krgv['id'] is None:
468
            flash(_('No event has been selected'), 'warning')
469
            raise redirect(request.environ.get('HTTP_REFERER', url('/')))
470

    
471
        # Le filtre permet d'éliminer les chaines vides contenues dans le
472
        # tableau ('a,b,' -> split -> ['a','b',''] -> filter -> ['a','b']).
473
        ids = map(int, filter(len, krgv['id'].split(',')))
474

    
475
        # Si l'utilisateur édite plusieurs événements à la fois,
476
        # il nous faut chacun des identifiants
477
       
478
        username = request.environ['repoze.who.identity']['repoze.who.userid']
479
        events = VigiboardRequest(User.by_user_name(username))
480
        events.add_table(CorrEvent)
481
        events.add_join((Event, CorrEvent.idcause == Event.idevent))
482
        events.add_join((events.items, 
483
            Event.idsupitem == events.items.c.idsupitem))
484
        events.add_filter(CorrEvent.idcorrevent.in_(ids))
485
        
486
        events.generate_request()
487
        idevents = [cause.idcause for cause in events.req]
488
        # Si des changements sont survenus depuis que la 
489
        # page est affichée, on en informe l'utilisateur.
490
        last_modification = get_last_modification_timestamp(idevents, None)
491
        if last_modification and datetime.fromtimestamp(\
492
            float(krgv['last_modification'])) < last_modification:
493
            flash(_('Changes have occurred since the page was last displayed, '
494
                    'your changes HAVE NOT been saved.'), 'warning')
495
            raise redirect(request.environ.get('HTTP_REFERER', url('/')))
496
        
497
        # Vérification que au moins un des identifiants existe et est éditable
498
        if not events.num_rows():
499
            flash(_('No access to this event'), 'error')
500
            redirect('/')
501
        
502
        # Modification des événements et création d'un historique
503
        # pour chacun d'eux.
504
        for req in events.req:
505
            if isinstance(req, CorrEvent):
506
                event = req
507
            else:
508
                event = req[0]
509

    
510
            if krgv['trouble_ticket'] != '' :
511
                history = EventHistory(
512
                        type_action="Ticket change",
513
                        idevent=event.idcause,
514
                        value=krgv['trouble_ticket'],
515
                        text="Changed trouble ticket from '%s' to '%s'" % (
516
                            event.trouble_ticket, krgv['trouble_ticket']
517
                        ),
518
                        username=username,
519
                        timestamp=datetime.now(),
520
                    )
521
                DBSession.add(history)   
522
                event.trouble_ticket = krgv['trouble_ticket']
523

    
524
            if krgv['ack'] != 'NoChange' :
525
                history = EventHistory(
526
                        type_action="Acknowledgement change state",
527
                        idevent=event.idcause,
528
                        value=krgv['ack'],
529
                        text="Changed acknowledgement status "
530
                            "from '%s' to '%s'" % (
531
                            event.status, krgv['ack']
532
                        ),
533
                        username=username,
534
                        timestamp=datetime.now(),
535
                    )
536
                DBSession.add(history)
537
                event.status = krgv['ack']
538

    
539
        DBSession.flush()
540
        flash(_('Updated successfully'))
541
        redirect(request.environ.get('HTTP_REFERER', url('/')))
542

    
543
    @validate(validators={
544
        "plugin_name": validators.OneOf([i[0] for i \
545
            in config.get('vigiboard_plugins', [])]),
546
        'idcorrevent': validators.Int(not_empty=True),
547
        }, error_handler=process_form_errors)
548
    @expose('json')
549
    @require(Any(not_anonymous(), msg=l_("You need to be authenticated")))
550
    def get_plugin_value(self, idcorrevent, plugin_name, *arg, **krgv):
551
        """
552
        Permet de récupérer la valeur d'un plugin associée à un CorrEvent
553
        donné via JSON.
554
        """
555
        plugins = config.get('vigiboard_plugins', {})
556

    
557
        # Permet de vérifier si l'utilisateur a bien les permissions
558
        # pour accéder à cet événement et si l'événement existe.
559
        username = request.environ['repoze.who.identity']['repoze.who.userid']
560
        events = VigiboardRequest(User.by_user_name(username), False)
561
        events.add_table(CorrEvent.idcorrevent)
562
        events.add_join((Event, CorrEvent.idcause == Event.idevent))
563
        events.add_join((events.items, 
564
            Event.idsupitem == events.items.c.idsupitem))
565
        events.add_filter(CorrEvent.idcorrevent == idcorrevent)
566

    
567
        # Pas d'événement ou permission refusée. On ne distingue pas
568
        # les 2 cas afin d'éviter la divulgation d'informations.
569
        if not events.num_rows():
570
            raise HTTPNotFound(_('No such incident or insufficient permissions'))
571

    
572
        plugin_class = [p[1] for p in plugins if p[0] == plugin_name]
573
        if not plugin_class:
574
            raise HTTPNotFound(_('No such plugin'))
575

    
576
        plugin_class = plugin_class[0]
577
        try:
578
            mypac = __import__(
579
                'vigiboard.controllers.plugins.' + plugin_name,
580
                globals(), locals(), [plugin_class], -1)
581
            plugin = getattr(mypac, plugin_class)
582
            if callable(plugin):
583
                return plugin().get_value(idcorrevent, *arg, **krgv)
584
            raise HTTPInternalServerError(_('Not a valid plugin'))
585
        except ImportError:
586
            raise HTTPInternalServerError(_('Plugin could not be loaded'))
587

    
588
    @validate(validators={
589
        "fontsize": validators.Regex(
590
            r'[0-9]+(pt|px|em|%)',
591
            regexOps = ('I',)
592
        )}, error_handler = process_form_errors)
593
    @expose('json')
594
    def set_fontsize(self, fontsize):
595
        """Enregistre la taille de la police dans les préférences."""
596
        session['fontsize'] = fontsize
597
        session.save()
598
        return dict()
599

    
600
    @validate(validators={"refresh": validators.Int()},
601
            error_handler=process_form_errors)
602
    @expose('json')
603
    def set_refresh(self, refresh):
604
        """Enregistre le temps de rafraichissement dans les préférences."""
605
        session['refresh'] = refresh
606
        session.save()
607
        return dict()
608

    
609
    @expose('json')
610
    def set_theme(self, theme):
611
        """Enregistre le thème à utiliser dans les préférences."""
612
        # On sauvegarde l'ID du thème sans vérifications
613
        # car les thèmes (styles CSS) sont définies dans
614
        # les packages de thèmes (ex: vigilo-themes-default).
615
        # La vérification de la valeur est faite dans les templates.
616
        session['theme'] = theme
617
        session.save()
618
        return dict()
619
    
620
def get_last_modification_timestamp(event_id_list, 
621
                                    value_if_none=datetime.now()):
622
    """
623
    Récupère le timestamp de la dernière modification 
624
    opérée sur l'un des événements dont l'identifiant
625
    fait partie de la liste passée en paramètre.
626
    """
627
    last_modification_timestamp = DBSession.query(
628
                                func.max(EventHistory.timestamp),
629
                         ).filter(EventHistory.idevent.in_(event_id_list)
630
                         ).scalar()
631
    if not last_modification_timestamp:
632
        if not value_if_none:
633
            return None
634
        else:
635
            last_modification_timestamp = value_if_none
636
    return datetime.fromtimestamp(mktime(
637
        last_modification_timestamp.timetuple()))
638

    
639
def get_plugins_instances():
640
    """
641
    Renvoie une liste d'instances de plugins pour VigiBoard.
642

643
    @return: Liste de tuples contenant le nom du plugin et l'instance associé.
644
    @rtype: C{list} of C{tuple}
645
    """
646
    plugins = config.get('vigiboard_plugins', [])
647
    plugins_instances = []
648
    for (plugin_name, plugin_class) in plugins:
649
        try:
650
            mypac = __import__(
651
                'vigiboard.controllers.plugins.' + plugin_name,
652
                globals(), locals(), [plugin_class], -1)
653
            plugin = getattr(mypac, plugin_class)
654
            if callable(plugin):
655
                plugins_instances.append((plugin_name, plugin()))
656
        except ImportError:
657
            pass
658
    return plugins_instances
659