Ecrasement lors d'une édition simultanée
Lorsque deux auteurs éditent un même extrait "en même temps", le dernier qui enregistre écrasera le texte de l'autre.

Or, vu l'issue #171 nous n'avons aucun outil de gestion de conflit. Et de toute façon, la conception actuelle de l'archi ne nous permet pas de remonter les conflits (puisque pas de branches).

L'idée la plus simple est de mettre un lock / message préventif pour que les contributeurs soient prévenus de la présence d'un autre membre sur le même extrait.

Le problème est encore plus important sur les articles puisque ça revient à être un simple extrait, donc le dernier qui save dégage toute modif qui aurait pu avoir lieu entre le début et la fin de son édition (temps de rédaction donc).

diff --git a/templates/tutorial/chapter/edit.html b/templates/tutorial/chapter/edit.html
index c18be0a24..3db72a8c7 100644
--- a/templates/tutorial/chapter/edit.html
+++ b/templates/tutorial/chapter/edit.html
@@ -40,5 +40,10 @@
 
 
 {% block content %}
+    {% if new_version %}
+        <p class="ico-after warning">
+            Une nouvelle version a été postée avant que vous ne validiez.
+        </p>
+    {% endif %}
     {% crispy form %}
 {% endblock %}
\ No newline at end of file
diff --git a/templates/tutorial/extract/edit.html b/templates/tutorial/extract/edit.html
index 3969f9c6e..45341465d 100644
--- a/templates/tutorial/extract/edit.html
+++ b/templates/tutorial/extract/edit.html
@@ -29,6 +29,11 @@
 
 
 {% block content %}
+    {% if new_version %}
+        <p class="ico-after warning">
+            Une nouvelle version a été postée avant que vous ne validiez.
+        </p>
+    {% endif %}
     {% crispy form %}
 
     {% if form.text.value %}
diff --git a/templates/tutorial/part/edit.html b/templates/tutorial/part/edit.html
index 421a8f826..ef9eff34a 100644
--- a/templates/tutorial/part/edit.html
+++ b/templates/tutorial/part/edit.html
@@ -35,5 +35,10 @@
 
 
 {% block content %}
+    {% if new_version %}
+        <p class="ico-after warning">
+            Une nouvelle version a été postée avant que vous ne validiez.
+        </p>
+    {% endif %}
    {% crispy form %}
 {% endblock %}
\ No newline at end of file
diff --git a/templates/tutorial/tutorial/edit.html b/templates/tutorial/tutorial/edit.html
index c14defe5f..6f3cdb358 100644
--- a/templates/tutorial/tutorial/edit.html
+++ b/templates/tutorial/tutorial/edit.html
@@ -21,11 +21,8 @@
 
 
 {% block headline_sub %}
-    {{ tutorial.description }}
+   {{ tutorial.description }}
 {% endblock %}
-
-
-
 {% block breadcrumb %}
     <li><a href="{{ tutorial.get_absolute_url }}">{{ tutorial.title }}</a></li>
     <li>Éditer le tutoriel</li>
@@ -34,5 +31,10 @@
 
 
 {% block content %}
+    {% if new_version %}
+        <p class="ico-after warning">
+            Une nouvelle version a été postée avant que vous ne validiez.
+        </p>
+    {% endif %}
     {% crispy form %}
 {% endblock %}
\ No newline at end of file
diff --git a/zds/tutorial/forms.py b/zds/tutorial/forms.py
index b5a7ea2ff..add0efbae 100644
--- a/zds/tutorial/forms.py
+++ b/zds/tutorial/forms.py
@@ -108,6 +108,7 @@ class TutorialForm(FormWithTitle):
             Field('image'),
             Field('introduction', css_class='md-editor'),
             Field('conclusion', css_class='md-editor'),
