diff --git a/FEATURES/77694 b/FEATURES/77694
new file mode 100644
index 0000000000000000000000000000000000000000..bc1b07d3809841949b0521738f5f33fc68990851
--- /dev/null
+++ b/FEATURES/77694
@@ -0,0 +1,10 @@
+        '77694' =>
+            ['Label' => $this->_('Suppression d\'utilisateurs par lot'),
+             'Desc' => $this->_('Bokeh permet la suppression d'utilisateurs par lot correspondant aux critères de recherche sélectionnés'),
+             'Image' => '',
+             'Video' => 'https://www.youtube.com/watch?v=nX_z3rWnql8',
+             'Category' => 'Administration',
+             'Right' => function($feature_description, $user) {return true;},
+             'Wiki' => 'http://wiki.bokeh-library-portal.org/index.php?title=Gestion_des_utilisateurs#Suppression_par_lot',
+             'Test' => '',
+             'Date' => '2019-06-20'],
\ No newline at end of file
diff --git a/VERSIONS_WIP/77694 b/VERSIONS_WIP/77694
new file mode 100644
index 0000000000000000000000000000000000000000..15bafff91d3946af3faca1fafcdc0ae467e3a6a0
--- /dev/null
+++ b/VERSIONS_WIP/77694
@@ -0,0 +1 @@
+ - ticket #77694 : Administration : Bokeh permet la suppression d'utilisateurs par lot correspondant aux critères de recherche sélectionnés 
\ No newline at end of file
diff --git a/application/modules/admin/controllers/UsersController.php b/application/modules/admin/controllers/UsersController.php
index c9570c46edc08a7f812659989e93216d3f9e4438..c9ba4fcc562f4b172a3ce6f702b96df81ae92a21 100644
--- a/application/modules/admin/controllers/UsersController.php
+++ b/application/modules/admin/controllers/UsersController.php
@@ -30,10 +30,10 @@ class Admin_UsersController extends ZendAfi_Controller_Action {
   public function indexAction()  {
     $this->view->titre = $this->_('Gestion des utilisateurs');
     $params = $this->_request->getParams();
-    $this->_helper
-      ->userSearch([],
-                   (new Class_User_SearchCriteria($params))
-                   ->addCriteria(new Class_User_SearchCriteria_RoleLevelLimit($params)));
+    $this->view->search = $this->_helper
+      ->search([],
+               (new Class_User_SearchCriteria($params))
+               ->addCriteria(new Class_User_SearchCriteria_RoleLevelLimit($params)));
   }
 
 
diff --git a/application/modules/admin/views/scripts/newsletter/edit-subscribers.phtml b/application/modules/admin/views/scripts/newsletter/edit-subscribers.phtml
index c1f138dbf63313d5e078902022172c874b89a68e..11d116719451188b721b6c808251c7fb9513a29d 100644
--- a/application/modules/admin/views/scripts/newsletter/edit-subscribers.phtml
+++ b/application/modules/admin/views/scripts/newsletter/edit-subscribers.phtml
@@ -45,50 +45,4 @@ echo $this->tagModelTable($this->groups,
                           [$actions],
                           'newsletter_user_groups');
 
-$build_url = function($action) {
-  return $this->url(array_merge(['module' => 'admin',
-                                 'controller' => 'newsletter',
-                                 'action' => 'edit-subscribers',
-                                 'id' => $this->newsletter->getId(),
-                                 'page' => $this->page ? $this->page: null],
-                                $this->params),
-                    null, true) . '/' . $action . '/%s';
-};
-
-$no_mail_action =   $this->tag('div' ,
-                               $skin->renderActionIconOn('help',
-                                                         $this,
-                                                         ['title' => $this->_('Utilisateur sans mail'),
-                                                          'class' => 'ico']),
-                               ['class' => 'actions']);
-
-$actions = function($model) use ($build_url, $no_mail_action) {
-  $is_recipient = $this->newsletter->hasRecipient($model, false);
-  $is_blacklisted = $this->newsletter->isBlackListed($model);
-
-  if (!$is_recipient)
-    return $this->renderModelActions($model,
-                                     [['url' => $build_url('subscribe'),
-                                       'icon' => 'add',
-                                       'label' => $this->_('Inscrire')]
-                                     ]);
-
-  if ($is_blacklisted)
-    return $this->renderModelActions($model,
-                               [['url' => $build_url('subscribe'),
-                                 'icon' => 'back',
-                                 'label' => $this->_('Réinscrire')]
-                                ]);;
-
-  if (! $model->hasMail())
-    return $no_mail_action;
-
-  return $this->renderModelActions($model,
-                                   [['url' => $build_url('unsubscribe'),
-                                     'icon' => 'cancel',
-                                     'label' => $this->_('Désinscrire')]
-                                   ]);
-};
-
-echo '<br><br>'
-  . $this->Admin_SearchUsers($this->users, $this->total, $this->form, $this->page, $this->params, $actions);
+echo '<br><br>' . $this->searchNewsletterUsers($this->newsletter, $this->search);
diff --git a/application/modules/admin/views/scripts/usergroup-agenda/all.phtml b/application/modules/admin/views/scripts/usergroup-agenda/all.phtml
index 093655819dfb5450b7ff41220ca4f0ce6f79ff17..7261467d4b42b3fdf759975f6db4f9a4a96866c5 100644
--- a/application/modules/admin/views/scripts/usergroup-agenda/all.phtml
+++ b/application/modules/admin/views/scripts/usergroup-agenda/all.phtml
@@ -1,2 +1,2 @@
 <?php
-echo $this->searchRendezVous($this->search, $this->rendezvous);
+echo $this->searchRendezVous($this->search);
diff --git a/application/modules/admin/views/scripts/usergroup-agenda/user-select.phtml b/application/modules/admin/views/scripts/usergroup-agenda/user-select.phtml
index d7610e8ad0368efefca8d32247b8f9067b73cb49..4b100db8810b55c7db6e6a7e454624c1b5887aa7 100644
--- a/application/modules/admin/views/scripts/usergroup-agenda/user-select.phtml
+++ b/application/modules/admin/views/scripts/usergroup-agenda/user-select.phtml
@@ -1,16 +1,3 @@
 <?php
-$actions = function($model) {
-  return $this->renderModelActions($model,
-                                   [['url' => '#',
-                                     'icon' => 'add_user',
-                                     'label' => $this->_('Ajouter "%s" comme destinataire',
-                                                         $model->getNomComplet()),
-                                     'anchorOptions' => ['class' => 'user_add_action',
-                                                         'data-userid' => $model->getId(),
-                                                         'data-username' => $model->getNomComplet()]]
-                                   ]);
-};
-
-echo $this->tag('div', $this->Admin_SearchUsers($this->users, $this->total, $this->form,
-                                                $this->page, $this->params, $actions),
+echo $this->tag('div', $this->searchRendezVousUsers($this->search),
                 ['class' => 'modules']);
diff --git a/application/modules/admin/views/scripts/users/index.phtml b/application/modules/admin/views/scripts/users/index.phtml
index aca988264f97563a108ce870efbf6764711e2b9f..9ff564788b2319a6c37b7522197efa7c1834e452 100644
--- a/application/modules/admin/views/scripts/users/index.phtml
+++ b/application/modules/admin/views/scripts/users/index.phtml
@@ -1,5 +1,5 @@
 <?php
-if(Class_Users::getIdentity()->isAdmin())
+if (Class_Users::isCurrentUserAdmin())
   echo $this->Button_New((new Class_Entity())
                          ->setText($this->_('Ajouter un utilisateur')));
 
@@ -23,12 +23,4 @@ echo $this->button(
                                                          'users'),
                                             ['style' => 'filter: invert();'])));
 
