diff --git a/FEATURES/78195 b/FEATURES/78195
new file mode 100644
index 0000000000000000000000000000000000000000..a7df344facbc8737f9164ec86db6b9067771db36
--- /dev/null
+++ b/FEATURES/78195
@@ -0,0 +1,10 @@
+        '78195' =>
+            ['Label' => $this->_('Export de l\'agenda'),
+             'Desc' => $this->_('La boîte agenda peut afficher un boutoun d\'export des événements au format iCal'),
+             'Image' => '',
+             'Video' => '',
+             'Category' => $this->_('Agenda'),
+             'Right' => function($feature_description, $user) {return true;},
+             'Wiki' => 'http://wiki.bokeh-library-portal.org/index.php?title=Exporter_l%27agenda_au_format_iCalendar_(iCal)',
+             'Test' => '',
+             'Date' => '2018-10-30'],
\ No newline at end of file
diff --git a/VERSIONS_WIP/78195 b/VERSIONS_WIP/78195
new file mode 100644
index 0000000000000000000000000000000000000000..1a16570e4ee5d1dac4f0d30126fe025d7812623a
--- /dev/null
+++ b/VERSIONS_WIP/78195
@@ -0,0 +1 @@
+ - ticket #78195 : La boîte agenda peut afficher un boutoun d\'export des événements au format iCal
\ No newline at end of file
diff --git a/application/modules/opac/controllers/CmsController.php b/application/modules/opac/controllers/CmsController.php
index 8963c5f7097675152cbf74857fd55c9375c0310b..70c434e69b4dc32a3402b9931b9d98509844d2fa 100644
--- a/application/modules/opac/controllers/CmsController.php
+++ b/application/modules/opac/controllers/CmsController.php
@@ -165,9 +165,13 @@ class CmsController extends ZendAfi_Controller_Action {
 
 
   public function icalAction() {
-    $id_profil = (int)$this->_getParam('id_profil',1);
+    $profil = Class_Profil::find((int)$this->_getParam('id_profil', 1));
+
     $id_module = (int)$this->_getParam('id_module');
-    $this->_helper->renderIcal($id_profil, $id_module, $this->view);
+
+    ($id_article = (int)$this->_getParam('id_article'))
+      ? $this->_helper->renderIcalArticles($profil, [Class_Article::find($id_article)])
+      : $this->_helper->renderIcal($profil, $id_module);
   }
 
 
diff --git a/library/Class/Systeme/ModulesAccueil/Calendrier.php b/library/Class/Systeme/ModulesAccueil/Calendrier.php
index 6b36ab28892528ce790ed6e513a0851f1ff52ad8..d3ff2169a141a89833d4aa0a7c54f10c7eb7d7a2 100644
--- a/library/Class/Systeme/ModulesAccueil/Calendrier.php
+++ b/library/Class/Systeme/ModulesAccueil/Calendrier.php
@@ -38,6 +38,7 @@ class Class_Systeme_ModulesAccueil_Calendrier extends Class_Systeme_ModulesAccue
                              'display_cat_select' => false,
                              'display_event_info'=> false,
                              'rss_avis' => false,
+                             'ical_feed' => 0,
                              'display_next_event' => '1',
                              'display_order' => 'EventDebut',
                              'display_mode' => 'Title',
diff --git a/library/ZendAfi/Controller/Action/Helper/RenderIcal.php b/library/ZendAfi/Controller/Action/Helper/RenderIcal.php
index 3636aa1c00bba567634fe115603fa03df2d44162..0e6efae8cb63d67d0244ec5700fb0334a5dca8cf 100644
--- a/library/ZendAfi/Controller/Action/Helper/RenderIcal.php
+++ b/library/ZendAfi/Controller/Action/Helper/RenderIcal.php
@@ -23,16 +23,8 @@
 class ZendAfi_Controller_Action_Helper_RenderIcal
   extends Zend_Controller_Action_Helper_Abstract {
 
-  const PRODID = 'http://bokeh-library-portal.org';
-  static protected $_autoloaded = false;
-
-  protected $_profil;
-
-
-  public function direct($id_profil, $id_module, $view) {
-    $this->_view = $view;
-
-    if (!$profil = Class_Profil::find($id_profil))
+   public function direct($profil, $id_module) {
+    if (!$profil)
       return $this->_render();
 
     if (!$preferences = $profil->getModuleAccueilPreferences($id_module, 'CALENDAR'))
@@ -41,53 +33,6 @@ class ZendAfi_Controller_Action_Helper_RenderIcal
     $this->_profil = $profil;
     $articles = Class_Calendar::getAllNextEventsByPref($preferences);
 
-    $this->_render($articles);
-  }
-
-
-  protected function _render($articles=[]) {
-    static::ensureAutoload();
-
-    $vCalendar = new \Eluceo\iCal\Component\Calendar(static::PRODID);
-
-    foreach($articles as $article)
-      $vCalendar->addComponent(Class_ICal_Event::newFrom($article, $this->_profil));
-
-    $this->_actionController
-      ->getHelper('ViewRenderer')->setNoRender();
-
-    $this->_actionController
-      ->getResponse()
-      ->setHeader('Content-Type', 'text/calendar;charset=utf-8');
-
-    $this->_actionController
-      ->getResponse()
-      ->setHeader('Content-Disposition', 'attachment;filename="calendar.ics"');
-
-    echo $vCalendar->render();
-  }
-
-
-  public static function ensureAutoload() {
-    if (static::$_autoloaded)
-      return;
-
-    $loader = function ($class) {
-      if (false === strpos($class, 'Eluceo\\iCal'))
-        return;
-
-      if ('\\' == substr($class, 0, 1))
-        $class = substr($class, 1);
-
-      $class = str_replace('Eluceo\\iCal\\', '', $class);
-      $class = str_replace('\\', '/', $class);
-      $path = __DIR__ . '/../../../../iCal/src/' . $class . '.php';
-
-      if (file_exists($path))
-        require_once $path;
-    };
-
-    spl_autoload_register($loader);
-    static::$_autoloaded = true;
+    $this->_actionController->getHelper('RenderIcalArticles')->direct($profil, $articles);
   }
 }
diff --git a/library/ZendAfi/Controller/Action/Helper/RenderIcalArticles.php b/library/ZendAfi/Controller/Action/Helper/RenderIcalArticles.php
new file mode 100644
index 0000000000000000000000000000000000000000..a43dfe844d59c12420a32b9a18c6c72d5dfd934f
--- /dev/null
+++ b/library/ZendAfi/Controller/Action/Helper/RenderIcalArticles.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * Copyright (c) 2012-2017, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+class ZendAfi_Controller_Action_Helper_RenderIcalArticles
+  extends Zend_Controller_Action_Helper_Abstract {
+
+  const PRODID = 'http://bokeh-library-portal.org';
+  static protected $_autoloaded = false;
+
+  public function direct($profil, $articles=[]) {
+    static::ensureAutoload();
+
+    $vCalendar = new \Eluceo\iCal\Component\Calendar(static::PRODID);
+
+    foreach($articles as $article)
+      $vCalendar->addComponent(Class_ICal_Event::newFrom($article, $profil));
+
+    $this->_actionController
+      ->getHelper('ViewRenderer')->setNoRender();
+
+    $this->_actionController
+      ->getResponse()
+      ->setHeader('Content-Type', 'text/calendar;charset=utf-8');
+
+    $this->_actionController
+      ->getResponse()
+      ->setHeader('Content-Disposition', 'attachment;filename="calendar.ics"');
+
+    echo $vCalendar->render();
+  }
+
+
+  public static function ensureAutoload() {
+    if (static::$_autoloaded)
+      return;
+
+    $loader = function ($class) {
+      if (false === strpos($class, 'Eluceo\\iCal'))
+        return;
+
+      if ('\\' == substr($class, 0, 1))
+        $class = substr($class, 1);
+
+      $class = str_replace('Eluceo\\iCal\\', '', $class);
+      $class = str_replace('\\', '/', $class);
+      $path = __DIR__ . '/../../../../iCal/src/' . $class . '.php';
+
+      if (file_exists($path))
+        require_once $path;
+    };
+
+    spl_autoload_register($loader);
+    static::$_autoloaded = true;
+  }
+}
diff --git a/library/ZendAfi/Form/Configuration/Widget/Articles.php b/library/ZendAfi/Form/Configuration/Widget/Articles.php
index 9d7829ec607a8a855d47855a6853a08c545f1a3d..ac188ff440686a03748818fef27b3047a86f12c4 100644
--- a/library/ZendAfi/Form/Configuration/Widget/Articles.php
+++ b/library/ZendAfi/Form/Configuration/Widget/Articles.php
@@ -67,6 +67,11 @@ class ZendAfi_Form_Configuration_Widget_Articles extends ZendAfi_Form_Configurat
   }
 
 
+  public function hasRssSupport() {
+    return true;
+  }
+
+
   public function populate(array $datas) {
     $this->getElement('style_liste')
          ->setPreferences($datas);
diff --git a/library/ZendAfi/Form/Configuration/Widget/Base.php b/library/ZendAfi/Form/Configuration/Widget/Base.php
index 71dd6abd2ebd54256f29254119b342263cd857bb..96cff5e5dd7854192a5332ddab5190a8f8eadfaa 100644
--- a/library/ZendAfi/Form/Configuration/Widget/Base.php
+++ b/library/ZendAfi/Form/Configuration/Widget/Base.php
@@ -21,7 +21,6 @@
 
 
 class ZendAfi_Form_Configuration_Widget_Base extends ZendAfi_Form {
-
   public function init() {
     parent::init();
 
@@ -34,12 +33,14 @@ class ZendAfi_Form_Configuration_Widget_Base extends ZendAfi_Form {
       ->addElement('select',
                    'boite',
                    ['label' => $this->_('Style de la boite'),
-                    'multiOptions' => (new Class_Profil_Templates(Class_Profil::getCurrentProfil()))->toArray()])
+                    'multiOptions' => (new Class_Profil_Templates(Class_Profil::getCurrentProfil()))->toArray()]);
 
-      ->addElement('checkbox',
-                   'rss_avis',
-                   ['label' => $this->_('Proposer un fil RSS'),
-                    'value' => 0]);
+    if ($this->hasRssSupport())
+      $this
+        ->addElement('checkbox',
+                     'rss_avis',
+                     ['label' => $this->_('Proposer un fil RSS'),
+                      'value' => 0]);
   }
 
 
@@ -48,10 +49,17 @@ class ZendAfi_Form_Configuration_Widget_Base extends ZendAfi_Form {
       ->addToHeadGroup(['titre',
                         'style_liste'])
 
-      ->addToStyleGroup(['boite',
-                         'rss_avis']);
+      ->addToStyleGroup(['boite']);
+
+    if ($this->hasRssSupport())
+      $this->addToDisplaySettingsGroup(['rss_avis']);
 
     Class_Template::current()->customWidgetForm($this);
     return parent::populate($datas);
   }
+
+
+  public function hasRssSupport() {
+    return false;
+  }
 }
\ No newline at end of file
diff --git a/library/ZendAfi/Form/Configuration/Widget/Calendar.php b/library/ZendAfi/Form/Configuration/Widget/Calendar.php
index 62bf453559575b8043ac13f1dc503548349dc183..fbc156fae782fccd1ffdbfc112cc6e5dbac55add 100644
--- a/library/ZendAfi/Form/Configuration/Widget/Calendar.php
+++ b/library/ZendAfi/Form/Configuration/Widget/Calendar.php
@@ -90,7 +90,17 @@ class ZendAfi_Form_Configuration_Widget_Calendar extends ZendAfi_Form_Configurat
 
       ->addElement('checkbox',
                    'display_cat_select',
-                   ['label' => $this->_('Afficher sélection')]);
+                   ['label' => $this->_('Afficher sélection')])
+
+      ->addElement('checkbox',
+                   'ical_feed',
+                   ['label' => $this->_('Proposer un flux iCal'),
+                    'value' => 0]);
+  }
+
+
+  public function hasRssSupport() {
+    return true;
   }
 
 
