From e073084f74af0fbcac0e38d22d6345501bd1fbb9 Mon Sep 17 00:00:00 2001 From: FinnStutzenstein Date: Fri, 13 Jul 2018 10:08:18 +0200 Subject: [PATCH] New temporal field for editing the final version of a motion. --- CHANGELOG.rst | 6 ++- ...09_motionversion_modified_final_version.py | 20 +++++++ openslides/motions/models.py | 52 +++++++++++++++---- openslides/motions/serializers.py | 11 +++- openslides/motions/static/js/motions/base.js | 34 +++++++++++- .../static/js/motions/motion-services.js | 48 +++++++++++++++++ openslides/motions/static/js/motions/pdf.js | 2 +- openslides/motions/static/js/motions/site.js | 33 ++++++++++-- .../templates/motions/motion-detail.html | 13 +++++ .../motions/motion-detail/toolbar.html | 27 +++++++++- .../motion-detail/view-modified-agreed.html | 28 ++++++++++ .../templates/motions/slide_motion.html | 6 +++ 12 files changed, 258 insertions(+), 22 deletions(-) create mode 100644 openslides/motions/migrations/0009_motionversion_modified_final_version.py create mode 100644 openslides/motions/static/templates/motions/motion-detail/view-modified-agreed.html diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 939fbb21e..55973fc71 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,9 +15,13 @@ Motions: - New feature to customize workflows and states [#3772]. - New config options to show logos on the right side in PDF [#3768]. - New table of contents with page numbers and categories in PDF [#3766]. - - Updated pdfMake to 0.1.37 [#3766]. + - New teporal field "modified final version" where the final version can + be edited [#3781]. + + Core: - Python 3.4 is not supported anymore [#3777]. - Support Python 3.7. + - Updated pdfMake to 0.1.37 [#3766]. Version 2.2 (2018-06-06) diff --git a/openslides/motions/migrations/0009_motionversion_modified_final_version.py b/openslides/motions/migrations/0009_motionversion_modified_final_version.py new file mode 100644 index 000000000..5aaca261c --- /dev/null +++ b/openslides/motions/migrations/0009_motionversion_modified_final_version.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.8 on 2018-07-13 08:02 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('motions', '0008_auto_20180702_1128'), + ] + + operations = [ + migrations.AddField( + model_name='motionversion', + name='modified_final_version', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/openslides/motions/models.py b/openslides/motions/models.py index f7d9e01bb..ae058553d 100644 --- a/openslides/motions/models.py +++ b/openslides/motions/models.py @@ -214,9 +214,10 @@ class Motion(RESTModelMixin, models.Model): * Else the given version is used. To create and use a new version object, you have to set it via the - use_version argument. You have to set the title, text/amendment_paragraphs and reason into - this version object before giving it to this save method. The properties - motion.title, motion.text, motion.amendment_paragraphs and motion.reason will be ignored. + use_version argument. You have to set the title, text/amendment_paragraphs, + modified final version and reason into this version object before giving it + to this save method. The properties motion.title, motion.text, + motion.amendment_paragraphs, motion.modified_final_version and motion.reason will be ignored. text and amendment_paragraphs are mutually exclusive; if both are given, amendment_paragraphs takes precedence. @@ -264,8 +265,8 @@ class Motion(RESTModelMixin, models.Model): return elif use_version is None: use_version = self.get_last_version() - # Save title, text, amendment paragraphs and reason into the version object. - for attr in ['title', 'text', 'amendment_paragraphs', 'reason']: + # Save title, text, amendment paragraphs, modified final version and reason into the version object. + for attr in ['title', 'text', 'amendment_paragraphs', 'modified_final_version', 'reason']: _attr = '_%s' % attr data = getattr(self, _attr, None) if data is not None: @@ -318,7 +319,8 @@ class Motion(RESTModelMixin, models.Model): """ Compare the version with the last version of the motion. - Returns True if the version data (title, text, reason) is different, + Returns True if the version data (title, text, amendment_paragraphs, + modified_final_version, reason) is different, else returns False. """ if not self.versions.exists(): @@ -326,7 +328,7 @@ class Motion(RESTModelMixin, models.Model): return True last_version = self.get_last_version() - for attr in ['title', 'text', 'amendment_paragraphs', 'reason']: + for attr in ['title', 'text', 'amendment_paragraphs', 'modified_final_version', 'reason']: if getattr(last_version, attr) != getattr(version, attr): return True return False @@ -494,6 +496,32 @@ class Motion(RESTModelMixin, models.Model): Is saved in a MotionVersion object. """ + def get_modified_final_version(self): + """ + Get the modified_final_version of the motion. + + Simular to get_title(). + """ + try: + return self._modified_final_version + except AttributeError: + return self.get_active_version().modified_final_version + + def set_modified_final_version(self, modified_final_version): + """ + Set the modified_final_version of the motion. + + Simular to set_title(). + """ + self._modified_final_version = modified_final_version + + modified_final_version = property(get_modified_final_version, set_modified_final_version) + """ + The modified_final_version for the motion. + + Is saved in a MotionVersion object. + """ + def get_reason(self): """ Get the reason of the motion. @@ -525,8 +553,9 @@ class Motion(RESTModelMixin, models.Model): Return a version object, not saved in the database. The version data of the new version object is populated with the data - set via motion.title, motion.text, motion.amendment_paragraphs and motion.reason if these data are - not given as keyword arguments. If the data is not set in the motion + set via motion.title, motion.text, motion.amendment_paragraphs, + motion.modified_final_version and motion.reason if these data are not + given as keyword arguments. If the data is not set in the motion attributes, it is populated with the data from the last version object if such object exists. """ @@ -539,7 +568,7 @@ class Motion(RESTModelMixin, models.Model): last_version = self.get_last_version() else: last_version = None - for attr in ['title', 'text', 'amendment_paragraphs', 'reason']: + for attr in ['title', 'text', 'amendment_paragraphs', 'modified_final_version', 'reason']: if attr in kwargs: continue _attr = '_%s' % attr @@ -840,6 +869,9 @@ class MotionVersion(RESTModelMixin, models.Model): amendment_paragraphs and text are mutually exclusive. """ + modified_final_version = models.TextField(null=True, blank=True) + """A field to copy in the final version of the motion and edit it there.""" + reason = models.TextField(null=True, blank=True) """The reason for a motion.""" diff --git a/openslides/motions/serializers.py b/openslides/motions/serializers.py index 9703a6aa1..e226501c0 100644 --- a/openslides/motions/serializers.py +++ b/openslides/motions/serializers.py @@ -317,6 +317,7 @@ class MotionVersionSerializer(ModelSerializer): 'title', 'text', 'amendment_paragraphs', + 'modified_final_version', 'reason',) @@ -369,6 +370,7 @@ class MotionSerializer(ModelSerializer): comments = MotionCommentsJSONSerializerField(required=False) log_messages = MotionLogSerializer(many=True, read_only=True) polls = MotionPollSerializer(many=True, read_only=True) + modified_final_version = CharField(allow_blank=True, required=False, write_only=True) reason = CharField(allow_blank=True, required=False, write_only=True) state_required_permission_to_see = SerializerMethodField() text = CharField(write_only=True, allow_blank=True) @@ -392,6 +394,7 @@ class MotionSerializer(ModelSerializer): 'title', 'text', 'amendment_paragraphs', + 'modified_final_version', 'reason', 'versions', 'active_version', @@ -419,6 +422,9 @@ class MotionSerializer(ModelSerializer): if 'text'in data: data['text'] = validate_html(data['text']) + if 'modified_final_version' in data: + data['modified_final_version'] = validate_html(data['modified_final_version']) + if 'reason' in data: data['reason'] = validate_html(data['reason']) @@ -451,6 +457,7 @@ class MotionSerializer(ModelSerializer): motion.title = validated_data['title'] motion.text = validated_data['text'] motion.amendment_paragraphs = validated_data.get('amendment_paragraphs') + motion.modified_final_version = validated_data.get('modified_final_version', '') motion.reason = validated_data.get('reason', '') motion.identifier = validated_data.get('identifier') motion.category = validated_data.get('category') @@ -489,8 +496,8 @@ class MotionSerializer(ModelSerializer): else: version = motion.get_last_version() - # Title, text, reason. - for key in ('title', 'text', 'amendment_paragraphs', 'reason'): + # Title, text, reason, ... + for key in ('title', 'text', 'amendment_paragraphs', 'modified_final_version', 'reason'): if key in validated_data.keys(): setattr(version, key, validated_data[key]) diff --git a/openslides/motions/static/js/motions/base.js b/openslides/motions/static/js/motions/base.js index f4183f6cd..d2741bb4c 100644 --- a/openslides/motions/static/js/motions/base.js +++ b/openslides/motions/static/js/motions/base.js @@ -322,7 +322,8 @@ angular.module('OpenSlidesApp.motions', [ if (titleChange) { if (changeRecommendationMode === "changed") { title = titleChange.text; - } else if (changeRecommendationMode === 'agreed' && !titleChange.rejected) { + } else if ((changeRecommendationMode === 'agreed' || + changeRecommendationMode === 'modified_agreed') && !titleChange.rejected) { title = titleChange.text; } else { title = this.getTitle(); @@ -349,6 +350,12 @@ angular.module('OpenSlidesApp.motions', [ return lineNumberingService.insertLineNumbers(html, lineLength, highlight, callback); }, + getModifiedFinalVersionWithLineBreaks: function (versionId) { + var lineLength = Config.get('motions_line_length').value, + html = this.getVersion(versionId).modified_final_version; + + return lineNumberingService.insertLineNumbers(html, lineLength); + }, getTextBetweenChanges: function (versionId, change1, change2, highlight) { var line_from = (change1 ? change1.line_to : 1), line_to = (change2 ? change2.line_from : null); @@ -490,7 +497,7 @@ angular.module('OpenSlidesApp.motions', [ }, getTextByMode: function(mode, versionId, highlight, lineBreaks) { /* - * @param mode ['original', 'diff', 'changed', 'agreed'] + * @param mode ['original', 'diff', 'changed', 'agreed', 'modified_agreed'] * @param versionId [if undefined, active_version will be used] * @param highlight [the line number to highlight] * @param lineBreaks [if line numbers / breaks should be included in the result] @@ -547,6 +554,17 @@ angular.module('OpenSlidesApp.motions', [ case 'agreed': text = this.getTextWithAgreedChanges(versionId, highlight, lineBreaks); break; + case 'modified_agreed': + text = this.getModifiedFinalVersion(versionId); + if (text) { + // Insert line numbers + var lineLength = Config.get('motions_line_length').value; + text = lineNumberingService.insertLineNumbers(text, lineLength); + } else { + // Use the agreed version as fallback + text = this.getTextByMode('agreed', versionId, highlight, lineBreaks); + } + break; } return text; }, @@ -855,6 +873,17 @@ angular.module('OpenSlidesApp.motions', [ setTextStrippingLineBreaks: function (text) { this.text = lineNumberingService.stripLineNumbers(text); }, + setModifiedFinalVersionStrippingLineBreaks: function (html) { + this.modified_final_version = lineNumberingService.stripLineNumbers(html); + }, + // Copies to final version to the modified_final_version field + copyModifiedFinalVersionStrippingLineBreaks: function () { + var finalVersion = this.getTextByMode('agreed'); + this.setModifiedFinalVersionStrippingLineBreaks(finalVersion); + }, + getModifiedFinalVersion: function (versionId) { + return this.getVersion(versionId).modified_final_version; + }, getReason: function (versionId) { return this.getVersion(versionId).reason; }, @@ -1457,6 +1486,7 @@ angular.module('OpenSlidesApp.motions', [ // Properties that are guaranteed to be constant this._change_object = { "type": "recommendation", + "other_description": recommendation.other_description, "id": "recommendation-" + recommendation.id, "original": recommendation, "saveStatus": function () { diff --git a/openslides/motions/static/js/motions/motion-services.js b/openslides/motions/static/js/motions/motion-services.js index c0e58e611..f4b6a32ef 100644 --- a/openslides/motions/static/js/motions/motion-services.js +++ b/openslides/motions/static/js/motions/motion-services.js @@ -472,6 +472,54 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions', }); }; + obj.copyToModifiedFinalVersion = function (motion, version) { + if (!motion.isAllowed('update')) { + throw 'No permission to update motion'; + } + + motion.copyModifiedFinalVersionStrippingLineBreaks(); + + Motion.inject(motion); + // save change motion object on server + Motion.save(motion, {method: 'PATCH'}).then(null, function (error) { + // save error: revert all changes by restore + // (refresh) original motion object from server + Motion.refresh(motion); + var message = ''; + for (var e in error.data) { + message += e + ': ' + error.data[e] + ' '; + } + $scope.alert = {type: 'danger', msg: message, show: true}; + }); + }; + + obj.deleteModifiedFinalVersion = function (motion, version) { + if (!motion.isAllowed('update')) { + throw 'No permission to update motion'; + } + + if (!motion.getModifiedFinalVersion(version)) { + return; + } + + motion.modified_final_version = ''; + + Motion.inject(motion); + // save change motion object on server + Motion.save(motion, {method: 'PATCH'}).then(function (success) { + $scope.viewChangeRecommendations.mode = 'agreed'; + }, function (error) { + // save error: revert all changes by restore + // (refresh) original motion object from server + Motion.refresh(motion); + var message = ''; + for (var e in error.data) { + message += e + ': ' + error.data[e] + ' '; + } + $scope.alert = {type: 'danger', msg: message, show: true}; + }); + }; + obj.newVersionIncludingChanges = function (motion, version) { if (!motion.isAllowed('update')) { throw 'No permission to update motion'; diff --git a/openslides/motions/static/js/motions/pdf.js b/openslides/motions/static/js/motions/pdf.js index ff574cdda..f7803f3bc 100644 --- a/openslides/motions/static/js/motions/pdf.js +++ b/openslides/motions/static/js/motions/pdf.js @@ -252,7 +252,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf']) } // summary of change recommendations (for motion diff version only) - if (params.changeRecommendationMode === "diff" && motion.changeRecommendations.length) { + if (params.changeRecommendationMode === 'diff' && motion.changeRecommendations.length) { var columnLineNumbers = []; var columnChangeType = []; angular.forEach(_.orderBy(motion.changeRecommendations, ['line_from']), function(change) { diff --git a/openslides/motions/static/js/motions/site.js b/openslides/motions/static/js/motions/site.js index ca1dab151..f35171f5e 100644 --- a/openslides/motions/static/js/motions/site.js +++ b/openslides/motions/static/js/motions/site.js @@ -938,7 +938,7 @@ angular.module('OpenSlidesApp.motions.site', [ {name: gettextCatalog.getString('Original version'), value: 'original'}, {name: gettextCatalog.getString('Changed version'), value: 'changed'}, {name: gettextCatalog.getString('Diff version'), value: 'diff'}, - {name: gettextCatalog.getString('Final version'), value: 'agreed'}, + {name: gettextCatalog.getString('Final version'), value: 'modified_agreed'}, ], }, hideExpression: "model.format !== 'pdf'", @@ -952,7 +952,7 @@ angular.module('OpenSlidesApp.motions.site', [ {name: gettextCatalog.getString('Original version'), value: 'original'}, {name: gettextCatalog.getString('Changed version'), value: 'changed'}, {name: gettextCatalog.getString('Diff version'), value: 'diff', disabled: true}, - {name: gettextCatalog.getString('Final version'), value: 'agreed'}, + {name: gettextCatalog.getString('Final version'), value: 'modified_agreed'}, ], }, hideExpression: "model.format === 'pdf'", @@ -1078,6 +1078,11 @@ angular.module('OpenSlidesApp.motions.site', [ }, includeComments: {}, }); + // Always change the mode from agreed to modified_agreed. If a motion does not have a modified + // final version, the agreed will be taken. + if ($scope.params.changeRecommendationMode === 'agreed') { + $scope.params.changeRecommendationMode = 'modified_agreed'; + } $scope.motions = motions; $scope.singleMotion = singleMotion; @@ -1614,10 +1619,16 @@ angular.module('OpenSlidesApp.motions.site', [ label: 'Diff version'}, {mode: 'agreed', label: 'Final version'}, + {mode: 'modified_agreed', + label: 'Modified final version'}, ]; - var motionDefaultTextMode = Config.get('motions_recommendation_text_mode').value; + var motionDefaultRecommendationTextMode = Config.get('motions_recommendation_text_mode').value; + // Change to the modified final version, if exists + if (motionDefaultRecommendationTextMode === 'agreed' && motion.getModifiedFinalVersion()) { + motionDefaultRecommendationTextMode = 'modified_agreed'; + } $scope.projectionMode = _.find($scope.projectionModes, function (mode) { - return mode.mode == motionDefaultTextMode; + return mode.mode == motionDefaultRecommendationTextMode; }); if (motion.isProjected().length) { var modeMapping = motion.isProjectedWithMode(); @@ -1919,6 +1930,17 @@ angular.module('OpenSlidesApp.motions.site', [ Config.get('motions_allow_disable_versioning').value); } ); + $scope.modifiedFinalVersionInlineEditing = MotionInlineEditing.createInstance($scope, motion, + 'view-modified-agreed-inline-editor', true, Editor.getOptions('inline'), + function (obj) { + return motion.getModifiedFinalVersionWithLineBreaks($scope.version); + }, + function (obj) { + motion.setModifiedFinalVersionStrippingLineBreaks(obj.editor.getData()); + motion.disable_versioning = (obj.trivialChange && + Config.get('motions_allow_disable_versioning').value); + } + ); // Wrapper functions for $scope.inlineEditing, to warn other users. var editingStoppedCallback; $scope.enableMotionInlineEditing = function () { @@ -1978,7 +2000,7 @@ angular.module('OpenSlidesApp.motions.site', [ // Change recommendation and amendment viewing $scope.viewChangeRecommendations = ChangeRecommendationView; - $scope.viewChangeRecommendations.initSite($scope, motion, Config.get('motions_recommendation_text_mode').value); + $scope.viewChangeRecommendations.initSite($scope, motion, motionDefaultRecommendationTextMode); // PDF creating functions $scope.pdfExport = function () { @@ -2083,6 +2105,7 @@ angular.module('OpenSlidesApp.motions.site', [ function ($scope, MotionChangeRecommendation, ChangeRecommendationTextForm, diffService, change, ErrorMessage) { $scope.alert = {}; $scope.model = angular.copy(change); + $scope.model._change_object = undefined; // get all form fields $scope.formFields = ChangeRecommendationTextForm.getFormFields(change.line_from, change.line_to); diff --git a/openslides/motions/static/templates/motions/motion-detail.html b/openslides/motions/static/templates/motions/motion-detail.html index b6b01236c..5a20c2d1a 100644 --- a/openslides/motions/static/templates/motions/motion-detail.html +++ b/openslides/motions/static/templates/motions/motion-detail.html @@ -555,6 +555,15 @@
+
+ +
+
+ + + + diff --git a/openslides/motions/static/templates/motions/motion-detail/toolbar.html b/openslides/motions/static/templates/motions/motion-detail/toolbar.html index 7520ba7cc..226c95954 100644 --- a/openslides/motions/static/templates/motions/motion-detail/toolbar.html +++ b/openslides/motions/static/templates/motions/motion-detail/toolbar.html @@ -1,5 +1,5 @@
- +
+ +
+ + +
+
@@ -86,6 +103,14 @@ ng-checked="viewChangeRecommendations.mode == 'agreed'"> Final version +
diff --git a/openslides/motions/static/templates/motions/motion-detail/view-modified-agreed.html b/openslides/motions/static/templates/motions/motion-detail/view-modified-agreed.html new file mode 100644 index 000000000..50aeb8468 --- /dev/null +++ b/openslides/motions/static/templates/motions/motion-detail/view-modified-agreed.html @@ -0,0 +1,28 @@ + +
+
+
+
+ +
+
+
The modified final version have been changed.
+ + + +
+
diff --git a/openslides/motions/static/templates/motions/slide_motion.html b/openslides/motions/static/templates/motions/slide_motion.html index 9dc60ab3a..7916ec22a 100644 --- a/openslides/motions/static/templates/motions/slide_motion.html +++ b/openslides/motions/static/templates/motions/slide_motion.html @@ -140,6 +140,12 @@ class="motion-text motion-text-changed line-numbers-{{ config('motions_default_line_numbering') }}">
+ +
+
+
+

Reason