-echo $this->Admin_SearchUsers($this->users,
-                              $this->total,
-                              $this->form,
-                              $this->page,
-                              $this->params,
-                              function($model)
-                              {
-                                return $this->renderPluginsActions($model);
-                              });
+echo $this->searchUsers($this->search, true);
diff --git a/application/modules/admin/views/scripts/users/mass-delete-run.phtml b/application/modules/admin/views/scripts/users/mass-delete-run.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..ec1035177e9c22af357d3f84e5a5f27683c970aa
--- /dev/null
+++ b/application/modules/admin/views/scripts/users/mass-delete-run.phtml
@@ -0,0 +1,47 @@
+<?php
+echo $this->tag('h3', 'Critères')
+  . $this->searchCriteriaDescription($this->criteria_description,
+                                     $this->_('Tous les utilisateurs'));
+
+echo $this->tag('h3', 'Progression');
+
+echo $this->tag('div', $this->tag('span', ''),
+                ['id' => 'userDeleteProgress']);
+
+Class_ScriptLoader::getInstance()
+->addJQueryReady('
+$("#userDeleteProgress").progressbar({value:false, max:' . $this->count . '});
+
+function runNext(done) {
+  $.getJSON("' . $this->url(['module' => 'admin',
+                             'controller' => 'users',
+                             'action' => 'mass-delete-run-step',
+                             'total' => $this->count],
+                            null, true) . '/done/" + done + "?' . http_build_query($this->criteria_params) . '",
+    function(data) {
+      $("#userDeleteProgress").progressbar("option", "max", data.total);
+      $("#userDeleteProgress").progressbar("value", data.done);
+      if (data.done < data.total)
+        return runNext(data.done);
+
+      $("#userDeleteProgress").progressbar("destroy");
+      $("#userDeleteProgress").html(\'<p class="success">Suppression terminée</p>\');
+    }
+  )
+  .fail(function() {
+    $("#userDeleteProgress").progressbar("destroy");
+    $("#userDeleteProgress").html(\'<p class="error">Une erreur est survenue</p>\');
+  });
+}
+
+runNext(0);
+');
+
+
+echo $this->tag('br')
+  . $this->button_Back((new Class_Entity)
+                       ->setUrl($this->url(['module' => 'admin',
+                                            'controller' => 'users',
+                                            'action' => 'index'],
+                                           null, true)
+                                . '?' . http_build_query($this->criteria_params)));
diff --git a/application/modules/admin/views/scripts/users/mass-delete.phtml b/application/modules/admin/views/scripts/users/mass-delete.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..1e3a3b0285b541c614120b2f671f142952030bac
--- /dev/null
+++ b/application/modules/admin/views/scripts/users/mass-delete.phtml
@@ -0,0 +1,57 @@
+<?php
+echo
+  $this->tag('h3', $this->_('ATTENTION : vous vous apprêtez à supprimer %s utilisateurs',
+                            $this->count),
+             ['class' => 'error'])
+
+  . $this->tag('p', $this->_('Correspondants aux critères suivants : '));
+
+echo $this->searchCriteriaDescription($this->criteria_description,
+                                      $this->_('Tous les utilisateurs'));
+
+echo $this->tag('p', $this->tag('strong', $this->_('NB : Votre compte ne peut pas être supprimé par cette mécanique.')),
+                ['style' => 'font-size:80%']);
+
+echo
+  $this->tag('h3', $this->_('Données liées supprimées'))
+  . $this->tag('p', $this->_('Les types de données suivants sont supprimés en même temps que les utilisateurs'))
+  . $this->tagUlLi([$this->_('L\'appartenance aux groupes'),
+                    $this->_('L\'inscription aux lettres d\'informations'),
+                    $this->_('L\'apartenance aux agendas de rendez-vous'),
+                    $this->_('Les cartes liées'),
+                    $this->_('Les recherches enregistrées')])
+  ;
+
+echo
+  $this->tag('h3', $this->_('Données liées anonymisées'))
+  . $this->tag('p', $this->_('Les types de données suivants sont anonymisés lorsque leurs utilisateurs sont supprimés'))
+  . $this->tagUlLi([$this->_('L\'apartenance à un envoi de lettre d\'informations'),
+                    $this->_('Les avis sur les notices'),
+                    $this->_('Les avis sur les articles'),
+                    $this->_('Les paniers de notices'),
+                    $this->_('Les suggestions d\'achat'),
+                    $this->_('L\'inscription aux activités'),
+                    $this->_('L\'intervention dans les activités'),
+                    $this->_('Les formulaires envoyés'),
+                    $this->_('Les réservations de postes multimédia')])
+  ;
+
+echo
+  $this->tag('br')
+  . $this->button_Back((new Class_Entity)
+                       ->setUrl($this->url(['module' => 'admin',
+                                            'controller' => 'users',
+                                            'action' => 'index'],
+                                           null, true)
+                                . '?' . http_build_query($this->criteria_params)))
+
+  . $this->button((new Class_Entity)
+                  ->setText($this->_('Lancer la suppression'))
+                  ->setImage($this->tagImg(Class_Admin_Skin::current()
+                                           ->getIconUrl('buttons', 'validate')))
+                  ->setUrl($this->url(['module' => 'admin',
+                                       'controller' => 'users',
+                                       'action' => 'mass-delete-run'],
+                                      null, true)
+                           . '?' . http_build_query($this->criteria_params)))
+  ;
diff --git a/library/Class/Formulaire.php b/library/Class/Formulaire.php
index e8dcffa769e0d05c6fee1cc78cf25de729184d40..9f06f343a717a6441074489b3a862d9060328f44 100644
--- a/library/Class/Formulaire.php
+++ b/library/Class/Formulaire.php
@@ -48,9 +48,8 @@ class Class_Formulaire extends Storm_Model_Abstract {
 
   public static function mergeDataNames($formulaires) {
     $names = [];
-    foreach($formulaires as $formulaire) {
+    foreach($formulaires as $formulaire)
       $names=array_merge($names,$formulaire->getDataNames());
-    }
 
     return array_unique($names);
   }
@@ -92,9 +91,9 @@ class Class_Formulaire extends Storm_Model_Abstract {
     return '';
   }
 
+
   public function getDataNames() {
     return array_keys(array_change_key_case($this->getDatas()));
-
   }
 
 
@@ -173,4 +172,12 @@ class Class_Formulaire extends Storm_Model_Abstract {
   public function beValidated() {
     return $this->setValidated(true);
   }
+
+
+  public function anonymize() {
+    if ($mail = $this->getMail())
+      $this->setMailAnswer(serialize($mail->clearRecipients()));
+
+    return $this;
+  }
 }
diff --git a/library/Class/Newsletter/DispatchUser.php b/library/Class/Newsletter/DispatchUser.php
index 7935ad5a2a851482fa11d6a2e1ef62c9de8f8d40..b054dddc9f4d7b3f007775d62ba9cf708da0ce4b 100644
--- a/library/Class/Newsletter/DispatchUser.php
+++ b/library/Class/Newsletter/DispatchUser.php
@@ -50,4 +50,9 @@ class Class_Newsletter_DispatchUser extends Storm_Model_Abstract {
     $this->setSent(1)->save();
     return $this;
   }
+
+
+  public function anonymize() {
+    return $this->setMail('');
+  }
 }
diff --git a/library/Class/RendezVous/SearchCriteria/Date.php b/library/Class/RendezVous/SearchCriteria/Date.php
index 948edb414408f3c69027a74f28636a857c350b64..40d260af5bb3885d7a519289c326452f561c7747 100644
--- a/library/Class/RendezVous/SearchCriteria/Date.php
+++ b/library/Class/RendezVous/SearchCriteria/Date.php
@@ -20,81 +20,24 @@
  */
 
 
-class Class_RendezVous_SearchCriteria_Date extends Class_SearchCriteria_Abstract {
-  use Trait_TimeSource;
+class Class_RendezVous_SearchCriteria_Date extends Class_SearchCriteria_DateRange {
+  protected $_name = 'date';
 
-  const DATE_FORMAT = 'd/m/Y';
-
-  protected
-    $_name = 'date',
-    $_start_name,
-    $_end_name,
-    $_value_start = '',
-    $_value_end = '';
-
-
-  public function __construct($params) {
-    $this->_start_name = $this->getName() . '_start';
-    $this->_end_name = $this->getName() . '_end';
-
-    parent::__construct($params);
-
-    $this->_value_start = isset($params[$this->_start_name])
-      ? $this->_filterDate($params[$this->_start_name])
-      : $this->getTimeSource()->dateFormat(static::DATE_FORMAT);
-    $this->_element->setStartValue($this->_value_start);
-
-    $this->_value_end = isset($params[$this->_end_name])
-      ? $this->_filterDate($params[$this->_end_name])
-      : $this->getTimeSource()->asDateTime()
-             ->modify('+3 month')
-             ->format(static::DATE_FORMAT);
-    $this->_element->setEndValue($this->_value_end);
-  }
-
-
-  protected function buildElement() {
-    $options = ['label' => $this->_('Date'),
-                'start' => ['name' => $this->_start_name],
-                'end' => ['name' => $this->_end_name],
-    ];
-
-    return new ZendAfi_Form_Element_DateRangePicker($this->getName(), $options);
+  protected function _defaultStart() {
+    return $this->getTimeSource()->dateFormat(static::DATE_FORMAT);
   }
 
 
-  public function acceptSearchVisitor($visitor) {
-    if ($this->_value_start)
-      $visitor->addWhereParam('date >= "' . $this->_sqlFormat($this->_value_start) . '"');
-
-    if ($this->_value_end)
-      $visitor->addWhereParam('date <= "' . $this->_sqlFormat($this->_value_end) . '"');
+  protected function _defaultEnd() {
+    return $this->getTimeSource()->asDateTime()
+                ->modify('+3 month')
+                ->format(static::DATE_FORMAT);
   }
 
 
-  protected function _filterDate($value) {
-    if (null === $value)
-      return;
-
-    if ('' === $value)
-      return '';
-
-    if (!(new ZendAfi_Validate_DateFormat())->isValid($value, static::DATE_FORMAT)) {
-      $this->_element->addError($this->_('Les dates doivent être au format JJ/MM/AAAA'));
-      return;
-    }
-
-    return $value;
-  }
-
-
-  protected function _sqlFormat($value) {
-    return implode('-', array_reverse(explode('/', $value)));
-  }
-
-
-  public function getCompositeValues() {
-    return [$this->_start_name => $this->_value_start,
-            $this->_end_name => $this->_value_end];
+  protected function buildElement() {
+    $element = parent::buildElement();
+    $element->setLabel($this->_('Date'));
+    return $element;
   }
 }
diff --git a/library/Class/SearchCriteria.php b/library/Class/SearchCriteria.php
index bdd4b1f7f7e043c24a090f91847350c93d58d246..282368b6136f2d18f59b969f254bbcca6cfcab2e 100644
--- a/library/Class/SearchCriteria.php
+++ b/library/Class/SearchCriteria.php
@@ -48,13 +48,13 @@ abstract class Class_SearchCriteria {
   }
 
 
-  public function findPage($page=1) {
+  public function findPage($page=1, $page_size=20) {
     $this->_buildSearchParams();
 
     return $this->_has_no_result
       ? []
       : call_user_func([$this->_model_class, 'findAllBy'],
-                       array_merge($this->_search_params, ['limitPage' => [$page, 20]]));
+                       array_merge($this->_search_params, ['limitPage' => [$page, $page_size]]));
   }
 
 
@@ -140,4 +140,11 @@ abstract class Class_SearchCriteria {
     $this->_has_no_result = true;
     return $this;
   }
+
+
+  public function describeOn($view) {
+    return array_filter((new Storm_Collection($this->_criteria))
+                        ->collect(function($c) use($view) { return $c->describeOn($view); })
+                        ->getArrayCopy());
+  }
 }
diff --git a/library/Class/SearchCriteria/Abstract.php b/library/Class/SearchCriteria/Abstract.php
index aa8233c0978495f823efb437dc190ba0685f4fdd..817d0e18023714c0cf3ef7d07284dc5b70db5492 100644
--- a/library/Class/SearchCriteria/Abstract.php
+++ b/library/Class/SearchCriteria/Abstract.php
@@ -66,4 +66,8 @@ abstract class Class_SearchCriteria_Abstract {
 
     $visitor->addParam($this->_name, $this->_value);
   }
+
+
+  public function describeOn($view) {
+  }
 }
\ No newline at end of file
diff --git a/library/Class/SearchCriteria/DateRange.php b/library/Class/SearchCriteria/DateRange.php
new file mode 100644
index 0000000000000000000000000000000000000000..81d54b34159abb03778e43490c8379d1b14182c9
--- /dev/null
+++ b/library/Class/SearchCriteria/DateRange.php
@@ -0,0 +1,121 @@
+<?php
+/**
+ * Copyright (c) 2012-2019, 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_SearchCriteria_DateRange extends Class_SearchCriteria_Abstract {
+  use Trait_TimeSource;
+
+  const DATE_FORMAT = 'd/m/Y';
+
+  protected
+    $_start_name,
+    $_end_name,
+    $_value_start = '',
+    $_value_end = '';
+
+
+  public function __construct($params) {
+    $this->_start_name = $this->getName() . '_start';
+    $this->_end_name = $this->getName() . '_end';
+
+    parent::__construct($params);
+
+    $this->_value_start = isset($params[$this->_start_name])
+      ? $this->_filterDate($params[$this->_start_name])
+      : $this->_defaultStart();
+    $this->_element->setStartValue($this->_value_start);
+
+    $this->_value_end = isset($params[$this->_end_name])
+      ? $this->_filterDate($params[$this->_end_name])
+      : $this->_defaultEnd();
+    $this->_element->setEndValue($this->_value_end);
+  }
+
+
+  protected function _defaultStart() {
+    return '';
+  }
+
+
+  protected function _defaultEnd() {
+    return '';
+  }
+
+
+  protected function buildElement() {
+    return new ZendAfi_Form_Element_DateRangePicker($this->getName(),
+                                                    ['start' => ['name' => $this->_start_name],
+                                                     'end' => ['name' => $this->_end_name]]);
+  }
+
+
+  public function acceptSearchVisitor($visitor) {
+    if ($this->_value_start)
+      $visitor->addWhereParam($this->_name . ' >= "' . $this->_sqlFormat($this->_value_start) . '"');
+
+    if ($this->_value_end)
+      $visitor->addWhereParam($this->_name . ' <= "' . $this->_sqlFormat($this->_value_end) . '"');
+  }
+
+
+  protected function _filterDate($value) {
+    if (null === $value)
+      return;
+
+    if ('' === $value)
+      return '';
+
+    if (!(new ZendAfi_Validate_DateFormat())->isValid($value, static::DATE_FORMAT)) {
+      $this->_element->addError($this->_('Les dates doivent être au format JJ/MM/AAAA'));
+      return;
+    }
+
+    return $value;
+  }
+
+
+  protected function _sqlFormat($value) {
+    return implode('-', array_reverse(explode('/', $value)));
+  }
+
+
+  public function getCompositeValues() {
+    return [$this->_start_name => $this->_value_start,
+            $this->_end_name => $this->_value_end];
+  }
+
+
+  public function describeOn($view) {
+    if ('' == $this->_value_start && '' == $this->_value_end)
+      return;
+
+    $description = $this->_element->getLabel() . ' : ';
+    if ('' != $this->_value_start && '' != $this->_value_end)
+      return $description . $this->_('entre le %s et le %s',
+                                     $this->_value_start, $this->_value_end);
+
+    if ('' != $this->_value_start)
+      return $description . $this->_('depuis le %s', $this->_value_start);
+
+    if ('' != $this->_value_end)
+      return $description . $this->_('jusqu\'au %s', $this->_value_end);
+  }
+}
diff --git a/library/Class/SearchCriteria/Select.php b/library/Class/SearchCriteria/Select.php
new file mode 100644
index 0000000000000000000000000000000000000000..ce2cbfeeb50b3684131f41e64e6ca258d3b825e8
--- /dev/null
+++ b/library/Class/SearchCriteria/Select.php
@@ -0,0 +1,36 @@
+<?php
+/**
+ * Copyright (c) 2012-2019, 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_SearchCriteria_Select extends Class_SearchCriteria_Abstract {
+  protected $_value = Class_SearchCriteria_Abstract::DEFAULT_VALUE;
+
+  protected function buildElement() {
+    return new Zend_Form_Element_Select($this->getName(), ['value' => $this->_value]);
+  }
+
+
+  public function describeOn($view) {
+    return ($this->_element && static::DEFAULT_VALUE != $this->_value)
+      ? $this->_element->getLabel() . ' : ' . $this->_element->getMultiOption($this->_value)
+      : '';
+  }
+}
diff --git a/library/Class/TableDescription/RendezVousUsers.php b/library/Class/TableDescription/RendezVousUsers.php
new file mode 100644
index 0000000000000000000000000000000000000000..85f1f9d006938ab8421991ccf6144218a4476e02
--- /dev/null
+++ b/library/Class/TableDescription/RendezVousUsers.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ * Copyright (c) 2012-2019, 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_RendezVousUsers extends Class_TableDescription_UsersBasic {
+  public function init() {
+    parent::init();
+    $this
+      ->addRowAction(function($model)
+                     {
+                       return $this->renderModelActions($model,
+                                                        [['url' => '#',
+                                                          'icon' => 'add_user',
+                                                          'label' => $this->_('Ajouter "%s" comme destinataire',
+                                                                              $model->getNomComplet()),
+                                                          'anchorOptions' => ['class' => 'user_add_action',
+                                                                              'data-userid' => $model->getId(),
+                                                                              'data-username' => $model->getNomComplet()]]
+                                                        ]);
+})
+      ;
+  }
+}
diff --git a/library/Class/TableDescription/Users.php b/library/Class/TableDescription/Users.php
index 5eb31b27d5b7904e679ae563fbf78ca14a6aa513..f72fcf68be1f35e6b12493a7595b44bc50e393e6 100644
--- a/library/Class/TableDescription/Users.php
+++ b/library/Class/TableDescription/Users.php
@@ -20,25 +20,9 @@
  */
 
 
-class Class_TableDescription_Users extends Class_TableDescription {
+class Class_TableDescription_Users extends Class_TableDescription_UsersBasic {
   public function init() {
-    $acl = new ZendAfi_Acl_AdminControllerRoles();
-
-    $role_renderer = function($model) use($acl) {
-      return $acl->getLibelleRole($model->getRoleLevel());
-    };
-
-    $library_renderer = function($model) {
-      return $model->getLibelleBib();
-    };
-
-    $this
-      ->addColumn($this->_('Identifiant'), 'login')
-      ->addColumn($this->_('Nom'), 'nom')
-      ->addColumn($this->_('Prénom'), 'prenom')
-      ->addColumn($this->_('Role'), ['attribute' => 'role_level',
-                                     'callback' => $role_renderer])
-      ->addColumn($this->_('Bibliothèque'), ['callback' => $library_renderer,
-                                             'sortable' => false]);
+    parent::init();
+    $this->addRowPluginsActions();
   }
 }
diff --git a/library/Class/TableDescription/UsersBasic.php b/library/Class/TableDescription/UsersBasic.php
new file mode 100644
index 0000000000000000000000000000000000000000..e72654e0bb75452688835d48bc7ed1b2483c6a8d
--- /dev/null
+++ b/library/Class/TableDescription/UsersBasic.php
@@ -0,0 +1,42 @@
+<?php
+/**
+ * Copyright (c) 2012-2019, 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_UsersBasic extends Class_TableDescription {
+  public function init() {
+    $role_renderer = function($model) {
+      return ZendAfi_Acl_AdminControllerRoles::getLibelleRole($model->getRoleLevel());
+    };
+
+    $library_renderer = function($model) {
+      return $model->getLibelleBib();
+    };
+
+    $this
+      ->addColumn($this->_('Identifiant'), 'login')
+      ->addColumn($this->_('Nom'), 'nom')
+      ->addColumn($this->_('Prénom'), 'prenom')
+      ->addColumn($this->_('Role'), ['attribute' => 'role_level',
+                                     'callback' => $role_renderer])
+      ->addColumn($this->_('Bibliothèque'), ['callback' => $library_renderer,
+                                             'sortable' => false]);
+  }
+}
diff --git a/library/Class/User/SearchCriteria.php b/library/Class/User/SearchCriteria.php
index 867c1a870334ec02043cf6c7cf52087edd64d525..8162b9b8f2609903673572cab2899b91e3d3096b 100644
--- a/library/Class/User/SearchCriteria.php
+++ b/library/Class/User/SearchCriteria.php
@@ -27,59 +27,65 @@ class Class_User_SearchCriteria extends Class_SearchCriteria {
     $this->_criteria = [new Class_User_SearchCriteriaLibrary($params),
                         new Class_User_SearchCriteriaRoleLevel($params),
                         new Class_User_SearchCriteriaValidSubscription($params),
+                        new Class_User_SearchCriteria_DateFin($params),
+                        new Class_User_SearchCriteria_InLastSigbExport($params),
+                        new Class_User_SearchCriteria_DateMaj($params),
                         new Class_User_SearchCriteria_NumberOfReviews($params),
                         new Class_User_SearchCriteria_NumberOfBaskets($params),
                         new Class_User_SearchCriteriaSearchFor($params),
                         new Class_User_SearchCriteria_Order($params)];
   }
-
-
-  public function getForm() {
-    Class_ScriptLoader::getInstance()
-      ->addJQueryReady('formSelectToggleVisibilityForElement("#search_role_level", $("#search_valid_subscription").closest("tr"), ["2"]);');
-
-    return parent::getForm();
-  }
 }
 
 
 
-
-class Class_User_SearchCriteriaLibrary extends Class_SearchCriteria_Abstract{
-  protected
-    $_name = 'id_site',
-    $_value = Class_SearchCriteria_Abstract::DEFAULT_VALUE;
+class Class_User_SearchCriteriaLibrary extends Class_SearchCriteria_Select {
+  protected $_name = 'id_site';
 
   public function buildElement() {
-    return new Zend_Form_Element_Select($this->getName(),
-                                        ['label' => $this->_('Bibliothèque'),
-                                         'multiOptions' => ['all' => $this->_('Toutes')] + Class_Bib::findAllLabels(),
-                                         'value' => $this->_value]);
+    return parent::buildElement()
+      ->setLabel($this->_('Bibliothèque'))
+      ->setMultiOptions(['all' => $this->_('Toutes')] + Class_Bib::findAllLabels());
   }
 }
 
 
 
-class Class_User_SearchCriteriaRoleLevel extends Class_SearchCriteria_Abstract {
-  protected
-    $_name = 'role_level',
-    $_value = Class_SearchCriteria_Abstract::DEFAULT_VALUE;
+class Class_User_SearchCriteriaRoleLevel extends Class_SearchCriteria_Select {
+  protected $_name = 'role_level';
 
 
   public function buildElement() {
+    $this->_headScript();
+
     if ((!Class_Users::getIdentity()->isAdmin())
-        && $this->_value == Class_SearchCriteria_Abstract::DEFAULT_VALUE)
+        && static::DEFAULT_VALUE == $this->_value)
       $this->_value = ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB;
 
-    return new Zend_Form_Element_Select($this->getName(),
-                                        ['label' => $this->_('Niveau d\'accès'),
-                                         'multiOptions' => ['all' => $this->_('Tous')] + ZendAfi_Acl_AdminControllerRoles::getRolesLabelsWithOutSuperAdmin(),
-                                         'value' => $this->_value]);
+    return parent::buildElement()
+      ->setLabel($this->_('Niveau d\'accès'))
+      ->setMultiOptions(['all' => $this->_('Tous')]
+                        + ZendAfi_Acl_AdminControllerRoles::getRolesLabelsWithOutSuperAdmin());
   }
 
 
   public function isAbonneSigb() {
-    return $this->_value == ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB;
+    return ZendAfi_Acl_AdminControllerRoles::ABONNE_SIGB == $this->_value;
+  }
+
+
+  protected function _headScript() {
+    $toggles = array_map(function($other)
+                         {
+                           return sprintf('formSelectToggleVisibilityForElement("#%s", $("#%s").closest("tr"), ["2"]);',
+                                          $this->getName(),
+                                          static::NAME_PREFIX . $other);
+                         },
+                         ['valid_subscription',
+                          'statut',
+                          'date_fin_start']);
+
+    Class_ScriptLoader::getInstance()->addJQueryReady(implode($toggles));
   }
 }
 
@@ -112,6 +118,13 @@ class Class_User_SearchCriteriaValidSubscription extends Class_SearchCriteria_Ab
 
     $visitor->addWhereParam('STR_TO_DATE(date_fin, \'%Y-%m-%d\') >= CURDATE()');
   }
+
+
+  public function describeOn($view) {
+    return (0 != $this->_value)
+      ? $this->_element->getLabel()
+      : '';
+  }
 }
 
 
@@ -153,4 +166,11 @@ class Class_User_SearchCriteriaSearchFor extends Class_SearchCriteria_Abstract{
 
     $visitor->addWhereParam(implode(' OR ', $table_or));
   }
+
+
+  public function describeOn($view) {
+    return ($this->_value)
+      ? ($this->_element->getLabel() . ' : ' . $this->_value)
+      : '';
+  }
 }
\ No newline at end of file
diff --git a/library/Class/User/SearchCriteria/DateFin.php b/library/Class/User/SearchCriteria/DateFin.php
new file mode 100644
index 0000000000000000000000000000000000000000..67c733eb10629880935e41f8fd13ccb3f698a2c5
--- /dev/null
+++ b/library/Class/User/SearchCriteria/DateFin.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Copyright (c) 2012-2019, 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_User_SearchCriteria_DateFin extends Class_SearchCriteria_DateRange {
+  protected
+    $_name = 'date_fin';
+
+
+  protected function buildElement() {
+    return parent::buildElement()->setLabel($this->_('Date de fin d\'abonnement'));
+  }
+}
diff --git a/library/Class/User/SearchCriteria/DateMaj.php b/library/Class/User/SearchCriteria/DateMaj.php
new file mode 100644
index 0000000000000000000000000000000000000000..ee3fd5661109a2f954c8bc30548062562f2842bc
--- /dev/null
+++ b/library/Class/User/SearchCriteria/DateMaj.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Copyright (c) 2012-2019, 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_User_SearchCriteria_DateMaj extends Class_SearchCriteria_DateRange {
+  protected
+    $_name = 'date_maj';
+
+
+  protected function buildElement() {
+    return parent::buildElement()->setLabel($this->_('Mis à jour'));
+  }
+}
diff --git a/library/Class/User/SearchCriteria/InLastSigbExport.php b/library/Class/User/SearchCriteria/InLastSigbExport.php
new file mode 100644
index 0000000000000000000000000000000000000000..98e93f2fa520e7d93a077a71bba40af4ab27d7fb
--- /dev/null
+++ b/library/Class/User/SearchCriteria/InLastSigbExport.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Copyright (c) 2012-2019, 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_User_SearchCriteria_InLastSigbExport extends Class_SearchCriteria_Select {
+  protected $_name = 'statut';
+
+  protected function buildElement() {
+    return parent::buildElement()
+      ->setLabel($this->_('Présent dans le dernier export SIGB'))
+      ->setMultiOptions([static::DEFAULT_VALUE => $this->_('Indifférent'),
+                         '0' => $this->_('Oui'),
+                         '1' => $this->_('Non')]);
+  }
+}
diff --git a/library/Class/User/SearchCriteria/NumberOfBaskets.php b/library/Class/User/SearchCriteria/NumberOfBaskets.php
index 38b7010fdaddedae19f30bdcf6e9013debfe848b..76997334343bf24853a0623c0fd6686598d8c26c 100644
--- a/library/Class/User/SearchCriteria/NumberOfBaskets.php
+++ b/library/Class/User/SearchCriteria/NumberOfBaskets.php
@@ -19,24 +19,21 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
 
-class Class_User_SearchCriteria_NumberOfBaskets extends Class_SearchCriteria_Abstract {
-  protected
-    $_name = 'basket',
-    $_value = 'all';
-
+class Class_User_SearchCriteria_NumberOfBaskets extends Class_SearchCriteria_Select {
+  protected $_name = 'basket';
 
   public function buildElement() {
-    return new Zend_Form_Element_Select($this->getName(),
-                                        ['label' => $this->_('A créé des paniers'),
-                                         'multiOptions' => ['yes' => $this->_('Oui'),
-                                                            'no' => $this->_('Non'),
-                                                            'all' => $this->_('Indifférent')],
-                                         'value' => $this->_value]);
+    return parent::buildElement()
+      ->setLabel($this->_('A créé des paniers'))
+      ->setMultiOptions(['yes' => $this->_('Oui'),
+                         'no' => $this->_('Non'),
+                         'all' => $this->_('Indifférent')])
+      ;
   }
 
 
   public function acceptSearchVisitor($visitor) {
-    if ('all' == $this->_value)
+    if (static::DEFAULT_VALUE == $this->_value)
       return;
 
     $ids = Class_PanierNotice::findAllUserIds();
diff --git a/library/Class/User/SearchCriteria/NumberOfReviews.php b/library/Class/User/SearchCriteria/NumberOfReviews.php
index 3b34c510fca6f907634d0bb441775d93dd6df141..5f069d055e62274321a3213e77de111ff14598d6 100644
--- a/library/Class/User/SearchCriteria/NumberOfReviews.php
+++ b/library/Class/User/SearchCriteria/NumberOfReviews.php
@@ -19,24 +19,21 @@
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
 
-class Class_User_SearchCriteria_NumberOfReviews extends Class_SearchCriteria_Abstract {
-  protected
-    $_name = 'review',
-    $_value = 'all';
-
+class Class_User_SearchCriteria_NumberOfReviews extends Class_SearchCriteria_Select {
+  protected $_name = 'review';
 
   public function buildElement() {
-    return new Zend_Form_Element_Select($this->getName(),
-                                        ['label' => $this->_('A rédigé des avis'),
-                                         'multiOptions' => ['yes' => $this->_('Oui'),
-                                                            'no' => $this->_('Non'),
-                                                            'all' => $this->_('Indifférent')],
-                                         'value' => $this->_value]);
+    return parent::buildElement()
+      ->setLabel($this->_('A rédigé des avis'))
+      ->setMultiOptions(['yes' => $this->_('Oui'),
+                         'no' => $this->_('Non'),
+                         'all' => $this->_('Indifférent')])
+      ;
   }
 
 
   public function acceptSearchVisitor($visitor) {
-    if ('all' == $this->_value)
+    if (static::DEFAULT_VALUE == $this->_value)
       return;
 
     $ids = Class_AvisNotice::findAllUserIds();
diff --git a/library/Class/Users.php b/library/Class/Users.php
index 9ca72c7a86b1a08d98f3390f73fc00f90a5b30c3..3b7a433574aaf5a0f4441133619c00c923a8dc5b 100644
--- a/library/Class/Users.php
+++ b/library/Class/Users.php
@@ -1942,4 +1942,13 @@ class Class_Users extends Storm_Model_Abstract {
   public function isAllowedToAccess($controller, $action) {
     return (new ZendAfi_Acl_AdminControllerGroup)->isAllowed($this, $controller, $action);
   }
+
+
+  public function afterDelete() {
+    foreach($this->getDispatchUsers() as $dispatch_user)
+      $dispatch_user->anonymize()->save();
+
+    foreach($this->getFormulaires() as $form)
+      $form->anonymize()->save();
+  }
 }
diff --git a/library/ZendAfi/Controller/Action/Helper/SearchRendezVous.php b/library/ZendAfi/Controller/Action/Helper/Search.php
similarity index 81%
rename from library/ZendAfi/Controller/Action/Helper/SearchRendezVous.php
rename to library/ZendAfi/Controller/Action/Helper/Search.php
index 7bc013dd97e476bb2ba69e0e307792fbbab8c80c..03bad744276f09963c92c8a3d00a49b87d456609 100644
--- a/library/ZendAfi/Controller/Action/Helper/SearchRendezVous.php
+++ b/library/ZendAfi/Controller/Action/Helper/Search.php
@@ -1,6 +1,6 @@
 <?php
 /**
- * Copyright (c) 2012-2014, Agence Française Informatique (AFI). All rights reserved.
+ * Copyright (c) 2012-2019, 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
@@ -20,11 +20,10 @@
  */
 
 
-class ZendAfi_Controller_Action_Helper_SearchRendezVous
+class ZendAfi_Controller_Action_Helper_Search
   extends Zend_Controller_Action_Helper_Abstract {
 
   protected
-    $view,
     $_models = [],
     $_total = 0,
     $_page = 1,
@@ -33,21 +32,18 @@ class ZendAfi_Controller_Action_Helper_SearchRendezVous
 
   /**
    * @param $action_params array
-   * @param $criteria Class_RendezVous_SearchCriteria
+   * @param $criteria Class_SearchCriteria
    */
-  public function searchRendezVous($action_params, $criteria) {
+  protected function _search($action_params, $criteria) {
     if (!$action_params)
       $action_params = [];
 
-    $this->view = $this->getActionController()->view;
-
     $this->_page = $this->_getParam('page', 1);
     $this->_models = $criteria->findPage($this->_page);
     $this->_total = $criteria->count();
     $this->_form = $this->_prepareForm($action_params, $criteria);
     $this->_params = array_merge($criteria->getFormValues(),
-                                 ['page' => $this->_page,
-                                  'search_order' => $this->_getParam('search_order', 'date desc')]);
+                                 ['page' => $this->_page]);
 
     return $this;
   }
@@ -96,6 +92,6 @@ class ZendAfi_Controller_Action_Helper_SearchRendezVous
 
 
   public function direct($action_params=[], $criteria) {
-    return $this->searchRendezVous($action_params, $criteria);
+    return $this->_search($action_params, $criteria);
   }
 }
\ No newline at end of file
diff --git a/library/ZendAfi/Controller/Action/Helper/UserSearch.php b/library/ZendAfi/Controller/Action/Helper/UserSearch.php
deleted file mode 100644
index 68d0f1158b4ecb4c8cd5498c27b2cae2c70b4f9e..0000000000000000000000000000000000000000
--- a/library/ZendAfi/Controller/Action/Helper/UserSearch.php
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-/**
- * Copyright (c) 2012-2014, 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_UserSearch extends Zend_Controller_Action_Helper_Abstract {
-  protected $view;
-
-  /**
-   * @param $action_params array
-   * @param $criteria Class_User_SearchCriteria
-   */
-  public function userSearch($action_params, $criteria) {
-    if (!$action_params)
-      $action_params = [];
-
-    $this->view = $this->getActionController()->view;
-
-    $this->view->page = $this->_getParam('page', 1);
-    $this->view->users = $criteria->findPage($this->view->page);
-    $this->view->total = $criteria->count();
-    $this->view->form = $this->_prepareForm($action_params, $criteria);
-    $this->view->params = array_merge($this->view->form->getValues(),
-                                      ['page' => $this->view->page,
-                                       'search_order' => $this->_getParam('search_order', 'nom asc')]);
-
-  }
-
-
-  protected function _prepareForm($action_params, $criteria) {
-    $form =  $criteria->getForm();
-
-    $url_params = array_merge(['module' => $this->getRequest()->getModuleName(),
-                               'controller' => $this->getRequest()->getControllerName(),
-                               'action' => $this->getRequest()->getActionName()],
-                              $action_params);
-
-    return $form->setAction(Class_Url::absolute($url_params, null, true));
-  }
-
-
-  protected function _getParam($name, $default=null) {
-    return $this->getRequest()->getParam($name, $default);
-  }
-
-
-  public function direct($action_params=[], $criteria) {
-    return $this->userSearch($action_params, $criteria);
-  }
-}
\ No newline at end of file
diff --git a/library/ZendAfi/Controller/Plugin/Manager/Newsletter.php b/library/ZendAfi/Controller/Plugin/Manager/Newsletter.php
index 29e7af8fde0b8dc4d673ac8202fdd89f8691a6c7..a5d3a8ae966f2fe2fdee4575eac2cc4a59e88fc2 100644
--- a/library/ZendAfi/Controller/Plugin/Manager/Newsletter.php
+++ b/library/ZendAfi/Controller/Plugin/Manager/Newsletter.php
@@ -181,11 +181,11 @@ class ZendAfi_Controller_Plugin_Manager_Newsletter extends ZendAfi_Controller_Pl
     $this->_view->newsletter = $model;
     $this->_view->groups = $model->getSortedRecipientsByDedicatedAndLabel();
 
-    $criteria = (new Class_User_SearchCriteria($this->_request->getParams()))
-      ->addCriteria(new Class_User_SearchCriteria_NewsletterSubscriptionStatus($this->_request->getParams()))
-      ->addCriteria(new Class_User_SearchCriteria_WithMail($this->_request->getParams()));
+    $params = $this->_request->getParams();
+    $criteria = (new Class_User_SearchCriteria($params))
+      ->addCriteria(new Class_User_SearchCriteria_NewsletterSubscriptionStatus($params))
+      ->addCriteria(new Class_User_SearchCriteria_WithMail($params));
 
-    $this->_helper->userSearch(['id' => $model->getId()], $criteria);
+    $this->_view->search = $this->_helper->search(['id' => $model->getId()], $criteria);
   }
 }
-?>
\ No newline at end of file
diff --git a/library/ZendAfi/Controller/Plugin/Manager/User.php b/library/ZendAfi/Controller/Plugin/Manager/User.php
index 71440fb095b9b0abe99d18a6f234ec17f59f52f0..7637f518550c5cfe70406ec6bed4e9aef11873b0 100644
--- a/library/ZendAfi/Controller/Plugin/Manager/User.php
+++ b/library/ZendAfi/Controller/Plugin/Manager/User.php
@@ -123,4 +123,63 @@ class ZendAfi_Controller_Plugin_Manager_User extends ZendAfi_Controller_Plugin_M
   protected function _canEdit($model) {
     return $model->getRoleLevel() <= Class_Users::getIdentity()->getRoleLevel();
   }
+
+
+  public function massDeleteAction() {
+    $this->_massDelete();
+  }
+
+
+  public function massDeleteRunAction() {
+    $this->_massDelete();
+  }
+
+
+  protected function _massDelete() {
+    if (!$criteria = $this->_massDeleteCriteria())
+      return;
+
+    $this->_view->titre = $this->_('Suppression de %s utilisateurs', $this->_view->count);
+    $this->_view->criteria_description = $criteria->describeOn($this->_view);
+    $this->_view->criteria_params = $criteria->getFormValues();
+  }
+
+
+  public function massDeleteRunStepAction() {
+    if (!$criteria = $this->_massDeleteCriteria()) {
+      $this->_helper->json([]);
+      return $this->_response->setHttpResponseCode(400);
+    }
+
+    $criteria
+      ->addParam('id_user not', Class_Users::getIdentity()->getId())
+      ->addParam('role_level not', ZendAfi_Acl_AdminControllerRoles::SUPER_ADMIN);
+
+    if (!$page = $criteria->findPage(1, 100)) {
+      $total = $this->_getParam('total', 0);
+      return $this->_helper->json(['total' => $total, 'done' => $total]);
+    }
+
+    foreach($page as $user)
+      $user->delete();
+
+    $this->_helper->json(['total' => $this->_getParam('total', 0),
+                          'done' => $this->_getParam('done', 0) + count($page)]);
+  }
+
+
+  protected function _massDeleteCriteria() {
+    $params = $this->_request->getParams();
+    $criteria = (new Class_User_SearchCriteria($params))
+      ->addCriteria(new Class_User_SearchCriteria_RoleLevelLimit($params));
+
+    $this->_view->count = $criteria->count();
+    if (1 < $this->_view->count)
+      return $criteria;
+
+    $this->_helper
+      ->notify($this->_('Impossible de supprimer une sélection de moins de 2 utilisateurs'));
+
+    $this->_redirectToReferer();
+  }
 }