@@ -121,7 +131,8 @@ class ZendAfi_Form_Configuration_Widget_Calendar extends ZendAfi_Form_Configurat
                                    'mode-affichage',
                                    'display_event_info',
                                    'event_filter',
-                                   'enabled_filters'])
+                                   'enabled_filters',
+                                   'ical_feed'])
 
       ->addDisplayGroup(['display_full_page',
                          'display_mode',
diff --git a/library/ZendAfi/Form/Configuration/Widget/Carousel.php b/library/ZendAfi/Form/Configuration/Widget/Carousel.php
index b4fc1d4d1b185b4b9ef072625a94794cd30902a8..38a53ecf9a3b1208a03295220d69273cbabc01a5 100644
--- a/library/ZendAfi/Form/Configuration/Widget/Carousel.php
+++ b/library/ZendAfi/Form/Configuration/Widget/Carousel.php
@@ -45,6 +45,11 @@ class ZendAfi_Form_Configuration_Widget_Carousel extends ZendAfi_Form_Configurat
   }
 
 
+  public function hasRssSupport() {
+    return true;
+  }
+
+
   public function init() {
     Class_ScriptLoader::getInstance()
       ->addJqueryReady('formSelectToggleVisibilityForElement("#aleatoire", $("#nb_analyse").closest("tr"), "1")');
diff --git a/library/ZendAfi/Form/Configuration/Widget/ListOfSites.php b/library/ZendAfi/Form/Configuration/Widget/ListOfSites.php
index ea0279832aed3cf98fa67d8e2d03552e4a61b088..c9c437980b64994b18515ec0a53312009849001d 100644
--- a/library/ZendAfi/Form/Configuration/Widget/ListOfSites.php
+++ b/library/ZendAfi/Form/Configuration/Widget/ListOfSites.php
@@ -74,4 +74,9 @@ class ZendAfi_Form_Configuration_Widget_ListOfSites extends ZendAfi_Form_Configu
                                    'nb_aff']);
     return parent::populate($datas);
   }
