From 370c8e902290baed64a12aa2f702b4559cac5df9 Mon Sep 17 00:00:00 2001
From: Ghislain Loas <ghislo@sandbox.pergame.net>
Date: Wed, 9 Dec 2015 17:13:05 +0100
Subject: [PATCH] dev #33376 first commit

---
 .gitmodules                                   |   3 +
 VERSIONS_WIP/33376                            |   1 +
 .../admin/controllers/BibController.php       |   8 +-
 .../admin/controllers/RedmineController.php   |  60 ++++
 .../admin/views/scripts/bib/_form.phtml       | 259 +++++++++---------
 cosmogramme/sql/patch/patch_283.php           |   7 +
 library/Class/AdminVar.php                    |  12 +-
 library/Class/WebService/Redmine.php          |  75 +++++
 library/Redmine                               |   1 +
 .../ZendAfi/View/Helper/Redmine/Account.php   |  83 ++++++
 library/startup.php                           |   9 +
 public/admin/css/global.css                   |  13 +
 public/opac/js/redmine.js                     |  50 ++++
 .../controllers/AdminIndexControllerTest.php  |   6 +
 .../admin/controllers/BibControllerTest.php   | 105 +++++++
 .../controllers/RedmineControllerTest.php     | 173 ++++++++++++
 tests/db/UpgradeDBTest.php                    |  29 ++
 tests/fixtures/RedmineFixtures.php            |  57 ++++
 18 files changed, 819 insertions(+), 132 deletions(-)
 create mode 100644 VERSIONS_WIP/33376
 create mode 100644 application/modules/admin/controllers/RedmineController.php
 create mode 100644 cosmogramme/sql/patch/patch_283.php
 create mode 100644 library/Class/WebService/Redmine.php
 create mode 160000 library/Redmine
 create mode 100644 library/ZendAfi/View/Helper/Redmine/Account.php
 create mode 100644 public/opac/js/redmine.js
 create mode 100644 tests/application/modules/admin/controllers/RedmineControllerTest.php
 create mode 100644 tests/fixtures/RedmineFixtures.php

diff --git a/.gitmodules b/.gitmodules
index 9e8a01cc2c3..6316c6af50d 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,6 @@
 [submodule "library/storm"]
 	path = library/storm
 	url = http://git.afi-sa.fr/afi/storm.git
