diff --git a/Gulpfile.js b/Gulpfile.js
index 5671aafe05..5c56fac08c 100644
--- a/Gulpfile.js
+++ b/Gulpfile.js
@@ -48,6 +48,8 @@ gulp.task('js', () =>
gulp.src([
require.resolve('jquery'),
require.resolve('cookies-eu-banner'),
+ require.resolve('codemirror'),
+ require.resolve('codemirror/addon/merge/merge'),
// Used by other scripts, must be first
'assets/js/modal.js',
'assets/js/tooltips.js',
@@ -56,6 +58,7 @@ gulp.task('js', () =>
'assets/js/accordeon.js',
'assets/js/ajax-actions.js',
'assets/js/autocompletion.js',
+ 'assets/js/auto-merge.js',
'assets/js/close-alert-box.js',
'assets/js/compare-commits.js',
'assets/js/dropdown-menu.js',
diff --git a/assets/js/auto-merge.js b/assets/js/auto-merge.js
new file mode 100644
index 0000000000..cbfa427152
--- /dev/null
+++ b/assets/js/auto-merge.js
@@ -0,0 +1,65 @@
+(function ($, undefined) {
+ "use strict";
+
+ $(document).ready(function () {
+
+ /**
+ * Sets up the merge interface (using codemirror) in the $div Object.
+ * Data is generally retrieved from a form field or an aditionnal
+ * div exposing the old data,also generated in the form.
+ * @param {Object} $div - The base object used to set up the interface. Generally created in forms files.
+ * @param {Object} $left - The object from which we will pick the content to put in the left hand side (lhs) of the editor.
+ * @param {Object} $right - The object from which we will pick the content to put in the right hand side (rhs) of the editor.
+ */
+ function mergeUISetUp(selector, $left, $right){
+ var target = document.getElementsByClassName(selector)[0]; // TODO remplacer par ID ou objet
+ if (target) {
+ target.innerHTML = "";
+ var merge = window.CodeMirror.MergeView(target, {
+ value: $left.html(),
+ orig: $right.html(),
+ lineNumbers: true,
+ highlightDifferences: true,
+ connect: "align",
+ collapseIdentical: true
+ });
+ return merge;
+ }
+ }
+
+ var mergeInterfaceList = {};
+ mergeInterfaceList.introduction = mergeUISetUp("compare-introduction",$("#your_introduction"),$("#id_introduction"));
+ mergeInterfaceList.conclusion = mergeUISetUp("compare-conclusion",$("#your_conclusion"),$("#id_conclusion"));
+ mergeInterfaceList.text = mergeUISetUp("compare-text",$("#your_text"),$("#id_text"));
+
+ $(".CodeMirror-merge-editor").append("Votre Version");
+ $(".CodeMirror-merge-right").append("La version courante");
+
+ /**
+ * Merge content
+ */
+ $(".merge-btn").on("click", function(e){
+ e.stopPropagation();
+ e.preventDefault();
+ var button = $(this);
+
+ Array.from(this.classList).forEach(function(element){
+ if (element.indexOf("need-to-merge-") >= 0) {
+ var substring = element.substring(14);
+ var toMerge = mergeInterfaceList[substring].editor().getValue();
+ $("#id_" + substring).text(toMerge);
+
+ // Confirmation message
+ var msg = "
";
+ button.before(msg);
+ setTimeout(function() {
+ $(".alert-merge").fadeOut("fast");
+ }, 2000);
+ }
+ });
+ });
+ });
+})(jQuery);
diff --git a/assets/scss/components/_auto-merge.scss b/assets/scss/components/_auto-merge.scss
new file mode 100644
index 0000000000..b2a9f53e5e
--- /dev/null
+++ b/assets/scss/components/_auto-merge.scss
@@ -0,0 +1,3 @@
+#compare-lhs-margin, #compare-rhs-margin {
+ display: none;
+}
diff --git a/assets/scss/components/_codemirror.scss b/assets/scss/components/_codemirror.scss
new file mode 100644
index 0000000000..62b88de4a2
--- /dev/null
+++ b/assets/scss/components/_codemirror.scss
@@ -0,0 +1,450 @@
+/* BASICS */
+
+.CodeMirror {
+ /* Set height, width, borders, and global font properties here */
+ font-family: monospace;
+ height: 300px;
+ color: black;
+}
+
+/* PADDING */
+
+.CodeMirror-lines {
+ padding: 4px 0; /* Vertical padding around content */
+}
+.CodeMirror pre {
+ padding: 0 4px; /* Horizontal padding of content */
+}
+
+.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
+ background-color: white; /* The little square between H and V scrollbars */
+}
+
+/* GUTTER */
+
+.CodeMirror-gutters {
+ border-right: 1px solid #ddd;
+ background-color: #f7f7f7;
+ white-space: nowrap;
+}
+.CodeMirror-linenumbers {}
+.CodeMirror-linenumber {
+ padding: 0 3px 0 5px;
+ min-width: 20px;
+ text-align: right;
+ color: #999;
+ white-space: nowrap;
+}
+
+.CodeMirror-guttermarker { color: black; }
+.CodeMirror-guttermarker-subtle { color: #999; }
+
+/* CURSOR */
+
+.CodeMirror-cursor {
+ border-left: 1px solid black;
+ border-right: none;
+ width: 0;
+}
+/* Shown when moving in bi-directional text */
+.CodeMirror div.CodeMirror-secondarycursor {
+ border-left: 1px solid silver;
+}
+.cm-fat-cursor .CodeMirror-cursor {
+ width: auto;
+ border: 0;
+ background: #7e7;
+}
+.cm-fat-cursor div.CodeMirror-cursors {
+ z-index: 1;
+}
+
+.cm-animate-fat-cursor {
+ width: auto;
+ border: 0;
+ -webkit-animation: blink 1.06s steps(1) infinite;
+ -moz-animation: blink 1.06s steps(1) infinite;
+ animation: blink 1.06s steps(1) infinite;
+ background-color: #7e7;
+}
+@-moz-keyframes blink {
+ 0% {}
+ 50% { background-color: transparent; }
+ 100% {}
+}
+@-webkit-keyframes blink {
+ 0% {}
+ 50% { background-color: transparent; }
+ 100% {}
+}
+@keyframes blink {
+ 0% {}
+ 50% { background-color: transparent; }
+ 100% {}
+}
+
+/* Can style cursor different in overwrite (non-insert) mode */
+.CodeMirror-overwrite .CodeMirror-cursor {}
+
+.cm-tab { display: inline-block; text-decoration: inherit; }
+
+.CodeMirror-ruler {
+ border-left: 1px solid #ccc;
+ position: absolute;
+}
+
+/* DEFAULT THEME */
+
+.cm-s-default .cm-header {color: blue;}
+.cm-s-default .cm-quote {color: #090;}
+.cm-negative {color: #d44;}
+.cm-positive {color: #292;}
+.cm-header, .cm-strong {font-weight: bold;}
+.cm-em {font-style: italic;}
+.cm-link {text-decoration: underline;}
+.cm-strikethrough {text-decoration: line-through;}
+
+.cm-s-default .cm-keyword {color: #708;}
+.cm-s-default .cm-atom {color: #219;}
+.cm-s-default .cm-number {color: #164;}
+.cm-s-default .cm-def {color: #00f;}
+.cm-s-default .cm-variable,
+.cm-s-default .cm-punctuation,
+.cm-s-default .cm-property,
+.cm-s-default .cm-operator {}
+.cm-s-default .cm-variable-2 {color: #05a;}
+.cm-s-default .cm-variable-3 {color: #085;}
+.cm-s-default .cm-comment {color: #a50;}
+.cm-s-default .cm-string {color: #a11;}
+.cm-s-default .cm-string-2 {color: #f50;}
+.cm-s-default .cm-meta {color: #555;}
+.cm-s-default .cm-qualifier {color: #555;}
+.cm-s-default .cm-builtin {color: #30a;}
+.cm-s-default .cm-bracket {color: #997;}
+.cm-s-default .cm-tag {color: #170;}
+.cm-s-default .cm-attribute {color: #00c;}
+.cm-s-default .cm-hr {color: #999;}
+.cm-s-default .cm-link {color: #00c;}
+
+.cm-s-default .cm-error {color: #f00;}
+.cm-invalidchar {color: #f00;}
+
+.CodeMirror-composing { border-bottom: 2px solid; }
+
+/* Default styles for common addons */
+
+div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
+div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
+.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
+.CodeMirror-activeline-background {background: #e8f2ff;}
+
+/* STOP */
+
+/* The rest of this file contains styles related to the mechanics of
+ the editor. You probably shouldn't touch them. */
+
+.CodeMirror {
+ position: relative;
+ overflow: hidden;
+ background: white;
+}
+
+.CodeMirror-scroll {
+ overflow: scroll !important; /* Things will break if this is overridden */
+ /* 30px is the magic margin used to hide the element's real scrollbars */
+ /* See overflow: hidden in .CodeMirror */
+ margin-bottom: -30px; margin-right: -30px;
+ padding-bottom: 30px;
+ height: 100%;
+ outline: none; /* Prevent dragging from highlighting the element */
+ position: relative;
+}
+.CodeMirror-sizer {
+ position: relative;
+ border-right: 30px solid transparent;
+}
+
+/* The fake, visible scrollbars. Used to force redraw during scrolling
+ before actual scrolling happens, thus preventing shaking and
+ flickering artifacts. */
+.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
+ position: absolute;
+ z-index: 6;
+ display: none;
+}
+.CodeMirror-vscrollbar {
+ right: 0; top: 0;
+ overflow-x: hidden;
+ overflow-y: scroll;
+}
+.CodeMirror-hscrollbar {
+ bottom: 0; left: 0;
+ overflow-y: hidden;
+ overflow-x: scroll;
+}
+.CodeMirror-scrollbar-filler {
+ right: 0; bottom: 0;
+}
+.CodeMirror-gutter-filler {
+ left: 0; bottom: 0;
+}
+
+.CodeMirror-gutters {
+ position: absolute; left: 0; top: 0;
+ z-index: 3;
+}
+.CodeMirror-gutter {
+ white-space: normal;
+ height: 100%;
+ display: inline-block;
+ margin-bottom: -30px;
+ /* Hack to make IE7 behave */
+ *zoom:1;
+ *display:inline;
+}
+.CodeMirror-gutter-wrapper {
+ position: absolute;
+ z-index: 4;
+ background: none !important;
+ border: none !important;
+}
+.CodeMirror-gutter-background {
+ position: absolute;
+ top: 0; bottom: 0;
+ z-index: 4;
+}
+.CodeMirror-gutter-elt {
+ position: absolute;
+ cursor: default;
+ z-index: 4;
+}
+.CodeMirror-gutter-wrapper {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+}
+
+.CodeMirror-lines {
+ cursor: text;
+ min-height: 1px; /* prevents collapsing before first draw */
+}
+.CodeMirror pre {
+ /* Reset some styles that the rest of the page might have set */
+ -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
+ border-width: 0;
+ background: transparent;
+ font-family: inherit;
+ font-size: inherit;
+ margin: 0;
+ white-space: pre;
+ word-wrap: normal;
+ line-height: inherit;
+ color: inherit;
+ z-index: 2;
+ position: relative;
+ overflow: visible;
+ -webkit-tap-highlight-color: transparent;
+}
+.CodeMirror-wrap pre {
+ word-wrap: break-word;
+ white-space: pre-wrap;
+ word-break: normal;
+}
+
+.CodeMirror-linebackground {
+ position: absolute;
+ left: 0; right: 0; top: 0; bottom: 0;
+ z-index: 0;
+}
+
+.CodeMirror-linewidget {
+ position: relative;
+ z-index: 2;
+ overflow: auto;
+}
+
+.CodeMirror-widget {}
+
+.CodeMirror-code {
+ outline: none;
+}
+
+/* Force content-box sizing for the elements where we expect it */
+.CodeMirror-scroll,
+.CodeMirror-sizer,
+.CodeMirror-gutter,
+.CodeMirror-gutters,
+.CodeMirror-linenumber {
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+}
+
+.CodeMirror-measure {
+ position: absolute;
+ width: 100%;
+ height: 0;
+ overflow: hidden;
+ visibility: hidden;
+}
+
+.CodeMirror-cursor { position: absolute; }
+.CodeMirror-measure pre { position: static; }
+
+div.CodeMirror-cursors {
+ visibility: hidden;
+ position: relative;
+ z-index: 3;
+}
+div.CodeMirror-dragcursors {
+ visibility: visible;
+}
+
+.CodeMirror-focused div.CodeMirror-cursors {
+ visibility: visible;
+}
+
+.CodeMirror-selected { background: #d9d9d9; }
+.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
+.CodeMirror-crosshair { cursor: crosshair; }
+.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
+.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
+
+.cm-searching {
+ background: #ffa;
+ background: rgba(255, 255, 0, .4);
+}
+
+/* IE7 hack to prevent it from returning funny offsetTops on the spans */
+.CodeMirror span { *vertical-align: text-bottom; }
+
+/* Used to force a border model for a node */
+.cm-force-border { padding-right: .1px; }
+
+@media print {
+ /* Hide the cursor when printing */
+ .CodeMirror div.CodeMirror-cursors {
+ visibility: hidden;
+ }
+}
+
+/* See issue #2901 */
+.cm-tab-wrap-hack:after { content: ''; }
+
+/* Help users use markselection to safely style text background */
+span.CodeMirror-selectedtext { background: none; }
+
+/* MERGE ADDON */
+.CodeMirror-merge {
+ position: relative;
+ border: 1px solid #ddd;
+ white-space: pre;
+}
+
+.CodeMirror-merge, .CodeMirror-merge .CodeMirror {
+ height: 350px;
+}
+
+.CodeMirror-merge-2pane .CodeMirror-merge-pane { width: 47%; }
+.CodeMirror-merge-2pane .CodeMirror-merge-gap { width: 6%; }
+.CodeMirror-merge-3pane .CodeMirror-merge-pane { width: 31%; }
+.CodeMirror-merge-3pane .CodeMirror-merge-gap { width: 3.5%; }
+
+.CodeMirror-merge-pane {
+ display: inline-block;
+ white-space: normal;
+ vertical-align: top;
+}
+.CodeMirror-merge-pane-rightmost {
+ position: absolute;
+ right: 0px;
+ z-index: 1;
+}
+
+.CodeMirror-merge-gap {
+ z-index: 2;
+ display: inline-block;
+ height: 100%;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ overflow: hidden;
+ border-left: 1px solid #ddd;
+ border-right: 1px solid #ddd;
+ position: relative;
+ background: #f8f8f8;
+}
+
+.CodeMirror-merge-scrolllock-wrap {
+ position: absolute;
+ bottom: 0; left: 50%;
+}
+.CodeMirror-merge-scrolllock {
+ position: relative;
+ left: -50%;
+ cursor: pointer;
+ color: #555;
+ line-height: 1;
+}
+
+.CodeMirror-merge-copybuttons-left, .CodeMirror-merge-copybuttons-right {
+ position: absolute;
+ left: 0; top: 0;
+ right: 0; bottom: 0;
+ line-height: 1;
+}
+
+.CodeMirror-merge-copy {
+ position: absolute;
+ cursor: pointer;
+ color: #44c;
+ z-index: 3;
+}
+
+.CodeMirror-merge-copy-reverse {
+ position: absolute;
+ cursor: pointer;
+ color: #44c;
+}
+
+.CodeMirror-merge-copybuttons-left .CodeMirror-merge-copy { left: 2px; }
+.CodeMirror-merge-copybuttons-right .CodeMirror-merge-copy { right: 2px; }
+
+.CodeMirror-merge-r-inserted, .CodeMirror-merge-l-inserted {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAYAAACddGYaAAAAGUlEQVQI12MwuCXy3+CWyH8GBgYGJgYkAABZbAQ9ELXurwAAAABJRU5ErkJggg==);
+ background-position: bottom left;
+ background-repeat: repeat-x;
+}
+
+.CodeMirror-merge-r-deleted, .CodeMirror-merge-l-deleted {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAYAAACddGYaAAAAGUlEQVQI12M4Kyb2/6yY2H8GBgYGJgYkAABURgPz6Ks7wQAAAABJRU5ErkJggg==);
+ background-position: bottom left;
+ background-repeat: repeat-x;
+}
+
+.CodeMirror-merge-r-chunk { background: #ffffe0; }
+.CodeMirror-merge-r-chunk-start { border-top: 1px solid #ee8; }
+.CodeMirror-merge-r-chunk-end { border-bottom: 1px solid #ee8; }
+.CodeMirror-merge-r-connect { fill: #ffffe0; stroke: #ee8; stroke-width: 1px; }
+
+.CodeMirror-merge-l-chunk { background: #eef; }
+.CodeMirror-merge-l-chunk-start { border-top: 1px solid #88e; }
+.CodeMirror-merge-l-chunk-end { border-bottom: 1px solid #88e; }
+.CodeMirror-merge-l-connect { fill: #eef; stroke: #88e; stroke-width: 1px; }
+
+.CodeMirror-merge-l-chunk.CodeMirror-merge-r-chunk { background: #dfd; }
+.CodeMirror-merge-l-chunk-start.CodeMirror-merge-r-chunk-start { border-top: 1px solid #4e4; }
+.CodeMirror-merge-l-chunk-end.CodeMirror-merge-r-chunk-end { border-bottom: 1px solid #4e4; }
+
+.CodeMirror-merge-collapsed-widget:before {
+ content: "(...)";
+}
+.CodeMirror-merge-collapsed-widget {
+ cursor: pointer;
+ color: #88b;
+ background: #eef;
+ border: 1px solid #ddf;
+ font-size: 90%;
+ padding: 0 3px;
+ border-radius: 4px;
+}
+.CodeMirror-merge-collapsed-line .CodeMirror-gutter-elt { display: none; }
+
diff --git a/assets/scss/components/_mergely.scss b/assets/scss/components/_mergely.scss
new file mode 100644
index 0000000000..832762d7b8
--- /dev/null
+++ b/assets/scss/components/_mergely.scss
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2016 by Jamie Peabody, http://www.mergely.com
+ * All rights reserved.
+ * Version: 3.4.3 2016-09-07
+ */
+
+/* required */
+.mergely-column textarea { width: 80px; height: 200px; }
+.mergely-column { float: left; }
+.mergely-margin { float: left; }
+.mergely-canvas { float: left; width: 28px; }
+
+/* resizeable */
+.mergely-resizer { width: 100%; height: 100%; }
+
+/* style configuration */
+.mergely-column { border: 1px solid #ccc; }
+.mergely-active { border: 1px solid #a3d1ff; }
+
+.mergely.a,.mergely.d,.mergely.c { color: #000; }
+
+.mergely.a.rhs.start { border-top: 1px solid #a3d1ff; }
+.mergely.a.lhs.start.end,
+.mergely.a.rhs.end { border-bottom: 1px solid #a3d1ff; }
+.mergely.a.rhs { background-color: #ddeeff; }
+.mergely.a.lhs.start.end.first { border-bottom: 0; border-top: 1px solid #a3d1ff; }
+
+.mergely.d.lhs { background-color: #ffe9e9; }
+.mergely.d.lhs.end,
+.mergely.d.rhs.start.end { border-bottom: 1px solid #f8e8e8; }
+.mergely.d.rhs.start.end.first { border-bottom: 0; border-top: 1px solid #f8e8e8; }
+.mergely.d.lhs.start { border-top: 1px solid #f8e8e8; }
+
+.mergely.c.lhs,
+.mergely.c.rhs { background-color: #fafafa; }
+.mergely.c.lhs.start,
+.mergely.c.rhs.start { border-top: 1px solid #a3a3a3; }
+.mergely.c.lhs.end,
+.mergely.c.rhs.end { border-bottom: 1px solid #a3a3a3; }
+
+.mergely.ch.a.rhs { background-color: #ddeeff; }
+.mergely.ch.d.lhs { background-color: #ffe9e9; text-decoration: line-through; color: red !important; }
+
+.mergely.current.start { border-top: 1px solid #000 !important; }
+.mergely.current.end { border-bottom: 1px solid #000 !important; }
+.mergely.current.lhs.a.start.end,
+.mergely.current.rhs.d.start.end { border-top: 0 !important; }
+.mergely.current.CodeMirror-linenumber { color: #F9F9F9; font-weight: bold; background-color: #777; }
+.CodeMirror-linenumber { cursor: pointer; }
+.CodeMirror-code { color: #717171; }
diff --git a/assets/scss/main.scss b/assets/scss/main.scss
index c9e4c97cfa..dab5607444 100644
--- a/assets/scss/main.scss
+++ b/assets/scss/main.scss
@@ -58,6 +58,7 @@
@import "components/authors";
@import "components/autocomplete";
@import "components/breadcrumb";
+@import "components/codemirror";
@import "components/content-item";
@import "components/editor";
@import "components/featured-item";
diff --git a/package.json b/package.json
index ef688e426b..e526947b48 100644
--- a/package.json
+++ b/package.json
@@ -27,10 +27,12 @@
"homepage": "https://github.com/zestedesavoir/zds-site",
"dependencies": {
"autoprefixer": "7.1.2",
+ "codemirror": "^5.28.0",
"cookies-eu-banner": "^1.2.10",
"cssnano": "3.10.0",
"del": "3.0.0",
- "gulp": "3.9.1",
+ "diff-merge-patch": "^0.6.0",
+ "gulp": "^3.9.1",
"gulp-concat": "2.6.1",
"gulp-imagemin": "3.3.0",
"gulp-postcss": "7.0.0",
diff --git a/templates/base.html b/templates/base.html
index d2b0db1aee..b36742dfd2 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -678,11 +678,9 @@ {{ headlin
{# Javascript stuff start #}
-
-
{% block extra_js %}
{% endblock %}
-
+
{# Google Analytics #}
diff --git a/templates/tutorialv2/edit/container.html b/templates/tutorialv2/edit/container.html
index 4810559c08..af222c4802 100644
--- a/templates/tutorialv2/edit/container.html
+++ b/templates/tutorialv2/edit/container.html
@@ -3,6 +3,10 @@
{% load i18n %}
{% load feminize %}
+{% block extra_js %}
+
+{% endblock %}
+
{% block title %}
{% trans "Éditer " %}{{ "un"|feminize:container.get_level_as_string }} {{ container.get_level_as_string|lower }}
{% endblock %}
@@ -44,4 +48,4 @@
{% trans "Ajouter une image à la galerie" %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/templates/tutorialv2/edit/content.html b/templates/tutorialv2/edit/content.html
index 74f55f0ba0..9bbb0d2b51 100644
--- a/templates/tutorialv2/edit/content.html
+++ b/templates/tutorialv2/edit/content.html
@@ -4,6 +4,10 @@
{% load i18n %}
{% load feminize %}
+{% block extra_js %}
+
+{% endblock %}
+
{% block title %}
{% trans "Éditer " %}{{ content.textual_type }}
{% endblock %}
@@ -42,4 +46,4 @@
{% trans "Ajouter une image à la galerie" %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/templates/tutorialv2/edit/extract.html b/templates/tutorialv2/edit/extract.html
index 7979ecfbd1..ad28f2fa8b 100644
--- a/templates/tutorialv2/edit/extract.html
+++ b/templates/tutorialv2/edit/extract.html
@@ -2,6 +2,9 @@
{% load crispy_forms_tags %}
{% load i18n %}
+{% block extra_js %}
+
+{% endblock %}
{% block title %}
{% trans "Éditer la section" %}
@@ -50,4 +53,4 @@
{% trans "Ajouter une image à la galerie" %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/yarn.lock b/yarn.lock
index a94766c5d5..02dcdb76cb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -535,6 +535,10 @@ code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+codemirror@^5.28.0:
+ version "5.28.0"
+ resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.28.0.tgz#2978d9280d671351a4f5737d06bbd681a0fd6f83"
+
color-convert@^1.3.0, color-convert@^1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a"
@@ -938,6 +942,21 @@ detect-newline@2.X:
version "2.1.0"
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
+diff-merge-patch@0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/diff-merge-patch/-/diff-merge-patch-0.4.0.tgz#c13251422e53fa4b72b2f7c3a3afd1cba2763ec8"
+ dependencies:
+ longest-common-substring "0.0.1"
+ underscore "~1.4.3"
+
+diff-merge-patch@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/diff-merge-patch/-/diff-merge-patch-0.6.0.tgz#3ff2886f00ce1de3d5f455e0bfee0b8ad950322b"
+ dependencies:
+ diff-merge-patch "0.4.0"
+ longest-common-substring "0.0.1"
+ underscore "~1.4.3"
+
dom-serializer@0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
@@ -1834,7 +1853,7 @@ gulp.spritesmith@6.5.1:
underscore "~1.8.3"
url2 "~1.0.4"
-gulp@3.9.1:
+gulp@^3.9.1:
version "3.9.1"
resolved "https://registry.yarnpkg.com/gulp/-/gulp-3.9.1.tgz#571ce45928dd40af6514fc4011866016c13845b4"
dependencies:
@@ -2659,6 +2678,10 @@ logalot@^2.0.0:
figures "^1.3.5"
squeak "^1.0.0"
+longest-common-substring@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/longest-common-substring/-/longest-common-substring-0.0.1.tgz#d91f8d08ab5d2debc9914fca8d4b91413956acc8"
+
longest@^1.0.0, longest@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
@@ -4443,7 +4466,7 @@ underscore.string@~3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.0.3.tgz#4617b8c1a250cf6e5064fbbb363d0fa96cf14552"
-underscore@~1.4.2:
+underscore@~1.4.2, underscore@~1.4.3:
version "1.4.4"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604"
diff --git a/zds/tutorialv2/forms.py b/zds/tutorialv2/forms.py
index ae4a5f8984..1f351083ea 100644
--- a/zds/tutorialv2/forms.py
+++ b/zds/tutorialv2/forms.py
@@ -14,7 +14,7 @@
from django.utils.translation import ugettext_lazy as _
from zds.member.models import Profile
from zds.tutorialv2.utils import slugify_raise_on_invalid, InvalidSlugError
-from zds.utils.forms import TagValidator
+from zds.utils.forms import TagValidator, MergeableFieldMixin
class FormWithTitle(forms.Form):
@@ -107,7 +107,7 @@ def clean_username(self):
return cleaned_data
-class ContainerForm(FormWithTitle):
+class ContainerForm(FormWithTitle, MergeableFieldMixin):
introduction = forms.CharField(
label=_('Introduction'),
@@ -149,18 +149,25 @@ def __init__(self, *args, **kwargs):
self.helper.form_class = 'content-wrapper'
self.helper.form_method = 'post'
- self.helper.layout = Layout(
- Field('title'),
- Field('introduction', css_class='md-editor preview-source'),
- ButtonHolder(StrictButton(_('Aperçu'), type='preview', name='preview',
- css_class='btn btn-grey preview-btn'),),
- HTML('{% if form.introduction.value %}{% include "misc/previsualization.part.html" \
- with text=form.introduction.value %}{% endif %}'),
- Field('conclusion', css_class='md-editor preview-source'),
- ButtonHolder(StrictButton(_('Aperçu'), type='preview', name='preview',
- css_class='btn btn-grey preview-btn'),),
- HTML('{% if form.conclusion.value %}{% include "misc/previsualization.part.html" \
- with text=form.conclusion.value %}{% endif %}'),
+ self.helper.layout = Layout(Field('title'))
+
+ if kwargs.get('data', None) is not None:
+ self.add_merge_interface_to_field('introduction', **kwargs)
+ self.add_merge_interface_to_field('conclusion', **kwargs)
+ else:
+ self.helper.layout.append(Layout(
+ Field('introduction', css_class='md-editor preview-source'),
+ ButtonHolder(StrictButton(_('Aperçu'), type='preview', name='preview',
+ css_class='btn btn-grey preview-btn'),),
+ HTML('{% if form.introduction.value %}{% include "misc/previsualization.part.html" \
+ with text=form.introduction.value %}{% endif %}'),
+ Field('conclusion', css_class='md-editor preview-source'),
+ ButtonHolder(StrictButton(_('Aperçu'), type='preview', name='preview',
+ css_class='btn btn-grey preview-btn'),
+ HTML('{% if form.conclusion.value %}{% include "misc/previsualization.part.html" \
+ with text=form.conclusion.value %}{% endif %}'))))
+
+ self.helper.layout.append(Layout(
Field('msg_commit'),
Field('last_hash'),
ButtonHolder(
@@ -168,10 +175,10 @@ def __init__(self, *args, **kwargs):
_('Valider'),
type='submit'),
)
- )
+ ))
-class ContentForm(ContainerForm):
+class ContentForm(ContainerForm, MergeableFieldMixin):
description = forms.CharField(
label=_('Description'),
@@ -230,7 +237,7 @@ class ContentForm(ContainerForm):
widget=forms.CheckboxSelectMultiple()
)
- def _create_layout(self, hide_help):
+ def _create_layout(self, hide_help, **kwargs):
html_part = HTML(_("
Demander de l'aide à la communauté !
"
"Si vous avez besoin d'un coup de main, "
"sélectionnez une ou plusieurs catégories d'aide ci-dessous "
@@ -243,21 +250,28 @@ def _create_layout(self, hide_help):
Field('description'),
Field('tags'),
Field('type'),
- Field('image'),
- Field('introduction', css_class='md-editor preview-source'),
- ButtonHolder(StrictButton(_('Aperçu'), type='preview', name='preview',
- css_class='btn btn-grey preview-btn'),),
- HTML('{% if form.introduction.value %}{% include "misc/previsualization.part.html" \
- with text=form.introduction.value %}{% endif %}'),
- Field('conclusion', css_class='md-editor preview-source'),
- ButtonHolder(StrictButton(_('Aperçu'), type='preview', name='preview',
- css_class='btn btn-grey preview-btn'),),
- HTML('{% if form.conclusion.value %}{% include "misc/previsualization.part.html" \
- with text=form.conclusion.value %}{% endif %}'),
+ Field('image'))
+
+ if kwargs.get('data') is not None:
+ self.add_merge_interface_to_field('introduction', **kwargs)
+ self.add_merge_interface_to_field('conclusion', **kwargs)
+ else:
+ self.helper.layout.append(Layout(
+ Field('introduction', css_class='md-editor preview-source'),
+ ButtonHolder(StrictButton(_('Aperçu'), type='preview', name='preview',
+ css_class='btn btn-grey preview-btn'),),
+ HTML('{% if form.introduction.value %}{% include "misc/previsualization.part.html" \
+ with text=form.introduction.value %}{% endif %}'),
+ Field('conclusion', css_class='md-editor preview-source'),
+ ButtonHolder(StrictButton(_('Aperçu'), type='preview', name='preview',
+ css_class='btn btn-grey preview-btn'),
+ HTML('{% if form.conclusion.value %}{% include "misc/previsualization.part.html" \
+ with text=form.conclusion.value %}{% endif %}'))))
+
+ self.helper.layout.append(Layout(
Field('last_hash'),
Field('licence'),
- Field('subcategory', template='crispy/checkboxselectmultiple.html'),
- )
+ Field('subcategory', template='crispy/checkboxselectmultiple.html')))
if not hide_help:
self.helper.layout.append(html_part)
@@ -270,10 +284,11 @@ def __init__(self, *args, **kwargs):
for_tribune = kwargs.pop('for_tribune', False)
super(ContentForm, self).__init__(*args, **kwargs)
+ self.helper = FormHelper()
self.helper = FormHelper()
self.helper.form_class = 'content-wrapper'
self.helper.form_method = 'post'
- self._create_layout(for_tribune)
+ self._create_layout(for_tribune, **kwargs)
if 'type' in self.initial:
self.helper['type'].wrap(
diff --git a/zds/utils/forms.py b/zds/utils/forms.py
index 867b330958..f831320dce 100644
--- a/zds/utils/forms.py
+++ b/zds/utils/forms.py
@@ -8,6 +8,24 @@
from zds.utils.misc import contains_utf8mb4
+class MergeableFieldMixin():
+ def add_merge_interface_to_field(self, field_name, **kwargs):
+ field_old_content = kwargs.get('data').get(field_name)
+ if field_old_content is None:
+ field_old_content = ''
+ self.helper.layout.append(Layout(Field(field_name, css_class='hidden')))
+
+ self.helper.layout.append(
+ Layout(HTML('
{1}
'
+ .format(field_name, field_old_content))))
+ self.helper.layout.append(
+ Layout(HTML(''
+ .format(field_name))))
+ self.helper.layout.append(Layout(
+ ButtonHolder(StrictButton(_('Valider cette version'), type='merge', name='merge',
+ css_class='btn btn-submit merge-btn need-to-merge-{0}'.format(field_name)))))
+
+
class CommonLayoutEditor(Layout):
def __init__(self, *args, **kwargs):
@@ -31,12 +49,18 @@ def __init__(self, *args, **kwargs):
)
-class CommonLayoutVersionEditor(Layout):
+class CommonLayoutVersionEditor(Layout, MergeableFieldMixin):
def __init__(self, *args, **kwargs):
+
+ if kwargs.get('data', None) is not None:
+ self.add_merge_interface_to_field('text', **kwargs)
+ else:
+ text_field = Field('text', css_class='md-editor')
+
super(CommonLayoutVersionEditor, self).__init__(
Div(
- Field('text', css_class='md-editor'),
+ text_field,
Field('msg_commit'),
ButtonHolder(
StrictButton(