+
+
+  public function hasRssSupport() {
+    return true;
+  }
 }
diff --git a/library/ZendAfi/Form/Configuration/Widget/Reviews.php b/library/ZendAfi/Form/Configuration/Widget/Reviews.php
index a1b490015070f62a190c9b2d57e1be290b86cb2f..6e8788235723b7c06e8da79489ba4901abb2eddd 100644
--- a/library/ZendAfi/Form/Configuration/Widget/Reviews.php
+++ b/library/ZendAfi/Form/Configuration/Widget/Reviews.php
@@ -82,4 +82,10 @@ class ZendAfi_Form_Configuration_Widget_Reviews extends ZendAfi_Form_Configurati
 
     return parent::populate($datas);
   }
+
+
+  public function hasRssSupport() {
+    return true;
+  }
+
 }
\ No newline at end of file
diff --git a/library/ZendAfi/View/Helper/Accueil/Base.php b/library/ZendAfi/View/Helper/Accueil/Base.php
index d7376fb5443e3ef25577d0ab06eb16d28889e268..0f35d2e3a65ef77781626a07eb6a1fe917650569 100644
--- a/library/ZendAfi/View/Helper/Accueil/Base.php
+++ b/library/ZendAfi/View/Helper/Accueil/Base.php
@@ -31,6 +31,7 @@ class ZendAfi_View_Helper_Accueil_Base extends ZendAfi_View_Helper_ModuleAbstrac
     $contenu = '',                   // Contenu du module
     $message,                   // Message au dessus de la boite du module
     $rss_interne,               // Lien sur fil rss interne
+    $_ical_feed,                 // Link to ical feed
     $_fonction_admin_helper = 'FonctionsAdmin_Boite',
     $_id_menu;                  //identifiant du menu si rendu dans un menu
 
@@ -120,9 +121,9 @@ class ZendAfi_View_Helper_Accueil_Base extends ZendAfi_View_Helper_ModuleAbstrac
 
 
   /*
-   * Génère l'url du RSS pour ce module
+   * Generate URL for $controller / $action
    */
-  protected function _getRSSurl($controller, $action) {
+  protected function _getUrlFor($controller, $action) {
     return Class_Profil::getCurrentProfil()->urlForModule($controller,
                                                           $action,
                                                           $this->id_module);
@@ -137,6 +138,7 @@ class ZendAfi_View_Helper_Accueil_Base extends ZendAfi_View_Helper_ModuleAbstrac
             "CONTENU" => $this->getFonctionAdmin().$this->contenu,
             "MESSAGE" => $this->message,
             "RSS" => $this->rss_interne,
+            "ICAL" =>  $this->_ical_feed,
             "TYPE_MODULE" => strtolower($this->type_module)];
   }
 