+[submodule "library/Redmine"]
+	path = library/Redmine
+	url = http://git.afi-sa.fr/afi/php-redmine-api.git
diff --git a/VERSIONS_WIP/33376 b/VERSIONS_WIP/33376
new file mode 100644
index 00000000000..98403ca586c
--- /dev/null
+++ b/VERSIONS_WIP/33376
@@ -0,0 +1 @@
+ - ticket #33376 : Créer un SSO entre Bokeh et afi-forge pour créer des tickets support suivre ses tickets (1/3)
\ No newline at end of file
diff --git a/application/modules/admin/controllers/BibController.php b/application/modules/admin/controllers/BibController.php
index daee143b5bc..778114f0dd6 100644
--- a/application/modules/admin/controllers/BibController.php
+++ b/application/modules/admin/controllers/BibController.php
@@ -195,6 +195,9 @@ class Admin_BibController extends Zend_Controller_Action {
       $horaire = trim(urlencode($this->_request->getPost('horaire')));
       $photo = trim($filter->filter($this->_request->getPost('photo')));
       $gln = trim($filter->filter($this->_request->getPost('gln')));
+      $redmine_login = trim($filter->filter($this->_request->getPost('redmine_login')));
+      $redmine_password = trim($filter->filter($this->_request->getPost('redmine_password')));
+      $redmine_api_key = trim($filter->filter($this->_request->getPost('redmine_api_key')));
 
       if ($id !== false ) {
         $data = ['ID_SITE' => $id,
@@ -218,7 +221,10 @@ class Admin_BibController extends Zend_Controller_Action {
                  'PHOTO' => $photo,
                  'HORAIRE' => $horaire,
                  'INTERDIRE_RESA' => $this->_request->getPost('interdire_resa'),
-                 'GLN' => $gln];
+                 'GLN' => $gln,
+                 'redmine_login' => $redmine_login,
+                 'redmine_password' => $redmine_password,
+                 'redmine_api_key' => $redmine_api_key];
 
         $bib->updateAttributes($data)->save();
 
diff --git a/application/modules/admin/controllers/RedmineController.php b/application/modules/admin/controllers/RedmineController.php
new file mode 100644
index 00000000000..9878356a63c
--- /dev/null
+++ b/application/modules/admin/controllers/RedmineController.php
@@ -0,0 +1,60 @@
+<?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 Admin_RedmineController extends ZendAfi_Controller_Action {
+
+  public function testAction() {
+    $renderSuccess = function($data) {
+      return $this->view->tag('p', $this->extractConnectedUser($data), ['data-success' => 'true']);
+    };
+
+    return $this->withResponseDoInPopup(Class_WebService_Redmine::getCurrentUser(Class_Bib::find($this->_getParam('id_bib', 0))),
+                                        $renderSuccess,
+                                        $this->_('Echec de connexion'),
+                                        $this->_('Test de l\'API Redmine'));
+  }
+
+
+  protected function extractConnectedUser($data) {
+    return $this->_('Vous êtes connecté(e) en temps que %s %s',
+                    $data['user']['firstname'],
+                    $data['user']['lastname']);
+  }
+
+
+  protected function withResponseDoInPopup($data, $call_back, $message, $title) {
+    return $this->_helper->json(['title' => $title,
+                                 'content' => $this->withResponseDo($data, $call_back, $message)]);
+  }
+
+
+  protected function withResponseDo($data, $call_back, $message) {
+    if(false === $data)
+      return $message;
+
+    if(isset($data['error_message']) && '' != $data['error_message'])
+      return $data['error_message'];
+
+    return call_user_func($call_back, $data);
+  }
+}
+?>
\ No newline at end of file
diff --git a/application/modules/admin/views/scripts/bib/_form.phtml b/application/modules/admin/views/scripts/bib/_form.phtml
index fd89a4ca62b..15682b20fdb 100644
--- a/application/modules/admin/views/scripts/bib/_form.phtml
+++ b/application/modules/admin/views/scripts/bib/_form.phtml
@@ -6,145 +6,146 @@ $combo_statut_bib = $bibClass->getComboStatutBib($this->bib->VISIBILITE);
 ?>
 
 <center><div class="form" style="text-align:center;">
-    <div class="formTable">
-      <form name="form" action="<?php echo BASE_URL ?>/admin/bib/<?php echo $this->action; ?>/id/<?php echo $this->id; ?>" method="post">
+  <div class="formTable">
+    <form name="form" action="<?php echo BASE_URL ?>/admin/bib/<?php echo $this->action; ?>/id/<?php echo $this->id; ?>" method="post">
 
-        <fieldset>
-          <span id="abonne_erreur" class="abonne"><?php echo $this->message; ?></span>
-          <legend><?php echo $this->traduire('Bibliothèque'); ?></legend>
-          <br />
-          <table>
-            <tr>
-              <td class="droite" style="width:120px;"><?php echo $this->traduire('Nom'); ?> *</td>
-              <td class="gauche"><input type="text" id="libelle" name="libelle"  value="<?php echo $this->escape(trim($this->bib->LIBELLE)); ?>" style="width:100%;" maxlength="70"/></td>
-            </tr>
-            <tr>
-              <td class="droite"><?php echo $this->traduire('Nom du responsable'); ?></td>
-              <td class="gauche"><input type="text" id="responsable" name="responsable"  value="<?php echo $this->escape(trim($this->bib->RESPONSABLE)); ?>" style="width:100%;" maxlength="50"/></td>
-            </tr>
-          </table>
-        </fieldset><br />
+      <fieldset>
+        <span id="abonne_erreur" class="abonne"><?php echo $this->message; ?></span>
+        <legend><?php echo $this->traduire('Bibliothèque'); ?></legend>
+        <br />
+        <table>
+          <tr>
+            <td class="droite" style="width:120px;"><?php echo $this->traduire('Nom'); ?> *</td>
+            <td class="gauche"><input type="text" id="libelle" name="libelle"  value="<?php echo $this->escape(trim($this->bib->LIBELLE)); ?>" style="width:100%;" maxlength="70"/></td>
+          </tr>
+          <tr>
+            <td class="droite"><?php echo $this->traduire('Nom du responsable'); ?></td>
+            <td class="gauche"><input type="text" id="responsable" name="responsable"  value="<?php echo $this->escape(trim($this->bib->RESPONSABLE)); ?>" style="width:100%;" maxlength="50"/></td>
+          </tr>
+        </table>
+      </fieldset><br />
 
-        <fieldset>
-          <legend><?php echo $this->traduire('Photo'); ?></legend>
-          <table>
-            <tr>
-              <td><iframe style="height:180px;width:650px;overflow:hidden" frameborder="0" src="<?php print(BASE_URL) ?>/admin/bib/photo?id=<?php echo $this->id; ?>"></iframe></td>
-            </tr>
+      <fieldset>
+        <legend><?php echo $this->traduire('Photo'); ?></legend>
+        <table>
+          <tr>
+            <td><iframe style="height:180px;width:650px;overflow:hidden" frameborder="0" src="<?php print(BASE_URL) ?>/admin/bib/photo?id=<?php echo $this->id; ?>"></iframe></td>
+          </tr>
 
-          </table>
-        </fieldset><br />
+        </table>
+      </fieldset><br />
 
-        <fieldset>
-          <legend><?php echo $this->traduire('Adresse'); ?></legend>
-          <br />
-          <table>
-            <tr>
-              <td class="droite" style="width:120px;"><?php echo $this->traduire('Adresse'); ?></td>
-              <td class="gauche"><textarea id="adresse" name="adresse" cols="35"><?php echo $this->escape(trim($this->bib->ADRESSE)); ?></textarea></td>
-            </tr>
-            <tr>
-              <td class="droite"><?php echo $this->traduire('Code postal'); ?></td>
-              <td class="gauche"><input type="text" id="cp" name="cp"  value="<?php echo $this->escape(trim($this->bib->CP)); ?>" style="width:100%;"/></td>
-            </tr>
-            <tr>
-              <td class="droite"><?php echo $this->traduire('Ville'); ?> *</td>
-              <td class="gauche"><input type="text" id="ville" name="ville"  value="<?php echo $this->escape(trim($this->bib->VILLE)); ?>" style="width:100%;"/></td>
-            </tr>
-            <tr>
-              <td class="droite"><?php echo $this->traduire('Téléphone'); ?></td>
-              <td class="gauche"><input type="text" id="tel" name="tel"  value="<?php echo $this->escape(trim($this->bib->TELEPHONE)); ?>" style="width:100%;"/></td>
-            </tr>
-            <tr>
-              <td class="droite"><?php echo $this->traduire('Mail'); ?></td>
-              <td class="gauche"><input type="text" id="mail" name="mail"  value="<?php echo $this->escape(trim($this->bib->MAIL)); ?>" style="width:100%;"/></td>
-            </tr>
-            <tr>
-              <td class="droite"><?php echo $this->traduire('Mail suggestions'); ?></td>
-              <td class="gauche"><input type="text" id="mail_suggestions" name="mail_suggestions"  value="<?php echo $this->escape(trim($this->bib->MAIL_SUGGESTIONS)); ?>" style="width:100%;"/></td>
-            </tr>
-          </table>
-        </fieldset><br />
+      <fieldset>
+        <legend><?php echo $this->traduire('Adresse'); ?></legend>
+        <br />
+        <table>
+          <tr>
+            <td class="droite" style="width:120px;"><?php echo $this->traduire('Adresse'); ?></td>
+            <td class="gauche"><textarea id="adresse" name="adresse" cols="35"><?php echo $this->escape(trim($this->bib->ADRESSE)); ?></textarea></td>
+          </tr>
+          <tr>
+            <td class="droite"><?php echo $this->traduire('Code postal'); ?></td>
+            <td class="gauche"><input type="text" id="cp" name="cp"  value="<?php echo $this->escape(trim($this->bib->CP)); ?>" style="width:100%;"/></td>
+          </tr>
+          <tr>
+            <td class="droite"><?php echo $this->traduire('Ville'); ?> *</td>
+            <td class="gauche"><input type="text" id="ville" name="ville"  value="<?php echo $this->escape(trim($this->bib->VILLE)); ?>" style="width:100%;"/></td>
+          </tr>
+          <tr>
+            <td class="droite"><?php echo $this->traduire('Téléphone'); ?></td>
+            <td class="gauche"><input type="text" id="tel" name="tel"  value="<?php echo $this->escape(trim($this->bib->TELEPHONE)); ?>" style="width:100%;"/></td>
+          </tr>
+          <tr>
+            <td class="droite"><?php echo $this->traduire('Mail'); ?></td>
+            <td class="gauche"><input type="text" id="mail" name="mail"  value="<?php echo $this->escape(trim($this->bib->MAIL)); ?>" style="width:100%;"/></td>
+          </tr>
+          <tr>
+            <td class="droite"><?php echo $this->traduire('Mail suggestions'); ?></td>
+            <td class="gauche"><input type="text" id="mail_suggestions" name="mail_suggestions"  value="<?php echo $this->escape(trim($this->bib->MAIL_SUGGESTIONS)); ?>" style="width:100%;"/></td>
+          </tr>
+        </table>
+      </fieldset><br />
 
-        <fieldset>
-          <legend><?php echo $this->traduire('Territoire'); ?></legend>
-          <br />
-          <table>
-            <tr>
-              <td class="droite" style="width:120px;"><?php echo $this->traduire('Territoire'); ?></td>
-              <td class="gauche"><?php echo $this->combo_zone; ?></td>
-            </tr>
-            <tr>
-              <td class="droite"><?php echo $this->traduire('Lien cartographique'); ?></td>
-              <td class="gauche"><input type="text" id="carto" name="carto"  value="<?php echo $this->escape(trim($this->bib->LIEN_CARTO)); ?>" style="width:530px;"/></td>
-            </tr>
-          </table>
-        </fieldset><br />
+      <fieldset>
+        <legend><?php echo $this->traduire('Territoire'); ?></legend>
+        <br />
+        <table>
+          <tr>
+            <td class="droite" style="width:120px;"><?php echo $this->traduire('Territoire'); ?></td>
+            <td class="gauche"><?php echo $this->combo_zone; ?></td>
+          </tr>
+          <tr>
+            <td class="droite"><?php echo $this->traduire('Lien cartographique'); ?></td>
+            <td class="gauche"><input type="text" id="carto" name="carto"  value="<?php echo $this->escape(trim($this->bib->LIEN_CARTO)); ?>" style="width:530px;"/></td>
+          </tr>
+        </table>
+      </fieldset><br />
 
-        <fieldset>
-          <legend><?php echo $this->traduire('Information'); ?></legend>
-          <br />
-          <table>
-            <tr>
-              <td class="droite" style="width:120px;"><?php echo $this->traduire('Inscription'); ?></td>
-              <td class="gauche"><textarea  id="inscription" name="inscription" cols="35" ><?php echo $this->escape(trim(urldecode($this->bib->INSCRIPTION))); ?> </textarea></td>
-            </tr>
-            <tr>
-              <td class="droite"><?php echo $this->traduire('Prêt'); ?></td>
-              <td class="gauche"><textarea id="pret" name="pret"  cols="35"><?php echo $this->escape(trim(urldecode($this->bib->PRET))); ?> </textarea></td>
-            </tr>
-            <tr>
-              <td class="droite"><?php echo $this->traduire('Fonds'); ?></td>
-              <td class="gauche"><textarea type="text" id="fond" name="fond" cols="35"><?php echo $this->escape(trim(urldecode($this->bib->FOND))); ?> </textarea></td>
-            </tr>
-            <tr>
-              <td class="droite"><?php echo $this->traduire('Comment se procurer ce document '); ?></td>
-              <td class="gauche"><textarea type="text" id="procur" name="procur" cols="35" ><?php echo $this->escape(trim(urldecode($this->bib->PROCURE))); ?> </textarea></td>
-            </tr>
-            <tr>
-              <td class="droite"><?php echo $this->traduire('Annexe'); ?></td>
-              <td class="gauche"><textarea type="text" id="annexe" name="annexe"  cols="35"><?php echo $this->escape(trim(urldecode($this->bib->ANNEXE))); ?> </textarea></td>
-            </tr>
-            <tr>
-              <td class="droite" style="width:120px;"><?php echo $this->traduire('Horaire'); ?></td>
-              <td class="gauche"><textarea  id="horaire" name="horaire" cols="35" ><?php echo $this->escape(trim(urldecode($this->bib->HORAIRE))); ?> </textarea></td>
-            </tr>
-          </table>
-          <input type="hidden" id="photo" name="photo"  value="<?php echo $this->escape(trim($this->bib->PHOTO)); ?>"/>
-        </fieldset><br />
+      <fieldset>
+        <legend><?php echo $this->traduire('Information'); ?></legend>
+        <br />
+        <table>
+          <tr>
+            <td class="droite" style="width:120px;"><?php echo $this->traduire('Inscription'); ?></td>
+            <td class="gauche"><textarea  id="inscription" name="inscription" cols="35" ><?php echo $this->escape(trim(urldecode($this->bib->INSCRIPTION))); ?> </textarea></td>
+          </tr>
+          <tr>
+            <td class="droite"><?php echo $this->traduire('Prêt'); ?></td>
+            <td class="gauche"><textarea id="pret" name="pret"  cols="35"><?php echo $this->escape(trim(urldecode($this->bib->PRET))); ?> </textarea></td>
+          </tr>
+          <tr>
+            <td class="droite"><?php echo $this->traduire('Fonds'); ?></td>
+            <td class="gauche"><textarea type="text" id="fond" name="fond" cols="35"><?php echo $this->escape(trim(urldecode($this->bib->FOND))); ?> </textarea></td>
+          </tr>
+          <tr>
+            <td class="droite"><?php echo $this->traduire('Comment se procurer ce document '); ?></td>
+            <td class="gauche"><textarea type="text" id="procur" name="procur" cols="35" ><?php echo $this->escape(trim(urldecode($this->bib->PROCURE))); ?> </textarea></td>
+          </tr>
+          <tr>
+            <td class="droite"><?php echo $this->traduire('Annexe'); ?></td>
+            <td class="gauche"><textarea type="text" id="annexe" name="annexe"  cols="35"><?php echo $this->escape(trim(urldecode($this->bib->ANNEXE))); ?> </textarea></td>
+          </tr>
+          <tr>
+            <td class="droite" style="width:120px;"><?php echo $this->traduire('Horaire'); ?></td>
+            <td class="gauche"><textarea  id="horaire" name="horaire" cols="35" ><?php echo $this->escape(trim(urldecode($this->bib->HORAIRE))); ?> </textarea></td>
+          </tr>
+        </table>
+        <input type="hidden" id="photo" name="photo"  value="<?php echo $this->escape(trim($this->bib->PHOTO)); ?>"/>
+      </fieldset><br />
 
-        <fieldset>
-          <legend><?php echo $this->traduire('Configuration'); ?></legend>
-          <br />
-          <table>
-            <tr>
-              <td class="droite" style="width:160px;"><?php echo $this->traduire('Statut de la bib'); ?></td>
-              <td class="gauche"><?php echo $combo_statut_bib; ?></td>
-            </tr>
-            <tr>
-              <td class="droite"><?php echo $this->traduire('Adresse URL'); ?></td>
-              <td class="gauche"><input type="text" id="url" name="url"  value="<?php echo $this->escape(trim($this->bib->URL_WEB)); ?>" style="width:100%;"/></td>
-            </tr>
-            <tr>
-              <td class="droite"><?php echo $this->traduire('Interdire les réservations'); ?></td>
-              <td class="gauche">
-                <?php echo $this->formSelect("interdire_resa", $this->bib->INTERDIRE_RESA, "", array("0" => "non", "1" => "oui")) ?>
-              </td>
-            </tr>
-            <?php if (Class_AdminVar::isDilicomPNBEnabled()) { ?>
+      <fieldset>
+        <legend><?php echo $this->traduire('Configuration'); ?></legend>
+        <br />
+        <table>
+          <tr>
+            <td class="droite" style="width:160px;"><?php echo $this->traduire('Statut de la bib'); ?></td>
+            <td class="gauche"><?php echo $combo_statut_bib; ?></td>
+          </tr>
+          <tr>
+            <td class="droite"><?php echo $this->traduire('Adresse URL'); ?></td>
+            <td class="gauche"><input type="text" id="url" name="url"  value="<?php echo $this->escape(trim($this->bib->URL_WEB)); ?>" style="width:100%;"/></td>
+          </tr>
+          <tr>
+            <td class="droite"><?php echo $this->traduire('Interdire les réservations'); ?></td>
+            <td class="gauche">
+              <?php echo $this->formSelect("interdire_resa", $this->bib->INTERDIRE_RESA, "", array("0" => "non", "1" => "oui")) ?>
+            </td>
+          </tr>
+          <?php if (Class_AdminVar::isDilicomPNBEnabled()) { ?>
             <tr>
               <td class="droite"><?php echo $this->_('GLN (PNB Dilicom)'); ?></td>
               <td class="gauche"><input type="text" id="gln" name="gln"  value="<?php echo $this->escape(trim($this->bib->GLN)); ?>" style="width:100%;"/></td>
             </tr>
-            <?php } ?>
-          </table>
-        </fieldset><br />
-        <table>
-          <tr>
-            <td align="right" style="padding-right:5px;"> <?php echo $this->bouton('type=V'); ?></td>
-            <td align="left" style="padding-left:5px;"> <?php echo $this->bouton('id=c_29', 'picto=del.gif', 'texte=Annuler', 'url=' . BASE_URL . '/admin/bib', 'largeur=120px'); ?></td>
-          </tr>
+          <?php } ?>
         </table>
-        <input type="hidden" name="id_bib" id="id_bib" value="<?php echo $this->bib->ID_SITE; ?>" />
-      </form></div></div><br>
+      </fieldset><br />
+      <?php echo $this->Redmine_Account($this->bib); ?>
+      <table>
+        <tr>
+          <td align="right" style="padding-right:5px;"> <?php echo $this->bouton('type=V'); ?></td>
+          <td align="left" style="padding-left:5px;"> <?php echo $this->bouton('id=c_29', 'picto=del.gif', 'texte=Annuler', 'url=' . BASE_URL . '/admin/bib', 'largeur=120px'); ?></td>
+        </tr>
+      </table>
+      <input type="hidden" name="id_bib" id="id_bib" value="<?php echo $this->bib->ID_SITE; ?>" />
+    </form></div></div><br>
 </center>
diff --git a/cosmogramme/sql/patch/patch_283.php b/cosmogramme/sql/patch/patch_283.php
new file mode 100644
index 00000000000..85f33d09f12
--- /dev/null
+++ b/cosmogramme/sql/patch/patch_283.php
@@ -0,0 +1,7 @@
+<?php
+$adapter = Zend_Db_Table::getDefaultAdapter();
+
+try {
+  $adapter->query('ALTER TABLE bib_c_site ADD COLUMN redmine_login varchar(250) null, ADD COLUMN redmine_password varchar(250) null, ADD COLUMN redmine_api_key varchar(250) null');
+} catch(Exception $e) {}
+?>
\ No newline at end of file
diff --git a/library/Class/AdminVar.php b/library/Class/AdminVar.php
index df929ab1c55..d92e3660073 100644
--- a/library/Class/AdminVar.php
+++ b/library/Class/AdminVar.php
@@ -286,10 +286,10 @@ class Class_AdminVarLoader extends Storm_Model_Loader {
                    'PREMIERCHAPITRE_BIB_ID' => Class_Adminvar_Meta::newDefault($this->_('Clé d\'identification Premier-Chapitre de la bibliothèque. Cette clé doit être fournie par Premier-Chapitre.'))->bePrivate(),
                    'PREMIERCHAPITRE_BMID' => Class_Adminvar_Meta::newDefault($this->_('Login du portail fourni par Premier-Chapitre.'))->bePrivate(),
                    'PREMIERCHAPITRE_BMKEY' => Class_Adminvar_Meta::newDefault($this->_('Password du portail fourni par Premier-Chapitre'))->bePrivate(),
-        ],
+                   'REDMINE_SERVER_URL' => Class_Adminvar_Meta::newDefault($this->_('Url du serveur redmine'))->bePrivate()       ],
           'users' => ['NDAYS_EXPIRY_NOTICE' => Class_AdminVar_Meta::newDefault($this->_('Prévenir l\'utilisateur xx jour(s) avant l\'expiration de son abonnement (par défaut 30 jours).'), ['value' => 30]),
                       'DISABLE_SUGGESTIONS' => Class_AdminVar_Meta::newOnOff($this->_('Désactivation des suggestions d\'achats'))->bePrivate(),
-        ]
+          ]
 
       ];
   }
