commit e9b44dc19fd4abe78b3035f50d83ce0ae9c24258
Author: jenselme <julien.enselme@centrale-marseille.fr>
Date:   Mon Jul 8 21:19:20 2013 +0200

    Version 2 de goto_redmine.py et constantes.py
    
    Réécriture du script avec des classes afin de permettre une résolution
    plus aisée des problèmes posés par l’ancien script :
    - Les captures d’écran ne sont pas présentes
    - Les liens vers les tâches sont cassés
    - L’ordre des commentaires n’est pas bon
    - Les informations sur les contributeurs et la date sont perdues
    Le script donne des informations sur la progression (ex post issue no 12 on 116)
    
    goto_redmine.py et constantes.py ont été modifiés afin d’écrire
    issues.csv et comments.csv qui permettent à fix-db de rétablir
    directement dans la base de données les informations concernant les
    contributeurs et la date de la contribution.
    
    J'ai ajouté également des réponses de redmine lorsqu’on poste une tâche e$
    commentaire pour information
    
    Correction de constantes.py pour gianny

diff --git a/scripts_divers/migrer_taches_vers_redmine/constantes.py b/scripts_divers/migrer_taches_vers_redmine/constantes.py
index a2c97b64..2ec26a72 100644
--- a/scripts_divers/migrer_taches_vers_redmine/constantes.py
+++ b/scripts_divers/migrer_taches_vers_redmine/constantes.py
@@ -1,39 +1,61 @@
-#Dictionnaire des gens dont on a la clé API
-#id: clé
-SUBMITERS = {'jenselme': '', 'admin': 'a7630a1244be353424cc0f56a49657c3fa9dbcc6'}
-MANAGER_TEST = 'admin'
-MANAGER = 'jenselme'
+######## Dict of people who have an api-keys
+#id: key
+SUBMITERS = {'jenselme': 'b3ce5345b1444b66f05f6a143ffec6f401e8f17e', 'admin': 'a7630a1244be353424cc0f56a49657c3fa9dbcc6'}
+MANAGER = 'admin' # Can post issues
+#MANAGER = 'jenselme'
 
-#les entêtes des requêtes http
+
+######## HTTP headers
 Headers = {'content-type': 'application/json', 'X-Redmine-API-Key': ''}
 Headers_GET = {'X-Redmine-API-Key': ''}
 
-#là où on poste les tâches
-URL_POST_ISSUES = 'https://forge.centrale-marseille.fr/issues'
-#La liste des tâches
-URL_ISSUES_JSON_TEST = 'http://localhost/redmine/projects/test/issues.json'
-URL_ISSUES_JSON = 'https://forge.centrale-marseille.fr/projects/clubdrupal/issues.json'
-#l’url du projet
-URL_PROJECT_TEST = 'http://localhost/redmine/projects/test'
-URL_PROJECT = 'https://forge.centrale-marseille.fr/projects/clubdrupal'
-URL_REDMINE_TEST = 'http://localhost/redmine'
-URL_REDMINE = 'http://forge.centrale-marseille.fr'
-
-#là où sont les tâches
-LIST_TODO = 'http://localhost/portail/liste-tache'
-
-#url de base de l’emplacement du contenu
-BASE_URL = 'http://localhost/portail'
-PROJECT_ID = 30
+
+######## Base URL of where to post the issues
+URL_ISSUES = 'http://debian/redmine/issues'
+#URL_POST_ISSUES = 'https://forge.centrale-marseille.fr/issues'
+
+
+######## Issues list in redmine of a give project
+URL_ISSUES_JSON = 'http://debian/redmine/projects/test/issues.json'
+#URL_ISSUES_JSON = 'https://forge.centrale-marseille.fr/projects/clubdrupal/issues.json'
+
+######## Project URL
+URL_PROJECT = 'http://debian/redmine/projects/test'
+#URL_PROJECT = 'https://forge.centrale-marseille.fr/projects/clubdrupal'
+URL_REDMINE = 'http://debian/redmine'
+#URL_REDMINE = 'http://forge.centrale-marseille.fr'
+
+
+######## Issues location in drupal (node id in a csv file to be parsed)
+LIST_TODO = 'http://debian/portail/liste-tache'
+LIST_TODO_CSV = 'http://debian/portail/liste_tache_csv'
+
+
+######## Base url of drupal
+BASE_URL = 'http://debian/portail'
+
+
+######## Various id
+PROJECT_ID = 1
+#PROJECT_ID = 30
 TRACKER_ID = 2
+USER_ID = {'ftorregrosa': 45, 'Ismaeil ABOULJAMAL': 44, 'iabouljamal': 44,\
+           'Nono LiNux': 47, 'nlehuby': 47, 'gfranchi': 78, 'assos': 78, '82': 78, 'dgeo': 22,\
+           'Jean': 48,'jfeutry': 48, 'adeprey': 49,'jpennec': 46, 'gt': 42, 'jenselme': 43,
+           '1': 44, '176': 43, '123': 48, '114': 49,\
+           '99': 46, '97': 45, '20': 47, '16': 42, '9': 22}
 
 
-########## dictionnaires de correspondance
+######## Dict to translate drupal's fields into redmine ids
 DONE_RATIO = {'En pause': 50, 'À commencer': 0, 'Entamée': 20, 'Bien avancée': 80, 'Terminée (success)': 100, 'Fermée (won\'t fix)': 100}
 PRIORITY = {'5 - Très basse': 3, '4 - Basse': 3, '3 - Moyenne': 4, '2 - Haute': 5,\
             '1 - Très haute': 6, 'Très basse': 3, 'Basse': 3, 'Moyenne': 4,\
             'Haute': 5, 'Très haute':6, '0': 3, '1': 3, '2': 4, '3': 5, '4': 6}
 STATUS = {'En cours': 2, 'Fermée': 5, 'Rejetée': 6, 'En pause': 7}
