diff --git a/openslides/motion/models.py b/openslides/motion/models.py index 25260f71d..ac9a41854 100644 --- a/openslides/motion/models.py +++ b/openslides/motion/models.py @@ -15,6 +15,8 @@ from datetime import datetime +import difflib + from django.core.urlresolvers import reverse from django.db import models from django.db.models import Max @@ -496,6 +498,12 @@ class MotionVersion(models.Model): """Return True, if the version is the active version of a motion. Else: False.""" return self.active_version.exists() + def make_htmldiff(self, rev1, rev2): + """Return string of html diff between two strings (rev1 and rev2)""" + + diff = difflib.HtmlDiff(wrapcolumn=60) + return diff.make_table(rev1.splitlines(), rev2.splitlines()) + class MotionSubmitter(models.Model): """Save the submitter of a Motion.""" diff --git a/openslides/motion/static/styles/motion.css b/openslides/motion/static/styles/motion.css new file mode 100644 index 000000000..951f94309 --- /dev/null +++ b/openslides/motion/static/styles/motion.css @@ -0,0 +1,41 @@ +/** + * OpenSlides motion style + * + * :copyright: 2013 by OpenSlides team, see AUTHORS. + * :license: GNU GPL, see LICENSE for more details. + */ + +/* motion version diff table */ +table.diff, .diff_row { + font-size: 12px; + width: 100%; + border: 0px; +} +table.diff[rules][rules="groups"] > colgroup, table[rules][rules="groups"] > tfoot, table[rules][rules="groups"] > thead, table[rules][rules="groups"] > tbody { + border: none; +} +table.diff td { + padding: 0 !important; + border: none; +} +.diff_header { + background-color:#e0e0e0; +} +td.diff_header { + text-align:right; + display: none; +} +.diff_next { + background-color:#c0c0c0; + display: none; +} +.diff_add { + background-color:#aaffaa; +} +.diff_chg { + background-color:#ffff77; +} +.diff_sub { + background-color:#ffaaaa; +} + diff --git a/openslides/motion/templates/motion/motion_detail.html b/openslides/motion/templates/motion/motion_detail.html index 231854e97..3548beba8 100644 --- a/openslides/motion/templates/motion/motion_detail.html +++ b/openslides/motion/templates/motion/motion_detail.html @@ -56,14 +56,14 @@
- {% if motion.active_version.id != motion.version.id %} + {% if motion.version.version_number < motion.last_version.version_number %} {% trans "This is not the newest version." %} {% trans "Go to version" %} {{ motion.last_version.version_number }}. {% else %} {% trans "This is not the authorized version." %} {% trans "Go to version" %} {{ motion.active_version.version_number }}. {% endif %} - {% endif %} +

{% trans "Motion text" %}:

@@ -80,84 +80,60 @@
- {% for version in motion.versions.all %} - {% if forloop.first %} -

{% trans "Version history" %}:

- - - - - - - - - - {% endif %} - - + + + + + + + {% if forloop.last %} +
{% trans "Version" %}{% trans "Time" %}{% trans "Title" %}{% trans "Text" %}{% trans "Reason" %}
- {% if version == motion.active_version %} - - {% else %} - {% if perms.motion.can_manage_motion %} - - {% endif %} - {% if not version.rejected and version.id > motion.active_version.id and perms.motion.can_manage_motion %} - - {% endif %} + {% with versions=motion.versions.all %} + {% if versions|length > 1 %} + {% for version in versions %} + {% if forloop.first %} +

{% trans "Version history" %}:

+
+ + + + + + + + {% endif %} - {% if version.rejected %} - - {% endif %} - - - - - - - - {% if forloop.last %} -
#{% trans "Time" %}{% trans "Actions" %}
{{ version.version_number }}{{ version.creation_time }} - {% ifchanged %} - {{ version.title }} - {% else %} - [{% trans "unchanged" %}] - {% endifchanged %} - - {% ifchanged %} - {{ version.text|linebreaks }} - {% else %} - [{% trans "unchanged" %}] - {% endifchanged %} - - {% ifchanged %} - {{ version.reason|linebreaks }} - {% else %} - [{% trans "unchanged" %}] - {% endifchanged %} -
+
+ {% if version == motion.active_version %} + + {% else %} + {% if perms.motion.can_manage_motion %} + + {% endif %} + {% if not version.rejected and version.id > motion.active_version.id and perms.motion.can_manage_motion %} + + {% endif %} + {% endif %} + {% if version.rejected %} + + {% endif %} + {{ version.version_number }}{{ version.creation_time }} + + + + + + + {# TODO: add delete version function #} + + + +
+ + {% endif %} + {% endfor %} {% endif %} - {% endfor %} - - -
    -{% for motion_version in motion.versions.all %} -
  1. - {% if motion_version.id == motion.version.id %} - {{ motion_version }} - {% else %} - {{ motion_version }} - {% endif %} - {% if motion_version.active %} - (active) - {% endif %} - {% if motion_version.rejected %} - (rejected) - {% endif %} -
  2. -{% endfor %} -
- - + {% endwith %} {% if perms.motion.can_manage_motion %} diff --git a/openslides/motion/templates/motion/motion_diff.html b/openslides/motion/templates/motion/motion_diff.html new file mode 100644 index 000000000..65ceec68f --- /dev/null +++ b/openslides/motion/templates/motion/motion_diff.html @@ -0,0 +1,67 @@ +{% extends "base.html" %} + +{% load tags %} +{% load i18n %} +{% load staticfiles %} + +{% block title %}{{ block.super }} – {% trans "Motion" %} {{ motion.number }}{% endblock %} + +{% block header %} + +{% endblock %} + + +{% block content %} +

+ {{ motion.title }} +
+ + {% if motion.number != None %} + {% trans "Motion" %} {{ motion.number }}, + {% else %} + [{% trans "no number" %}], + {% endif %} + {% trans "Diff view" %} + + + + +

+ +{% if version_rev1 and version_rev2 %} + + + + + + + + {% if not version_rev1.text == version_rev2.text %} + + {% else %} + + + {% endif %} + + + + + {% if not version_rev1.reason == version_rev2.reason %} + + {% else %} + + + {% endif %} + +
{% trans "Version" %} {{ version_rev1.version_number }}
+ {% trans "created: " %} {{ version_rev1.creation_time }}
+

{{ version_rev1.title }}

+
{% trans "Version" %} {{ version_rev2.version_number }}
+ {% trans "created: " %} {{ version_rev1.creation_time }}
+

{{ version_rev2.title }}

+
{{ diff_text|safe }}{{ version_rev1.text }}{{ version_rev2.text }}

{% trans "Reason" %}:

{{ diff_reason|safe }}{{ version_rev1.reason }}{{ version_rev2.reason }}
+{% endif %} + +{% endblock %} diff --git a/openslides/motion/urls.py b/openslides/motion/urls.py index 1fd507ad1..946aab95b 100644 --- a/openslides/motion/urls.py +++ b/openslides/motion/urls.py @@ -55,6 +55,11 @@ urlpatterns = patterns('openslides.motion.views', name='motion_version_reject', ), + url(r'^(?P\d+)/diff/$', + 'version_diff', + name='motion_version_diff', + ), + url(r'^(?P\d+)/support/$', 'motion_support', name='motion_support', diff --git a/openslides/motion/views.py b/openslides/motion/views.py index c871b443a..27df47795 100644 --- a/openslides/motion/views.py +++ b/openslides/motion/views.py @@ -230,6 +230,38 @@ class VersionRejectView(GetVersionMixin, SingleObjectMixin, QuestionMixin, Redir version_reject = VersionRejectView.as_view() +class VersionDiffView(GetVersionMixin, DetailView): + """Show diff between two versions of a motion.""" + permission_required = 'motion.can_see_motion' + model = Motion + template_name = 'motion/motion_diff.html' + + def get_context_data(self, **kwargs): + """Return the template context with versions and html diff strings.""" + try: + rev1 = int(self.request.GET['rev1']) + rev2 = int(self.request.GET['rev2']) + version_rev1 = self.object.version.motion.versions.get(version_number=self.request.GET['rev1']) + version_rev2 = self.object.version.motion.versions.get(version_number=self.request.GET['rev2']) + diff_text = self.object.version.make_htmldiff(version_rev1.text, version_rev2.text) + diff_reason = self.object.version.make_htmldiff(version_rev1.reason, version_rev2.reason) + except (KeyError, ValueError, MotionVersion.DoesNotExist): + messages.error(self.request, _('At least one version number was not valid.')) + version_rev1 = None + version_rev2 = None + diff_text = None + diff_reason = None + context = super(VersionDiffView, self).get_context_data(**kwargs) + context.update({ + 'version_rev1': version_rev1, + 'version_rev2': version_rev2, + 'diff_text': diff_text, + 'diff_reason': diff_reason, + }) + return context + +version_diff = VersionDiffView.as_view() + class SupportView(SingleObjectMixin, RedirectView): """View to support or unsupport a motion.