@@ -438,6 +438,14 @@ class Class_AdminVarLoader extends Storm_Model_Loader {
   }
 
 
+  /**
+   * @return bool
+   */
+  public function isRedmineEnabled() {
+    return ('' != Class_AdminVar::get('REDMINE_SERVER_URL'));
+  }
+
+
   /**
    * @return bool
    */
diff --git a/library/Class/WebService/Redmine.php b/library/Class/WebService/Redmine.php
new file mode 100644
index 00000000000..43698210e0c
--- /dev/null
+++ b/library/Class/WebService/Redmine.php
@@ -0,0 +1,75 @@
+<?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 Class_WebService_Redmine extends Class_WebService_Abstract {
+  use Trait_Translator;
+
+  protected static $_redmine_client;
+
+  protected function missingApiRequirement($library) {
+    if($error_message = $this->missingApiRequirementMessage($library))
+      return ['error_message' => $error_message];
+    return null;
+  }
+
+
+  protected function missingApiRequirementMessage($library) {
+    if(!$library)
+      return $this->_('Vous devez fournir un identifiant de bibliothèque en paramètre');
+
+    if(!Class_AdminVar::isRedmineEnabled())
+      return $this->_('Aucun serveur Redmine est renseigné');
+
+    if((!$library->getRedmineLogin() || !$library->getRedminePassword()) && !$library->getRedmineApiKey())
+      return $this->_('Vous devez renseigner les champs login et password ou le chammp clé d\'API');
+
+    return null;
+  }
+
+
+  protected function getUserApi($library) {
+    return $this->getClient($library)->api('user');
+  }
+
+
+  public function getClient($library) {
+    if(static::$_redmine_client == null || !isset(static::$_redmine_client))
+      static::$_redmine_client = new Redmine\Client(Class_AdminVar::get('REDMINE_SERVER_URL'), $library->getRedmineLogin(), $library->getRedminePassword());
+
+    return static::$_redmine_client;
+  }
+
+
+  public static function setClient($redmine_client) {
+    static::$_redmine_client = $redmine_client;
+  }
+
+
+  public static function getCurrentUser($library) {
+    $instance = new self();
+
+    if($requirement = $instance->missingApiRequirement($library))
+      return $requirement;
+
+    return $instance->getUserApi($library)->getCurrentUser();
+  }
+}
+?>
\ No newline at end of file
diff --git a/library/Redmine b/library/Redmine
new file mode 160000
index 00000000000..c6b073270e2
--- /dev/null
+++ b/library/Redmine
@@ -0,0 +1 @@
+Subproject commit c6b073270e243f72aac0d164654cd39f25b5a7bf
diff --git a/library/ZendAfi/View/Helper/Redmine/Account.php b/library/ZendAfi/View/Helper/Redmine/Account.php
new file mode 100644
index 00000000000..7b2f522c639
--- /dev/null
+++ b/library/ZendAfi/View/Helper/Redmine/Account.php
@@ -0,0 +1,83 @@
+<?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_Redmine_Account extends ZendAfi_View_Helper_BaseHelper {
+
+  public function Redmine_Account($library) {
+    if (!Class_AdminVar::isRedmineEnabled())
+      return '';
+
+    $html =
+      '<fieldset>' .
+      '<legend> ' . $this->_('Configuration du compte Redmine') . '</legend><br />' .
+      '<table>' .
+      '<tr>'.
+      '<td class="droite">' . $this->_('Login') . '</td>' .
+      '<td class="gauche"><input type="text" id="redmine_login" name="redmine_login"  value="' . $library->getRedmineLogin() . '" style="width:100%;"/></td>' .
+      '</tr>' .
+      '<tr>' .
+      '<td class="droite">' . $this->_('Password') . '</td>' .
+      '<td class="gauche"><input type="password" id="redmine_password" name="redmine_password"  value="' . $library->getRedminePassword() . '" style="width:100%;"/></td>' .
+      '</tr>' .
+      '<tr>'.
+      '<td class="droite"></td>' .
+      '<td class="gauche" style="width: 100%">' . $this->_('ou') . '</td>' .
+      '</tr>'.
+      '<tr>' .
+      '<td class="droite">' . $this->_('Clé d\'API') . '</td>' .
+      '<td class="gauche"><input type="text" id="redmine_api_key" name="redmine_api_key"  value="' . $library->getRedmineApiKey() . '" style="width:100%;"/></td>' .
+      '</tr>' .
+      '<tr>' .
+      '<td class="droite">' . $this->_('Tester les paramètres') . '</td>' .
+      '<td class="gauche">' . $this->Redmine_AccountStatus($library) . '</td>' .
+      '</tr>' .
+      '</table>' .
+      '</fieldset><br />';
+
+    return $html;
+  }
+
+
+  public function Redmine_AccountStatus($library) {
+    if (!Class_AdminVar::isRedmineEnabled())
+      return '';
+
+        Class_ScriptLoader::getInstance()
+      ->addOpacScript('redmine')
+      ->addJQueryReady('$("a.redmine_status").autoRefresh();');
+
+    $url = $this->view->url(['module' => 'admin',
+                                          'controller' => 'redmine',
+                                          'action' => 'test',
+                                          'id_bib' => $library->getId()], null, true);
+
+    $anchor = $this->view->tagAnchor($url,
+                                  '',
+                                  ['data-popup' => 'true',
+                                   'data-status' => $url,
+                                   'title' => $this->_('Tester la connexion vers l\'API de redmine'),
+                                   'class' => 'redmine_status']);
+
+    return $anchor;
+  }
+}
+?>
\ No newline at end of file
diff --git a/library/startup.php b/library/startup.php
index 1763d0903e2..1a678be59db 100644
--- a/library/startup.php
+++ b/library/startup.php
@@ -51,6 +51,7 @@ function setupOpac() {
   $front_controller = setupFrontController($cfg);
   setupPagination();
   setupRestful();
+  setupRedmine();
 
   return $front_controller;
 }
