commit dc00558861c121d5112d4bcf0be941a37a05f5c2
Author: Francois POIROTTE <francois.poirotte@c-s.fr>
Date:   Fri Apr 27 10:51:42 2012 +0200

    Liens vers les cartes dans VigiBoard (#959).
    
    Ajoute des liens vers les cartes sur lesquelles l'hôte concerné par un
    événement apparaît dans le dialogue de détail d'un l'événement corrélé.
    
    L'URL de base de VigiMap doit être renseignée dans le fichier de
    configuration de VigiBoard pour que ceci fonctionne. La configuration de
    l'URL se fait grâce à l'option de configuration "interfaces.vigimap" (le
    nom est le même que celui attendu par VigiAdmin).
    
    Le nombre de liens pouvant apparaître est configurable grâce à l'option
    "max_maps" avec 2 valeurs particulières :
    * -1 inhibe toute limite (ie. affiche toutes les cartes)
    * 0 désactive la fonctionnalité (ie. n'affiche aucune carte, ni même la
      partie réservée à l'affichage des liens dans la boîte de dialogue)
    Si le nombre de cartes disponibles est supérieur à la limite définie et
    que le mécanisme d'affiche des liens est activé, un message avertit en
    plus l'utilisateur.
    
    Ajoute des tests sur cette nouvelle fonctionnalité.
    De plus, le code !JavaScript du module "details" a été externalisé afin
    de faciliter la lecture du fichier .html du module.
    
    Enfin, le comportement d'ouverture des liens (ex: dans l'onglet courant
    ou dans un nouvel onglet) est configurable grâce à l'option
    "links_target" (il s'agit du même nom d'option que dans VigiAdmin).
    
    Change-Id: I0d1b73fc4e9e7caf36e10a87bc3e0ae3d8972d88
    Refs: #959.
    Reviewed-on: https://vigilo-dev.si.c-s.fr/review/690
    Tested-by: Build system <qa@vigilo-dev.si.c-s.fr>
    Reviewed-by: Aurelien BOMPARD <aurelien.bompard@c-s.fr>

diff --git a/Makefile b/Makefile
index 3b84cb4..44122c3 100644
--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,7 @@ build: $(SUBST_FILES)
 
 include buildenv/Makefile.common.python
 MODULE := $(NAME)
-EPYDOC_PARSE := vigiboard\.(widgets|controllers)
+EPYDOC_PARSE := vigiboard\.(widgets|controllers|tests)
 
 deployment/%: deployment/%.in
 	sed -e 's,@SYSCONFDIR@,$(SYSCONFDIR),g' \
diff --git a/deployment/settings.ini.in b/deployment/settings.ini.in
index 69df949..1e57848 100644
--- a/deployment/settings.ini.in
+++ b/deployment/settings.ini.in
@@ -110,6 +110,30 @@ vigiboard_priority_order = asc
 ; (état d'acquittement et priorité ITIL).
 state_first = True
 
+; Adresse des autres interfaces de Vigilo.
+; Il peut s'agir de chemins absolus sur le même serveur (ex: /../vigimap/)
+; ou d'URL complètes (http://other-server.example.com/vigilo/vigimap/).
+; Si les liens sont donnés sous forme absolue, ils sont en fait interprétés
+; comme des liens relatifs par rapport à VigiBoard.
+interfaces.vigimap = /../vigimap/
+
+; Cible pour l'ouverture des liens vers les autres interfaces.
+; Les valeurs possibles sont:
+; "_self"   : ouvre la nouvelle page dans le cadre courant.
+; "_top"    : ouvre la nouvelle page à la place de la page courante.
+; "_blank"  : ouvre la nouvelle page dans une nouvelle fenêtre
+;             ou dans un nouvel onglet.
+; "_parent" : ouvre la nouvelle page dans le cadre parent.
+; <autre>   : ouvre la nouvelle page dans le cadre nommé <autre>.
+; La valeur par défaut est "_blank".
+;links_target = _blank
+
+; Indique le nombre maximum de cartes qui peuvent être affichées
+; dans le module de détail de l'événement corrélé.
+; La valeur -1 supprime toute limite, tandis que la valeur 0
+; supprime l'affichage des liens vers les cartes.
+max_maps = -1
+
 ;
 ; 4 - Configuration du proxy Nagios.
 ;
diff --git a/development.ini b/development.ini
index de3825e..fac7aac 100644
--- a/development.ini
+++ b/development.ini
@@ -26,6 +26,8 @@ db_basename=vigilo_
 password_hashing_function=md5
 lang=fr
 help_link=http://foo.bar/help
+interfaces.vigimap=http://localhost:8080/
+max_maps = -1
 
 ; Délai de rafraîchissement automatique (en secondes).
 refresh_delay=30
diff --git a/test.ini b/test.ini
index d6d7d08..6e3b66f 100644
--- a/test.ini
+++ b/test.ini
@@ -24,3 +24,15 @@ use = config:development.ini
 [app:main_without_authn]
 use = main
 skip_authentication = True
+
+[app:limited_maps]
+use = main_without_authn
+max_maps = 1
+
+[app:disabled_maps]
+use = main_without_authn
+max_maps = 0
+
+[app:unlimited_maps]
+use = main_without_authn
+max_maps = -2
diff --git a/vigiboard/controllers/plugins/details.py b/vigiboard/controllers/plugins/details.py
index fe4a027..8ca2671 100644
--- a/vigiboard/controllers/plugins/details.py
+++ b/vigiboard/controllers/plugins/details.py
@@ -25,12 +25,16 @@ applications externes.
 """
 
 import urllib
-from tg import config, url
+from tg import config, url, request
 from sqlalchemy.sql.expression import null as expr_null, union_all
 
+from repoze.what.predicates import has_permission, in_group
+from vigilo.turbogears.helpers import get_current_user
+
 from vigilo.models.session import DBSession
-from vigilo.models.tables import Event, \
-    CorrEvent, Host, LowLevelService, StateName
+from vigilo.models.tables import Event, CorrEvent, Host, LowLevelService, \
+    StateName, Map, MapNode, MapNodeHost, MapGroup
+from vigilo.models.tables.secondary_tables import MAP_GROUP_TABLE
 
 from vigiboard.controllers.plugins import VigiboardRequestPlugin
 
@@ -52,11 +56,13 @@ class PluginDetails(VigiboardRequestPlugin):
         # Obtention de données sur l'événement et sur son historique
         host_query = DBSession.query(
             Host.idhost.label("idsupitem"),
+            Host.idhost.label("idhost"),
             Host.name.label("host"),
             expr_null().label("service"),
         )
         lls_query = DBSession.query(
             LowLevelService.idservice.label("idsupitem"),
+            Host.idhost.label("idhost"),
             Host.name.label("host"),
             LowLevelService.servicename.label("service"),
         ).join(
@@ -66,6 +72,7 @@ class PluginDetails(VigiboardRequestPlugin):
         event = DBSession.query(
             CorrEvent.idcorrevent,
             CorrEvent.idcause,
+            supitems.c.idhost,
             supitems.c.host,
             supitems.c.service,
             Event.message,
@@ -78,11 +85,43 @@ class PluginDetails(VigiboardRequestPlugin):
         ).filter(CorrEvent.idcorrevent == idcorrevent
         ).first()
 
+        # On détermine les cartes auxquelles cet utilisateur a accès.
+        user_maps = {}
+        max_maps = int(config['max_maps'])
+        is_manager = in_group('managers').is_met(request.environ)
+        if max_maps != 0 and (is_manager or
+            has_permission('vigimap-access').is_met(request.environ)):
+            items = DBSession.query(
+                    Map.idmap,
+                    Map.title,
+                ).distinct(
+                ).join(
+                    (MAP_GROUP_TABLE, MAP_GROUP_TABLE.c.idmap == Map.idmap),
+                    (MapGroup, MapGroup.idgroup == MAP_GROUP_TABLE.c.idgroup),
+                    (MapNodeHost, MapNodeHost.idmap == Map.idmap),
+                ).order_by(Map.title.asc()
+                ).filter(MapNodeHost.idhost == event.idhost)
+
+            if not is_manager:
+                mapgroups = get_current_user().mapgroups(only_direct=True)
+                # pylint: disable-msg=E1103
+                items = items.filter(MapGroup.idgroup.in_(mapgroups))
+
+            # La valeur -1 supprime la limite.
+            if max_maps > 0:
+                # On limite au nombre maximum de cartes demandés + 1.
+                # Un message sera affiché s'il y a effectivement plus
+                # de cartes que la limite configurée.
+                items = items.limit(max_maps + 1)
+
+            user_maps = dict([(m.idmap, m.title) for m in items.all()])
+
         context = {
             'idcorrevent': idcorrevent,
             'host': event.host,
             'service': event.service,
             'message': event.message,
+            'maps': user_maps,
         }
 
         eventdetails = {}
@@ -122,5 +161,6 @@ class PluginDetails(VigiboardRequestPlugin):
                 host = event.host,
                 service = event.service,
                 eventdetails = eventdetails,
+                maps = user_maps,
                 idcause = event.idcause,
             )
diff --git a/vigiboard/i18n/de/LC_MESSAGES/vigiboard.po b/vigiboard/i18n/de/LC_MESSAGES/vigiboard.po
index 3e5d393..063f050 100644
--- a/vigiboard/i18n/de/LC_MESSAGES/vigiboard.po
+++ b/vigiboard/i18n/de/LC_MESSAGES/vigiboard.po
@@ -8,7 +8,7 @@ msgstr ""
 "Project-Id-Version: vigiboard 2.0.0\n"
 "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
 "POT-Creation-Date: 2010-11-19 10:22+0100\n"
-"PO-Revision-Date: 2012-01-06 11:27+0100\n"
+"PO-Revision-Date: 2012-04-27 10:57+0200\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: de_DE <LL@li.org>\n"
 "Plural-Forms: nplurals=2; plural=(n != 1)\n"
@@ -117,36 +117,36 @@ msgid ""
 "\"%(previous)s\" to \"%(new)s\" on event #%(idevent)d"
 msgstr ""
 
-#: vigiboard/controllers/root.py:650
+#: vigiboard/controllers/root.py:651
 #, python-format
 msgid "User \"%(user)s\" (%(address)s) forcefully closed event #%(idevent)d"
 msgstr ""
 
-#: vigiboard/controllers/root.py:666
+#: vigiboard/controllers/root.py:667
 msgid "Forced"
 msgstr ""
 
-#: vigiboard/controllers/root.py:684
+#: vigiboard/controllers/root.py:686
 #, python-format
 msgid ""
 "User \"%(user)s\" (%(address)s) changed the state from \"%(previous)s\" "
 "to \"%(new)s\" on event #%(idevent)d"
 msgstr ""
 
-#: vigiboard/controllers/root.py:696
+#: vigiboard/controllers/root.py:698
 msgid "Updated successfully"
 msgstr "Erfolgreich aktualisiert"
 
-#: vigiboard/controllers/root.py:721
+#: vigiboard/controllers/root.py:723
 #, fuzzy, python-format
 msgid "No such plugin '%s'"
 msgstr "Plug-In \"%s\" nicht vorhanden"
 
-#: vigiboard/controllers/root.py:764
+#: vigiboard/controllers/root.py:766
 msgid "No such incident or insufficient permissions"
 msgstr "Ereignis nicht vorhanden oder unzureichende Berechtigungen"
 
-#: vigiboard/controllers/root.py:876
+#: vigiboard/controllers/root.py:878
 #, python-format
 msgid "Next %(limit)s"
 msgstr ""
diff --git a/vigiboard/i18n/en/LC_MESSAGES/vigiboard.po b/vigiboard/i18n/en/LC_MESSAGES/vigiboard.po
index d685cf9..84fb16d 100644
--- a/vigiboard/i18n/en/LC_MESSAGES/vigiboard.po
+++ b/vigiboard/i18n/en/LC_MESSAGES/vigiboard.po
@@ -6,10 +6,10 @@
 #
 msgid ""
 msgstr ""
-"Project-Id-Version: vigilo-vigiboard 2.0.11\n"
+"Project-Id-Version: vigilo-vigiboard 2.0.16\n"
 "Report-Msgid-Bugs-To: contact@projet-vigilo.org\n"
-"POT-Creation-Date: 2012-01-06 11:27+0100\n"
-"PO-Revision-Date: 2012-01-06 11:27+0100\n"
+"POT-Creation-Date: 2012-04-27 10:57+0200\n"
+"PO-Revision-Date: 2012-04-27 10:57+0200\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: en <LL@li.org>\n"
 "Plural-Forms: nplurals=2; plural=(n != 1)\n"
@@ -117,16 +117,16 @@ msgstr ""
 "User \"%(user)s\" (%(address)s) changed the trouble ticket from "
 "\"%(previous)s\" to \"%(new)s\" on event #%(idevent)d"
 
-#: vigiboard/controllers/root.py:650
+#: vigiboard/controllers/root.py:651
 #, python-format
 msgid "User \"%(user)s\" (%(address)s) forcefully closed event #%(idevent)d"
 msgstr "User \"%(user)s\" (%(address)s) forcefully closed event #%(idevent)d"
 
-#: vigiboard/controllers/root.py:666
+#: vigiboard/controllers/root.py:667
 msgid "Forced"
 msgstr "Forced"
 
-#: vigiboard/controllers/root.py:684
+#: vigiboard/controllers/root.py:686
 #, python-format
 msgid ""
 "User \"%(user)s\" (%(address)s) changed the state from \"%(previous)s\" "
@@ -135,20 +135,20 @@ msgstr ""
 "User \"%(user)s\" (%(address)s) changed the state from \"%(previous)s\" "
 "to \"%(new)s\" on event #%(idevent)d"
 
-#: vigiboard/controllers/root.py:696
+#: vigiboard/controllers/root.py:698
 msgid "Updated successfully"
 msgstr "Updated successfully"
 
-#: vigiboard/controllers/root.py:721
+#: vigiboard/controllers/root.py:723
 #, python-format
 msgid "No such plugin '%s'"
 msgstr "No such plugin '%s'"
 
-#: vigiboard/controllers/root.py:764
+#: vigiboard/controllers/root.py:766
 msgid "No such incident or insufficient permissions"
 msgstr "No such incident or insufficient permissions"
 
-#: vigiboard/controllers/root.py:876
+#: vigiboard/controllers/root.py:878
 #, python-format
 msgid "Next %(limit)s"
 msgstr "Next %(limit)s"
diff --git a/vigiboard/i18n/fr/LC_MESSAGES/vigiboard.po b/vigiboard/i18n/fr/LC_MESSAGES/vigiboard.po
index 6b2bd8e..d7136c3 100644
--- a/vigiboard/i18n/fr/LC_MESSAGES/vigiboard.po
+++ b/vigiboard/i18n/fr/LC_MESSAGES/vigiboard.po
@@ -8,7 +8,7 @@ msgstr ""
 "Project-Id-Version: vigiboard 0.1\n"
 "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
 "POT-Creation-Date: 2009-07-06 11:19+0200\n"
-"PO-Revision-Date: 2012-01-06 11:27+0100\n"
+"PO-Revision-Date: 2012-04-27 10:57+0200\n"
 "Last-Translator: Thomas ANDREJAK <thomas.andrejak@c-s.fr>\n"
 "Language-Team: fr_FR <LL@li.org>\n"
 "Plural-Forms: nplurals=2; plural=(n > 1)\n"
@@ -116,18 +116,18 @@ msgstr ""
 "L'utilisateur \"%(user)s\" (%(address)s) a changé le ticket d'incident de"
 " l'événement n°%(idevent)d de \"%(previous)s\" vers \"%(new)s\""
 
-#: vigiboard/controllers/root.py:650
+#: vigiboard/controllers/root.py:651
 #, python-format
 msgid "User \"%(user)s\" (%(address)s) forcefully closed event #%(idevent)d"
 msgstr ""
 "L'utilisateur \"%(user)s\" (%(address)s) a forcé la fermeture de "
 "l'événement #%(idevent)d"
 
-#: vigiboard/controllers/root.py:666
+#: vigiboard/controllers/root.py:667
 msgid "Forced"
 msgstr "Forcé"
 
-#: vigiboard/controllers/root.py:684
+#: vigiboard/controllers/root.py:686
 #, python-format
 msgid ""
 "User \"%(user)s\" (%(address)s) changed the state from \"%(previous)s\" "
@@ -136,20 +136,20 @@ msgstr ""
 "L'utilisateur \"%(user)s\" (%(address)s) a changé l'état de l'événement "
 "n°%(idevent)d de \"%(previous)s\" vers \"%(new)s\""
 
-#: vigiboard/controllers/root.py:696
+#: vigiboard/controllers/root.py:698
 msgid "Updated successfully"
 msgstr "Mise à jour réussie"
 
-#: vigiboard/controllers/root.py:721
+#: vigiboard/controllers/root.py:723
 #, python-format
 msgid "No such plugin '%s'"
 msgstr "Module introuvable \"%s\""
 
-#: vigiboard/controllers/root.py:764
+#: vigiboard/controllers/root.py:766
 msgid "No such incident or insufficient permissions"
 msgstr "Aucun incident correspondant ou permissions insuffisantes"
 
-#: vigiboard/controllers/root.py:876
+#: vigiboard/controllers/root.py:878
 #, python-format
 msgid "Next %(limit)s"
 msgstr "%(limit)s suivants"
diff --git a/vigiboard/i18n/vigiboard.pot b/vigiboard/i18n/vigiboard.pot
index cf2cb0b..c9276ba 100644
--- a/vigiboard/i18n/vigiboard.pot
+++ b/vigiboard/i18n/vigiboard.pot
@@ -7,9 +7,9 @@
 #, fuzzy
 msgid ""
 msgstr ""
-"Project-Id-Version: vigilo-vigiboard 2.0.11\n"
+"Project-Id-Version: vigilo-vigiboard 2.0.16\n"
 "Report-Msgid-Bugs-To: contact@projet-vigilo.org\n"
-"POT-Creation-Date: 2012-01-06 11:27+0100\n"
+"POT-Creation-Date: 2012-04-27 10:57+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -113,36 +113,36 @@ msgid ""
 "\"%(previous)s\" to \"%(new)s\" on event #%(idevent)d"
 msgstr ""
 
-#: vigiboard/controllers/root.py:650
+#: vigiboard/controllers/root.py:651
 #, python-format
 msgid "User \"%(user)s\" (%(address)s) forcefully closed event #%(idevent)d"
 msgstr ""
 
-#: vigiboard/controllers/root.py:666
+#: vigiboard/controllers/root.py:667
 msgid "Forced"
 msgstr ""
 
-#: vigiboard/controllers/root.py:684
+#: vigiboard/controllers/root.py:686
 #, python-format
 msgid ""
 "User \"%(user)s\" (%(address)s) changed the state from \"%(previous)s\" to "
 "\"%(new)s\" on event #%(idevent)d"
 msgstr ""
 
-#: vigiboard/controllers/root.py:696
+#: vigiboard/controllers/root.py:698
 msgid "Updated successfully"
 msgstr ""
 
-#: vigiboard/controllers/root.py:721
+#: vigiboard/controllers/root.py:723
 #, python-format
 msgid "No such plugin '%s'"
 msgstr ""
 
-#: vigiboard/controllers/root.py:764
+#: vigiboard/controllers/root.py:766
 msgid "No such incident or insufficient permissions"
 msgstr ""
 
-#: vigiboard/controllers/root.py:876
+#: vigiboard/controllers/root.py:878
 #, python-format
 msgid "Next %(limit)s"
 msgstr ""
diff --git a/vigiboard/tests/functional/plugins/test_details_plugin.py b/vigiboard/tests/functional/plugins/test_details_plugin.py
index 48b5f19..a40a50f 100644
--- a/vigiboard/tests/functional/plugins/test_details_plugin.py
+++ b/vigiboard/tests/functional/plugins/test_details_plugin.py
@@ -115,7 +115,8 @@ class TestDetailsPlugin(TestController):
             "peak_state": "WARNING",
             "current_state": "WARNING",
             "host": "bar",
-            "initial_state": "WARNING"
+            "initial_state": "WARNING",
+            "maps": {},
         })
 
     def test_details_plugin_LLS_alert_when_manager(self):
@@ -140,7 +141,8 @@ class TestDetailsPlugin(TestController):
             "peak_state": "WARNING",
             "current_state": "WARNING",
             "host": "bar",
-            "initial_state": "WARNING"
+            "initial_state": "WARNING",
+            "maps": {},
         })
 
     def test_details_plugin_host_alert_when_allowed(self):
@@ -165,7 +167,8 @@ class TestDetailsPlugin(TestController):
             "peak_state": "WARNING",
             "current_state": "WARNING",
             "host": "bar",
-            "initial_state": "WARNING"
+            "initial_state": "WARNING",
+            "maps": {},
         })
 
     def test_details_plugin_host_alert_when_manager(self):
@@ -190,7 +193,8 @@ class TestDetailsPlugin(TestController):
             "peak_state": "WARNING",
             "current_state": "WARNING",
             "host": "bar",
-            "initial_state": "WARNING"
+            "initial_state": "WARNING",
+            "maps": {},
         })
 
     def test_details_plugin_LLS_when_forbidden(self):
diff --git a/vigiboard/tests/functional/plugins/test_details_plugin_maps.py b/vigiboard/tests/functional/plugins/test_details_plugin_maps.py
new file mode 100644
index 0000000..b87474f
--- /dev/null
+++ b/vigiboard/tests/functional/plugins/test_details_plugin_maps.py
@@ -0,0 +1,257 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2006-2011 CS-SI
+# License: GNU GPL v2 <http://www.gnu.org/licenses/gpl-2.0.html>
+
+"""
+Teste la partie "Cartes" du formulaire contenant les détails
+pour un événement corrélé.
+"""
+from datetime import datetime
+import transaction
+
+from vigiboard.tests import TestController
+from vigilo.models.session import DBSession
+from vigilo.models.demo import functions
+from vigilo.models.tables import Permission, CorrEvent, Host, LowLevelService
+
+class TestDetailsPluginMapsHostLimited(TestController):
+    """
+    Teste l'affichage des cartes dans le module de détails
+    lorsqu'une limite à été réglée pour l'affichage.
+    """
+    application_under_test = 'limited_maps'
+    # Seules les 2 premières cartes doivent figurer.
+    # La 1ère correspond à la limite, la 2 seconde permet
+    # de détecter qu'il y avait plus de cartes que la limite.
+    manager = {'1': 'M1', '2': 'M2'}
+    # L'utilisateur avec droits étendus voit
+    # la même chose que le manager (2 cartes).
+    unrestricted = manager.copy()
+    # L'utilisateur avec droits restreints ne voit
+    # qu'une seule carte : "M2".
+    restricted = {'2': 'M2'}
+    supitem_class = Host
+
+    def shortDescription(self, *args, **kwargs):
+        """Description courte du test en cours d'exécution."""
+        res = TestController.shortDescription(*args, **kwargs)
+        return "%s (limited + host)" % (res, )
+
+    def setUp(self):
+        """Initialisation avant chaque test."""
+        super(TestDetailsPluginMapsHostLimited, self).setUp()
+        # On fait manuellement ce que l'initialisation de VigiMap ferait
+        # (car on est dans les tests de VigiBoard, pas ceux de VigiMap).
+        root = functions.add_mapgroup(u'Root')
+        DBSession.add(Permission(permission_name=u'vigimap-access'))
+
+        print "Creation hote, service et cartes"
+        host = functions.add_host(u'localhost éçà')
+        functions.add_lowlevelservice(host, u'lls éçà')
+        sig = functions.add_supitemgroup(u'supitemgroup éçà')
+        functions.add_host2group(host, sig)
+        mg = functions.add_mapgroup(u'éçà', root)
+        m1 = functions.add_map(u'M1', root)
+        # La seconde carte appartient à "/Root/éçà"
+        # et permet de tester les accès indirects.
+        m2 = functions.add_map(u'M2', mg)
+        m3 = functions.add_map(u'M3', root)
+
+        # On ajoute l'hôte 2 fois sur M1 pour vérifier
+        # l'absense de doublons dans les liens.
+        print "Preparation cartes"
+        functions.add_node_host(host, 'h1', m1)
+        functions.add_node_host(host, 'h2', m1)
+        functions.add_node_host(host, 'h', m2)
+        functions.add_node_host(host, 'h', m3)
+
+        # Création de quelques utilisateurs.
+        print "Creation des comptes utilisateurs et reglages permissions"
+        functions.add_user(u'restricted', u're@strict.ed',
+                           u'restricted', u'restricted',
+                           u'restricted')
+        functions.add_user(u'unrestricted', u'unre@strict.ed',
+                           u'unrestricted', u'unrestricted',
+                           u'unrestricted')
+        functions.add_user(u'no_rights', u'no_r@igh.ts',
+                           u'no_rights', u'no_rights',
+                           u'no_rights')
+        # Les 3 utilisateurs ont accès à VigiBoard.
+        functions.add_usergroup_permission(u'no_rights', u'vigiboard-access')
+        functions.add_usergroup_permission(u'restricted', u'vigiboard-access')
+        functions.add_usergroup_permission(u'unrestricted', u'vigiboard-access')
+        # Mais seuls "restricted" et "unrestricted" ont accès à VigiMap.
+        functions.add_usergroup_permission(u'restricted', u'vigimap-access')
+        functions.add_usergroup_permission(u'unrestricted', u'vigimap-access')
+        # Ils voient tous les trois les événements dans VigiBoard...
+        functions.add_supitemgrouppermission(sig, 'no_rights')
+        functions.add_supitemgrouppermission(sig, 'restricted')
+        functions.add_supitemgrouppermission(sig, 'unrestricted')
+        # ... mais "restricted" ne peut voir que "M2" ...
+        functions.add_MapGroupPermission(mg, 'restricted')
+        # ... tandis que "unrestricted" voit les 3 cartes (par héritage).
+        functions.add_MapGroupPermission(root, 'unrestricted')
+
+        DBSession.flush()
+        transaction.commit()
+
+    def tearDown(self):
+        """Nettoyage après chaque test."""
+        transaction.abort()
+        DBSession.expunge_all()
+        super(TestDetailsPluginMapsHostLimited, self).tearDown()
+
+    def _insert_dep(self):
+        """Insertion de l'événement corrélé de test."""
+        print "Insertion evenement correle"
+        timestamp = datetime.now()
+        supitem = DBSession.query(self.supitem_class).one()
+        if isinstance(supitem, Host):
+            event = functions.add_event(supitem, u'DOWN', u'', timestamp)
+        else: # Sinon, il s'agit d'un LowLevelService.
+            event = functions.add_event(supitem, u'CRITICAL', u'', timestamp)
+        functions.add_correvent([event], timestamp=timestamp)
+        DBSession.flush()
+        transaction.commit()
+        correvent = DBSession.query(CorrEvent.idcorrevent).one()
+        return correvent.idcorrevent
+
+    def test_maps_links_anonymous(self):
+        """Cartes dans dialogue détails en tant qu'anonyme."""
+        idcorrevent = self._insert_dep()
+        # 401 : Unauthorized car l'utilisateur doit s'authentifier
+        # pour pouvoir accéder à la boîte de dialogue.
+        self.app.post('/plugin_json', {
+                'idcorrevent': idcorrevent,
+                'plugin_name': 'details',
+            }, extra_environ={}, status=401)
+
+    def test_maps_links_manager(self):
+        """Cartes dans dialogue détails en tant que manager."""
+        idcorrevent = self._insert_dep()
+        response = self.app.post('/plugin_json', {
+                'idcorrevent': idcorrevent,
+                'plugin_name': 'details',
+            }, extra_environ={'REMOTE_USER': 'manager'})
+        # Le manager a toujours accès à tout, la seule limite possible
+        # est celle imposée par l'option "max_maps" dans la configuration.
+        self.assertEquals(response.json['maps'], self.manager)
+
+    def test_maps_links_no_rights(self):
+        """Cartes dans dialogue détails sans droits."""
+        idcorrevent = self._insert_dep()
+        response = self.app.post('/plugin_json', {
+                'idcorrevent': idcorrevent,
+                'plugin_name': 'details',
+            }, extra_environ={'REMOTE_USER': 'no_rights'})
+        # L'utilisateur n'a pas accès à VigiMap, donc il ne doit pas voir
+        # les cartes, même s'il a accès à VigiBoard par ailleurs.
+        self.assertEquals(response.json['maps'], {})
+
+    def test_maps_links_restricted(self):
+        """Cartes dans dialogue détails avec droits restreints."""
+        idcorrevent = self._insert_dep()
+        response = self.app.post('/plugin_json', {
+                'idcorrevent': idcorrevent,
+                'plugin_name': 'details',
+            }, extra_environ={'REMOTE_USER': 'restricted'})
+        # L'utilisateur ne doit voir que les cartes pour lesquelles
+        # il a été autorisé.
+        self.assertEquals(response.json['maps'], self.restricted)
+
+    def test_maps_links_unrestricted(self):
+        """Cartes dans dialogue détails avec droits étendus."""
+        idcorrevent = self._insert_dep()
+        response = self.app.post('/plugin_json', {
+                'idcorrevent': idcorrevent,
+                'plugin_name': 'details',
+            }, extra_environ={'REMOTE_USER': 'unrestricted'})
+        # L'utilisateur ne doit voir que les cartes pour lesquelles
+        # il a été autorisé (les mêmes que que "restricted" + "M2").
+        self.assertEquals(response.json['maps'], self.unrestricted)
+
+
+class TestDetailsPluginMapsHostDisabled(TestDetailsPluginMapsHostLimited):
+    """
+    Teste la désactivation de l'affichage des cartes
+    dans le module de détails.
+    """
+    application_under_test = 'disabled_maps'
+    # La réponse ne doit contenir aucune carte,
+    # quel que soit l'utilisateur qui interroge
+    # VigiBoard (la fonctionnalité est désactivée).
+    manager = {}
+    unrestricted = {}
+    restricted = {}
+
+    def shortDescription(self, *args, **kwargs):
+        """Description courte du test en cours d'exécution."""
+        res = TestController.shortDescription(*args, **kwargs)
+        return "%s (disabled + host)" % (res, )
+
+
+class TestDetailsPluginMapsHostUnlimited(TestDetailsPluginMapsHostLimited):
+    """
+    Teste l'affichage des cartes dans le module de détails
+    lorsqu'il n'y a aucun limite.
+    """
+    application_under_test = 'unlimited_maps'
+    # Le manager voit tout et en particulier les 3 cartes sur lesquelles
+    # l'hôte apparaît. M1 ne doit apparaître qu'une seule fois
+    # même si l'hôte est présent 2 fois sur la carte.
+    manager = {'1': 'M1', '3': 'M3', '2': 'M2'}
+    # L'utilisateur avec droits étendus voit
+    # la même chose que le manager (3 cartes).
+    unrestricted = manager.copy()
+    # L'utilisateur avec droits restreints ne voit pas 'M2'.
+    restricted = {'2': 'M2'}
+
+    def shortDescription(self, *args, **kwargs):
+        """Description courte du test en cours d'exécution."""
+        res = TestController.shortDescription(*args, **kwargs)
+        return "%s (unlimited + host)" % (res, )
+
+class TestDetailsPluginMapsServiceLimited(
+    TestDetailsPluginMapsHostLimited):
+    """
+    Idem que la classe mère mais teste un événement corrélé
+    portant sur un service de l'hôte plutôt que sur l'hôte lui-même.
+    """
+    supitem_class = LowLevelService
+
+    def shortDescription(self, *args, **kwargs):
+        """Description courte du test en cours d'exécution."""
+        # On court-circuite la hiérarchie de classes
+        # pour appeler directement la méthode de nose/unittest.
+        res = TestController.shortDescription(*args, **kwargs)
+        return "%s (limited + service)" % (res, )
+
+class TestDetailsPluginMapsServiceDisable(
+    TestDetailsPluginMapsHostDisabled):
+    """
+    Idem que la classe mère mais teste un événement corrélé
+    portant sur un service de l'hôte plutôt que sur l'hôte lui-même.
+    """
+    supitem_class = LowLevelService
+
+    def shortDescription(self, *args, **kwargs):
+        """Description courte du test en cours d'exécution."""
+        # On court-circuite la hiérarchie de classes
+        # pour appeler directement la méthode de nose/unittest.
+        res = TestController.shortDescription(*args, **kwargs)
+        return "%s (disabled + service)" % (res, )
+
+class TestDetailsPluginMapsServiceUnlimited(
+    TestDetailsPluginMapsHostUnlimited):
+    """
+    Idem que la classe mère mais teste un événement corrélé
+    portant sur un service de l'hôte plutôt que sur l'hôte lui-même.
+    """
+    supitem_class = LowLevelService
+
+    def shortDescription(self, *args, **kwargs):
+        """Description courte du test en cours d'exécution."""
+        # On court-circuite la hiérarchie de classes
+        # pour appeler directement la méthode de nose/unittest.
+        res = TestController.shortDescription(*args, **kwargs)
+        return "%s (unlimited + service)" % (res, )
