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:
- New export dialog [#3185].
- 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
settings [#3187].
- Fixed empty motion comment field in motion update form [#3194].

View File

@ -280,8 +280,12 @@ class MotionChangeRecommendationSerializer(ModelSerializer):
'text',
'creation_time',)
def is_title_cr(self, data):
return int(data['line_from']) == 0 and int(data['line_to']) == 0
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'])
return data

View File

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

View File

@ -106,16 +106,12 @@
left: 20px;
}
.line-numbers-outside .os-line-number.selectable:hover:before, .line-numbers-outside .os-line-number.selected:before {
@mixin addChangeRecommendationBtn {
cursor: pointer;
content: "\f067";
display: inline-block;
position: absolute;
width: 14px;
height: 14px;
border-radius: 0.25em;
top: 4px;
left: 43px;
font-family: FontAwesome;
font-size: 12px;
color: white;
@ -124,6 +120,55 @@
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 {
display: none;
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 {
margin-right: -220px;
}

View File

@ -272,6 +272,22 @@ angular.module('OpenSlidesApp.motions', [
title += this.getTitle();
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 () {
var id = this.id + '';
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) {
var lineLength = Config.get('motions_line_length').value,
html = this.getVersion(versionId).text,
changes = this.getChangeRecommendations(versionId, 'DESC');
changes = this.getTextChangeRecommendations(versionId, 'DESC');
for (var i = 0; i < changes.length; i++) {
var change = changes[i];
@ -405,7 +421,7 @@ angular.module('OpenSlidesApp.motions', [
}
break;
case 'diff':
var changes = this.getChangeRecommendations(versionId, 'ASC');
var changes = this.getTextChangeRecommendations(versionId, 'ASC');
text = '';
for (var i = 0; i < changes.length; i++) {
text += this.getTextBetweenChangeRecommendations(versionId, (i === 0 ? null : changes[i - 1]), changes[i], highlight);
@ -500,7 +516,7 @@ angular.module('OpenSlidesApp.motions', [
}
return foundSomething;
},
getChangeRecommendations: function (versionId, order) {
getTextChangeRecommendations: function (versionId, order) {
/*
* Returns all change recommendations for this given version, sorted by line
* @param versionId
@ -516,8 +532,26 @@ angular.module('OpenSlidesApp.motions', [
orderBy: [
['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 () {
return DS.filter('motions/motion', {parent_id: this.id}).length > 0;
},
@ -861,6 +895,12 @@ angular.module('OpenSlidesApp.motions', [
saveStatus: function() {
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) {
var lineLength = Config.get('motions_line_length').value,
html = lineNumberingService.insertLineNumbers(motion.getVersion(version).text, lineLength),

View File

@ -49,7 +49,7 @@ angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx'])
// motions
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.');
return $q(function (resolve) {
@ -77,11 +77,11 @@ angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx'])
return _.orderBy(categories, [sortKey]);
};
var getMotionShortData = function (motions) {
var getMotionShortData = function (motions, params) {
return _.map(motions, function (motion) {
return {
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;
// promises for create the actual motion data
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 reason = params.include.reason ? motion.getReason() : '';
var comments = getMotionComments(motion, params.includeComments);
@ -114,7 +115,7 @@ angular.module('OpenSlidesApp.motions.docx', ['OpenSlidesApp.core.docx'])
// Actual data
id: motion.id,
identifier: motion.identifier || '',
title: motion.getTitle(),
title: title,
submitters: params.include.submitters ? _.map(motion.submitters, function (submitter) {
return submitter.get_full_name();
}).join(', ') : '',

View File

@ -43,7 +43,7 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
obj.editor = CKEDITOR.inline(selector, ckeditorOptions);
obj.editor.on('change', function () {
$timeout(function() {
if (obj.editor.getData() != obj.originalHtml) {
if (obj.editor.getData() !== obj.originalHtml) {
obj.changed = true;
} else {
obj.changed = false;
@ -183,11 +183,14 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
.factory('ChangeRecommendationCreate', [
'ngDialog',
'ChangeRecommendationForm',
function(ngDialog, ChangeRecommendationForm) {
'ChangeRecommendationTitleForm',
'ChangeRecommendationTextForm',
function(ngDialog, ChangeRecommendationTitleForm, ChangeRecommendationTextForm) {
var MODE_INACTIVE = 0,
MODE_SELECTING_FROM = 1,
MODE_SELECTING_TO = 2;
MODE_SELECTING_TO = 2,
TITLE_DUMMY_LINE_NUMBER = 0;
var obj = {
mode: MODE_INACTIVE,
@ -200,7 +203,7 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
var $scope, motion, version;
obj._getAffectedLineNumbers = function () {
var changeRecommendations = motion.getChangeRecommendations(version),
var changeRecommendations = motion.getTextChangeRecommendations(version.id),
affectedLines = [];
for (var i = 0; i < changeRecommendations.length; i++) {
var change = changeRecommendations[i];
@ -211,23 +214,29 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
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 () {
if (obj.mode > MODE_SELECTING_FROM || !motion.isAllowed('can_manage')) {
return;
}
$(".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) {
obj.mode = MODE_SELECTING_FROM;
var alreadyAffectedLines = obj._getAffectedLineNumbers();
$lineNumbers.each(function () {
var $this = $(this),
lineNumber = $this.data("line-number");
if (alreadyAffectedLines.indexOf(lineNumber) == -1) {
if (alreadyAffectedLines.indexOf(lineNumber) === -1) {
$(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 () {
var $this = $(this);
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");
} else {
$(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");
};
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) {
if (line < obj.lineFrom) {
return;
}
obj.mode = MODE_INACTIVE;
obj.lineTo = line + 1;
ngDialog.open(ChangeRecommendationForm.getCreateDialog(
motion,
version,
obj.lineFrom,
obj.lineTo
));
ngDialog.open(ChangeRecommendationTextForm.getCreateDialog(motion, version, obj.lineFrom, line + 1));
obj.lineFrom = 0;
obj.lineTo = 0;
@ -288,19 +301,19 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
};
obj.lineClicked = function (ev) {
if (obj.mode == MODE_INACTIVE) {
if (obj.mode === MODE_INACTIVE) {
return;
}
if (obj.mode == MODE_SELECTING_FROM) {
if (obj.mode === MODE_SELECTING_FROM) {
obj.setFromLine($(ev.target).data("line-number"));
$(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.mouseOver = function (ev) {
if (obj.mode != MODE_SELECTING_TO) {
if (obj.mode !== MODE_SELECTING_TO) {
return;
}
var hoverLine = $(ev.target).data("line-number");
@ -316,31 +329,37 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
obj.setVersion = function (_motion, _version) {
motion = _motion;
version = _version;
version = motion.getVersion(_version);
};
obj.editDialog = function(change_recommendation) {
ngDialog.open(ChangeRecommendationForm.getEditDialog(change_recommendation));
obj.editTextDialog = function(change_recommendation) {
ngDialog.open(ChangeRecommendationTextForm.getEditDialog(change_recommendation));
};
obj.editTitleDialog = function(change_recommendation) {
ngDialog.open(ChangeRecommendationTitleForm.getEditDialog(change_recommendation));
};
obj.init = function (_scope, _motion) {
$scope = _scope;
motion = _motion;
version = $scope.version;
version = motion.getVersion($scope.version);
var $content = $("#content");
$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("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 () {
return $scope.change_recommendations.length;
}, 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
// that has just been created
$(".motion-text-original .os-line-number").removeClass("selected selectable");
$(".motion-title .change-title").removeClass("selected selectable");
obj.startCreating();
}
});
@ -353,9 +372,10 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
obj.destroy = function () {
var $content = $("#content");
$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("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;

View File

@ -63,10 +63,8 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
// title
var identifier = motion.identifier ? ' ' + motion.identifier : '';
var title = PDFLayout.createTitle(
gettextCatalog.getString('Motion') + identifier + ': ' +
motion.getTitle(motionVersion)
);
var titlePlain = motion.getTitleWithChanges(params.changeRecommendationMode, motionVersion);
var title = PDFLayout.createTitle(gettextCatalog.getString('Motion') + identifier + ': ' + titlePlain);
// subtitle and sequential number
var subtitleLines = [];
@ -251,20 +249,26 @@ 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) {
// line numbers column
var line;
if (change.line_from >= change.line_to - 1) {
line = change.line_from;
if (change.isTitleRecommendation()) {
columnLineNumbers.push(
gettextCatalog.getString('Title') + ': '
);
} else {
line = change.line_from + ' - ' + (change.line_to - 1);
// line numbers column
var line;
if (change.line_from >= change.line_to - 1) {
line = change.line_from;
} else {
line = change.line_from + ' - ' + (change.line_to - 1);
}
columnLineNumbers.push(
gettextCatalog.getString('Line') + ' ' + line + ': '
);
}
columnLineNumbers.push(
gettextCatalog.getString('Line') + ' ' + line + ': '
);
// change type column
if (change.getType(motion.getVersion(motionVersion).text) === 0) {
columnChangeType.push(gettextCatalog.getString("Replacement"));
@ -322,7 +326,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
var motionTitle = function() {
if (params.include.text) {
return [{
text: motion.getTitle(motionVersion),
text: titlePlain,
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)
var motionText = function() {
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);
}
};

View File

@ -159,17 +159,88 @@ angular.module('OpenSlidesApp.motions.site', [
}
])
.factory('ChangeRecommendationForm', [
.factory('ChangeRecommendationTitleForm', [
'gettextCatalog',
'Editor',
'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 {
// ngDialog for motion form
getCreateDialog: function (motion, version, lineFrom, lineTo) {
return {
template: 'static/templates/motions/change-recommendation-form.html',
controller: 'ChangeRecommendationCreateCtrl',
controller: 'ChangeRecommendationTextCreateCtrl',
className: 'ngdialog-theme-default wide-form',
closeByEscape: false,
closeByDocument: false,
@ -192,7 +263,7 @@ angular.module('OpenSlidesApp.motions.site', [
getEditDialog: function(change) {
return {
template: 'static/templates/motions/change-recommendation-form.html',
controller: 'ChangeRecommendationUpdateCtrl',
controller: 'ChangeRecommendationTextUpdateCtrl',
className: 'ngdialog-theme-default wide-form',
closeByEscape: false,
closeByDocument: false,
@ -1362,9 +1433,19 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.$watch(function () {
return MotionChangeRecommendation.lastModified();
}, function () {
$scope.change_recommendations = MotionChangeRecommendation.filter({
$scope.change_recommendations = [];
$scope.title_change_recommendation = null;
MotionChangeRecommendation.filter({
'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) {
$scope.setProjectionMode($scope.projectionModes[0]);
}
@ -1399,6 +1480,8 @@ angular.module('OpenSlidesApp.motions.site', [
}
webpageTitle += $scope.motion.getTitle();
WebpageTitle.updateTitle(webpageTitle);
$scope.createChangeRecommendation.setVersion(motion, motion.active_version);
});
$scope.projectionModes = [
{mode: 'original',
@ -1783,27 +1866,26 @@ angular.module('OpenSlidesApp.motions.site', [
}
])
.controller('ChangeRecommendationUpdateCtrl', [
.controller('ChangeRecommendationTitleUpdateCtrl', [
'$scope',
'MotionChangeRecommendation',
'ChangeRecommendationForm',
'ChangeRecommendationTitleForm',
'diffService',
'change',
'ErrorMessage',
function ($scope, MotionChangeRecommendation, ChangeRecommendationForm, diffService, change, ErrorMessage) {
function ($scope, MotionChangeRecommendation, ChangeRecommendationTitleForm, diffService, change, ErrorMessage) {
$scope.alert = {};
$scope.model = angular.copy(change);
// get all form fields
$scope.formFields = ChangeRecommendationForm.getFormFields(change.line_from, change.line_to);
$scope.formFields = ChangeRecommendationTitleForm.getFormFields();
// 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(success) {
function() {
$scope.closeThisDialog();
},
function (error) {
@ -1815,22 +1897,87 @@ angular.module('OpenSlidesApp.motions.site', [
}
])
.controller('ChangeRecommendationCreateCtrl', [
.controller('ChangeRecommendationTitleCreateCtrl', [
'$scope',
'Motion',
'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',
'diffService',
'motion',
'version',
'lineFrom',
'lineTo',
function($scope, Motion, MotionChangeRecommendation, ChangeRecommendationForm, Config, diffService, motion,
function($scope, Motion, MotionChangeRecommendation, ChangeRecommendationTextForm, Config, diffService, motion,
version, lineFrom, lineTo) {
$scope.alert = {};
var html = motion.getTextWithLineBreaks(version),
var html = motion.getTextWithLineBreaks(version.id),
lineData = diffService.extractRangeByLineNumbers(html, lineFrom, lineTo);
$scope.model = {
@ -1838,17 +1985,17 @@ angular.module('OpenSlidesApp.motions.site', [
lineData.html + lineData.innerContextEnd + lineData.outerContextEnd,
line_from: lineFrom,
line_to: lineTo,
motion_version_id: version,
motion_version_id: version.id,
type: 0
};
// get all form fields
$scope.formFields = ChangeRecommendationForm.getFormFields(lineFrom, lineTo);
$scope.formFields = ChangeRecommendationTextForm.getFormFields(lineFrom, lineTo);
// save motion
$scope.save = function (motion) {
motion.text = diffService.removeDuplicateClassesInsertedByCkeditor(motion.text);
MotionChangeRecommendation.create(motion).then(
function(success) {
function() {
$scope.closeThisDialog();
}
);

View File

@ -1,4 +1,4 @@
<div class="header">
<div class="header motion-header">
<div class="title">
<div class="submenu">
<a ui-sref="motions.motion.list" class="btn btn-sm btn-default">
@ -59,13 +59,22 @@
<translate>PDF</translate>
</a>
</div>
<h1>
{{ motion.getTitle() }}
<i class="fa pointer" ng-class="motion.personalNote.star ? 'fa-star' : 'fa-star-o'"
ng-if="operator.user"
title="{{ 'Set as favorite' | translate }}" ng-click="toggleStar()"></i>
</h1>
<div class="row">
<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'"
ng-if="operator.user"
title="{{ 'Set as favorite' | translate }}" ng-click="toggleStar()"></i>
</h1>
<div class="row">
<div class="col-sm-6">
<h2>
<translate>Motion</translate> {{ motion.identifier }}

View File

@ -11,7 +11,16 @@
<translate>Reject all change recommendations</translate>
</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')) ">
<a href='' ng-click="viewChangeRecommendations.scrollToDiffBox(change.id)">
<span ng-if="change.line_from >= change.line_to - 1" class="line-number">
@ -35,7 +44,7 @@
</li>
</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>
</div>
</section>

View File

@ -92,7 +92,7 @@
</div>
<!-- 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">
<!-- change recommendations for resonsive size medium/large (button group) -->

View File

@ -1,4 +1,48 @@
<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 -->
<div class="motion-text-with-diffs line-numbers-{{ lineNumberMode }}">
@ -31,7 +75,7 @@
<i class="fa fa-trash"></i>
</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 }}">
<i class="fa fa-pencil"></i>
</button>