-#NB sur le portail, on a les équivalences suivantes
-#pour le champ version de drupal : 17 : drupal6, 18 : drupal7
+#NB: sur le portail, on a les équivalences suivantes
+# For drupal_version_field : 17 : drupal6, 18 : drupal7
 DRUPAL_VERSION = {'17': 2, '18': 1}
+
+
+###### Other
+REGEXP_NAME = 'http://debian/portail/content/t%C3%A2che/(.*)'
diff --git a/scripts_divers/migrer_taches_vers_redmine/fix-db.pl b/scripts_divers/migrer_taches_vers_redmine/fix-db.pl
new file mode 100644
index 00000000..134f3635
--- /dev/null
+++ b/scripts_divers/migrer_taches_vers_redmine/fix-db.pl
@@ -0,0 +1,214 @@
+# In order to complete the migration to Redmine, we need to preserve date
+# and username. To do this, we need to fix the database.
+
+use strict;
+use warnings;
+use DBI;
+
+my $db		= 'redmine_default';
+my $server	= 'localhost'; # Il est possible de mettre une adresse IP
+my $user        = 'root';      # identifiant
+my $passwd	= 'tata';
+my $port	= '';
+
+
+##### Create DB connection
+my $dbh = DBI->connect( "DBI:mysql:database=$db;host=$server;port=$port", $user, $passwd, { RaiseError => 1, } ) or die "Connection impossible à la base de données $db !\n $! \n $@\n$DBI::errstr";
+
+
+
+###### Initialisation of hashes needed to fix URL
+open(my $fix_url_issues_csv, '<', 'fix_url_issues.csv') or die "Couldn't open fix_url_issues.csv";
+my %name2iid = ();
+my %nid2iid = ();
+my %cid2iid = ();
+my %cid2post_nb = ();
+
+while (my $line = <$fix_url_issues_csv>)
+  {
+    chomp $line;
+    my ($nid, $name, $iid) = split ",", $line;
+    $name2iid{$name} = $iid;
+    $nid2iid{$nid} = $iid;
+  }
+close $fix_url_issues_csv;
+
+open(my $fix_url_comments_csv, '<', 'fix_url_comments.csv') or die "Couldn't open fix_url_comments.csv";
+while (my $line = <$fix_url_comments_csv>)
+  {
+    chomp $line;
+    my ($cid, $iid, $post_nb) = split ",", $line;
+    $cid2iid{$cid} = $iid;
+    $cid2post_nb{$cid} = $post_nb;
+  }
+close $fix_url_comments_csv;
+
+
+
+###### Definition of functions
+sub fix_issues_link {
+    my $text = $_[0];
+
+    # Find all links in text
+    my @links = ($text =~ m/:http:\/\/assos\.centrale-marseille\.fr(?:\/|\/lessive\/)(?:content\/t%C3%A2che|node)\/(.*)/g);
+
+    if ( @links )
+    {
+	foreach (@links)
+	{
+	    print $_ ."\n";
+	    $text =~ s/"http:\/\/assos\.centrale-marseille\.fr(\/|\/lessive\/)(content\/t%C3%A2che|node)\/.*":http:\/\/assos\.centrale-marseille\.fr(\/|\/lessive\/)(content\/t%C3%A2che|node)\/$_/#$cid2iid{$_}#note-$cid2post_nb{$_}/g;
+	}
+    }
+    return $text;
+}
+
+sub fix_comments_link {
+    my $text = $_[0];
+
+    # Find all links in text
+    my @links = $text =~ m/"http:\/\/assos\.centrale-marseille\.fr(\/|\/lessive\/)comment\/.*"/g;
+
+     # Foreach link, get comment cid.
+    my @cids = ();
+    if (@links)
+    {
+	foreach (@links)
+	{
+	    my $cid =~ s/"http:\/\/assos\.centrale-marseille\.fr(\/|\/lessive\/)comment\/(.*)#.*"/$1/g;
+	    push @cids, $cid;
+	}
+
+	foreach (@cids)
+	{
+	    $text =~ s/"http:\/\/assos\.centrale-marseille\.fr(\/|\/lessive\/)comment\/$_#$_":http:\/\/assos\.centrale-marseille\.fr\/comment\/$_#$_/#$name2iid{$_}/g;
+	}
+    }
+
+    return $text;
+}
+
+
+
+###### Update issues
+my $sql_update_issue =<<"SQL";
+UPDATE issues
+  SET author_id = ?, created_on = ?
+  WHERE  id = ?
+SQL
+
+my $sql_fix_links_issue =<<"SQL";
+UPDATE issues
+    SET description = ?
+    WHERE id = ?
+SQL
+
+my $req_update_issue = $dbh->prepare($sql_update_issue);
+my $req_fix_links_issue = $dbh->prepare($sql_fix_links_issue);
+
+# Reading file
+open(my $issues, '<issues.csv') or die "Couldn't open issues.txt\n";
+
+while (my $line = <$issues>)
+  {
+    # Update author and date of creation
+    chomp $line;
+
+    my ($iid, $author_id, $created_on, $nid, $name) = split ",", $line;
+
+    $req_update_issue->execute($author_id, $created_on, $iid);
+
+    ## Fix links
+    # issues
+    my $select_description = "SELECT description FROM issues WHERE id = 1569";
+
+    my ($description) = $dbh->selectrow_array($select_description);
+
+    $description = fix_issues_link($description);
+
+    die "stop";
+
+    print $description . "\n";
+#    print $links[0];
+#    print $name2iid{$links[0]};
+    # Comments
+#    $description = fix_comments_link($description);
+
+#    $req_fix_links_issue->execute($description, $iid);
+  }
+
+$req_update_issue->finish;
+$req_fix_links_issue->finish;
+
+
+###### Update journals
+my $sql_get_id =<<"SQL";
+SELECT id FROM journals WHERE journalized_id = ?
+ORDER BY created_on ASC
+SQL
+
+my $sql_update_journals =<<"SQL";
+UPDATE journals
+SET created_on = ?, user_id = ?
+WHERE id = ?
+SQL
+
+my $sql_updated_on =<<"SQL";
+UPDATE issues SET updated_on = ?
+WHERE id = ?
+SQL
+
+my $sql_fix_links_comment =<<"SQL";
+UPDATE journals
+    SET notes = ?
+    WHERE id = ?
+SQL
+
+my $req_get_id = $dbh->prepare($sql_get_id);
+my $req_update_journals = $dbh->prepare($sql_update_journals);
+my $req_updated_on = $dbh->prepare($sql_updated_on);
+my $req_fix_links_comment = $dbh->prepare($sql_fix_links_comment);
+
+# Reading file.
+open(my $comments, '<comments.csv') or die "Couldn't open comments.txt\n";
+
+# We must remember current iid in order to fetch result or execute query: req_get_id return
+# more than one value
+my $current_iid = -1;
+
+while (my $line = <$comments>)
+  {
+    chomp $line;
+
+    my ($iid, $user_id, $created_on) = split ",", $line;
+
+    # We get the id of the comment.
+    if ($current_iid != $iid)
+    {
+	$current_iid = $iid;
+	$req_get_id->execute($iid);
+    }
+
+    my ($id) = $req_get_id->fetchrow_array;
+
+    # We do the update.
+    $req_update_journals->execute($created_on, $user_id, $id);
+    $req_updated_on->execute($created_on, $iid);
+
+    ## Fix links.
+    # issues
+    my $select_notes = "SELECT notes FROM journals WHERE id = $id";
+    my ($notes) = $dbh->selectrow_array($select_notes);
+#    $notes = fix_issues_link($notes);
+    # Comments
+#    $notes = fix_comments_link($notes);
+
+#    $req_fix_links_comment->execute($notes, $id);
+  }
+
+$req_get_id->finish;
+$req_update_journals->finish;
+$req_updated_on->finish;
+
+####### Close the db connection.
+$dbh->disconnect;
diff --git a/scripts_divers/migrer_taches_vers_redmine/goto_redmine.py b/scripts_divers/migrer_taches_vers_redmine/goto_redmine.py
index 23514679..af7fdeee 100644
--- a/scripts_divers/migrer_taches_vers_redmine/goto_redmine.py
+++ b/scripts_divers/migrer_taches_vers_redmine/goto_redmine.py
@@ -1,168 +1,469 @@
-"""
-Pandoc est requis pour convertir le html en textile !
+#!/usr/bin/env python3
+
+"""This script can migrate issues from drupal to redmine.
+
+It is design be launched with the -i option: most variables you may need for
+debugging are easily accessible from interpreter (like HTTP status codes).
+We make some assertion based on HTTP status code. Here are the codet you may
+wish to know:
+- 200: OK
+- 201: Created
+- 404: Not found
+- 403: Forbidden
+- 500: Internal error
+Redmine gives you more intel in its response. Read them!
+Pandoc is required to convert html syntax to textile syntax
 """
 
-import url_parser  #permet de connaître les id des taches
-import urllib.request #permet de récupérer une page web
-import httplib2 #pour faire des requêtes http
+import requests
 import json
-import re #pour les expressions régulières
-import os #pour pouvoir faire appel à pandoc (commande system)
-
-######## NB : cid : comment id, nid : node id, urls : urls des tâches
-
-#Dictionnaire des gens dont on a la clé API
-#id: clé
-SUBMITERS = {'jenselme': '464c7c05b9bb53fb136092f1b9807ad91ec51321'}
-
-#les entêtes des requêtes POST et PUT
-Headers = {'content-type': 'application/json', 'X-Redmine-API-Key': ''}
-
-#là où on poste les tâches
-URL = 'https://forge.centrale-marseille.fr/issues'
-
-#là où sont les tâches
-LIST_TODO = 'http://localhost/portail/liste-tache'
-
-#url de base de l’emplacement du contenu
-BASE_URL = 'http://localhost/portail'
-PROJECT_ID = 30
-TRACKER_ID = 2
-
-
-########## dictionnaires de correspondance
-DONE_RATIO = {'En pause': 50, 'À commencer': 0, 'Entamée': 20, 'Bien avancée': 80, 'Terminée (success)': 100, 'Fermée (won\'t fix)': 100}
-PRIORITY = {'5 - Très basse': 3, '4 - Basse': 3, '3 - Moyenne': 4, '2 - Haute': 5, '1 - Très haute': 6,\
-        'Très basse': 3, 'Basse': 3, 'Moyenne': 4, 'Haute': 5, 'Très haute':6,\
-        '0': 3, '1': 3, '2': 4, '3': 5, '4': 6}
-STATUS = {'En cours': 2, 'Fermée': 5, 'Rejetée': 6, 'En pause': 7}
-#NB sur le portail, on a les équivalences suivantes
-#pour le champ version de drupal : 17 : drupal6, 18 : drupal7
-DRUPAL_VERSION = {'17': 2, '18': 1}
-
-def give_api_key(submiter):
-    "Donne la clé API de submiter ou celle de jenselme si c’est la seule"
-    if submiter in SUBMITERS:
-        return SUBMITERS[submiter]
-    else:
-        return  SUBMITERS['jenselme']
-
-
-def give_comments_ids(nid):
-    "permet de récupérer les id des commentaires de la tâche nid"
-    page = urllib.request.urlopen(BASE_URL + '/entity_json/node/' + nid).read()
-    page_json = json.loads(page.decode('utf-8'))
-    comments_json = page_json['comments']
-    #S’il n’y a pas de commentaire, comments_json est une liste vide et pas un dictionnaire
-    if comments_json:
-        comments = list(comments_json.keys())
-        comments.sort() #ce sont les clés d’un dictionnaire. Pas d’ordre à priori
-        return comments
-    else:
-        return list()
-
-
-def give_comments(cids):
-    "Donne la liste du texte des commentaires pour chaque cid in cids"
-    comments = list()
-    for cid in cids:
-        comment = urllib.request.urlopen(BASE_URL + '/comment/' + cid + '.json').read()
-        comments.append(json.loads(comment.decode('utf-8')))
-    return comments
-
-
-def format(txt):
-    "prend le texte en html et le renvoie en textile"
+import sys
+import datetime
+import os #we need system() to call pandoc
+import re
+
+import constantes as cst
+
+######## Global variables
+REGEXP_FIND_IMG = re.compile('!/.*!')
+REGEXP_NAME_IMG = re.compile('!.*/(.*)!')
+
+######## Common functions
+
+def handle_image(txt):
+    "Images are not posted automatically. There are only few of them.\
+    We just format the text with the correct textile syntax and when we post them,\
+    we will add comment and node id into a file. They should be attached to the\
+    correct issue afterwards"
+    has_image = False
+    # In textile, images are between !
+    images = REGEXP_FIND_IMG.findall(txt)
+    print(images)
+    if images:
+        has_image = True
+        for image in images:
+            img_name = REGEXP_NAME_IMG.sub(r'!\1!', image)
+            txt.replace(image, img_name)
+    return txt, has_image
+
+def html2textile(txt):
+    "Convert a txt from html to textile using pandoc"
+    # We remove line breaks and tabs, otherwise the conversion doesn't work properly
     txt.replace('\n', '')
     txt.replace('\t', '')
+    # pandoc can only manipulates files
     with open('tmp.html', 'w') as f:
         f.write(txt)
     os.system('pandoc -f html tmp.html -t textile -o tmp.textile')
     with open('tmp.textile', 'r') as f:
         txt = f.read()
-    return txt
-
-
-def give_redmine_status_id(tache):
-    drupal_status = ''
-    for elt in tache['field_avancement']:
-        if "Terminée" in elt:
-            drupal_status = 'Fermée'
-            break
-        elif "Fermée" in elt:
-            drupal_status = 'Rejetée'
-            break
-        elif "pause" in elt:
-            drupal_status = 'En pause'
-            del elt
-            break
-    if not drupal_status:
-        drupal_status = 'En cours'
-    return STATUS[drupal_status]
-
-
-def give_redmine_issue(tache):
-    issue = dict()
-    issue['project_id'] = PROJECT_ID
-    issue['tracker_id'] = TRACKER_ID
-    issue['subject'] = tache['title']
-    issue['description'] = format(tache['body']['value'])
-    #de temps en temps, le champ priorité est vide. On met 'Normale' dans ce cas
-    if tache['field_prioritaecute']:
-        issue['priority_id'] = PRIORITY[tache['field_prioritaecute']]
-    else:
-        issue['priority_id'] = PRIORITY['3 - Moyenne']
-    if tache['field_avancement']:
-        issue['done_ratio'] = DONE_RATIO[tache['field_avancement'][0]]
+    # Cleaning temporary files
+    os.remove('tmp.html')
+    os.remove('tmp.textile')
+    return handle_image(txt)
+
+def egalise(string, length):
+    "Make the length of string equals to length if shorter"
+    if len(string) < length:
+        return ' '*(length - len(string)) + string
     else:
-        issue['done_ratio'] = DONE_RATIO['À commencer']
-    issue['status_id'] = give_redmine_status_id(tache)
-    issue['fixed_version_id'] = DRUPAL_VERSION[tache['taxonomy_vocabulary_8']['id']]
-    return issue
-
-
-######### Main
-
-nids, urls = url_parser.give_json_urls(LIST_TODO, BASE_URL)
+        return string
+
+def percentage(integer):
+    "Converts integer into a string used to indicate the percentage of completion\
+    of a command"
+    string = str(integer)
+    string = egalise(string, 3)
+    string = 'Completion: ' + string + '%'
+    return string + '\b'*len(string)
 
-h = httplib2.Http()
+def print_progress(str):
+    sys.stdout.write(str)
+    sys.stdout.flush()
 
-for post_url in urls:
-    nid = nids[urls.index(post_url)]
-    print(nid)
-    tache_json = urllib.request.urlopen(post_url)
-    tache_drupal = json.loads(tache_json.read().decode('utf-8'))
+def format_date(timestamp):
+    str_timestamp = float(timestamp)
+    date = datetime.datetime.fromtimestamp(str_timestamp)
+    return date.strftime('%Y-%m-%d %H:%M:%S')
 
-    cids = give_comments_ids(nid)
-    comments_drupal = give_comments(cids)
-
-    issue = {}
-    issue['issue'] = give_redmine_issue(tache_drupal)
-    data = json.dumps(issue)
-
-    Headers['X-Redmine-API-Key'] = SUBMITERS['jenselme']
-
-    resp, content = h.request(URL + '.json', 'POST', body=data, headers=Headers)
-
-    #on récupère l’issue id pour savoir où poster les commentaires
-    iid = re.findall(r',"id":([0-9]*),', content.decode('utf-8'))[0]
-
-    #on a besoin de l’url à laquelle on met les commentaires, pour changer le status
-    put_url = URL + '/' + iid + '.json'
-    for index, comment in enumerate(comments_drupal):
-        submiter = comment['name']  #le premier est celui qui a soumis le node
-        Headers['X-Redmine-API-Key'] = give_api_key(submiter)
-        #si la personne n’a pas sa clé, on modifie le commentaire
-        comment_body = format(comment['comment_body']['value'])
-        if not submiter in SUBMITERS:
-            comment_body = "_{}_ a dit que :\n\n{}".format(submiter, comment_body)
-        update = {}
-        update['issue'] = {'notes': comment_body}
-        data = json.dumps(update)
-        h.request(put_url, 'PUT', body=data, headers=Headers)
 
-    #Les taches sont crées avec le status nouveau peu importe ce qu’il y a dans le json
-    #on modifie le status après coup
-    update_status = {'issue': {'status_id': issue['issue']['status_id']}}
-    data = json.dumps(update_status)
-    h.request(put_url, 'PUT', body=data, headers=Headers)
+
+######## Definition of classes
+
+
+class Comment:
+    """Represents a drupal comment
+    """
+
+    def __init__(self, cid, author, post_date, content, has_img):
+        self._cid = cid # comment id in drupal
+        self._author = author
+        self._post_date = post_date
+        self._content = content
+        # json representation, to be posted in redmine
+        self._update = {'issue': {'notes': self._content }}
+        self._update_json = json.dumps(self._update)
+        self._resp = None #will be used to store the put response
+        self._has_img = has_img
+
+    def post(self, url, headers, iid, post_nb):
+        "Post the comment to url with headers (required for authentication)"
+        self._resp = requests.put(url, headers=headers, data=self._update_json)
+        assert self._resp.status_code == 200
+
+        # We write iid,author_id,created_on in comments.csv
+        with open('comments.csv', 'a', encoding='utf8') as comments_csv:
+            comments_csv.write('{},{},{}\n'.\
+                        format(iid, cst.USER_ID[self._author], self._post_date))
+        with open('fix_url_comments.csv', 'a', encoding='utf8') as fix_url_csv:
+            fix_url_csv.write('{},{},{}\n'.format(self._cid, iid, post_nb))
+
+    @property
+    def post_date(self):
+        return self._post_date
+
+    @property
+    def resp(self):
+        return self._resp
+
+    @property
+    def cid(self):
+        return self._cid
+
+    @property
+    def has_img(self):
+        return self._has_img
+
+
+
+class Updates:
+    """Represents all the comments of a task
+    """
+
+    def __init__(self, comments):
+        self._comments = comments
+
+    def sort(self):
+        "Sort all the updates by date of creation"
+        sorted_date = False
+        while not sorted_date:
+            sorted_date = True
+            i = 0
+            while i < len(self._comments) - 1:
+                if self._comments[i].post_date > self._comments[i + 1].post_date:
+                    self._comments[i], self._comments[i + 1] = self._comments[i + 1],\
+                                                               self._comments[i]
+                    sorted_date = False
+                i += 1
+
+    def __getitem__(self, index):
+        return self._comments[index]
+
+    def __len__(self):
+        return len(self._comments)
+
+    def __iter__(self):
+        self.__i = -1
+        return self
+
+    def __next__(self):
+        self.__i += 1
+        if self.__i >= len(self._comments) or len(self._comments) == 0:
+            raise StopIteration
+        return self._comments[self.__i]
+
+
+
+class Issue:
+    """Represents a drupal issue
+    """
+
+    def __init__(self, nid, comments):
+        self._nid = nid #node id
+        self._iid = None #issue id, unknown until creation
+        self._resp = None #will be used to store the response of requests.post
+        self._comments = Updates(comments)
+        self._comments.sort()
+        self._issue = self.give_redmine_issue(nid) #the actual content, it's a dict
+
+    def give_redmine_status_id(self, node):
+        "Translate the drupal status field to an integer representing the\
+        redmine status id"
+        drupal_status = ''
+        for elt in node['field_avancement']:
+            if "Terminée" in elt:
+                drupal_status = 'Fermée'
+                break
+            elif "Fermée" in elt:
+                drupal_status = 'Rejetée'
+                break
+            elif "pause" in elt:
+                drupal_status = 'En pause'
+                del elt
+                break
+        if not drupal_status:
+            drupal_status = 'En cours'
+        return cst.STATUS[drupal_status]
+
+    def give_redmine_issue(self, nid):
+        "Uses the nid to find the node and converts its content to something\
+        redmine can understand. Read examples for more intels"
+        node_json = requests.get(cst.BASE_URL + '/node/{}.json'.format(nid)).text
+        node = json.loads(node_json)
+        issue = dict()
+        issue['project_id'] = cst.PROJECT_ID
+        issue['tracker_id'] = cst.TRACKER_ID
+        issue['subject'] = node['title']
+        issue['description'], self._has_img = html2textile(node['body']['value'])
+        # We get the name of the node
+        self._name = re.findall(cst.REGEXP_NAME, node['url'])[0]
+        # field_prioritaecute can be empty. We then assume it is normal
+        if node['field_prioritaecute']:
+            issue['priority_id'] = cst.PRIORITY[node['field_prioritaecute']]
+        else:
+            issue['priority_id'] = cst.PRIORITY['3 - Moyenne']
+        # field_avancement can be empty. We then assume it is to be started
+        if node['field_avancement']:
+            issue['done_ratio'] = cst.DONE_RATIO[node['field_avancement'][0]]
+        else:
+            issue['done_ratio'] = cst.DONE_RATIO['À commencer']
+        # Status id = open, fix, closed…
+        issue['status_id'] = self.give_redmine_status_id(node)
+        issue['fixed_version_id'] = cst.DRUPAL_VERSION[node['taxonomy_vocabulary_8']['id']]
+        issue['created'] = format_date(node['created'])
+        issue['author_id'] = node['author']['id']
+        # Do we have attached files?
+        if node['field_fichier']:
+            self._has_files = True
+        else:
+            self._has_files = False
+        return issue
+
+    def post(self, url, headers):
+        "Post the comment to url with headers (required for authentication)"
+        issue = {'issue': self._issue}
+        data = json.dumps(issue)
+
+        self._resp = requests.post(url, headers=headers, data=data)
+
+        assert self._resp.status_code == 201
+
+        resp_json = json.loads(self._resp.text)
+        self._iid = resp_json['issue']['id']
+
+        # We write iid,author_id,created_on in issues.csv
+        with open('issues.csv', 'a', encoding='utf8') as issues_csv:
+            author_id = self._issue['author_id']
+            redmine_author_id = cst.USER_ID[author_id]
+            created_on = self._issue['created']
+            issues_csv.write('{},{},{}\n'.\
+                             format(self._iid, redmine_author_id, created_on))
+        with open('fix_url_issues.csv', 'a') as fix_url:
+            fix_url.write('{},{},{}\n'.format(self._nid, self._name, self._iid))
+
+        # We post comments
+        nb_comments = len(self._comments)
+        i = 0
+        for comment in self._comments:
+            i += 1
+            put_url =  cst.URL_ISSUES + '/{}.json'.format(self._iid)
+            comment.post(put_url, headers, self._iid, i)
+            print_progress(percentage(i//nb_comments*100))
+
+        # We take care of images and files
+        self.handle_image()
+        self.handle_files()
+
+    def handle_files(self):
+        if self._has_files:
+            with open('has_files.txt', 'a', encoding='utf8') as has_files_file:
+                has_files_file.write('{}\n'.format(self._nid))
+
+    def handle_image(self):
+        for comment in self._comments:
+            self._has_img = comment.has_img or self._has_img
+        if self._has_img:
+            with open('has_img.txt', 'a', encoding='utf8') as has_img_file:
+                has_img_file.write('{}\n'.format(self._nid))
+
+    @property
+    def resp(self):
+        return self._resp
+
+    @property
+    def comments_resp(self):
+        resps = dict()
+        for comment in self._comments:
+            resps[comment.cid] = comment.resp
+        return resps
+
+
+
+class Laundry:
+    """Contains all issues and has methods to perform the migration
+
+    Iteration of this object traverses all issues.
+    You can access any issue with the container notation
+    """
+
+    def __init__(self, url, test=True):
+        self._issues = self.give_issues(url, test)
+
+    def give_issues(self, url, test):
+        "Returns a list of all issues"
+        issues = []
+        r = requests.get(url)
+
+        assert r.status_code == 200
+
+        # 1st element is 'Nid', and last is ''
+        nids = r.text.split('\r\n')[1:-1]
+        # for test, we only use 3 issues (faster)
+        if test:
+            nids = nids[:3]
+        self.nb_task = len(nids)
+        i = 0
+        for nid in nids:
+            i += 1
+            comments = self.give_comments(nid)
+            print('Fetching issue no {} on {}'.format(i, self.nb_task))
+            issues.append(Issue(nid, comments))
+        return issues
+
+    def give_comments(self, nid):
+        "Returns the list of all comments of a node"
+        cids, comments_json = self.give_comments_json(nid)
+        comments = []
+        i = 0
+        nb_comments = len(comments_json)
+        for comment_json in comments_json:
+            author = comment_json['name']
+            post_date = format_date(comment_json['created'])
+            content, has_img = html2textile(comment_json['comment_body']['value'])
+            comments.append(Comment(cids[comments_json.index(comment_json)], author, post_date, content, has_img))
+            i += 1
+            print_progress(percentage(i//nb_comments*100))
+        return comments
+
+    def give_comments_json(self, nid):
+        "Get the raw json version of the drupal comment"
+        cids = self.give_comments_ids(nid)
+        comments = list()
+        for cid in cids:
+            comment = requests.get(cst.BASE_URL + '/comment/' + cid + '.json')
+            comments.append(json.loads(comment.text))
+        return cids, comments
+
+    def give_comments_ids(self, nid):
+        "Get the cid (comnments id) for a node"
+        headers = cst.Headers_GET
+        headers['X-Redmine-API-Key'] = cst.SUBMITERS[cst.MANAGER]
+        r = requests.get(cst.BASE_URL + '/entity_json/node/{}'.format(nid), headers=headers)
+        page_json = json.loads(r.text)
+        comments_json = page_json['comments']
+        #If the issue has no comment, comments_json is a list, not a dict
+        if comments_json:
+            comments = list(comments_json.keys())
+            return comments
+        else:
+            return list()
+
+    def __iter__(self):
+        self.__i = -1
+        return self
+
+    def __next__(self):
+        self.__i += 1
+        if self.__i >= len(self._issues):
+            raise StopIteration
+        return self._issues[self.__i]
+
+    def __getitem__(self, index):
+        return self._issues[index]
+
+    def __len__(self):
+        return len(self._issues)
+
+
+
+class Redmine:
+    """Main class.
+
+    Allows the interaction with the program
+    You can access any issue with the container notation.
+    """
+    def __init__(self, test=True):
+        self.reset()
+        self._test = test
+
+    def reset(self):
+        "Go to initial stage. All attributes are set to None"
+        self._laundry = None
+        self._headers = None
+        self._headers_get = None
+
+    def init(self, issues_file='issues.csv', x_redmine_api_key=cst.SUBMITERS[cst.MANAGER]):
+        "Initialize the attribute for post uses"
+        self._headers = cst.Headers
+        self._headers['X-Redmine-API-Key'] = x_redmine_api_key
+        self._laundry = Laundry(cst.LIST_TODO_CSV, self._test)
+
+    def post(self, post_url=cst.URL_ISSUES_JSON):
+        "Post all issues"
+        nb_issues = len(self._laundry)
+        i = 0
+        for issue in self._laundry:
+            i += 1
+            print('Posting issues {} on {}'.format(i, nb_issues))
+            issue.post(post_url, self._headers)
+
+    def sweep(self):
+        "Clean the redmine project of all issues."
+        print('You are about to delete all issues on your redmine project.')
+        ok = input('Do you wish to continue? (yes/no): ')
+
+        if ok == 'yes':
+            # Get the right headers
+            self._headers_get = cst.Headers_GET
+            self._headers_get['X-Redmine-API-Key'] = cst.SUBMITERS[cst.MANAGER]
+            # Redmine give at maximum 100 issues. We may need to do it many times
+            pass_number = 1
+            while True:
+                r = requests.get(cst.URL_ISSUES_JSON + '?status_id=*&limit=100',\
+                                 headers=cst.Headers_GET)
+
+                assert r.status_code == 200
+
+                if not json.loads(r.text)['issues']: # There are no more issues to sweep
+                    break
+                print('Pass {}'.format(pass_number))
+                taches_json = json.loads(r.text)['issues']
+                # Print a nice completion percentage
+                sys.stdout.flush()
+                compt = 0
+                print_progress(percentage(compt//len(taches_json)*100))
+                for tache in taches_json:
+                    tid = tache['id']
+                    r = requests.delete(cst.URL_REDMINE + '/issues/{}.json'.format(tid),\
+                                        headers=cst.Headers_GET)
+                    compt += 1
+                    print_progress(percentage(int(compt/len(taches_json)*100)))
+                sys.stdout.write("\n")
+                pass_number += 1
+        else:
+            print('Wise decision')
+
+    def __getitem__(self, index):
+        if self._laundry:
+            return self._laundry[index]
+        else:
+            raise IndexError('Index out of range')
+
+
+
+
+######## Main program
+if __name__ == "__main__":
+    redmine = Redmine(test=False)
+    redmine.init()
+    redmine.post()
diff --git a/scripts_divers/migrer_taches_vers_redmine/output_post.html b/scripts_divers/migrer_taches_vers_redmine/output_post.html
new file mode 100644
index 00000000..1a03eb41
--- /dev/null
+++ b/scripts_divers/migrer_taches_vers_redmine/output_post.html
@@ -0,0 +1,30 @@
+* About to connect() to localhost port 80 (#0)
+*   Trying ::1...
+* connected
+* Connected to localhost (::1) port 80 (#0)
+> POST /redmine/issues.json HTTP/1.1
+> User-Agent: curl/7.26.0
+> Host: localhost
+> Accept: */*
+> Content-Type: application/json
+> X-Redmine-API-Key: a7630a1244be353424cc0f56a49657c3fa9dbcc6
+> Content-Length: 255
+> 
+* upload completely sent off: 255 out of 255 bytes
+* additional stuff not fine transfer.c:1037: 0 0
+* HTTP 1.1 or later with persistent connection, pipelining supported
+< HTTP/1.1 201 Created
+< Date: Tue, 02 Jul 2013 10:18:03 GMT
+< Server: Apache/2.2.22 (Debian)
+< X-Powered-By: Phusion Passenger (mod_rails/mod_rack) 3.0.13
+< Cache-Control: no-cache
+< X-Runtime: 173
+< Set-Cookie: autologin=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT
+< Set-Cookie: _redmine_default=BAh7BjoPc2Vzc2lvbl9pZCIlOWMyMDE3ZjUyOWMwYzFhNzkzODk3NzkwMzcxMDI0Njg%3D--53d46d1fb4e9684381a35ad0951b669869dde966; path=/redmine; HttpOnly
+< Location: http://localhost/redmine/issues/7
+< Content-Length: 433
+< Status: 201
+< Content-Type: application/json; charset=utf-8
+< 
+* Connection #0 to host localhost left intact
+{"issue":{"status":{"name":"New","id":1},"done_ratio":100,"subject":"tache 8","updated_on":"2013/07/02 11:18:03 +0100","tracker":{"name":"Feature","id":2},"spent_hours":0.0,"description":"J'\u00e9cris plein de chose\r\nPour ma premi\u00e8re tache","start_date":"2013/07/02","author":{"name":"Redmine Admin","id":1},"created_on":"2013/07/02 11:18:03 +0100","project":{"name":"test","id":1},"id":7,"priority":{"name":"Urgent","id":6}}}* Closing connection #0
diff --git a/scripts_divers/migrer_taches_vers_redmine/output_put.html b/scripts_divers/migrer_taches_vers_redmine/output_put.html
new file mode 100644
index 00000000..3909c506
--- /dev/null
+++ b/scripts_divers/migrer_taches_vers_redmine/output_put.html
@@ -0,0 +1,30 @@
+* About to connect() to localhost port 80 (#0)
+*   Trying ::1...
+* connected
+* Connected to localhost (::1) port 80 (#0)
+> PUT /redmine/issues/6.json HTTP/1.1
+> User-Agent: curl/7.26.0
+> Host: localhost
+> Accept: */*
+> Content-Type: application/json
+> X-Redmine-API-Key: a7630a1244be353424cc0f56a49657c3fa9dbcc6
+> Content-Length: 52
+> 
+* upload completely sent off: 52 out of 52 bytes
+* additional stuff not fine transfer.c:1037: 0 0
+* HTTP 1.1 or later with persistent connection, pipelining supported
+< HTTP/1.1 200 OK
+< Date: Tue, 02 Jul 2013 10:15:18 GMT
+< Server: Apache/2.2.22 (Debian)
+< X-Powered-By: Phusion Passenger (mod_rails/mod_rack) 3.0.13
+< Cache-Control: no-cache
+< X-Runtime: 133
+< Set-Cookie: autologin=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT
+< Set-Cookie: _redmine_default=BAh7BzoPc2Vzc2lvbl9pZCIlOTFmNDkxMzg3MTZmNDUzYzk3NDc4NTliNjYzZmZiZDIiCmZsYXNoSUM6J0FjdGlvbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhhc2h7BjoLbm90aWNlIipNaXNlIMOgIGpvdXIgZWZmZWN0dcOpZSBhdmVjIHN1Y2PDqHMuBjoKQHVzZWR7BjsHRg%3D%3D--252700c9688855c080f30d577da1cb9e60ebc53b; path=/redmine; HttpOnly
+< Content-Length: 1
+< Status: 200
+< Content-Type: application/json; charset=utf-8
+< 
+* Connection #0 to host localhost left intact
+ * Closing connection #0
+   
diff --git a/scripts_divers/migrer_taches_vers_redmine/purge_issues_redmine.py b/scripts_divers/migrer_taches_vers_redmine/purge_issues_redmine.py
deleted file mode 100644
index 0a19c71a..00000000
--- a/scripts_divers/migrer_taches_vers_redmine/purge_issues_redmine.py
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/usr/env python3
-
-'''Ce script supprime TOUTES les tâches redmines du projet indiqué. À utiliser avec prudence.
-'''
-
-import requests #pour faire des requêtes http
-import json
-import sys
-
-import constantes as const
-
-print('Ce script supprime TOUTES les tâches redmines du projet indiqué. À utiliser avec prudence.')
-ok = input('Continuer ? (oui/non) : ')
-
-if ok == 'oui':
-    const.Headers_GET['X-Redmine-API-Key'] = const.SUBMITERS[const.MANAGER]
-    r = requests.get(const.URL_ISSUES_JSON + '?status_id=*&limit=100', headers=const.Headers_GET)
-
-    taches_json = json.loads(r.text)['issues']
-
-    sys.stdout.write('Pourcentage de complétion : 00%\b\b\b')
-    sys.stdout.flush()
-    compt = 0
-    for tache in taches_json:
-        tid = tache['id']
-        r = requests.delete(const.URL_REDMINE + '/issues/{}.json'.format(tid), headers=const.Headers_GET)
-
-        #on affiche le pourcentage de complétion
-        compt += 1
-        pourcentage = compt/len(taches_json)*100
-        sys.stdout.write(str(pourcentage) + '%\b\b\b')
-        sys.stdout.flush()
-else:
-    print('Sage décision')
diff --git a/scripts_divers/migrer_taches_vers_redmine/url_parser.py b/scripts_divers/migrer_taches_vers_redmine/url_parser.py
deleted file mode 100644
index 2a44f7b9..00000000
--- a/scripts_divers/migrer_taches_vers_redmine/url_parser.py
+++ /dev/null
@@ -1,50 +0,0 @@
-#!/usr/share/env python3
-
-from html.parser import HTMLParser
-import httplib2
-
-
-class LinksParser(HTMLParser):
-    "Classe permettant de parser du html"
-    def __init__(self):
-        HTMLParser.__init__(self)
-        self.recording = 0
-        self.data = []
-
-    def handle_starttag(self, tag, attributes):
-        if tag != 'span':
-            return
-        if self.recording:
-            self.recording += 1
-            return
-        for name, value in attributes:
-            if name == 'class' and value == 'parse_me':
-                break
-        else:
-            return
-        self.recording = 1
-
-    def handle_endtag(self, tag):
-        if tag == 'span' and self.recording:
-            self.recording -= 1
-
-    def handle_data(self, data):
-        if self.recording:
-            self.data.append(data)
-
-def give_nids(url):
-    p = LinksParser()
-    h = httplib2.Http()
-
-    resp, content = h.request(url, 'GET')
-    text = content.decode('utf-8')
-
-    p.feed(text)
-    return p.data
-
-def give_json_urls(url, base_url):
-    nids = give_nids(url)
-    tache_urls = []
-    for nid in nids:
-        tache_urls.append(base_url + '/node/' + nid + '.json')
-    return nids, tache_urls #on a besoin des nids pour après.
\ No newline at end of file
diff --git a/scripts_divers/rm-prefix.pl b/scripts_divers/rm-prefix.pl
new file mode 100644
index 00000000..00931287
--- /dev/null
+++ b/scripts_divers/rm-prefix.pl
@@ -0,0 +1,35 @@
+### Ce script a été écrit afin de supprimer les préfixes des tables des bases données.
+### Ces préfixes étaient indispensables quand tous les sites étaient dans une même base de données.
+
+use strict;
+use warnings;
+use DBI;
+
+my $bd		= $ARGV[0];
+my $serveur	= 'localhost'; # Il est possible de mettre une adresse IP
+my $identifiant = 'root';      # identifiant
+my $motdepasse	= 'tata';
+my $port	= '';
+
+my ($prefix) = $ARGV[1];
+
+my $dbh = DBI->connect( "DBI:mysql:database=$bd;host=$serveur;port=$port", $identifiant, $motdepasse, { RaiseError => 1, } ) or die "Connection impossible à la base de données $bd !\n $! \n $@\n$DBI::errstr";
+
+my @table_names = $dbh->tables;
+foreach my $table(@table_names)
+{
+    #print $table =~ m/$prefix/;
+    #print $table . "\n";
+
+    if( $table =~ m/^`$bd`\.`$prefix/ )
+    {
+	my ($new_name) = ( $table =~ m/$prefix(.*)`/ );
+	my $new_table = "`$bd`.`$new_name`";
+	print $new_name . "\n";
+	print $new_table . "\n";
+	$dbh->do('DROP TABLE IF EXISTS ' . $new_name);
+	$dbh->do('RENAME TABLE ' . $table . ' TO ' . $new_name) or die 'Ne peut exécuter la requête';
+    }
+}
+
+$dbh->disconnect();