@@ -359,3 +360,11 @@ function setupRestful() {
                               'rest',
                               Zend_Controller_Action_HelperBroker::getExistingHelper('ViewRenderer'));
 }
+
+
+function setupRedmine() {
+  if(!Class_AdminVar::isRedmineEnabled())
+    return;
+
+  require_once __DIR__ . '/Redmine/lib/autoload.php';
+}
\ No newline at end of file
diff --git a/public/admin/css/global.css b/public/admin/css/global.css
index 6c8c328d7f2..fd8e7927ea0 100644
--- a/public/admin/css/global.css
+++ b/public/admin/css/global.css
@@ -1318,3 +1318,16 @@ div#reader {
     font-weight: bold;
     color:#808080;
 }
+
+
+.redmine_status {
+    display: inline-block;
+    border-radius: 8px;
+    width: 16px;
+    height: 16px;
+    background-color: red;
+}
+
+.redmine_status.redmine_green {
+    background-color: green;
+}
\ No newline at end of file
diff --git a/public/opac/js/redmine.js b/public/opac/js/redmine.js
new file mode 100644
index 00000000000..3e12ed93849
--- /dev/null
+++ b/public/opac/js/redmine.js
@@ -0,0 +1,50 @@
+/**
+ * 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
+ */
+
+(function ( $ ) {
+  $.fn.autoRefresh = function () {
+    var anchor = $(this);
+    
+    var update = function(anchor) {
+      $.getJSON(anchor.attr('data-status')).done( function(data) {
+	anchor.addClass('redmine_green');
+	if( -1 == data.content.indexOf('data-success'))
+	  anchor.removeClass('redmine_green');
+      });
+    };
+
+    update(anchor);
+
+    // var form = $('[action*="admin/bib/edit"]');
+    // form.find('#redmine_login, #redmine_password').change(function() {
+    //   form.submit(function (e) {
+    // 	var url = form.attr('action');
+    // 	$.ajax({
+    //   	  type: 'POST',
+    //   	  url: url,
+    //   	  data: form.serialize()
+    // 	}).done(function(msg) {
+    // 	  update(anchor);
+    // 	});
+    //   });
+    // });
+
+  };
+} (jQuery));
diff --git a/tests/application/modules/admin/controllers/AdminIndexControllerTest.php b/tests/application/modules/admin/controllers/AdminIndexControllerTest.php
index 916e14fe861..69489966362 100644
--- a/tests/application/modules/admin/controllers/AdminIndexControllerTest.php
+++ b/tests/application/modules/admin/controllers/AdminIndexControllerTest.php
@@ -187,6 +187,12 @@ class AdminIndexControllerAdminVarActionTest extends Admin_AbstractControllerTes
   }
 
 
+  /** @test */
+  public function redmineServerUrlShouldBePresent() {
+    $this->assertXpathContentContains('//td', 'REDMINE_SERVER_URL');
+  }
+
+
   /** @test */
   public function pageShouldBeHTML5Valid() {
     $this->assertHTML5();
diff --git a/tests/application/modules/admin/controllers/BibControllerTest.php b/tests/application/modules/admin/controllers/BibControllerTest.php
index bfe4484309f..5136632b1cf 100644
--- a/tests/application/modules/admin/controllers/BibControllerTest.php
+++ b/tests/application/modules/admin/controllers/BibControllerTest.php
@@ -38,6 +38,9 @@ abstract class BibControllerTestCase extends AbstractControllerTestCase {
                                         'mail' => 'jp@annecy.com',
                                         'telephone' => '04 50 51 32 12',
                                         'gln' => '45321',
+                                        'redmine_login' => '',
+                                        'redmine_password' => '',
+                                        'redmine_api_key' => '',
                                         'article_categories' => []]);
 
 
@@ -237,6 +240,12 @@ class BibControllerWithAdminBibEditAnnecyTest extends BibControllerWithAdminBibT
   public function inputGlnShouldNotBeVisible() {
     $this->assertNotXPath('//input[@name="gln"]');
   }