+            Hidden('last_hash', '{{ last_hash }}'),
             Field('subcategory'),
             Field('licence'),
             ButtonHolder(
@@ -153,6 +154,7 @@ class PartForm(FormWithTitle):
             Field('title'),
             Field('introduction', css_class='md-editor'),
             Field('conclusion', css_class='md-editor'),
+            Hidden('last_hash', '{{ last_hash }}'),
             ButtonHolder(
                 StrictButton(
                     'Valider',
@@ -200,6 +202,7 @@ class ChapterForm(FormWithTitle):
             Field('image'),
             Field('introduction', css_class='md-editor'),
             Field('conclusion', css_class='md-editor'),
+            Hidden('last_hash', '{{ last_hash }}'),
             ButtonHolder(
                 StrictButton(
                     'Valider',
@@ -236,7 +239,8 @@ class EmbdedChapterForm(forms.Form):
                 u'Contenu',
                 Field('image'),
                 Field('introduction', css_class='md-editor'),
-                Field('conclusion', css_class='md-editor')
+                Field('conclusion', css_class='md-editor'),
+                Hidden('last_hash', '{{ last_hash }}'),
             ),
             ButtonHolder(
                 Submit('submit', 'Valider')
@@ -265,6 +269,7 @@ class ExtractForm(FormWithTitle):
 
         self.helper.layout = Layout(
             Field('title'),
+            Hidden('last_hash', '{{ last_hash }}'),
             CommonLayoutEditor()
         )
 
@@ -387,8 +392,9 @@ class AskValidationForm(forms.Form):
             StrictButton(
                 'Confirmer',
                 type='submit'),
-            Hidden('tutorial', '{{ tutorial.pk }}'), 
-            Hidden('version', '{{ version }}'), )
+            Hidden(
+                'tutorial', '{{ tutorial.pk }}'), Hidden(
+                'version', '{{ version }}'), )
 
 
 class ValidForm(forms.Form):
diff --git a/zds/tutorial/tests.py b/zds/tutorial/tests.py
index 442a91598..09181b375 100644
--- a/zds/tutorial/tests.py
+++ b/zds/tutorial/tests.py
@@ -18,7 +18,7 @@ from zds.tutorial.factories import BigTutorialFactory, MiniTutorialFactory, Part
 from zds.gallery.factories import GalleryFactory
 from zds.tutorial.models import Note, Tutorial, Validation, Extract, Part, Chapter
 from zds.utils.models import SubCategory, Licence, Alert
-
+from zds.utils.misc import compute_hash
 
 @override_settings(MEDIA_ROOT=os.path.join(SITE_ROOT, 'media-test'))
 @override_settings(REPO_PATH=os.path.join(SITE_ROOT, 'tutoriels-private-test'))
@@ -540,7 +540,7 @@ class BigTutorialTests(TestCase):
         self.assertEqual(result.status_code, 302)
         self.assertEqual(Part.objects.filter(tutorial=tuto).count(), 2)
         p2 = Part.objects.filter(tutorial=tuto).last()
-        
+        self.assertEqual(u"Analyse", p2.get_introduction())
         #check view offline
         result = self.client.get(
             reverse(
@@ -706,6 +706,8 @@ class BigTutorialTests(TestCase):
                 'title': u"Partie 2 : edition de titre",
                 'introduction': u"Expérimentation : edition d'introduction",
                 'conclusion': u"C'est terminé : edition de conlusion",
+                "last_hash": compute_hash([os.path.join(p2.tutorial.get_path(), p2.introduction),
+                    os.path.join(p2.tutorial.get_path(), p2.conclusion)])
             },
             follow=True)
         self.assertContains(response=result, text = u"Partie 2 : edition de titre")
@@ -720,13 +722,15 @@ class BigTutorialTests(TestCase):
                 'title': u"Chapitre 3 : edition de titre",
                 'introduction': u"Edition d'introduction",
                 'conclusion': u"Edition de conlusion",
+                "last_hash": compute_hash([os.path.join(c3.get_path(),"introduction.md"),
+				    os.path.join(c3.get_path(),"conclusion.md")])
             },
             follow=True)
         self.assertContains(response=result, text = u"Chapitre 3 : edition de titre")
         self.assertContains(response=result, text = u"Edition d'introduction")
         self.assertContains(response=result, text = u"Edition de conlusion")
         self.assertEqual(Chapter.objects.filter(part=p2.pk).count(), 3)
-        
+        p2 = Part.objects.filter(pk=p2.pk).first()
         #edit part 2
         result = self.client.post(
             reverse('zds.tutorial.views.edit_part') + '?partie={}'.format(p2.pk),
@@ -734,6 +738,8 @@ class BigTutorialTests(TestCase):
                 'title': u"Partie 2 : seconde edition de titre",
                 'introduction': u"Expérimentation : seconde edition d'introduction",
                 'conclusion': u"C'est terminé : seconde edition de conlusion",
+                "last_hash": compute_hash([os.path.join(p2.tutorial.get_path(), p2.introduction),
+                    os.path.join(p2.tutorial.get_path(), p2.conclusion)])
             },
             follow=True)
         self.assertContains(response=result, text = u"Partie 2 : seconde edition de titre")