@@ -259,14 +261,23 @@ class ZendAfi_View_Helper_Accueil_Base extends ZendAfi_View_Helper_ModuleAbstrac
   //------------------------------------------------------------------------------------------------------
   public function getBoiteFromTemplate($template, $html_array)
   {
-    $html_array['RSS'] = array_isset('RSS', $html_array)
-      ? $this->view->tagAnchor($html_array['RSS'],
-                               $this->view->tagImg(URL_IMG . 'rss.gif',
-                                                   ['alt' => $this->_('flux RSS de la boite %s',
-                                                                      $this->preferences['titre'])]),
-                               ['target' => '_blank',
-                                'style' => 'border: 0px;'])
-      : '';
+    $feeds = [];
+    if (array_isset('RSS', $html_array))
+      $feeds []= $this->view->tagAnchor($html_array['RSS'],
+                                        $this->view->tagImg(URL_IMG . 'rss.gif',
+                                                            ['alt' => $this->_('flux RSS de la boite %s',
+                                                                               $this->preferences['titre'])]),
+                                        ['target' => '_blank',
+                                         'style' => 'border: 0px;']);
+
+    if (array_isset('ICAL', $html_array))
+      $feeds []= $this->view->tagICal($html_array['ICAL'],
+                                      $this->_('Importer "%s" dans votre agenda',
+                                               $this->preferences['titre']));
+
+
+
+    $html_array['RSS'] = implode($feeds);
 
     // Lire le template
     if(file_exists($template))
@@ -502,8 +513,15 @@ class ZendAfi_View_Helper_Accueil_Base extends ZendAfi_View_Helper_ModuleAbstrac
   public function getFloodTools() {
     $html = [];
 
-    if($this->rss_interne)
-      $html [] = $this->view->tagRss($this->rss_interne, $this->_('Accéder au flux RSS de %s', $this->getTitle()));
+    if ($this->rss_interne)
+      $html [] = $this->view->tagRss($this->rss_interne,
+                                     $this->_('Accéder au flux RSS de %s',
+                                              $this->getTitle()));
+
+    if ($this->_ical_feed)
+      $html [] = $this->view->tagICal($this->_ical_feed,
+                                      $this->_('Importer "%s" dans votre agenda',
+                                               $this->getTitle()));
 
     if(empty($html))
       return '';
diff --git a/library/ZendAfi/View/Helper/Accueil/Calendar.php b/library/ZendAfi/View/Helper/Accueil/Calendar.php
index 97a2857d4ffb146d195a56f966367da6a1d5f1c4..e4aaaf4d02714733e164d991790cf094d96b690d 100644
--- a/library/ZendAfi/View/Helper/Accueil/Calendar.php
+++ b/library/ZendAfi/View/Helper/Accueil/Calendar.php
@@ -47,8 +47,13 @@ class ZendAfi_View_Helper_Accueil_Calendar extends ZendAfi_View_Helper_Accueil_B
                                           $this->preferences["titre"]);
 
 
+    $this->rss_interne = '';
     if ($this->preferences['rss_avis'])
-      $this->rss_interne = $this->_getRSSurl('cms', 'calendarrss');
+      $this->rss_interne = $this->_getUrlFor('cms', 'calendarrss');
+
+    if ($this->preferences['ical_feed']) {
+      $this->_ical_feed = $this->_getUrlFor('cms', 'ical');
+    }
 
     $calendar = new Class_Calendar($this->id_module, $this->preferences);
     $this->contenu = $this->view->calendarContent($calendar, $this->preferences);
diff --git a/library/ZendAfi/View/Helper/Accueil/Critiques.php b/library/ZendAfi/View/Helper/Accueil/Critiques.php
index 1c0e147fea217f93a7c4de0315a1e7ef0d0c29c7..addcf761c0114449c219a5abed0b596af5dad59a 100644
--- a/library/ZendAfi/View/Helper/Accueil/Critiques.php
+++ b/library/ZendAfi/View/Helper/Accueil/Critiques.php
@@ -34,7 +34,7 @@ class ZendAfi_View_Helper_Accueil_Critiques extends ZendAfi_View_Helper_Accueil_
     $this->titre = $this->_getTitle();
 
     if ($this->getPreference('rss_avis'))
-      $this->rss_interne = $this->_getRSSurl('rss', 'critiques');
+      $this->rss_interne = $this->_getUrlFor('rss', 'critiques');
 
     $fetched_avis = Class_AvisNotice::getAvisFromPreferences($this->getPreferences());
 
diff --git a/library/ZendAfi/View/Helper/Accueil/Kiosque.php b/library/ZendAfi/View/Helper/Accueil/Kiosque.php
index 9787da375953c96f5012e95695839a15989ac816..c03a474dd2a051c8c145418ae20ac1faa1caa81d 100644
--- a/library/ZendAfi/View/Helper/Accueil/Kiosque.php
+++ b/library/ZendAfi/View/Helper/Accueil/Kiosque.php
@@ -262,7 +262,7 @@ class ZendAfi_View_Helper_Accueil_Kiosque extends ZendAfi_View_Helper_Accueil_Ba
 
   public function getRss() {
     return $this->preferences['rss_avis']
-      ? $this->rss_interne = $this->_getRSSurl('rss', 'kiosque')
+      ? $this->rss_interne = $this->_getUrlFor('rss', 'kiosque')
       : '';
   }
 
diff --git a/library/ZendAfi/View/Helper/Accueil/News.php b/library/ZendAfi/View/Helper/Accueil/News.php
index d9cd50a6ab38ff298891cc6badcf564fc32fbca2..4328142a27687c56e3499da470bacb5ff2c65981 100644
--- a/library/ZendAfi/View/Helper/Accueil/News.php
+++ b/library/ZendAfi/View/Helper/Accueil/News.php
@@ -69,7 +69,7 @@ class ZendAfi_View_Helper_Accueil_News extends ZendAfi_View_Helper_Accueil_Base
     $this->titre = $this->getHtmlTitre();
 
     if ($this->preferences['rss_avis'])
-      $this->rss_interne = $this->_getRSSurl('cms', 'rss');
+      $this->rss_interne = $this->_getUrlFor('cms', 'rss');
 
     return $this->getHtmlArray();
   }