\ No newline at end of file
diff --git a/library/ZendAfi/Controller/Plugin/Manager/UsergroupAgenda.php b/library/ZendAfi/Controller/Plugin/Manager/UsergroupAgenda.php
index af68dd42bab2ec774f4d28ad05543a460521677a..eebaba6df889fae789a7fc43f8399d5d4d840405 100644
--- a/library/ZendAfi/Controller/Plugin/Manager/UsergroupAgenda.php
+++ b/library/ZendAfi/Controller/Plugin/Manager/UsergroupAgenda.php
@@ -52,12 +52,7 @@ class ZendAfi_Controller_Plugin_Manager_UsergroupAgenda
 
   public function allAction() {
     $this->_view->titre = $this->_('Tous les rendez-vous');
-    $this->_view->rendezvous = Class_RendezVous::findAllBy([ "order" => ["date desc", "begin_time desc" , "end_time desc"]]);
-
-    $params = $this->_request->getParams();
-    $this->_view->search = $this->_helper
-      ->searchRendezVous([],
-                         new Class_RendezVous_SearchCriteria($params));
+    $this->_view->search = $this->_helper->search([], new Class_RendezVous_SearchCriteria($this->_request->getParams()));
   }
 
 
@@ -69,7 +64,7 @@ class ZendAfi_Controller_Plugin_Manager_UsergroupAgenda
       ->withoutCriteria('Class_User_SearchCriteria_NumberOfReviews')
       ->withoutCriteria('Class_User_SearchCriteria_NumberOfBaskets');
 