+
+
+  /** @test */
+  public function withNoRedmineAPISetRedmineAccountNameShouldNotBePresent() {
+    $this->assertNotXPathContentContains('//table//tr//td', 'nom d\'utilsateur');
+  }
 }
 
 
@@ -1159,4 +1168,100 @@ class BibControllerPermissionsPortalPostActionTest
                                                                $this->group,
                                                                Class_Bib::getPortail()));
   }
+}
+
+
+
+abstract class BibControllerWithRedmineAPITestCase extends BibControllerWithAdminBibTestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->fixture('Class_AdminVar',
+                   ['id' => 'REDMINE_API_KEY', 'valeur' => '123456789']);
+
+    $this->fixture('Class_AdminVar',
+                   ['id' => 'REDMINE_SERVER_URL', 'valeur' => 'http://redmine-forge.gnu']);
+  }
+}
+
+
+
+class BibControllerWithRemineAPITest extends BibControllerWithRedmineAPITestCase {
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('admin/bib/edit/id/2', true);
+  }
+
+
+  /** @test */
+  public function redmineAccountNameShouldBeEmpty() {
+    $this->assertXPath('//table//tr//td//input[@name="redmine_login"][@value=""]', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function redminePasswordShouldBeEmpty() {
+    $this->assertXPath('//table//tr//td//input[@name="redmine_password"][@value=""][@type="password"]', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function redmineApiKeyShouldBeEmpty() {
+    $this->assertXPath('//table//tr//td//input[@name="redmine_api_key"][@value=""][@type="text"]', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function anchorTestApiSettingsShouldBePresent() {
+    $this->assertXPath('//a[contains(@href, "/admin/redmine/test/id_bib/2")][@data-popup="true"][@data-status="/admin/redmine/test/id_bib/2"]', $this->_response->getBody());
+  }
+
+
+  /** @test */
+  public function redmineJSShouldBeLoaded() {
+    $this->assertXPath('//script[contains(@src, "/opac/js/redmine.js")]');
+  }
+
+
+  /** @test */
+  public function redmineAutoRefreshShouldLoaded() {
+    $this->assertXPathContentContains('//script', '$("a.redmine_status").autoRefresh();');
+  }
+}
+
+
+
+class BibControllerWithRemineAPIPostDispatchTest extends BibControllerWithRedmineAPITestCase {
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture('Class_Users',
+                   ['id' => 1,
+                    'login' => 'admin',
+                    'password' => 'admin',
+                    'bib' => Class_Bib::find(2),
+                    'role_level' => ZendAfi_Acl_AdminControllerRoles::ADMIN_BIB]);
+
+    $this->postDispatch('admin/bib/edit/id/2', ['id_bib' => 2,
+                                                'redmine_login' => 'lib@annecy',
+                                                'redmine_password' => 'pwd',
+                                                'redmine_api_key' => '123456789']);
+  }
+
+
+  /** @test */
+  public function libAnnecyRedmineLoginShouldBeLibAtAnnecy() {
+    $this->assertEquals('lib@annecy', Class_Bib::find(2)->getRedmineLogin());
+  }
+
+
+  /** @test */
+  public function libAnnecyRedminePasswordShouldBePwd() {
+    $this->assertEquals('pwd', Class_Bib::find(2)->getRedminePassword());
+  }
+
+
+  /** @test */
+  public function libAnnecyRedmineAPIKeyShouldBe123456789() {
+    $this->assertEquals('123456789', Class_Bib::find(2)->getRedmineApiKey());
+  }
 }
\ No newline at end of file
diff --git a/tests/application/modules/admin/controllers/RedmineControllerTest.php b/tests/application/modules/admin/controllers/RedmineControllerTest.php
new file mode 100644
index 00000000000..a5be148472b
--- /dev/null
+++ b/tests/application/modules/admin/controllers/RedmineControllerTest.php
@@ -0,0 +1,173 @@
+<?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
+ */
+
+require_once 'tests/fixtures/RedmineFixtures.php';
+
+class Admin_RedmineControllerTestCase extends Admin_AbstractControllerTestCase {
+  protected $_storm_default_to_volatile = true;
+}
+
+
+
+class Admin_RedmineControllerTestActionWithNoBibTest extends Admin_RedmineControllerTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/redmine/test', true);
+  }
+
+
+  /** @test */
+  public function responseShouldContainsNoBibGiven() {
+    $this->assertEquals(RedmineFixtures::noLibraryGiven(), $this->_response->getBody());
+  }
+}
+
+
+
+class Admin_RedmineControllerTestActionWithBibTest extends Admin_RedmineControllerTestCase {
+
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture('Class_Bib',
+                   ['id' => 1,
+                    'libelle' => 'Mediatheque d\'Annecy',
+                    'redmine_api_key' => '123456789',
+                    'redmine_login' => 'annecy@customer',
+                    'redmine_password' => 'soon']);
+
+    $this->dispatch('/admin/redmine/test/id_bib/1', true);
+  }
+
+
+  /** @test */
+  public function responseShouldContainsNoBibGiven() {
+    $this->assertEquals(RedmineFixtures::noServerUrlGiven(), $this->_response->getBody());
+  }
+}
+
+
+
+class Admin_RedmineControllerTestActionWithEmptyBibTest extends Admin_RedmineControllerTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture('Class_AdminVar',
+                   ['id' => 'REDMINE_SERVER_URL',
+                    'valeur' => 'http://forge.afi-sa.fr']);
+
+    $this->fixture('Class_Bib',
+                   ['id' => 1,
+                    'libelle' => 'Mediatheque d\'Annecy',
+                    'redmine_api_key' => '',
+                    'redmine_login' => 'haveBeenSet',
+                    'redmine_password' => '']);
+
+    $this->dispatch('/admin/redmine/test/id_bib/1', true);
+  }
+
+
+  /** @test */
+  public function responseShouldContainsNoBibGiven() {
+    $this->assertEquals(RedmineFixtures::missingParams(), $this->_response->getBody());
+  }
+}
+
+
+
+abstract class Admin_RedmineControllerWithApiRequirementSetTestCase extends Admin_RedmineControllerTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $redmine_api = Storm_Test_ObjectWrapper::mock();
+    $redmine_api
+      ->whenCalled('getCurrentUser')
+      ->answers(RedmineFixtures::currentUser());
+
+    $redmine_client = Storm_Test_ObjectWrapper::mock();
+    $redmine_client
+      ->whenCalled('api')
+      ->answers($redmine_api);
+
+    Class_WebService_Redmine::setClient($redmine_client);
+
+    $this->fixture('Class_AdminVar',
+                   ['id' => 'REDMINE_SERVER_URL',
+                    'valeur' => 'http://forge.afi-sa.fr']);
+
+    $this->fixture('Class_Bib',
+                   ['id' => 1,
+                    'libelle' => 'Mediatheque d\'Annecy',
+                    'redmine_api_key' => '',
+                    'redmine_login' => 'haveBeenSet',
+                    'redmine_password' => '123456']);
+  }
+
+
+  public function tearDown() {
+    Class_WebService_Redmine::setClient(null);
+    parent::tearDown();
+  }
+}
+
+
+
+class Admin_RedmineControllerTestActionWithBibAndLoginPasswordSetTest extends Admin_RedmineControllerWithApiRequirementSetTestCase {
+
+  public function setUp() {
+    parent::setUp();
+    $this->dispatch('/admin/redmine/test/id_bib/1', true);
+  }
+
+
+  /** @test */
+  public function responseShouldContainsSandreBocoeur() {
+    $this->assertEquals(RedmineFixtures::sandreBocoeur(), $this->_response->getBody());
+  }
+}
+
+
+
+class Admin_RedmineControllerTestActionWithBibAndAPIKKeyTest extends Admin_RedmineControllerWithApiRequirementSetTestCase {
+
+  public function setUp() {
+    parent::setUp();
+
+    $this->fixture('Class_Bib',
+                   ['id' => 1,
+                    'libelle' => 'Mediatheque d\'Annecy',
+                    'redmine_api_key' => '123456789abc',
+                    'redmine_login' => '',
+                    'redmine_password' => '']);
+
+    $this->dispatch('/admin/redmine/test/id_bib/1', true);
+  }
+
+
+  /** @test */
+  public function responseShouldContainsSandreBocoeur() {
+    $this->assertEquals(RedmineFixtures::sandreBocoeur(), $this->_response->getBody());
+  }
+}
diff --git a/tests/db/UpgradeDBTest.php b/tests/db/UpgradeDBTest.php
index b1fe317f8a1..c244161f473 100644
--- a/tests/db/UpgradeDBTest.php
+++ b/tests/db/UpgradeDBTest.php
@@ -307,3 +307,32 @@ class UpgradeDB_282_Test extends UpgradeDBTestCase {
     $this->assertColumnExists('bib_admin_users.SETTINGS');
   }
 }