diff --git a/library/ZendAfi/View/Helper/Accueil/Sito.php b/library/ZendAfi/View/Helper/Accueil/Sito.php
index 57624a1a952a96ca081389ddb130c0ff2152ea24..ee3af7942b26fc6cf18c5c07520e1de61c6e926a 100644
--- a/library/ZendAfi/View/Helper/Accueil/Sito.php
+++ b/library/ZendAfi/View/Helper/Accueil/Sito.php
@@ -81,7 +81,7 @@ class ZendAfi_View_Helper_Accueil_Sito extends ZendAfi_View_Helper_Accueil_Base
 
   public function getHtml() {
     if ($this->getPreference('rss') || $this->getPreference('rss_avis'))
-      $this->rss_interne = $this->_getRSSurl('sito', 'sito-rss');
+      $this->rss_interne = $this->_getUrlFor('sito', 'sito-rss');
 
     $strategy = $this->getStrategy();
     $this->titre = $strategy->getTitle();
diff --git a/library/ZendAfi/View/Helper/Article/RenderAbstract.php b/library/ZendAfi/View/Helper/Article/RenderAbstract.php
index f86454457e43b3f900f9fae94a6c21b4f71992a6..41aa6c547544fb5280ca607e529fb3f8f24ed61f 100644
--- a/library/ZendAfi/View/Helper/Article/RenderAbstract.php
+++ b/library/ZendAfi/View/Helper/Article/RenderAbstract.php
@@ -34,6 +34,7 @@ abstract class ZendAfi_View_Helper_Article_RenderAbstract
                     ['class' => 'article_content'])
       . $this->_tag('footer',
                     $this->renderLieu($article)
+                    .$this->renderICalLink($article)
                     .$this->renderReseauxSociaux($article)
                     .$this->renderAvis($article))
       . '<!-- RSPEAK_STOP -->';
@@ -108,8 +109,8 @@ abstract class ZendAfi_View_Helper_Article_RenderAbstract
     if (!$article->hasSummary())
       return '';
     return $this->view->tagAnchor($this->view->url($article->getUrl()),
-    $this->view->_("Lire l'article complet"),
-    ['class' => 'article_read_full']);
+                                  $this->view->_("Lire l'article complet"),
+                                  ['class' => 'article_read_full']);
   }
 
 
@@ -131,6 +132,9 @@ abstract class ZendAfi_View_Helper_Article_RenderAbstract
   }
 
 
+  public function renderICalLink($article) { }
+
+
   public function renderArticleInfo($article) {
     return $this->view->tagArticleInfo($article);
   }
diff --git a/library/ZendAfi/View/Helper/Article/RenderFullContent.php b/library/ZendAfi/View/Helper/Article/RenderFullContent.php
index be29acf21f2e2d11c41cc7eb684933351d9552ee..b27c4ea50cb951ef704e9c3640e26118d349afd3 100644
--- a/library/ZendAfi/View/Helper/Article/RenderFullContent.php
+++ b/library/ZendAfi/View/Helper/Article/RenderFullContent.php
@@ -23,12 +23,26 @@ class ZendAfi_View_Helper_Article_RenderFullContent extends ZendAfi_View_Helper_
     return $this->renderArticle($article, 'article');
   }
 
+
   public function renderTitreHeader($article) {
     return $article->getCacherTitre() ? '' : parent::renderTitreHeader($article);
   }
 
+
   public function renderContent($article) {
     return $this->view->article_ReplaceWidgets($article->getFullContent());
   }
+
+
+  public function renderICalLink($article) {
+    return
+      $article->hasEventsDebut()
+      ? $this->view->tagICal(Class_Url::absolute(['controller' => 'cms',
+                                                  'action' => 'ical',
+                                                  'id_article' => $article->getId()]),
+                             $this->view->_('Ajouter au calendrier'))
+      : '';
+  }
+
 }
 ?>
\ No newline at end of file
diff --git a/library/ZendAfi/View/Helper/TagICal.php b/library/ZendAfi/View/Helper/TagICal.php
new file mode 100644
index 0000000000000000000000000000000000000000..3ef1181521bcc99fa145755749566904418d8b91
--- /dev/null
+++ b/library/ZendAfi/View/Helper/TagICal.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Copyright (c) 2012, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE as published by
+ * the Free Software Foundation.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * along with BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+class ZendAfi_View_Helper_TagICal extends ZendAfi_View_Helper_BaseHelper {
+  public function tagICal($url, $label) {
+    return $this->_tag('div',
+                       $this->view->tagAnchor($url,
+                                              $this->view->tagImg(
+                                                                  $this->view->skinImageUrl('ical.png'),
+                                                                  ['alt' => $label]),
+                                              ['title' => $label,
+                                               'target' => '_blank']),
+                       ['class' => 'ical']);
+  }
+}
\ No newline at end of file
diff --git a/public/opac/css/global.css b/public/opac/css/global.css
index 9399658dc5a93d39f84b6330696aa34c1bf8d4ab..e409b216689943ea2aabbfb00a85e5d5229efa84 100644
--- a/public/opac/css/global.css
+++ b/public/opac/css/global.css
@@ -119,6 +119,11 @@ form.login td.droite {
 
 /* ARTICLES */
 
+article footer .ical {
+    float: left;
+    margin-right: 3px;
+}
+
 dl.article_info {
     display:none;
 }
diff --git a/public/opac/css/responsive.css b/public/opac/css/responsive.css
index 4118e21c3a19fa6094e4ea8b018eb0af00804118..f3aecd8a9b1859971f74781d4a5d24e0b0e07eaf 100644
--- a/public/opac/css/responsive.css
+++ b/public/opac/css/responsive.css
@@ -146,6 +146,11 @@
     }
 
 
+    #col_wrapper .boite > div {
+	overflow: hidden;
+    }
+
+
     #colGaucheInner, 
     #colDroiteInner, 
     #colMilieuInner {
@@ -167,16 +172,15 @@
     #abonne_edit > *, 
     #abonne_edit fieldset, 
     #abonne_edit fieldset td,
-
     #suggestion,
     #suggestion > *, 
     #suggestion fieldset, 
     #suggestion fieldset td,
-
     .filtre_recherche,
     .filtre_recherche > *,
     #colMilieuInner .contenuInner ,    
     #colMilieuInner .contenuInner > *,
