Change recommendations for titles

This commit is contained in:
Tobias Hößl 2018-03-07 16:36:30 +01:00
parent d073cbbf6f
commit 9f8dce6e34
No known key found for this signature in database
GPG Key ID: 1D780C7599C2D2A2
13 changed files with 426 additions and 85 deletions

View File

@ -21,6 +21,7 @@ Agenda:
Motions: Motions:
- New export dialog [#3185]. - New export dialog [#3185].
- New feature: Personal notes for motions [#3190, #3267, #3404]. - New feature: Personal notes for motions [#3190, #3267, #3404].
- New feature: Change recommendations for the title of a motion [#3626].
- Fixed issue when creating/deleting motion comment fields in the - Fixed issue when creating/deleting motion comment fields in the
settings [#3187]. settings [#3187].
- Fixed empty motion comment field in motion update form [#3194]. - Fixed empty motion comment field in motion update form [#3194].

View File

@ -280,8 +280,12 @@ class MotionChangeRecommendationSerializer(ModelSerializer):
'text', 'text',
'creation_time',) 'creation_time',)
def is_title_cr(self, data):
return int(data['line_from']) == 0 and int(data['line_to']) == 0
def validate(self, data): def validate(self, data):
if 'text' in data: # Change recommendations for titles are stored as plain-text, thus they don't need to be html-escaped
if 'text' in data and not self.is_title_cr(data):
data['text'] = validate_html(data['text']) data['text'] = validate_html(data['text'])
return data return data

View File

@ -4,7 +4,7 @@
border-radius: 3px; border-radius: 3px;
margin-bottom: 5px; margin-bottom: 5px;
margin-top: -15px; margin-top: -15px;
padding-top: 5px; padding: 5px 5px 0 5px;
h3 { h3 {
margin-top: 10px; margin-top: 10px;

View File

@ -106,16 +106,12 @@
left: 20px; left: 20px;
} }
.line-numbers-outside .os-line-number.selectable:hover:before, .line-numbers-outside .os-line-number.selected:before { @mixin addChangeRecommendationBtn {
cursor: pointer; cursor: pointer;
content: "\f067"; content: "\f067";
display: inline-block;
position: absolute;
width: 14px; width: 14px;
height: 14px; height: 14px;
border-radius: 0.25em; border-radius: 0.25em;
top: 4px;
left: 43px;
font-family: FontAwesome; font-family: FontAwesome;
font-size: 12px; font-size: 12px;
color: white; color: white;
@ -124,6 +120,55 @@
background-color: #337ab7; background-color: #337ab7;
} }
.line-numbers-outside .os-line-number.selectable:hover:before, .line-numbers-outside .os-line-number.selected:before {
position: absolute;
top: 4px;
left: 43px;
display: inline-block;
@include addChangeRecommendationBtn();
}
.motion-header {
.submenu {
position: relative;
z-index: 2;
}
.motion-title {
position: relative;
z-index: 1;
// Grab the left padding of the parent element to catch hover-events for the :before element
margin-left: -20px;
padding-left: 20px;
.change-title {
position: relative;
width: 0;
height: 0;
}
.change-title:before {
position: absolute;
top: 18px;
left: -17px;
@include addChangeRecommendationBtn();
display: none;
}
&:hover .change-title.selectable:before {
display: block;
}
.title-change-indicator {
background-color: #0333ff;
position: absolute;
width: 4px;
height: 32px;
left: 10px;
top: 5px;
cursor: pointer;
}
}
}
.tt_change_recommendation_create_help { .tt_change_recommendation_create_help {
display: none; display: none;
max-width: 150px; max-width: 150px;
@ -190,6 +235,13 @@
} }
} }
.diff-box-title {
margin-bottom: 10px;
.description {
font-weight: bold;
}
}
.motion-text-with-diffs.line-numbers-inline .diff-box, .motion-text-with-diffs.line-numbers-none .diff-box { .motion-text-with-diffs.line-numbers-inline .diff-box, .motion-text-with-diffs.line-numbers-none .diff-box {
margin-right: -220px; margin-right: -220px;
} }

View File

@ -272,6 +272,22 @@ angular.module('OpenSlidesApp.motions', [
title += this.getTitle(); title += this.getTitle();
return title; return title;
}, },
getTitleWithChanges: function (changeRecommendationMode, versionId) {
var titleChange = this.getTitleChangeRecommendation(versionId);
var title;
if (titleChange) {
if (changeRecommendationMode === "changed") {
title = titleChange.text;
} else if (changeRecommendationMode === 'agreed' && !titleChange.rejected) {
title = titleChange.text;
} else {
title = this.getTitle();
}
} else {
title = this.getTitle();
}
return title;
},
getSequentialNumber: function () { getSequentialNumber: function () {
var id = this.id + ''; var id = this.id + '';
var zeros = Math.max(0, OpenSlidesSettings.MOTION_IDENTIFIER_MIN_DIGITS - id.length); var zeros = Math.max(0, OpenSlidesSettings.MOTION_IDENTIFIER_MIN_DIGITS - id.length);
@ -359,7 +375,7 @@ angular.module('OpenSlidesApp.motions', [
_getTextWithChangeRecommendations: function (versionId, highlight, lineBreaks, statusCompareCb) { _getTextWithChangeRecommendations: function (versionId, highlight, lineBreaks, statusCompareCb) {
var lineLength = Config.get('motions_line_length').value, var lineLength = Config.get('motions_line_length').value,
html = this.getVersion(versionId).text, html = this.getVersion(versionId).text,
changes = this.getChangeRecommendations(versionId, 'DESC'); changes = this.getTextChangeRecommendations(versionId, 'DESC');
for (var i = 0; i < changes.length; i++) { for (var i = 0; i < changes.length; i++) {
var change = changes[i]; var change = changes[i];
@ -405,7 +421,7 @@ angular.module('OpenSlidesApp.motions', [
} }
break; break;
case 'diff': case 'diff':
var changes = this.getChangeRecommendations(versionId, 'ASC'); var changes = this.getTextChangeRecommendations(versionId, 'ASC');
text = ''; text = '';
for (var i = 0; i < changes.length; i++) { for (var i = 0; i < changes.length; i++) {
text += this.getTextBetweenChangeRecommendations(versionId, (i === 0 ? null : changes[i - 1]), changes[i], highlight); text += this.getTextBetweenChangeRecommendations(versionId, (i === 0 ? null : changes[i - 1]), changes[i], highlight);
@ -500,7 +516,7 @@ angular.module('OpenSlidesApp.motions', [
} }
return foundSomething; return foundSomething;
}, },
getChangeRecommendations: function (versionId, order) { getTextChangeRecommendations: function (versionId, order) {
/* /*
* Returns all change recommendations for this given version, sorted by line * Returns all change recommendations for this given version, sorted by line
* @param versionId * @param versionId
@ -516,8 +532,26 @@ angular.module('OpenSlidesApp.motions', [
orderBy: [ orderBy: [
['line_from', order] ['line_from', order]
] ]
}).filter(function(change) {
return change.isTextRecommendation();
}); });
}, },
getTitleChangeRecommendation: function (versionId) {
/**
* Returns the change recommendation affecting the title, or null
* @param versionId
* @returns MotionChangeRecommendation|null
*/
versionId = versionId || this.active_version;
var changes = MotionChangeRecommendation.filter({
where: {
motion_version_id: versionId,
line_from: 0,
line_to: 0
}
});
return (changes.length > 0 ? changes[0] : null);
},
hasAmendments: function () { hasAmendments: function () {
return DS.filter('motions/motion', {parent_id: this.id}).length > 0; return DS.filter('motions/motion', {parent_id: this.id}).length > 0;
}, },
@ -861,6 +895,12 @@ angular.module('OpenSlidesApp.motions', [
saveStatus: function() { saveStatus: function() {
this.DSSave(); this.DSSave();
}, },
isTitleRecommendation: function() {
return (this.line_from === 0 && this.line_to === 0);
},
isTextRecommendation: function() {
return (this.line_from !== 0 || this.line_to !== 0);
},
getDiff: function(motion, version, highlight) { getDiff: function(motion, version, highlight) {
var lineLength = Config.get('motions_line_length').value, var lineLength = Config.get('motions_line_length').value,
html = lineNumberingService.insertLineNumbers(motion.getVersion(version).text, lineLength), html = lineNumberingService.insertLineNumbers(motion.getVersion(version).text, lineLength),

View File

@ -49,7 +49,7 @@ angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx'])
// motions // motions
data.tableofcontents_translation = gettextCatalog.getString('Table of contents'); data.tableofcontents_translation = gettextCatalog.getString('Table of contents');
data.motions_list = getMotionShortData(motions); data.motions_list = getMotionShortData(motions, params);
data.no_motions = gettextCatalog.getString('No motions available.'); data.no_motions = gettextCatalog.getString('No motions available.');
return $q(function (resolve) { return $q(function (resolve) {
@ -77,11 +77,11 @@ angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx'])
return _.orderBy(categories, [sortKey]); return _.orderBy(categories, [sortKey]);
}; };
var getMotionShortData = function (motions) { var getMotionShortData = function (motions, params) {
return _.map(motions, function (motion) { return _.map(motions, function (motion) {
return { return {
identifier: motion.identifier || '', identifier: motion.identifier || '',
title: motion.getTitle(), title: motion.getTitleWithChanges(params.changeRecommendationMode),
}; };
}); });
}; };
@ -97,6 +97,7 @@ angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx'])
var sequential_enabled = Config.get('motions_export_sequential_number').value; var sequential_enabled = Config.get('motions_export_sequential_number').value;
// promises for create the actual motion data // promises for create the actual motion data
var promises = _.map(motions, function (motion) { var promises = _.map(motions, function (motion) {
var title = motion.getTitleWithChanges(params.changeRecommendationMode);
var text = params.include.text ? motion.getTextByMode(params.changeRecommendationMode, null, null, false) : ''; var text = params.include.text ? motion.getTextByMode(params.changeRecommendationMode, null, null, false) : '';
var reason = params.include.reason ? motion.getReason() : ''; var reason = params.include.reason ? motion.getReason() : '';
var comments = getMotionComments(motion, params.includeComments); var comments = getMotionComments(motion, params.includeComments);
@ -114,7 +115,7 @@ angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx'])
// Actual data // Actual data
id: motion.id, id: motion.id,
identifier: motion.identifier || '', identifier: motion.identifier || '',
title: motion.getTitle(), title: title,
submitters: params.include.submitters ? _.map(motion.submitters, function (submitter) { submitters: params.include.submitters ? _.map(motion.submitters, function (submitter) {
return submitter.get_full_name(); return submitter.get_full_name();
}).join(', ') : '', }).join(', ') : '',

View File

@ -43,7 +43,7 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
obj.editor = CKEDITOR.inline(selector, ckeditorOptions); obj.editor = CKEDITOR.inline(selector, ckeditorOptions);
obj.editor.on('change', function () { obj.editor.on('change', function () {
$timeout(function() { $timeout(function() {
if (obj.editor.getData() != obj.originalHtml) { if (obj.editor.getData() !== obj.originalHtml) {
obj.changed = true; obj.changed = true;
} else { } else {
obj.changed = false; obj.changed = false;
@ -183,11 +183,14 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
.factory('ChangeRecommendationCreate', [ .factory('ChangeRecommendationCreate', [
'ngDialog', 'ngDialog',
'ChangeRecommendationForm', 'ChangeRecommendationTitleForm',
function(ngDialog, ChangeRecommendationForm) { 'ChangeRecommendationTextForm',
function(ngDialog, ChangeRecommendationTitleForm, ChangeRecommendationTextForm) {
var MODE_INACTIVE = 0, var MODE_INACTIVE = 0,
MODE_SELECTING_FROM = 1, MODE_SELECTING_FROM = 1,
MODE_SELECTING_TO = 2; MODE_SELECTING_TO = 2,
TITLE_DUMMY_LINE_NUMBER = 0;
var obj = { var obj = {
mode: MODE_INACTIVE, mode: MODE_INACTIVE,
@ -200,7 +203,7 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
var $scope, motion, version; var $scope, motion, version;
obj._getAffectedLineNumbers = function () { obj._getAffectedLineNumbers = function () {
var changeRecommendations = motion.getChangeRecommendations(version), var changeRecommendations = motion.getTextChangeRecommendations(version.id),
affectedLines = []; affectedLines = [];
for (var i = 0; i < changeRecommendations.length; i++) { for (var i = 0; i < changeRecommendations.length; i++) {
var change = changeRecommendations[i]; var change = changeRecommendations[i];
@ -211,23 +214,29 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
return affectedLines; return affectedLines;
}; };
// startCreating is called right at the beginning after the users interacts with the text for the first time.
// This ensures all necessary nodes have been initialized
obj.startCreating = function () { obj.startCreating = function () {
if (obj.mode > MODE_SELECTING_FROM || !motion.isAllowed('can_manage')) { if (obj.mode > MODE_SELECTING_FROM || !motion.isAllowed('can_manage')) {
return; return;
} }
$(".tt_change_recommendation_create_help").removeClass("opened"); $(".tt_change_recommendation_create_help").removeClass("opened");
var $lineNumbers = $(".motion-text-original .os-line-number"); var $lineNumbers = $(".motion-text-original .os-line-number"),
$title = $(".motion-title .change-title");
if ($lineNumbers.filter(".selectable").length === 0) { if ($lineNumbers.filter(".selectable").length === 0) {
obj.mode = MODE_SELECTING_FROM; obj.mode = MODE_SELECTING_FROM;
var alreadyAffectedLines = obj._getAffectedLineNumbers(); var alreadyAffectedLines = obj._getAffectedLineNumbers();
$lineNumbers.each(function () { $lineNumbers.each(function () {
var $this = $(this), var $this = $(this),
lineNumber = $this.data("line-number"); lineNumber = $this.data("line-number");
if (alreadyAffectedLines.indexOf(lineNumber) == -1) { if (alreadyAffectedLines.indexOf(lineNumber) === -1) {
$(this).addClass("selectable"); $(this).addClass("selectable");
} }
}); });
if (alreadyAffectedLines.indexOf(TITLE_DUMMY_LINE_NUMBER) === -1) {
$title.addClass("selectable");
}
} }
}; };
@ -253,7 +262,7 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
$(".motion-text-original .os-line-number").each(function () { $(".motion-text-original .os-line-number").each(function () {
var $this = $(this); var $this = $(this);
if ($this.data("line-number") >= line && !foundCollission) { if ($this.data("line-number") >= line && !foundCollission) {
if (alreadyAffectedLines.indexOf($this.data("line-number")) == -1) { if (alreadyAffectedLines.indexOf($this.data("line-number")) === -1) {
$(this).addClass("selectable"); $(this).addClass("selectable");
} else { } else {
$(this).removeClass("selectable"); $(this).removeClass("selectable");
@ -268,18 +277,22 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
$(".tt_change_recommendation_create_help").css("top", tt_pos).addClass("opened"); $(".tt_change_recommendation_create_help").css("top", tt_pos).addClass("opened");
}; };
obj.titleClicked = function () {
ngDialog.open(ChangeRecommendationTitleForm.getCreateDialog(motion, version));
obj.mode = MODE_INACTIVE;
obj.lineFrom = 0;
obj.lineTo = 0;
$(".motion-text-original .os-line-number").removeClass("selected selectable");
obj.startCreating();
};
obj.setToLine = function (line) { obj.setToLine = function (line) {
if (line < obj.lineFrom) { if (line < obj.lineFrom) {
return; return;
} }
obj.mode = MODE_INACTIVE; obj.mode = MODE_INACTIVE;
obj.lineTo = line + 1; ngDialog.open(ChangeRecommendationTextForm.getCreateDialog(motion, version, obj.lineFrom, line + 1));
ngDialog.open(ChangeRecommendationForm.getCreateDialog(
motion,
version,
obj.lineFrom,
obj.lineTo
));
obj.lineFrom = 0; obj.lineFrom = 0;
obj.lineTo = 0; obj.lineTo = 0;
@ -288,19 +301,19 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
}; };
obj.lineClicked = function (ev) { obj.lineClicked = function (ev) {
if (obj.mode == MODE_INACTIVE) { if (obj.mode === MODE_INACTIVE) {
return; return;
} }
if (obj.mode == MODE_SELECTING_FROM) { if (obj.mode === MODE_SELECTING_FROM) {
obj.setFromLine($(ev.target).data("line-number")); obj.setFromLine($(ev.target).data("line-number"));
$(ev.target).addClass("selected"); $(ev.target).addClass("selected");
} else if (obj.mode == MODE_SELECTING_TO) { } else if (obj.mode === MODE_SELECTING_TO) {
obj.setToLine($(ev.target).data("line-number")); obj.setToLine($(ev.target).data("line-number"));
} }
}; };
obj.mouseOver = function (ev) { obj.mouseOver = function (ev) {
if (obj.mode != MODE_SELECTING_TO) { if (obj.mode !== MODE_SELECTING_TO) {
return; return;
} }
var hoverLine = $(ev.target).data("line-number"); var hoverLine = $(ev.target).data("line-number");
@ -316,31 +329,37 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
obj.setVersion = function (_motion, _version) { obj.setVersion = function (_motion, _version) {
motion = _motion; motion = _motion;
version = _version; version = motion.getVersion(_version);
}; };
obj.editDialog = function(change_recommendation) { obj.editTextDialog = function(change_recommendation) {
ngDialog.open(ChangeRecommendationForm.getEditDialog(change_recommendation)); ngDialog.open(ChangeRecommendationTextForm.getEditDialog(change_recommendation));
};
obj.editTitleDialog = function(change_recommendation) {
ngDialog.open(ChangeRecommendationTitleForm.getEditDialog(change_recommendation));
}; };
obj.init = function (_scope, _motion) { obj.init = function (_scope, _motion) {
$scope = _scope; $scope = _scope;
motion = _motion; motion = _motion;
version = $scope.version; version = motion.getVersion($scope.version);
var $content = $("#content"); var $content = $("#content");
$content.on("click", ".line-numbers-outside .os-line-number.selectable", obj.lineClicked); $content.on("click", ".line-numbers-outside .os-line-number.selectable", obj.lineClicked);
$content.on("click", ".motion-title .change-title.selectable", obj.titleClicked);
$content.on("click", obj.cancelCreating); $content.on("click", obj.cancelCreating);
$content.on("mouseover", ".line-numbers-outside .os-line-number.selectable", obj.mouseOver); $content.on("mouseover", ".line-numbers-outside .os-line-number.selectable", obj.mouseOver);
$content.on("mouseover", ".motion-text-original", obj.startCreating); $content.on("mouseover", ".motion-text-original, .motion-title", obj.startCreating);
$scope.$watch(function () { $scope.$watch(function () {
return $scope.change_recommendations.length; return $scope.change_recommendations.length;
}, function () { }, function () {
if (obj.mode == MODE_INACTIVE || obj.mode == MODE_SELECTING_FROM) { if (obj.mode === MODE_INACTIVE || obj.mode === MODE_SELECTING_FROM) {
// Recalculate the affected lines so we cannot select lines affected by a recommendation // Recalculate the affected lines so we cannot select lines affected by a recommendation
// that has just been created // that has just been created
$(".motion-text-original .os-line-number").removeClass("selected selectable"); $(".motion-text-original .os-line-number").removeClass("selected selectable");
$(".motion-title .change-title").removeClass("selected selectable");
obj.startCreating(); obj.startCreating();
} }
}); });
@ -353,9 +372,10 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
obj.destroy = function () { obj.destroy = function () {
var $content = $("#content"); var $content = $("#content");
$content.off("click", ".line-numbers-outside .os-line-number.selectable", obj.lineClicked); $content.off("click", ".line-numbers-outside .os-line-number.selectable", obj.lineClicked);
$content.off("click", ".motion-title .change-title.selectable", obj.titleClicked);
$content.off("click", obj.cancelCreating); $content.off("click", obj.cancelCreating);
$content.off("mouseover", ".line-numbers-outside .os-line-number.selectable", obj.mouseOver); $content.off("mouseover", ".line-numbers-outside .os-line-number.selectable", obj.mouseOver);
$content.off("mouseover", ".motion-text-original", obj.startCreating); $content.off("mouseover", ".motion-text-original, .motion-title", obj.startCreating);
}; };
return obj; return obj;

View File

@ -63,10 +63,8 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
// title // title
var identifier = motion.identifier ? ' ' + motion.identifier : ''; var identifier = motion.identifier ? ' ' + motion.identifier : '';
var title = PDFLayout.createTitle( var titlePlain = motion.getTitleWithChanges(params.changeRecommendationMode, motionVersion);
gettextCatalog.getString('Motion') + identifier + ': ' + var title = PDFLayout.createTitle(gettextCatalog.getString('Motion') + identifier + ': ' + titlePlain);
motion.getTitle(motionVersion)
);
// subtitle and sequential number // subtitle and sequential number
var subtitleLines = []; var subtitleLines = [];
@ -251,10 +249,15 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
} }
// summary of change recommendations (for motion diff version only) // 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 columnLineNumbers = [];
var columnChangeType = []; var columnChangeType = [];
angular.forEach(_.orderBy(motion.changeRecommendations, ['line_from']), function(change) { angular.forEach(_.orderBy(motion.changeRecommendations, ['line_from']), function(change) {
if (change.isTitleRecommendation()) {
columnLineNumbers.push(
gettextCatalog.getString('Title') + ': '
);
} else {
// line numbers column // line numbers column
var line; var line;
if (change.line_from >= change.line_to - 1) { if (change.line_from >= change.line_to - 1) {
@ -265,6 +268,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
columnLineNumbers.push( columnLineNumbers.push(
gettextCatalog.getString('Line') + ' ' + line + ': ' gettextCatalog.getString('Line') + ' ' + line + ': '
); );
}
// change type column // change type column
if (change.getType(motion.getVersion(motionVersion).text) === 0) { if (change.getType(motion.getVersion(motionVersion).text) === 0) {
columnChangeType.push(gettextCatalog.getString("Replacement")); columnChangeType.push(gettextCatalog.getString("Replacement"));
@ -322,7 +326,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
var motionTitle = function() { var motionTitle = function() {
if (params.include.text) { if (params.include.text) {
return [{ return [{
text: motion.getTitle(motionVersion), text: titlePlain,
style: 'heading3' style: 'heading3'
}]; }];
} }
@ -338,10 +342,20 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
} }
}; };
var escapeHtml = function(text) {
return text.replace(/&/, "&amp;").replace(/</, "&lt;").replace(/>/, "&gt;");
};
// motion text (with line-numbers) // motion text (with line-numbers)
var motionText = function() { var motionText = function() {
if (params.include.text) { if (params.include.text) {
var motionTextContent = motion.getTextByMode(params.changeRecommendationMode, motionVersion); var motionTextContent = '';
var titleChange = motion.getTitleChangeRecommendation();
if (params.changeRecommendationMode === 'diff' && titleChange) {
motionTextContent += '<p><strong>' + gettextCatalog.getString('New title') + ':</strong> ' +
escapeHtml(titleChange.text) + '</p>';
}
motionTextContent += motion.getTextByMode(params.changeRecommendationMode, motionVersion);
return converter.convertHTML(motionTextContent, params.lineNumberMode); return converter.convertHTML(motionTextContent, params.lineNumberMode);
} }
}; };

View File

@ -159,17 +159,88 @@ angular.module('OpenSlidesApp.motions.site', [
} }
]) ])
.factory('ChangeRecommendationForm', [ .factory('ChangeRecommendationTitleForm', [
'gettextCatalog', 'gettextCatalog',
'Editor', 'Editor',
'Config', 'Config',
function(gettextCatalog, Editor, Config) { function(gettextCatalog) {
return {
// ngDialog for motion form
getCreateDialog: function (motion, version) {
return {
template: 'static/templates/motions/change-recommendation-form.html',
controller: 'ChangeRecommendationTitleCreateCtrl',
className: 'ngdialog-theme-default wide-form',
closeByEscape: false,
closeByDocument: false,
resolve: {
motion: function() {
return motion;
},
version: function() {
return version;
}
}
};
},
getEditDialog: function(change) {
return {
template: 'static/templates/motions/change-recommendation-form.html',
controller: 'ChangeRecommendationTitleUpdateCtrl',
className: 'ngdialog-theme-default wide-form',
closeByEscape: false,
closeByDocument: false,
resolve: {
change: function() {
return change;
}
}
};
},
// angular-formly fields for motion form
getFormFields: function () {
return [
{
key: 'identifier',
type: 'input',
templateOptions: {
label: gettextCatalog.getString('Identifier')
},
hide: true
},
{
key: 'motion_version_id',
type: 'input',
templateOptions: {
label: gettextCatalog.getString('Motion')
},
hide: true
},
{
key: 'text',
type: 'input',
templateOptions: {
label: gettextCatalog.getString('New title'),
required: false
}
}
];
}
};
}
])
.factory('ChangeRecommendationTextForm', [
'gettextCatalog',
'Editor',
'Config',
function(gettextCatalog, Editor) {
return { return {
// ngDialog for motion form // ngDialog for motion form
getCreateDialog: function (motion, version, lineFrom, lineTo) { getCreateDialog: function (motion, version, lineFrom, lineTo) {
return { return {
template: 'static/templates/motions/change-recommendation-form.html', template: 'static/templates/motions/change-recommendation-form.html',
controller: 'ChangeRecommendationCreateCtrl', controller: 'ChangeRecommendationTextCreateCtrl',
className: 'ngdialog-theme-default wide-form', className: 'ngdialog-theme-default wide-form',
closeByEscape: false, closeByEscape: false,
closeByDocument: false, closeByDocument: false,
@ -192,7 +263,7 @@ angular.module('OpenSlidesApp.motions.site', [
getEditDialog: function(change) { getEditDialog: function(change) {
return { return {
template: 'static/templates/motions/change-recommendation-form.html', template: 'static/templates/motions/change-recommendation-form.html',
controller: 'ChangeRecommendationUpdateCtrl', controller: 'ChangeRecommendationTextUpdateCtrl',
className: 'ngdialog-theme-default wide-form', className: 'ngdialog-theme-default wide-form',
closeByEscape: false, closeByEscape: false,
closeByDocument: false, closeByDocument: false,
@ -1362,9 +1433,19 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.$watch(function () { $scope.$watch(function () {
return MotionChangeRecommendation.lastModified(); return MotionChangeRecommendation.lastModified();
}, function () { }, function () {
$scope.change_recommendations = MotionChangeRecommendation.filter({ $scope.change_recommendations = [];
$scope.title_change_recommendation = null;
MotionChangeRecommendation.filter({
'where': {'motion_version_id': {'==': motion.active_version}} 'where': {'motion_version_id': {'==': motion.active_version}}
}).forEach(function(change) {
if (change.isTextRecommendation()) {
$scope.change_recommendations.push(change);
}
if (change.isTitleRecommendation()) {
$scope.title_change_recommendation = change;
}
}); });
if ($scope.change_recommendations.length === 0) { if ($scope.change_recommendations.length === 0) {
$scope.setProjectionMode($scope.projectionModes[0]); $scope.setProjectionMode($scope.projectionModes[0]);
} }
@ -1399,6 +1480,8 @@ angular.module('OpenSlidesApp.motions.site', [
} }
webpageTitle += $scope.motion.getTitle(); webpageTitle += $scope.motion.getTitle();
WebpageTitle.updateTitle(webpageTitle); WebpageTitle.updateTitle(webpageTitle);
$scope.createChangeRecommendation.setVersion(motion, motion.active_version);
}); });
$scope.projectionModes = [ $scope.projectionModes = [
{mode: 'original', {mode: 'original',
@ -1783,27 +1866,26 @@ angular.module('OpenSlidesApp.motions.site', [
} }
]) ])
.controller('ChangeRecommendationUpdateCtrl', [ .controller('ChangeRecommendationTitleUpdateCtrl', [
'$scope', '$scope',
'MotionChangeRecommendation', 'MotionChangeRecommendation',
'ChangeRecommendationForm', 'ChangeRecommendationTitleForm',
'diffService', 'diffService',
'change', 'change',
'ErrorMessage', 'ErrorMessage',
function ($scope, MotionChangeRecommendation, ChangeRecommendationForm, diffService, change, ErrorMessage) { function ($scope, MotionChangeRecommendation, ChangeRecommendationTitleForm, diffService, change, ErrorMessage) {
$scope.alert = {}; $scope.alert = {};
$scope.model = angular.copy(change); $scope.model = angular.copy(change);
// get all form fields // get all form fields
$scope.formFields = ChangeRecommendationForm.getFormFields(change.line_from, change.line_to); $scope.formFields = ChangeRecommendationTitleForm.getFormFields();
// save motion // save motion
$scope.save = function (change) { $scope.save = function (change) {
change.text = diffService.removeDuplicateClassesInsertedByCkeditor(change.text);
// inject the changed change recommendation (copy) object back into DS store // inject the changed change recommendation (copy) object back into DS store
MotionChangeRecommendation.inject(change); MotionChangeRecommendation.inject(change);
// save changed change recommendation object on server // save changed change recommendation object on server
MotionChangeRecommendation.save(change).then( MotionChangeRecommendation.save(change).then(
function(success) { function() {
$scope.closeThisDialog(); $scope.closeThisDialog();
}, },
function (error) { function (error) {
@ -1815,22 +1897,87 @@ angular.module('OpenSlidesApp.motions.site', [
} }
]) ])
.controller('ChangeRecommendationCreateCtrl', [ .controller('ChangeRecommendationTitleCreateCtrl', [
'$scope', '$scope',
'Motion', 'Motion',
'MotionChangeRecommendation', 'MotionChangeRecommendation',
'ChangeRecommendationForm', 'ChangeRecommendationTitleForm',
'Config',
'diffService',
'motion',
'version',
function($scope, Motion, MotionChangeRecommendation, ChangeRecommendationTitleForm, Config, diffService, motion,
version) {
$scope.alert = {};
$scope.model = {
text: version.title,
motion_version_id: version.id
};
// get all form fields
$scope.formFields = ChangeRecommendationTitleForm.getFormFields();
// save motion
$scope.save = function (change) {
change.line_from = 0;
change.line_to = 0;
MotionChangeRecommendation.create(change).then(
function() {
$scope.closeThisDialog();
}
);
};
}
])
.controller('ChangeRecommendationTextUpdateCtrl', [
'$scope',
'MotionChangeRecommendation',
'ChangeRecommendationTextForm',
'diffService',
'change',
'ErrorMessage',
function ($scope, MotionChangeRecommendation, ChangeRecommendationTextForm, diffService, change, ErrorMessage) {
$scope.alert = {};
$scope.model = angular.copy(change);
// get all form fields
$scope.formFields = ChangeRecommendationTextForm.getFormFields(change.line_from, change.line_to);
// save motion
$scope.save = function (change) {
change.text = diffService.removeDuplicateClassesInsertedByCkeditor(change.text);
// inject the changed change recommendation (copy) object back into DS store
MotionChangeRecommendation.inject(change);
// save changed change recommendation object on server
MotionChangeRecommendation.save(change).then(
function() {
$scope.closeThisDialog();
},
function (error) {
MotionChangeRecommendation.refresh(change);
$scope.alert = ErrorMessage.forAlert(error);
}
);
};
}
])
.controller('ChangeRecommendationTextCreateCtrl', [
'$scope',
'Motion',
'MotionChangeRecommendation',
'ChangeRecommendationTextForm',
'Config', 'Config',
'diffService', 'diffService',
'motion', 'motion',
'version', 'version',
'lineFrom', 'lineFrom',
'lineTo', 'lineTo',
function($scope, Motion, MotionChangeRecommendation, ChangeRecommendationForm, Config, diffService, motion, function($scope, Motion, MotionChangeRecommendation, ChangeRecommendationTextForm, Config, diffService, motion,
version, lineFrom, lineTo) { version, lineFrom, lineTo) {
$scope.alert = {}; $scope.alert = {};
var html = motion.getTextWithLineBreaks(version), var html = motion.getTextWithLineBreaks(version.id),
lineData = diffService.extractRangeByLineNumbers(html, lineFrom, lineTo); lineData = diffService.extractRangeByLineNumbers(html, lineFrom, lineTo);
$scope.model = { $scope.model = {
@ -1838,17 +1985,17 @@ angular.module('OpenSlidesApp.motions.site', [
lineData.html + lineData.innerContextEnd + lineData.outerContextEnd, lineData.html + lineData.innerContextEnd + lineData.outerContextEnd,
line_from: lineFrom, line_from: lineFrom,
line_to: lineTo, line_to: lineTo,
motion_version_id: version, motion_version_id: version.id,
type: 0 type: 0
}; };
// get all form fields // get all form fields
$scope.formFields = ChangeRecommendationForm.getFormFields(lineFrom, lineTo); $scope.formFields = ChangeRecommendationTextForm.getFormFields(lineFrom, lineTo);
// save motion // save motion
$scope.save = function (motion) { $scope.save = function (motion) {
motion.text = diffService.removeDuplicateClassesInsertedByCkeditor(motion.text); motion.text = diffService.removeDuplicateClassesInsertedByCkeditor(motion.text);
MotionChangeRecommendation.create(motion).then( MotionChangeRecommendation.create(motion).then(
function(success) { function() {
$scope.closeThisDialog(); $scope.closeThisDialog();
} }
); );

View File

@ -1,4 +1,4 @@
<div class="header"> <div class="header motion-header">
<div class="title"> <div class="title">
<div class="submenu"> <div class="submenu">
<a ui-sref="motions.motion.list" class="btn btn-sm btn-default"> <a ui-sref="motions.motion.list" class="btn btn-sm btn-default">
@ -59,12 +59,21 @@
<translate>PDF</translate> <translate>PDF</translate>
</a> </a>
</div> </div>
<h1>
{{ motion.getTitle() }} <h1 class="motion-title">
<span class="title-change-indicator"
ng-if="viewChangeRecommendations.mode == 'original' && title_change_recommendation"
ng-click="viewChangeRecommendations.scrollToDiffBox(title_change_recommendation.id)"></span>
<span class="change-title"
ng-if="motion.isAllowed('update') && viewChangeRecommendations.mode == 'original' && !title_change_recommendation"></span>
<span>{{ motion.getTitleWithChanges(viewChangeRecommendations.mode) }}</span>
<i class="fa pointer" ng-class="motion.personalNote.star ? 'fa-star' : 'fa-star-o'" <i class="fa pointer" ng-class="motion.personalNote.star ? 'fa-star' : 'fa-star-o'"
ng-if="operator.user" ng-if="operator.user"
title="{{ 'Set as favorite' | translate }}" ng-click="toggleStar()"></i> title="{{ 'Set as favorite' | translate }}" ng-click="toggleStar()"></i>
</h1> </h1>
<div class="row"> <div class="row">
<div class="col-sm-6"> <div class="col-sm-6">
<h2> <h2>

View File

@ -11,7 +11,16 @@
<translate>Reject all change recommendations</translate> <translate>Reject all change recommendations</translate>
</button> </button>
<ul ng-if="change_recommendations.length > 0"> <ul ng-if="change_recommendations.length > 0 || title_change_recommendation">
<li ng-if="title_change_recommendation">
<a href='' ng-click="viewChangeRecommendations.scrollToDiffBox(title_change_recommendation.id)">
<span class="line-number"><translate>Title</translate>:</span>
<span class="operation"><translate>Replacement</translate></span>
<span class="status">
<translate ng-if="title_change_recommendation.rejected">Rejected</translate>
</span>
</a>
</li>
<li ng-repeat="change in (changes = (change_recommendations | filter:{motion_version_id:version}:true | orderBy: 'line_from')) "> <li ng-repeat="change in (changes = (change_recommendations | filter:{motion_version_id:version}:true | orderBy: 'line_from')) ">
<a href='' ng-click="viewChangeRecommendations.scrollToDiffBox(change.id)"> <a href='' ng-click="viewChangeRecommendations.scrollToDiffBox(change.id)">
<span ng-if="change.line_from >= change.line_to - 1" class="line-number"> <span ng-if="change.line_from >= change.line_to - 1" class="line-number">
@ -35,7 +44,7 @@
</li> </li>
</ul> </ul>
<div ng-if="change_recommendations.length == 0" class="no-changes"> <div ng-if="change_recommendations.length == 0 && !title_change_recommendation" class="no-changes">
<translate>No change recommendations yet</translate> <translate>No change recommendations yet</translate>
</div> </div>
</section> </section>

View File

@ -92,7 +92,7 @@
</div> </div>
<!-- View Modes (Original, Diff, Changed) --> <!-- View Modes (Original, Diff, Changed) -->
<div class="motion-toolbar" ng-if="change_recommendations.length > 0"> <div class="motion-toolbar" ng-if="change_recommendations.length > 0 || title_change_recommendation">
<div class="toolbar-left"> <div class="toolbar-left">
<!-- change recommendations for resonsive size medium/large (button group) --> <!-- change recommendations for resonsive size medium/large (button group) -->

View File

@ -1,4 +1,48 @@
<div ng-if="viewChangeRecommendations.mode == 'diff'"> <div ng-if="viewChangeRecommendations.mode == 'diff'">
<!-- The changed title -->
<div ng-if="title_change_recommendation" ng-class="motion.isAllowed('can_manage') ? 'diff-box' : ''"
class="diff-box-{{ title_change_recommendation.id }} diff-box-title clearfix">
<div class="action-row" ng-if="motion.isAllowed('can_manage')">
<div class="btn-group" data-toggle="buttons">
<label class="btn btn-sm btn-default" ng-class="{active: !title_change_recommendation.rejected}"
title="{{ 'Not rejected' | translate }}"
ng-click="title_change_recommendation.rejected = false; title_change_recommendation.saveStatus();">
<input type="radio" name="changeRecommendationRejected[{{ title_change_recommendation.id }}]" value="0"
ng-change="title_change_recommendation.saveStatus()" ng-model="change.rejected"
ng-checked="title_change_recommendation.rejected == false">
<i class="fa fa-thumbs-up"></i>
</label>
<label class="btn btn-sm btn-default" ng-class="{active: title_change_recommendation.rejected}"
title="{{ 'Rejected' | translate }}" ng-click="title_change_recommendation.rejected = true; title_change_recommendation.saveStatus();">
<input type="radio" name="changeRecommendationRejected[{{ title_change_recommendation.id }}]" value="1"
ng-change="title_change_recommendation.saveStatus()" ng-model="change.rejected"
ng-checked="title_change_recommendation.rejected == true">
<i class="fa fa-thumbs-down"></i>
</label>
</div>
<button class="btn btn-default btn-sm pull-right btn-delete"
ng-bootbox-confirm="{{ 'Are you sure you want to delete this change recommendation?' | translate }}"
ng-bootbox-confirm-action="viewChangeRecommendations.delete(title_change_recommendation.id)"
title="{{ 'Delete' | translate }}">
<i class="fa fa-trash"></i>
</button>
<button class="btn btn-default btn-sm pull-right btn-edit"
ng-click="createChangeRecommendation.editTitleDialog(title_change_recommendation)"
title="{{ 'Edit' | translate }}">
<i class="fa fa-pencil"></i>
</button>
</div>
<div class="status-row" ng-if="!motion.isAllowed('can_manage') && title_change_recommendation.rejected">
<translate>Rejected</translate>
</div>
<div class="motion-text motion-text-diff line-numbers-{{ lineNumberMode }}">
<div class="description"><translate>New title</translate>:</div>
<div>{{ title_change_recommendation.text }}</div>
</div>
</div>
<!-- The actual diff view --> <!-- The actual diff view -->
<div class="motion-text-with-diffs line-numbers-{{ lineNumberMode }}"> <div class="motion-text-with-diffs line-numbers-{{ lineNumberMode }}">
@ -31,7 +75,7 @@
<i class="fa fa-trash"></i> <i class="fa fa-trash"></i>
</button> </button>
<button class="btn btn-default btn-sm pull-right btn-edit" ng-click="createChangeRecommendation.editDialog(change)" <button class="btn btn-default btn-sm pull-right btn-edit" ng-click="createChangeRecommendation.editTextDialog(change)"
title="{{ 'Edit' | translate }}"> title="{{ 'Edit' | translate }}">
<i class="fa fa-pencil"></i> <i class="fa fa-pencil"></i>
</button> </button>