-    $this->_helper->userSearch([], $criteria);
+    $this->_view->search = $this->_helper->search([], $criteria);
   }
 
 
diff --git a/library/ZendAfi/Form/Decorator/UserSelection.php b/library/ZendAfi/Form/Decorator/UserSelection.php
index bd38a2150ca52a16c45744a7b6809c02e17ce06f..67298fd12c5c3914ef6402df7c49e0b9e8f47bc9 100644
--- a/library/ZendAfi/Form/Decorator/UserSelection.php
+++ b/library/ZendAfi/Form/Decorator/UserSelection.php
@@ -36,7 +36,7 @@ remove_icon_url:' . json_encode($skin->getIconUrl('actions', 'delete')) . ',
 add_message:' . json_encode($this->_element->getAddMessage()). ',
 delete_message:' . json_encode($this->_element->getDeleteMessage()). '})');
 
-    $description = (new Class_TableDescription_Users('current_user_selection_' . $this->_element->getName()))
+    $description = (new Class_TableDescription_UsersBasic('current_user_selection_' . $this->_element->getName()))
       ->addRowAction(function($model) use($view)
                      {
                        return $view->renderModelActions($model,
diff --git a/library/ZendAfi/Mail.php b/library/ZendAfi/Mail.php
index fc8ab96a5d515226fd111a3767a757d5b35d109e..a1b7039c084f39212dc7b2f0814ee0b2369f80ff 100644
--- a/library/ZendAfi/Mail.php
+++ b/library/ZendAfi/Mail.php
@@ -92,6 +92,11 @@ class ZendAfi_Mail extends Zend_Mail {
     parent::setBodyText($txt, 'utf-8', Zend_Mime::ENCODING_8BIT);
     return $this;
   }
-}
 
-?>
\ No newline at end of file
+
+  public function clearRecipients() {
+    $this->_recipients = [];
+    $this->_headers['To'] = [];
+    return $this;
+  }
+}
diff --git a/library/ZendAfi/View/Helper/Admin/Search.php b/library/ZendAfi/View/Helper/Admin/Search.php
new file mode 100644
index 0000000000000000000000000000000000000000..c1a437b29ab6af160ea404186678900f12a6ed8b
--- /dev/null
+++ b/library/ZendAfi/View/Helper/Admin/Search.php
@@ -0,0 +1,105 @@
+<?php
+/**
+ * Copyright (c) 2012-2019, 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
+ */
+
+
+abstract class ZendAfi_View_Helper_Admin_Search extends ZendAfi_View_Helper_BaseHelper {
+  protected
+    $_context,
+    $_table_description_class,
+    $_table_id;
+
+  protected function _search($context) {
+    $this->_context = $context;
+
+    return
+      $this->view->renderForm($context->getForm())
+      . $this->_headerLine()
+      . $this->_getTable()
+      ;
+  }
+
+
+  protected function _headerLine() {
+    return $this->_tag('p',
+                       $this->_resultMessage($this->_context->getTotal()));
+  }
+
+
+  protected function _resultMessage($total) {
+    return  $this->view->_plural($total,
+                                 'Auncun élément trouvé',
+                                 '%d élément trouvé',
+                                 '%d éléments trouvés',
+                                 $total);
+  }
+
+
+  protected function _getTable() {
+    $pager = $this->view->Pager($this->_context->getTotal(),
+                                20,
+                                $this->_context->getPage(),
+                                array_merge($this->_context->getParams(), ['page' => null]));
+    return $pager
+      . $this->view->renderTable($this->_getTableDescription(), $this->_context->getModels())
+      . $pager;
+  }
+
+
+  protected function _getTableDescription() {
+    $description_class = $this->_table_description_class;
+    $order_anchor_callback = function($label, $attribute) {
+      return $this->_orderAnchor($label, $attribute);
+    };
+
+    return (new $description_class($this->_table_id, $order_anchor_callback))
+      ->setSorterServer();
+  }
+
+
+  protected function _orderAnchor($label, $attribute) {
+    $order = $this->_context->getParams()['search_order'];
+    $order_param = $attribute;
+
+    if((0 === strpos($order, $attribute)) && (false === strpos($order, 'desc')))
+      $order_param .= ' desc';
+
+    $data_order = (0 === strpos($order, $attribute))
+      ? str_replace(' ', '_', 'order_' . $order_param)
+      : '';
+
+    $action_params = ['module' => 'admin',
+                      'controller' => $this->_context->getRequest()->getControllerName(),
+                      'action' => $this->_context->getRequest()->getActionName()];
+
+    if ($this->view->isPopup())
+      $action_params['render'] = 'popup';
+
+    $url = $this->view->url($action_params, null, true)
+      . '?'
+      . http_build_query(array_merge($this->_context->getParams(),
+                                     ['search_order' => $order_param,
+                                      'page' => null]))
+      ;
+    $html = $this->view->tagAnchor($url, $label, ['data-order' => $data_order]);
+
+    return function() use($html) { return $html; };
+  }
+}
\ No newline at end of file
diff --git a/library/ZendAfi/View/Helper/Admin/SearchCriteriaDescription.php b/library/ZendAfi/View/Helper/Admin/SearchCriteriaDescription.php
new file mode 100644
index 0000000000000000000000000000000000000000..f1ee0bce6e912540d01ca529c52e43c033befe11
--- /dev/null
+++ b/library/ZendAfi/View/Helper/Admin/SearchCriteriaDescription.php
@@ -0,0 +1,32 @@
+<?php
+/**
+ * Copyright (c) 2012-2019, 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_Admin_SearchCriteriaDescription extends ZendAfi_View_Helper_BaseHelper {
+  public function searchCriteriaDescription($description, $empty_label=null) {
+    if (!$empty_label)
+      $empty_label = $this->_('Tous');
+
+    return $this->_tag('ul',
+                       implode(array_map(function($item) { return $this->_tag('li', $item); },
+                                         $description ? $description : [$empty_label])));
+  }
+}
diff --git a/library/ZendAfi/View/Helper/Admin/SearchNewsletterUsers.php b/library/ZendAfi/View/Helper/Admin/SearchNewsletterUsers.php
new file mode 100644
index 0000000000000000000000000000000000000000..5cc11c697656883c6e1b4c03af54826bc020c29e
--- /dev/null
+++ b/library/ZendAfi/View/Helper/Admin/SearchNewsletterUsers.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * Copyright (c) 2012-2014, 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_Admin_SearchNewsletterUsers
+  extends ZendAfi_View_Helper_Admin_SearchUsers {
+
+  protected
+    $_table_description_class = 'Class_TableDescription_UsersBasic',
+    $_newsletter;
+
+
+  public function searchNewsletterUsers($newsletter, $context) {
+    $this->_newsletter = $newsletter;
+    return $this->_search($context);
+  }
+
+
+  protected function _getTableDescription() {
+    $description = parent::_getTableDescription();
+
+    $no_mail_action = $this->_tag('div' ,
+                                  Class_Admin_Skin::current()
+                                  ->renderActionIconOn('help',
+                                                       $this->view,
+                                                       ['title' => $this->_('Utilisateur sans mail'),
+                                                        'class' => 'ico']),
+                                  ['class' => 'actions']);
+
+    $actions = function($model) use ($no_mail_action) {
+      $is_recipient = $this->_newsletter->hasRecipient($model, false);
+      $is_blacklisted = $this->_newsletter->isBlackListed($model);
+
+      if (!$is_recipient)
+        return $this->view->renderModelActions($model,
+                                               [['url' => $this->_buildUrl('subscribe'),
+                                                 'icon' => 'add',
+                                                 'label' => $this->_('Inscrire')]
+                                               ]);
+
+      if ($is_blacklisted)
+        return $this->view->renderModelActions($model,
+                                               [['url' => $this->_buildUrl('subscribe'),
+                                                 'icon' => 'back',
+                                                 'label' => $this->_('Réinscrire')]
+                                               ]);;
+
+      if (!$model->hasMail())
+        return $no_mail_action;
+
+      return $this->view->renderModelActions($model,
+                                             [['url' => $this->_buildUrl('unsubscribe'),
+                                               'icon' => 'cancel',
+                                               'label' => $this->_('Désinscrire')]
+                                             ]);
+    };
+
+    return $description->addRowAction($actions);
+  }
+
+
+  protected function _buildUrl($action) {
+    return $this->view->url(['module' => 'admin',
+                             'controller' => 'newsletter',
+                             'action' => 'edit-subscribers',
+                             'id' => $this->_newsletter->getId()],
+                            null, true)
+      . '/' . $action . '/%s'
+      . '?' . http_build_query($this->_context->getParams());
+  }
+}
\ No newline at end of file
diff --git a/library/ZendAfi/View/Helper/Admin/SearchRendezVous.php b/library/ZendAfi/View/Helper/Admin/SearchRendezVous.php
index 89af2681183053ccda471da9ce2c4e6ea21fe07f..279a0f859dd4f7984ba9bac42f36cb006ad545bb 100644
--- a/library/ZendAfi/View/Helper/Admin/SearchRendezVous.php
+++ b/library/ZendAfi/View/Helper/Admin/SearchRendezVous.php
@@ -20,77 +20,23 @@
  */
 
 
-class ZendAfi_View_Helper_Admin_SearchRendezVous extends ZendAfi_View_Helper_BaseHelper {
+class ZendAfi_View_Helper_Admin_SearchRendezVous
+  extends ZendAfi_View_Helper_Admin_Search {
+
   protected
-    $_context;
+    $_table_description_class = 'Class_TableDescription_RendezVousSearch',
+    $_table_id = 'rendez-vous';
 
   public function searchRendezVous($context) {
-    $this->_context = $context;
-    $total = $context->getTotal();
-
-    return
-      $this->view->renderForm($context->getForm(),
-                              [$this->view->button((new Class_Entity())
-                                                   ->setText($this->_('Rechercher'))
-                                                   ->setImage($this->view->tagImg(Class_Admin_Skin::current()
-                                                                                  ->getIconUrl('actions',
-                                                                                               'loupe'),
-                                                                                  ['style' => 'filter: invert();']))
-                                                   ->setAttribs(['onclick' => "var form=$(this).parents('form'); if (!form.length) form=$(this).parents('.boutons, .admin-buttons').prevAll('form');if (!form.length) form=$(this).parents('.boutons, .admin-buttons').nextAll('form');form.submit(); return false;",
-                                                                 'type' => 'submit',
-                                                                 'class' => 'search',
-                                                                 'title' => $this->_('Lancer la recherche')]))]) .
-
-      $this->_tag('p', $this->view->_plural($total,
-                                            'Auncun rendez-vous trouvé',
-                                            '%d rendez-vous trouvé',
-                                            '%d rendez-vous trouvés',
-                                            $total)) .
-      $this->_getTable();
-  }
-
-
-  protected function _getTable() {
-    $pager = $this->view->Pager($this->_context->getTotal(),
-                                20,
-                                $this->_context->getPage(),
-                                array_merge($this->_context->getParams(), ['page' => null]));
-
-    $description = (new Class_TableDescription_RendezVousSearch('rendez-vous',
-                                                                function($label, $attribute)
-                                                                {
-                                                                  return $this->_orderAnchor($label, $attribute);
-                                                                }))
-      ->setSorterServer();
-
-    return $pager
-      . $this->view->renderTable($description, $this->_context->getModels())
-      . $pager;
+    return $this->_search($context);
   }
 
 
-  protected function _orderAnchor($label, $attribute) {
-    $order = $this->_context->getParams()['search_order'];
-    $order_param = $attribute;
-
-    if((0 === strpos($order, $attribute)) && (false === strpos($order, 'desc')))
-      $order_param .= ' desc';
-
-    $data_order = (0 === strpos($order, $attribute))
-      ? str_replace(' ', '_', 'order_' . $order_param)
-      : '';
-
-    $url = $this->view->url(['module' => 'admin',
-                             'controller' => $this->_context->getRequest()->getControllerName(),
-                             'action' => $this->_context->getRequest()->getActionName()],
-                            null, true)
-      . '?'
-      . http_build_query(array_merge($this->_context->getParams(),
-                                     ['search_order' => $order_param,
-                                      'page' => null]))
-      ;
-    $html = $this->view->tagAnchor($url, $label, ['data-order' => $data_order]);
-
-    return function() use($html) { return $html; };
+  protected function _resultMessage($total) {
+    return $this->_tag('p', $this->view->_plural($total,
+                                                 'Auncun rendez-vous trouvé',
+                                                 '%d rendez-vous trouvé',
+                                                 '%d rendez-vous trouvés',
+                                                 $total));
   }
 }
\ No newline at end of file
diff --git a/library/ZendAfi/View/Helper/Admin/SearchRendezVousUsers.php b/library/ZendAfi/View/Helper/Admin/SearchRendezVousUsers.php
new file mode 100644
index 0000000000000000000000000000000000000000..6d445c7b38b136f500e9b092d3fdd8fb34b4ef57
--- /dev/null
+++ b/library/ZendAfi/View/Helper/Admin/SearchRendezVousUsers.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Copyright (c) 2012-2014, 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_Admin_SearchRendezVousUsers
+  extends ZendAfi_View_Helper_Admin_SearchUsers {
+
+  protected $_table_description_class = 'Class_TableDescription_UsersBasic';
+
+
+  public function searchRendezVousUsers($context) {
+    return $this->_search($context);
+  }
+
+
+  protected function _getTableDescription() {
+    $description = parent::_getTableDescription();
+    $actions = function($model) {
+      return $this->view
+      ->renderModelActions($model,
+                           [['url' => '#',
+                             'icon' => 'add_user',
+                             'label' => $this->_('Ajouter "%s" comme destinataire',
+                                                 $model->getNomComplet()),
+                             'anchorOptions' => ['class' => 'user_add_action',
+                                                 'data-userid' => $model->getId(),
+                                                 'data-username' => $model->getNomComplet()]]
+                           ]);
+    };
+    $description->addRowAction($actions);
+
+    return $description;
+  }
+}
\ No newline at end of file
diff --git a/library/ZendAfi/View/Helper/Admin/SearchUsers.php b/library/ZendAfi/View/Helper/Admin/SearchUsers.php
index 65b57586b1e412803d984508e6cb8e70c14e3891..015e79889637bcb60c8baa61394d7a9613b69aaf 100644
--- a/library/ZendAfi/View/Helper/Admin/SearchUsers.php
+++ b/library/ZendAfi/View/Helper/Admin/SearchUsers.php
@@ -20,85 +20,50 @@
  */
 
 
-class ZendAfi_View_Helper_Admin_SearchUsers extends ZendAfi_View_Helper_BaseHelper {
+class ZendAfi_View_Helper_Admin_SearchUsers
+  extends ZendAfi_View_Helper_Admin_Search {
 
   protected
-    $users,
-    $total,
-    $params,
-    $actions;
+    $_table_description_class = 'Class_TableDescription_Users',
+    $_table_id = 'users_table',
+    $_with_delete;
 
-  public function admin_searchUsers($users, $total, $form, $page, $params, $actions) {
-    $this->users = $users;
-    $this->total = $total;
-    $this->page = $page;
-    $this->params = $params;
-    $this->actions = $actions;
 
-    return
-      $this->view->renderForm($form,
-                              [$this->view->button((new Class_Entity())
-                                                   ->setText($this->_('Rechercher'))
-                                                   ->setImage($this->view->tagImg(Class_Admin_Skin::current()
-                                                                                  ->getIconUrl('actions',
-                                                                                               'loupe'),
-                                                                                  ['style' => 'filter: invert();']))
-                                                   ->setAttribs(['onclick' => "var form=$(this).parents('form'); if (!form.length) form=$(this).parents('.boutons, .admin-buttons').prevAll('form');if (!form.length) form=$(this).parents('.boutons, .admin-buttons').nextAll('form');form.submit(); return false;",
-                                                                 'type' => 'submit',
-                                                                 'class' => 'search',
-                                                                 'title' => $this->_('Lancer la recherche')]))]) .
-      $this->_tag('p', $this->view->_plural($this->total,
-                                            'Auncun utilisateur trouvé',
-                                            '%d utilisateur trouvé',
-                                            '%d utilisateurs trouvés',
-                                            $this->total)) .
-      $this->_getUsersTable();
+  public function searchUsers($context, $with_delete=false) {
+    $this->_with_delete = $with_delete;
+    return $this->_search($context);
   }
 
 
-  protected function _getUsersTable() {
-    $pager = $this->view->Pager($this->total,
-                                20,
-                                $this->page,
-                                array_merge($this->params, ['page' => null]));
-
-    return $pager
-      . $this->_getTable()
-      . $pager;
-  }
-
-
-  protected function _getTable() {
-    $description = (new Class_TableDescription_Users('users_table',
-                                                     function($label, $attribute)
-                                                     {
-                                                       return $this->_orderAnchor($label, $attribute);
-                                                     }))
-      ->addRowAction($this->actions)
-      ->setSorterServer();
+  protected function _headerLine() {
+    if (!$this->_with_delete)
+      return parent::_headerLine();
+
+    $total = $this->_context->getTotal();
+    $mass_delete = 1 < $total && Class_Users::isCurrentUserAdmin()
+      ? $this->view->button((new Class_Entity())
+                            ->setText($this->_('Supprimer ces utilisateurs ...'))
+                            ->setUrl($this->view->url(['module' => 'admin',
+                                                       'controller' => 'users',
+                                                       'action' => 'mass-delete'],
+                                                      null, true)
+                                     . '?' . http_build_query($this->_context->getParams()))
+                            ->setImage($this->view->tagImg(Class_Admin_Skin::current()
+                                                           ->getIconUrl('buttons',
+                                                                        'delete'),
+                                                           ['style' => 'filter: invert();']))
+                            ->setAttribs(['style' => 'vertical-align: middle;']))
+      : '';
 
-    return $this->view->renderTable($description,
-                                    (new Class_TableDescription_Models($this->users)));
+    return $this->_tag('p', $this->_resultMessage($total) . $mass_delete);
   }
 
 
-  protected function _orderAnchor($label, $attribute) {
-    $order = $this->params['search_order'];
-    $order_param = $attribute;
-
-    if((0 === strpos($order, $attribute)) && (false === strpos($order, 'desc')))
-      $order_param .= ' desc';
-
-    $data_order = (0 === strpos($order, $attribute))
-      ? str_replace(' ', '_', 'order_' . $order_param)
-      : '';
-
-    return function($view) use($order_param, $label, $data_order) {
-      return $view->tagAnchor(array_merge(array_filter($this->params),
-                                          ['search_order' => $order_param,
-                                          'page' => null]),
-                             $label,
-                             ['data-order' => $data_order]);
-    };
+  protected function _resultMessage($total) {
+    return $this->view->_plural($total,
+                                'Aucun utilisateur trouvé',
+                                '%d utilisateur trouvé',
+                                '%d utilisateurs trouvés',
+                                $total);
   }
 }
\ No newline at end of file
diff --git a/library/ZendAfi/View/Helper/TagUlLi.php b/library/ZendAfi/View/Helper/TagUlLi.php
new file mode 100644
index 0000000000000000000000000000000000000000..8a113da93acf92604fa68ec6f682c8f6a62c96c4
--- /dev/null
+++ b/library/ZendAfi/View/Helper/TagUlLi.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Copyright (c) 2019, 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_TagUlLi extends ZendAfi_View_Helper_BaseHelper {
+  public function tagUlLi($items) {
+    return $items
+      ? $this->_tag('ul',
+                    implode(array_map(function($item) { return $this->_tag('li', $item); },
+                                      $items)))
+      : '';
+  }
+}
diff --git a/library/storm b/library/storm
index facf291bab370a3f01057114eecab38c2349cb97..8e752b86bc642f6563eb4b42b20fcbe8ece45b5c 160000
--- a/library/storm
+++ b/library/storm
@@ -1 +1 @@
-Subproject commit facf291bab370a3f01057114eecab38c2349cb97
+Subproject commit 8e752b86bc642f6563eb4b42b20fcbe8ece45b5c
diff --git a/tests/application/modules/admin/controllers/NewsletterControllerTest.php b/tests/application/modules/admin/controllers/NewsletterControllerTest.php
index 38f1401956f4458391bbfc934168644842ad6df3..c2f7b5e762cf8328e7a0f7cb3e3822864ebacd78 100644
--- a/tests/application/modules/admin/controllers/NewsletterControllerTest.php
+++ b/tests/application/modules/admin/controllers/NewsletterControllerTest.php
@@ -1335,6 +1335,12 @@ class Admin_NewsletterControllerEditSubcsribersSearchAllSubscriptionTest
   }
 
 
+  /** @test */
+  public function massDeleteButtonShouldNotBePresent() {
+    $this->assertNotXPath('//button[contains(@onclick, "admin/users/mass-delete")]');
+  }
+
+
   /** @test */
   public function laurentShouldBeInList() {
     $this->assertXPathContentContains('//table[contains(@class, "models")]//td', 'laurent');
@@ -1598,7 +1604,7 @@ class Admin_NewsletterControllerEditSubcsribersSearchUnsubscribedWithoutResultTe
 
   /** @test */
   public function shouldFindNobody() {
-    $this->assertNotXPathContentContains('//p', 'Aucun utilisateur trouvé');
+    $this->assertXPathContentContains('//p', 'Aucun utilisateur trouvé');
   }
 }
 
diff --git a/tests/application/modules/admin/controllers/UsersControllerTest.php b/tests/application/modules/admin/controllers/UsersControllerTest.php
index d9a967a1bbc4a32fe447681a72e2de8fc8eb7075..bafe7faa9617709a88b81db82719d66e0705ec05 100644
--- a/tests/application/modules/admin/controllers/UsersControllerTest.php
+++ b/tests/application/modules/admin/controllers/UsersControllerTest.php
@@ -125,6 +125,9 @@ class UsersControllerIndexTest extends UsersControllerWithMarcusTestCase {
          ->whenCalled('isCurrentUserCanAccesBackend')
          ->answers(false)
 
+         ->whenCalled('isCurrentUserAdmin')
+         ->answers(true)
+
          ->whenCalled('getIdentity')
          ->answers($user)
 
@@ -157,13 +160,13 @@ class UsersControllerIndexTest extends UsersControllerWithMarcusTestCase {
 
 
   /** @test */
-  public function formShouldContainsInputSearch() {
+  public function formShouldContainsTextSearch() {
     $this->assertXPath('//input[@name="search_search_for"]');
   }
 
 
   /** @test */
-  public function formShouldContainsInputReview() {
+  public function formShouldContainsHasReviewSearch() {
     $this->assertXPath('//select[@name="search_review"]');
   }
 
@@ -174,6 +177,27 @@ class UsersControllerIndexTest extends UsersControllerWithMarcusTestCase {
   }
 
 
+  /** @test */
+  public function formShouldContainsSubscriptionEndDateRange() {
+    $this->assertXPath('//input[@name="search_date_fin_start"]');
+    $this->assertXPath('//input[@name="search_date_fin_end"]');
+  }
+
+
+  /** @test */
+  public function formShouldContainsInLastExportSelect() {
+    $this->assertXPathContentContains('//select[@name="search_statut"]//option[@value="all"]',
+                                      'Indifférent');
+  }
+
+
+  /** @test */
+  public function formShouldContainsUpdateDateRange() {
+    $this->assertXPath('//input[@name="search_date_maj_start"]');
+    $this->assertXPath('//input[@name="search_date_maj_end"]');
+  }
+
+
   /** @test */
   public function libSelectorShouldBePresent() {
     $this->assertXPath('//select[@name="search_id_site"]');
@@ -188,7 +212,7 @@ class UsersControllerIndexTest extends UsersControllerWithMarcusTestCase {
 
   /** @test */
   public function totalUsersShouldBeFiftyFive() {
-    $this->assertXPathContentContains('//p','55 utilisateurs trouvés');
+    $this->assertXPathContentContains('//p', '55 utilisateurs trouvés');
   }
 
 
@@ -227,6 +251,13 @@ class UsersControllerIndexTest extends UsersControllerWithMarcusTestCase {
   public function allLibraryShouldBeSelectable() {
     $this->assertXPath('//select[@name="search_id_site"]//option[@value="all"]');
   }
+
+
+  /** @test */
+  public function massDeleteButtonWithCurrentQueryParamsShouldBePresent() {
+    $this->assertXPathContentContains('//button[contains(@onclick, "admin/users/mass-delete?search_id_site=all&search_role_level=2&search_valid_subscription=1&search_statut=all&search_review=1")]',
+                                      'Supprimer ces utilisateurs');
+  }
 }
 
 
@@ -421,6 +452,7 @@ class UsersControllerEditMarcusAsAdminPortailTest extends UsersControllerWithMar
 
 
 
+
 class UsersControllerDeleteMarcusTest extends UsersControllerWithMarcusTestCase {
   public function setUp() {
     parent::setUp();
@@ -438,6 +470,7 @@ class UsersControllerDeleteMarcusTest extends UsersControllerWithMarcusTestCase
 
 
 
+
 class UsersControllerPostMarcusDataTest extends UsersControllerWithMarcusTestCase {
   public function setUp() {
     parent::setUp();
@@ -472,76 +505,94 @@ class UsersControllerPostMarcusDataTest extends UsersControllerWithMarcusTestCas
                         Class_Users::find(10));
   }
 
+
   /** @test */
   public function civiliteShouldBeMadame() {
     $this->assertEquals(Class_Users::CIVILITE_MADAME, $this->marcus->getCivilite());
   }
 
+
   /** @test */
   public function mobileShouldBe0612450987() {
     $this->assertEquals('06 12 45 09 87', $this->marcus->getMobile());
   }
 
+
   public function testLoginIsMDavis() {
     $this->assertEquals('mdavis', $this->marcus->getLogin());
   }
 
+
   public function testPasswordIsTutu() {
     $this->assertEquals('tutu', $this->marcus->getPassword());
   }
 
+
   public function testNomIsDavis() {
     $this->assertEquals('Davis', $this->marcus->getNom());
   }
 
+
   public function testPrenomIsMiles() {
     $this->assertEquals('Miles', $this->marcus->getPrenom());
   }
 
+
   public function testNaissanceIs1976() {
     $this->assertEquals('1976-02-17', $this->marcus->getNaissance());
   }
 
+
   public function testTelephoneIs09_87_76_54_32_12() {
     $this->assertEquals('09 87 76 54 32 12', $this->marcus->getTelephone());
   }
 
+
   public function testAdresseIs12RueMiles() {
     $this->assertEquals('12 rue miles', $this->marcus->getAdresse());
   }
 
+
   public function testCodePostalIs75000() {
     $this->assertEquals('75000', $this->marcus->getCodePostal());
   }
 
+
   public function testVilleIsParis() {
     $this->assertEquals('Paris', $this->marcus->getVille());
   }
 
+
   public function testMailIsMDavisAtFreeDotFr() {
     $this->assertEquals('mdavis@free.fr', $this->marcus->getMail());
   }
 
+
   public function testRoleLevelIsFour() {
     $this->assertEquals(4, $this->marcus->getRoleLevel());
   }
 
+
   public function testIdSiteIsOne() {
     $this->assertEquals(1, $this->marcus->getIdSite());
   }
 
+
   public function testRoleIsAdministrateur() {
     $this->assertEquals('admin_bib', $this->marcus->getNomRole());
   }
 
+
   public function testOrdreabonIsTwo() {
     $this->assertEquals(2, $this->marcus->getOrdreabon());
   }
 
+
   public function testPseudoIsDave() {
     $this->assertEquals('Dave', $this->marcus->getPseudo());
   }
 
+
   public function testUserGroupsAreReferentAndStagiaires() {
     $this->assertEquals([22, 25], $this->marcus->getUserGroupsIds());
   }
@@ -549,6 +600,7 @@ class UsersControllerPostMarcusDataTest extends UsersControllerWithMarcusTestCas
 
 
 
+
 class UsersControllerPostMarcusInvalidDataTest extends UsersControllerWithMarcusTestCase {
   public function testNoLoginPasswordAndRole() {
     $this->_postEditData(['login' => '',
@@ -607,6 +659,8 @@ class UsersControllerPostMarcusInvalidDataTest extends UsersControllerWithMarcus
 }
 
 
+
+
 class UsersControllerPostValidDataWithCommOpsysTest
   extends UsersControllerWithMarcusTestCase {
 
@@ -680,6 +734,7 @@ class UsersControllerPostValidDataWithCommOpsysTest
 
 
 
+
 class UsersControllerAddViewTest extends AbstractControllerTestCase {
   protected $_storm_default_to_volatile = true;
 
@@ -716,51 +771,53 @@ class UsersControllerAddViewTest extends AbstractControllerTestCase {
 
 
 
+
 class UsersControllerReferentIndexTest extends UsersControllerWithMarcusTestCase {
   use Trait_UserGroupFixtures;
 
-  protected function _loginHook($account) {
-    $account->ROLE_LEVEL = ZendAfi_Acl_AdminControllerRoles::MODO_PORTAIL;
-    $account->ROLE = 'moderateur';
-
-  }
-
   public function setUp() {
     parent::setUp();
 
-    $roger = $this->fixture('Class_Users',
-                            ['id' => 345,
-                             'login' => 'roger',
-                             'password' => 'roger']);
-
+    $this->fixture('Class_Users', ['id' => 345,
+                                   'login' => 'roger',
+                                   'password' => 'roger']);
+    $this->fixture('Class_Users', ['id' => 346,
+                                   'login' => 'norbert',
+                                   'password' => 'norbert']);
 
-    $this->user_loader = Storm_Test_ObjectWrapper::onLoaderOfModel('Class_Users');
-    $this->user_loader->whenCalled('getIdentity')
-                      ->answers($user = Class_Users::newInstanceWithId(2)
-                                ->setLogin('referent')
-                                ->setRoleLevel(ZendAfi_Acl_AdminControllerRoles::MODO_PORTAIL)
-                                ->setPseudo('referent'));
+    $user = $this->fixture('Class_Users',
+                           ['id' => 2,
+                            'login' => 'referent',
+                            'password' => 'referent',
+                            'role_level' => ZendAfi_Acl_AdminControllerRoles::MODO_PORTAIL]);
 
     $this->addUserToRightsReferent($user);
 
+    ZendAfi_Auth::getInstance()->logUser($user);
     $this->dispatch('/admin/users/index/order/prenom+desc', true);
   }
 
 
   /** @test **/
-  public function testSelectedRoleForReferentIsAbonneSIGB() {
+  public function selectedRoleShouldBeAbonneSIGB() {
     $this->assertXPathContentContains("//select[@name='search_role_level']/option[@value='2'][@selected='selected']", 'Abonné SIGB');
   }
 
 
   /** @test **/
-  public function testReferentCanNotCreateNewUser() {
-    $this->assertNotXPathContentContains("//td", 'Ajouter un utilisateur');
+  public function pageShouldContainsLinkToAddUser() {
+    $this->assertNotXPath('//button[contains(@onclick, "/admin/users/add")]');
+  }
+
+
+  /** @test */
+  public function pageShouldNotContainsLinkToMassDelete() {
+    $this->assertNotXPath('//button[contains(@onclick, "/admin/users/mass-delete")]');
   }
 
 
   /** @test **/
-  public function testReferentCanNotEditUser() {
+  public function pageShouldNotContainsLinkToEditUser() {
     $this->assertNotXPath("//table//a[contains(@href, 'admin/users/edit/id/345')]");
   }
 }
@@ -842,6 +899,7 @@ class UsersControllerAddActionPostTest extends UsersControllerWithMarcusTestCase
 
 
 
+
 abstract class Admin_UsersControllerEditAdminTestCase extends Admin_AbstractControllerTestCase {
 
   protected
@@ -900,6 +958,7 @@ abstract class Admin_UsersControllerEditAdminTestCase extends Admin_AbstractCont
 
 
 
+
 class Admin_UsersControllerFormEditAdminTest extends Admin_UsersControllerEditAdminTestCase {
   public function setUp() {
     parent::setup();
@@ -950,6 +1009,7 @@ class Admin_UsersControllerFormEditAdminTest extends Admin_UsersControllerEditAd
 
 
 
+
 class UsersControllerPostEditAdminTest extends Admin_UsersControllerEditAdminTestCase {
   public function setUp() {
     parent::setUp();
@@ -999,6 +1059,7 @@ class UsersControllerPostEditAdminTest extends Admin_UsersControllerEditAdminTes
 
 
 
+
 class UsersControllerPostEditWithHashedPasswordAdminTest extends Admin_UsersControllerEditAdminTestCase {
   public function setUp() {
     parent::setUp();
@@ -1036,6 +1097,7 @@ class UsersControllerPostEditWithHashedPasswordAdminTest extends Admin_UsersCont
 
 
 
+
 class UsersControllerEditSuperAdminTest extends Admin_AbstractControllerTestCase {
   protected $_storm_default_to_volatile = true;
 
@@ -1101,6 +1163,8 @@ class Admin_UsersControllerChangeAdminSkinActionTest extends Admin_AbstractContr
 }
 
 
+
+
 class Admin_UsersControllerChangeRoleLevelOfUserInMyBibAsAdminBibTest extends Admin_AbstractControllerTestCase {
   protected $_storm_default_to_volatile = true;
 
@@ -1159,6 +1223,7 @@ class Admin_UsersControllerChangeRoleLevelOfUserInMyBibAsAdminBibTest extends Ad
 
 
 
+
 class UsersControllerWithAdminPortalTest extends Admin_AbstractControllerTestCase {
   protected $_storm_default_to_volatile = true;
 
@@ -1208,13 +1273,13 @@ class UsersControllerWithAdminPortalTest extends Admin_AbstractControllerTestCas
          ->answers(1)
       ;
 
-    $this->dispatch('/admin/users/index/search_order/prenom+desc', true);
+    $this->dispatch('/admin/users/index?search_order=prenom+desc', true);
   }
 
 
   /** @test */
   public function tableHeaderShouldContainsLinkToOrderByLastNameAsc() {
-    $this->assertXPath('//th//a[contains(@href, "/search_order/prenom/")]');
+    $this->assertXPath('//th//a[contains(@href, "search_order=prenom")]');
   }
 
 
@@ -1263,7 +1328,7 @@ class UsersControllerWithAdminPortalTest extends Admin_AbstractControllerTestCas
 
   /** @test */
   public function tableHeadShouldContainsLinksWithOrderNameAsc() {
-    $this->assertXPath('//th/a[contains(@href, "/search_order/nom/")]');
+    $this->assertXPath('//th/a[contains(@href, "search_order=nom")]');
   }
 }
 
@@ -1575,6 +1640,7 @@ class UsersControllerManageDoubleTest extends UsersControllerDoubleTestCase {
 
 
 
+
 class UsersControllerManageDoubleWithCustomCriteriaTest extends UsersControllerDoubleTestCase {
 
   public function setUp() {
@@ -1602,6 +1668,7 @@ class UsersControllerManageDoubleWithCustomCriteriaTest extends UsersControllerD
 
 
 
+
 class UsersControllerDeleteDoubleEndCursorTest extends Admin_AbstractControllerTestCase {
 
   public function setUp() {
@@ -1626,7 +1693,6 @@ class UsersControllerDeleteDoubleEndCursorTest extends Admin_AbstractControllerT
 
 
 
-
 class UsersControllerDeleteDoubleTest extends UsersControllerDoubleTestCase {
 
   public function setUp() {
@@ -1889,3 +1955,229 @@ class UsersControllerIndexWithDoubleTest extends UsersControllerDoubleTestCase {
     $this->assertNotXPath('//td//a[contains(@href, "/admin/users/manage-double-user/id_user/123")]');
   }
 }
+
+
+
+
+abstract class UsersControllerMassDeleteTestCase extends Admin_AbstractControllerTestCase {
+  protected $_storm_default_to_volatile = true;
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->onLoaderOfModel('Class_Users')
+         ->whenCalled('countBy')
+         ->with(['role_level' => 2,
+                 'statut' => 1,
+                 'order' => 'nom asc',
+                 'where' => '(date_fin <= "2018-05-28") AND (date_maj <= "2018-05-28") AND (login LIKE "%andy%" OR nom LIKE "%andy%" OR prenom LIKE "%andy%" OR pseudo LIKE "%andy%" OR mail LIKE "%andy%" OR idabon LIKE "%andy%") AND (role_level <= 7)'])
+         ->answers(2)
+
+         ->whenCalled('countBy')
+         ->willDo(function($params)
+                  {
+                    $this->fail('Unexpected call to countBy with : ' . json_encode($params));
+                  })
+      ;
+  }
+}
+
+
+
+
+class UsersControllerMassDeleteTest extends UsersControllerMassDeleteTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/users/mass-delete?search_id_site=all&search_role_level=2&search_statut=1&search_date_fin_start=&search_date_fin_end=28%2F05%2F2018&search_date_maj_start=&search_date_maj_end=28%2F05%2F2018&search_search_for=andy');
+  }
+
+
+  /** @test */
+  public function pageTitleShouldBeSuppressionDe2DUtilisateurs() {
+    $this->assertXPathContentContains('//h1', 'Suppression de 2 utilisateurs');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsRoleLevelDescription() {
+    $this->assertXPathContentContains('//li', 'Niveau d\'accès : Abonné SIGB');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsStatutDescription() {
+    $this->assertXPathContentContains('//li', 'Présent dans le dernier export SIGB : Non');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsSubscriptionEndDateDescription() {
+    $this->assertXPathContentContains('//li', 'Date de fin d\'abonnement : jusqu\'au 28/05/2018');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsUpdateDateDescription() {
+    $this->assertXPathContentContains('//li', 'Mis à jour : jusqu\'au 28/05/2018');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsSearchForDescription() {
+    $this->assertXPathContentContains('//li', 'Rechercher : andy', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function pageShouldContainsBackButton() {
+    $this->assertXPathContentContains('//button[contains(@onclick, "&search_date_fin_end=28%2F05%2F2018")]', 'Retour');
+  }
+
+
+  /** @test */
+  public function pageShouldContainsRunButton() {
+    $this->assertXPathContentContains('//button[contains(@onclick, "/admin/users/mass-delete-run")][contains(@onclick, "&search_date_fin_end=28%2F05%2F2018")]', 'Lancer la suppression');
+  }
+}
+
+
+
+
+class UsersControllerMassDeleteRunTest extends UsersControllerMassDeleteTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/users/mass-delete-run?search_id_site=all&search_role_level=2&search_statut=1&search_date_fin_start=&search_date_fin_end=28%2F05%2F2018&search_date_maj_start=&search_date_maj_end=28%2F05%2F2018&search_search_for=andy');
+  }
+
+
+  /** @test */
+  public function progressBarShouldBeLoaded() {
+    $this->assertXPathContentContains('//script',
+                                      '$("#userDeleteProgress").progressbar({value:false, max:2})');
+  }
+}
+
+
+
+class UsersControllerMassDeleteRunStepWithOneUserTest extends UsersControllerMassDeleteTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->fixture('Class_Users',
+                   ['id' => 999,
+                    'login' => 'junchiro',
+                    'password' => 'makoto',
+                    'role_level' => 2,
+                    'statut' => 1,
+                    'id_site' => 3,
+                    'idabon' => '00399933J28332']);
+
+    $this->dispatch('/admin/users/mass-delete-run-step/total/2/done/0?search_id_site=all&search_role_level=2&search_statut=1&search_date_fin_start=&search_date_fin_end=28%2F05%2F2018&search_date_maj_start=&search_date_maj_end=28%2F05%2F2018&search_search_for=andy');
+  }
+
+
+  /** @test */
+  public function shouldHaveDeletedJunchiro() {
+    $this->assertNull(Class_Users::find(999));
+  }
+
+
+  /** @test */
+  public function shouldRespondDoneOverTotalInJson() {
+    $json = json_decode($this->_response->getBody());
+    $this->assertEquals(1, $json->done);
+    $this->assertEquals(2, $json->total);
+  }
+}
+
+
+
+
+class UsersControllerMassDeleteRunStepWithoutUserTest extends UsersControllerMassDeleteTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/users/mass-delete-run-step/total/2/done/0?search_id_site=all&search_role_level=2&search_statut=1&search_date_fin_start=&search_date_fin_end=28%2F05%2F2018&search_date_maj_start=&search_date_maj_end=28%2F05%2F2018&search_search_for=andy');
+  }
+
+
+  /** @test */
+  public function shouldRespondTotallyDoneInJson() {
+    $json = json_decode($this->_response->getBody());
+    $this->assertEquals(2, $json->done);
+    $this->assertEquals(2, $json->total);
+  }
+}
+
+
+
+
+class UsersControllerMassDeleteRunStepMatchingCurrentUserTest
+  extends UsersControllerMassDeleteTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    ZendAfi_Auth::getInstance()
+      ->logUser($this->fixture('Class_Users',
+                               ['id' => 78789,
+                                'login' => 'admin',
+                                'password' => 'admin',
+                                'role_level' => ZendAfi_Acl_AdminControllerRoles::ADMIN_PORTAIL]));
+    Class_Users::getLoader()
+      ->whenCalled('countBy')
+      ->with(['order' => 'nom asc',
+              'where' => '(role_level <= 6)'])
+      ->answers(2);
+
+    $this->dispatch('/admin/users/mass-delete-run-step/total/2/done/0?search_id_site=all');
+  }
+
+
+  /** @test */
+  public function shouldNotDeleteCurrentUser() {
+    $this->assertNotNull(Class_Users::find(78789));
+  }
+
+
+  /** @test */
+  public function shouldStillRespondAllDoneInJson() {
+    $json = json_decode($this->_response->getBody());
+    $this->assertEquals(2, $json->done);
+    $this->assertEquals(2, $json->total);
+  }
+}
+
+
+
+
+class UsersControllerMassDeleteRunStepMatchingSuperAdminUserTest
+  extends UsersControllerMassDeleteTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    $this->fixture('Class_Users', ['id' => 78789,
+                                   'login' => 'super',
+                                   'password' => 'admin',
+                                   'role_level' => ZendAfi_Acl_AdminControllerRoles::SUPER_ADMIN]);
+
+    Class_Users::getLoader()
+      ->whenCalled('countBy')
+      ->with(['order' => 'nom asc',
+              'where' => '(role_level <= 7)'])
+      ->answers(2);
+
+    $this->dispatch('/admin/users/mass-delete-run-step/total/2/done/0?search_id_site=all');
+  }
+
+
+  /** @test */
+  public function shouldNotDeleteSuperAdminUser() {
+    $this->assertNotNull(Class_Users::find(78789));
+  }
+
+
+  /** @test */
+  public function shouldStillRespondAllDoneInJson() {
+    $json = json_decode($this->_response->getBody());
+    $this->assertEquals(2, $json->done);
+    $this->assertEquals(2, $json->total);
+  }
+}
\ No newline at end of file
diff --git a/tests/library/Class/UsersTest.php b/tests/library/Class/UsersTest.php
index 0e8c278a66950458461d50dc0e0cf3a47d7540a7..264f7d3a267fc540377d220e960a8c6c2454965d 100644
--- a/tests/library/Class/UsersTest.php
+++ b/tests/library/Class/UsersTest.php
@@ -152,7 +152,6 @@ class UsersTestAssociations extends ModelTestCase {
 
 
 
-
 class UsersAgeTest extends ModelTestCase {
   public function setUp() {
     parent::setUp();
@@ -197,7 +196,6 @@ class UsersAgeTest extends ModelTestCase {
 
 
 
-
 abstract class UsersMailingActionTestCase extends ModelTestCase {
   protected $mock_transport, $user;
 
@@ -487,7 +485,6 @@ class UsersFicheAbonneTest extends ModelTestCase {
 
 
 
-
 class UsersGetIdentityWithSessionErrorTest extends ModelTestCase {
   /** @test */
   public function getIdentityShouldReturnNullOnZendSessionException() {
@@ -510,6 +507,7 @@ class UsersGetIdentityWithSessionErrorTest extends ModelTestCase {
 }
 
 
+
 class UsersGetUsersWithBlowfishPasswordTest extends ModelTestCase {
   public function setUp() {
     parent::setUp();
@@ -541,7 +539,7 @@ class UsersGetUsersWithBlowfishPasswordTest extends ModelTestCase {
 
 
 
-class UserGetBookmarkedLibraryTest extends ModelTestCase {
+class UsersGetBookmarkedLibraryTest extends ModelTestCase {
   protected $_storm_default_to_volatile = true,
     $_steph;
 
@@ -645,4 +643,55 @@ class UsersWithLoginThroughSigbOnlyTest extends ModelTestCase {
 
     $this->assertTrue((new Class_User_Password($user))->isBlowFish());
   }
+}
+
+
+
+class UsersDeleteAnonymizeTest extends ModelTestCase {
+  public function setUp() {
+    parent::setUp();
+
+    $godzy = $this->fixture('Class_Users',
+                            ['id' => 1967,
+                             'login' => 'Godzilla',
+                             'password' => 'GrrRRrrR']);
+
+    $this->fixture('Class_Newsletter_DispatchUser',
+                   ['id' => 90,
+                    'user_id' => 1967,
+                    'mail' => 'godzy@my-kaiju-heaven.nl',
+                    'sent' => 1]);
+
+    $mail = (new ZendAfi_Mail())
+      ->setFrom('admin@my-bokeh.tw')
+      ->addTo('godzy@my-kaiju-heaven.nl')
+      ->setSubject('Never mind')
+      ->setBodyText('Just ignore tanks and pass through the city center');
+
+    $this->fixture('Class_Formulaire',
+                   ['id' => 804,
+                    'id_user' => 1967,
+                    'mail_answer' => serialize($mail)]);
+
+    $godzy->delete();
+  }
+
+
+  /** @test */
+  public function newsletterDispatchUserShouldBeAnonymized() {
+    $this->assertEquals('', Class_Newsletter_DispatchUser::find(90)->getMail());
+  }
+
+
+  /** @test */
+  public function formMailAnswerRecipientShouldBeAnonymized() {
+    $this->assertEquals('', Class_Formulaire::find(804)->getMailDestinataire());
+  }
+
+
+  /** @test */
+  public function formMailAnswerHeadersShouldBeAnonymized() {
+    $to = Class_Formulaire::find(804)->getMail()->getHeaders()['To'];
+    $this->assertNotContains('godzy@my-kaiju-heaven.nl', $to, json_encode($to, JSON_PRETTY_PRINT));
+  }
 }
\ No newline at end of file
diff --git a/tests/scenarios/RendezVous/UsergroupAgendaAdminTest.php b/tests/scenarios/RendezVous/UsergroupAgendaAdminTest.php
index 779d866ccb8e7b5489e57c4198adc82e4256e931..925d63e02ddd853a915c409040964ab81ee973ab 100644
--- a/tests/scenarios/RendezVous/UsergroupAgendaAdminTest.php
+++ b/tests/scenarios/RendezVous/UsergroupAgendaAdminTest.php
@@ -501,6 +501,12 @@ class UsergroupAgendaAdminUserSelectActionTest extends UsergroupAgendaAdminModoP
   }
 
 
+  /** @test */
+  public function massDeleteButtonShouldNotBePresent() {
+    $this->assertNotXPath('//button[contains(@onclick, "admin/users/mass-delete")]');
+  }
+
+
   /** @test */
   public function useradminPortailShouldBePresent() {
     $this->assertXPathContentContains('//td','padawan');
@@ -508,7 +514,7 @@ class UsergroupAgendaAdminUserSelectActionTest extends UsergroupAgendaAdminModoP
 
 
   /** @test */
-  public function lienAjoutShouldBePresent() {
+  public function addLinkShouldBePresent() {
     $this->assertXPath('//a[contains(@data-userid, "35")][@data-username="padawan"]');
   }
 }