+    #colContenuInner .boiteMilieu,
     .resultat_recherche,
     .resultat_recherche > div ,
     .resultat_recherche .notice_wrapper,
@@ -208,15 +212,12 @@
 
     #site_web_content > *,
     .siteWeb div#col_wrapper,
-
     #abonne_edit > *,
     #abonne_edit fieldset, 
     #abonne_edit fieldset td,
-
     #suggestion > *,
     #suggestion fieldset, 
     #suggestion fieldset td,
-
     .resultat_recherche,
     .siteWeb,
     .footer,
@@ -603,6 +604,7 @@
     .facette_outer {
         margin: 10px 0 !important;
         padding: 10px 0 !important;
+	position: relative;
     }
 
 
@@ -704,10 +706,12 @@
         display: block;
         height: 2em;
         width: 40px;
-        float: right;
         background-image: url(../images/buttons/down-chevron.png);
         background-position: center center;
         background-repeat: no-repeat;
+	position: absolute;
+	top: 1ex;
+	right: 0;
     }
 
 
diff --git a/public/opac/images/ical.png b/public/opac/images/ical.png
new file mode 100644
index 0000000000000000000000000000000000000000..763b0e8e21ae2ada6c8308cf377b4745d750129a
Binary files /dev/null and b/public/opac/images/ical.png differ
diff --git a/tests/application/modules/admin/controllers/WidgetControllerTest.php b/tests/application/modules/admin/controllers/WidgetControllerTest.php
index 7cb15573ca063d8a643578df5cd950d01bf1d645..5452e33a311418df119771ab3dad01ccb90fd959 100644
--- a/tests/application/modules/admin/controllers/WidgetControllerTest.php
+++ b/tests/application/modules/admin/controllers/WidgetControllerTest.php
@@ -309,6 +309,7 @@ class WidgetControllerCalendarTest extends WidgetControllerDispatchWidgetConfigu
     $this->_type_module = 'CALENDAR';
     $this->_preferences = ['nb_events' => 3,
                            'rss_avis' => 0,
+                           'ical_feed' => 1,
                            'display_calendar' => 1,
                            'mode-affichage' => 'diaporama_navigation',
                            'op_navigation_mode' => 'next_previous',
@@ -348,6 +349,12 @@ class WidgetControllerCalendarTest extends WidgetControllerDispatchWidgetConfigu
   }
 
 
+  /** @test */
+  public function checkboxICALFeedShouldBeChecked() {
+    $this->assertXPath('//fieldset//input[@type="checkbox"][@checked="checked"][@name="ical_feed"]');
+  }
+
+
   /** @test */
   public function selectDisplayModeShouldBeDisplay() {
     $this->assertXPathContentContains('//select[@name="mode-affichage"]/option[@value="diaporama_navigation"]', 'Diaporama avec navigation');
@@ -1249,6 +1256,12 @@ class WidgetControllerCalendarSimpleTest extends WidgetControllerDispatchWidgetC
   }
 
 
+  /** @test */
+  public function checkboxICALFeedShouldNotBeChecked() {
+    $this->assertXPath('//fieldset//input[@type="checkbox"][not(@checked)][@name="ical_feed"]');
+  }
+
+
   /** @test */
   public function inputTitleShouldDisplayAgenda() {
     $this->assertXPath("//input[@name='titre'][@value='Agenda']");
diff --git a/tests/application/modules/opac/controllers/CmsControllerCalendarActionTest.php b/tests/application/modules/opac/controllers/CmsControllerCalendarActionTest.php
index 9e391c3fe66abb2d3aca28d2d89d31ff0fd5ab29..4f842401bed5cf301be1bcb0c86cfa41575a73a2 100644
--- a/tests/application/modules/opac/controllers/CmsControllerCalendarActionTest.php
+++ b/tests/application/modules/opac/controllers/CmsControllerCalendarActionTest.php
@@ -737,7 +737,7 @@ abstract class CmsControllerCalendarActionIcalExportTestCase
       ->whenCalled('getArticlesByPreferences')
       ->answers($this->_articles_by_pref);
 
-    $this->dispatch('/cms/ical');
+    $this->_dispatch();
     $this->_ical = $this->_response->getBody();
   }
 
@@ -745,6 +745,11 @@ abstract class CmsControllerCalendarActionIcalExportTestCase
   protected function _prepareFixtures() {}
 
 