+
+
+
+class UpgradeDB_283_Test extends UpgradeDBTestCase {
+
+  public function prepare() {
+    try {
+      $this->query('ALTER TABLE bib_c_site DROP redmine_login, DROP redmine_password, DROP redmine_api_key');
+    } catch(Exception $e) {}
+  }
+
+
+  /** @test */
+  public function bibRedmineLoginColumnShouldExist() {
+    $this->assertColumn('bib_c_site', 'redmine_login');
+  }
+
+
+  /** @test */
+  public function bibRedminePasswordColumnShouldExist() {
+    $this->assertColumn('bib_c_site', 'redmine_password');
+  }
+
+
+  /** @test */
+  public function bibRedmineApiKeyColumnShouldExist() {
+    $this->assertColumn('bib_c_site', 'redmine_api_key');
+  }
+}
diff --git a/tests/fixtures/RedmineFixtures.php b/tests/fixtures/RedmineFixtures.php
new file mode 100644
index 00000000000..67c93f9ef0c
--- /dev/null
+++ b/tests/fixtures/RedmineFixtures.php
@@ -0,0 +1,57 @@
+<?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 RedmineFixtures {
+  public static function noLibraryGiven() {
+    return '{"title":"Test de l\'API Redmine","content":"Vous devez fournir un identifiant de biblioth\u00e8que en param\u00e8tre"}';
+  }
+
+
+  public static function noServerUrlGiven() {
+    return '{"title":"Test de l\'API Redmine","content":"Aucun serveur Redmine est renseign\u00e9"}';
+  }
+
+
+  public static function missingParams() {
+    return '{"title":"Test de l\'API Redmine","content":"Vous devez renseigner les champs login et password ou le chammp cl\u00e9 d\'API"}';
+  }
+
+
+  public static function currentUser() {
+    return ['user' => ['id' => '123456',
+                       'login' => 'haveBeenSet',
+                       'firstname' => 'Sandre',
+                       'lastname' => 'Bocoeur',
+                       'mail' => 'bocoeur@bokeh-users.fr',
+                       'created_on' => '2013-04-02T11:41:36Z',
+                       'last_login_on' => '2015-12-09T11:02:27Z',
+                       'api_key' => '123456789abcd',
+                       'custom_fields' => [],
+                       'memberships' => [] ] ];
+  }
+
+
+  public static function sandreBocoeur() {
+    return '{"title":"Test de l\'API Redmine","content":"<p data-success=\"true\">Vous \u00eates connect\u00e9(e) en temps que Sandre Bocoeur<\/p>"}';
+  }
+}
+?>
\ No newline at end of file
-- 
GitLab