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,\ ...@@ -5,6 +5,7 @@ from django.http import HttpResponse, HttpResponseForbidden,\
import models import models
from wiki.core.exceptions import NoRootURL from wiki.core.exceptions import NoRootURL
from django.shortcuts import redirect, get_object_or_404 from django.shortcuts import redirect, get_object_or_404
from django.core.urlresolvers import reverse
def json_view(func): def json_view(func):
def wrap(request, *a, **kw): def wrap(request, *a, **kw):
...@@ -32,9 +33,10 @@ def get_article(func=None, can_read=True, can_write=False): ...@@ -32,9 +33,10 @@ def get_article(func=None, can_read=True, can_write=False):
return redirect('wiki:root_create') return redirect('wiki:root_create')
except models.URLPath.DoesNotExist: except models.URLPath.DoesNotExist:
try: 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) 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: except models.URLPath.DoesNotExist:
return HttpResponseNotFound("This article was not found. This page should look nicer.") return HttpResponseNotFound("This article was not found. This page should look nicer.")
article = urlpath.article article = urlpath.article
......
...@@ -8,6 +8,7 @@ from wiki import models ...@@ -8,6 +8,7 @@ from wiki import models
from django.forms.util import flatatt from django.forms.util import flatatt
from django.utils.encoding import force_unicode from django.utils.encoding import force_unicode
from django.utils.html import escape, conditional_escape from django.utils.html import escape, conditional_escape
from wiki.core.diff import simple_merge
class CreateRoot(forms.Form): class CreateRoot(forms.Form):
...@@ -26,22 +27,47 @@ class EditForm(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.'), 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) 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) self.preview = kwargs.pop('preview', False)
if instance: self.initial_revision = current_revision
initial = {'content': instance.content, self.presumed_revision = None
'title': instance.title,} if current_revision:
initial = {'content': current_revision.content,
'title': current_revision.title,
'current_revision': current_revision.id}
initial.update(kwargs.get('initial', {})) 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 kwargs['initial'] = initial
self.instance = instance
super(EditForm, self).__init__(*args, **kwargs) super(EditForm, self).__init__(*args, **kwargs)
def clean(self): def clean(self):
cd = self.cleaned_data 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.')) raise forms.ValidationError(_(u'No changes made. Nothing to save.'))
return cd return cd
......
...@@ -121,6 +121,8 @@ class Article(models.Model): ...@@ -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 return ArticleForObject.objects.get(object_id=obj.id, content_type=ContentType.objects.get_for_model(obj)).article
def __unicode__(self): def __unicode__(self):
if self.current_revision:
return self.current_revision.title
return self.title return self.title
class Meta: class Meta:
......
...@@ -17,9 +17,9 @@ ...@@ -17,9 +17,9 @@
$(document).ready( $(document).ready(
function() { function() {
$('.accordion input[disabled!="disabled"][type="radio"]').first().attr('checked', 'true'); $('.accordion input[disabled!="disabled"][type="radio"]').first().attr('checked', 'true');
// Fix modal heights // Fix modal heights
$('.modal-body').css('height', $(window).height()*0.70 + 'px'); $('.modal-body').css('height', $(window).height()*0.70 + 'px');
$('.modal').css('max-height', $(window).height() + 'px'); $('.modal').css('max-height', $(window).height() + 'px');
}); });
</script> </script>
{% endaddtoblock %} {% endaddtoblock %}
...@@ -69,10 +69,8 @@ ...@@ -69,10 +69,8 @@
</h1> </h1>
</li> </li>
</ul> </ul>
<div class="tab-content"> <form method="GET">
<div class="tab-content" style="overflow: visible;">
<form method="GET">
{% for revision in revisions %} {% for revision in revisions %}
<div class="accordion" id="accordion{{ revision.revision_number }}"> <div class="accordion" id="accordion{{ revision.revision_number }}">
<div class="accordion-group"> <div class="accordion-group">
...@@ -103,7 +101,9 @@ ...@@ -103,7 +101,9 @@
{% trans "Show changes" %} {% trans "Show changes" %}
</a> </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 %}" /> <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>
<div style="clear: both"></div> <div style="clear: both"></div>
...@@ -133,7 +133,7 @@ ...@@ -133,7 +133,7 @@
{% include "wiki/includes/pagination.html" %} {% include "wiki/includes/pagination.html" %}
{% if revisions.count > 1 and not is_paginated %} {% if revisions.count > 1 %}
<div class="form-actions"> <div class="form-actions">
<div class="pull-right"> <div class="pull-right">
{% if article|can_write:user %} {% if article|can_write:user %}
...@@ -153,65 +153,64 @@ ...@@ -153,65 +153,64 @@
</button> </button>
</div> </div>
</div> </div>
<div class="modal hide fade" id="previewModal" style="width: 80%; margin-left: -40%;"> {% endif %}
<div class="modal-body">
<iframe name="previewWindow" style="width: 100%; height: 100%; border: 0;" frameborder="0"></iframe> </div>
</div> <div class="modal hide fade" id="previewModal" style="width: 80%; margin-left: -40%;">
<div class="modal-footer"> <div class="modal-body">
<a href="#" class="btn btn-large" data-dismiss="modal"> <iframe name="previewWindow" style="width: 100%; height: 100%; border: 0;" frameborder="0"></iframe>
<span class="icon-circle-arrow-left"></span> </div>
{% trans "Back to history view" %} <div class="modal-footer">
</a> <a href="#" class="btn btn-large" data-dismiss="modal">
{% if article|can_write:user %} <span class="icon-circle-arrow-left"></span>
<a href="#" class="btn btn-large btn-primary switch-to-revision"> {% trans "Back to history view" %}
<span class="icon-flag"></span> </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" %} {% trans "Switch to this version" %}
</a> </a>
{% else %} {% endif %}
<a href="#" class="btn btn-large btn-primary disabled">
<span class="icon-lock"></span>
{% trans "Switch to this version" %}
</a>
{% endif %}
</div>
</div> </div>
</div>
<div class="modal hide fade" id="mergeModal" style="width: 80%; margin-left: -40%;"> <div class="modal hide fade" id="mergeModal" style="width: 80%; margin-left: -40%;">
<div class="modal-header"> <div class="modal-header">
<h1>{% trans "Merge with current" %}</h1> <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> <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>
<div class="modal-body"> <div class="modal-body">
<iframe name="mergeWindow" style="width: 100%; height: 100%; border: 0;" frameborder="0"></iframe> <iframe name="mergeWindow" style="width: 100%; height: 100%; border: 0;" frameborder="0"></iframe>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a href="#" class="btn btn-large" data-dismiss="modal"> <a href="#" class="btn btn-large" data-dismiss="modal">
<span class="icon-circle-arrow-left"></span> <span class="icon-circle-arrow-left"></span>
{% trans "Back to history view" %} {% trans "Back to history view" %}
</a> </a>
{% if article|can_write:user %} {% if article|can_write:user %}
<a href="#" class="btn btn-large btn-primary merge-revision-commit"> <a href="#" class="btn btn-large btn-primary merge-revision-commit">
<span class="icon-file"></span> <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" %} {% trans "Create new merged version" %}
</a> </a>
{% else %} {% endif %}
<a href="#" class="btn btn-large btn-primary disabled">
<span class="icon-lock"></span>
{% trans "Create new merged version" %}
</a>
{% endif %}
</div>
</div> </div>
</div>
{% endif %} </form>
</form>
</div>
</div> </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"> <ul class="nav nav-tabs">
<li style="margin-top: 10px;"><em>{% trans "Article last modified:" %} {{ article.current_revision.modified }}</em></li> <li style="margin-top: 10px;"><em>{% trans "Article last modified:" %} {{ article.current_revision.modified }}</em></li>
</ul> </ul>
......
...@@ -14,7 +14,7 @@ urlpatterns = patterns('', ...@@ -14,7 +14,7 @@ urlpatterns = patterns('',
url('^_revision/preview/(?P<article_id>\d+)/$', 'wiki.views.preview', name='preview_revision'), 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('^_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>.+/|)_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>.+/|)_preview/$', 'wiki.views.preview', name='preview_url'),
url('^(?P<path>.+/|)_history/$', views.History.as_view(), name='history_url'), url('^(?P<path>.+/|)_history/$', views.History.as_view(), name='history_url'),
url('^(?P<path>.+/|)_settings/$', views.Settings.as_view(), name='settings_url'), url('^(?P<path>.+/|)_settings/$', views.Settings.as_view(), name='settings_url'),
......
...@@ -20,6 +20,7 @@ from wiki.decorators import get_article ...@@ -20,6 +20,7 @@ from wiki.decorators import get_article
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from wiki.core import plugins_registry from wiki.core import plugins_registry
from wiki.core.diff import simple_merge
@get_article(can_read=True) @get_article(can_read=True)
def preview(request, article, urlpath=None, template_file="wiki/preview_inline.html"): 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): ...@@ -55,39 +56,54 @@ def root(request, article, template_file="wiki/article.html", urlpath=None):
'article': article,}) 'article': article,})
return render_to_response(template_file, c) return render_to_response(template_file, c)
@get_article(can_write=True) class Edit(FormView):
def edit(request, article, template_file="wiki/edit.html", urlpath=None):
if request.method == 'POST': form_class = forms.EditForm
edit_form = forms.EditForm(article.current_revision, request.POST) template_name="wiki/edit.html"
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)
c = RequestContext(request, {'article': article, @method_decorator(get_article(can_write=True))
'urlpath': urlpath, def dispatch(self, request, article, *args, **kwargs):
'edit_form': edit_form, self.urlpath = kwargs.pop('urlpath', None)
'editor': editors.editor}) self.article = article
return render_to_response(template_file, c) 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): class Create(FormView):
...@@ -104,7 +120,11 @@ class Create(FormView): ...@@ -104,7 +120,11 @@ class Create(FormView):
""" """
Returns an instance of the form to be used in this view. 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): def form_valid(self, form):
user=None user=None
...@@ -247,13 +267,10 @@ def merge(request, article, revision_id, urlpath=None, template_file="wiki/previ ...@@ -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) revision = get_object_or_404(models.ArticleRevision, article=article, id=revision_id)
baseText = article.current_revision.content if article.current_revision else "" current_text = article.current_revision.content if article.current_revision else ""
newText = revision.content new_text = revision.content
differ = difflib.Differ(charjunk=difflib.IS_CHARACTER_JUNK)
diff = differ.compare(baseText.splitlines(1), newText.splitlines(1))
content = "".join([l[2:] for l in diff]) content = simple_merge(current_text, new_text)
# Save new revision # Save new revision
if not preview: 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