New temporal field for editing the final version of a motion.

This commit is contained in:
FinnStutzenstein 2018-07-13 10:08:18 +02:00
parent 15e4832d40
commit e073084f74
12 changed files with 258 additions and 22 deletions

View File

@ -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)

View File

@ -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),
),
]

View File

@ -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."""

View File

@ -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])

View File

@ -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 () {

View File

@ -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';

View File

@ -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) {

View File

@ -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);

View File

@ -555,6 +555,15 @@
<div ng-bind-html="motion.getTextByMode('agreed', version, highlight) | trusted"
class="motion-text motion-text-changed line-numbers-{{ lineNumberMode }}"></div>
<div style="text-align: right;" ng-if="(change_recommendations | filter:{motion_version_id:version}:true).length > 0">
<button class="btn btn-default"
ng-bootbox-confirm="{{ 'Do you want to copy the final version to the modified final version field?' | translate }}"
ng-bootbox-confirm-action="viewChangeRecommendations.copyToModifiedFinalVersion(motion, version)">
<i class="fa fa-file-text"></i>
<translate>Copy to modified final version</translate>
</button>
</div>
<div style="text-align: right;" ng-if="motion.state.versioning && (change_recommendations | filter:{motion_version_id:version}:true).length > 0">
<button class="btn btn-default"
ng-bootbox-confirm="{{ 'Do you want to create a new version of this motion based on this changes?' | translate }}"
@ -564,6 +573,10 @@
</button>
</div>
</div>
<!-- modified agreed view -->
<ng-include src="'static/templates/motions/motion-detail/view-modified-agreed.html'"></ng-include>
</div>
</div>

View File

@ -1,5 +1,5 @@
<div class="motion-toolbar">
<!-- inline editing -->
<!-- inline editing for original mode -->
<div class="pull-right inline-editing-activator"
ng-if="motion.isAllowed('update') && version == motion.getVersion(-1).id && viewChangeRecommendations.mode == 'original'">
<button ng-if="!inlineEditing.active && !has_proposed_changes" ng-click="enableMotionInlineEditing()"
@ -19,6 +19,23 @@
</button>
</div>
<!-- inline editing for modified agreed view -->
<div class="pull-right inline-editing-activator"
ng-if="motion.isAllowed('update') && version == motion.getVersion(-1).id && viewChangeRecommendations.mode == 'modified_agreed'">
<button ng-if="!modifiedFinalVersionInlineEditing.active"
ng-click="modifiedFinalVersionInlineEditing.enable()"
class="btn btn-sm btn-default">
<i class="fa fa-pencil-square-o"></i>
<translate>Inline editing</translate>
</button>
<button ng-if="modifiedFinalVersionInlineEditing.active"
ng-click="modifiedFinalVersionInlineEditing.disable()"
class="btn btn-sm btn-default">
<i class="fa fa-times-circle"></i>
<translate>Inline editing</translate>
</button>
</div>
<div class="toolbar-left {{ lineNumberMode }}">
<ng-include src="'static/templates/motions/motion-detail/toolbar-line-numbering.html'"></ng-include>
@ -86,6 +103,14 @@
ng-checked="viewChangeRecommendations.mode == 'agreed'">
<translate>Final version</translate>
</label>
<label class="btn btn-sm btn-default" ng-if="motion.getModifiedFinalVersion()"
ng-class="{active: (viewChangeRecommendations.mode == 'modified_agreed')}"
ng-click="viewChangeRecommendations.mode = 'modified_agreed'">
<input type="radio" name="viewChangeRecommendations.mode" value="modified_agreed"
ng-model="viewChangeRecommendations.mode"
ng-checked="viewChangeRecommendations.mode == 'modified_agreed'">
<translate>Modified final version</translate>
</label>
</div>
<!-- change recommendations for resonsive size small/extra small (dropdown) -->

View File

@ -0,0 +1,28 @@
<!-- Modified agreed view -->
<div ng-if="viewChangeRecommendations.mode == 'modified_agreed'">
<div id="view-modified-agreed-inline-editor" ng-bind-html="motion.getModifiedFinalVersionWithLineBreaks(version) | trusted"
class="motion-text motion-text-original line-numbers-{{ lineNumberMode }}"
contenteditable="{{ modifiedFinalVersionInlineEditing.isEditable }}">
</div>
<div style="text-align: right;">
<button class="btn btn-default btn-danger"
ng-bootbox-confirm="{{ 'Do you want to delete the modified final version?' | translate }}"
ng-bootbox-confirm-action="viewChangeRecommendations.deleteModifiedFinalVersion(motion, version)">
<i class="fa fa-trash"></i>
<translate>Delete modified final version</translate>
</button>
</div>
<div class="motion-save-toolbar" ng-class="{ 'visible': modifiedFinalVersionInlineEditing.active && modifiedFinalVersionInlineEditing.changed }">
<div class="changed-hint" translate>The modified final version have been changed.</div>
<button type="button" ng-click="modifiedFinalVersionInlineEditing.save()" class="btn btn-primary" translate>
Save
</button>
<button type="button" ng-click="modifiedFinalVersionInlineEditing.revert()" class="btn btn-default" translate>
Revert
</button>
<label ng-if="motion.state.versioning && config('motions_allow_disable_versioning')">
<input type="checkbox" ng-model="modifiedFinalVersionInlineEditing.trivialChange" value="1">
<span translate>Trivial change</span>
</label>
</div>
</div>

View File

@ -140,6 +140,12 @@
class="motion-text motion-text-changed line-numbers-{{ config('motions_default_line_numbering') }}"></div>
</div>
<!-- Modified agreed View -->
<div ng-if="mode == 'modified_agreed'">
<div ng-bind-html="motion.getTextByMode('modified_agreed', null, line) | trusted"
class="motion-text motion-text-changed line-numbers-{{ config('motions_default_line_numbering') }}"></div>
</div>
<!-- Reason -->
<div ng-if="motion.getReason() && !config('motions_disable_reason_on_projector')">
<h3 translate>Reason</h3>