@@ -748,6 +754,8 @@ class BigTutorialTests(TestCase):
                 'title': u"Chapitre 2 : edition de titre",
                 'introduction': u"Edition d'introduction",
                 'conclusion': u"Edition de conlusion",
+                "last_hash": compute_hash([os.path.join(c2.get_path(),"introduction.md"),
+				    os.path.join(c2.get_path(),"conclusion.md")])
             },
             follow=True)
         self.assertContains(response=result, text = u"Chapitre 2 : edition de titre")
@@ -792,52 +800,111 @@ class BigTutorialTests(TestCase):
         self.assertEqual(Chapter.objects.filter(part__tutorial=tuto.pk).count(), 2)
         self.assertEqual(Part.objects.filter(tutorial=tuto.pk).count(), 2)
 
-    def test_available_tuto(self):
-        """ Test that all page of big tutorial is available"""
-        parts = self.bigtuto.get_parts()
-        for part in parts:
-            result = self.client.get(reverse(
-                                        'zds.tutorial.views.view_part_online',
-                                        args=[
-                                            self.bigtuto.pk,
-                                            self.bigtuto.slug,
-                                            part.pk,
-                                            part.slug]),
-                                    follow=True)
-            self.assertEqual(result.status_code, 200)
-            result = self.client.get(reverse(
-                                        'zds.tutorial.views.view_part',
-                                        args=[
-                                            self.bigtuto.pk,
-                                            self.bigtuto.slug,
-                                            part.pk,
-                                            part.slug]),
-                                    follow=True)
-            self.assertEqual(result.status_code, 200)
-            chapters = part.get_chapters()
-            for chapter in chapters:
-                result = self.client.get(reverse(
-                                            'zds.tutorial.views.view_chapter_online',
-                                            args=[
-                                                self.bigtuto.pk,
-                                                self.bigtuto.slug,
-                                                part.pk,
-                                                part.slug,
-                                                chapter.pk,
-                                                chapter.slug]),
-                                        follow=True)
-                self.assertEqual(result.status_code, 200)
-                result = self.client.get(reverse(
-                                            'zds.tutorial.views.view_chapter',
-                                            args=[
-                                                self.bigtuto.pk,
-                                                self.bigtuto.slug,
-                                                part.pk,
-                                                part.slug,
-                                                chapter.pk,
-                                                chapter.slug]),
-                                        follow=True)
-                self.assertEqual(result.status_code, 200)
+    def test_conflict_does_not_destroy(self):
+        """tests that simultaneous edition does not conflict"""
+        sub = SubCategory()
+        sub.title = "toto"
+        sub.save()
+       	# logout before
+        self.client.logout()
+        # first, login with author :
+        self.assertEqual(
+            self.client.login(
+                username=self.user_author.username,
+                password='hostel77'),
+            True)
+        # test tuto
+        (introduction_path, conclusion_path) =(os.path.join(self.bigtuto.get_path(),"introduction.md"), os.path.join(self.bigtuto.get_path(),"conclusion.md"))
+        hash = compute_hash([introduction_path, conclusion_path])
+        self.client.post(
+            reverse('zds.tutorial.views.edit_tutorial')+'?tutoriel={0}'.format(self.bigtuto.pk),
+            {
+                'title': self.bigtuto.title,
+                'description': "nouvelle description",
+                'subcategory': [sub.pk],
+                'introduction': self.bigtuto.get_introduction() +" un essai",
+                'conclusion': self.bigtuto.get_conclusion(),
+                'last_hash': hash 
+            }, follow= True)
+        conflict_result = self.client.post(
+            reverse('zds.tutorial.views.edit_tutorial')+'?tutoriel={0}'.format(self.bigtuto.pk),
+            {
+                'title': self.bigtuto.title,
+                'description': "nouvelle description",
+                'subcategory': [sub.pk],
+                'introduction': self.bigtuto.get_introduction() +" conflictual",
+                'conclusion': self.bigtuto.get_conclusion(),
+                'last_hash': hash 
+            }, follow= False)
+        self.assertEqual(conflict_result.status_code, 200)
+        self.assertContains(response=conflict_result, text = u"nouvelle version")
+
+        # test parts
+
+        result = self.client.post(
+            reverse('zds.tutorial.views.add_part') + '?tutoriel={}'.format(self.bigtuto.pk),
+            {
+                'title': u"Partie 2",
+                'introduction': u"Analyse",
+                'conclusion': u"Fin de l'analyse",
+            },
+            follow=False)
+        p1 = Part.objects.last()
+        hash = compute_hash([os.path.join(p1.tutorial.get_path(), p1.introduction),
+                    os.path.join(p1.tutorial.get_path(), p1.conclusion)])        
+        self.client.post(
+            reverse('zds.tutorial.views.edit_part') + '?partie={}'.format(p1.pk),
+            {
+                'title': u"Partie 2 : edition de titre",
+                'introduction': u"Expérimentation : edition d'introduction",
+                'conclusion': u"C'est terminé : edition de conlusion",
+                "last_hash": hash
+            },
+            follow=False)
+        conflict_result = self.client.post(
+            reverse('zds.tutorial.views.edit_part') + '?partie={}'.format(p1.pk),
+            {
+                'title': u"Partie 2 : edition de titre",
+                'introduction': u"Expérimentation : edition d'introduction conflit",
+                'conclusion': u"C'est terminé : edition de conlusion",
+                "last_hash": hash
+            },
+            follow=False)
+        self.assertEqual(conflict_result.status_code, 200)
+        self.assertContains(response=conflict_result, text = u"nouvelle version")
+
+        # test chapter
+        result = self.client.post(
+            reverse('zds.tutorial.views.add_chapter') + '?partie={}'.format(p1.pk),
+            {
+                'title': u"Chapitre 1",
+                'introduction':"Mon premier chapitre",
+                'conclusion': "Fin de mon premier chapitre",
+            },
+            follow=False)
+        c1 = Chapter.objects.last()
+        hash = compute_hash([os.path.join(c1.get_path(),"introduction.md"),
+		    os.path.join(c1.get_path(),"conclusion.md")])
+        self.client.post(
+            reverse('zds.tutorial.views.edit_chapter') + '?chapitre={}'.format(c1.pk),
+            {
+                'title': u"Chapitre 3 : edition de titre",
+                'introduction': u"Edition d'introduction",
+                'conclusion': u"Edition de conlusion",
+                "last_hash": hash
+            },
+            follow=True)
+        conflict_result = self.client.post(
+            reverse('zds.tutorial.views.edit_chapter') + '?chapitre={}'.format(c1.pk),
+            {
+                'title': u"Chapitre 3 : edition de titre",
+                'introduction': u"Edition d'introduction conflict",
+                'conclusion': u"Edition de conlusion",
+                "last_hash": hash
+            },
+            follow=True)
+        self.assertEqual(conflict_result.status_code, 200)
+        self.assertContains(response=conflict_result, text = u"nouvelle version")
 
     def test_url_for_member(self):
         """Test simple get request by simple member."""
@@ -1378,6 +1445,8 @@ class BigTutorialTests(TestCase):
                 'introduction': self.bigtuto.introduction,
                 'description': self.bigtuto.description,
                 'conclusion': self.bigtuto.conclusion,
+                'last_hash': compute_hash([os.path.join(self.bigtuto.get_path(),"introduction.md"),
+					    os.path.join(self.bigtuto.get_path(),"conclusion.md")])
             },
             follow=True)
 
@@ -2069,6 +2138,8 @@ class MiniTutorialTests(TestCase):
                     'subcategory': [sub.pk],
                     'introduction': self.minituto.get_introduction(),
                     'conclusion': self.minituto.get_conclusion(),
+                    'last_hash': compute_hash([os.path.join(self.minituto.get_path(),"introduction.md"),
+					    os.path.join(self.minituto.get_path(),"conclusion.md")])
                 },
                 follow=False
         )
@@ -2086,6 +2157,8 @@ class MiniTutorialTests(TestCase):
                     'subcategory': [sub.pk],
                     'introduction': self.minituto.get_introduction(),
                     'conclusion': self.minituto.get_conclusion(),
+                    'last_hash': compute_hash([os.path.join(self.minituto.get_path(),"introduction.md"),
+					    os.path.join(self.minituto.get_path(),"conclusion.md")])
                 },
                 follow=False
         )
