Commit 15a363d0 by benjaoming

Detection of editing conflicts, ie. concurrent article edits. If the revision…

Detection of editing conflicts, ie. concurrent article edits. If the revision number has changed while editing, warn the user and merge the user's content with the new revision.
parent 9d92e966
import difflib
def simple_merge(txt1, txt2):
"""Merges two texts"""
differ = difflib.Differ(charjunk=difflib.IS_CHARACTER_JUNK)
diff = differ.compare(txt1.splitlines(1), txt2.splitlines(1))
content = "".join([l[2:] for l in diff])
return content
\ No newline at end of file
......@@ -5,6 +5,7 @@ from django.http import HttpResponse, HttpResponseForbidden,\
import models
from wiki.core.exceptions import NoRootURL
from django.shortcuts import redirect, get_object_or_404
from django.core.urlresolvers import reverse
def json_view(func):
def wrap(request, *a, **kw):
......@@ -32,9 +33,10 @@ def get_article(func=None, can_read=True, can_write=False):
return redirect('wiki:root_create')
except models.URLPath.DoesNotExist:
try:
path = "/".join(filter(lambda x: x!="", path.split("/"),)[:-1])
pathlist = filter(lambda x: x!="", path.split("/"),)
path = "/".join(pathlist[:-1])
parent = models.URLPath.get_by_path(path)
return redirect("wiki:create_url", parent.path)
return redirect(reverse("wiki:create_url", args=(parent.path,)) + "?slug=%s" % pathlist[-1])
except models.URLPath.DoesNotExist:
return HttpResponseNotFound("This article was not found. This page should look nicer.")
article = urlpath.article
......
......@@ -8,6 +8,7 @@ from wiki import models
from django.forms.util import flatatt
from django.utils.encoding import force_unicode
from django.utils.html import escape, conditional_escape
from wiki.core.diff import simple_merge
class CreateRoot(forms.Form):
......@@ -26,22 +27,47 @@ class EditForm(forms.Form):
summary = forms.CharField(label=_(u'Summary'), help_text=_(u'Give a short reason for your edit, which will be stated in the revision log.'),
required=False)
def __init__(self, instance, *args, **kwargs):
current_revision = forms.IntegerField(required=False, widget=forms.HiddenInput())
def __init__(self, current_revision, *args, **kwargs):
self.preview = kwargs.pop('preview', False)
if instance:
initial = {'content': instance.content,
'title': instance.title,}
self.initial_revision = current_revision
self.presumed_revision = None
if current_revision:
initial = {'content': current_revision.content,
'title': current_revision.title,
'current_revision': current_revision.id}
initial.update(kwargs.get('initial', {}))
# Manipulate any data put in args[0] such that the current_revision
# is reset to match the actual current revision.
data = None
if len(args) > 0:
data = args[0]
if not data:
data = kwargs.get('data', None)
if data:
self.presumed_revision = data.get('current_revision', None)
print self.initial_revision.id, self.presumed_revision
if not str(self.presumed_revision) == str(self.initial_revision.id):
newdata = {}
for k,v in data.items():
newdata[k] = v
newdata['current_revision'] = self.initial_revision.id
newdata['content'] = simple_merge(self.initial_revision.content,
data.get('content', ""))
kwargs['data'] = newdata
kwargs['initial'] = initial
self.instance = instance
super(EditForm, self).__init__(*args, **kwargs)
def clean(self):
cd = self.cleaned_data
if cd['title'] == self.instance.title and cd['content'] == self.instance.content:
if not str(self.initial_revision.id) == str(self.presumed_revision):
raise forms.ValidationError(_(u'While you were editing, someone else changed the revision. Your contents have been automatically merged with the new contents. Please review the text below.'))
if cd['title'] == self.initial_revision.title and cd['content'] == self.initial_revision.content:
raise forms.ValidationError(_(u'No changes made. Nothing to save.'))
return cd
......
......@@ -121,6 +121,8 @@ class Article(models.Model):
return ArticleForObject.objects.get(object_id=obj.id, content_type=ContentType.objects.get_for_model(obj)).article
def __unicode__(self):
if self.current_revision:
return self.current_revision.title
return self.title
class Meta:
......
......@@ -17,9 +17,9 @@
$(document).ready(
function() {
$('.accordion input[disabled!="disabled"][type="radio"]').first().attr('checked', 'true');
// Fix modal heights
$('.modal-body').css('height', $(window).height()*0.70 + 'px');
$('.modal').css('max-height', $(window).height() + 'px');
// Fix modal heights
$('.modal-body').css('height', $(window).height()*0.70 + 'px');
$('.modal').css('max-height', $(window).height() + 'px');
});
</script>
{% endaddtoblock %}
......@@ -69,10 +69,8 @@
</h1>
</li>
</ul>
<div class="tab-content">
<form method="GET">
<form method="GET">
<div class="tab-content" style="overflow: visible;">
{% for revision in revisions %}
<div class="accordion" id="accordion{{ revision.revision_number }}">
<div class="accordion-group">
......@@ -103,7 +101,9 @@
{% trans "Show changes" %}
</a>
{% if article|can_write:user %}
<input type="radio"{% if revision == article.current_revision %} disabled="true"{% endif %} style="margin: 0 10px;" value="{{ revision.id }}" name="revision_id" switch-button-href="{% url 'wiki:change_revision_url' urlpath.path revision.id %}" merge-button-href="{% url 'wiki:merge_revision_preview' article.id revision.id %}" merge-button-commit-href="{% url 'wiki:merge_revision_url' urlpath.path revision.id %}" />
{% endif %}
</div>
<div style="clear: both"></div>
......@@ -133,7 +133,7 @@
{% include "wiki/includes/pagination.html" %}
{% if revisions.count > 1 and not is_paginated %}
{% if revisions.count > 1 %}
<div class="form-actions">
<div class="pull-right">
{% if article|can_write:user %}
......@@ -153,65 +153,64 @@
</button>
</div>
</div>
<div class="modal hide fade" id="previewModal" style="width: 80%; margin-left: -40%;">
<div class="modal-body">
<iframe name="previewWindow" style="width: 100%; height: 100%; border: 0;" frameborder="0"></iframe>
</div>
<div class="modal-footer">
<a href="#" class="btn btn-large" data-dismiss="modal">
<span class="icon-circle-arrow-left"></span>
{% trans "Back to history view" %}
</a>
{% if article|can_write:user %}
<a href="#" class="btn btn-large btn-primary switch-to-revision">
<span class="icon-flag"></span>
{% endif %}
</div>
<div class="modal hide fade" id="previewModal" style="width: 80%; margin-left: -40%;">
<div class="modal-body">
<iframe name="previewWindow" style="width: 100%; height: 100%; border: 0;" frameborder="0"></iframe>
</div>
<div class="modal-footer">
<a href="#" class="btn btn-large" data-dismiss="modal">
<span class="icon-circle-arrow-left"></span>
{% trans "Back to history view" %}
</a>
{% if article|can_write:user %}
<a href="#" class="btn btn-large btn-primary switch-to-revision">
<span class="icon-flag"></span>
{% trans "Switch to this version" %}
</a>
{% else %}
<a href="#" class="btn btn-large btn-primary disabled">
<span class="icon-lock"></span>
{% trans "Switch to this version" %}
</a>
{% else %}
<a href="#" class="btn btn-large btn-primary disabled">
<span class="icon-lock"></span>
{% trans "Switch to this version" %}
</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
<div class="modal hide fade" id="mergeModal" style="width: 80%; margin-left: -40%;">
<div class="modal-header">
<h1>{% trans "Merge with current" %}</h1>
<p class="lead"><span class="icon-info-sign"></span> {% trans "When you merge a revision with the current, all data will be retained from both versions and merged at its approximate location from each revision." %} <strong>{% trans "After this, it's important to do a manual review." %}</strong></p>
</div>
<div class="modal-body">
<iframe name="mergeWindow" style="width: 100%; height: 100%; border: 0;" frameborder="0"></iframe>
</div>
<div class="modal-footer">
<a href="#" class="btn btn-large" data-dismiss="modal">
<span class="icon-circle-arrow-left"></span>
{% trans "Back to history view" %}
</a>
{% if article|can_write:user %}
<a href="#" class="btn btn-large btn-primary merge-revision-commit">
<span class="icon-file"></span>
<div class="modal hide fade" id="mergeModal" style="width: 80%; margin-left: -40%;">
<div class="modal-header">
<h1>{% trans "Merge with current" %}</h1>
<p class="lead"><span class="icon-info-sign"></span> {% trans "When you merge a revision with the current, all data will be retained from both versions and merged at its approximate location from each revision." %} <strong>{% trans "After this, it's important to do a manual review." %}</strong></p>
</div>
<div class="modal-body">
<iframe name="mergeWindow" style="width: 100%; height: 100%; border: 0;" frameborder="0"></iframe>
</div>
<div class="modal-footer">
<a href="#" class="btn btn-large" data-dismiss="modal">
<span class="icon-circle-arrow-left"></span>
{% trans "Back to history view" %}
</a>
{% if article|can_write:user %}
<a href="#" class="btn btn-large btn-primary merge-revision-commit">
<span class="icon-file"></span>
{% trans "Create new merged version" %}
</a>
{% else %}
<a href="#" class="btn btn-large btn-primary disabled">
<span class="icon-lock"></span>
{% trans "Create new merged version" %}
</a>
{% else %}
<a href="#" class="btn btn-large btn-primary disabled">
<span class="icon-lock"></span>
{% trans "Create new merged version" %}
</a>
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
</form>
</div>
</div>
</form>
</div>
<div class="tabbable tabs-below" style="margin-top: 20px;">
<div style="clear:both"></div>
<div class="tabbable tabs-below" style="margin-top: 30px;">
<ul class="nav nav-tabs">
<li style="margin-top: 10px;"><em>{% trans "Article last modified:" %} {{ article.current_revision.modified }}</em></li>
</ul>
......
......@@ -14,7 +14,7 @@ urlpatterns = patterns('',
url('^_revision/preview/(?P<article_id>\d+)/$', 'wiki.views.preview', name='preview_revision'),
url('^_revision/merge/(?P<article_id>\d+)/(?P<revision_id>\d+)/preview/$', 'wiki.views.merge', name='merge_revision_preview', kwargs={'preview': True}),
url('^(?P<path>.+/|)_create/$', views.Create.as_view(), name='create_url'),
url('^(?P<path>.+/|)_edit/$', 'wiki.views.edit', name='edit_url'),
url('^(?P<path>.+/|)_edit/$', views.Edit.as_view(), name='edit_url'),
url('^(?P<path>.+/|)_preview/$', 'wiki.views.preview', name='preview_url'),
url('^(?P<path>.+/|)_history/$', views.History.as_view(), name='history_url'),
url('^(?P<path>.+/|)_settings/$', views.Settings.as_view(), name='settings_url'),
......
......@@ -20,6 +20,7 @@ from wiki.decorators import get_article
from django.views.generic.base import TemplateView
from wiki.core import plugins_registry
from wiki.core.diff import simple_merge
@get_article(can_read=True)
def preview(request, article, urlpath=None, template_file="wiki/preview_inline.html"):
......@@ -55,39 +56,54 @@ def root(request, article, template_file="wiki/article.html", urlpath=None):
'article': article,})
return render_to_response(template_file, c)
@get_article(can_write=True)
def edit(request, article, template_file="wiki/edit.html", urlpath=None):
class Edit(FormView):
if request.method == 'POST':
edit_form = forms.EditForm(article.current_revision, request.POST)
if edit_form.is_valid():
revision = models.ArticleRevision()
revision.inherit_predecessor(article)
revision.title = edit_form.cleaned_data['title']
revision.content = edit_form.cleaned_data['content']
revision.user_message = edit_form.cleaned_data['summary']
if request.user:
revision.user = request.user
if settings.LOG_IPS_USERS:
revision.ip_address = request.META.get('REMOTE_ADDR', None)
elif settings.LOG_IPS_ANONYMOUS:
revision.ip_address = request.META.get('REMOTE_ADDR', None)
article.add_revision(revision)
messages.success(request, _(u'A new revision of the article was succesfully added.'))
if not urlpath is None:
return redirect("wiki:get_url", urlpath.path)
# TODO: Where to go if it's a different object? It's probably
# an ajax callback, so we don't care... but should perhaps return
# a status
return
else:
edit_form = forms.EditForm(article.current_revision)
form_class = forms.EditForm
template_name="wiki/edit.html"
c = RequestContext(request, {'article': article,
'urlpath': urlpath,
'edit_form': edit_form,
'editor': editors.editor})
return render_to_response(template_file, c)
@method_decorator(get_article(can_write=True))
def dispatch(self, request, article, *args, **kwargs):
self.urlpath = kwargs.pop('urlpath', None)
self.article = article
return super(Edit, self).dispatch(request, *args, **kwargs)
def get_form(self, form_class):
"""
Returns an instance of the form to be used in this view.
"""
return form_class(self.article.current_revision, **self.get_form_kwargs())
def form_valid(self, form):
revision = models.ArticleRevision()
revision.inherit_predecessor(self.article)
revision.title = form.cleaned_data['title']
revision.content = form.cleaned_data['content']
revision.user_message = form.cleaned_data['summary']
if self.request.user:
revision.user = self.request.user
if settings.LOG_IPS_USERS:
revision.ip_address = self.request.META.get('REMOTE_ADDR', None)
elif settings.LOG_IPS_ANONYMOUS:
revision.ip_address = self.request.META.get('REMOTE_ADDR', None)
self.article.add_revision(revision)
messages.success(self.request, _(u'A new revision of the article was succesfully added.'))
return self.get_success_url()
def get_success_url(self):
if not self.urlpath is None:
return redirect("wiki:get_url", self.urlpath.path)
# TODO: Where to go if it's a different object? It's probably
# an ajax callback, so we don't care... but should perhaps return
# a status
return
def get_context_data(self, **kwargs):
kwargs['urlpath'] = self.urlpath
kwargs['article'] = self.article
kwargs['edit_form'] = kwargs.pop('form', None)
kwargs['editor'] = editors.editor
return super(Edit, self).get_context_data(**kwargs)
class Create(FormView):
......@@ -104,7 +120,11 @@ class Create(FormView):
"""
Returns an instance of the form to be used in this view.
"""
return form_class(self.urlpath, **self.get_form_kwargs())
kwargs = self.get_form_kwargs()
initial = kwargs.get('initial', {})
initial['slug'] = self.request.GET.get('slug', None)
kwargs['initial'] = initial
return form_class(self.urlpath, **kwargs)
def form_valid(self, form):
user=None
......@@ -247,13 +267,10 @@ def merge(request, article, revision_id, urlpath=None, template_file="wiki/previ
revision = get_object_or_404(models.ArticleRevision, article=article, id=revision_id)
baseText = article.current_revision.content if article.current_revision else ""
newText = revision.content
differ = difflib.Differ(charjunk=difflib.IS_CHARACTER_JUNK)
diff = differ.compare(baseText.splitlines(1), newText.splitlines(1))
current_text = article.current_revision.content if article.current_revision else ""
new_text = revision.content
content = "".join([l[2:] for l in diff])
content = simple_merge(current_text, new_text)
# Save new revision
if not preview:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment