From 20c494e93bae171b595178e8c68ca596cb649a5e Mon Sep 17 00:00:00 2001
From: Patrick Barroca <pbarroca@afi-sa.fr>
Date: Fri, 4 Jun 2021 18:52:22 +0200
Subject: [PATCH] hotline #125173 : enhance forms moderation

---
 VERSIONS_HOTLINE/125173                       |   1 +
 .../admin/controllers/ModoController.php      |  71 +-
 .../scripts/modo/articlesformulaires.phtml    |  38 +-
 .../scripts/modo/choose-form-columns.phtml    |   2 +
 .../views/scripts/modo/formulaires.phtml      | 116 ++--
 .../admin/views/scripts/modo/index.phtml      |  27 +-
 .../modo/visualiser-reponse-ajax.phtml        |   2 +
 library/Class/Dict.php                        |  66 ++
 library/Class/Formulaire.php                  |  36 +-
 .../TableDescription/ArticleFormResponse.php  |  70 ++
 library/Class/User/Settings.php               |  26 +-
 library/Class/Users.php                       |  10 +
 .../Form/Admin/ModoFormulaireColumns.php      | 111 +++
 .../ZendAfi/Form/ReponseFormulaireMail.php    |  32 +-
 library/ZendAfi/Mail.php                      |   5 +
 .../ZendAfi/View/Helper/ReponseFormulaire.php | 131 ++--
 .../View/Helper/ReponseFormulaireFilled.php   |  31 +-
 .../ModoControllerFormulaireTest.php          | 646 ++++++++++++------
 .../View/Helper/ReponseFormulaireTest.php     | 199 +++---
 19 files changed, 1085 insertions(+), 535 deletions(-)
 create mode 100644 VERSIONS_HOTLINE/125173
 create mode 100644 application/modules/admin/views/scripts/modo/choose-form-columns.phtml
 create mode 100644 application/modules/admin/views/scripts/modo/visualiser-reponse-ajax.phtml
 create mode 100644 library/Class/Dict.php
 create mode 100644 library/Class/TableDescription/ArticleFormResponse.php
 create mode 100644 library/ZendAfi/Form/Admin/ModoFormulaireColumns.php

diff --git a/VERSIONS_HOTLINE/125173 b/VERSIONS_HOTLINE/125173
new file mode 100644
index 00000000000..4998c3c8c87
--- /dev/null
+++ b/VERSIONS_HOTLINE/125173
@@ -0,0 +1 @@
+ - ticket #125173 : Modération : amélioration de l'écran de modération des formulaires
\ No newline at end of file
diff --git a/application/modules/admin/controllers/ModoController.php b/application/modules/admin/controllers/ModoController.php
index e431ae085e6..aa20b53d765 100644
--- a/application/modules/admin/controllers/ModoController.php
+++ b/application/modules/admin/controllers/ModoController.php
@@ -837,32 +837,59 @@ class Admin_ModoController extends ZendAfi_Controller_Action {
 
 
   public function formulairesAction() {
-    $article = Class_Article::find($this->_getParam('id_article'));
-    if ($article)
-      $this->view->subview = $this->view->partial(
-        'modo/formulaires.phtml',
-        ['formulaires' =>
-         ($this->_getParam('liste')==='all')
-         ? $article->getFormulaires()
-         : $article->getFormulairesToValidate(),
-         'article' => $article,
-         'liste' =>  $this->_getParam('liste')]);
-    else
-      $this->view->subview = $this->view->partial('modo/articlesformulaires.phtml',
-                                                  ['articles' => Class_Article::articlesWithFormulaire()]);
+    if (!$article = Class_Article::find($this->_getParam('id_article'))) {
+      $this->view->subview = $this->view
+        ->partial('modo/articlesformulaires.phtml',
+                  ['articles' => Class_Article::articlesWithFormulaire()]);
+      return $this->_forward('index');
+    }
+
+    $forms = 'all' === $this->_getParam('liste')
+      ? $article->getFormulaires()
+      : $article->getFormulairesToValidate();
+
+    $this->view->subview = $this->view->partial('modo/formulaires.phtml',
+                                                ['formulaires' => $forms,
+                                                 'article' => $article,
+                                                 'liste' =>  $this->_getParam('liste')]);
 
     $this->_forward('index');
   }
 
 
+  public function chooseFormColumnsAction() {
+    $user = Class_Users::getIdentity();
+    session_write_close();
+
+    if (!$article = Class_Article::find((int)$this->_getParam('id'))) {
+      $this->_helper->notify($this->_('Formulaire introuvable'));
+      return $this->_redirectCloseReferer();
+    }
+
+    $form = new ZendAfi_Form_Admin_ModoFormulaireColumns(['action' => $this->view->url(),
+                                                          'article' => $article]);
+
+    if ($this->_request->isPost()
+        && $form->isValid($this->_request->getPost())) {
+      Class_Users::getIdentity()
+        ->setFormColumns($article->getId(),
+                         array_filter(explode(';', $form->getValue('columns'))))
+        ->save();
+
+      $this->_helper->notify($this->_('Choix des colonnes enregistré'));
+      return $this->_redirectCloseReferer();
+    }
+
+
+    $this->view->titre = $this->_('Choisir les colonnes à afficher');
+    $this->view->form = $form;
+  }
+
+
   public function visualiserReponseAjaxAction() {
     $formulaire = Class_Formulaire::find($this->_getParam('id'));
-    $article = Class_Article::find($this->_getParam('id_article'));
-
-    $this->renderPopupResult($this->view->_('Réponse au formulaire: ').$article->getTitre(),
-                             $this->view->reponseFormulaire($formulaire),
-                             ['width' => '400',
-                              'show_modal' => 'false']);
+    $this->view->titre = $this->_('Réponse au formulaire: %s', $formulaire->getArticleTitle());
+    $this->view->formulaire = $formulaire;
   }
 
 
@@ -902,7 +929,8 @@ class Admin_ModoController extends ZendAfi_Controller_Action {
     $this->getHelper('ViewRenderer')->setNoRender();
     $formulaire='';
 
-    if($this->_request->isPost() && $formulaire = Class_Formulaire::find((int)$this->_getParam('id')) ) {
+    if ($this->_request->isPost()
+       && $formulaire = Class_Formulaire::find((int)$this->_getParam('id')) ) {
 
       $mail = new ZendAfi_Mail();
       $mail
@@ -915,7 +943,7 @@ class Admin_ModoController extends ZendAfi_Controller_Action {
 
       try {
         $mail->send();
-        $formulaire->setMailAnswer(serialize($mail))->save();
+        $formulaire->setMailAnswer($mail)->save();
         $this->_helper->notify($this->view->_('Courriel envoyé à: ').$this->_request->getPost('mail_destinataire'));
 
       } catch (Zend_Mail_Exception $e) {
@@ -932,7 +960,6 @@ class Admin_ModoController extends ZendAfi_Controller_Action {
                                          'action' => 'visualiser-reponse-ajax',
                                          'id_article' => $formulaire->getArticle()->getId(),
                                          'id' => $this->_getParam('id')]));
-
   }
 
 
diff --git a/application/modules/admin/views/scripts/modo/articlesformulaires.phtml b/application/modules/admin/views/scripts/modo/articlesformulaires.phtml
index 6339ef59ab3..b4e7612e3e0 100644
--- a/application/modules/admin/views/scripts/modo/articlesformulaires.phtml
+++ b/application/modules/admin/views/scripts/modo/articlesformulaires.phtml
@@ -1,23 +1,21 @@
 <?php
-$items = [];
-$editArticleHelper = $this->getHelper('tagEditArticle');
-foreach ($this->articles as $article) {
-  $modo_url = $this->url(['module' => 'admin',
-                          'controller' => 'modo',
-                          'action' => 'formulaires',
-                          'id_article' => $article->getId()],
-                         null, true);
-  $modo_label = sprintf('%s [%d/%d]',
-                        $article->getTitre(),
-                        $article->numberOfFormulairesToValidate(),
-                        $article->numberOfFormulaires());
+echo $this->tag('h1', $this->_('Modération des formulaires'));
 
-  $items[] = $this
-    ->tag('li',
-          $this->tag('a', $modo_label,
-                     ['href' => $modo_url])
-          . $editArticleHelper->renderEdit($article)
-    );
-}
+$description = (new Class_TableDescription('articles'))
+  ->addColumn($this->_('Titre'), 'titre')
+  ->addColumn($this->_('À valider / Total'),
+              ['callback' => function($model)
+               {
+                 return sprintf('%d / %d',
+                                $model->numberOfFormulairesToValidate(),
+                                $model->numberOfFormulaires());
+               }])
+  ->addRowAction(['url' => '/admin/modo/formulaires/id_article/%s',
+                  'label' => $this->_('Voir les réponses'),
+                  'icon' => 'view'])
+  ->addRowAction(function($model)
+                 {
+                   return $this->tagEditArticle($model);
+                 });
 
-echo $this->tag('ul', implode('', $items));
+echo $this->renderTable($description, $this->articles);
diff --git a/application/modules/admin/views/scripts/modo/choose-form-columns.phtml b/application/modules/admin/views/scripts/modo/choose-form-columns.phtml
new file mode 100644
index 00000000000..a0dcb1c119d
--- /dev/null
+++ b/application/modules/admin/views/scripts/modo/choose-form-columns.phtml
@@ -0,0 +1,2 @@
+<?php
+echo $this->renderForm($this->form);
\ No newline at end of file
diff --git a/application/modules/admin/views/scripts/modo/formulaires.phtml b/application/modules/admin/views/scripts/modo/formulaires.phtml
index 288ddfd5ae9..10eec9a12c7 100644
--- a/application/modules/admin/views/scripts/modo/formulaires.phtml
+++ b/application/modules/admin/views/scripts/modo/formulaires.phtml
@@ -1,70 +1,48 @@
-<h1><?php echo $this->_('Modération des formulaires: '.$this->article->getTitre());?></h1>
 <?php
-if($this->liste==='all')
-echo $this->tagAnchor($this->url(['module'=>'admin',
-                                  'controller'=>'modo',
-                                  'action'=>'formulaires',
-                                  'id_article'=>$this->article->getId(),
-                                  'liste' => null]),
-                      $this->_('Afficher uniquement les réponses à valider'));
-else
-echo $this->tagAnchor($this->url(['module'=>'admin',
-                                  'controller'=>'modo',
-                                  'action'=>'formulaires',
-                                  'id_article'=>$this->article->getId(),
-                                  'liste'=>'all']),
-                      $this->_('Afficher toutes les réponses'));
-
-echo $this->tagAnchor($this->url(['module'=>'admin',
-                                  'controller'=>'modo',
-                                  'action'=>'export-csv-formulaire',
-                                  'id_article'=>$this->article->getId(),
-                                  'liste'=>null]),
-                      $this->_('Export CSV'));
-?>
-<div  class="table_scroll">
-
-  <?php $data_names = Class_Formulaire::mergeDataNames($this->formulaires); ?>
-
-
-  <table id="formulaires" class="models">
-    <thead>
-      <tr>
-        <th><?php echo $this->_('Date') ?></th>
-        <th><?php echo $this->_('Posté par') ?></th>
-        <th><?php echo $this->_('Bibliothèque') ?></th>
-        <?php foreach($data_names as $name) echo '<th>'.$name.'</th>'; ?>
-          <th><?php echo $this->_('Actions') ?></th>
-      </tr>
-    </thead>
-    <tbody>
-      <?php
-      foreach($this->formulaires as $formulaire) {
-        $datas = [$this->humanDate($formulaire->getDateCreation(), 'dd/MM/yyyy'),
-                  $formulaire->getCompte(),
-                  $formulaire->getLibelleBib()];
-        foreach($data_names as $name)
-        $datas[]=$formulaire->getDataNamed($name);
-
-        echo '<tr>';
-        foreach($datas as $data) echo '<td>' . $this->escape($data) . '</td>';
-
-        echo '<td>';
-        echo $this->tagAnchor(['action' => 'visualiser-reponse-ajax',
-                               'id' => $formulaire->getId()], $this->boutonIco('type=show'),
-                              ['data-popup'=>'true']);
-        echo $this->tagAnchor(['action' => 'delete-formulaire',
-                               'id' => $formulaire->getId()], $this->boutonIco('type=del'));
-        if (!$formulaire->isValidated())
-        echo $this->tagAnchor(['action' => 'validate-formulaire',
-                               'id' => $formulaire->getId()],
-                              $this->boutonIco('type=validate'));
-
-        echo '</td>';
-        echo '</tr>';
-      }
-      ?>
-    </tbody>
-  </table>
-
-</div>
+$current_skin = Class_Admin_Skin::current();
+
+echo $this->tag('h1', $this->_('Modération des formulaires: %s',
+                               $this->article->getTitre()));
+
+$id_article = $this->article->getId();
+
+$params = ['module' => 'admin',
+           'controller' => 'modo',
+           'action' => 'formulaires',
+           'id_article' => $id_article,
+           'liste' => ('all' === $this->liste) ? null: 'all',
+           ];
+
+$label = ('all' === $this->liste)
+  ? $this->_('Afficher uniquement les réponses à valider')
+  : $this->_('Afficher toutes les réponses');
+
+echo $this->button((new Class_Entity)
+                   ->setText($label)
+                   ->setUrl($this->url($params))
+                   ->setImage($this->tagImg($current_skin->getIconUrl('actions', 'view'),
+                                            ['style' => 'filter: invert();'])));
+
+echo $this->button((new Class_Entity)
+                   ->setText($this->_('Tout exporter au format CSV'))
+                   ->setUrl($this->url(['action' => 'export-csv-formulaire',
+                                        'id_article' => $id_article,
+                                        'liste' => null]))
+                   ->setImage($this->tagImg($current_skin->getIconUrl('actions', 'shopping'),
+                                            ['style' => 'filter: invert();'])));
+
+echo $this->button((new Class_Entity)
+                   ->setText($this->_('Choisir les colonnes à afficher'))
+                   ->setUrl($this->url(['module' => 'admin',
+                                        'controller' => 'modo',
+                                        'action' => 'choose-form-columns',
+                                        'id' => $id_article],
+                                       null, true))
+                   ->setImage($this->tagImg($current_skin->getIconUrl('actions', 'box_configuration'),
+                                            ['style' => 'filter: invert();']))
+                   ->setAttribs(['data-popup' => 'true']));
+
+$description = (new Class_TableDescription_ArticleFormResponse('responses'))
+  ->setArticle($this->article);
+
+echo $this->renderTable($description, $this->formulaires);
diff --git a/application/modules/admin/views/scripts/modo/index.phtml b/application/modules/admin/views/scripts/modo/index.phtml
index 6f638794f06..2bbce7481e0 100644
--- a/application/modules/admin/views/scripts/modo/index.phtml
+++ b/application/modules/admin/views/scripts/modo/index.phtml
@@ -25,16 +25,19 @@ $menus = [["icon" => "articles",
           ];
 
 if (isset($modstats['formulaires'])) {
-  $menus[] =
-  ["icon" => "formulaires_16.png",
-   "label" => $this->_('Formulaires'),
-   'title' => $this->_('Réponses aux formulaires dans les articles'),
-   "url" => $this->url(['action' => 'formulaires', 'id_article' => null, 'status' => null, 'active_tab' => null, 'page' => null]),
-   "count" => $modstats['formulaires']['count']]
-  ;}
-?>
-<div class="menu"><?php echo $this->getHelper('Admin_Nav')->generateMenu($menus);?></div>
+  $menus[] = ["icon" => "formulaires_16.png",
+              "label" => $this->_('Formulaires'),
+              'title' => $this->_('Réponses aux formulaires dans les articles'),
+              "url" => $this->url(['action' => 'formulaires', 'id_article' => null, 'status' => null, 'active_tab' => null, 'page' => null]),
+              "count" => $modstats['formulaires']['count']];
+}
 
-<?php if ($this->subview) { ?>
-  <div class="subview"><?php echo $this->subview;?><div class="clear"></div></div>
-<?php } ?>
+
+echo $this->tag('div',
+                $this->getHelper('Admin_Nav')->generateMenu($menus),
+                ['class' => 'menu']);
+
+if ($this->subview)
+  echo $this->tag('div',
+                  $this->subview . $this->tag('div', '', ['class' => 'clear']),
+                  ['class' => 'subview']);
diff --git a/application/modules/admin/views/scripts/modo/visualiser-reponse-ajax.phtml b/application/modules/admin/views/scripts/modo/visualiser-reponse-ajax.phtml
new file mode 100644
index 00000000000..bf6f3bba1ce
--- /dev/null
+++ b/application/modules/admin/views/scripts/modo/visualiser-reponse-ajax.phtml
@@ -0,0 +1,2 @@
+<?php
+echo $this->reponseFormulaire($this->formulaire);
\ No newline at end of file
diff --git a/library/Class/Dict.php b/library/Class/Dict.php
new file mode 100644
index 00000000000..8eb7bfb8742
--- /dev/null
+++ b/library/Class/Dict.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * Copyright (c) 2012-2021, 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 Class_Dict {
+  protected $_datas;
+
+  public static function pairsFrom($datas) {
+    return (new static($datas))->pairs();
+  }
+
+
+  public function __construct($datas=[]) {
+    $this->_datas = $datas;
+  }
+
+
+  public function pairs() {
+    return array_map(function($key, $value)
+                     {
+                       return new Class_Dict_Pair($key, $value);
+                     },
+                     array_keys($this->_datas),
+                     $this->_datas);
+  }
+}
+
+
+
+
+class Class_Dict_Pair {
+  protected $_key, $_value;
+
+  public function __construct($key, $value) {
+    $this->_key = $key;
+    $this->_value = $value;
+  }
+
+
+  public function getKey() {
+    return $this->_key;
+  }
+
+
+  public function getValue() {
+    return $this->_value;
+  }
+}
diff --git a/library/Class/Formulaire.php b/library/Class/Formulaire.php
index 9f06f343a71..edce7210dbf 100644
--- a/library/Class/Formulaire.php
+++ b/library/Class/Formulaire.php
@@ -55,6 +55,13 @@ class Class_Formulaire extends Storm_Model_Abstract {
   }
 
 
+  public function getArticleTitle() {
+    return ($article = $this->getArticle())
+      ? $article->getTitre()
+      : '';
+  }
+
+
   /*
    * @return ZendAfi_Mail
    */
@@ -69,7 +76,7 @@ class Class_Formulaire extends Storm_Model_Abstract {
 
 
   public function getMailSubject () {
-    return $this->getMail()->getSubject();
+    return $this->getMail()->getDecodedSubject();
   }
 
 
@@ -140,6 +147,11 @@ class Class_Formulaire extends Storm_Model_Abstract {
   }
 
 
+  public function getDateCreationText() {
+    return strftime('%d/%m/%Y', strtotime($this->getDateCreation()));
+  }
+
+
   public function getCompte() {
     if ($this->hasUser())
       return $this->getUser()->getNomComplet();
@@ -148,10 +160,16 @@ class Class_Formulaire extends Storm_Model_Abstract {
 
 
   public function getLibelleBib() {
-    if ($user = $this->getUser())
-      return $user->getLibelleBib();
+    return ($user = $this->getUser())
+      ? $user->getLibelleBib()
+      : '';
+  }
 
-    return '';
+
+  public function getUserMail() {
+    return ($user = $this->getUser())
+      ? $user->getMail()
+      : '';
   }
 
 
@@ -176,8 +194,16 @@ class Class_Formulaire extends Storm_Model_Abstract {
 
   public function anonymize() {
     if ($mail = $this->getMail())
-      $this->setMailAnswer(serialize($mail->clearRecipients()));
+      $this->setMailAnswer($mail->clearRecipients());
 
     return $this;
   }
+
+
+  public function setMailAnswer($mail_or_string) {
+    if (!is_string($mail_or_string))
+      $mail_or_string = serialize($mail_or_string);
+
+    return $this->_set('mail_answer', $mail_or_string);
+  }
 }
diff --git a/library/Class/TableDescription/ArticleFormResponse.php b/library/Class/TableDescription/ArticleFormResponse.php
new file mode 100644
index 00000000000..366bf3b7f08
--- /dev/null
+++ b/library/Class/TableDescription/ArticleFormResponse.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * Copyright (c) 2012-2021, 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 Class_TableDescription_ArticleFormResponse extends Class_TableDescription {
+  protected $_article;
+
+  public function setArticle($article) {
+    $this->_article = $article;
+    return $this->_addChosenColumns();
+  }
+
+
+  public function init() {
+    $this
+      ->addRowAction(['url' => '/admin/modo/visualiser-reponse-ajax/id/%s',
+                      'label' => $this->_('Voir les détails de la réponse'),
+                      'icon' => 'view',
+                      'anchorOptions' => ['data-popup' => 'true']])
+
+      ->addRowAction(['url' => '/admin/modo/delete-formulaire/id/%s',
+                      'label' => $this->_('Supprimer la réponse'),
+                      'icon' => 'delete',
+                      'anchorOptions' => ['onclick' => 'return confirm($(this).find(\'img\').attr(\'alt\') + \' ?\');']])
+
+      ->addRowAction(['url' => '/admin/modo/validate-formulaire/id/%s',
+                      'label' => $this->_('Valider la réponse'),
+                      'icon' => 'validate',
+                      'condition' => function($model) { return !$model->isValidated(); } ])
+      ;
+  }
+
+
+  protected function _addChosenColumns() {
+    if ($this->_article && ($user = Class_Users::getIdentity())) {
+      foreach(array_reverse($user->getFormColumns($this->_article->getId())) as $column)
+        $this->prependColumn($column, ['callback' => [$this, 'renderChosenColumn'],
+                                       'attribute' => $column]);
+    }
+
+    return $this
+      ->prependColumn($this->_('Bibliothèque'), 'libelle_bib')
+      ->prependColumn($this->_('Posté par'), 'compte')
+      ->prependColumn($this->_('Date'), 'date_creation_text')
+      ;
+  }
+
+
+  public function renderChosenColumn($model, $attribute, $canvas) {
+    return $canvas->getView()->escape($model->getDataNamed($attribute));
+  }
+}
diff --git a/library/Class/User/Settings.php b/library/Class/User/Settings.php
index 377e7fa133b..89e8dfffb85 100644
--- a/library/Class/User/Settings.php
+++ b/library/Class/User/Settings.php
@@ -33,7 +33,8 @@ class Class_User_Settings {
     PROFILE_IMAGE = 'profile_image',
     SEARCH_ORDER = 'search_order',
     SEARCH_LAYOUT = 'search_layout',
-    SEARCH_PAGE_SIZE = 'search_page_size';
+    SEARCH_PAGE_SIZE = 'search_page_size',
+    FORM_COLUMNS = 'form_columns';
 
   protected static
     $_cache = [],
@@ -380,7 +381,28 @@ class Class_User_Settings {
 
   public function setSearchPageSize($size) {
     return $this->set(static::SEARCH_PAGE_SIZE, $size);
-   }
+  }
+
+
+  public function getFormColumns($article_id) {
+    if (!$columns = $this->get(static::FORM_COLUMNS))
+      return [];
+
+    return isset($columns[$article_id])
+      ? $columns[$article_id]
+      : [];
+  }
+
+
+  public function setFormColumns($article_id, $columns) {
+    if (!$current = $this->get(static::FORM_COLUMNS))
+      $current = [];
+
+    $current[$article_id] = $columns;
+    $this->set(static::FORM_COLUMNS, $current);
+
+    return $this->_user;
+  }
 
 
   protected function _initDefaultProfileImage() {
diff --git a/library/Class/Users.php b/library/Class/Users.php
index d8e029645dc..1a40bbf053d 100644
--- a/library/Class/Users.php
+++ b/library/Class/Users.php
@@ -1834,6 +1834,16 @@ class Class_Users extends Storm_Model_Abstract {
   }
 
 
+  public function getFormColumns($article_id) {
+    return $this->getSettingsModel()->getFormColumns($article_id);
+  }
+
+
+  public function setFormColumns($article_id, $columns) {
+    return $this->getSettingsModel()->setFormColumns($article_id, $columns);
+  }
+
+
   protected function getSettingsModel() {
     return Class_User_Settings::newWith($this);
   }
diff --git a/library/ZendAfi/Form/Admin/ModoFormulaireColumns.php b/library/ZendAfi/Form/Admin/ModoFormulaireColumns.php
new file mode 100644
index 00000000000..b9c2bcd0554
--- /dev/null
+++ b/library/ZendAfi/Form/Admin/ModoFormulaireColumns.php
@@ -0,0 +1,111 @@
+<?php
+/**
+ * Copyright (c) 2012-2021, 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_Form_Admin_ModoFormulaireColumns extends ZendAfi_Form {
+  protected $_article;
+
+  public function setArticle($article) {
+    $this->_article = $article;
+  }
+
+
+  public function init() {
+    parent::init();
+    $this
+      ->addElement('dragAndDrop', 'columns',
+                   ['label' => '',
+                    'value' => ($this->_article && ($user = Class_Users::getIdentity())
+                                ? implode(';', $user->getFormColumns($this->_article->getId()))
+                                : ''),
+                    'entityFactory' => function($value) {
+                       $selected_columns = $this->_selectedColumnsFrom($value);
+                       $available_columns = $this->_availableColumnsWithout($selected_columns);
+
+                       return new Class_Entity(['Id' => 'columns',
+                                                'AvailableHeader' => $this->_('Colonnes disponibles'),
+                                                'SelectedHeader' => $this->_('Colonnes activées'),
+                                                'Available' => $available_columns,
+                                                'Selected' => $selected_columns]);
+                    }])
+
+      ->addUniqDisplayGroup('default', ['legend' => $this->_('Sélection')]);
+  }
+
+
+  protected function _selectedColumnsFrom($value) {
+    return $this->_columns(explode(';', $value));
+  }
+
+
+  protected function _availableColumnsWithout($selected_columns) {
+    if (!$this->_article)
+      return [];
+
+    $data_names = Class_Formulaire::mergeDataNames($this->_article->getFormulaires());
+    if (!$available_columns = $this->_columns($data_names))
+      return [];
+
+    $selected_ids = (new Storm_Collection($selected_columns))
+      ->collect(function($selected_item)
+                {
+                  return $selected_item->getId();
+                });
+
+    return array_filter($available_columns,
+                        function($item) use($selected_ids)
+                        {
+                          return ! $selected_ids->includes($item->getId());
+                        });
+  }
+
+
+  protected function _columns($columns) {
+    return array_filter(array_map(function($name)
+                                  {
+                                    return $name
+                                      ? new ZendAfi_Form_Admin_ModoFormulaireColumns_Column($name)
+                                      : null;
+                                  },
+                                  $columns));
+  }
+}
+
+
+
+
+class ZendAfi_Form_Admin_ModoFormulaireColumns_Column {
+  protected $_id;
+
+  public function __construct($id) {
+    $this->_id = $id;
+  }
+
+
+  public function getId() {
+    return $this->_id;
+  }
+
+
+  public function getLabel() {
+    return $this->getId();
+  }
+}
\ No newline at end of file
diff --git a/library/ZendAfi/Form/ReponseFormulaireMail.php b/library/ZendAfi/Form/ReponseFormulaireMail.php
index 50f30941025..ebb02bd08b1 100644
--- a/library/ZendAfi/Form/ReponseFormulaireMail.php
+++ b/library/ZendAfi/Form/ReponseFormulaireMail.php
@@ -16,7 +16,7 @@
  *
  * 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 
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
 
 class ZendAfi_Form_ReponseFormulaireMail extends ZendAfi_Form {
@@ -25,34 +25,36 @@ class ZendAfi_Form_ReponseFormulaireMail extends ZendAfi_Form {
     parent::init();
     $this
       ->setName('reponse_formulaire_mail')
-      ->addElement('text', 
+      ->addElement('text',
                    'mail_expediteur',
                    ['label' => $this->_("De"),
                     'allowEmpty' => false,
                     'required' => true])
-      ->addElement('text', 
-                   'mail_destinataire', 
+
+      ->addElement('text',
+                   'mail_destinataire',
                    ['label' => $this->_("A"),
                     'allowEmpty' => false,
                     'required' => true])
-      ->addElement('text', 
-                   'sujet', 
+
+      ->addElement('text',
+                   'sujet',
                    ['label' => $this->_("Sujet"),
                     'allowEmpty' => false,
                     'required' => true])
-      ->addElement('textarea', 
-                   'reponse', 
+
+      ->addElement('textarea',
+                   'reponse',
                    ['label'=> $this->_('Réponse: '),
                     'placeholder' => 'ex: Merci pour vos encouragements.',
                     'allowEmpty' => false,
+                    'required' => true,
                     'cols' => 35,
                     'rows' => 10])
-      ->addElement('submit', 
-                   'envoyer', 
-                   ['label' => $this->_('Envoyer')]);
-    
+
+      ->addElement('submit',
+                   'envoyer',
+                   ['label' => $this->_('Envoyer')])
+      ;
   }
-  
 }
-
-?>
\ No newline at end of file
diff --git a/library/ZendAfi/Mail.php b/library/ZendAfi/Mail.php
index 9d306266952..8aec63b477f 100644
--- a/library/ZendAfi/Mail.php
+++ b/library/ZendAfi/Mail.php
@@ -72,4 +72,9 @@ class ZendAfi_Mail extends Zend_Mail {
     $this->_headers['To'] = [];
     return $this;
   }
+
+
+  public function getDecodedSubject() {
+    return iconv_mime_decode($this->getSubject());
+  }
 }
diff --git a/library/ZendAfi/View/Helper/ReponseFormulaire.php b/library/ZendAfi/View/Helper/ReponseFormulaire.php
index e8847bdf621..5b9321bc067 100644
--- a/library/ZendAfi/View/Helper/ReponseFormulaire.php
+++ b/library/ZendAfi/View/Helper/ReponseFormulaire.php
@@ -16,107 +16,124 @@
  *
  * 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 
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
 
-class ZendAfi_View_Helper_ReponseFormulaire extends Zend_View_Helper_HtmlElement {
+class ZendAfi_View_Helper_ReponseFormulaire extends ZendAfi_View_Helper_BaseHelper {
 
   protected $_formulaire;
 
   public function reponseFormulaire($formulaire) {
     $this->_formulaire = $formulaire;
-    $html='';
-    $html.= $this->buildReponseContent($this->_formulaire->getDatas());
-    $html.= $this->addMailFormulaire();
-    $html.= $this->addActionButton();
-    $html.= $this->addScripts();
 
-    return $html;
+    return $this->buildReponseContent($this->_formulaire->getDatas())
+      . $this->addMailFormulaire()
+      . $this->addActionButton()
+      . $this->addScripts();
   }
 
+
   public function buildReponseContent($datas) {
-    $html='<dl>';
-    foreach($datas as $name => $value){
-      $html.= '<dt>'.$name.'</dt>';
-      $html.= '<dd>'.nl2br($value).'</dd>';
-    }
-    return $html.='</dl>';
+    $description = (new Class_TableDescription('response'))
+      ->addColumn($this->_('Champs'),
+                  ['callback' => function($model) {
+                      return $this->_tag('strong',
+                                         $this->view->escape($model->getKey()));
+                    },
+                   'sortable' => false])
+      ->addColumn($this->_('Valeurs'),
+                  ['callback' => function($model) {
+                      return nl2br($this->view->escape($model->getValue()));
+                    },
+                   'sortable' => false]);
+
+    return $this->view->renderTable($description, Class_Dict::pairsFrom($datas));
   }
-  
+
 
   public function addActionButton() {
-    $html='<div class="action-button">';
-    $html.= $this->view->tagAnchor(['action' => 'delete-formulaire',
-                                    'id' => $this->_formulaire->getId()], $this->view->boutonIco('type=del'));
+    $current_skin = Class_Admin_Skin::current();
+    $buttons =
+      [$this->view->button((new Class_Entity)
+                           ->setText($this->_('Supprimer'))
+                           ->setUrl($this->_url(['action' => 'delete-formulaire',
+                                                 'id' => $this->_formulaire->getId()]))
+                           ->setImage($this->view
+                                      ->tagImg($current_skin->getIconUrl('actions', 'delete'),
+                                               ['style' => 'filter: invert();'])))
+    ];
+
     if (!$this->_formulaire->isValidated())
-      $html.= $this->view->tagAnchor(['action' => 'validate-formulaire',
-                                      'id' => $this->_formulaire->getId()],
-                                     $this->view->boutonIco('type=validate'));
-    return $html.='</div>';
+      $buttons[] = $this->view->button((new Class_Entity)
+                           ->setText($this->_('Valider'))
+                           ->setUrl($this->_url(['action' => 'validate-formulaire',
+                                                 'id' => $this->_formulaire->getId()]))
+                           ->setImage($this->view
+                                      ->tagImg($current_skin->getIconUrl('actions', 'validate'),
+                                               ['style' => 'filter: invert();'])));
+
+    return $this->_div(['class' => 'action-button'],
+                       implode($buttons));
   }
 
 
   public function addMailFormulaire() {
-    $html= ($user = $this->_formulaire->getUser()) 
+    $html= ($user = $this->_formulaire->getUser())
       ? ( ($user->getMail())
           ? $this->mailFormulaire()
-          : $this->mailFormulaire($this->view->_('L\'utilisateur n\'a pas renseigné son adresse e-mail.')))
-      : $this->mailFormulaire($this->view->_("Utilisateur introuvable"));
-        
+          : $this->mailFormulaire($this->_('L\'utilisateur n\'a pas renseigné son adresse e-mail.')))
+      : $this->mailFormulaire($this->_('Utilisateur introuvable'));
+
     return $html;
   }
 
 
   public function mailFormulaire($warning = '') {
-    $html = '<div id="form_accordion">';
-    $html.= ($this->_formulaire->hasMailAnswer()) 
-      ? $this->filledMailFormulaire()
-      : $this->emptyMailFormulaire($warning);
-    return $html.='</div>';
+    return $this->_div(['id' => 'form_accordion'],
+                       ($this->_formulaire->hasMailAnswer())
+                       ? $this->filledMailFormulaire()
+                       : $this->emptyMailFormulaire($warning));
   }
-  
+
 
   public function filledMailFormulaire() {
-    $html = $this->getFormLegend($this->view->_('Voir la réponse.'));
-    return $html.=$this->view->reponseFormulaireFilled($this->_formulaire);
+    return $this->getFormLegend($this->_('Voir la réponse'))
+      . $this->view->reponseFormulaireFilled($this->_formulaire);
   }
 
-  
+
   public function emptyMailFormulaire($warning) {
-    $html = $this->getFormLegend($this->view->_('Rédiger une réponse.'));
-    $html.='<div>';
-    $html.= $this->displayWarning($warning);
-    $form = new ZendAfi_Form_ReponseFormulaireMail(['action' => $this->view->url(['module' => 'admin',
-                                                                      'controller' => 'modo',
-                                                                      'action'=> 'reponse-formulaire-send-mail',
-                                                                      'id' => $this->_formulaire->getId()])]);
-    $form->populate(['mail_expediteur' => Class_Users::getIdentity()->getMail(),
-                     'mail_destinataire' => ($user = $this->_formulaire->getUser()) ? $user->getMail() : "" ,
-                     'sujet' => $this->_formulaire->getArticle()->getTitre()]);
-
-    $html .= $form->render();
-    return $html.'</div>';
+    $action = $this->view->url(['module' => 'admin',
+                                'controller' => 'modo',
+                                'action'=> 'reponse-formulaire-send-mail',
+                                'id' => $this->_formulaire->getId()]);
+
+    $form = (new ZendAfi_Form_ReponseFormulaireMail(['action' => $action]))
+      ->populate(['mail_expediteur' => Class_Users::getIdentity()->getMail(),
+                  'mail_destinataire' => $this->_formulaire->getUserMail(),
+                  'sujet' => $this->_formulaire->getArticleTitle()]);
+
+    return $this->getFormLegend($this->_('Rédiger une réponse'))
+      . $this->_div([], $this->displayWarning($warning) . $form->render());
   }
 
-  
+
   public function getFormLegend($label) {
-    return '<h3>'.$label.'</h3>';
+    return $this->_tag('h3', $label);
   }
 
-  
+
   public function displayWarning($warning) {
-    
-    return ($warning==='') ? '' : '<span>'.$warning.'</span>';
+    return $warning
+      ? $this->_tag('span', $warning)
+      : '';
   }
 
 
   public function addScripts() {
     $accordion = '$(\'#form_accordion\').accordion({collapsible: true, active: false ,icons: null });';
     $focus = '$(\'.ui-dialog-titlebar-close\').focus();';
-  
+
     return '<script>setTimeout(function(){'.$accordion.$focus.'},5);</script>';
   }
-
 }
-?>
-
diff --git a/library/ZendAfi/View/Helper/ReponseFormulaireFilled.php b/library/ZendAfi/View/Helper/ReponseFormulaireFilled.php
index 53b6289834d..c815b206fa1 100644
--- a/library/ZendAfi/View/Helper/ReponseFormulaireFilled.php
+++ b/library/ZendAfi/View/Helper/ReponseFormulaireFilled.php
@@ -16,30 +16,25 @@
  *
  * 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 
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
 
-class ZendAfi_View_Helper_ReponseFormulaireFilled extends Zend_View_Helper_HtmlElement {
+class ZendAfi_View_Helper_ReponseFormulaireFilled extends ZendAfi_View_Helper_BaseHelper {
 
   public function reponseFormulaireFilled($formulaire) {
 
-    $html='<dl>';
-    
-    $datas = [$this->view->_('Envoyé le') => $formulaire->getMailDate(),
-              $this->view->_('Expéditeur') => $formulaire->getMailExpediteur(),
-              $this->view->_('Destinataire') => $formulaire->getMailDestinataire(),
-              $this->view->_('Sujet') => $formulaire->getMailSubject(),
-              $this->view->_('Réponse') => $formulaire->getMailBody()];
+    $datas = [$this->_('Envoyé le') => $formulaire->getMailDate(),
+              $this->_('Expéditeur') => $formulaire->getMailExpediteur(),
+              $this->_('Destinataire') => $formulaire->getMailDestinataire(),
+              $this->_('Sujet') => $formulaire->getMailSubject(),
+              $this->_('Réponse') => $formulaire->getMailBody()];
 
-    foreach($datas as $name => $value){
-      $html.= '<dt>'.$name.'</dt>';
-      $html.= '<dd>'.nl2br($value).'</dd>';
-    }   
-
-    return $html.='</dl>';
+    $html = '';
+    foreach($datas as $name => $value) {
+      $html .= $this->_tag('dt', $name);
+      $html .= $this->_tag('dd', nl2br($this->view->escape($value)));
+    }
 
+    return $this->_tag('dl', $html);
   }
-
 }
-
-?>
\ No newline at end of file
diff --git a/tests/application/modules/admin/controllers/ModoControllerFormulaireTest.php b/tests/application/modules/admin/controllers/ModoControllerFormulaireTest.php
index f7917663920..29f2daab454 100644
--- a/tests/application/modules/admin/controllers/ModoControllerFormulaireTest.php
+++ b/tests/application/modules/admin/controllers/ModoControllerFormulaireTest.php
@@ -18,7 +18,147 @@
  * along with BOKEH; if not, write to the Free Software
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
-require_once 'AdminAbstractControllerTestCase.php';
+
+class ModoControllerFormulaireDesactivatedTest extends Admin_AbstractControllerTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    Class_AdminVar::newInstanceWithId('CMS_FORMULAIRES')->setValeur(0);
+    $this->dispatch('/admin/modo/');
+  }
+
+
+  /** @test */
+  public function linkToModerateFormulairesShouldNotBePresent() {
+    $this->assertNotXPath('//a[contains(@href, "/admin/modo/formulaires")]');
+  }
+}
+
+
+
+
+class ModoControllerFormulaireActivatedTest extends Admin_AbstractControllerTestCase {
+  public function setUp() {
+    parent::setUp();
+    Class_AdminVar::newInstanceWithId('CMS_FORMULAIRES')->setValeur(1);
+
+    $this->onLoaderOfModel(Class_Formulaire::class)
+         ->whenCalled('countBy')
+         ->with(['validated' => false])
+         ->answers(2);
+
+    $this->dispatch('/admin/modo/');
+  }
+
+
+  /** @test */
+  public function linkToModerateFormulairesShouldBePresent() {
+    $this->assertXPathContentContains('//a[contains(@href, "/admin/modo/formulaires")]/span', '2');
+  }
+}
+
+
+
+
+class ModoControllerFormulaireListTest extends Admin_AbstractControllerTestCase {
+  protected $_storm_default_to_volatile = true;
+
+  public function setUp() {
+    parent::setUp();
+    $hackaton = $this->fixture(Class_Article::class,
+                               ['id' => 4,
+                                'titre' => 'Inscrivez vous au Hackaton',
+                                'contenu' => 'Car c\'est cool']);
+
+    $preinscription = $this->fixture(Class_Article::class,
+                                     ['id' => 2,
+                                      'titre' => 'Formulaire de préinscription',
+                                      'contenu' => 'pour se préinscrire']);
+
+    $this->onLoaderOfModel(Class_Article::class)
+         ->whenCalled('findAll')
+         ->with('select id_article,titre from cms_article where id_article in (select distinct id_article from formulaires)')
+         ->answers([$hackaton, $preinscription]);
+
+    $this->onLoaderOfModel(Class_Formulaire::class)
+         ->whenCalled('countBy')
+         ->with(['model' => $hackaton,
+                 'role' => 'article'])
+         ->answers(2)
+
+         ->whenCalled('countBy')
+         ->with(['model' => $hackaton,
+                 'role' => 'article',
+                 'scope' => ['validated' => false]])
+         ->answers(2)
+
+         ->whenCalled('countBy')
+         ->with(['model' => $preinscription,
+                 'role' => 'article'])
+         ->answers(4)
+
+         ->whenCalled('countBy')
+         ->with(['model' => $preinscription,
+                 'role' => 'article',
+                 'scope' => ['validated' => false]])
+         ->answers(1)
+
+         ->beStrict();
+
+    $this->dispatch('/admin/modo/formulaires/');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsHackaton() {
+    $this->assertXPathContentContains('//td', 'Inscrivez vous au Hackaton');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsHackatonCounters() {
+    $this->assertXPathContentContains('//td', '2 / 2');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToViewHackatonResponses() {
+    $this->assertXPath('//a[contains(@href,"admin/modo/formulaires/id_article/4")]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToEditHackaton() {
+    $this->assertXPath('//a[contains(@href,"admin/cms/edit/id/4")]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsPreinscription() {
+    $this->assertXPathContentContains('//td', 'Formulaire de préinscription');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsPreinscriptionCounters() {
+    $this->assertXPathContentContains('//td', '1 / 4');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToViewPreinscriptionResponses() {
+    $this->assertXPath('//a[contains(@href,"admin/modo/formulaires/id_article/2")]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToEditPreinscription() {
+    $this->assertXPath('//a[contains(@href,"admin/cms/edit/id/2")]');
+  }
+}
+
+
+
 
 abstract class ModoControllerFormulaireForArticleTestCase extends Admin_AbstractControllerTestCase {
   protected
@@ -28,9 +168,9 @@ abstract class ModoControllerFormulaireForArticleTestCase extends Admin_Abstract
   public function setUp() {
     parent::setUp();
 
-    Class_AdminVar::newInstanceWithId('CMS_FORMULAIRES')->setValeur(1);
+    Class_AdminVar::set('CMS_FORMULAIRES', 1);
 
-    $zork = $this->fixture('Class_Users',
+    $zork = $this->fixture(Class_Users::class,
                            ['id' => 34,
                             'login' => 'zork',
                             'nom' => 'Bougie',
@@ -40,29 +180,30 @@ abstract class ModoControllerFormulaireForArticleTestCase extends Admin_Abstract
                             'bib' => $this->fixture('Class_Bib', ['id' => 4,
                                                                   'libelle' => 'Annecy'])]);
 
-    $article = $this->fixture('Class_Article',
+    $article = $this->fixture(Class_Article::class,
                               ['id' => 12,
                                'titre' => 'Inscrivez vous au Hackaton',
                                'contenu' => 'Un évenement qui a la classe']);
 
-    $formulaire_de_tinguette = $this->fixture('Class_Formulaire',
+    $formulaire_de_tinguette = $this->fixture(Class_Formulaire::class,
                                               ['id' => 3,
                                                'data' => serialize(['nom' => 'Tinguette',
                                                                     'prenom' => 'Quentine']),
                                                'date_creation' => '2012-12-05 12:00:23',
                                                'article' => $article,
-                                               'validated' => false]);
+                                               'validated' => true]);
 
-    $this->formulaire_de_bougie = $this->fixture('Class_Formulaire',
+    $this->formulaire_de_bougie = $this->fixture(Class_Formulaire::class,
                                                  ['id' => 5,
                                                   'data' => serialize(['nom' => 'Bougie',
-                                                                       'Prenom' => 'Mireille']),
+                                                                       'Prenom' => 'Mireille',
+                                                                       'ouch' => 'a content with tags <span></span>']),
                                                   'date_creation' => '2012-12-06 10:00:01',
                                                   'article' => $article,
                                                   'validated' => false,
                                                   'user' => $zork]);
 
-    $formulaire_de_lefort = $this->fixture('Class_Formulaire',
+    $formulaire_de_lefort = $this->fixture(Class_Formulaire::class,
                                            ['id' => 6,
                                             'data' => serialize(['name' => 'Lefort',
                                                                  'prenom' => 'Nono',
@@ -72,7 +213,7 @@ abstract class ModoControllerFormulaireForArticleTestCase extends Admin_Abstract
                                             'date_creation' => '2012-11-06 17:00:01',
                                             'article' => $article]);
 
-    $arold_form = $this->fixture('Class_Formulaire',
+    $arold_form = $this->fixture(Class_Formulaire::class,
                                  ['id' => 98,
                                   'data' => serialize(['name' => '<script>$("body *").remove();</script>',
                                                        'prenom' => '<script>$("body").append("Welcome");</script>',
@@ -80,48 +221,74 @@ abstract class ModoControllerFormulaireForArticleTestCase extends Admin_Abstract
                                   'date_creation' => '2012-11-06 17:00:01',
                                   'article' => $article]);
 
-    Storm_Test_ObjectWrapper::onLoaderOfModel('Class_Formulaire')
-      ->whenCalled('findAllBy')
-      ->with(['role' => 'article',
-              'model' => $article,
-              'order' => 'date_creation desc'])
-      ->answers([$formulaire_de_tinguette,
-                 $this->formulaire_de_bougie,
-                 $formulaire_de_lefort,
-                 $arold_form])
-
-      ->whenCalled('findAllBy')
-      ->with([ 'role' => 'article',
-               'model' => $article,
-               'order' => 'date_creation desc',
-               'validated' => false])
-      ->answers([$formulaire_de_lefort])
-
-      ->whenCalled('countNotValidated')
-      ->answers(1);
+    $this->onLoaderOfModel(Class_Formulaire::class)
+         ->whenCalled('findAllBy')
+         ->with(['role' => 'article',
+                 'model' => $article,
+                 'order' => 'date_creation desc'])
+         ->answers([$formulaire_de_tinguette,
+                    $this->formulaire_de_bougie,
+                    $formulaire_de_lefort,
+                    $arold_form])
+
+         ->whenCalled('findAllBy')
+         ->with([ 'role' => 'article',
+                 'model' => $article,
+                 'order' => 'date_creation desc',
+                 'validated' => false])
+         ->answers([$formulaire_de_lefort])
+
+         ->whenCalled('countNotValidated')
+         ->answers(1);
+  }
+
+
+  protected function _assertPageContainsButton($url, $label) {
+    $this->assertXPathContentContains('//button[contains(@data-url, "' . $url . '")]',
+                                      $label);
   }
 }
 
 
 
 
-class ModoControllerFormulaireForArticleListWithListeAllParameterTest extends ModoControllerFormulaireForArticleTestCase {
+class ModoControllerFormulairesHackatonListeAllTest
+  extends ModoControllerFormulaireForArticleTestCase {
+
   public function setUp() {
     parent::setUp();
+    Class_Users::getIdentity()
+      ->setFormColumns(12, ['nom', 'securite-sociale', 'budget_(totem)_régional']);
 
-    $this->dispatch('admin/modo/formulaires/id_article/12/liste/all', true);
+    $this->dispatch('admin/modo/formulaires/id_article/12/liste/all');
   }
 
 
   /** @test */
-  public function h1ShouldContainsFormulairesAndArticleTitle() {
-    $this->assertXPathContentContains('//h1', 'Modération des formulaires: Inscrivez vous au Hackaton');
+  public function h1ShouldContainsInscrivezVousAuHackaton() {
+    $this->assertXPathContentContains('//h1',
+                                      'Modération des formulaires: Inscrivez vous au Hackaton');
   }
 
 
   /** @test **/
-  public function subviewShouldContainsVoirLesReponsesAValider() {
-    $this->assertXPathContentContains('//a[contains(@href, "admin/modo/formulaires/id_article/12")]','Afficher uniquement les réponses à valider');
+  public function pageShouldContainsButtonVoirLesReponsesAValider() {
+    $this->_assertPageContainsButton('/modo/formulaires/id_article/12',
+                                     'Afficher uniquement les réponses à valider');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsButtonToExportCsv() {
+    $this->_assertPageContainsButton('/modo/export-csv-formulaire/id_article/12',
+                                     'Tout exporter au format CSV');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsButtonToChooseColumns() {
+    $this->_assertPageContainsButton('/modo/choose-form-columns/id/12',
+                                     'Choisir les colonnes à afficher');
   }
 
 
@@ -161,12 +328,6 @@ class ModoControllerFormulaireForArticleListWithListeAllParameterTest extends Mo
   }
 
 
-  /** @test */
-  public function formulaireQuentineShouldNotHaveLinkToValidate() {
-    $this->assertNotXPath('//a[contains(@href, "validate-formulaire/id_article/12/id/3")]');
-  }
-
-
   /** @test */
   public function mireilleRowShouldContainsUserZork() {
     $this->assertXPathContentContains('//tbody//tr[2]//td', 'Bougie');
@@ -181,49 +342,67 @@ class ModoControllerFormulaireForArticleListWithListeAllParameterTest extends Mo
 
   /** @test */
   public function mireilleRowShouldContainsDate06_12_2012 () {
-    $this->assertXPathContentContains('//tbody//tr[2]//td', '06/12/2012', $this->_response->getBody());
+    $this->assertXPathContentContains('//tbody//tr[2]//td', '06/12/2012');
   }
 
 
   /** @test */
-  public function aTDShouldContainsActionToDeleteFormulaireMireille() {
-    $this->assertXPath('//tbody//tr[2]//td/a[contains(@href, "admin/modo/delete-formulaire/id_article/12/liste/all/id/5")]');
+  public function aroldFormulairesShouldNotContainsScriptTags() {
+    $this->assertNotXPath('//tr//td//script');
   }
 
 
   /** @test */
-  public function aTDShouldContainsActionToValidateFormulaireMireille() {
-    $this->assertXPath('//tbody//tr[2]//td/a[contains(@href, "admin/modo/validate-formulaire/id_article/12/liste/all/id/5")]');
+  public function pageShouldNotContainsLinkToValidateQuentineForm() {
+    $this->assertNotXPath('//a[contains(@href, "/modo/validate-formulaire/id/3")]');
   }
 
 
   /** @test */
-  public function linkToExportCsvShouldBePresent() {
-    $this->assertXPathContentContains('//a[contains(@href, "admin/modo/export-csv-formulaire/id_article/12")]',
-                                      'Export CSV');
+  public function pageShouldContainsLinkToDeleteQuentineForm() {
+    $this->assertXPath('//a[contains(@href, "/modo/delete-formulaire/id/3")]');
   }
 
 
   /** @test */
-  public function mainFormulairesMenuShouldNotContainsIdArticleParam() {
-    $this->assertXPath('//div[@class="menu"]//a[@href="/admin/modo/formulaires/liste/all"]');
+  public function pageShouldContainsLinkToViewQuentineFormInPopup() {
+    $this->assertXPath('//a[contains(@href, "/modo/visualiser-reponse-ajax/id/3")][@data-popup="true"]');
   }
 
 
   /** @test */
-  public function aroldFormulairesShouldNotContainsScriptTags() {
-    $this->assertNotXPath('//tr//td//script');
+  public function pageContainsLinkToDeleteMireilleForm() {
+    $this->assertXPath('//a[contains(@href, "/modo/delete-formulaire/id/5")]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToValidateMireilleForm() {
+    $this->assertXPath('//a[contains(@href, "/modo/validate-formulaire/id/5")]');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsLinkToViewMireilleFormInPopup() {
+    $this->assertXPath('//a[contains(@href, "/modo/visualiser-reponse-ajax/id/5")][@data-popup="true"]');
+  }
+
+
+  /** @test */
+  public function formulairesMenuShouldNotContainsIdArticleParam() {
+    $this->assertXPath('//div[@class="menu"]//a[@href="/admin/modo/formulaires/liste/all"]');
   }
 }
 
 
 
 
-class ModoControllerFormulaireForArticleValidateFormulaireMireilleTest extends ModoControllerFormulaireForArticleTestCase {
+class ModoControllerFormulaireForArticleValidateFormulaireMireilleTest
+  extends ModoControllerFormulaireForArticleTestCase {
+
   public function setUp() {
     parent::setUp();
-
-    $this->dispatch('admin/modo/validate-formulaire/id_article/12/id/5', true);
+    $this->dispatch('admin/modo/validate-formulaire/id_article/12/id/5');
   }
 
 
@@ -254,11 +433,11 @@ class ModoControllerFormulaireForArticleValidateFormulaireMireilleTest extends M
 
 
 
-class ModoControllerFormulaireForArticleDeleteTest extends ModoControllerFormulaireForArticleTestCase {
+class ModoControllerFormulaireDeleteTest extends ModoControllerFormulaireForArticleTestCase {
   public function setUp() {
     parent::setUp();
     $_SERVER['HTTP_REFERER'] = '/admin/modo/formulaires/id_article/12';
-    $this->dispatch('admin/modo/delete-formulaire/id_article/12/id/5', true);
+    $this->dispatch('admin/modo/delete-formulaire/id_article/12/id/5');
   }
 
 
@@ -277,32 +456,27 @@ class ModoControllerFormulaireForArticleDeleteTest extends ModoControllerFormula
 
 
 
-class ModoControllerFormulaireExportCSVForArticlTest extends ModoControllerFormulaireForArticleTestCase {
+class ModoControllerFormulaireExportCsvTest extends ModoControllerFormulaireForArticleTestCase {
+  protected $_content;
+
   public function setUp() {
     parent::setUp();
-
-    $this->dispatch('admin/modo/export-csv-formulaire/id_article/12', true);
-  }
-
-
-  /** @test */
-  public function secondFormulaireShouldBeCSV() {
-    $this->assertContains('"2012-12-06 10:00:01";"Mireille Bougie";Annecy;Bougie;Mireille;;;;',
-                          $this->_response->getBody());
+    $this->dispatch('admin/modo/export-csv-formulaire/id_article/12');
+    $this->_content = $this->_response->getBody();
   }
 
 
   /** @test */
-  public function thirdFormulaireShouldBeCSV() {
-    $this->assertContains('"2012-11-06 17:00:01";;;;Nono;Lefort;12;789;"budget régional"',
-                          $this->_response->getBody());
+  public function mireilleResponseShouldBeInCsv() {
+    $this->assertContains('"2012-12-06 10:00:01";"Mireille Bougie";Annecy;Bougie;Mireille;"a content with tags <span></span>";;;;',
+                          $this->_content);
   }
 
 
   /** @test */
-  public function csvShouldContainsAttributeNames() {
-    $this->assertContains('date_creation;compte;libelle_bib;nom;prenom;name;age;securite-sociale;budget_(totem)_régional',
-                          $this->_response->getBody());
+  public function nonoResponseShouldBeInCsv() {
+    $this->assertContains('"2012-11-06 17:00:01";;;;Nono;;Lefort;12;789;"budget régional"',
+                          $this->_content);
   }
 
 
@@ -321,197 +495,117 @@ class ModoControllerFormulaireExportCSVForArticlTest extends ModoControllerFormu
 
 
 
-class ModoControllerFormulaireListTest extends Admin_AbstractControllerTestCase {
-  protected $_storm_default_to_volatile = true;
-
+class ModoControllerFormulaireHackatonListTest extends ModoControllerFormulaireForArticleTestCase {
   public function setUp() {
     parent::setUp();
-    $hackaton = $this->fixture('Class_Article',
-                               ['id' => 4,
-                                'titre' => 'Inscrivez vous au Hackaton',
-                                'contenu' => 'Car c\'est cool']);
-
-    $preinscription = $this->fixture('Class_Article',
-                                     ['id' => 2,
-                                      'titre' => 'Formulaire de préinscription',
-                                      'contenu' => 'pour se préinscrire']);
-
-    Storm_Test_ObjectWrapper::onLoaderOfModel('Class_Article')
-      ->whenCalled('findAll')
-      ->with('select id_article,titre from cms_article where id_article in (select distinct id_article from formulaires)')
-      ->answers([$hackaton, $preinscription]);
-
-    Storm_Test_ObjectWrapper::onLoaderOfModel('Class_Formulaire')
-      ->whenCalled('countBy')
-      ->with(['model' => $hackaton,
-              'role' => 'article'])
-      ->answers(2)
+    Class_Users::getIdentity()->setFormColumns(12, ['name']);
 
-      ->whenCalled('countBy')
-      ->with(['model' => $hackaton,
-              'role' => 'article',
-              'scope' => ['validated' => false]])
-      ->answers(2)
-
-      ->whenCalled('countBy')
-      ->with(['model' => $preinscription,
-              'role' => 'article'])
-      ->answers(4)
-
-      ->whenCalled('countBy')
-      ->with(['model' => $preinscription,
-              'role' => 'article',
-              'scope' => ['validated' => false]])
-      ->answers(1)
-
-      ->whenCalled('countNotValidated')
-      ->answers(3)
+    $this->dispatch('admin/modo/formulaires/id_article/12');
+  }
 
-      ->beStrict();
 
-    $this->dispatch('admin/modo/formulaires/', true);
+  /** @test **/
+  public function pageShouldContainsButtonToViewAllResponses() {
+    $this->_assertPageContainsButton('/modo/formulaires/id_article/12/liste/all',
+                                     'Afficher toutes les réponses');
   }
 
 
   /** @test */
-  public function liShouldContainsLinkToFormulaireForHackaton() {
-    $this->assertXPathContentContains('//div/ul/li[1]/a[contains(@href,"admin/modo/formulaires/id_article/4")]', 'Inscrivez vous au Hackaton [2/2]');
+  public function lefortShouldBeDisplay() {
+    $this->assertXPathContentContains('//td', 'Lefort');
   }
 
 
   /** @test */
-  public function liShouldContainsLinkToEditFormulaireHackaton() {
-    $this->assertXPath('//div/ul/li[1]/a[contains(@href,"admin/cms/edit/id/4")]');
+  public function tinguetteShouldBeDisplay() {
+    $this->assertNotXPathContentContains('//td', 'Tinguette');
   }
 
 
   /** @test */
-  public function liShouldContainsLinkToFormulaireForPreinscription() {
-    $this->assertXPathContentContains('//div/ul/li[2]/a[contains(@href,"admin/modo/formulaires/id_article/2")]', 'Formulaire de préinscription [1/4]');
+  public function bougieShouldBeDisplay() {
+    $this->assertNotXPathContentContains('//td', 'Bougie');
   }
-}
-
-
 
 
-class ModoControllerFormulaireIndexWithOptionActivatedTest extends Admin_AbstractControllerTestCase {
-  public function setUp() {
-    parent::setUp();
-    Class_AdminVar::newInstanceWithId('CMS_FORMULAIRES')->setValeur(1);
+  /** @test */
+  public function pageContainsLinkToDeleteLefortForm() {
+    $this->assertXPath('//a[contains(@href, "/modo/delete-formulaire/id/6")]');
+  }
 
-    Storm_Test_ObjectWrapper::onLoaderOfModel('Class_Formulaire')
-      ->whenCalled('countBy')
-      ->with(['validated' => false])
-      ->answers(2);
 
-    $this->dispatch('admin/modo/', true);
+  /** @test */
+  public function pageShouldContainsLinkToValidateLefortForm() {
+    $this->assertXPath('//a[contains(@href, "/modo/validate-formulaire/id/6")]');
   }
 
 
   /** @test */
-  public function linkToModerateFormulairesShouldBePresent() {
-    $this->assertXPathContentContains('//a[contains(@href, "/admin/modo/formulaires")]/span', '2');
+  public function pageShouldContainsLinkToViewLefortFormInPopup() {
+    $this->assertXPath('//a[contains(@href, "/modo/visualiser-reponse-ajax/id/6")][@data-popup="true"]');
   }
 }
 
 
 
 
-class ModoControllerFormulaireIndexWithOptionDesactivatedTest extends Admin_AbstractControllerTestCase {
-  public function setUp() {
-    parent::setUp();
-    Class_AdminVar::newInstanceWithId('CMS_FORMULAIRES')->setValeur(0);
-    $this->dispatch('admin/modo/', true);
-  }
+class ModoControllerFormulaireVisualiserReponseArticleTest
+  extends ModoControllerFormulaireForArticleTestCase {
 
-
-  /** @test */
-  public function linkToModerateFormulairesShouldNotBePresent() {
-    $this->assertNotXPath('//a[contains(@href, "/admin/modo/formulaires")]');
-  }
-}
-
-
-class ModoControllerFormulaireForArticleListTest extends ModoControllerFormulaireForArticleTestCase {
   public function setUp() {
     parent::setUp();
-    $this->dispatch('admin/modo/formulaires/id_article/12',true);
+    $this->dispatch('/admin/modo/visualiser-reponse-ajax/id/5');
   }
 
 
   /** @test **/
-  public function subviewShouldContainsVoirTousLesReponses() {
-    $this->assertXPathContentContains('//a[contains(@href, "admin/modo/formulaires/id_article/12/liste/all")]','Afficher toutes les réponses');
+  public function pageShouldContainsFieldNom() {
+    $this->assertXPathContentContains('//table[@id="response"]//td', 'nom');
   }
 
 
   /** @test */
-  public function lefortShouldBeDisplay() {
-    $this->assertXPathContentContains('//td', 'Lefort');
+  public function pageShouldContainsValueBougie() {
+    $this->assertXPathContentContains('//table[@id="response"]//td', 'Bougie');
   }
 
 
   /** @test */
-  public function tinguetteShouldBeDisplay() {
-    $this->assertNotXPathContentContains('//td', 'Tinguette');
+  public function pageShouldContainsEscapedValue() {
+    $this->assertXPathContentContains('//table[@id="response"]//td', 'with tags &lt;span&gt;');
   }
 
 
   /** @test */
-  public function bougieShouldBeDisplay() {
-    $this->assertNotXPathContentContains('//td', 'Bougie');
-  }
-
-
-    /** @test */
-  public function aTDShouldContainsActionToValidateFormulaireLefort() {
-    $this->assertXPath('//tbody//tr//td/a[contains(@href, "admin/modo/validate-formulaire/id_article/12/id/6")]');
+  public function pageShouldContainsRedigerUneRéponse() {
+    $this->assertXPathContentContains('//h3', 'Rédiger une réponse');
   }
 
 
   /** @test */
-  public function aTDShouldContainsActionToVisualiserFormulaireLefort() {
-    $this->assertXPath('//tbody//tr//td/a[contains(@href, "admin/modo/visualiser-reponse-ajax/id_article/12/id/6")]',$this->_response->getBody());
-  }
-}
-
-
-
-class ModoControllerFormulaireVisualiserReponseArticleTest extends ModoControllerFormulaireForArticleTestCase {
-
-
-  public function setUp() {
-    parent::setUp();
-
-    $this->dispatch('admin/modo/visualiser-reponse-ajax/id_article/12/id/5', true);
+  public function pageShouldContainsAccordionScript() {
+    $this->assertXPathContentContains('//script', '#form_accordion\').accordion(');
   }
 
 
   /** @test **/
-  public function actionShouldDisplayDdBougie() {
-    $result = json_decode($this->_response->getBody());
-    $this->assertContains('<dl><dt>nom</dt><dd>Bougie</dd><dt>Prenom</dt><dd>Mireille</dd>',$result->content);
-  }
-
-
-    /** @test **/
-  public function actionShouldDisplayDeleteFormulaire() {
-    $result = json_decode($this->_response->getBody());
-    $this->assertContains('delete-formulaire',$result->content);
+  public function pageShouldContainsDeleteButton() {
+    $this->assertXPath('//button[contains(@data-url, "/modo/delete-formulaire/id/5")]');
   }
 
 
-    /** @test **/
-  public function actionShouldDisplayValidateFormulaire() {
-    $result = json_decode($this->_response->getBody());
-    $this->assertContains('validate-formulaire',$result->content);
+  /** @test **/
+  public function pageShouldContainsValidateButton() {
+    $this->assertXPath('//button[contains(@data-url, "/modo/validate-formulaire/id/5")]');
   }
-
 }
 
 
 
-class ModoControllerFormulaireVisualiserReponseArticleSendResponseByMailTest extends ModoControllerFormulaireForArticleTestCase {
+
+class ModoControllerFormulaireVisualiserReponseArticleSendResponseByMailTest
+  extends ModoControllerFormulaireForArticleTestCase {
 
   public function setup() {
     parent::setup();
@@ -520,13 +614,13 @@ class ModoControllerFormulaireVisualiserReponseArticleSendResponseByMailTest ext
     Zend_Mail::setDefaultTransport($this->mock_transport);
     Class_Users::getIdentity()->setMail('laurent@afi-sa.fr');
 
-    Storm_Test_ObjectWrapper::onLoaderOfModel('Class_Formulaire')
-      ->whenCalled('save')
-      ->answers(true)
+    $this->onLoaderOfModel('Class_Formulaire')
+         ->whenCalled('save')
+         ->answers(true)
 
-      ->whenCalled('find')
-      ->with(['id'=>5])
-      ->answers($this->formulaire_de_bougie);
+         ->whenCalled('find')
+         ->with(['id'=>5])
+         ->answers($this->formulaire_de_bougie);
 
     $this->postDispatch('admin/modo/reponse-formulaire-send-mail/id_article/12/id/5',
                         ['mail_expediteur' => 'gigi@afi-sa.fr',
@@ -562,41 +656,171 @@ class ModoControllerFormulaireVisualiserReponseArticleSendResponseByMailTest ext
   }
 
 
-/** @test **/
+  /** @test **/
   public function mailSendShouldHaveBeenSave() {
       $this->assertEquals(5, Class_Formulaire::getFirstAttributeForLastCallOn('save')->getId());
   }
 
 
-/** @test **/
+  /** @test **/
   public function dateTimeShouldHaveBeenSave() {
     $this->assertNotEmpty($this->formulaire_de_bougie->getMailDate());
   }
 
 
-/** @test **/
+  /** @test **/
   public function sujetShouldHaveBeenSave() {
     $this->assertEquals('Versaille', $this->formulaire_de_bougie->getMailSubject());
   }
 
 
-/** @test **/
+  /** @test **/
   public function bodyShouldHaveBeenSave() {
     $this->assertEquals("Les lits sont trop petits pour aujourd'hui! éèà@€ôö", $this->formulaire_de_bougie->getMailBody());
   }
 
 
-/** @test **/
+  /** @test **/
   public function destinataireShouldHaveBeenSave() {
     $this->assertEquals('martine@afi-sa.fr', $this->formulaire_de_bougie->getMailDestinataire());
   }
 
 
-/** @test **/
+  /** @test **/
   public function expediteurShouldHaveBeenSave() {
     $this->assertEquals('gigi@afi-sa.fr', $this->formulaire_de_bougie->getMailExpediteur());
   }
+}
+
+
+
+
+abstract class ModoControllerFormulaireChooseHackatonColumnsTestCase
+  extends ModoControllerFormulaireForArticleTestCase {
+
+  public function setup() {
+    parent::setup();
+    $this->_prepareFixtures();
+    $this->dispatch('/admin/modo/choose-form-columns/id/12');
+  }
+
+
+  protected function _prepareFixtures() {
+    return $this;
+  }
+
 
+  public function availableFields() {
+    return [
+            ['nom'],
+            ['prenom'],
+            ['name'],
+            ['age'],
+            ['securite-sociale'],
+            ['budget_(totem)_régional'],
+    ];
+  }
+
+
+  /**
+   * @test
+   * @dataProvider availableFields
+   **/
+  public function pageShouldContainsAvailableField($field) {
+    $path = '//div[contains(@class, "available_items")]//li[@data-id="' . $field . '"]';
+    $this->assertXPathContentContains($path, $field);
+  }
 }
 
-?>
+
+
+
+class ModoControllerFormulaireChooseHackatonColumnsDefaultTest
+  extends ModoControllerFormulaireChooseHackatonColumnsTestCase {
+
+  /** @test */
+  public function nothingShouldBeSelected() {
+    $this->assertNotXPath('//div[contains(@class, "selected_items")]//li');
+  }
+}
+
+
+
+
+class ModoControllerFormulaireChooseHackatonColumnsWithChosenTest
+  extends ModoControllerFormulaireChooseHackatonColumnsTestCase {
+
+  protected function _prepareFixtures() {
+    parent::_prepareFixtures();
+    Class_Users::getIdentity()->setFormColumns(12, ['age', 'nom']);
+    return $this;
+  }
+
+
+  public function availableFields() {
+    return [
+            ['prenom'],
+            ['name'],
+            ['securite-sociale'],
+            ['budget_(totem)_régional'],
+    ];
+  }
+
+
+  public function selectedFields() {
+    return [
+            ['nom'],
+            ['age'],
+    ];
+  }
+
+
+  /**
+   * @test
+   * @dataProvider selectedFields
+   */
+  public function pageShouldContainsSelectedField($field) {
+    $this->assertXPath('//div[contains(@class, "selected_items")]//li[@data-id="' . $field . '"]');
+  }
+
+
+  /**
+   * @test
+   * @dataProvider selectedFields
+   **/
+  public function pageShouldNotContainsSelectedFieldInAvailables($field) {
+    $path = '//div[contains(@class, "available_items")]//li[@data-id="' . $field . '"]';
+    $this->assertNotXPathContentContains($path, $field);
+  }
+}
+
+
+
+
+class ModoControllerFormulaireChooseHackatonColumnsPostTest
+  extends ModoControllerFormulaireForArticleTestCase {
+
+  public function setup() {
+    parent::setup();
+    $this->postDispatch('/admin/modo/choose-form-columns/id/12',
+                        ['columns' => 'age;name']);
+  }
+
+
+  /** @test */
+  public function userSettingsShouldBeSetForHackaton() {
+    $this->assertEquals(['age', 'name'], Class_Users::getIdentity()->getFormColumns(12));
+  }
+
+
+  /** @test */
+  public function shouldNotifySuccess() {
+    $this->assertFlashMessengerContentContains('Choix des colonnes enregistré');
+  }
+
+
+  /** @test */
+  public function shouldRedirect() {
+    $this->assertRedirect();
+  }
+}
diff --git a/tests/library/ZendAfi/View/Helper/ReponseFormulaireTest.php b/tests/library/ZendAfi/View/Helper/ReponseFormulaireTest.php
index 64812325cdb..89a91244abd 100644
--- a/tests/library/ZendAfi/View/Helper/ReponseFormulaireTest.php
+++ b/tests/library/ZendAfi/View/Helper/ReponseFormulaireTest.php
@@ -16,187 +16,178 @@
  *
  * 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 
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
 
-require_once 'library/ZendAfi/View/Helper/ViewHelperTestCase.php';
-
 abstract class AbstractReponseSetupFormulaireTest extends ViewHelperTestCase {
 
-  protected $_html;
-  protected $_zork;
-  protected $_formulaire_de_bougie;
+  protected
+    $_storm_default_to_volatile = true,
+    $_helper,
+    $_html,
+    $_zork,
+    $_formulaire_de_bougie;
 
   public function setup() {
     parent::setup();
-    
-    $expediteur = Class_Users::newInstanceWithId(2,['nom' => 'La',
-                                                    'prenom' => 'Tome',
-                                                    'mail' => 'deBozon@afi-sa.fr']);
-
-    ZendAfi_Auth::getInstance()->logUser($expediteur);
+    $this->_prepareFixtures();
 
-    $this->_zork = 
-      Class_Users::newInstanceWithId(34, 
-                                     ['login' => 'zork',
-                                      'nom' => 'Bougie',
-                                      'prenom' => 'Mireille',
-                                      'mail' => 'mireille@afi-sa.fr',
-                                      'bib' => Class_Bib::newInstanceWithId(4, ['libelle' => 'Annecy'])]);
-
-    $article = Class_Article::newInstanceWithId(12, ['titre' => 'Inscrivez vous au Hackaton']);
-
-    $this->_formulaire_de_bougie = 
-      Class_Formulaire::newInstanceWithId(5, ['data' => serialize(['nom' => 'Bougie',
-                                                                   'Prenom' => 'Mireille']),
-                                              'date_creation' => '2012-12-06 10:00:01',
-                                              'article' => $article,
-                                              'validated' => false,
-                                              'user' => $this->_zork]);
+    $this->_helper = new ZendAfi_View_Helper_ReponseFormulaire();
+    $this->_helper->setView($this->view);
+    $this->_html = $this->_helper->reponseFormulaire($this->_formulaire_de_bougie);
   }
-}
-
 
 
-class ReponseFormulaireWithZorkAsMailTest extends AbstractReponseSetupFormulaireTest {
+  protected function _prepareFixtures() {
+    $expediteur = $this->fixture(Class_Users::class,
+                                 ['id' => 2,
+                                  'login' => 'latome',
+                                  'password' => 'chuuut',
+                                  'nom' => 'La',
+                                  'prenom' => 'Tome',
+                                  'mail' => 'deBozon@afi-sa.fr']);
 
-  public function setup() {
-    parent::setup();
+    ZendAfi_Auth::getInstance()->logUser($expediteur);
 
-    $this->_helper = new ZendAfi_View_Helper_ReponseFormulaire();
-    $this->_helper->setView(new ZendAfi_Controller_Action_Helper_View());
+    $this->_zork = $this->fixture(Class_Users::class,
+                                  ['id' => 34,
+                                   'login' => 'zork',
+                                   'password' => 'tuuuhc',
+                                   'nom' => 'Bougie',
+                                   'prenom' => 'Mireille',
+                                   'mail' => 'mireille@afi-sa.fr',
+                                   'bib' => $this->fixture(Class_Bib::class,
+                                                           ['id' => 4,
+                                                            'libelle' => 'Annecy'])]);
+
+    $article = $this->fixture(Class_Article::class,
+                              ['id' => 12,
+                               'contenu' => 'a content',
+                               'titre' => 'Inscrivez vous au Hackaton']);
+
+    $this->_formulaire_de_bougie = $this->fixture(Class_Formulaire::class,
+                                                  ['id' => 5,
+                                                   'data' => serialize(['nom' => 'Bougie',
+                                                                        'Prenom' => 'Mireille',
+                                                                        'ouch' => 'data with <span>tags</span>']),
+                                                   'date_creation' => '2012-12-06 10:00:01',
+                                                   'article' => $article,
+                                                   'validated' => false,
+                                                   'user' => $this->_zork]);
+
+    return $this;
+  }
+}
 
-    $this->_html = $this->_helper->reponseFormulaire($this->_formulaire_de_bougie);
 
-  }
 
+class ReponseFormulaireOfAuthorHavingMailTest extends AbstractReponseSetupFormulaireTest {
   /** @test **/
-  public function aDtShouldContainNom() {
-    $this->assertXPathContentContains($this->_html,
-                                      '//dt',
-                                      'nom');
+  public function shouldDisplayNom() {
+    $this->assertXPathContentContains($this->_html, '//td', 'nom');
   }
 
+
   /** @test **/
-  public function aDdShouldContainBougie() {
-    $this->assertXPathContentContains($this->_html,
-                                      '//dd',
-                                      'Bougie');
+  public function shouldDisplayBougie() {
+    $this->assertXPathContentContains($this->_html, '//td', 'Bougie');
   }
 
+
   /** @test **/
-  public function aDtShouldContainPrenom() {
-    $this->assertXPathContentContains($this->_html,
-                                      '//dt',
-                                      'nom');
+  public function shouldDisplayPrenom() {
+    $this->assertXPathContentContains($this->_html, '//td', 'Prenom');
   }
 
+
   /** @test **/
-  public function aDdShouldContainMireill() {
-    $this->assertXPathContentContains($this->_html,
-                                      '//dd',
-                                      'Mireille');
+  public function shouldDisplayMireille() {
+    $this->assertXPathContentContains($this->_html, '//td', 'Mireille');
   }
-  
+
 
   /** @test **/
-  public function formSendMailShouldBeDisplayWithZorkMail() {
-    $this->assertXPathContentContains($this->_html,
-                                      '//form',
-                                      'Envoyer');
+  public function shouldDisplayEscapedDataWithTags() {
+    $this->assertXPathContentContains($this->_html, '//td',
+                                      'data with &lt;span&gt;tags&lt;/span&gt;');
+    $this->assertNotXPathContentContains($this->_html, '//td//span', 'tags');
   }
 
 
   /** @test **/
-  public function formSendMailShouldContainsDeBozonEmailInInputMailExpediteur() {
-    $this->assertXPath($this->_html,'//input[@name="mail_expediteur"][@value="deBozon@afi-sa.fr"]');
+  public function mailExpediteurValueShouldBeDeBozonAtAfiSaFr() {
+    $this->assertXPath($this->_html,
+                       '//input[@name="mail_expediteur"][@value="deBozon@afi-sa.fr"]');
   }
 
 
   /** @test **/
-  public function formSendMailShouldContiansZorkEmailInInputMailDesinataire() {
-    $this->assertXPath($this->_html,'//input[@name="mail_destinataire"][@value="mireille@afi-sa.fr"]');
+  public function mailDestinatairValueShouldBeMireilleAtAfiSaFr() {
+    $this->assertXPath($this->_html,
+                       '//input[@name="mail_destinataire"][@value="mireille@afi-sa.fr"]');
   }
 
 
   /** @test **/
-  public function formSendMailShouldContiansSujetHackaton() {
+  public function sujetValueShouldBeInscrivezVousAuHackaton() {
     $this->assertXPath($this->_html,'//input[@name="sujet"][@value="Inscrivez vous au Hackaton"]');
   }
 }
 
 
 
-class ReponseFormulaireWithZorkAsNoEmailTest extends AbstractReponseSetupFormulaireTest {
-
-  protected $_html;
 
-  public function setup() {
-    parent::setup();
+class ReponseFormulaireWithZorkAsNoEmailTest extends AbstractReponseSetupFormulaireTest {
+  public function _prepareFixtures() {
+    parent::_prepareFixtures();
     $this->_zork->setMail('');
-    $this->_helper = new ZendAfi_View_Helper_ReponseFormulaire();
-    $this->_helper->setView(new ZendAfi_Controller_Action_Helper_View());
-
-    $this->_html = $this->_helper->reponseFormulaire($this->_formulaire_de_bougie);
   }
 
 
   /** @test **/
-  public function messageEmailVideShouldBeDisplay() {
-    $this->assertXPathContentContains($this->_html,'//div',
-                                      "L'utilisateur n'a pas ");
+  public function userDidNotProvideMailShouldBeDisplay() {
+    $this->assertXPathContentContains($this->_html, '//div',
+                                      utf8_encode("L'utilisateur n'a pas renseigné son adresse e-mail"));
   }
 }
 
 
 
-class ReponseFormulaireWithNoUserTest extends AbstractReponseSetupFormulaireTest {
-
-  protected $_html;
 
-  public function setup() {
-    parent::setup();
+class ReponseFormulaireWithNoUserTest extends AbstractReponseSetupFormulaireTest {
+  public function _prepareFixtures() {
+    parent::_prepareFixtures();
     $this->_formulaire_de_bougie->setUser(null);
-    $this->_helper = new ZendAfi_View_Helper_ReponseFormulaire();
-    $this->_helper->setView(new ZendAfi_Controller_Action_Helper_View());
-
-    $this->_html = $this->_helper->reponseFormulaire($this->_formulaire_de_bougie);
   }
 
 
   /** @test **/
-  public function messageUtilisateurIntrouvableShouldBeDisplay() {
-    $this->assertXPathContentContains($this->_html,'//div//span',
-                                      "Utilisateur introuvable");
+  public function userUnknownShouldBeDisplay() {
+    $this->assertXPathContentContains($this->_html, '//div//span', "Utilisateur introuvable");
   }
 }
 
 
 
+
 class ReponseFormulaireWithReponseInDbTest extends AbstractReponseSetupFormulaireTest {
+  public function _prepareFixtures() {
+    parent::_prepareFixtures();
+    $sent_mail = (new ZendAfi_Mail)->setFrom('toto')
+                                   ->setSubject("les tests c'est bien");
 
-  protected $_html;
+    $this->_formulaire_de_bougie->setMailAnswer($sent_mail);
+  }
 
-  public function setup() {
-    parent::setup();
-    $this->_formulaire_de_bougie
-      ->setMailAnswer(serialize((new ZendAfi_Mail())->setFrom('toto')
-                      ->setSubject("les tests c'est bien")));
-    $this->_helper = new ZendAfi_View_Helper_ReponseFormulaire();
-    $this->_helper->setView(new ZendAfi_Controller_Action_Helper_View());
 
-    $this->_html = $this->_helper->reponseFormulaire($this->_formulaire_de_bougie);
+  /** @test */
+  public function titleVoirLaReponseShouldBePresent() {
+    $this->assertXPathContentContains($this->_html, '//h3', utf8_encode('Voir la réponse'));
   }
 
 
   /** @test **/
   public function mailReponseShouldBeLesTestsCEstBien() {
-    $this->assertXPathContentContains($this->_html,'//dd',
-                                      "les tests c'est bien");
+    $this->assertXPathContentContains($this->_html, '//dd', "les tests c'est bien");
   }
 }
-
-
-
-
-?>
\ No newline at end of file
-- 
GitLab