@@ -2411,6 +2484,7 @@ class MiniTutorialTests(TestCase):
             {
                 'title': u"Extrait 2 : edition de titre",
                 'text': u"Edition d'introduction",
+                "last_hash": compute_hash([e2.get_path()])
             },
             follow=True)
         self.assertEqual(result.status_code, 200)
diff --git a/zds/tutorial/views.py b/zds/tutorial/views.py
index ed7ca774b..b59f3f57b 100644
--- a/zds/tutorial/views.py
+++ b/zds/tutorial/views.py
@@ -54,8 +54,18 @@ from zds.utils.mps import send_mp
 from zds.utils.paginator import paginator_range
 from zds.utils.templatetags.emarkdown import emarkdown
 from zds.utils.tutorials import get_blob, export_tutorial_to_md, move
+from zds.utils.misc import compute_hash, content_has_changed
 
+def render_chapter_form(chapter):
+    if chapter.part:
+        return ChapterForm({"title": chapter.title,
+                            "introduction": chapter.get_introduction(),
+                            "conclusion": chapter.get_conclusion()})
+    else:
 
+        return \
+            EmbdedChapterForm({"introduction": chapter.get_introduction(),
+                               "conclusion": chapter.get_conclusion()})
 
 def index(request):
     """Display all public tutorials of the website."""
@@ -704,11 +714,10 @@ def view_tutorial(request, tutorial_pk, tutorial_slug):
                                            version=sha)\
                                     .order_by("-date_proposition")\
                                     .first()
+    formAskValidation = AskValidationForm()
     if tutorial.source:
-        formAskValidation = AskValidationForm(initial={"source": tutorial.source})
         formValid = ValidForm(initial={"source": tutorial.source})
     else:
-        formAskValidation = AskValidationForm()
         formValid = ValidForm()
     formReject = RejectForm()
     return render_template("tutorial/tutorial/view.html", {
@@ -968,10 +977,29 @@ def edit_tutorial(request):
     if request.user not in tutorial.authors.all():
         if not request.user.has_perm("tutorial.change_tutorial"):
             raise PermissionDenied
+    introduction = os.path.join(tutorial.get_path(), "introduction.md")
+    conclusion = os.path.join(tutorial.get_path(), "conclusion.md")
     if request.method == "POST":
         form = TutorialForm(request.POST, request.FILES)
         if form.is_valid():
             data = form.data
+            if content_has_changed([introduction, conclusion], data["last_hash"]):
+                form = TutorialForm(initial={
+                    "title": tutorial.title,
+                    "type": tutorial.type,
+                    "licence": tutorial.licence,
+                    "description": tutorial.description,
+                    "subcategory": tutorial.subcategory.all(),
+                    "introduction": tutorial.get_introduction(),
+                    "conclusion": tutorial.get_conclusion(),
+
+                })
+                return render_template("tutorial/tutorial/edit.html",
+                           {
+                               "tutorial": tutorial, "form": form,
+                               "last_hash": compute_hash([introduction, conclusion]),
+                               "new_version": True
+                           })        
             old_slug = tutorial.get_path()
             tutorial.title = data["title"]
             tutorial.description = data["description"]
@@ -1036,7 +1064,7 @@ def edit_tutorial(request):
             "conclusion": tutorial.get_conclusion(),
         })
     return render_template("tutorial/tutorial/edit.html",
-                           {"tutorial": tutorial, "form": form})
+                           {"tutorial": tutorial, "form": form, "last_hash": compute_hash([introduction, conclusion])})
 
 # Parts.
 
@@ -1295,7 +1323,8 @@ def edit_part(request):
     except KeyError:
         raise Http404
     part = get_object_or_404(Part, pk=part_pk)
-
+    introduction = os.path.join(part.get_path(), "introduction.md")
+    conclusion = os.path.join(part.get_path(), "conclusion.md")
     # Make sure the user is allowed to do that
 
     if request.user not in part.tutorial.authors.all() and not request.user.has_perm("tutorial.change_tutorial"):
@@ -1304,7 +1333,18 @@ def edit_part(request):
         form = PartForm(request.POST)
         if form.is_valid():
             data = form.data
-
+            # avoid collision
+            if content_has_changed([introduction, conclusion],data["last_hash"]):
+                form = PartForm({"title": part.title,
+                         "introduction": part.get_introduction(),
+                         "conclusion": part.get_conclusion()})
+                return render_template("tutorial/part/edit.html", 
+                    {
+                         "part": part,
+                         "last_hash": compute_hash([introduction, conclusion]),
+                         "new_version":True,
+                         "form": form
+                    })
             # Update title and his slug.
 
             part.title = data["title"]
@@ -1333,8 +1373,12 @@ def edit_part(request):
         form = PartForm({"title": part.title,
                          "introduction": part.get_introduction(),
                          "conclusion": part.get_conclusion()})
-    return render_template("tutorial/part/edit.html", {"part": part,
-                                                       "form": form})
+    return render_template("tutorial/part/edit.html",
+        {
+            "part": part,
+	        "last_hash": compute_hash([introduction, conclusion]),
+            "form": form
+        })
 
 
 # Chapters.
@@ -1388,7 +1432,7 @@ def view_chapter(
             args=[
                 tutorial.pk,
                 tutorial.slug,
-                part["pk"],
+                part_pk,
                 part["slug"]])
         part["tutorial"] = tutorial
         for chapter in part["chapters"]:
@@ -1398,15 +1442,8 @@ def view_chapter(
             chapter["type"] = "BIG"
             chapter["position_in_part"] = cpt_c
             chapter["position_in_tutorial"] = cpt_c * cpt_p
-            chapter["get_absolute_url"] = reverse(
-                                        "zds.tutorial.views.view_chapter",
-                                        args=[
-                                              tutorial.pk,
-                                              tutorial.slug,
-                                              part["pk"],
-                                              part["slug"],
-                                              chapter["pk"],
-                                              chapter["slug"]])
+            chapter["get_absolute_url"] = part["get_absolute_url"] \
+                + "{0}/{1}/".format(chapter["pk"], chapter["slug"])
             if chapter_pk == str(chapter["pk"]):
                 chapter["intro"] = get_blob(repo.commit(sha).tree,
                                             chapter["introduction"])
@@ -1427,8 +1464,10 @@ def view_chapter(
             cpt_c += 1
         cpt_p += 1
 
-    prev_chapter = (chapter_tab[final_position - 1] if final_position > 0 else None)
-    next_chapter = (chapter_tab[final_position + 1] if final_position + 1 < len(chapter_tab) else None)
+    prev_chapter = (chapter_tab[final_position - 1] if final_position
+                    > 0 else None)
+    next_chapter = (chapter_tab[final_position + 1] if final_position + 1
+                    < len(chapter_tab) else None)
     
     return render_template("tutorial/chapter/view.html", {
         "tutorial": tutorial,
@@ -1474,7 +1513,7 @@ def view_chapter_online(
             args=[
                 tutorial.pk,
                 tutorial.slug,
-                part["pk"],
+                part_pk,
                 part["slug"]])
         part["tutorial"] = mandata
         part["position_in_tutorial"] = cpt_p
@@ -1486,15 +1525,8 @@ def view_chapter_online(
             chapter["type"] = "BIG"
             chapter["position_in_part"] = cpt_c
             chapter["position_in_tutorial"] = cpt_c * cpt_p
-            chapter["get_absolute_url_online"] = reverse(
-                                                "zds.tutorial.views.view_chapter_online",
-                                                args=[
-                                                      tutorial.pk,
-                                                      tutorial.slug,
-                                                      part["pk"],
-                                                      part["slug"],
-                                                      chapter["pk"],
-                                                      chapter["slug"]]) 
+            chapter["get_absolute_url_online"] = part[
+                "get_absolute_url_online"] + "{0}/{1}/".format(chapter["pk"], chapter["slug"])
             if chapter_pk == str(chapter["pk"]):
                 intro = open(
                     os.path.join(
@@ -1718,7 +1750,10 @@ def edit_chapter(request):
             or small and request.user not in chapter.tutorial.authors.all())\
          and not request.user.has_perm("tutorial.change_tutorial"):
         raise PermissionDenied
+    introduction = os.path.join(chapter.get_path(), "introduction.md")
+    conclusion = os.path.join(chapter.get_path(), "conclusion.md")
     if request.method == "POST":
+        
         if chapter.part:
             form = ChapterForm(request.POST, request.FILES)
             gal = chapter.part.tutorial.gallery
@@ -1727,6 +1762,16 @@ def edit_chapter(request):
             gal = chapter.tutorial.gallery
         if form.is_valid():
             data = form.data
+            # avoid collision
+            if content_has_changed([introduction, conclusion], data["last_hash"]):
+                form = render_chapter_form(chapter)
+                return render_template("tutorial/part/edit.html", 
+                    {
+                         "chapter": chapter,
+                         "last_hash": compute_hash([introduction, conclusion]),
+                         "new_version":True,
+                         "form": form
+                    })
             chapter.title = data["title"]
             
             old_slug = chapter.get_path()
@@ -1764,16 +1809,9 @@ def edit_chapter(request):
             )
             return redirect(chapter.get_absolute_url())
     else:
-        if chapter.part:
-            form = ChapterForm({"title": chapter.title,
-                                "introduction": chapter.get_introduction(),
-                                "conclusion": chapter.get_conclusion()})
-        else:
-
-            form = \
-                EmbdedChapterForm({"introduction": chapter.get_introduction(),
-                                   "conclusion": chapter.get_conclusion()})
+        form = render_chapter_form(chapter)
     return render_template("tutorial/chapter/edit.html", {"chapter": chapter,
+                                                          "last_hash": compute_hash([introduction, conclusion]),
                                                           "form": form})
 
 
@@ -1860,7 +1898,17 @@ def edit_extract(request):
             raise PermissionDenied
     if request.method == "POST":
         data = request.POST
-
+        if content_has_changed([extract.get_path()], data["last_hash"]):
+            form = form = ExtractForm(initial={
+                "title": extract.title,
+                "text": extract.get_text()})
+            return render_template("tutorial/extract/edit.html", 
+                {
+                     "extract": extract,
+                     "last_hash": compute_hash([extract.get_path()]),
+                     "new_version":True,
+                     "form": form
+                })
         # Using the « preview button »
 
         if "preview" in data:
@@ -1909,8 +1957,12 @@ def edit_extract(request):
     else:
         form = ExtractForm({"title": extract.title,
                             "text": extract.get_text()})
-    return render_template("tutorial/extract/edit.html", {"extract": extract,
-                                                          "form": form})
+    return render_template("tutorial/extract/edit.html", 
+        {
+            "extract": extract,
+            "last_hash": compute_hash([extract.get_path()]),
+            "form": form
+        })
 
 
 @can_write_and_read_now
@@ -2958,10 +3010,9 @@ def answer(request):
                 raise PermissionDenied
             for line in note_cite.text.splitlines():
                 text = text + "> " + line + "\n"
-            text = u"{0}Source:[{1}]({2}{3})".format(
+            text = u"{0}Source:[{1}]({2})".format(
                 text,
                 note_cite.author.username,
-                settings.SITE_URL,
                 note_cite.get_absolute_url())
         form = NoteForm(tutorial, request.user, initial={"text": text})
         return render_template("tutorial/comment/new.html", {
diff --git a/zds/utils/misc.py b/zds/utils/misc.py
index 21a01718e..f3ddea168 100644
--- a/zds/utils/misc.py
+++ b/zds/utils/misc.py
@@ -3,7 +3,7 @@
 import os
 import string
 import uuid
-
+import hashlib
 
 THUMB_MAX_WIDTH = 80
 THUMB_MAX_HEIGHT = 80
@@ -11,6 +11,22 @@ THUMB_MAX_HEIGHT = 80
 MEDIUM_MAX_WIDTH = 200
 MEDIUM_MAX_HEIGHT = 200
 
+def compute_hash(filenames):
+    """returns a md5 hexdigest of group of files to check if they have change"""
+    md5_hash = hashlib.md5()
+    for filename in filenames:
+        file_handle = open(filename, 'rb')
+        must_continue = True
+        while must_continue:
+            read_bytes = file_handle.read(8096)
+            if not read_bytes:
+                must_continue = False
+            else:
+                md5_hash.update(read_bytes)
+    return md5_hash.hexdigest()
+
+def content_has_changed(filenames, md5):
+    return md5 != compute_hash(filenames)
 
 def image_path(instance, filename):
     """Return path to an image."""
diff --git a/zds/utils/tutorials.py b/zds/utils/tutorials.py
index acf84fda9..fa5361598 100644
--- a/zds/utils/tutorials.py
+++ b/zds/utils/tutorials.py
@@ -9,7 +9,6 @@ from git import *
 
 from zds.utils import slugify
 
-
 # Export-to-dict functions
 def export_chapter(chapter, export_all=True):
     from zds.tutorial.models import Extract
@@ -289,3 +288,5 @@ def move(obj, new_pos, position_f, parent_f, children_fn):
     # All objects have been updated except the current one we want to move, so
     # we can do it now
     setattr(obj, position_f, new_pos)
+
+
['templates/tutorial/tutorial/edit.html', 'zds/tutorial/forms.py', 'zds/tutorial/tests.py', 'templates/tutorial/part/edit.html', 'zds/tutorial/views.py', 'zds/utils/misc.py', 'templates/tutorial/extract/edit.html', 'zds/utils/tutorials.py', 'templates/tutorial/chapter/edit.html']