+  protected function _dispatch() {
+    $this->dispatch('/cms/ical');
+  }
+
+
   /** @test */
   public function contentTypeHeaderShouldBeTextCalendarUtf8() {
     $this->assertHeaderContains('Content-Type', 'text/calendar;charset=utf-8');
@@ -1114,6 +1119,48 @@ class CmsControllerCalendarActionWithOutDateTest extends AbstractControllerTestC
 
 
 
+class CmsControllerCalendarActionIcalExportOneArticleByIdTest
+  extends CmsControllerCalendarActionIcalExportTestCase {
+
+  protected function _prepareFixtures () {
+    $this->fixture('Class_Article',
+                   ['id' => 5,
+                    'titre' => 'OPAC 4 en prod !',
+                    'contenu' => '<h3>youpi &amp; oui c&#39;est beau !</h3><img src="/userfiles/images/youpi.png">',
+                    'lieu' => $this->annecy,
+                    'all_day' => 1,
+                    'pick_day' => '1,3,0',
+                    'categorie' => $this->category_events,
+                    'events_debut' => '2018-02-17',
+                    'events_fin' => '2018-02-22']);
+  }
+
+
+  protected function _dispatch() {
+    $this->dispatch('/cms/ical/id_article/5');
+  }
+
+
+  public function descriptionShouldBeYoupiWithoutHtml() {
+    $this->assertContains('DESCRIPTION:youpi & oui c\'est beau !', $this->_ical);
+  }
+
+
+  /** @test */
+  public function xAltDescShouldBeYoupiWithHtml() {
+    $this->assertContains('X-ALT-DESC;FMTTYPE=text/html:<h3>youpi &amp\;', $this->_ical);
+  }
+
+
+  /** @test */
+  public function summaryShouldBeOpac4EnProd() {
+    $this->assertContains('SUMMARY:OPAC 4 en prod !', $this->_ical);
+  }
+}
+
+
+
+
 class CmsControllerCalendarDuplicateDayDueToHourChangeTest extends CmsControllerCalendarActionTestCase {
   public function setUp() {
     parent::setUp();
diff --git a/tests/application/modules/opac/controllers/CmsControllerTest.php b/tests/application/modules/opac/controllers/CmsControllerTest.php
index feef5c25d60d76932ef266a482d8d7b791b14e19..4f53fb037763087ea770b00d0484f6517c1245de 100644
--- a/tests/application/modules/opac/controllers/CmsControllerTest.php
+++ b/tests/application/modules/opac/controllers/CmsControllerTest.php
@@ -224,26 +224,31 @@ class CmsControllerRssWithProfileAndArticle extends AbstractControllerTestCase {
 
   }
 
+
   /** @test */
   public function channelTitleShouldBeLesDernieresNouvelles() {
     $this->assertXpathContentContains('//channel/title', 'Les dernières nouvelles');
   }
 
+
   /** @test */
   public function feteDeLaBananeTitreShouldBePresent() {
     $this->assertXpathContentContains('//channel/item[1]/title', 'La fête de la banane');
   }
 
+
   /** @test */
   public function feteLeLaBananeDescriptionShouldBeQuiGlisse() {
     $this->assertXpathContentContains("//channel/item[1]/description", 'Une fête qui glisse !');
   }
 
+
   /** @test */
   public function feteDeLaFriteShouldBePresent() {
     $this->assertXpathContentContains('//channel/item[2]/title', 'La fête de la frite');
   }
 
+
   /** @test */
   public function feteLeLaFriteDescriptionShouldBeQuiSent() {
     $this->assertXpathContentContains('//channel/item[2]/description', 'Une fête qui sent !');
@@ -474,7 +479,8 @@ class CmsControllerArticleViewByDateTest extends AbstractCmsControllerArticleVie
 
   /** @test */
   public function dateForFeteDeLaBananeShouldBePresent() {
-    $this->assertXpathContentContains('//ul//li//span', 'Du samedi 03 septembre au lundi 03 octobre');
+    $this->assertXpathContentContains('//ul//li//span',
+                                      'Du samedi 03 septembre au lundi 03 octobre');
   }
 
 
@@ -725,12 +731,19 @@ class CmsControllerArticleViewByDateWithRadioSummarySelectedTest extends Abstrac
                                       'La fête de la banane');
   }
 
+
   /** @test */
   public function articleTextLaFeteDeLaBananeShouldContainsSummary() {
     $this->assertXpathContentContains('//article/div[@class="article_content"]', 'Pas qu\'à moitié');
   }
 
 
+  /** @test */
+  public function pageShouldNotContainsLinkToIcal() {
+    $this->assertNotXPath('//a[contains(@href, "/cms/ical")]');
+  }
+
+
   /** @test */
   public function articleTextLaFeteDeLaBananeShouldNotContainsFullArticle() {
     $this->assertNotXpathContentContains('//article', ' Une fête qui glisse !', $this->_response->getBody());
@@ -797,10 +810,18 @@ class CmsControllerArticleViewByDateWithRadioFullArticleSelectedTest extends Abs
     $this->assertXpathContentContains('//h1', 'La fête de la banane');
   }
 
+
   /** @test */
   public function articleFullTextLaFeteDeLaBananeShouldContainsUneFeteQuiGlisse() {
     $this->assertXpathContentContains('//div', 'Une fête qui glisse !');
   }
+
+
+  /** @test */
+  public function feteDeLaBananeShouldContainsLinkToIcal() {
+    $this->assertXpathContentContains('//div[@class="ical"]//a[contains(@href, "/cms/ical/id_article/1")]',
+                                      'Ajouter au calendrier');
+  }
 }
 
 
@@ -927,14 +948,14 @@ class CmsControllerViewNoticeMetasTest extends CmsControllerWithFeteDeLaFriteTes
 
 class CmsControllerArticleViewInEnglishTest extends CmsControllerWithFeteDeLaFriteTestCase {
   /** @test */
-  function withLanguageEnShouldReturnEnglishTranslation() {
+  public function withLanguageEnShouldReturnEnglishTranslation() {
     $this->dispatch('/cms/articleview/id/224/language/en');
     $this->assertXpathContentContains('//h1', 'Feast of fried');
   }
 
 
   /** @test */
-  function withLanguageEnEventDateShouldBeTranslated() {
+  public function withLanguageEnEventDateShouldBeTranslated() {
     $this->dispatch('/cms/articleview/id/224/language/en', true);
     $this->assertXpathContentContains('//span[@class="calendar_event_date"]',
                                       'From Saturday 03 September to Wednesday 05 October 2011',
@@ -943,7 +964,7 @@ class CmsControllerArticleViewInEnglishTest extends CmsControllerWithFeteDeLaFri
 
 
   /** @test */
-  function withCurrentLocaleEnShouldReturnEnglishTranslation() {
+  public function withCurrentLocaleEnShouldReturnEnglishTranslation() {
     Zend_Registry::get('session')->language = 'en';
     $this->dispatch('/cms/articleview/id/224');
     $this->assertXpathContentContains('//h1', 'Feast of fried');
@@ -1150,7 +1171,7 @@ class CmsControllerArticleViewTest extends CmsControllerWithFeteDeLaFriteTestCas
 
 
   /** @test */
-  public function withCurrentLocaleEnShouldReturnEnglishTranslation() {
+  function withCurrentLocaleEnShouldReturnEnglishTranslation() {
     $this->bootstrap();
     Zend_Registry::get('session')->language = 'en';
     $this->dispatch('/cms/articleview/id/224');
@@ -1165,14 +1186,15 @@ class CmsControllerArticleViewTest extends CmsControllerWithFeteDeLaFriteTestCas
 
 
   /** @test */
-  public function divShouldContainsAdresseBonlieu() {
+  function divShouldContainsAdresseBonlieu() {
     $this->assertXPathContentContains('//div[@class="lieu"]', 'Annecy');
   }
 
 
   /** @test */
   public function divShouldContainsStaticMap() {
-    $this->assertXPath('//div[@class="lieu"]//img[contains(@src,"https://smap.afi-sa.net/staticmap.php")]');
+    $this->assertXPath('//div[@class="lieu"]//img[contains(@src,"https://smap.afi-sa.net/staticmap.php")]',
+                       $this->_response->getBody());
   }
 
 
@@ -1811,6 +1833,13 @@ class CmsControllerWithArticleWithWallKioskTest extends CmsControllerWithArticle
   }
 
 
+  /** @test */
+  public function pageShouldNotContainsLinkToIcal() {
+    $this->assertNotXPath('//a[contains(@href, "/cms/ical")]');
+  }
+
+
+
   /** @test */
   public function pageShouldContainsKiosk() {
     $this->assertXPath( '//div[@class="liste_mur"]');;
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index bb3c8304f5c2faff74eb358d6853c6eabc3f206d..985f82c0234a9aa4e6df58cab02c002da82a376e 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -62,7 +62,7 @@ $_SERVER['HTTP_HOST'] = 'localhost';
 
 setupOpac();
 
-ZendAfi_Controller_Action_Helper_RenderIcal::ensureAutoload();
+ZendAfi_Controller_Action_Helper_RenderIcalArticles::ensureAutoload();
 require_once __DIR__ . '/../library/PhpParser/lib/bootstrap.php';
 
 (new Storm_Cache())->getCache()->setOption('caching', true);
diff --git a/tests/library/ZendAfi/View/Helper/Accueil/CalendarTest.php b/tests/library/ZendAfi/View/Helper/Accueil/CalendarTest.php
index c475ff9c8c695a73dd64d8721a7c0cbf2489095a..3d721cc059589220f2a8432f214b20dda19f7f02 100644
--- a/tests/library/ZendAfi/View/Helper/Accueil/CalendarTest.php
+++ b/tests/library/ZendAfi/View/Helper/Accueil/CalendarTest.php
@@ -648,34 +648,37 @@ class CalendarWithWorkFlowStatusTest  extends CalendarWithEmptyPreferencesTestCa
 }
 
 
-class CalendarWithCategorySelectorAndRssPreferencesTest extends CalendarViewHelperTestCase {
+class CalendarWithCategorySelectorAndRssAndICalPreferencesTest extends CalendarViewHelperTestCase {
   public function setUp() {
     parent::setUp();
 
-    $params = array('division' => '2',
-                    'type_module' => 'CALENDAR',
-                    'preferences' => array('titre' => 'Concerts !',
-                                           'rss_avis' => '1',
-                                           'id_categorie' => '1-12-23',
-                                           'display_cat_select' => '1',
-                                           'display_event_info' => 'none',
-                                           'select_id_categorie' => '12',
-                                           'display_date' => '2011-12-25'));
+    $params = ['division' => '2',
+               'type_module' => 'CALENDAR',
+               'preferences' => ['titre' => 'Concerts !',
+                                 'rss_avis' => '1',
+                                 'ical_feed' => 1,
+                                 'id_categorie' => '1-12-23',
+                                 'display_cat_select' => '1',
+                                 'display_event_info' => 'none',
+                                 'select_id_categorie' => '12',
+                                 'display_date' => '2011-12-25']];
     $helper = new ZendAfi_View_Helper_Accueil_Calendar(2, $params);
     $helper->setView(new ZendAfi_Controller_Action_Helper_View());
 
     Storm_Test_ObjectWrapper::onLoaderOfModel('Class_Article')
       ->whenCalled('getArticlesByPreferences')
-      ->with(array(
-                   'display_order' => 'EventDebut',
-                   'id_categorie' => '12',
-                   'event_date' => '2011-12',
-                   'id_bib' => 0,
-                   'id_lieu' => '',
-                   'custom_fields' => [],
-                   'events_only' => true,
-                   'published' => false))
-      ->answers(array($this->nanook2, $this->opac4, $this->amber))
+      ->with([
+              'display_order' => 'EventDebut',
+              'id_categorie' => '12',
+              'event_date' => '2011-12',
+              'id_bib' => 0,
+              'id_lieu' => '',
+              'custom_fields' => [],
+              'events_only' => true,
+              'published' => false])
+      ->answers([$this->nanook2,
+                 $this->opac4,
+                 $this->amber])
 
       ->whenCalled('getArticlesByPreferences')
       ->with([
@@ -688,7 +691,9 @@ class CalendarWithCategorySelectorAndRssPreferencesTest extends CalendarViewHelp
               'custom_fields' => [],
               'events_only' => true,
               'published' => false])
-      ->answers(array($this->nanook2, $this->opac4, $this->amber))
+      ->answers([$this->nanook2,
+                 $this->opac4,
+                 $this->amber])
 
 
       ->beStrict();
@@ -705,6 +710,14 @@ class CalendarWithCategorySelectorAndRssPreferencesTest extends CalendarViewHelp
   }
 
 
+  /** @test */
+  public function linkToCmsICalActionShouldBeVisible() {
+    $this->assertXPath($this->html,
+                       '//a[contains(@href, "/cms/ical?id_module=2&id_profil=2&language=")]',
+                       $this->html);
+  }
+
+
   /** @test */
   public function shouldContainsCategorySelector() {
     $this->assertXPath( $this->html,