Merge pull request #3637 from CatoTH/Paragraph-Based-Amendments

Paragraph based amendments / Diff
This commit is contained in:
Emanuel Schütze 2018-06-14 11:11:39 +02:00 committed by GitHub
commit 5a5475299c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 3619 additions and 951 deletions

View File

@ -10,6 +10,8 @@ Version 2.3 (unreleased)
Motions: Motions:
- New feature to scroll the projector to a specific line [#3748]. - New feature to scroll the projector to a specific line [#3748].
- New possibility to sort submitters [#3647]. - New possibility to sort submitters [#3647].
- New representation of amendments (paragraph based creation, new diff
and list views for amendments) [#3637].
Version 2.2 (2018-06-06) Version 2.2 (2018-06-06)

View File

@ -18,6 +18,10 @@
padding-right: 10px; padding-right: 10px;
} }
.col-space {
padding: 5px 7px 5px 7px;
}
// TODO: Isn't this defined in the _helper.scss? // TODO: Isn't this defined in the _helper.scss?
.centered { .centered {
text-align: center; text-align: center;

View File

@ -229,6 +229,11 @@ strong, b, th {
padding: 0 30px; padding: 0 30px;
} }
#content .containerOSExpanded {
height: 100%;
margin: 0 auto 0 auto;
padding: 0 20px;
}
/** Content **/ /** Content **/
#content { #content {

View File

@ -20,7 +20,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
PDFLayout.createTitle = function(title) { PDFLayout.createTitle = function(title) {
return { return {
text: title, text: title,
style: "title" style: 'title'
}; };
}; };
@ -28,7 +28,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
PDFLayout.createSubtitle = function(subtitle) { PDFLayout.createSubtitle = function(subtitle) {
return { return {
text: subtitle.join('\n'), text: subtitle.join('\n'),
style: "subtitle" style: 'subtitle'
}; };
}; };
@ -45,9 +45,9 @@ angular.module('OpenSlidesApp.core.pdf', [])
// table row style // table row style
PDFLayout.flipTableRowStyle = function(currentTableSize) { PDFLayout.flipTableRowStyle = function(currentTableSize) {
if (currentTableSize % 2 === 0) { if (currentTableSize % 2 === 0) {
return "tableEven"; return 'tableEven';
} else { } else {
return "tableOdd"; return 'tableOdd';
} }
}; };
@ -76,7 +76,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
BallotCircleDimensions.size) BallotCircleDimensions.size)
}, },
{ {
width: "auto", width: 'auto',
text: decision text: decision
} }
], ],
@ -92,7 +92,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
PDFLayout.imageURLtoBase64 = function(url) { PDFLayout.imageURLtoBase64 = function(url) {
var promise = new Promise(function(resolve, reject) { var promise = new Promise(function(resolve, reject) {
var img = new Image(); var img = new Image();
img.crossOrigin = "Anonymous"; img.crossOrigin = 'Anonymous';
img.onerror = function () { img.onerror = function () {
reject({ reject({
msg: '<i class="fa fa-exclamation-triangle fa-lg spacer-right"></i>' + msg: '<i class="fa fa-exclamation-triangle fa-lg spacer-right"></i>' +
@ -101,12 +101,12 @@ angular.module('OpenSlidesApp.core.pdf', [])
}); });
}; };
img.onload = function () { img.onload = function () {
var canvas = document.createElement("canvas"); var canvas = document.createElement('canvas');
canvas.width = img.width; canvas.width = img.width;
canvas.height = img.height; canvas.height = img.height;
var ctx = canvas.getContext("2d"); var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0); ctx.drawImage(img, 0, 0);
var dataURL = canvas.toDataURL("image/png"); var dataURL = canvas.toDataURL('image/png');
var imageData = { var imageData = {
data: dataURL, data: dataURL,
width: img.width, width: img.width,
@ -154,7 +154,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
}); });
return '<p>' + str + '</p>'; return '<p>' + str + '</p>';
} else { } else {
return ''; //needed for blank "reasons" field return ''; //needed for blank 'reasons' field
} }
}; };
return HTMLValidizer; return HTMLValidizer;
@ -460,25 +460,25 @@ angular.module('OpenSlidesApp.core.pdf', [])
*/ */
convertHTML = function(html, lineNumberMode) { convertHTML = function(html, lineNumberMode) {
var elementStyles = { var elementStyles = {
"b": ["font-weight:bold"], 'b': ['font-weight:bold'],
"strong": ["font-weight:bold"], 'strong': ['font-weight:bold'],
"u": ["text-decoration:underline"], 'u': ['text-decoration:underline'],
"em": ["font-style:italic"], 'em': ['font-style:italic'],
"i": ["font-style:italic"], 'i': ['font-style:italic'],
"h1": ["font-size:14", "font-weight:bold"], 'h1': ['font-size:14', 'font-weight:bold'],
"h2": ["font-size:12", "font-weight:bold"], 'h2': ['font-size:12', 'font-weight:bold'],
"h3": ["font-size:10", "font-weight:bold"], 'h3': ['font-size:10', 'font-weight:bold'],
"h4": ["font-size:10", "font-style:italic"], 'h4': ['font-size:10', 'font-style:italic'],
"h5": ["font-size:10"], 'h5': ['font-size:10'],
"h6": ["font-size:10"], 'h6': ['font-size:10'],
"a": ["color:blue", "text-decoration:underline"], 'a': ['color:blue', 'text-decoration:underline'],
"strike": ["text-decoration:line-through"], 'strike': ['text-decoration:line-through'],
"del": ["color:red", "text-decoration:line-through"], 'del': ['color:red', 'text-decoration:line-through'],
"ins": ["color:green", "text-decoration:underline"] 'ins': ['color:green', 'text-decoration:underline']
}, },
classStyles = { classStyles = {
"delete": ["color:red", "text-decoration:line-through"], 'delete': ['color:red', 'text-decoration:line-through'],
"insert": ["color:green", "text-decoration:underline"] 'insert': ['color:green', 'text-decoration:underline']
}, },
getLineNumber = function (element) { getLineNumber = function (element) {
if (element && element.nodeName == 'SPAN' && element.getAttribute('class') && if (element && element.nodeName == 'SPAN' && element.getAttribute('class') &&
@ -595,54 +595,54 @@ angular.module('OpenSlidesApp.core.pdf', [])
*/ */
ComputeStyle = function(o, styles) { ComputeStyle = function(o, styles) {
styles.forEach(function(singleStyle) { styles.forEach(function(singleStyle) {
var styleDefinition = singleStyle.trim().toLowerCase().split(":"); var styleDefinition = singleStyle.trim().toLowerCase().split(':');
var style = styleDefinition[0]; var style = styleDefinition[0];
var value = styleDefinition[1]; var value = styleDefinition[1];
if (styleDefinition.length === 2) { if (styleDefinition.length === 2) {
switch (style) { switch (style) {
case "padding-left": case 'padding-left':
o.margin = [parseInt(value), 0, 0, 0]; o.margin = [parseInt(value), 0, 0, 0];
break; break;
case "font-size": case 'font-size':
o.fontSize = parseInt(value); o.fontSize = parseInt(value);
break; break;
case "text-align": case 'text-align':
switch (value) { switch (value) {
case "right": case 'right':
case "center": case 'center':
case "justify": case 'justify':
o.alignment = value; o.alignment = value;
break; break;
} }
break; break;
case "font-weight": case 'font-weight':
switch (value) { switch (value) {
case "bold": case 'bold':
o.bold = true; o.bold = true;
break; break;
} }
break; break;
case "text-decoration": case 'text-decoration':
switch (value) { switch (value) {
case "underline": case 'underline':
o.decoration = "underline"; o.decoration = 'underline';
break; break;
case "line-through": case 'line-through':
o.decoration = "lineThrough"; o.decoration = 'lineThrough';
break; break;
} }
break; break;
case "font-style": case 'font-style':
switch (value) { switch (value) {
case "italic": case 'italic':
o.italics = true; o.italics = true;
break; break;
} }
break; break;
case "color": case 'color':
o.color = parseColor(value); o.color = parseColor(value);
break; break;
case "background-color": case 'background-color':
o.background = parseColor(value); o.background = parseColor(value);
break; break;
} }
@ -683,16 +683,16 @@ angular.module('OpenSlidesApp.core.pdf', [])
styles = styles ? _.clone(styles) : []; styles = styles ? _.clone(styles) : [];
var classes = []; var classes = [];
if (element.getAttribute) { if (element.getAttribute) {
var nodeStyle = element.getAttribute("style"); var nodeStyle = element.getAttribute('style');
if (nodeStyle) { if (nodeStyle) {
nodeStyle.split(";").forEach(function(nodeStyle) { nodeStyle.split(';').forEach(function(nodeStyle) {
var tmp = nodeStyle.replace(/\s/g, ''); var tmp = nodeStyle.replace(/\s/g, '');
styles.push(tmp); styles.push(tmp);
}); });
} }
var nodeClass = element.getAttribute("class"); var nodeClass = element.getAttribute('class');
if (nodeClass) { if (nodeClass) {
classes = nodeClass.toLowerCase().split(" "); classes = nodeClass.toLowerCase().split(' ');
classes.forEach(function(nodeClass) { classes.forEach(function(nodeClass) {
if (typeof(classStyles[nodeClass]) != 'undefined') { if (typeof(classStyles[nodeClass]) != 'undefined') {
classStyles[nodeClass].forEach(function(style) { classStyles[nodeClass].forEach(function(style) {
@ -710,37 +710,40 @@ angular.module('OpenSlidesApp.core.pdf', [])
} }
var nodeName = element.nodeName.toLowerCase(); var nodeName = element.nodeName.toLowerCase();
switch (nodeName) { switch (nodeName) {
case "h1": case 'h1':
case "h2": case 'h2':
case "h3": case 'h3':
case "h4": case 'h4':
case "h5": case 'h5':
case "h6": case 'h6':
if (lineNumberMode === "outside" && if (lineNumberMode === 'outside' &&
element.childNodes.length > 0 && element.childNodes.length > 0 &&
element.childNodes[0].getAttribute) { element.childNodes[0].getAttribute) {
// A heading may have multiple lines, so handle line by line separated by line number elements // A heading may have multiple lines, so handle line by line separated by line number elements
var outerStack = create("stack"); var outerStack = create('stack');
var currentCol; var currentCol, currentText;
_.forEach(element.childNodes, function (node) { _.forEach(element.childNodes, function (node) {
if (node.getAttribute && node.getAttribute('data-line-number')) { if (node.getAttribute && node.getAttribute('data-line-number')) {
if (currentCol) { if (currentCol) {
ComputeStyle(currentCol, elementStyles[nodeName]); ComputeStyle(currentCol, elementStyles[nodeName]);
outerStack.stack.push(currentCol); outerStack.stack.push(currentCol);
} }
currentText = create('text');
currentCol = { currentCol = {
columns: [ columns: [
getLineNumberObject({ getLineNumberObject({
lineNumber: node.getAttribute('data-line-number') lineNumber: node.getAttribute('data-line-number')
}), }),
currentText,
], ],
margin: [0, 2, 0, 0], margin: [0, 2, 0, 0],
}; };
} else if (node.textContent) { } else {
var HeaderText = { var parsedText = ParseElement([], node, create('text'), styles, diff_mode);
text: node.textContent, // append the parsed text to the currentText
}; _.forEach(parsedText.text, function (text) {
currentCol.columns.push(HeaderText); currentText.text.push(text);
});
} }
}); });
ComputeStyle(currentCol, elementStyles[nodeName]); ComputeStyle(currentCol, elementStyles[nodeName]);
@ -751,30 +754,30 @@ angular.module('OpenSlidesApp.core.pdf', [])
} }
alreadyConverted.push(outerStack); alreadyConverted.push(outerStack);
} else { } else {
currentParagraph = create("text"); currentParagraph = create('text');
currentParagraph.marginBottom = 4; currentParagraph.marginBottom = 4;
currentParagraph.marginTop = 10; currentParagraph.marginTop = 10;
currentParagraph = parseChildren(alreadyConverted, element, currentParagraph, styles.concat(elementStyles[nodeName]), diff_mode); currentParagraph = parseChildren(alreadyConverted, element, currentParagraph, styles.concat(elementStyles[nodeName]), diff_mode);
alreadyConverted.push(currentParagraph); alreadyConverted.push(currentParagraph);
} }
break; break;
case "a": case 'a':
case "b": case 'b':
case "strong": case 'strong':
case "u": case 'u':
case "em": case 'em':
case "i": case 'i':
case "ins": case 'ins':
case "del": case 'del':
case "strike": case 'strike':
currentParagraph = parseChildren(alreadyConverted, element, currentParagraph, styles.concat(elementStyles[nodeName]), diff_mode); currentParagraph = parseChildren(alreadyConverted, element, currentParagraph, styles.concat(elementStyles[nodeName]), diff_mode);
break; break;
case "table": case 'table':
var t = create("table", { var t = create('table', {
widths: [], widths: [],
body: [] body: []
}); });
var border = element.getAttribute("border"); var border = element.getAttribute('border');
var isBorder = false; var isBorder = false;
if (border) { if (border) {
isBorder = (parseInt(border) === 1); isBorder = (parseInt(border) === 1);
@ -782,58 +785,58 @@ angular.module('OpenSlidesApp.core.pdf', [])
t.layout = 'noBorders'; t.layout = 'noBorders';
} }
currentParagraph = parseChildren(t.table.body, element, currentParagraph, styles, diff_mode); currentParagraph = parseChildren(t.table.body, element, currentParagraph, styles, diff_mode);
var widths = element.getAttribute("widths"); var widths = element.getAttribute('widths');
if (!widths) { if (!widths) {
if (t.table.body.length !== 0) { if (t.table.body.length !== 0) {
if (t.table.body[0].length !== 0) if (t.table.body[0].length !== 0)
for (var k = 0; k < t.table.body[0].length; k++) for (var k = 0; k < t.table.body[0].length; k++)
t.table.widths.push("*"); t.table.widths.push('*');
} }
} else { } else {
var w = widths.split(","); var w = widths.split(',');
for (var ko = 0; ko < w.length; ko++) t.table.widths.push(w[ko]); for (var ko = 0; ko < w.length; ko++) t.table.widths.push(w[ko]);
} }
alreadyConverted.push(t); alreadyConverted.push(t);
break; break;
case "tbody": case 'tbody':
currentParagraph = parseChildren(alreadyConverted, element, currentParagraph, styles, diff_mode); currentParagraph = parseChildren(alreadyConverted, element, currentParagraph, styles, diff_mode);
break; break;
case "tr": case 'tr':
var row = []; var row = [];
currentParagraph = parseChildren(row, element, currentParagraph, styles, diff_mode); currentParagraph = parseChildren(row, element, currentParagraph, styles, diff_mode);
alreadyConverted.push(row); alreadyConverted.push(row);
break; break;
case "td": case 'td':
currentParagraph = create("text"); currentParagraph = create('text');
var st = create("stack"); var st = create('stack');
st.stack.push(currentParagraph); st.stack.push(currentParagraph);
var rspan = element.getAttribute("rowspan"); var rspan = element.getAttribute('rowspan');
if (rspan) if (rspan)
st.rowSpan = parseInt(rspan); st.rowSpan = parseInt(rspan);
var cspan = element.getAttribute("colspan"); var cspan = element.getAttribute('colspan');
if (cspan) if (cspan)
st.colSpan = parseInt(cspan); st.colSpan = parseInt(cspan);
currentParagraph = parseChildren(st.stack, element, currentParagraph, styles, diff_mode); currentParagraph = parseChildren(st.stack, element, currentParagraph, styles, diff_mode);
alreadyConverted.push(st); alreadyConverted.push(st);
break; break;
case "span": case 'span':
if (element.getAttribute("data-line-number")) { if (element.getAttribute('data-line-number')) {
if (lineNumberMode === "inline") { if (lineNumberMode === 'inline') {
if (diff_mode !== DIFF_MODE_INSERT) { if (diff_mode !== DIFF_MODE_INSERT) {
var lineNumberInline = element.getAttribute("data-line-number"), var lineNumberInline = element.getAttribute('data-line-number'),
lineNumberObjInline = { lineNumberObjInline = {
text: lineNumberInline, text: lineNumberInline,
color: "gray", color: 'gray',
fontSize: 5 fontSize: 5
}; };
currentParagraph.text.push(lineNumberObjInline); currentParagraph.text.push(lineNumberObjInline);
} }
} else if (lineNumberMode === "outside") { } else if (lineNumberMode === 'outside') {
var lineNumberOutline; var lineNumberOutline;
if (diff_mode === DIFF_MODE_INSERT) { if (diff_mode === DIFF_MODE_INSERT) {
lineNumberOutline = ""; lineNumberOutline = '';
} else { } else {
lineNumberOutline = element.getAttribute("data-line-number"); lineNumberOutline = element.getAttribute('data-line-number');
} }
var col = { var col = {
columns: [ columns: [
@ -842,7 +845,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
}), }),
] ]
}; };
currentParagraph = create("text"); currentParagraph = create('text');
currentParagraph.lineHeight = 1.25; currentParagraph.lineHeight = 1.25;
col.columns.push(currentParagraph); col.columns.push(currentParagraph);
alreadyConverted.push(col); alreadyConverted.push(col);
@ -852,7 +855,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
currentParagraph = parseChildren(alreadyConverted, element, currentParagraph, styles, diff_mode); currentParagraph = parseChildren(alreadyConverted, element, currentParagraph, styles, diff_mode);
} }
break; break;
case "br": case 'br':
var brParent = element.parentNode; var brParent = element.parentNode;
var brParentNodeName = brParent.nodeName; var brParentNodeName = brParent.nodeName;
//in case of no or inline-line-numbers and the ignore os-line-breaks. //in case of no or inline-line-numbers and the ignore os-line-breaks.
@ -860,10 +863,10 @@ angular.module('OpenSlidesApp.core.pdf', [])
hasClass(element, 'os-line-break')) { hasClass(element, 'os-line-break')) {
break; break;
} else { } else {
currentParagraph = create("text"); currentParagraph = create('text');
if (lineNumberMode === "outside" && if (lineNumberMode === 'outside' &&
brParentNodeName !== "LI" && brParentNodeName !== 'LI' &&
element.parentNode.parentNode.nodeName !== "LI") { element.parentNode.parentNode.nodeName !== 'LI') {
if (brParentNodeName === 'INS' || brParentNodeName === 'DEL') { if (brParentNodeName === 'INS' || brParentNodeName === 'DEL') {
var hasPrevSiblingALineNumber = function (element) { var hasPrevSiblingALineNumber = function (element) {
@ -896,11 +899,11 @@ angular.module('OpenSlidesApp.core.pdf', [])
alreadyConverted.push(currentParagraph); alreadyConverted.push(currentParagraph);
} }
break; break;
case "li": case 'li':
case "div": case 'div':
currentParagraph = create("text"); currentParagraph = create('text');
currentParagraph.lineHeight = 1.25; currentParagraph.lineHeight = 1.25;
var stackDiv = create("stack"); var stackDiv = create('stack');
if (_.indexOf(classes, 'os-split-before') > -1) { if (_.indexOf(classes, 'os-split-before') > -1) {
stackDiv.listType = 'none'; stackDiv.listType = 'none';
} }
@ -912,9 +915,9 @@ angular.module('OpenSlidesApp.core.pdf', [])
currentParagraph = parseChildren(stackDiv.stack, element, currentParagraph, [], diff_mode); currentParagraph = parseChildren(stackDiv.stack, element, currentParagraph, [], diff_mode);
alreadyConverted.push(stackDiv); alreadyConverted.push(stackDiv);
break; break;
case "p": case 'p':
var pObjectToPush; //determine what to push later var pObjectToPush; //determine what to push later
currentParagraph = create("text"); currentParagraph = create('text');
// If this element is inside a list (happens if copied from word), do not set spaces // If this element is inside a list (happens if copied from word), do not set spaces
// and margins. Just leave the paragraph there.. // and margins. Just leave the paragraph there..
if (!isInsideAList(element)) { if (!isInsideAList(element)) {
@ -927,19 +930,19 @@ angular.module('OpenSlidesApp.core.pdf', [])
} }
} }
currentParagraph.lineHeight = 1.25; currentParagraph.lineHeight = 1.25;
var stackP = create("stack"); var stackP = create('stack');
stackP.stack.push(currentParagraph); stackP.stack.push(currentParagraph);
ComputeStyle(stackP, styles); ComputeStyle(stackP, styles);
currentParagraph = parseChildren(stackP.stack, element, currentParagraph, [], diff_mode); currentParagraph = parseChildren(stackP.stack, element, currentParagraph, [], diff_mode);
pObjectToPush = stackP; //usually we want to push stackP pObjectToPush = stackP; //usually we want to push stackP
if (lineNumberMode === "outside") { if (lineNumberMode === 'outside') {
if (element.childNodes.length > 0) { //if we hit = 0, the code would fail if (element.childNodes.length > 0) { //if we hit = 0, the code would fail
// add empty line number column for inline diff or pragraph diff mode // add empty line number column for inline diff or pragraph diff mode
if (element.childNodes[0].tagName === "INS" || if (element.childNodes[0].tagName === 'INS' ||
element.childNodes[0].tagName === "DEL") { element.childNodes[0].tagName === 'DEL') {
var pLineNumberPlaceholder = { var pLineNumberPlaceholder = {
width: 20, width: 20,
text: "", text: '',
fontSize: 8, fontSize: 8,
margin: [0, 2, 0, 0] margin: [0, 2, 0, 0]
}; };
@ -955,7 +958,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
} }
alreadyConverted.push(pObjectToPush); alreadyConverted.push(pObjectToPush);
break; break;
case "img": case 'img':
var path = element.getAttribute('src'); var path = element.getAttribute('src');
var height = images[path].height; var height = images[path].height;
var width = images[path].width; var width = images[path].width;
@ -989,8 +992,8 @@ angular.module('OpenSlidesApp.core.pdf', [])
height: height, height: height,
}); });
break; break;
case "ul": case 'ul':
case "ol": case 'ol':
var list = create(nodeName); var list = create(nodeName);
if (nodeName == 'ol') { if (nodeName == 'ol') {
var start = element.getAttribute('start'); var start = element.getAttribute('start');
@ -999,7 +1002,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
} }
} }
ComputeStyle(list, styles); ComputeStyle(list, styles);
if (lineNumberMode === "outside") { if (lineNumberMode === 'outside') {
var lines = extractLineNumbers(element); var lines = extractLineNumbers(element);
currentParagraph = parseChildren(list[nodeName], element, currentParagraph, styles, diff_mode); currentParagraph = parseChildren(list[nodeName], element, currentParagraph, styles, diff_mode);
if (lines.length > 0) { if (lines.length > 0) {
@ -1028,7 +1031,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
} }
break; break;
default: default:
var defaultText = create("text", element.textContent.replace(/\n/g, "")); var defaultText = create('text', element.textContent.replace(/\n/g, ''));
ComputeStyle(defaultText, styles); ComputeStyle(defaultText, styles);
if (!currentParagraph) { if (!currentParagraph) {
currentParagraph = {}; currentParagraph = {};
@ -1047,8 +1050,8 @@ angular.module('OpenSlidesApp.core.pdf', [])
*/ */
ParseHtml = function(converted, htmlText) { ParseHtml = function(converted, htmlText) {
var html = HTMLValidizer.validize(htmlText); var html = HTMLValidizer.validize(htmlText);
html = $(html.replace(/\t/g, "").replace(/\n/g, "")); html = $(html.replace(/\t/g, '').replace(/\n/g, ''));
var emptyParagraph = create("text"); var emptyParagraph = create('text');
slice(html).forEach(function(element) { slice(html).forEach(function(element) {
ParseElement(converted, element, null, [], DIFF_MODE_NORMAL); ParseElement(converted, element, null, [], DIFF_MODE_NORMAL);
}); });
@ -1067,7 +1070,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
}, },
{ {
text: line.lineNumber, text: line.lineNumber,
color: "gray", color: 'gray',
fontSize: standardFontsize - 2, fontSize: standardFontsize - 2,
decoration: '', decoration: '',
}, },
@ -1082,8 +1085,8 @@ angular.module('OpenSlidesApp.core.pdf', [])
}, },
/** /**
* Creates containerelements for pdfMake * Creates containerelements for pdfMake
* e.g create("text":"MyText") result in { text: "MyText" } * e.g create('text':'MyText') result in { text: 'MyText' }
* or complex objects create("stack", [{text:"MyText"}, {text:"MyText2"}]) * or complex objects create('stack', [{text:'MyText'}, {text:'MyText2'}])
*for units / paragraphs of text *for units / paragraphs of text
* *
* @function * @function
@ -1160,8 +1163,8 @@ angular.module('OpenSlidesApp.core.pdf', [])
/* /*
* Returns a map from urls to arrays of font types used by PdfMake. * Returns a map from urls to arrays of font types used by PdfMake.
* E.g. if the font "regular" and bold" have the urls "fonts/myFont.ttf", * E.g. if the font 'regular' and 'bold' have the urls 'fonts/myFont.ttf',
* the map fould be "fonts/myFont.ttf": ["OSFont-regular.ttf", "OSFont-bold.ttf"] * the map fould be 'fonts/myFont.ttf': ['OSFont-regular.ttf', 'OSFont-bold.ttf']
*/ */
var getUrlMapping = function () { var getUrlMapping = function () {
var urlMap = {}; var urlMap = {};

View File

@ -166,6 +166,19 @@ angular.module('OpenSlidesApp.core.site', [
} }
]) ])
// Make the main content expandable
.run([
'$rootScope',
function ($rootScope) {
$rootScope.$on('$stateChangeSuccess', function() {
$rootScope.expandContent = false;
});
$rootScope.toggleExpandContent = function () {
$rootScope.expandContent = !$rootScope.expandContent;
};
}
])
.config([ .config([
'mainMenuProvider', 'mainMenuProvider',
'gettext', 'gettext',

View File

@ -178,9 +178,10 @@
<!-- Content --> <!-- Content -->
<div id="content" ng-controller="ProjectorSidebarCtrl"> <div id="content" ng-controller="ProjectorSidebarCtrl">
<div class="containerOS"> <div ng-class="expandContent ? 'containerOSExpanded' : 'containerOS'">
<!-- col2 sidebar-xs (for small devices)--> <!-- col2 sidebar-xs (for small devices)-->
<div ng-if="!expandContent">
<div id="sidebar-xs" class="col2" os-perms="core.can_see_projector" ng-class="{ <div id="sidebar-xs" class="col2" os-perms="core.can_see_projector" ng-class="{
'sidebar-max': isProjectorSidebar && operator.hasPerms('core.can_see_projector'), 'sidebar-max': isProjectorSidebar && operator.hasPerms('core.can_see_projector'),
'sidebar-min': !isProjectorSidebar && operator.hasPerms('core.can_see_projector'), 'sidebar-min': !isProjectorSidebar && operator.hasPerms('core.can_see_projector'),
@ -211,12 +212,13 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- col1 --> <!-- col1 -->
<div id="main-column" class="col1" ng-class="{ <div id="main-column" class="col1" ng-class="{
'sidebar-max': isProjectorSidebar && operator.hasPerms('core.can_see_projector'), 'sidebar-max': isProjectorSidebar && operator.hasPerms('core.can_see_projector') && !expandContent,
'sidebar-min': !isProjectorSidebar && operator.hasPerms('core.can_see_projector'), 'sidebar-min': !isProjectorSidebar && operator.hasPerms('core.can_see_projector') && !expandContent,
'sidebar-none': !operator.hasPerms('core.can_see_projector') }"> 'sidebar-none': !operator.hasPerms('core.can_see_projector') || expandContent}">
<!-- dynamic views --> <!-- dynamic views -->
<div ui-view ng-if="openslidesBootstrapDone && baseViewPermissionsGranted"></div> <div ui-view ng-if="openslidesBootstrapDone && baseViewPermissionsGranted"></div>
<!-- footer --> <!-- footer -->
@ -228,6 +230,7 @@
</div> </div>
<!-- col2 normal sidebar --> <!-- col2 normal sidebar -->
<div ng-if="!expandContent">
<div id="sidebar" class="col2" os-perms="core.can_see_projector" ng-class="{ <div id="sidebar" class="col2" os-perms="core.can_see_projector" ng-class="{
'sidebar-max': isProjectorSidebar && operator.hasPerms('core.can_see_projector'), 'sidebar-max': isProjectorSidebar && operator.hasPerms('core.can_see_projector'),
'sidebar-min': !isProjectorSidebar && operator.hasPerms('core.can_see_projector'), 'sidebar-min': !isProjectorSidebar && operator.hasPerms('core.can_see_projector'),
@ -255,6 +258,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</div><!--end content-container--> </div><!--end content-container-->

View File

@ -166,11 +166,15 @@ def get_config_variables():
subgroup='Amendments') subgroup='Amendments')
yield ConfigVariable( yield ConfigVariable(
name='motions_amendments_apply_text', name='motions_amendments_text_mode',
default_value=False, default_value='freestyle',
input_type='boolean', input_type='choice',
label='Apply text for new amendments', label='How to create new amendments',
help_text='The title of the motion is always applied.', choices=(
{'value': 'freestyle', 'display_name': 'Empty text field'},
{'value': 'fulltext', 'display_name': 'Edit the whole motion text'},
{'value': 'paragraph', 'display_name': 'Paragraph-based, Diff-enabled'},
),
weight=342, weight=342,
group='Motions', group='Motions',
subgroup='Amendments') subgroup='Amendments')

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2018-03-07 10:46
from __future__ import unicode_literals
import jsonfield.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('motions', '0006_submitter_model'),
]
operations = [
migrations.AddField(
model_name='motionversion',
name='amendment_paragraphs',
field=jsonfield.fields.JSONField(null=True),
),
]

View File

@ -214,9 +214,12 @@ class Motion(RESTModelMixin, models.Model):
* Else the given version is used. * Else the given version is used.
To create and use a new version object, you have to set it via the 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 and reason into 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 this version object before giving it to this save method. The properties
motion.title, motion.text and motion.reason will be ignored. motion.title, motion.text, motion.amendment_paragraphs and motion.reason will be ignored.
text and amendment_paragraphs are mutually exclusive; if both are given,
amendment_paragraphs takes precedence.
""" """
if not self.state: if not self.state:
self.reset_state() self.reset_state()
@ -261,8 +264,8 @@ class Motion(RESTModelMixin, models.Model):
return return
elif use_version is None: elif use_version is None:
use_version = self.get_last_version() use_version = self.get_last_version()
# Save title, text and reason into the version object. # Save title, text, amendment paragraphs and reason into the version object.
for attr in ['title', 'text', 'reason']: for attr in ['title', 'text', 'amendment_paragraphs', 'reason']:
_attr = '_%s' % attr _attr = '_%s' % attr
data = getattr(self, _attr, None) data = getattr(self, _attr, None)
if data is not None: if data is not None:
@ -323,7 +326,7 @@ class Motion(RESTModelMixin, models.Model):
return True return True
last_version = self.get_last_version() last_version = self.get_last_version()
for attr in ['title', 'text', 'reason']: for attr in ['title', 'text', 'amendment_paragraphs', 'reason']:
if getattr(last_version, attr) != getattr(version, attr): if getattr(last_version, attr) != getattr(version, attr):
return True return True
return False return False
@ -460,7 +463,33 @@ class Motion(RESTModelMixin, models.Model):
text = property(get_text, set_text) text = property(get_text, set_text)
""" """
The text of a motin. The text of a motion.
Is saved in a MotionVersion object.
"""
def get_amendment_paragraphs(self):
"""
Get the paragraphs of the amendment.
Returns an array of entries that are either null (paragraph is not changed)
or a string (the new version of this paragraph).
"""
try:
return self._amendment_paragraphs
except AttributeError:
return self.get_active_version().amendment_paragraphs
def set_amendment_paragraphs(self, text):
"""
Set the paragraphs of the amendment.
Has to be an array of entries that are either null (paragraph is not changed)
or a string (the new version of this paragraph).
"""
self._amendment_paragraphs = text
amendment_paragraphs = property(get_amendment_paragraphs, set_amendment_paragraphs)
"""
The paragraphs of the amendment.
Is saved in a MotionVersion object. Is saved in a MotionVersion object.
""" """
@ -496,7 +525,7 @@ class Motion(RESTModelMixin, models.Model):
Return a version object, not saved in the database. Return a version object, not saved in the database.
The version data of the new version object is populated with the data The version data of the new version object is populated with the data
set via motion.title, motion.text, motion.reason if these data are 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 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 attributes, it is populated with the data from the last version
object if such object exists. object if such object exists.
@ -510,7 +539,7 @@ class Motion(RESTModelMixin, models.Model):
last_version = self.get_last_version() last_version = self.get_last_version()
else: else:
last_version = None last_version = None
for attr in ['title', 'text', 'reason']: for attr in ['title', 'text', 'amendment_paragraphs', 'reason']:
if attr in kwargs: if attr in kwargs:
continue continue
_attr = '_%s' % attr _attr = '_%s' % attr
@ -693,6 +722,13 @@ class Motion(RESTModelMixin, models.Model):
""" """
return config['motions_amendments_enabled'] and self.parent is not None return config['motions_amendments_enabled'] and self.parent is not None
def is_paragraph_based_amendment(self):
"""
Returns True if the motion is an amendment that stores the changes on a per-paragraph-basis
and is therefore eligible to be shown in diff-view.
"""
return self.is_amendment() and self.amendment_paragraphs
def get_amendments_deep(self): def get_amendments_deep(self):
""" """
Generator that yields all amendments of this motion including all Generator that yields all amendments of this motion including all
@ -702,6 +738,12 @@ class Motion(RESTModelMixin, models.Model):
yield amendment yield amendment
yield from amendment.get_amendments_deep() yield from amendment.get_amendments_deep()
def get_paragraph_based_amendments(self):
"""
Returns a list of all paragraph-based amendments to this motion
"""
return list(filter(lambda amend: amend.is_paragraph_based_amendment(), self.amendments.all()))
class SubmitterManager(models.Manager): class SubmitterManager(models.Manager):
""" """
@ -789,6 +831,15 @@ class MotionVersion(RESTModelMixin, models.Model):
text = models.TextField() text = models.TextField()
"""The text of a motion.""" """The text of a motion."""
amendment_paragraphs = JSONField(null=True)
"""
If paragraph-based, diff-enabled amendment style is used, this field stores an array of strings or null values.
Each entry corresponds to a paragraph of the text of the original motion.
If the entry is null, then the paragraph remains unchanged.
If the entry is a string, this is the new text of the paragraph.
amendment_paragraphs and text are mutually exclusive.
"""
reason = models.TextField(null=True, blank=True) reason = models.TextField(null=True, blank=True)
"""The reason for a motion.""" """The reason for a motion."""

View File

@ -29,10 +29,13 @@ class MotionSlide(ProjectorElement):
yield motion.agenda_item yield motion.agenda_item
yield motion.state.workflow yield motion.state.workflow
yield from self.required_motions_for_state_and_recommendation(motion) yield from self.required_motions_for_state_and_recommendation(motion)
yield from motion.get_paragraph_based_amendments()
for submitter in motion.submitters.all(): for submitter in motion.submitters.all():
yield submitter.user yield submitter.user
yield from motion.supporters.all() yield from motion.supporters.all()
yield from MotionChangeRecommendation.objects.filter(motion_version=motion.get_active_version().id) yield from MotionChangeRecommendation.objects.filter(motion_version=motion.get_active_version().id)
if motion.parent:
yield motion.parent
def required_motions_for_state_and_recommendation(self, motion): def required_motions_for_state_and_recommendation(self, motion):
""" """

View File

@ -134,6 +134,28 @@ class MotionCommentsJSONSerializerField(Field):
return data return data
class AmendmentParagraphsJSONSerializerField(Field):
"""
Serializer for motions's amendment_paragraphs JSONField.
"""
def to_representation(self, obj):
"""
Returns the value of the field.
"""
return obj
def to_internal_value(self, data):
"""
Checks that data is a list of strings.
"""
if type(data) is not list:
raise ValidationError({'detail': 'Data must be a list.'})
for paragraph in data:
if type(paragraph) is not str and paragraph is not None:
raise ValidationError({'detail': 'Paragraph must be either a string or null/None.'})
return data
class MotionLogSerializer(ModelSerializer): class MotionLogSerializer(ModelSerializer):
""" """
Serializer for motion.models.MotionLog objects. Serializer for motion.models.MotionLog objects.
@ -250,6 +272,8 @@ class MotionPollSerializer(ModelSerializer):
class MotionVersionSerializer(ModelSerializer): class MotionVersionSerializer(ModelSerializer):
amendment_paragraphs = AmendmentParagraphsJSONSerializerField(required=False)
""" """
Serializer for motion.models.MotionVersion objects. Serializer for motion.models.MotionVersion objects.
""" """
@ -261,6 +285,7 @@ class MotionVersionSerializer(ModelSerializer):
'creation_time', 'creation_time',
'title', 'title',
'text', 'text',
'amendment_paragraphs',
'reason',) 'reason',)
@ -315,8 +340,9 @@ class MotionSerializer(ModelSerializer):
polls = MotionPollSerializer(many=True, read_only=True) polls = MotionPollSerializer(many=True, read_only=True)
reason = 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() state_required_permission_to_see = SerializerMethodField()
text = CharField(write_only=True) text = CharField(write_only=True, allow_blank=True)
title = CharField(max_length=255, write_only=True) title = CharField(max_length=255, write_only=True)
amendment_paragraphs = AmendmentParagraphsJSONSerializerField(required=False, write_only=True)
versions = MotionVersionSerializer(many=True, read_only=True) versions = MotionVersionSerializer(many=True, read_only=True)
workflow_id = IntegerField( workflow_id = IntegerField(
min_value=1, min_value=1,
@ -334,6 +360,7 @@ class MotionSerializer(ModelSerializer):
'identifier', 'identifier',
'title', 'title',
'text', 'text',
'amendment_paragraphs',
'reason', 'reason',
'versions', 'versions',
'active_version', 'active_version',
@ -360,12 +387,25 @@ class MotionSerializer(ModelSerializer):
def validate(self, data): def validate(self, data):
if 'text'in data: if 'text'in data:
data['text'] = validate_html(data['text']) data['text'] = validate_html(data['text'])
if 'reason' in data: if 'reason' in data:
data['reason'] = validate_html(data['reason']) data['reason'] = validate_html(data['reason'])
validated_comments = dict() validated_comments = dict()
for id, comment in data.get('comments', {}).items(): for id, comment in data.get('comments', {}).items():
validated_comments[id] = validate_html(comment) validated_comments[id] = validate_html(comment)
data['comments'] = validated_comments data['comments'] = validated_comments
if 'amendment_paragraphs' in data:
data['amendment_paragraphs'] = list(map(lambda entry: validate_html(entry) if type(entry) is str else None,
data['amendment_paragraphs']))
data['text'] = ''
else:
if 'text' in data and len(data['text']) == 0:
raise ValidationError({
'detail': _('This field may not be blank.')
})
return data return data
@transaction.atomic @transaction.atomic
@ -379,6 +419,7 @@ class MotionSerializer(ModelSerializer):
motion = Motion() motion = Motion()
motion.title = validated_data['title'] motion.title = validated_data['title']
motion.text = validated_data['text'] motion.text = validated_data['text']
motion.amendment_paragraphs = validated_data.get('amendment_paragraphs')
motion.reason = validated_data.get('reason', '') motion.reason = validated_data.get('reason', '')
motion.identifier = validated_data.get('identifier') motion.identifier = validated_data.get('identifier')
motion.category = validated_data.get('category') motion.category = validated_data.get('category')
@ -418,7 +459,7 @@ class MotionSerializer(ModelSerializer):
version = motion.get_last_version() version = motion.get_last_version()
# Title, text, reason. # Title, text, reason.
for key in ('title', 'text', 'reason'): for key in ('title', 'text', 'amendment_paragraphs', 'reason'):
if key in validated_data.keys(): if key in validated_data.keys():
setattr(version, key, validated_data[key]) setattr(version, key, validated_data[key])

View File

@ -0,0 +1,94 @@
.paragraph-select-list {
display: table;
border: 1px solid #d3d3d3;
width: 100%;
margin-bottom: 10px;
.paragraph-select-holder {
display: table-row;
cursor: pointer;
border-bottom: 1px solid #d3d3d3;
.paragraph-select {
display: table-cell;
width: 30px;
padding-top: 5px;
text-align: center;
}
.text-holder {
display: table-cell;
background-color: white;
padding: 5px 10px;
:last-child {
margin-bottom: 0;
}
// Show line numbers at the side
@media screen and (min-width: 800px) {
padding-left: 30px;
position: relative;
.os-line-number {
display: inline-block;
font-size: 0;
line-height: 0;
width: 22px;
height: 22px;
position: absolute;
left: -15px;
padding-right: 45px;
&:after {
content: attr(data-line-number);
position: absolute;
top: 8px;
right: 5px;
vertical-align: top;
color: #a9a9a9;
font-size: 12px;
font-weight: normal;
}
}
}
// Show line numbers at the side
@media screen and (max-width: 799px) {
.os-line-break {
display: none;
}
.os-line-number {
display: inline-block;
&:after {
display: inline-block;
content: attr(data-line-number);
vertical-align: top;
font-size: 10px;
font-weight: normal;
color: gray;
margin-top: -3px;
margin-left: 0;
margin-right: 0;
}
}
}
}
&:hover {
.text-holder {
background-color: #f0f0f0;
}
}
&.selected {
.paragraph-select {
background-color: #ddd;
}
.text-holder {
background-color: #ddd;
}
}
}
}

View File

@ -31,6 +31,12 @@ ul.os-split-after, ol.os-split-after {
padding-bottom: 0; padding-bottom: 0;
} }
} }
.collission-hint {
color: red;
float: left;
margin-left: -19px;
margin-top: 10px;
}
} }
.motion-text-diff { .motion-text-diff {
@ -51,4 +57,15 @@ ul.os-split-after, ol.os-split-after {
&.line-numbers-inline .insert .os-line-number { &.line-numbers-inline .insert .os-line-number {
display: none; display: none;
} }
.paragraph-context {
opacity: 0.5;
}
&.amendment-context {
.paragraph-context {
opacity: 1;
}
}
.amendment-line-header {
margin: 10px 0 0 0;
}
} }

View File

@ -14,8 +14,9 @@
padding-bottom: 10px; padding-bottom: 10px;
} }
ul { ol, ul {
margin-bottom: 0px; margin-left: 15px;
margin-bottom: 0;
} }
.highlight { .highlight {

View File

@ -1,3 +1,4 @@
@import "amendments";
@import "diff"; @import "diff";
@import "change-recommendation-overview"; @import "change-recommendation-overview";
@import "inline-editing"; @import "inline-editing";
@ -72,6 +73,10 @@
margin-right: 5px; margin-right: 5px;
} }
ng-include {
display: inline-block;
}
.btn.disabled { .btn.disabled {
cursor: default; cursor: default;
opacity: 1; opacity: 1;
@ -224,6 +229,11 @@
.btn-edit { .btn-edit {
margin-left: 5px; margin-left: 5px;
} }
.btn-amend-info {
margin-left: 5px;
min-width: 68px;
}
} }
.status-row { .status-row {
font-style: italic; font-style: italic;

View File

@ -229,6 +229,7 @@ angular.module('OpenSlidesApp.motions', [
.factory('Motion', [ .factory('Motion', [
'DS', 'DS',
'$http', '$http',
'$cacheFactory',
'MotionPoll', 'MotionPoll',
'MotionStateAndRecommendationParser', 'MotionStateAndRecommendationParser',
'MotionChangeRecommendation', 'MotionChangeRecommendation',
@ -243,9 +244,13 @@ angular.module('OpenSlidesApp.motions', [
'Projector', 'Projector',
'ProjectHelper', 'ProjectHelper',
'operator', 'operator',
function(DS, $http, MotionPoll, MotionStateAndRecommendationParser, MotionChangeRecommendation, 'UnifiedChangeObjectCollission',
function(DS, $http, $cacheFactory, MotionPoll, MotionStateAndRecommendationParser, MotionChangeRecommendation,
MotionComment, jsDataModel, gettext, gettextCatalog, Config, lineNumberingService, MotionComment, jsDataModel, gettext, gettextCatalog, Config, lineNumberingService,
diffService, OpenSlidesSettings, Projector, ProjectHelper, operator) { diffService, OpenSlidesSettings, Projector, ProjectHelper, operator, UnifiedChangeObjectCollission) {
var diffCache = $cacheFactory('motion.service');
var name = 'motions/motion'; var name = 'motions/motion';
return DS.defineResource({ return DS.defineResource({
name: name, name: name,
@ -277,6 +282,10 @@ angular.module('OpenSlidesApp.motions', [
} }
return this.versions[index] || {}; return this.versions[index] || {};
}, },
isParagraphBasedAmendment: function () {
var version = this.getVersion();
return this.isAmendment && version.amendment_paragraphs;
},
getTitle: function (versionId) { getTitle: function (versionId) {
return this.getVersion(versionId).title; return this.getVersion(versionId).title;
}, },
@ -331,19 +340,31 @@ angular.module('OpenSlidesApp.motions', [
return lineNumberingService.insertLineNumbers(html, lineLength, highlight, callback); return lineNumberingService.insertLineNumbers(html, lineLength, highlight, callback);
}, },
getTextBetweenChangeRecommendations: function (versionId, change1, change2, highlight) { getTextBetweenChanges: function (versionId, change1, change2, highlight) {
var line_from = (change1 ? change1.line_to : 1), var line_from = (change1 ? change1.line_to : 1),
line_to = (change2 ? change2.line_from : null); line_to = (change2 ? change2.line_from : null);
if (line_from > line_to) { if (line_from > line_to) {
throw 'Invalid call of getTextBetweenChangeRecommendations: change1 needs to be before change2'; throw 'Invalid call of getTextBetweenChanges: change1 needs to be before change2';
} }
if (line_from == line_to) { if (line_from === line_to) {
return ''; return '';
} }
return this.getTextInLineRange(versionId, line_from, line_to, highlight);
},
getTextInLineRange: function (versionId, line_from, line_to, highlight) {
var lineLength = Config.get('motions_line_length').value, var lineLength = Config.get('motions_line_length').value,
html = lineNumberingService.insertLineNumbers(this.getVersion(versionId).text, lineLength), htmlRaw = this.getVersion(versionId).text;
var cacheKey = 'getTextInLineRange ' + line_from + ' ' + line_to + ' ' + highlight + ' ' +
lineNumberingService.djb2hash(htmlRaw),
cached = diffCache.get(cacheKey);
if (!angular.isUndefined(cached)) {
return cached;
}
var html = lineNumberingService.insertLineNumbers(htmlRaw, lineLength),
data; data;
try { try {
@ -362,9 +383,11 @@ angular.module('OpenSlidesApp.motions', [
data.html + data.innerContextEnd + data.outerContextEnd; data.html + data.innerContextEnd + data.outerContextEnd;
html = lineNumberingService.insertLineNumbers(html, lineLength, highlight, null, line_from); html = lineNumberingService.insertLineNumbers(html, lineLength, highlight, null, line_from);
diffCache.put(cacheKey, html);
return html; return html;
}, },
getTextRemainderAfterLastChangeRecommendation: function(versionId, changes, highlight) { getTextRemainderAfterLastChange: function(versionId, changes, highlight) {
var maxLine = 0; var maxLine = 0;
for (var i = 0; i < changes.length; i++) { for (var i = 0; i < changes.length; i++) {
if (changes[i].line_to > maxLine) { if (changes[i].line_to > maxLine) {
@ -398,18 +421,37 @@ angular.module('OpenSlidesApp.motions', [
} }
return html; return html;
}, },
_getTextWithChangeRecommendations: function (versionId, highlight, lineBreaks, statusCompareCb) { _getTextWithChanges: function (versionId, highlight, lineBreaks, recommendation_filter, amendment_filter) {
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.getTextChangeRecommendations(versionId, 'DESC'); change_recommendations = this.getTextChangeRecommendations(versionId, 'DESC'),
amendments = this.getParagraphBasedAmendments();
for (var i = 0; i < changes.length; i++) { var allChanges = [];
var change = changes[i]; change_recommendations.filter(recommendation_filter).forEach(function(change) {
if (typeof statusCompareCb === 'undefined' || statusCompareCb(change.rejected)) { allChanges.push({"text": change.text, "line_from": change.line_from, "line_to": change.line_to});
});
amendments.filter(amendment_filter).forEach(function(amend) {
var change = amend.getAmendmentsAffectedLinesChanged();
allChanges.push({"text": change.text, "line_from": change.line_from, "line_to": change.line_to});
});
// Changes need to be applied from the bottom up, to prevent conflicts with changing line numbers.
allChanges.sort(function(change1, change2) {
if (change1.line_from < change2.line_from) {
return 1;
} else if (change1.line_from > change2.line_from) {
return -1;
} else {
return 0;
}
});
allChanges.forEach(function(change) {
html = lineNumberingService.insertLineNumbers(html, lineLength, null, null, 1); html = lineNumberingService.insertLineNumbers(html, lineLength, null, null, 1);
html = diffService.replaceLines(html, change.text, change.line_from, change.line_to); html = diffService.replaceLines(html, change.text, change.line_from, change.line_to);
} });
}
if (lineBreaks) { if (lineBreaks) {
html = lineNumberingService.insertLineNumbers(html, lineLength, highlight, null, 1); html = lineNumberingService.insertLineNumbers(html, lineLength, highlight, null, 1);
@ -418,13 +460,23 @@ angular.module('OpenSlidesApp.motions', [
return html; return html;
}, },
getTextWithAllChangeRecommendations: function (versionId, highlight, lineBreaks) { getTextWithAllChangeRecommendations: function (versionId, highlight, lineBreaks) {
return this._getTextWithChangeRecommendations(versionId, highlight, lineBreaks, function() { return this._getTextWithChanges(versionId, highlight, lineBreaks, function() {
return true; return true; // All change recommendations
}, function() {
return false; // No amendments
}); });
}, },
getTextWithoutRejectedChangeRecommendations: function (versionId, highlight, lineBreaks) { getTextWithAgreedChanges: function (versionId, highlight, lineBreaks) {
return this._getTextWithChangeRecommendations(versionId, highlight, lineBreaks, function(rejected) { return this._getTextWithChanges(versionId, highlight, lineBreaks, function(recommendation) {
return !rejected; return !recommendation.rejected;
}, function(amendment) {
if (amendment.state && amendment.state.name === 'rejected') {
return false;
}
if (amendment.state && amendment.state.name === 'accepted') {
return true;
}
return (amendment.recommendation && amendment.recommendation.name === 'accepted');
}); });
}, },
getTextByMode: function(mode, versionId, highlight, lineBreaks) { getTextByMode: function(mode, versionId, highlight, lineBreaks) {
@ -447,13 +499,34 @@ angular.module('OpenSlidesApp.motions', [
} }
break; break;
case 'diff': case 'diff':
var changes = this.getTextChangeRecommendations(versionId, 'ASC'); var amendments_crs = this.getTextChangeRecommendations(versionId, 'ASC').map(function (cr) {
text = ''; return cr.getUnifiedChangeObject();
for (var i = 0; i < changes.length; i++) { }).concat(
text += this.getTextBetweenChangeRecommendations(versionId, (i === 0 ? null : changes[i - 1]), changes[i], highlight); this.getParagraphBasedAmendmentsForDiffView().map(function (amendment) {
text += changes[i].getDiff(this, versionId, highlight); return amendment.getUnifiedChangeObject();
})
);
amendments_crs.sort(function (change1, change2) {
if (change1.line_from > change2.line_from) {
return 1;
} else if (change1.line_from < change2.line_from) {
return -1;
} else {
return 0;
} }
text += this.getTextRemainderAfterLastChangeRecommendation(versionId, changes); });
text = '';
for (var i = 0; i < amendments_crs.length; i++) {
if (i===0) {
text += this.getTextBetweenChanges(versionId, null, amendments_crs[0], highlight);
} else if (amendments_crs[i - 1].line_to < amendments_crs[i].line_from) {
text += this.getTextBetweenChanges(versionId, amendments_crs[i - 1], amendments_crs[i], highlight);
}
text += amendments_crs[i].getDiff(this, versionId, highlight);
}
text += this.getTextRemainderAfterLastChange(versionId, amendments_crs);
if (!lineBreaks) { if (!lineBreaks) {
text = lineNumberingService.stripLineNumbers(text); text = lineNumberingService.stripLineNumbers(text);
@ -463,11 +536,313 @@ angular.module('OpenSlidesApp.motions', [
text = this.getTextWithAllChangeRecommendations(versionId, highlight, lineBreaks); text = this.getTextWithAllChangeRecommendations(versionId, highlight, lineBreaks);
break; break;
case 'agreed': case 'agreed':
text = this.getTextWithoutRejectedChangeRecommendations(versionId, highlight, lineBreaks); text = this.getTextWithAgreedChanges(versionId, highlight, lineBreaks);
break; break;
} }
return text; return text;
}, },
getTextParagraphs: function(versionId, lineBreaks) {
/*
* @param versionId [if undefined, active_version will be used]
* @param lineBreaks [if line numbers / breaks should be included in the result]
*/
var text;
if (lineBreaks) {
text = this.getTextWithLineBreaks(versionId);
} else {
text = this.getVersion(versionId).text;
}
return lineNumberingService.splitToParagraphs(text);
},
getTextHeadings: function(versionId) {
var html = this.getTextWithLineBreaks(versionId);
return lineNumberingService.getHeadingsWithLineNumbers(html);
},
getAmendmentParagraphsByMode: function (mode, versionId, lineBreaks) {
/*
* @param mode ['original', 'diff', 'changed']
* @param versionId [if undefined, active_version will be used]
* @param lineBreaks [if line numbers / breaks should be included in the result]
*
* Structure of the return array elements:
* {
* "paragraphNo": paragraph number, starting with 0
* "lineFrom": First line number of the affected paragraph
* "lineTo": Last line number of the affected paragraph;
* refers to the line breaking element at the end, i.e. the start of the following line
* "text": the actual text
* }
*/
lineBreaks = (lineBreaks === undefined ? true : lineBreaks);
var cacheKey = 'getAmendmentParagraphsByMode ' + mode + ' ' + versionId + ' ' + lineBreaks +
lineNumberingService.djb2hash(JSON.stringify(this.getVersion(versionId).amendment_paragraphs)),
cached = diffCache.get(cacheKey);
if (!angular.isUndefined(cached)) {
return cached;
}
var original_text = this.getParentMotion().getTextByMode('original', null, null, true);
var original_paragraphs = lineNumberingService.splitToParagraphs(original_text);
var output = [];
this.getVersion(versionId).amendment_paragraphs.forEach(function(paragraph_amend, paragraphNo) {
if (paragraph_amend === null) {
return;
}
if (original_paragraphs[paragraphNo] === undefined) {
throw "The amendment appears to have more paragraphs than the motion. This means, the data might be corrupt";
}
var paragraph_orig = original_paragraphs[paragraphNo];
var line_range = lineNumberingService.getLineNumberRange(paragraph_orig);
var line_length = Config.get('motions_line_length').value;
paragraph_orig = lineNumberingService.stripLineNumbers(paragraph_orig);
var text = null;
switch (mode) {
case "diff":
if (lineBreaks) {
text = diffService.diff(paragraph_orig, paragraph_amend, line_length, line_range.from);
} else {
text = diffService.diff(paragraph_orig, paragraph_amend);
}
break;
case "original":
text = paragraph_orig;
if (lineBreaks) {
text = lineNumberingService.insertLineNumbers(text, line_length, null, null, line_range.from);
}
break;
case "changed":
text = paragraph_amend;
if (lineBreaks) {
text = lineNumberingService.insertLineNumbers(text, line_length, null, null, line_range.from);
}
break;
default:
throw "Invalid text mode: " + mode;
}
output.push({
"paragraphNo": paragraphNo,
"lineFrom": line_range.from,
"lineTo": line_range.to,
"text": text
});
});
diffCache.put(cacheKey, output);
return output;
},
getAmendmentParagraphsLinesByMode: function (mode, versionId, lineBreaks) {
/*
* @param mode ['original', 'diff', 'changed']
* @param versionId [if undefined, active_version will be used]
* @param lineBreaks [if line numbers / breaks should be included in the result]
*
* Structure of the return array elements:
* {
* "paragraphNo": paragraph number, starting with 0
* "paragraphLineFrom": First line number of the affected paragraph
* "paragraphLineTo": End of the affected paragraph (line number + 1)
* "diffLineFrom": First line number of the affected lines
* "diffLineTo": End of the affected lines (line number + 1)
* "textPre": The beginning of the paragraph, before the diff
* "text": the diff
* "textPost": The end of the paragraph, after the diff
* }
*/
if (!this.isParagraphBasedAmendment() || !this.getParentMotion()) {
return [];
}
var cacheKey = 'getAmendmentParagraphsLinesByMode ' + mode + ' ' + versionId + ' ' + lineBreaks +
lineNumberingService.djb2hash(JSON.stringify(this.getVersion(versionId).amendment_paragraphs)),
cached = diffCache.get(cacheKey);
if (!angular.isUndefined(cached)) {
return cached;
}
var original_text = this.getParentMotion().getTextByMode('original', null, null, true);
var original_paragraphs = lineNumberingService.splitToParagraphs(original_text);
var output = [];
this.getVersion(versionId).amendment_paragraphs.forEach(function(paragraph_amend, paragraphNo) {
if (paragraph_amend === null) {
return;
}
if (original_paragraphs[paragraphNo] === undefined) {
throw "The amendment appears to have more paragraphs than the motion. This means, the data might be corrupt";
}
var line_length = Config.get('motions_line_length').value,
paragraph_orig = original_paragraphs[paragraphNo],
paragraph_line_range = lineNumberingService.getLineNumberRange(paragraph_orig),
diff = diffService.diff(paragraph_orig, paragraph_amend),
affected_lines = diffService.detectAffectedLineRange(diff);
if (!affected_lines) {
return;
}
// TODO: Make this work..
var base_paragraph;
switch (mode) {
case 'original':
//base_paragraph = paragraph_orig;
//base_paragraph = diffService.diff(paragraph_orig, paragraph_orig, line_length, paragraph_line_range.from);
base_paragraph = diff;
break;
case 'diff':
base_paragraph = diff;
break;
case 'changed':
//base_paragraph = paragraph_amend;
//base_paragraph = diffService.diff(paragraph_amend, paragraph_amend, line_length, paragraph_line_range.from);
base_paragraph = diff;
break;
}
var textPre = '';
var textPost = '';
if (affected_lines.from > paragraph_line_range.from) {
textPre = diffService.extractRangeByLineNumbers(base_paragraph, paragraph_line_range.from, affected_lines.from);
if (lineBreaks) {
textPre = diffService.formatDiffWithLineNumbers(textPre, line_length, paragraph_line_range.from);
}
}
if (paragraph_line_range.to > affected_lines.to) {
textPost = diffService.extractRangeByLineNumbers(base_paragraph, affected_lines.to, paragraph_line_range.to);
if (lineBreaks) {
textPost = diffService.formatDiffWithLineNumbers(textPost, line_length, affected_lines.to);
}
}
var text = diffService.extractRangeByLineNumbers(base_paragraph, affected_lines.from, affected_lines.to);
if (lineBreaks) {
text = diffService.formatDiffWithLineNumbers(text, line_length, affected_lines.from);
}
output.push({
"paragraphNo": paragraphNo,
"paragraphLineFrom": paragraph_line_range.from,
"paragraphLineTo": paragraph_line_range.to,
"diffLineFrom": affected_lines.from,
"diffLineTo": affected_lines.to,
"textPre": textPre,
"text": text,
"textPost": textPost
});
});
diffCache.put(cacheKey, output);
return output;
},
getAmendmentParagraphsLinesDiff: function (versionId) {
/*
* @param versionId [if undefined, active_version will be used]
*
*/
return this.getAmendmentParagraphsLinesByMode('diff', versionId, true);
},
getAmendmentsAffectedLinesChanged: function () {
var paragraph_diff = this.getAmendmentParagraphsByMode("diff")[0],
affected_lines = diffService.detectAffectedLineRange(paragraph_diff.text);
var extracted_lines = diffService.extractRangeByLineNumbers(paragraph_diff.text, affected_lines.from, affected_lines.to);
var diff_html = extracted_lines.outerContextStart + extracted_lines.innerContextStart +
extracted_lines.html + extracted_lines.innerContextEnd + extracted_lines.outerContextEnd;
diff_html = diffService.diffHtmlToFinalText(diff_html);
return {
"line_from": affected_lines.from,
"line_to": affected_lines.to,
"text": diff_html
};
},
getUnifiedChangeObject: function () {
var paragraph = this.getAmendmentParagraphsByMode("diff")[0];
var affected_lines = diffService.detectAffectedLineRange(paragraph.text);
if (!affected_lines) {
// no changes, no object to use
return null;
}
var extracted_lines = diffService.extractRangeByLineNumbers(paragraph.text, affected_lines.from, affected_lines.to);
var lineLength = Config.get('motions_line_length').value;
var diff_html = diffService.formatDiffWithLineNumbers(extracted_lines, lineLength, affected_lines.from);
var acceptance_state = null;
var rejection_state = null;
this.state.getRecommendations().forEach(function(state) {
if (state.name === "accepted") {
acceptance_state = state.id;
}
if (state.name === "rejected") {
rejection_state = state.id;
}
});
// The interface of this object needs to be synchronized with the same method in MotionChangeRecommendation
//
// The change object needs to be cached to prevent confusing Angular's change detection
// Otherwise, a new object would be created with every call, leading to flickering
var amendment = this;
if (this._change_object === undefined) {
// Properties that are guaranteed to be constant
this._change_object = {
"type": "amendment",
"id": "amendment-" + amendment.id,
"original": amendment,
"saveStatus": function () {
// The status needs to be reset first, as the workflow does not allow changing from
// acceptance to rejection directly or vice-versa.
amendment.setState(null).then(function () {
if (amendment._change_object.accepted) {
amendment.setState(acceptance_state);
}
if (amendment._change_object.rejected) {
amendment.setState(rejection_state);
}
});
},
"getDiff": function (motion, version, highlight) {
if (highlight > 0) {
diff_html = lineNumberingService.highlightLine(diff_html, highlight);
}
return diff_html;
}
};
}
// Properties that might change when the Amendment is edited
this._change_object.line_from = affected_lines.from;
this._change_object.line_to = affected_lines.to;
this._change_object.accepted = false;
this._change_object.rejected = false;
if (this.state && this.state.name === 'rejected') {
this._change_object.rejected = true;
} else if (this.state && this.state.name === 'accepted') {
this._change_object.accepted = true;
} else if (this.recommendation && this.recommendation.name === 'rejected') {
this._change_object.rejected = true;
}
UnifiedChangeObjectCollission.populate(this._change_object);
return this._change_object;
},
setTextStrippingLineBreaks: function (text) { setTextStrippingLineBreaks: function (text) {
this.text = lineNumberingService.stripLineNumbers(text); this.text = lineNumberingService.stripLineNumbers(text);
}, },
@ -490,6 +865,14 @@ angular.module('OpenSlidesApp.motions', [
} }
return MotionStateAndRecommendationParser.parse(name); return MotionStateAndRecommendationParser.parse(name);
}, },
// ID of the state - or null, if to be reset
setState: function(state_id) {
if (state_id === null) {
return $http.put('/rest/motions/motion/' + this.id + '/set_state/', {});
} else {
return $http.put('/rest/motions/motion/' + this.id + '/set_state/', {'state': state_id});
}
},
// full recommendation string - optional with custom recommendationextension // full recommendation string - optional with custom recommendationextension
// depended by state and provided by a custom comment field // depended by state and provided by a custom comment field
getRecommendationName: function () { getRecommendationName: function () {
@ -506,6 +889,14 @@ angular.module('OpenSlidesApp.motions', [
} }
return MotionStateAndRecommendationParser.parse(recommendation); return MotionStateAndRecommendationParser.parse(recommendation);
}, },
// ID of the state - or null, if to be reset
setRecommendation: function(recommendation_id) {
if (recommendation_id === null) {
return $http.put('/rest/motions/motion/' + this.id + '/set_recommendation/', {});
} else {
return $http.put('/rest/motions/motion/' + this.id + '/set_recommendation/', {'recommendation': recommendation_id});
}
},
// link name which is shown in search result // link name which is shown in search result
getSearchResultName: function () { getSearchResultName: function () {
return this.getTitle(); return this.getTitle();
@ -578,9 +969,42 @@ angular.module('OpenSlidesApp.motions', [
}); });
return (changes.length > 0 ? changes[0] : null); return (changes.length > 0 ? changes[0] : null);
}, },
getAmendments: function () {
return DS.filter('motions/motion', {parent_id: this.id});
},
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;
}, },
getParagraphBasedAmendments: function () {
return DS.filter('motions/motion', {parent_id: this.id}).filter(function(amendment) {
return (amendment.isParagraphBasedAmendment());
});
},
getParagraphBasedAmendmentsForDiffView: function () {
return _.filter(this.getParagraphBasedAmendments(), function(amendment) {
// If no accepted/rejected status is given, only amendments that have a recommendation
// of "accepted" and have not been officially rejected are to be shown in the diff-view
if (amendment.state && amendment.state.name === 'rejected') {
return false;
}
if (amendment.state && amendment.state.name === 'accepted') {
return true;
}
return (amendment.recommendation && amendment.recommendation.name === 'accepted');
});
},
getParentMotion: function () {
if (this.parent_id > 0) {
var parents = DS.filter('motions/motion', {id: this.parent_id});
if (parents.length > 0) {
return parents[0];
} else {
return null;
}
} else {
return null;
}
},
isAllowed: function (action) { isAllowed: function (action) {
/* /*
* Return true if the requested user is allowed to do the specific action. * Return true if the requested user is allowed to do the specific action.
@ -844,6 +1268,23 @@ angular.module('OpenSlidesApp.motions', [
} }
); );
}, },
getFormField : function (id) {
var fields = this.getNoSpecialCommentsFields();
var field = fields[id];
if (field) {
return {
key: 'comment_' + id,
type: 'editor',
templateOptions: {
label: field.name,
},
data: {
ckeditorOptions: Editor.getOptions()
},
hide: !operator.hasPerms("motions.can_manage_comments")
};
}
},
populateFields: function (motion) { populateFields: function (motion) {
// Populate content of motion.comments to the single comment // Populate content of motion.comments to the single comment
var fields = this.getCommentsFields(); var fields = this.getCommentsFields();
@ -915,8 +1356,10 @@ angular.module('OpenSlidesApp.motions', [
'jsDataModel', 'jsDataModel',
'diffService', 'diffService',
'lineNumberingService', 'lineNumberingService',
'UnifiedChangeObjectCollission',
'gettextCatalog', 'gettextCatalog',
function (DS, Config, jsDataModel, diffService, lineNumberingService, gettextCatalog) { function (DS, Config, jsDataModel, diffService, lineNumberingService,
UnifiedChangeObjectCollission, gettextCatalog) {
return DS.defineResource({ return DS.defineResource({
name: 'motions/motion-change-recommendation', name: 'motions/motion-change-recommendation',
useClass: jsDataModel, useClass: jsDataModel,
@ -993,12 +1436,89 @@ angular.module('OpenSlidesApp.motions', [
} }
title = title.replace('%FROM%', this.line_from).replace('%TO%', (this.line_to - 1)); title = title.replace('%FROM%', this.line_from).replace('%TO%', (this.line_to - 1));
return title; return title;
},
getUnifiedChangeObject: function () {
// The interface of this object needs to be synchronized with the same method in Motion
//
// The change object needs to be cached to prevent confusing Angular's change detection
// Otherwise, a new object would be created with every call, leading to flickering
var recommendation = this;
if (this._change_object === undefined) {
// Properties that are guaranteed to be constant
this._change_object = {
"type": "recommendation",
"id": "recommendation-" + recommendation.id,
"original": recommendation,
"saveStatus": function () {
recommendation.rejected = recommendation._change_object.rejected;
recommendation.saveStatus();
},
"getDiff": function (motion, version, highlight) {
return recommendation.getDiff(motion, version, highlight);
}
};
}
// Properties that might change when the Change Recommendation is edited
this._change_object.line_from = recommendation.line_from;
this._change_object.line_to = recommendation.line_to;
this._change_object.rejected = recommendation.rejected;
this._change_object.accepted = !recommendation.rejected;
UnifiedChangeObjectCollission.populate(this._change_object);
return this._change_object;
} }
} }
}); });
} }
]) ])
.factory('UnifiedChangeObjectCollission', [
function () {
return {
populate: function (obj) {
obj.otherChanges = [];
obj.setOtherChangesForCollission = function (changes) {
obj.otherChanges = changes;
};
obj.getCollissions = function(onlyAccepted) {
return obj.otherChanges.filter(function(otherChange) {
if (onlyAccepted && !otherChange.accepted) {
return false;
}
return (otherChange.id !== obj.id && (
(otherChange.line_from >= obj.line_from && otherChange.line_from < obj.line_to) ||
(otherChange.line_to > obj.line_from && otherChange.line_to <= obj.line_to) ||
(otherChange.line_from < obj.line_from && otherChange.line_to > obj.line_to)
));
});
};
obj.getAcceptedCollissions = function() {
return obj.getCollissions().filter(function(colliding) {
return colliding.accepted;
});
};
obj.setAccepted = function($event) {
if (obj.getAcceptedCollissions().length > 0) {
$event.preventDefault();
$event.stopPropagation();
return;
}
obj.accepted = true;
obj.rejected = false;
obj.saveStatus();
};
obj.setRejected = function($event) {
obj.rejected = true;
obj.accepted = false;
obj.saveStatus();
};
},
};
}
])
.run([ .run([
'Motion', 'Motion',
'Category', 'Category',

View File

@ -127,6 +127,84 @@ angular.module('OpenSlidesApp.motions.csv', [])
}, },
}; };
} }
])
.factory('AmendmentCsvExport', [
'gettextCatalog',
'CsvDownload',
'lineNumberingService',
function (gettextCatalog, CsvDownload, lineNumberingService) {
var makeHeaderline = function () {
var headerline = ['Identifier', 'Submitters', 'Category', 'Motion block',
'Leadmotion', 'Line', 'Old text', 'New text'];
return _.map(headerline, function (entry) {
return gettextCatalog.getString(entry);
});
};
return {
export: function (amendments) {
var csvRows = [
makeHeaderline()
];
_.forEach(amendments, function (amendment) {
var row = [];
// Identifier and title
row.push('"' + amendment.identifier !== null ? amendment.identifier : '' + '"');
// Submitters
var submitters = [];
angular.forEach(amendment.submitters, function(user) {
var user_short_name = [user.title, user.first_name, user.last_name].join(' ').trim();
submitters.push(user_short_name);
});
row.push('"' + submitters.join('; ') + '"');
// Category
var category = amendment.category ? amendment.category.name : '';
row.push('"' + category + '"');
// Motion block
var blockTitle = amendment.motionBlock ? amendment.motionBlock.title : '';
row.push('"' + blockTitle + '"');
// Lead motion
var leadmotion = amendment.getParentMotion();
if (leadmotion) {
var leadmotionTitle = leadmotion.identifier ? leadmotion.identifier + ': ' : '';
leadmotionTitle += leadmotion.getTitle();
row.push('"' + leadmotionTitle + '"');
} else {
row.push('""');
}
// changed paragraph
if (amendment.isParagraphBasedAmendment()) {
// TODO: get old and new paragraphLine. Resolve todo
// in motion.getAmendmentParagraphsLinesByMode
var p_old = amendment.getAmendmentParagraphsLinesByMode('original', null, false)[0];
//var p_new = amendment.getAmendmentParagraphsLinesByMode('changed', null, false)[0];
var lineStr = p_old.diffLineFrom;
if (p_old.diffLineTo != p_old.diffLineFrom + 1) {
lineStr += '-' + p_old.diffLineTo;
}
row.push('"' + lineStr + '"');
//row.push('"' + p_old.text.html + '"');
//row.push('"' + p_new.text.html + '"');
// Work around: Export the full paragraphs instead of changed lines
row.push('"' + amendment.getAmendmentParagraphsByMode('original', null, false)[0].text + '"');
row.push('"' + amendment.getAmendmentParagraphsByMode('changed', null, false)[0].text + '"');
} else {
row.push('""');
row.push('""');
row.push('"' + amendment.getText() + '"');
}
csvRows.push(row);
});
CsvDownload(csvRows, 'amendments-export.csv');
},
};
}
]); ]);
}()); }());

View File

@ -23,6 +23,42 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
return fragment.querySelector('os-linebreak.os-line-number.line-number-' + lineNumber); return fragment.querySelector('os-linebreak.os-line-number.line-number-' + lineNumber);
}; };
/**
* @param {Element} element
*/
this._getFirstLineNumberNode = function(element) {
if (element.nodeType === TEXT_NODE) {
return null;
}
if (element.nodeName === 'OS-LINEBREAK') {
return element;
}
var found = element.querySelectorAll('OS-LINEBREAK');
if (found.length > 0) {
return found.item(0);
} else {
return null;
}
};
/**
* @param {Element} element
*/
this._getLastLineNumberNode = function(element) {
if (element.nodeType === TEXT_NODE) {
return null;
}
if (element.nodeName === 'OS-LINEBREAK') {
return element;
}
var found = element.querySelectorAll('OS-LINEBREAK');
if (found.length > 0) {
return found.item(found.length - 1);
} else {
return null;
}
};
this._getNodeContextTrace = function(node) { this._getNodeContextTrace = function(node) {
var context = [], var context = [],
currNode = node; currNode = node;
@ -169,14 +205,14 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
}; };
this._serializeTag = function(node) { this._serializeTag = function(node) {
if (node.nodeType == DOCUMENT_FRAGMENT_NODE) { if (node.nodeType === DOCUMENT_FRAGMENT_NODE) {
// Fragments are only placeholders and do not have an HTML representation // Fragments are only placeholders and do not have an HTML representation
return ''; return '';
} }
var html = '<' + node.nodeName; var html = '<' + node.nodeName;
for (var i = 0; i < node.attributes.length; i++) { for (var i = 0; i < node.attributes.length; i++) {
var attr = node.attributes[i]; var attr = node.attributes[i];
if (attr.name != 'os-li-number') { if (attr.name !== 'os-li-number') {
html += ' ' + attr.name + '="' + attr.value + '"'; html += ' ' + attr.name + '="' + attr.value + '"';
} }
} }
@ -226,21 +262,21 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
if (lineNumberingService._isOsLineNumberNode(node) || lineNumberingService._isOsLineBreakNode(node)) { if (lineNumberingService._isOsLineNumberNode(node) || lineNumberingService._isOsLineBreakNode(node)) {
return ''; return '';
} }
if (node.nodeName == 'OS-LINEBREAK') { if (node.nodeName === 'OS-LINEBREAK') {
return ''; return '';
} }
var html = this._serializeTag(node); var html = this._serializeTag(node);
for (var i = 0, found = false; i < node.childNodes.length && !found; i++) { for (var i = 0, found = false; i < node.childNodes.length && !found; i++) {
if (node.childNodes[i] == toChildTrace[0]) { if (node.childNodes[i] === toChildTrace[0]) {
found = true; found = true;
var remainingTrace = toChildTrace; var remainingTrace = toChildTrace;
remainingTrace.shift(); remainingTrace.shift();
if (!lineNumberingService._isOsLineNumberNode(node.childNodes[i])) { if (!lineNumberingService._isOsLineNumberNode(node.childNodes[i])) {
html += this._serializePartialDomToChild(node.childNodes[i], remainingTrace, stripLineNumbers); html += this._serializePartialDomToChild(node.childNodes[i], remainingTrace, stripLineNumbers);
} }
} else if (node.childNodes[i].nodeType == TEXT_NODE) { } else if (node.childNodes[i].nodeType === TEXT_NODE) {
html += node.childNodes[i].nodeValue; html += node.childNodes[i].nodeValue;
} else { } else {
if (!stripLineNumbers || (!lineNumberingService._isOsLineNumberNode(node.childNodes[i]) && if (!stripLineNumbers || (!lineNumberingService._isOsLineNumberNode(node.childNodes[i]) &&
@ -263,13 +299,13 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
if (lineNumberingService._isOsLineNumberNode(node) || lineNumberingService._isOsLineBreakNode(node)) { if (lineNumberingService._isOsLineNumberNode(node) || lineNumberingService._isOsLineBreakNode(node)) {
return ''; return '';
} }
if (node.nodeName == 'OS-LINEBREAK') { if (node.nodeName === 'OS-LINEBREAK') {
return ''; return '';
} }
var html = ''; var html = '';
for (var i = 0, found = false; i < node.childNodes.length; i++) { for (var i = 0, found = false; i < node.childNodes.length; i++) {
if (node.childNodes[i] == fromChildTrace[0]) { if (node.childNodes[i] === fromChildTrace[0]) {
found = true; found = true;
var remainingTrace = fromChildTrace; var remainingTrace = fromChildTrace;
remainingTrace.shift(); remainingTrace.shift();
@ -277,7 +313,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
html += this._serializePartialDomFromChild(node.childNodes[i], remainingTrace, stripLineNumbers); html += this._serializePartialDomFromChild(node.childNodes[i], remainingTrace, stripLineNumbers);
} }
} else if (found) { } else if (found) {
if (node.childNodes[i].nodeType == TEXT_NODE) { if (node.childNodes[i].nodeType === TEXT_NODE) {
html += node.childNodes[i].nodeValue; html += node.childNodes[i].nodeValue;
} else { } else {
if (!stripLineNumbers || (!lineNumberingService._isOsLineNumberNode(node.childNodes[i]) && if (!stripLineNumbers || (!lineNumberingService._isOsLineNumberNode(node.childNodes[i]) &&
@ -291,12 +327,16 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
console.trace(); console.trace();
throw "Inconsistency or invalid call of this function detected (from)"; throw "Inconsistency or invalid call of this function detected (from)";
} }
if (node.nodeType != DOCUMENT_FRAGMENT_NODE) { if (node.nodeType !== DOCUMENT_FRAGMENT_NODE) {
html += '</' + node.nodeName + '>'; html += '</' + node.nodeName + '>';
} }
return html; return html;
}; };
/**
* @param {string} html
* @return {DocumentFragment}
*/
this.htmlToFragment = function(html) { this.htmlToFragment = function(html) {
var fragment = document.createDocumentFragment(), var fragment = document.createDocumentFragment(),
div = document.createElement('DIV'); div = document.createElement('DIV');
@ -332,7 +372,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
* Returns the HTML snippet between two given line numbers. * Returns the HTML snippet between two given line numbers.
* *
* Hint: * Hint:
* - The last line (toLine) is not included anymore, as the number refers to the line breaking element * - The last line (toLine) is not included anymore, as the number refers to the line breaking element at the end of the line
* - if toLine === null, then everything from fromLine to the end of the fragment is returned * - if toLine === null, then everything from fromLine to the end of the fragment is returned
* *
* In addition to the HTML snippet, additional information is provided regarding the most specific DOM element * In addition to the HTML snippet, additional information is provided regarding the most specific DOM element
@ -408,7 +448,6 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
followingHtmlStartSnippet = '', followingHtmlStartSnippet = '',
fakeOl, offset; fakeOl, offset;
fromChildTraceAbs.shift(); fromChildTraceAbs.shift();
var previousHtml = this._serializePartialDomToChild(fragment, fromChildTraceAbs, false); var previousHtml = this._serializePartialDomToChild(fragment, fromChildTraceAbs, false);
toChildTraceAbs.shift(); toChildTraceAbs.shift();
@ -526,6 +565,16 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
return ret; return ret;
}; };
/*
* Convenience method that takes the html-attribute from an extractRangeByLineNumbers()-method,
* wraps it with the context and adds line numbers.
*/
this.formatDiffWithLineNumbers = function(diff, lineLength, firstLine) {
var text = diff.outerContextStart + diff.innerContextStart + diff.html + diff.innerContextEnd + diff.outerContextEnd;
text = lineNumberingService.insertLineNumbers(text, lineLength, null, null, firstLine);
return text;
};
/* /*
* This is a workardoun to prevent the last word of the inserted text from accidently being merged with the * This is a workardoun to prevent the last word of the inserted text from accidently being merged with the
* first word of the following line. * first word of the following line.
@ -537,13 +586,13 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
this._insertDanglingSpace = function(element) { this._insertDanglingSpace = function(element) {
if (element.childNodes.length > 0) { if (element.childNodes.length > 0) {
var lastChild = element.childNodes[element.childNodes.length - 1]; var lastChild = element.childNodes[element.childNodes.length - 1];
if (lastChild.nodeType == TEXT_NODE && !lastChild.nodeValue.match(/[\S]/) && element.childNodes.length > 1) { if (lastChild.nodeType === TEXT_NODE && !lastChild.nodeValue.match(/[\S]/) && element.childNodes.length > 1) {
// If the text node only contains whitespaces, chances are high it's just space between block elmeents, // If the text node only contains whitespaces, chances are high it's just space between block elmeents,
// like a line break between </LI> and </UL> // like a line break between </LI> and </UL>
lastChild = element.childNodes[element.childNodes.length - 2]; lastChild = element.childNodes[element.childNodes.length - 2];
} }
if (lastChild.nodeType == TEXT_NODE) { if (lastChild.nodeType === TEXT_NODE) {
if (lastChild.nodeValue === '' || lastChild.nodeValue.substr(-1) != ' ') { if (lastChild.nodeValue === '' || lastChild.nodeValue.substr(-1) !== ' ') {
lastChild.nodeValue += ' '; lastChild.nodeValue += ' ';
} }
} else { } else {
@ -674,7 +723,13 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
'&Auml;': 'Ä', '&Auml;': 'Ä',
'&Ouml;': 'Ö', '&Ouml;': 'Ö',
'&Uuml;': 'Ü', '&Uuml;': 'Ü',
'&szlig;': 'ß' '&szlig;': 'ß',
'&bdquo;': '„',
'&ldquo;': '“',
'&bull;': '•',
'&sect;': '§',
'&eacute;': 'é',
'&euro;': '€'
}; };
html = html.replace(/\s+<\/P>/gi, '</P>').replace(/\s+<\/DIV>/gi, '</DIV>').replace(/\s+<\/LI>/gi, '</LI>'); html = html.replace(/\s+<\/P>/gi, '</P>').replace(/\s+<\/DIV>/gi, '</DIV>').replace(/\s+<\/LI>/gi, '</LI>');
@ -693,6 +748,111 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
return html; return html;
}; };
this._getAllNextSiblings = function(element) {
var elements = [];
while (element.nextSibling) {
elements.push(element.nextSibling);
element = element.nextSibling;
}
return elements;
};
this._getAllPrevSiblingsReversed = function(element) {
var elements = [];
while (element.previousSibling) {
elements.push(element.previousSibling);
element = element.previousSibling;
}
return elements;
};
/**
* This returns the line number range in which changes (insertions, deletions) are encountered.
* As in extractRangeByLineNumbers(), "to" refers to the line breaking element at the end, i.e. the start of the following line.
*
* @param {string} diffHtml
*/
this.detectAffectedLineRange = function (diffHtml) {
var cacheKey = lineNumberingService.djb2hash(diffHtml),
cached = diffCache.get(cacheKey);
if (!angular.isUndefined(cached)) {
return cached;
}
var fragment = this.htmlToFragment(diffHtml);
this._insertInternalLineMarkers(fragment);
this._insertInternalLiNumbers(fragment);
var changes = fragment.querySelectorAll('ins, del, .insert, .delete'),
firstChange = changes.item(0),
lastChange = changes.item(changes.length - 1),
i, j;
if (!firstChange || !lastChange) {
// There are no changes
return null;
}
var firstTrace = this._getNodeContextTrace(firstChange),
lastLineNumberBefore = null;
for (j = firstTrace.length - 1; j >= 0 && lastLineNumberBefore === null; j--) {
var prevSiblings = this._getAllPrevSiblingsReversed(firstTrace[j]);
for (i = 0; i < prevSiblings.length && lastLineNumberBefore === null; i++) {
lastLineNumberBefore = this._getLastLineNumberNode(prevSiblings[i]);
}
}
var lastTrace = this._getNodeContextTrace(lastChange),
firstLineNumberAfter = null;
for (j = lastTrace.length - 1; j >= 0 && firstLineNumberAfter === null; j--) {
var nextSiblings = this._getAllNextSiblings(lastTrace[j]);
for (i = 0; i < nextSiblings.length && firstLineNumberAfter === null; i++) {
firstLineNumberAfter = this._getFirstLineNumberNode(nextSiblings[i]);
}
}
var range = {
"from": parseInt(lastLineNumberBefore.getAttribute("data-line-number")),
"to": parseInt(firstLineNumberAfter.getAttribute("data-line-number"))
};
diffCache.put(cacheKey, range);
return range;
};
/**
* Removes .delete-nodes and <del>-Tags (including content)
* Removes the .insert-classes and the wrapping <ins>-Tags (while maintaining content)
* @param html
*/
this.diffHtmlToFinalText = function(html) {
var fragment = this.htmlToFragment(html);
var delNodes = fragment.querySelectorAll('.delete, del');
for (var i = 0; i < delNodes.length; i++) {
delNodes[i].parentNode.removeChild(delNodes[i]);
}
var insNodes = fragment.querySelectorAll('ins');
for (i = 0; i < insNodes.length; i++) {
var ins = insNodes[i];
while (ins.childNodes.length > 0) {
var child = ins.childNodes.item(0);
ins.removeChild(child);
ins.parentNode.insertBefore(child, ins);
}
ins.parentNode.removeChild(ins);
}
var insertNodes = fragment.querySelectorAll('.insert');
for (i = 0;i < insertNodes.length; i++) {
this.removeCSSClass(insertNodes[i], 'insert');
}
return this._serializeDom(fragment, false);
};
/** /**
* @param {string} htmlOld * @param {string} htmlOld
* @param {string} htmlNew * @param {string} htmlNew
@ -702,13 +862,13 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
htmlOld = this._normalizeHtmlForDiff(htmlOld); htmlOld = this._normalizeHtmlForDiff(htmlOld);
htmlNew = this._normalizeHtmlForDiff(htmlNew); htmlNew = this._normalizeHtmlForDiff(htmlNew);
if (htmlOld == htmlNew) { if (htmlOld === htmlNew) {
return this.TYPE_REPLACEMENT; return this.TYPE_REPLACEMENT;
} }
var i, foundDiff; var i, foundDiff;
for (i = 0, foundDiff = false; i < htmlOld.length && i < htmlNew.length && foundDiff === false; i++) { for (i = 0, foundDiff = false; i < htmlOld.length && i < htmlNew.length && foundDiff === false; i++) {
if (htmlOld[i] != htmlNew[i]) { if (htmlOld[i] !== htmlNew[i]) {
foundDiff = true; foundDiff = true;
} }
} }
@ -718,11 +878,11 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
type = this.TYPE_REPLACEMENT; type = this.TYPE_REPLACEMENT;
if (remainderOld.length > remainderNew.length) { if (remainderOld.length > remainderNew.length) {
if (remainderOld.substr(remainderOld.length - remainderNew.length) == remainderNew) { if (remainderOld.substr(remainderOld.length - remainderNew.length) === remainderNew) {
type = this.TYPE_DELETION; type = this.TYPE_DELETION;
} }
} else if (remainderOld.length < remainderNew.length) { } else if (remainderOld.length < remainderNew.length) {
if (remainderNew.substr(remainderNew.length - remainderOld.length) == remainderOld) { if (remainderNew.substr(remainderNew.length - remainderOld.length) === remainderOld) {
type = this.TYPE_INSERTION; type = this.TYPE_INSERTION;
} }
} }
@ -867,7 +1027,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
} }
for (i in ns) { for (i in ns) {
if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { if (ns[i].rows.length === 1 && typeof(os[i]) !== "undefined" && os[i].rows.length === 1) {
newArr[ns[i].rows[0]] = {text: newArr[ns[i].rows[0]], row: os[i].rows[0]}; newArr[ns[i].rows[0]] = {text: newArr[ns[i].rows[0]], row: os[i].rows[0]};
oldArr[os[i].rows[0]] = {text: oldArr[os[i].rows[0]], row: ns[i].rows[0]}; oldArr[os[i].rows[0]] = {text: oldArr[os[i].rows[0]], row: ns[i].rows[0]};
} }
@ -1034,28 +1194,42 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
}; };
/** /**
* * @param {string} html
* @return {boolean}
* @private
*/
this._isValidInlineHtml = function(html) {
// If there are no HTML tags, we assume it's valid and skip further checks
if (!html.match(/<[^>]*>/)) {
return true;
}
// We check if this is a valid HTML that closes all its tags again using the innerHTML-Hack to correct
// the string and check if the number of HTML tags changes by this
var doc = document.createElement('div');
doc.innerHTML = html;
var tagsBefore = (html.match(/</g) || []).length;
var tagsCorrected = (doc.innerHTML.match(/</g) || []).length;
if (tagsBefore !== tagsCorrected) {
// The HTML has changed => it was not valid
return false;
}
// If there is any block element inside, we consider it as broken, as this string will be displayed
// inside of <ins>/<del> tags
if (html.match(/<(div|p|ul|li|blockquote)\W/i)) {
return false;
}
return true;
};
/**
* @param {string} html * @param {string} html
* @returns {boolean} * @returns {boolean}
* @private * @private
*/ */
this._diffDetectBrokenDiffHtml = function(html) { this._diffDetectBrokenDiffHtml = function(html) {
// If a regular HTML tag is enclosed by INS/DEL, the HTML is broken
var match = html.match(/<(ins|del)><[^>]*><\/(ins|del)>/gi);
if (match !== null && match.length > 0) {
return true;
}
// Opening tags, followed by </del> or </ins>, indicate broken HTML (if it's not a <ins> / <del>)
var brokenRegexp = /<(\w+)[^>]*><\/(ins|del)>/gi,
result;
while ((result = brokenRegexp.exec(html)) !== null) {
if (result[1].toLowerCase() !== 'ins' && result[1].toLowerCase() !== 'del') {
return true;
}
}
// If other HTML tags are contained within INS/DEL (e.g. "<ins>Test</p></ins>"), let's better be cautious // If other HTML tags are contained within INS/DEL (e.g. "<ins>Test</p></ins>"), let's better be cautious
// The "!!(found=...)"-construction is only used to make jshint happy :) // The "!!(found=...)"-construction is only used to make jshint happy :)
var findDel = /<del>(.*?)<\/del>/gi, var findDel = /<del>(.*?)<\/del>/gi,
@ -1069,7 +1243,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
} }
while (!!(found = findIns.exec(html))) { while (!!(found = findIns.exec(html))) {
inner = found[1].replace(/<br[^>]*>/gi, ''); inner = found[1].replace(/<br[^>]*>/gi, '');
if (inner.match(/<[^>]*>/)) { if (!this._isValidInlineHtml(inner)) {
return true; return true;
} }
} }
@ -1194,14 +1368,55 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
} }
}; };
/**
* This fixes a very specific, really weird bug that is tested in the test case "does not a change in a very specific case".
*
* @param {string}diffStr
* @return {string}
* @private
*/
this._fixWrongChangeDetection = function (diffStr) {
if (diffStr.indexOf('<del>') === -1 || diffStr.indexOf('<ins>') === -1) {
return diffStr;
}
var findDelGroupFinder = /(?:<del>.*?<\/del>)+/gi,
found,
returnStr = diffStr;
while (!!(found = findDelGroupFinder.exec(diffStr))) {
var del = found[0],
split = returnStr.split(del);
var findInsGroupFinder = /^(?:<ins>.*?<\/ins>)+/gi,
foundIns = findInsGroupFinder.exec(split[1]);
if (foundIns) {
var ins = foundIns[0];
var delShortened = del.replace(
/<del>((<BR CLASS="os-line-break"><\/del><del>)?(<span[^>]+os-line-number[^>]+?>)(\s|<\/?del>)*<\/span>)<\/del>/gi,
''
).replace(/<\/del><del>/g, '');
var insConv = ins.replace(/<ins>/g, '<del>').replace(/<\/ins>/g, '</del>').replace(/<\/del><del>/g, '');
if (delShortened.indexOf(insConv) !== -1) {
delShortened = delShortened.replace(insConv, '');
if (delShortened === '') {
returnStr = returnStr.replace(del + ins, del.replace(/<del>/g, '').replace(/<\/del>/g, ''));
}
}
}
}
return returnStr;
};
/** /**
* This function calculates the diff between two strings and tries to fix problems with the resulting HTML. * This function calculates the diff between two strings and tries to fix problems with the resulting HTML.
* If lineLength and firstLineNumber is given, line numbers will be returned es well * If lineLength and firstLineNumber is given, line numbers will be returned es well
* *
* @param {number} lineLength
* @param {number} firstLineNumber
* @param {string} htmlOld * @param {string} htmlOld
* @param {string} htmlNew * @param {string} htmlNew
* @param {number} lineLength - optional
* @param {number} firstLineNumber - optional
* @returns {string} * @returns {string}
*/ */
this.diff = function (htmlOld, htmlNew, lineLength, firstLineNumber) { this.diff = function (htmlOld, htmlNew, lineLength, firstLineNumber) {
@ -1241,6 +1456,9 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
var str = this._diffString(workaroundPrepend + htmlOld, workaroundPrepend + htmlNew), var str = this._diffString(workaroundPrepend + htmlOld, workaroundPrepend + htmlNew),
diffUnnormalized = str.replace(/^\s+/g, '').replace(/\s+$/g, '').replace(/ {2,}/g, ' '); diffUnnormalized = str.replace(/^\s+/g, '').replace(/\s+$/g, '').replace(/ {2,}/g, ' ');
diffUnnormalized = this._fixWrongChangeDetection(diffUnnormalized);
// Remove <del> tags that only delete line numbers // Remove <del> tags that only delete line numbers
// We need to do this before removing </del><del> as done in one of the next statements // We need to do this before removing </del><del> as done in one of the next statements
diffUnnormalized = diffUnnormalized.replace( diffUnnormalized = diffUnnormalized.replace(
@ -1287,7 +1505,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
remainderOld = oldText, remainderNew = newText; remainderOld = oldText, remainderNew = newText;
while (remainderOld.length > 0 && remainderNew.length > 0 && !foundDiff) { while (remainderOld.length > 0 && remainderNew.length > 0 && !foundDiff) {
if (remainderOld[0] == remainderNew[0]) { if (remainderOld[0] === remainderNew[0]) {
commonStart += remainderOld[0]; commonStart += remainderOld[0];
remainderOld = remainderOld.substr(1); remainderOld = remainderOld.substr(1);
remainderNew = remainderNew.substr(1); remainderNew = remainderNew.substr(1);
@ -1298,7 +1516,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
foundDiff = false; foundDiff = false;
while (remainderOld.length > 0 && remainderNew.length > 0 && !foundDiff) { while (remainderOld.length > 0 && remainderNew.length > 0 && !foundDiff) {
if (remainderOld[remainderOld.length - 1] == remainderNew[remainderNew.length - 1]) { if (remainderOld[remainderOld.length - 1] === remainderNew[remainderNew.length - 1]) {
commonEnd = remainderOld[remainderOld.length - 1] + commonEnd; commonEnd = remainderOld[remainderOld.length - 1] + commonEnd;
remainderNew = remainderNew.substr(0, remainderNew.length - 1); remainderNew = remainderNew.substr(0, remainderNew.length - 1);
remainderOld = remainderOld.substr(0, remainderOld.length - 1); remainderOld = remainderOld.substr(0, remainderOld.length - 1);

View File

@ -75,7 +75,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
this._isOsLineBreakNode = function (node) { this._isOsLineBreakNode = function (node) {
var isLineBreak = false; var isLineBreak = false;
if (node && node.nodeType === ELEMENT_NODE && node.nodeName == 'BR' && node.hasAttribute('class')) { if (node && node.nodeType === ELEMENT_NODE && node.nodeName === 'BR' && node.hasAttribute('class')) {
var classes = node.getAttribute('class').split(' '); var classes = node.getAttribute('class').split(' ');
if (classes.indexOf('os-line-break') > -1) { if (classes.indexOf('os-line-break') > -1) {
isLineBreak = true; isLineBreak = true;
@ -86,7 +86,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
this._isOsLineNumberNode = function (node) { this._isOsLineNumberNode = function (node) {
var isLineNumber = false; var isLineNumber = false;
if (node && node.nodeType === ELEMENT_NODE && node.nodeName == 'SPAN' && node.hasAttribute('class')) { if (node && node.nodeType === ELEMENT_NODE && node.nodeName === 'SPAN' && node.hasAttribute('class')) {
var classes = node.getAttribute('class').split(' '); var classes = node.getAttribute('class').split(' ');
if (classes.indexOf('os-line-number') > -1) { if (classes.indexOf('os-line-number') > -1) {
isLineNumber = true; isLineNumber = true;
@ -189,13 +189,16 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
out.push(node); out.push(node);
return node; return node;
}; };
var addLinebreakToPreviousNode = function (node, offset, highlight) { var addLinebreakToPreviousNode = function (node, offset) {
var firstText = node.nodeValue.substr(0, offset + 1), var firstText = node.nodeValue.substr(0, offset + 1),
secondText = node.nodeValue.substr(offset + 1); secondText = node.nodeValue.substr(offset + 1);
var lineBreak = service._createLineBreak(); var lineBreak = service._createLineBreak();
var firstNode = document.createTextNode(firstText); var firstNode = document.createTextNode(firstText);
node.parentNode.insertBefore(firstNode, node); node.parentNode.insertBefore(firstNode, node);
node.parentNode.insertBefore(lineBreak, node); node.parentNode.insertBefore(lineBreak, node);
if (service._currentLineNumber !== null) {
node.parentNode.insertBefore(service._createLineNumber(), node);
}
node.nodeValue = secondText; node.nodeValue = secondText;
}; };
@ -244,7 +247,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
} else { } else {
// The last possible breaking point was not in this text not, but one we have already passed // The last possible breaking point was not in this text not, but one we have already passed
var remainderOfPrev = lineBreakAt.node.nodeValue.length - lineBreakAt.offset - 1; var remainderOfPrev = lineBreakAt.node.nodeValue.length - lineBreakAt.offset - 1;
addLinebreakToPreviousNode(lineBreakAt.node, lineBreakAt.offset, highlight); addLinebreakToPreviousNode(lineBreakAt.node, lineBreakAt.offset);
this._currentInlineOffset = i + remainderOfPrev; this._currentInlineOffset = i + remainderOfPrev;
this._lastInlineBreakablePoint = null; this._lastInlineBreakablePoint = null;
@ -298,7 +301,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
if (!node.firstChild) { if (!node.firstChild) {
return 0; return 0;
} }
if (node.firstChild.nodeType == TEXT_NODE) { if (node.firstChild.nodeType === TEXT_NODE) {
var parts = node.firstChild.nodeValue.split(' '); var parts = node.firstChild.nodeValue.split(' ');
return parts[0].length; return parts[0].length;
} else { } else {
@ -317,12 +320,12 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
} }
for (i = 0; i < oldChildren.length; i++) { for (i = 0; i < oldChildren.length; i++) {
if (oldChildren[i].nodeType == TEXT_NODE) { if (oldChildren[i].nodeType === TEXT_NODE) {
var ret = this._textNodeToLines(oldChildren[i], length, highlight); var ret = this._textNodeToLines(oldChildren[i], length, highlight);
for (var j = 0; j < ret.length; j++) { for (var j = 0; j < ret.length; j++) {
node.appendChild(ret[j]); node.appendChild(ret[j]);
} }
} else if (oldChildren[i].nodeType == ELEMENT_NODE) { } else if (oldChildren[i].nodeType === ELEMENT_NODE) {
var firstword = this._lengthOfFirstInlineWord(oldChildren[i]), var firstword = this._lengthOfFirstInlineWord(oldChildren[i]),
overlength = ((this._currentInlineOffset + firstword) > length && this._currentInlineOffset > 0); overlength = ((this._currentInlineOffset + firstword) > length && this._currentInlineOffset > 0);
if (overlength && this._isInlineElement(oldChildren[i])) { if (overlength && this._isInlineElement(oldChildren[i])) {
@ -399,7 +402,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
} }
for (i = 0; i < oldChildren.length; i++) { for (i = 0; i < oldChildren.length; i++) {
if (oldChildren[i].nodeType == TEXT_NODE) { if (oldChildren[i].nodeType === TEXT_NODE) {
if (!oldChildren[i].nodeValue.match(/\S/)) { if (!oldChildren[i].nodeValue.match(/\S/)) {
// White space nodes between block elements should be ignored // White space nodes between block elements should be ignored
var prevIsBlock = (i > 0 && !this._isInlineElement(oldChildren[i - 1])); var prevIsBlock = (i > 0 && !this._isInlineElement(oldChildren[i - 1]));
@ -413,7 +416,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
for (var j = 0; j < ret.length; j++) { for (var j = 0; j < ret.length; j++) {
node.appendChild(ret[j]); node.appendChild(ret[j]);
} }
} else if (oldChildren[i].nodeType == ELEMENT_NODE) { } else if (oldChildren[i].nodeType === ELEMENT_NODE) {
var firstword = this._lengthOfFirstInlineWord(oldChildren[i]), var firstword = this._lengthOfFirstInlineWord(oldChildren[i]),
overlength = ((this._currentInlineOffset + firstword) > length && this._currentInlineOffset > 0); overlength = ((this._currentInlineOffset + firstword) > length && this._currentInlineOffset > 0);
if (overlength && this._isInlineElement(oldChildren[i]) && !this._isIgnoredByLineNumbering(oldChildren[i])) { if (overlength && this._isInlineElement(oldChildren[i]) && !this._isIgnoredByLineNumbering(oldChildren[i])) {
@ -491,7 +494,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
/** /**
* *
* @param {string} html * @param {string} html
* @param {number} lineLength * @param {number|string} lineLength
* @param {number|null} highlight - optional * @param {number|null} highlight - optional
* @param {number|null} firstLine * @param {number|null} firstLine
*/ */
@ -588,6 +591,115 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
return root.innerHTML; return root.innerHTML;
}; };
/**
* @param {string} html
* @returns {object}
* {"from": 23, "to": 42} ; "to" refers to the line breaking element at the end of the last line,
* i.e. the line number of the following line
*/
this.getLineNumberRange = function (html) {
var fragment = this._htmlToFragment(html),
range = {
"from": null,
"to": null
};
var lineNumbers = fragment.querySelectorAll('.os-line-number');
for (var i = 0; i < lineNumbers.length; i++) {
var node = lineNumbers.item(i);
var number = parseInt(node.getAttribute("data-line-number"));
if (range.from === null || number < range.from) {
range.from = number;
}
if (range.to === null || (number + 1) > range.to) {
range.to = number + 1;
}
}
return range;
};
/**
* @param {string} html
*/
this.getHeadingsWithLineNumbers = function (html) {
var fragment = this._htmlToFragment(html),
headings = [];
var headingNodes = fragment.querySelectorAll('h1, h2, h3, h4, h5, h6');
for (var i = 0; i < headingNodes.length; i++) {
var heading = headingNodes.item(i);
var linenumbers = heading.querySelectorAll('.os-line-number');
if (linenumbers.length > 0) {
var number = parseInt(linenumbers.item(0).getAttribute("data-line-number"));
headings.push({
"lineNumber": number,
"level": parseInt(heading.nodeName.substr(1)),
"text": heading.innerText.replace(/^\s/, "").replace(/\s$/, "")
});
}
}
return headings.sort(function(heading1, heading2) {
if (heading1.lineNumber < heading2.lineNumber) {
return 0;
} else if (heading1.lineNumber > heading2.lineNumber) {
return 1;
} else {
return 0;
}
});
};
/**
* @param {Element} node
* @returns {array}
* @private
*/
this._splitNodeToParagraphs = function (node) {
var elements = [];
for (var i = 0; i < node.childNodes.length; i++) {
var childNode = node.childNodes.item(i);
if (childNode.nodeType === TEXT_NODE) {
continue;
}
if (childNode.nodeName === 'UL' || childNode.nodeName === 'OL') {
var start = 1;
if (childNode.getAttribute("start") !== null) {
start = parseInt(childNode.getAttribute("start"));
}
for (var j = 0; j < childNode.childNodes.length; j++) {
if (childNode.childNodes.item(j).nodeType === TEXT_NODE) {
continue;
}
var newParent = childNode.cloneNode(false);
if (childNode.nodeName === 'OL') {
newParent.setAttribute('start', start);
}
newParent.appendChild(childNode.childNodes.item(j).cloneNode(true));
elements.push(newParent);
start++;
}
} else {
elements.push(childNode);
}
}
return elements;
};
/**
* Splitting the text into paragraphs:
* - Each root-level-element is considered as a paragraph.
* Inline-elements at root-level are not expected and treated as block elements.
* Text-nodes at root-level are not expected and ignored. Every text needs to be wrapped e.g. by <p> or <div>.
* - If a UL or OL is encountered, paragraphs are defined by the child-LI-elements.
* List items of nested lists are not considered as a paragraph of their own.
*
* @param {string} html
* @return {string[]}
*/
this.splitToParagraphs = function (html) {
var fragment = this._htmlToFragment(html);
return this._splitNodeToParagraphs(fragment).map(function(node) { return node.outerHTML; });
};
/** /**
* Traverses up the DOM tree until it finds a node with a nextSibling, then returns that sibling * Traverses up the DOM tree until it finds a node with a nextSibling, then returns that sibling
* *

View File

@ -391,10 +391,11 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
'$interval', '$interval',
'$timeout', '$timeout',
function (Motion, MotionChangeRecommendation, Config, lineNumberingService, diffService, $interval, $timeout) { function (Motion, MotionChangeRecommendation, Config, lineNumberingService, diffService, $interval, $timeout) {
var $scope; var $scope, motion;
var obj = { var obj = {
mode: 'original' mode: 'original',
context: null
}; };
obj.diffFormatterCb = function (change, oldFragment, newFragment) { obj.diffFormatterCb = function (change, oldFragment, newFragment) {
@ -430,7 +431,7 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
MotionChangeRecommendation.destroy(changeId); MotionChangeRecommendation.destroy(changeId);
}; };
obj.rejectAll = function (motion) { obj.rejectAllChangeRecommendations = function (motion) {
var changeRecommendations = MotionChangeRecommendation.filter({ var changeRecommendations = MotionChangeRecommendation.filter({
'where': {'motion_version_id': {'==': motion.active_version}} 'where': {'motion_version_id': {'==': motion.active_version}}
}); });
@ -508,8 +509,99 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
}, 0, false); }, 0, false);
}; };
obj.init = function (_scope, viewMode) { // $scope.amendments_crs holds the change objects of all change recommendations regarding the text,
// and all amendments with a "accepted"-recommendation, ordered by the first affected line number.
obj.set_amendments_crs_watcher = function($scope, motion) {
$scope.amendments_crs = [];
$scope.change_recommendations = [];
$scope.paragraph_amendments = [];
$scope.has_proposed_changes = false;
$scope.changed_version_has_collissions = false;
var rebuild_amendments_crs = function () {
$scope.amendments_crs = $scope.change_recommendations.map(function (cr) {
return cr.getUnifiedChangeObject();
}).concat(
$scope.paragraph_amendments.map(function (amendment) {
return amendment.getUnifiedChangeObject();
})
);
$scope.amendments_crs.sort(function (change1, change2) {
if (change1.line_from > change2.line_from) {
return 1;
} else if (change1.line_from < change2.line_from) {
return -1;
} else {
return 0;
}
});
// Set all crs and amendments for collission detection.
_.forEach($scope.amendments_crs, function (change) {
change.setOtherChangesForCollission($scope.amendments_crs);
});
$scope.has_proposed_changes = ($scope.amendments_crs.length > 0);
$scope.changed_version_has_accepted_collissions = ($scope.amendments_crs.find(function(change) {
return (change.getCollissions(true).length !== 0);
}) !== undefined);
if (obj.context === 'site') {
if (!$scope.has_proposed_changes) {
$scope.setProjectionMode($scope.projectionModes[0]);
}
if ($scope.has_proposed_changes) {
$scope.disableMotionInlineEditing();
}
}
};
$scope.$watch(function () {
return MotionChangeRecommendation.lastModified();
}, function () {
$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;
}
});
rebuild_amendments_crs();
});
$scope.$watch(function () {
return Motion.lastModified();
}, function () {
$scope.paragraph_amendments = motion.getParagraphBasedAmendmentsForDiffView();
rebuild_amendments_crs();
});
};
obj.setVersion = function (_motion/*, _version*/) {
motion = _motion;
};
obj.initProjector = function (_scope, _motion, viewMode) {
obj.context = 'projector';
$scope = _scope; $scope = _scope;
motion = _motion;
obj.set_amendments_crs_watcher($scope, motion);
obj.mode = viewMode;
};
obj.initSite = function (_scope, _motion, viewMode) {
obj.context = 'site';
$scope = _scope;
motion = _motion;
obj.set_amendments_crs_watcher($scope, motion);
$scope.$evalAsync(function() { $scope.$evalAsync(function() {
obj.repositionOriginalAnnotations(); obj.repositionOriginalAnnotations();
}); });
@ -518,12 +610,12 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
}, obj.repositionOriginalAnnotations); }, obj.repositionOriginalAnnotations);
var checkGotoOriginal = function () { var checkGotoOriginal = function () {
if ($scope.change_recommendations.length === 0 && $scope.title_change_recommendation === null) { if ($scope.amendments_crs.length === 0 && $scope.title_change_recommendation === null) {
obj.mode = 'original'; obj.mode = 'original';
} }
}; };
$scope.$watch(function () { $scope.$watch(function () {
return $scope.change_recommendations.length; return $scope.amendments_crs.length;
}, checkGotoOriginal); }, checkGotoOriginal);
$scope.$watch(function () { $scope.$watch(function () {
return $scope.title_change_recommendation; return $scope.title_change_recommendation;
@ -535,7 +627,7 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
var $holder = $(".motion-text-original"), var $holder = $(".motion-text-original"),
newHeight = $holder.height(), newHeight = $holder.height(),
classes = $holder.attr("class"); classes = $holder.attr("class");
if (newHeight != sizeCheckerLastSize || sizeCheckerLastClass != classes) { if (newHeight !== sizeCheckerLastSize || sizeCheckerLastClass !== classes) {
sizeCheckerLastSize = newHeight; sizeCheckerLastSize = newHeight;
sizeCheckerLastClass = classes; sizeCheckerLastClass = classes;
obj.repositionOriginalAnnotations(); obj.repositionOriginalAnnotations();

View File

@ -327,7 +327,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
// motion title // motion title
var motionTitle = function() { var motionTitle = function() {
if (params.include.text) { if (params.include.text && !motion.isParagraphBasedAmendment()) {
return [{ return [{
text: titlePlain, text: titlePlain,
style: 'heading3' style: 'heading3'
@ -337,30 +337,45 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
// motion preamble // motion preamble
var motionPreamble = function () { var motionPreamble = function () {
if (params.include.text) {
return { return {
text: Config.translate(Config.get('motions_preamble').value), text: Config.translate(Config.get('motions_preamble').value),
margin: [0, 10, 0, 0] margin: [0, 10, 0, 0]
}; };
}
}; };
var escapeHtml = function(text) { var escapeHtml = function(text) {
return text.replace(/&/, "&amp;").replace(/</, "&lt;").replace(/>/, "&gt;"); return text.replace(/&/, '&amp;').replace(/</, '&lt;').replace(/>/, '&gt;');
}; };
// motion text (with line-numbers) // motion text (with line-numbers)
var motionText = function() { var motionText = function() {
var content = [];
if (params.include.text) { if (params.include.text) {
var motionTextContent = ''; var motionTextContent = '';
if (motion.isParagraphBasedAmendment()) {
// paragraph based amendment
var diffs = motion.getAmendmentParagraphsLinesDiff();
if (diffs.length) {
content.push(motionPreamble());
_.forEach(diffs, function (diff) {
motionTextContent += diff.textPre + diff.text + diff.textPost;
});
} else {
motionTextContent += gettextCatalog.getString('No changes at the text.');
}
} else {
// lead motion or normal amendment
content.push(motionPreamble());
var titleChange = motion.getTitleChangeRecommendation(); var titleChange = motion.getTitleChangeRecommendation();
if (params.changeRecommendationMode === 'diff' && titleChange) { if (params.changeRecommendationMode === 'diff' && titleChange) {
motionTextContent += '<p><strong>' + gettextCatalog.getString('New title') + ':</strong> ' + motionTextContent += '<p><strong>' + gettextCatalog.getString('New title') + ':</strong> ' +
escapeHtml(titleChange.text) + '</p>'; escapeHtml(titleChange.text) + '</p>';
} }
motionTextContent += motion.getTextByMode(params.changeRecommendationMode, motionVersion); motionTextContent += motion.getTextByMode(params.changeRecommendationMode, motionVersion);
return converter.convertHTML(motionTextContent, params.lineNumberMode);
} }
content.push(converter.convertHTML(motionTextContent, params.lineNumberMode));
}
return content;
}; };
// motion reason heading // motion reason heading
@ -421,10 +436,10 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
title, title,
subtitle, subtitle,
metaTable(), metaTable(),
motionTitle(), motionTitle()
motionPreamble(),
motionText(),
]; ];
content = content.concat(motionText());
var reason = motionReason(); var reason = motionReason();
if (reason) { if (reason) {
content.push(reason); content.push(reason);
@ -965,6 +980,210 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
} }
]) ])
.factory('AmendmentContentProvider', [
'$q',
'ImageConverter',
'PdfMakeConverter',
'HTMLValidizer',
'PDFLayout',
'Config',
'gettextCatalog',
function ($q, ImageConverter, PdfMakeConverter, HTMLValidizer, PDFLayout, Config, gettextCatalog) {
var createInstance = function (motions) {
motions = _.filter(motions, function (motion) {
return motion.parent_id;
});
var converter, imageMap = {};
// Query all image sources from motion text and reason
var getImageSources = function () {
var sources = [];
_.forEach(motions, function (motion) {
var text = motion.getText();
var reason = motion.getReason();
var content = HTMLValidizer.validize(text) + HTMLValidizer.validize(motion.getReason());
_.forEach($(content).find('img'), function (element) {
sources.push(element.getAttribute('src'));
});
});
return _.uniq(sources);
};
var createBundleContent = function (bundle) {
return _.flatten(_.map(bundle, function (motion) {
var content = [];
// get diffs and title of the changed motions
var motionText;
var title = motion.identifier ? gettextCatalog.getString('Motion') + ' ' + motion.identifier : motion.getTitle();
if (motion.isParagraphBasedAmendment()) {
// get changed parts
var paragraphs = motion.getAmendmentParagraphsLinesDiff();
if (paragraphs.length) {
// Put the changed lines into the info column
var p = paragraphs[0];
title += ' (' + gettextCatalog.getString('Line') + ' ';
if (p.diffLineTo === p.diffLineFrom + 1) {
title += p.diffLineFrom;
} else {
title += p.diffLineFrom + '-' + p.diffLineTo;
}
title += ')';
// get the diff
motionText = p.text;
} else {
motionText = gettextCatalog.getString('No changes at the text.');
}
} else { // 'normal' amendment
motionText = motion.getText();
}
content.push({
text: title,
style: 'heading3',
marginTop: 15,
});
// submitters
var submitters = _.map(motion.submitters, function (submitter) {
return submitter.get_full_name();
}).join(', ');
content.push({
text: gettextCatalog.getString('Submitters') + ': ' + submitters,
});
// state
content.push({
text: gettextCatalog.getString('State') + ': ' + motion.getStateName(),
});
// recommendation
var recommendations_by = Config.get('motions_recommendations_by').value;
var recommendation = motion.getRecommendationName();
if (recommendations_by && recommendation) {
content.push({
text: recommendations_by + ': ' + recommendation,
});
}
return _.concat(content, converter.convertHTML(motionText, 'outside'));
}));
};
var getBundleContent = function (bundle) {
var leadMotion = bundle[0].getParentMotion();
// title
var title = leadMotion.identifier ? ' ' + leadMotion.identifier : '';
title += ': ' + leadMotion.getTitle();
title = PDFLayout.createTitle(gettextCatalog.getString('Amendments of motion') + title);
var content = [title],
foundAmendments = [];
var headings = leadMotion.getTextHeadings().map(function(heading) {
heading.amendments = [];
return heading;
});
bundle.forEach(function(amendment) {
var headingIdx = null;
var changes = amendment.getAmendmentParagraphsByMode('diff');
if (changes.length === 0) {
return;
}
var amendmentLineNumber = changes[0].lineFrom;
for (var i = 0; i < headings.length; i++) {
if (headings[i].lineNumber <= amendmentLineNumber) {
headingIdx = i;
}
}
if (headingIdx !== null) {
headings[headingIdx].amendments.push(amendment);
foundAmendments.push(amendment.id);
}
});
headings.forEach(function(heading) {
if (heading.amendments.length === 0) {
return;
}
content.push({
text: heading.text,
style: "heading2",
marginTop: 25,
});
content = _.concat(content, createBundleContent(heading.amendments));
});
// If there was an amendment that did not have a heading, we append it at the bottom
var missedAmendments = [];
bundle.forEach(function(amendment) {
if (foundAmendments.indexOf(amendment.id) === -1) {
missedAmendments.push(amendment);
}
});
if (missedAmendments.length > 0) {
content = _.concat(content, createBundleContent(missedAmendments));
}
return content;
};
// Generates content as a pdfmake consumable
var getContent = function() {
if (motions.length === 0) {
return [];
}
// Creates bundles of motions. All motions with the same parent are bundled together
// respecting the order, in which they are sorted.
// motionBundles is an array containing Arrays of motions with the same parent.
var parentId = motions[0].parent_id;
var motionBundles = [];
var currentBundle = [];
_.forEach(motions, function (motion) {
if (motion.parent_id === parentId) {
currentBundle.push(motion);
} else {
motionBundles.push(currentBundle);
currentBundle = [motion];
parentId = motion.parent_id;
}
});
motionBundles.push(currentBundle);
// Make the amendment table for each motion bundle.
return _.map(motionBundles, function (bundle, index) {
var content = getBundleContent(bundle);
if (index < motionBundles.length - 1) {
content.push(PDFLayout.addPageBreak());
}
return content;
});
};
var getImageMap = function() {
return imageMap;
};
return $q(function (resolve) {
ImageConverter.toBase64(getImageSources()).then(function (_imageMap) {
imageMap = _imageMap;
converter = PdfMakeConverter.createInstance(_imageMap);
resolve({
getContent: getContent,
getImageMap: getImageMap,
});
});
});
};
return {
createInstance: createInstance,
};
}
])
.factory('MotionPdfExport', [ .factory('MotionPdfExport', [
'$http', '$http',
'$q', '$q',
@ -980,6 +1199,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
'PollContentProvider', 'PollContentProvider',
'PdfMakeBallotPaperProvider', 'PdfMakeBallotPaperProvider',
'MotionPartialContentProvider', 'MotionPartialContentProvider',
'AmendmentContentProvider',
'PdfCreate', 'PdfCreate',
'PDFLayout', 'PDFLayout',
'PersonalNoteManager', 'PersonalNoteManager',
@ -988,8 +1208,8 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
'FileSaver', 'FileSaver',
function ($http, $q, operator, Config, gettextCatalog, MotionChangeRecommendation, HTMLValidizer, function ($http, $q, operator, Config, gettextCatalog, MotionChangeRecommendation, HTMLValidizer,
PdfMakeConverter, MotionContentProvider, MotionCatalogContentProvider, PdfMakeDocumentProvider, PdfMakeConverter, MotionContentProvider, MotionCatalogContentProvider, PdfMakeDocumentProvider,
PollContentProvider, PdfMakeBallotPaperProvider, MotionPartialContentProvider, PdfCreate, PollContentProvider, PdfMakeBallotPaperProvider, MotionPartialContentProvider, AmendmentContentProvider,
PDFLayout, PersonalNoteManager, MotionComment, Messaging, FileSaver) { PdfCreate, PDFLayout, PersonalNoteManager, MotionComment, Messaging, FileSaver) {
return { return {
getDocumentProvider: function (motions, params, singleMotion) { getDocumentProvider: function (motions, params, singleMotion) {
params = _.clone(params || {}); // Clone this to avoid sideeffects. params = _.clone(params || {}); // Clone this to avoid sideeffects.
@ -1158,6 +1378,13 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
}); });
} }
}, },
exportAmendments: function (motions, filename) {
AmendmentContentProvider.createInstance(motions).then(function (contentProvider) {
PdfMakeDocumentProvider.createInstance(contentProvider).then(function (documentProvider) {
PdfCreate.download(documentProvider, filename);
});
});
},
}; };
} }
]); ]);

View File

@ -4,6 +4,7 @@
angular.module('OpenSlidesApp.motions.projector', [ angular.module('OpenSlidesApp.motions.projector', [
'OpenSlidesApp.motions', 'OpenSlidesApp.motions',
'OpenSlidesApp.motions.motionservices',
'OpenSlidesApp.motions.motionBlockProjector', 'OpenSlidesApp.motions.motionBlockProjector',
]) ])
@ -18,17 +19,20 @@ angular.module('OpenSlidesApp.motions.projector', [
.controller('SlideMotionCtrl', [ .controller('SlideMotionCtrl', [
'$scope', '$scope',
'Config',
'Motion', 'Motion',
'MotionChangeRecommendation', 'MotionChangeRecommendation',
'ChangeRecommendationView',
'User', 'User',
'Notify', 'Notify',
'ProjectorID', 'ProjectorID',
function($scope, Motion, MotionChangeRecommendation, User, Notify, ProjectorID) { function($scope, Config, Motion, MotionChangeRecommendation, ChangeRecommendationView, User, Notify, ProjectorID) {
// Attention! Each object that is used here has to be dealt on server side. // Attention! Each object that is used here has to be dealt on server side.
// Add it to the coresponding get_requirements method of the ProjectorElement // Add it to the coresponding get_requirements method of the ProjectorElement
// class. // class.
var id = $scope.element.id; var motionId = $scope.element.id;
$scope.mode = $scope.element.mode || 'original'; $scope.mode = $scope.element.mode || 'original';
$scope.lineNumberMode = Config.get('motions_default_line_numbering').value;
var notifyNamePrefix = 'projector_' + ProjectorID() + '_motion_line_'; var notifyNamePrefix = 'projector_' + ProjectorID() + '_motion_line_';
var callbackId = Notify.registerCallback(notifyNamePrefix + 'request', function (params) { var callbackId = Notify.registerCallback(notifyNamePrefix + 'request', function (params) {
@ -55,27 +59,19 @@ angular.module('OpenSlidesApp.motions.projector', [
Notify.deregisterCallback(callbackId); Notify.deregisterCallback(callbackId);
}); });
Motion.bindOne(id, $scope, 'motion');
User.bindAll({}, $scope, 'users'); User.bindAll({}, $scope, 'users');
$scope.$watch(function () { $scope.$watch(function () {
return MotionChangeRecommendation.lastModified(); return Motion.lastModified(motionId);
}, function () { }, function () {
$scope.change_recommendations = []; $scope.motion = Motion.get(motionId);
$scope.title_change_recommendation = null; $scope.amendment_diff_paragraphs = $scope.motion.getAmendmentParagraphsLinesDiff();
if ($scope.motion) { $scope.viewChangeRecommendations.setVersion($scope.motion, $scope.motion.active_version);
MotionChangeRecommendation.filter({
'where': {'motion_version_id': {'==': $scope.motion.active_version}}
}).forEach(function(change) {
if (change.isTextRecommendation()) {
$scope.change_recommendations.push(change);
}
if (change.isTitleRecommendation()) {
$scope.title_change_recommendation = change;
}
});
}
}); });
// Change recommendation viewing
$scope.viewChangeRecommendations = ChangeRecommendationView;
$scope.viewChangeRecommendations.initProjector($scope, Motion.get(motionId), $scope.mode);
} }
]); ]);

View File

@ -102,6 +102,21 @@ angular.module('OpenSlidesApp.motions.site', [
basePerm: 'motions.can_manage', basePerm: 'motions.can_manage',
}, },
}) })
.state('motions.motion.amendment-list', {
url: '/{id:int}/amendments',
controller: 'MotionAmendmentListStateCtrl',
params: {
motionId: null,
},
})
.state('motions.motion.allamendments', {
url: '/amendments',
templateUrl: 'static/templates/motions/motion-amendment-list.html',
controller: 'MotionAmendmentListStateCtrl',
resolve: {
motionId: function() { return void 0; },
}
})
.state('motions.motion.import', { .state('motions.motion.import', {
url: '/import', url: '/import',
controller: 'MotionImportCtrl', controller: 'MotionImportCtrl',
@ -365,6 +380,28 @@ angular.module('OpenSlidesApp.motions.site', [
} }
]) ])
// Service for choosing the paragraph of a given motion that is to be amended
.factory('AmendmentParagraphChooseForm', [
function () {
return {
// ngDialog for motion form
getDialog: function (motion, successCb) {
return {
template: 'static/templates/motions/amendment-paragraph-choose-form.html',
controller: 'AmendmentParagraphChooseCtrl',
className: 'ngdialog-theme-default wide-form',
closeByEscape: false,
closeByDocument: false,
resolve: {
motion: function () { return motion; },
successCb: function() { return successCb; },
}
};
}
};
}
])
// Service for generic motion form (create and update) // Service for generic motion form (create and update)
.factory('MotionForm', [ .factory('MotionForm', [
'$filter', '$filter',
@ -385,7 +422,11 @@ angular.module('OpenSlidesApp.motions.site', [
Config, Mediafile, MotionBlock, Tag, User, Workflow, Agenda, AgendaTree) { Config, Mediafile, MotionBlock, Tag, User, Workflow, Agenda, AgendaTree) {
return { return {
// ngDialog for motion form // ngDialog for motion form
getDialog: function (motion) { // If motion is given and not null, we're editing an already existing motion
// If parentMotion is give, we're dealing with an amendment
// If paragraphNo is given as well, the amendment is paragraph-based
// If paragraphTextPre is given, we're creating a modified version of another paragraph-based amendment
getDialog: function (motion, parentMotion, paragraphNo, paragraphTextPre) {
return { return {
template: 'static/templates/motions/motion-form.html', template: 'static/templates/motions/motion-form.html',
controller: motion ? 'MotionUpdateCtrl' : 'MotionCreateCtrl', controller: motion ? 'MotionUpdateCtrl' : 'MotionCreateCtrl',
@ -394,11 +435,14 @@ angular.module('OpenSlidesApp.motions.site', [
closeByDocument: false, closeByDocument: false,
resolve: { resolve: {
motionId: function () {return motion ? motion.id : void 0;}, motionId: function () {return motion ? motion.id : void 0;},
}, parentMotion: function () {return parentMotion;},
paragraphNo: function () {return paragraphNo;},
paragraphTextPre: function () {return paragraphTextPre;}
}
}; };
}, },
// angular-formly fields for motion form // angular-formly fields for motion form
getFormFields: function (isCreateForm) { getFormFields: function (isCreateForm, isParagraphBasedAmendment) {
var workflows = Workflow.getAll(); var workflows = Workflow.getAll();
var images = Mediafile.getAllImages(); var images = Mediafile.getAllImages();
var formFields = []; var formFields = [];
@ -432,7 +476,8 @@ angular.module('OpenSlidesApp.motions.site', [
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Title'), label: gettextCatalog.getString('Title'),
required: true required: true
} },
hide: isParagraphBasedAmendment && isCreateForm
}, },
{ {
template: '<p class="spacer-top-lg no-padding">' + Config.translate(Config.get('motions_preamble').value) + '</p>' template: '<p class="spacer-top-lg no-padding">' + Config.translate(Config.get('motions_preamble').value) + '</p>'
@ -442,7 +487,7 @@ angular.module('OpenSlidesApp.motions.site', [
type: 'editor', type: 'editor',
templateOptions: { templateOptions: {
label: gettextCatalog.getString('Text'), label: gettextCatalog.getString('Text'),
required: true required: !isParagraphBasedAmendment // Deleting the whole paragraph in an amendment should be possible
}, },
data: { data: {
ckeditorOptions: Editor.getOptions() ckeditorOptions: Editor.getOptions()
@ -612,6 +657,34 @@ angular.module('OpenSlidesApp.motions.site', [
} }
]) ])
.factory('MotionCommentForm', [
'MotionComment',
function (MotionComment) {
return {
// ngDialog for motion comment form
getDialog: function (motion, commentFieldId) {
return {
template: 'static/templates/motions/motion-comment-form.html',
controller: 'MotionCommentCtrl',
className: 'ngdialog-theme-default wide-form',
closeByEscape: false,
closeByDocument: false,
resolve: {
motionId: function () {return motion.id;},
commentFieldId: function () {return commentFieldId;},
},
};
},
// angular-formly fields for motion comment form
getFormFields: function (commentFieldId) {
return [
MotionComment.getFormField(commentFieldId)
];
},
};
}
])
.factory('CategoryForm', [ .factory('CategoryForm', [
'gettextCatalog', 'gettextCatalog',
function (gettextCatalog) { function (gettextCatalog) {
@ -739,6 +812,9 @@ angular.module('OpenSlidesApp.motions.site', [
getFormFields: function (singleMotion, motions, formatChangeCallback) { getFormFields: function (singleMotion, motions, formatChangeCallback) {
var fields = []; var fields = [];
var commentsAvailable = _.keys(noSpecialCommentsFields).length !== 0; var commentsAvailable = _.keys(noSpecialCommentsFields).length !== 0;
var someMotionsHaveAmendments = _.some(motions, function (motion) {
return motion.hasAmendments();
});
var getMetaInformationOptions = function (disabled) { var getMetaInformationOptions = function (disabled) {
if (!disabled) { if (!disabled) {
disabled = {}; disabled = {};
@ -788,6 +864,19 @@ angular.module('OpenSlidesApp.motions.site', [
} }
]; ];
} }
if (someMotionsHaveAmendments) {
fields.push({
key: 'amendments',
type: 'radio-buttons',
templateOptions: {
label: gettextCatalog.getString('Amendments'),
options: [
{name: gettextCatalog.getString('Include'), value: true},
{name: gettextCatalog.getString('Exclude'), value: false},
],
},
});
}
if (operator.hasPerms('motions.can_manage')) { if (operator.hasPerms('motions.can_manage')) {
fields.push.apply(fields, [ fields.push.apply(fields, [
{ {
@ -952,6 +1041,7 @@ angular.module('OpenSlidesApp.motions.site', [
pdfFormat: 'pdf', pdfFormat: 'pdf',
changeRecommendationMode: Config.get('motions_recommendation_text_mode').value, changeRecommendationMode: Config.get('motions_recommendation_text_mode').value,
lineNumberMode: Config.get('motions_default_line_numbering').value, lineNumberMode: Config.get('motions_default_line_numbering').value,
amendments: false,
include: { include: {
text: true, text: true,
reason: true, reason: true,
@ -967,7 +1057,24 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.motions = motions; $scope.motions = motions;
$scope.singleMotion = singleMotion; $scope.singleMotion = singleMotion;
// Add amendments to motions. The amendments are sorted by their identifier
var prepareAmendments = function (motions) {
var allMotions = [];
_.forEach(motions, function (motion) {
allMotions.push(motion);
allMotions = allMotions.concat(
_.sortBy(motion.getAmendments(), function (amendment) {
return amendment.identifier;
})
);
});
return allMotions;
};
$scope.export = function () { $scope.export = function () {
if ($scope.params.amendments) {
motions = prepareAmendments(motions);
}
switch ($scope.params.format) { switch ($scope.params.format) {
case 'pdf': case 'pdf':
if ($scope.params.pdfFormat === 'pdf') { if ($scope.params.pdfFormat === 'pdf') {
@ -1078,8 +1185,8 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.$watch(function () { $scope.$watch(function () {
return Motion.lastModified(); return Motion.lastModified();
}, function () { }, function () {
// always order by identifier (after custom ordering) // get all main motions and order by identifier (after custom ordering)
$scope.motions = _.orderBy(Motion.getAll(), ['identifier']); $scope.motions = _.orderBy(Motion.filter({parent_id: undefined}), ['identifier']);
_.forEach($scope.motions, function (motion) { _.forEach($scope.motions, function (motion) {
MotionComment.populateFields(motion); MotionComment.populateFields(motion);
motion.personalNote = PersonalNoteManager.getNote(motion); motion.personalNote = PersonalNoteManager.getNote(motion);
@ -1172,8 +1279,12 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.stateFilter = _.uniq($scope.stateFilter); $scope.stateFilter = _.uniq($scope.stateFilter);
}; };
// This value may be overritten, so the filters, sorting and pagination in an
// derived view are independent to this view.
var osTablePrefix = $scope.osTablePrefix || 'MotionTable';
// Filtering // Filtering
$scope.filter = osTableFilter.createInstance('MotionTableFilter'); $scope.filter = osTableFilter.createInstance(osTablePrefix + 'Filter');
if (!$scope.filter.existsStorageEntry()) { if (!$scope.filter.existsStorageEntry()) {
$scope.filter.multiselectFilters = { $scope.filter.multiselectFilters = {
@ -1185,11 +1296,6 @@ angular.module('OpenSlidesApp.motions.site', [
comment: [], comment: [],
}; };
$scope.filter.booleanFilters = { $scope.filter.booleanFilters = {
isAmendment: {
value: undefined,
choiceYes: gettext('Is an amendment'),
choiceNo: gettext('Is not an amendment'),
},
isFavorite: { isFavorite: {
value: undefined, value: undefined,
choiceYes: gettext('Marked as favorite'), choiceYes: gettext('Marked as favorite'),
@ -1245,7 +1351,7 @@ angular.module('OpenSlidesApp.motions.site', [
updateStateFilter(); updateStateFilter();
}; };
// Sorting // Sorting
$scope.sort = osTableSort.createInstance('MotionTableSort'); $scope.sort = osTableSort.createInstance(osTablePrefix + 'Sort');
if (!$scope.sort.column) { if (!$scope.sort.column) {
$scope.sort.column = 'identifier'; $scope.sort.column = 'identifier';
} }
@ -1269,24 +1375,7 @@ angular.module('OpenSlidesApp.motions.site', [
]; ];
// pagination // pagination
$scope.pagination = osTablePagination.createInstance('MotionTablePagination'); $scope.pagination = osTablePagination.createInstance(osTablePrefix + 'Pagination');
// update state
$scope.updateState = function (motion, state_id) {
$http.put('/rest/motions/motion/' + motion.id + '/set_state/', {'state': state_id});
};
// reset state
$scope.resetState = function (motion) {
$http.put('/rest/motions/motion/' + motion.id + '/set_state/', {});
};
// update recommendation
$scope.updateRecommendation = function (motion, recommendation_id) {
$http.put('/rest/motions/motion/' + motion.id + '/set_recommendation/', {'recommendation': recommendation_id});
};
// reset recommendation
$scope.resetRecommendation = function (motion) {
$http.put('/rest/motions/motion/' + motion.id + '/set_recommendation/', {});
};
$scope.hasTag = function (motion, tag) { $scope.hasTag = function (motion, tag) {
return _.indexOf(motion.tags_id, tag.id) > -1; return _.indexOf(motion.tags_id, tag.id) > -1;
@ -1340,19 +1429,19 @@ angular.module('OpenSlidesApp.motions.site', [
ngDialog.open(MotionForm.getDialog(motion)); ngDialog.open(MotionForm.getDialog(motion));
}; };
// Export dialog // Export dialog
$scope.openExportDialog = function () { $scope.openExportDialog = function (motions) {
ngDialog.open(MotionExportForm.getDialog($scope.motionsFiltered)); ngDialog.open(MotionExportForm.getDialog(motions));
}; };
$scope.pdfExport = function () { $scope.pdfExport = function (motions) {
MotionPdfExport.export($scope.motionsFiltered); MotionPdfExport.export(motions);
}; };
// *** select mode functions *** // *** select mode functions ***
$scope.isSelectMode = false; $scope.isSelectMode = false;
// check all checkboxes from filtered motions // check all checkboxes from filtered motions
$scope.checkAll = function () { $scope.checkAll = function (motions) {
$scope.selectedAll = !$scope.selectedAll; $scope.selectedAll = !$scope.selectedAll;
angular.forEach($scope.motionsFiltered, function (motion) { _.forEach(motions, function (motion) {
motion.selected = $scope.selectedAll; motion.selected = $scope.selectedAll;
}); });
}; };
@ -1360,13 +1449,13 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.uncheckAll = function () { $scope.uncheckAll = function () {
if (!$scope.isSelectMode) { if (!$scope.isSelectMode) {
$scope.selectedAll = false; $scope.selectedAll = false;
angular.forEach($scope.motions, function (motion) { _.forEach($scope.motions, function (motion) {
motion.selected = false; motion.selected = false;
}); });
} }
}; };
var selectModeAction = function (predicate) { var selectModeAction = function (motions, predicate) {
angular.forEach($scope.motionsFiltered, function (motion) { angular.forEach(motions, function (motion) {
if (motion.selected) { if (motion.selected) {
predicate(motion); predicate(motion);
} }
@ -1375,27 +1464,27 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.uncheckAll(); $scope.uncheckAll();
}; };
// delete selected motions // delete selected motions
$scope.deleteMultiple = function () { $scope.deleteMultiple = function (motions) {
selectModeAction(function (motion) { selectModeAction(motions, function (motion) {
$scope.delete(motion); $scope.delete(motion);
}); });
}; };
// set status for selected motions // set status for selected motions
$scope.setStatusMultiple = function (stateId) { $scope.setStatusMultiple = function (motions, stateId) {
selectModeAction(function (motion) { selectModeAction(motions, function (motion) {
$scope.updateState(motion, stateId); $http.put('/rest/motions/motion/' + motion.id + '/set_state/', {'state': stateId});
}); });
}; };
// set category for selected motions // set category for selected motions
$scope.setCategoryMultiple = function (categoryId) { $scope.setCategoryMultiple = function (motions, categoryId) {
selectModeAction(function (motion) { selectModeAction(motions, function (motion) {
motion.category_id = categoryId === 'no_category_selected' ? null : categoryId; motion.category_id = categoryId === 'no_category_selected' ? null : categoryId;
$scope.save(motion); $scope.save(motion);
}); });
}; };
// set status for selected motions // set status for selected motions
$scope.setMotionBlockMultiple = function (motionBlockId) { $scope.setMotionBlockMultiple = function (motions, motionBlockId) {
selectModeAction(function (motion) { selectModeAction(motions, function (motion) {
motion.motion_block_id = motionBlockId === 'no_motionBlock_selected' ? null : motionBlockId; motion.motion_block_id = motionBlockId === 'no_motionBlock_selected' ? null : motionBlockId;
$scope.save(motion); $scope.save(motion);
}); });
@ -1413,6 +1502,7 @@ angular.module('OpenSlidesApp.motions.site', [
'ngDialog', 'ngDialog',
'gettextCatalog', 'gettextCatalog',
'MotionForm', 'MotionForm',
'AmendmentParagraphChooseForm',
'ChangeRecommendationCreate', 'ChangeRecommendationCreate',
'ChangeRecommendationView', 'ChangeRecommendationView',
'MotionStateAndRecommendationParser', 'MotionStateAndRecommendationParser',
@ -1438,7 +1528,7 @@ angular.module('OpenSlidesApp.motions.site', [
'WebpageTitle', 'WebpageTitle',
'EditingWarning', 'EditingWarning',
function($scope, $http, $timeout, $window, $filter, operator, ngDialog, gettextCatalog, function($scope, $http, $timeout, $window, $filter, operator, ngDialog, gettextCatalog,
MotionForm, ChangeRecommendationCreate, ChangeRecommendationView, MotionForm, AmendmentParagraphChooseForm, ChangeRecommendationCreate, ChangeRecommendationView,
MotionStateAndRecommendationParser, MotionChangeRecommendation, Motion, MotionComment, MotionStateAndRecommendationParser, MotionChangeRecommendation, Motion, MotionComment,
Category, Mediafile, Tag, User, Workflow, Config, motionId, MotionInlineEditing, Category, Mediafile, Tag, User, Workflow, Config, motionId, MotionInlineEditing,
MotionCommentsInlineEditing, Editor, Projector, ProjectionDefault, MotionBlock, MotionCommentsInlineEditing, Editor, Projector, ProjectionDefault, MotionBlock,
@ -1451,29 +1541,8 @@ angular.module('OpenSlidesApp.motions.site', [
Workflow.bindAll({}, $scope, 'workflows'); Workflow.bindAll({}, $scope, 'workflows');
MotionBlock.bindAll({}, $scope, 'motionBlocks'); MotionBlock.bindAll({}, $scope, 'motionBlocks');
Motion.bindAll({}, $scope, 'motions'); Motion.bindAll({}, $scope, 'motions');
$scope.$watch(function () {
return MotionChangeRecommendation.lastModified();
}, function () {
$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]);
}
if ($scope.change_recommendations.length > 0) {
$scope.disableMotionInlineEditing();
}
});
$scope.$watch(function () { $scope.$watch(function () {
return Projector.lastModified(); return Projector.lastModified();
}, function () { }, function () {
@ -1487,6 +1556,7 @@ angular.module('OpenSlidesApp.motions.site', [
return Motion.lastModified(motionId); return Motion.lastModified(motionId);
}, function () { }, function () {
$scope.motion = Motion.get(motionId); $scope.motion = Motion.get(motionId);
$scope.amendment_diff_paragraphs = $scope.motion.getAmendmentParagraphsLinesDiff();
MotionComment.populateFields($scope.motion); MotionComment.populateFields($scope.motion);
if (motion.comments) { if (motion.comments) {
$scope.stateExtension = $scope.motion.comments[$scope.commentFieldForStateId]; $scope.stateExtension = $scope.motion.comments[$scope.commentFieldForStateId];
@ -1503,6 +1573,7 @@ angular.module('OpenSlidesApp.motions.site', [
WebpageTitle.updateTitle(webpageTitle); WebpageTitle.updateTitle(webpageTitle);
$scope.createChangeRecommendation.setVersion(motion, motion.active_version); $scope.createChangeRecommendation.setVersion(motion, motion.active_version);
$scope.viewChangeRecommendations.setVersion(motion, motion.active_version);
}); });
$scope.$watch(function () { $scope.$watch(function () {
return Motion.lastModified(); return Motion.lastModified();
@ -1554,6 +1625,13 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.lineNumberMode = mode; $scope.lineNumberMode = mode;
}; };
$scope.showAmendmentContext = false;
$scope.setShowAmendmentContext = function($event) {
$event.preventDefault();
$event.stopPropagation();
$scope.showAmendmentContext = !$scope.showAmendmentContext;
};
if (motion.parent_id) { if (motion.parent_id) {
Motion.bindOne(motion.parent_id, $scope, 'parent'); Motion.bindOne(motion.parent_id, $scope, 'parent');
} }
@ -1641,27 +1719,26 @@ angular.module('OpenSlidesApp.motions.site', [
}; };
// open dialog for new amendment // open dialog for new amendment
$scope.newAmendment = function () { $scope.newAmendment = function () {
var dialog = MotionForm.getDialog(); var openMainDialog = function (paragraphNo) {
if (typeof dialog.scope === 'undefined') { var dialog = MotionForm.getDialog(null, motion, paragraphNo);
dialog.scope = {};
}
dialog.scope = $scope; dialog.scope = $scope;
ngDialog.open(dialog); ngDialog.open(dialog);
}; };
if (Config.get('motions_amendments_text_mode').value === 'paragraph') {
var dialog = AmendmentParagraphChooseForm.getDialog($scope.motion, openMainDialog);
dialog.scope = $scope;
ngDialog.open(dialog);
} else {
openMainDialog();
}
};
// follow recommendation // follow recommendation
$scope.followRecommendation = function () { $scope.followRecommendation = function () {
$http.post('/rest/motions/motion/' + motion.id + '/follow_recommendation/', { $http.post('/rest/motions/motion/' + motion.id + '/follow_recommendation/', {
'recommendationExtension': $scope.recommendationExtension 'recommendationExtension': $scope.recommendationExtension
}); });
}; };
// update state
$scope.updateState = function (state_id) {
$http.put('/rest/motions/motion/' + motion.id + '/set_state/', {'state': state_id});
};
// reset state
$scope.reset_state = function () {
$http.put('/rest/motions/motion/' + motion.id + '/set_state/', {});
};
// toggle functions for meta information // toggle functions for meta information
$scope.toggleCategory = function (category) { $scope.toggleCategory = function (category) {
if ($scope.motion.category_id == category.id) { if ($scope.motion.category_id == category.id) {
@ -1706,14 +1783,6 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.addMotionToRecommendationField = function (motion) { $scope.addMotionToRecommendationField = function (motion) {
$scope.recommendationExtension += MotionStateAndRecommendationParser.formatMotion(motion); $scope.recommendationExtension += MotionStateAndRecommendationParser.formatMotion(motion);
}; };
// update recommendation
$scope.updateRecommendation = function (recommendation_id) {
$http.put('/rest/motions/motion/' + motion.id + '/set_recommendation/', {'recommendation': recommendation_id});
};
// reset recommendation
$scope.resetRecommendation = function () {
$http.put('/rest/motions/motion/' + motion.id + '/set_recommendation/', {});
};
// create poll // create poll
$scope.create_poll = function () { $scope.create_poll = function () {
$http.post('/rest/motions/motion/' + motion.id + '/create_poll/', {}); $http.post('/rest/motions/motion/' + motion.id + '/create_poll/', {});
@ -1746,6 +1815,7 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.inlineEditing.setVersion(motion, version.id); $scope.inlineEditing.setVersion(motion, version.id);
$scope.reasonInlineEditing.setVersion(motion, version.id); $scope.reasonInlineEditing.setVersion(motion, version.id);
$scope.createChangeRecommendation.setVersion(motion, version.id); $scope.createChangeRecommendation.setVersion(motion, version.id);
$scope.viewChangeRecommendations.setVersion(motion, motion.active_version);
}; };
// permit specific version // permit specific version
$scope.permitVersion = function (version) { $scope.permitVersion = function (version) {
@ -1882,9 +1952,9 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.createChangeRecommendation = ChangeRecommendationCreate; $scope.createChangeRecommendation = ChangeRecommendationCreate;
$scope.createChangeRecommendation.init($scope, motion); $scope.createChangeRecommendation.init($scope, motion);
// Change recommendation viewing // Change recommendation and amendment viewing
$scope.viewChangeRecommendations = ChangeRecommendationView; $scope.viewChangeRecommendations = ChangeRecommendationView;
$scope.viewChangeRecommendations.init($scope, Config.get('motions_recommendation_text_mode').value); $scope.viewChangeRecommendations.initSite($scope, motion, Config.get('motions_recommendation_text_mode').value);
// PDF creating functions // PDF creating functions
$scope.pdfExport = function () { $scope.pdfExport = function () {
@ -2052,6 +2122,32 @@ angular.module('OpenSlidesApp.motions.site', [
} }
]) ])
.controller('AmendmentParagraphChooseCtrl', [
'$scope',
'$state',
'Motion',
'motion',
'successCb',
function($scope, $state, Motion, motion, successCb) {
$scope.model = angular.copy(motion);
$scope.model.paragraph_selected = null;
$scope.paragraphs = motion.getTextParagraphs(motion.active_version, true).map(function(text, index) {
// This prevents an error in ng-repeater's duplication detection if two identical paragraphs occur
return {
"paragraphNo": index,
"text": text
};
});
$scope.gotoMotionForm = function() {
var paragraphNo = parseInt($scope.model.paragraph_selected);
successCb(paragraphNo);
$scope.closeThisDialog();
};
}
])
.controller('MotionCreateCtrl', [ .controller('MotionCreateCtrl', [
'$scope', '$scope',
'$state', '$state',
@ -2060,6 +2156,9 @@ angular.module('OpenSlidesApp.motions.site', [
'operator', 'operator',
'Motion', 'Motion',
'MotionForm', 'MotionForm',
'parentMotion',
'paragraphNo',
'paragraphTextPre',
'Category', 'Category',
'Config', 'Config',
'Mediafile', 'Mediafile',
@ -2068,8 +2167,9 @@ angular.module('OpenSlidesApp.motions.site', [
'Workflow', 'Workflow',
'Agenda', 'Agenda',
'ErrorMessage', 'ErrorMessage',
function($scope, $state, gettext, gettextCatalog, operator, Motion, MotionForm, function($scope, $state, gettext, gettextCatalog, operator, Motion, MotionForm, parentMotion,
Category, Config, Mediafile, Tag, User, Workflow, Agenda, ErrorMessage) { paragraphNo, paragraphTextPre, Category, Config, Mediafile, Tag, User, Workflow,
Agenda, ErrorMessage) {
Category.bindAll({}, $scope, 'categories'); Category.bindAll({}, $scope, 'categories');
Mediafile.bindAll({}, $scope, 'mediafiles'); Mediafile.bindAll({}, $scope, 'mediafiles');
Tag.bindAll({}, $scope, 'tags'); Tag.bindAll({}, $scope, 'tags');
@ -2080,30 +2180,54 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.alert = {}; $scope.alert = {};
// Check whether this is a new amendment. // Check whether this is a new amendment.
var isAmendment = $scope.$parent.motion && $scope.$parent.motion.id; var isAmendment = parentMotion && parentMotion.id,
isParagraphBasedAmendment = false;
// Set default values for create form // Set default values for create form
// ... for amendments add parent_id // ... for amendments add parent_id
if (isAmendment) { if (isAmendment) {
if (Config.get('motions_amendments_apply_text').value) { if (Config.get('motions_amendments_text_mode').value === 'fulltext') {
$scope.model.text = $scope.$parent.motion.getText(); $scope.model.text = parentMotion.getText();
} }
$scope.model.title = $scope.$parent.motion.getTitle(); if (Config.get('motions_amendments_text_mode').value === 'paragraph' &&
$scope.model.parent_id = $scope.$parent.motion.id; paragraphNo !== undefined) {
$scope.model.category_id = $scope.$parent.motion.category_id; var paragraphs = parentMotion.getTextParagraphs(parentMotion.active_version, false);
$scope.model.motion_block_id = $scope.$parent.motion.motion_block_id; $scope.model.text = paragraphs[paragraphNo];
isParagraphBasedAmendment = true;
}
if (paragraphTextPre !== undefined) {
$scope.model.text = paragraphTextPre;
}
if (parentMotion.identifier) {
$scope.model.title = gettextCatalog.getString('Amendment to') +
' ' + parentMotion.identifier;
} else {
$scope.model.title = gettextCatalog.getString('Amendment to motion ') +
' ' + parentMotion.getTitle();
}
$scope.model.paragraphNo = paragraphNo;
$scope.model.parent_id = parentMotion.id;
$scope.model.category_id = parentMotion.category_id;
$scope.model.motion_block_id = parentMotion.motion_block_id;
Motion.bindOne($scope.model.parent_id, $scope, 'parent'); Motion.bindOne($scope.model.parent_id, $scope, 'parent');
} }
// ... preselect default workflow // ... preselect default workflow
if (operator.hasPerms('motions.can_manage')) {
$scope.model.workflow_id = Config.get('motions_workflow').value; $scope.model.workflow_id = Config.get('motions_workflow').value;
}
// get all form fields // get all form fields
$scope.formFields = MotionForm.getFormFields(true); $scope.formFields = MotionForm.getFormFields(true, isParagraphBasedAmendment);
// save motion // save motion
$scope.save = function (motion, gotoDetailView) { $scope.save = function (motion, gotoDetailView) {
motion.agenda_type = motion.showAsAgendaItem ? 1 : 2; motion.agenda_type = motion.showAsAgendaItem ? 1 : 2;
if (isAmendment && motion.paragraphNo !== undefined) {
var orig_paragraphs = parentMotion.getTextParagraphs(parentMotion.active_version, false);
motion.amendment_paragraphs = orig_paragraphs.map(function (_, idx) {
return (idx === motion.paragraphNo ? motion.text : null);
});
}
// The attribute motion.agenda_parent_id is set by the form, see form definition. // The attribute motion.agenda_parent_id is set by the form, see form definition.
Motion.create(motion).then( Motion.create(motion).then(
function(success) { function(success) {
@ -2149,12 +2273,48 @@ angular.module('OpenSlidesApp.motions.site', [
// set initial values for form model by create deep copy of motion object // set initial values for form model by create deep copy of motion object
// so list/detail view is not updated while editing // so list/detail view is not updated while editing
var motion = Motion.get(motionId); var motion = Motion.get(motionId);
$scope.model = angular.copy(motion); // We need to clone this by hand. angular and lodash are not capable of keeping
// crossreferences out.
$scope.model = {
id: motion.id,
parent_id: motion.parent_id,
identifier: motion.identifier,
title: motion.getTitle(),
text: motion.getText(),
reason: motion.getReason(),
submitters_id: _.map(motion.submitters_id),
supporters_id: _.map(motion.supporters_id),
tags_id: _.map(motion.tags_id),
state_id: motion.state_id,
recommendation_id: motion.recommendation_id,
origin: motion.origin,
workflow_id: motion.workflow_id,
comments: _.clone(motion.comments),
attachments_id: _.map(motion.attachments_id),
active_version: motion.active_version,
agenda_item_id: motion.agenda_item_id,
category_id: motion.category_id,
motion_block_id: motion.motion_block_id,
};
// Clone comments
_.forEach(motion.comments, function (comment, index) {
$scope.model['comment_' + index] = comment;
});
$scope.model.disable_versioning = false; $scope.model.disable_versioning = false;
$scope.model.more = false; $scope.model.more = false;
if (motion.isParagraphBasedAmendment()) {
motion.getVersion(motion.active_version).amendment_paragraphs.forEach(function(paragraph_amend, paragraphNo) {
// Hint: this assumes there is only one modified paragraph
if (paragraph_amend !== null) {
$scope.model.text = paragraph_amend;
$scope.model.paragraphNo = paragraphNo;
}
});
$scope.model.title = motion.getTitle();
}
// get all form fields // get all form fields
$scope.formFields = MotionForm.getFormFields(); $scope.formFields = MotionForm.getFormFields(false, motion.isParagraphBasedAmendment());
// override default values for update form // override default values for update form
for (var i = 0; i < $scope.formFields.length; i++) { for (var i = 0; i < $scope.formFields.length; i++) {
if ($scope.formFields[i].key == "identifier") { if ($scope.formFields[i].key == "identifier") {
@ -2190,15 +2350,90 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.$on('$destroy', editingStoppedCallback); $scope.$on('$destroy', editingStoppedCallback);
// Save motion // Save motion
$scope.save = function (motion, gotoDetailView) { $scope.save = function (model, gotoDetailView) {
if ($scope.model.paragraphNo !== undefined) {
var parentMotion = motion.getParentMotion();
var orig_paragraphs = parentMotion.getTextParagraphs(parentMotion.active_version, false);
$scope.model.amendment_paragraphs = orig_paragraphs.map(function (_, idx) {
return (idx === $scope.model.paragraphNo ? $scope.model.text : null);
});
}
// inject the changed motion (copy) object back into DS store
Motion.inject(model);
// save changed motion object on server
Motion.save(model).then(
function(success) {
if (gotoDetailView) {
$state.go('motions.motion.detail', {id: success.id});
}
$scope.closeThisDialog();
},
function (error) {
// save error: revert all changes by restore
// (refresh) original motion object from server
Motion.refresh(model);
$scope.alert = ErrorMessage.forAlert(error);
}
);
};
}
])
.controller('MotionCommentCtrl', [
'$scope',
'Motion',
'MotionComment',
'MotionCommentForm',
'motionId',
'commentFieldId',
'gettextCatalog',
'ErrorMessage',
function ($scope, Motion, MotionComment, MotionCommentForm, motionId, commentFieldId,
gettextCatalog, ErrorMessage) {
$scope.alert = {};
// set initial values for form model by create deep copy of motion object
// so list/detail view is not updated while editing
var motion = Motion.get(motionId);
$scope.model = angular.copy(motion);
$scope.formFields = MotionCommentForm.getFormFields(commentFieldId);
var fields = MotionComment.getNoSpecialCommentsFields();
var title = gettextCatalog.getString('Edit comment %%comment%% of motion %%motion%%');
title = title.replace('%%comment%%', fields[commentFieldId].name);
$scope.title = title.replace('%%motion%%', motion.getTitle());
$scope.model.title = motion.getTitle(-1);
$scope.model.text = motion.getText(-1);
$scope.model.reason = motion.getReason(-1);
if (motion.isParagraphBasedAmendment()) {
motion.getVersion(motion.active_version).amendment_paragraphs.forEach(function(paragraph_amend, paragraphNo) {
// Hint: this assumes there is only one modified paragraph
if (paragraph_amend !== null) {
$scope.model.text = paragraph_amend;
$scope.model.paragraphNo = paragraphNo;
}
});
}
$scope.save = function (motion) {
if (motion.isParagraphBasedAmendment()) {
motion.getVersion(motion.active_version).amendment_paragraphs.forEach(function(paragraph_amend, paragraphNo) {
// Hint: this assumes there is only one modified paragraph
if (paragraph_amend !== null) {
$scope.model.text = paragraph_amend;
$scope.model.paragraphNo = paragraphNo;
}
});
}
// inject the changed motion (copy) object back into DS store // inject the changed motion (copy) object back into DS store
Motion.inject(motion); Motion.inject(motion);
// save changed motion object on server // save changed motion object on server
Motion.save(motion).then( Motion.save(motion).then(
function(success) { function(success) {
if (gotoDetailView) {
$state.go('motions.motion.detail', {id: success.id});
}
$scope.closeThisDialog(); $scope.closeThisDialog();
}, },
function (error) { function (error) {
@ -2309,6 +2544,202 @@ angular.module('OpenSlidesApp.motions.site', [
} }
]) ])
.controller('MotionAmendmentListStateCtrl', [
'$scope',
'motionId',
function ($scope, motionId) {
$scope.motionId = motionId;
$scope.osTablePrefix = 'AmendmentTable';
}
])
.controller('MotionAmendmentListCtrl', [
'$scope',
'$sessionStorage',
'$state',
'Motion',
'MotionComment',
'MotionForm',
'PersonalNoteManager',
'ngDialog',
'MotionCommentForm',
'MotionChangeRecommendation',
'MotionPdfExport',
'AmendmentCsvExport',
'gettextCatalog',
'gettext',
function ($scope, $sessionStorage, $state, Motion, MotionComment, MotionForm,
PersonalNoteManager, ngDialog, MotionCommentForm, MotionChangeRecommendation,
MotionPdfExport, AmendmentCsvExport, gettextCatalog, gettext) {
if ($scope.motionId) {
$scope.leadMotion = Motion.get($scope.motionId);
}
var updateMotions = function () {
// check, if lead motion is given
var amendments;
if ($scope.leadMotion) {
amendments = Motion.filter({parent_id: $scope.leadMotion.id});
} else {
amendments = _.filter(Motion.getAll(), function (motion) {
return motion.parent_id;
});
}
// always order by identifier (after custom ordering)
$scope.amendments = _.orderBy(amendments, ['identifier']);
_.forEach($scope.amendments, function (amendment) {
MotionComment.populateFields(amendment);
amendment.personalNote = PersonalNoteManager.getNote(amendment);
// For filtering, we cannot filter for .personalNote.star
amendment.star = amendment.personalNote ? amendment.personalNote.star : false;
amendment.hasPersonalNote = amendment.personalNote ? !!amendment.personalNote.note : false;
if (amendment.star === undefined) {
amendment.star = false;
}
// add a custom sort attribute
var parentMotion = amendment.getParentMotion();
amendment.parentMotionAndLineNumber = parentMotion.identifier;
if (amendment.isParagraphBasedAmendment()) {
var paragraphs = amendment.getAmendmentParagraphsLinesDiff();
var diffLine = '0';
if (paragraphs.length) {
diffLine = '' + paragraphs[0].diffLineFrom;
}
while (diffLine.length < 6) {
diffLine = '0' + diffLine;
}
amendment.parentMotionAndLineNumber += ' ' + diffLine;
}
});
// Get all lead motions
$scope.leadMotions = _.orderBy(Motion.filter({parent_id: undefined}), ['identifier']);
//updateCollissions();
};
var updateCollissions = function () {
$scope.collissions = {};
_.forEach($scope.amendments, function (amendment) {
if (amendment.isParagraphBasedAmendment()) {
var parentMotion = amendment.getParentMotion();
// get all change recommendations _and_ changes by amendments from the
// parent motion. From all get the unified change object.
var parentChangeRecommendations = _.filter(
MotionChangeRecommendation.filter({
'where': {'motion_version_id': {'==': parentMotion.active_version}}
}), function (change) {
return change.isTextRecommendation();
}
);
var parentChanges = parentChangeRecommendations.map(function (cr) {
return cr.getUnifiedChangeObject();
}).concat(
_.map(parentMotion.getParagraphBasedAmendmentsForDiffView(), function (amendment) {
return amendment.getUnifiedChangeObject();
})
);
var change = amendment.getUnifiedChangeObject();
if (change) {
change.setOtherChangesForCollission(parentChanges);
$scope.collissions[amendment.id] = !!change.getCollissions().length;
}
}
});
};
//$scope.$watch(function () {
// return MotionChangeRecommendation.lastModified();
//}, updateCollissions);
$scope.$watch(function () {
return Motion.lastModified();
}, updateMotions);
$scope.selectLeadMotion = function (motion) {
$scope.leadMotion = motion;
updateMotions();
if ($scope.leadMotion) {
$state.transitionTo('motions.motion.amendment-list',
{id: $scope.leadMotion.id},
{notify: false}
);
} else {
$state.transitionTo('motions.motion.allamendments', {},
{notify: false}
);
}
};
// Save expand state so the session
if ($sessionStorage.amendmentTableExpandState) {
$scope.toggleExpandContent();
}
$scope.saveExpandState = function (state) {
$sessionStorage.amendmentTableExpandState = state;
};
// add custom sorting
$scope.sortOptions.unshift({
name: 'parentMotionAndLineNumber',
display_name: gettext('Parent motion and line number'),
});
if (!$scope.sort.column || $scope.sort.column === 'identifier') {
$scope.sort.column = 'parentMotionAndLineNumber';
}
$scope.isTextExpandable = function (comment, characters) {
comment = $(comment).text();
return comment.length > characters;
};
$scope.getTextPreview = function (comment, characters) {
comment = $(comment).text();
if (comment.length > characters) {
comment = comment.substr(0, characters) + '...';
}
return comment;
};
$scope.editComment = function (motion, fieldId) {
ngDialog.open(MotionCommentForm.getDialog(motion, fieldId));
};
$scope.createModifiedAmendment = function (amendment) {
var paragraphNo,
paragraphText;
if (amendment.isParagraphBasedAmendment()) {
// We assume there is only one affected paragraph
amendment.getVersion(amendment.active_version).amendment_paragraphs.forEach(function(parText, parNo) {
if (parText !== null) {
paragraphNo = parNo;
paragraphText = parText;
}
});
} else {
paragraphText = amendment.getText();
}
ngDialog.open(MotionForm.getDialog(null, amendment.getParentMotion(), paragraphNo, paragraphText));
};
$scope.amendmentPdfExport = function (motions) {
var filename;
if ($scope.leadMotion) {
filename = gettextCatalog.getString('Amendments to') + ' ' +
$scope.leadMotion.getTitle();
} else {
filename = gettextCatalog.getString('Amendments');
}
filename += '.pdf';
MotionPdfExport.exportAmendments(motions, filename);
};
$scope.exportCsv = function (motions) {
AmendmentCsvExport.export(motions);
};
}
])
.controller('MotionImportCtrl', [ .controller('MotionImportCtrl', [
'$scope', '$scope',
'$q', '$q',
@ -2581,7 +3012,6 @@ angular.module('OpenSlidesApp.motions.site', [
} }
]) ])
.controller('CategoryListCtrl', [ .controller('CategoryListCtrl', [
'$scope', '$scope',
'Category', 'Category',
@ -2740,6 +3170,7 @@ angular.module('OpenSlidesApp.motions.site', [
gettext('Name of recommender'); gettext('Name of recommender');
gettext('Default text version for change recommendations'); gettext('Default text version for change recommendations');
gettext('Will be displayed as label before selected recommendation. Use an empty value to disable the recommendation system.'); gettext('Will be displayed as label before selected recommendation. Use an empty value to disable the recommendation system.');
gettext('Edit comment %%comment%% of motion %%motion%%');
// subgroup Amendments // subgroup Amendments
gettext('Amendments'); gettext('Amendments');
@ -2747,6 +3178,7 @@ angular.module('OpenSlidesApp.motions.site', [
gettext('Prefix for the identifier for amendments'); gettext('Prefix for the identifier for amendments');
gettext('Apply text for new amendments'); gettext('Apply text for new amendments');
gettext('The title of the motion is always applied.'); gettext('The title of the motion is always applied.');
gettext('Amendment to');
// subgroup Supporters // subgroup Supporters
gettext('Supporters'); gettext('Supporters');

View File

@ -0,0 +1,27 @@
<h1 translate>Choose the paragraph to amend</h1>
<div uib-alert ng-show="alert.show" ng-class="'alert-' + (alert.type || 'warning')" close="alert={}">
{{ alert.msg }}
</div>
<form name="motionForm" ng-submit="gotoMotionForm(model)" novalidate>
<div class="paragraph-select-list motion-text">
<div ng-repeat="paragraph in paragraphs" class="paragraph-select-holder"
ng-class="model.paragraph_selected == paragraph.paragraphNo ? 'selected' : ''">
<div class="paragraph-select">
<input type="radio" name="paragraph_selected" value="{{ paragraph.paragraphNo }}"
ng-model="model.paragraph_selected">
</div>
<div class="text-holder" ng-click="model.paragraph_selected = paragraph.paragraphNo"
ng-bind-html="paragraph.text | trusted"></div>
</div>
</div>
<button type="submit" ng-disabled="model.paragraph_selected === null" class="btn btn-primary" translate>
Continue
</button>
<button type="button" ng-click="closeThisDialog()" class="btn btn-default" translate>
Cancel
</button>
</form>

View File

@ -0,0 +1,408 @@
<div ng-controller="MotionListCtrl">
<div ng-controller="MotionAmendmentListCtrl">
<div class="header">
<div class="title">
<div class="submenu">
<a ui-sref="motions.motion.list" class="btn btn-sm btn-default">
<i class="fa fa-angle-double-left fa-lg"></i>
<translate>Back to motions overview</translate>
</a>
<button type="button" class="btn btn-sm"
ng-class="expandContent ? 'btn-primary' : 'btn-default'"
ng-click="toggleExpandContent(); saveExpandState(expandContent)">
<i class="fa fa-arrows-h fa-lg"></i>
<span ng-if="!expandContent" translate>Expand</span>
<span ng-if="expandContent" translate>Reduce</span>
</button>
</div>
<h1 translate>Amendments</h1>
<div ng-mouseover="selectHover=true" ng-mouseleave="selectHover=false"
class="dropdown-hover-space">
<h3>
<a ui-sref="motions.motion.detail({id: leadMotion.id})" ng-if="leadMotion">
<span ng-if="leadMotion.identifier">
{{ leadMotion.identifier }} &mdash;
</span>
{{ leadMotion.getTitle() }}
</a>
<span ng-if="!leadMotion" translate>
All motions
</span>
<span ng-class="{'hiddenDiv': !selectHover}" uib-dropdown>
<i class="fa fa-cog pointer" uib-dropdown-toggle id="selectDropdown"></i>
<ul class="dropdown-menu" aria-labelledby="selectDropdown">
<li ng-repeat="motion in leadMotions">
<a href ng-click="selectLeadMotion(motion)">
<span ng-if="motion.identifier">
{{ motion.identifier }} &mdash;
</span>
{{ motion.getTitle() | limitTo: 35 }}{{ motion.getTitle().length > 35 ? '...' : '' }}
</a>
</li>
<li class="divider" ng-if="amendment.state.getNextStates().length"></li>
<li>
<a href ng-click="selectLeadMotion(null)" translate>
All motions
</a>
</li>
</ul>
</span>
</h3>
</div>
</div>
</div>
<div class="details">
<div class="row">
<div class="col-sm-12">
<!-- select mode -->
<button os-perms="motions.can_manage" class="btn btn-sm"
ng-class="$parent.isSelectMode ? 'btn-primary' : 'btn-default'"
ng-click="$parent.isSelectMode = !$parent.isSelectMode; uncheckAll()">
<i class="fa fa-check-square-o"></i>
<translate>Select ...</translate>
</button>
<!-- Export dropdown -->
<div class="pull-right" uib-dropdown>
<button type="button" class="btn btn-default btn-sm" id="dropdownExport" uib-dropdown-toggle>
<i class="fa fa-upload"></i>
<span ng-if="amendmentsFiltered.length === amendments.length" translate>
Export all
</span>
<span ng-if="amendmentsFiltered.length !== amendments.length" translate>
Export filtered
</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownExport">
<!-- PDF export -->
<li os-perms="motions.can_manage">
<a href="" ng-click="openExportDialog(amendmentsFiltered)">
<i class="fa fa-file-pdf-o"></i>
<translate>Export dialog</translate>
</a>
</li>
<li os-perms="!motions.can_manage">
<a href="" ng-click="pdfExport(amendmentsFiltered)">
<i class="fa fa-file-pdf-o"></i>
PDF
</a>
</li>
<!-- amendment PDF export -->
<li>
<a href="" ng-click="amendmentPdfExport(amendmentsFiltered)">
<i class="fa fa-file-pdf-o"></i>
<translate>Amendment list PDF</translate>
</a>
</li>
<!-- CSV export -->
<li>
<a href="" ng-click="exportCsv(amendmentsFiltered)">
<i class="fa fa-file-text-o"></i>
CSV
</a>
</li>
</ul>
</div>
</div>
</div>
<div uib-collapse="!isSelectMode" class="row spacer">
<div class="col-sm-12 text-left form-inline" ng-show="isSelectMode" os-perms="motions.can_manage">
<!-- actions -->
<select ng-model="selectedAction" class="form-control input-sm">
<option value="" translate>--- Select action ---</option>
<option value="delete" translate>Delete</option>
<option value="setStatus" translate>Set status</option>
<option value="setCategory" ng-if="categories.length" translate>Set category</option>
<option value="setMotionBlock" ng-if="motionBlocks.length" translate>Set motion block</option>
</select>
<!-- state select -->
<select ng-show="selectedAction == 'setStatus'" ng-model="selectedState" class="form-control input-sm">
<option value="" translate>--- Select state ---</option>
<option ng-repeat="state in states" ng-if="!state.divider" ng-disabled="state.workflowHeader" value="{{ state.id }}">
{{ (state.workflowHeader ? state.headername : state.name) | translate }}
</option>
</select>
<!-- set state button -->
<a ng-show="selectedAction == 'setStatus' && selectedState"
ng-click="setStatusMultiple(amendmentsFiltered, selectedState)" class="btn btn-default btn-sm">
<translate>Set status</translate>
</a>
<!-- category select -->
<select ng-show="selectedAction == 'setCategory'" ng-model="selectedCategory" class="form-control input-sm">
<option value="" translate>--- Select category ---</option>
<option ng-repeat="category in categories | orderBy: config('motions_export_category_sorting')"
value="{{ category.id }}">
{{ category.prefix }} &ndash; {{ category.name }}
</option>
<option value="no_category_selected" translate>No category</option>
</select>
<!-- set category button -->
<a ng-show="selectedAction == 'setCategory' && selectedCategory"
ng-click="setCategoryMultiple(amendmentsFiltered, selectedCategory)" class="btn btn-default btn-sm">
<translate>Set category</translate>
</a>
<!-- motionBlock select -->
<select ng-show="selectedAction == 'setMotionBlock'" ng-model="selectedMotionBlock" class="form-control input-sm">
<option value="" translate>--- Select motion block ---</option>
<option ng-repeat="motionBlock in motionBlocks" value="{{ motionBlock.id }}">
{{ motionBlock.title }}
</option>
<option value="no_motionBlock_selected" translate>No motion block</option>
</select>
<!-- set motion block button -->
<a ng-show="selectedAction == 'setMotionBlock' && selectedMotionBlock"
ng-click="setMotionBlockMultiple(amendmentsFiltered, selectedMotionBlock)" class="btn btn-default btn-sm">
<translate>Set motion block</translate>
</a>
<!-- delete button -->
<a ng-show="selectedAction == 'delete'"
ng-bootbox-confirm="{{ 'Are you sure you want to delete all selected amendments?' | translate }}"
ng-bootbox-confirm-action="deleteMultiple(amendmentsFiltered)"
class="btn btn-default btn-sm btn-danger">
<i class="fa fa-trash fa-lg"></i>
<translate>Delete selected amendments</translate>
</a>
</div>
</div>
<div class="spacer-top-lg italic row">
<div class="col-md-6">
{{ amendmentsFiltered.length }} /
{{ amendments.length }}
<translate>amendments</translate><span ng-if="(amendments|filter:{selected:true}).length > 0">,
{{(amendments|filter:{selected:true}).length}} {{ "selected" | translate }}</span>
</div>
<div class="col-md-6" ng-show="amendmentsFiltered.length > pagination.itemsPerPage">
<span class="pull-right">
<translate>Page</translate> {{ pagination.currentPage }} /
{{ Math.ceil(amendmentsFiltered.length/pagination.itemsPerPage) }}
</span>
</div>
</div>
<div class="os-table container-fluid">
<div class="row header-row">
<div class="col-xs-1 centered" ng-if="isSelectMode">
<i class="fa text-danger pointer" ng-class=" selectedAll ? 'fa-check-square-o' : 'fa-square-o'"
ng-click="checkAll(amendmentsFiltered)"></i>
</div>
<div class="col-xs-11 main-header" ng-style="{'width': isSelectMode ? '' : '100%'}">
<ng-include src="'static/templates/motions/motion-table-filters.html'"></ng-include>
</div>
</div>
<!-- main table -->
<div class="row data-row" ng-repeat="amendment in amendmentsFiltered = (amendments
| osFilter: filter.filterString : filter.getObjectQueryString
| MultiselectFilter: stateFilter : getItemId.state
| MultiselectFilter: filter.multiselectFilters.comment : getItemId.comment
| MultiselectFilter: filter.multiselectFilters.category : getItemId.category
| MultiselectFilter: filter.multiselectFilters.motionBlock : getItemId.motionBlock
| MultiselectFilter: filter.multiselectFilters.recommendation : getItemId.recommendation
| MultiselectFilter: filter.multiselectFilters.tag : getItemId.tag
| filter: {star: filter.booleanFilters.isFavorite.value}
| filter: {hasPersonalNote: filter.booleanFilters.hasPersonalNote.value}
| filter: {isAmendment: filter.booleanFilters.isAmendment.value}
| toArray
| orderByEmptyLast: sort.column : sort.reverse)
| limitTo : pagination.itemsPerPage : pagination.limitBegin"
ng-class="{'projected': amendment.isProjected().length}">
<!-- select column -->
<div ng-show="isSelectMode" os-perms="motions.can_manage" class="col-xs-1 centered">
<i class="fa text-danger pointer" ng-click="amendment.selected=!amendment.selected"
ng-class="amendment.selected ? 'fa-check-square-o' : 'fa-square-o'"></i>
</div>
<!-- projector column -->
<div class="col-xs-1 centered projector" os-perms="core.can_manage_projector">
<projector-button model="amendment" default-projector-id="defaultProjectorId">
</projector-button>
</div>
<div class="no-projector-spacer" os-perms="!core.can_manage_projector"></div>
<!-- data -->
<div class="col-xs-2 content">
<div>
<!-- ID and title -->
<div>
<a class="title" ui-sref="motions.motion.detail({id: amendment.id})">
<span ng-if="amendment.identifier">{{ amendment.identifier }}</span>
<span ng-if="!amendment.identifier">{{ amendment.getTitle() }}</span>
<span ng-if="amendment.isParagraphBasedAmendment()"
ng-init="paragraph = amendment.getAmendmentParagraphsLinesDiff()[0]">
<span ng-if="paragraph">
<br>
<span ng-if="paragraph.diffLineTo == paragraph.diffLineFrom + 1">
(<translate>Line</translate> {{ paragraph.diffLineFrom }})
</span>
<span ng-if="paragraph.diffLineTo != paragraph.diffLineFrom + 1">
(<translate>Line</translate> {{ paragraph.diffLineFrom }}-{{ paragraph.diffLineTo }})
</span>
</span>
</span>
</a>
<a href="" ng-click="toggleStar(amendment)">
<i class="fa" ng-class="amendment.personalNote.star ? 'fa-star' : 'fa-star-o'"
title="{{ 'Set as favorite' | translate }}" ng-if="(amendment.personalNote.star || amendment.hover) && operator.user"></i>
</a>
<i class="fa fa-paperclip" ng-if="amendment.attachments_id.length > 0"></i>
</div>
<!-- state -->
<div os-perms="!motions.can_manage">
<span class="label" ng-class="'label-'+amendment.state.css_class">
{{ amendment.getStateName() }}
</span>
</div>
<div os-perms="motions.can_manage" uib-dropdown>
<a class="pointer label" uib-dropdown-toggle id="stateDropdown{{ amendment.id }}"
ng-class="'label-'+amendment.state.css_class">
{{ amendment.getStateName() }}
</a>
<ul class="dropdown-menu" aria-labelledby="stateDropdown{{ amendment.id }}">
<li ng-repeat="state in amendment.state.getNextStates()">
<a href ng-click="amendment.setState(state.id)">{{ state.action_word | translate }}</a>
</li>
<li class="divider" ng-if="amendment.state.getNextStates().length"></li>
<li>
<a href ng-if="amendment.isAllowed('reset_state')" ng-click="amendment.setState(null)">
<i class="fa fa-exclamation-triangle"></i>
<translate>Reset state</translate>
</a>
</li>
</ul>
</div>
<!-- recommendation -->
<div ng-if="config('motions_recommendations_by')">
<span os-perms="!motions.can_manage">
<span ng-if="amendment.recommendation" class="label"
ng-class="'label-'+amendment.recommendation.css_class">
{{ amendment.getRecommendationName() }}
</span>
<span ng-if="!amendment.recommendation" class="label label-default"
uib-tooltip="{{ config('motions_recommendations_by') }}" translate>
No recomendation set
</span>
</span>
<span os-perms="motions.can_manage" uib-dropdown>
<a class="pointer" uib-dropdown-toggle id="recommendationDropdown{{ amendment.id }}">
<span ng-if="amendment.recommendation" class="label"
ng-class="'label-'+amendment.recommendation.css_class">
{{ amendment.getRecommendationName() }}
</span>
<span ng-if="!amendment.recommendation" class="label label-default"
uib-tooltip="{{ config('motions_recommendations_by') }}" translate>
No recomendation set
</span>
</a>
<ul class="dropdown-menu" aria-labelledby="recommendationDropdown{{ amendment.id }}">
<li ng-repeat="recommendation in amendment.state.getRecommendations()">
<a href ng-click="amendment.setRecommendation(recommendation.id)">
{{ recommendation.recommendation_label | translate }}
</a>
</li>
<li class="divider" ng-if="amendment.state.getRecommendations().length && amendment.recommendation"></li>
<li ng-if="amendment.recommendation">
<a href ng-click="amendment.setRecommendation(null)">
<i class="fa fa-exclamation-triangle"></i>
<translate>Reset recommendation</translate>
</a>
</li>
</ul>
</span>
</div>
<!-- Submitters -->
<div ng-if="amendment.submitters.length">
<small>
<span class="optional" translate>by</span>
<span class="optional" ng-repeat="submitter in amendment.submitters | limitTo:1">
{{ submitter.get_full_name() }}<span ng-if="!$last">,</span></span><span ng-if="amendment.submitters.length > 1">,
... [+{{ amendment.submitters.length - 1 }}]</span>
<!-- sorry for merging them together, but otherwise there would be a whitespace because of the new line -->
</small>
</div>
<!-- hover menu -->
<div ng-if="amendment.isAllowed('update')">
<small>
<a href="" ng-click="openDialog(amendment)" translate>Edit</a>
<span ng-if="amendment.isAllowed('delete')"> &middot;
<a href="" class="text-danger"
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br><b>{{ amendment.getTitle() }}</b>"
ng-bootbox-confirm-action="delete(amendment)" translate>Delete</a>
</span>
</small>
</div>
</div>
</div>
<div class="col-xs-6 col-space">
<!-- The diff -->
<section class="motion-text-holder" ng-if="amendment.isParagraphBasedAmendment()"
ng-init="paragraphs = amendment.getAmendmentParagraphsLinesDiff()">
<div ng-if="!paragraphs.length" translate>
No changes at the text.
</div>
<div ng-repeat="paragraph in paragraphs" class="motion-text motion-text-diff line-numbers-none">
<div ng-bind-html="paragraph.text | trusted"></div>
</div>
</section> <!-- Diff end -->
<div ng-if="!amendment.isParagraphBasedAmendment()">
{{ getTextPreview(amendment.getText(), 400) }}
</div>
</div>
<div class="col-xs-4 content" ng-style="{'width': isSelectMode ? 'calc(33.33% - 120px)' : 'calc(33.33% - 70px)'}">
<div style="width: 90%;">
<div ng-repeat="(id, field) in noSpecialCommentsFields">
<div class="nobr">
<i class="fa pointer spacer-right" ng-class="field[amendment.id] ? 'fa-caret-down' : 'fa-caret-right'"
ng-click="field[amendment.id] = !field[amendment.id]"
ng-if="isTextExpandable(amendment.comments[id], 30)"></i>
<strong>{{ field.name }}</strong>
</div>
<div ng-if="!field[amendment.id]">
{{ getTextPreview(amendment.comments[id], 30) }}
</div>
<div ng-if="field[amendment.id]" ng-bind-html="amendment.comments[id]"></div>
</div>
<div class="spacer-top" os-perms="motions.can_manage">
<button class="btn-link" ng-click="createModifiedAmendment(amendment)">
<translate>Create modified amendment</translate>
</button>
</div>
</div>
<div class="pull-right" style="width: 10%;">
<div class="centered" ng-if="config('motions_min_supporters') != 0"
uib-tooltip="{{ amendment.supporters.length }} {{ 'Supporters' | translate }}
{{ (config('motions_min_supporters') - amendment.supporters.length) > 0 ? '(' + (config('motions_min_supporters') - amendment.supporters.length) + ' ' + ('needed' | translate) + ')': '' }}"
tooltip-class="nobr">
<span class="badge"
ng-class="amendment.supporters.length < config('motions_min_supporters') ? 'badge-info' : 'badge-success'">
{{ amendment.supporters.length }}
</span>
</div>
</div>
</div>
</div>
<ul uib-pagination
ng-show="amendmentsFiltered.length > pagination.itemsPerPage"
total-items="amendmentsFiltered.length"
items-per-page="pagination.itemsPerPage"
ng-model="pagination.currentPage"
ng-change="pagination.pageChanged()"
class="pagination-sm"
direction-links="false"
boundary-links="true"
first-text="&laquo;"
last-text="&raquo;">
</ul>
</div> <!-- container -->
</div>
</div>
</div>

View File

@ -0,0 +1,16 @@
<h1>{{ title }}</h1>
<div uib-alert ng-show="alert.show" ng-class="'alert-' + (alert.type || 'warning')" close="alert={}">
{{ alert.msg }}
</div>
<form name="motionCommentForm" ng-submit="save(model)" novalidate>
<formly-form model="model" fields="formFields">
<button type="submit" ng-disabled="motionCommentForm.$invalid" class="btn btn-primary" translate>
Save
</button>
<button type="button" ng-click="closeThisDialog()" class="btn btn-default" translate>
Cancel
</button>
</formly-form>
</form>

View File

@ -1,10 +1,21 @@
<div class="header motion-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 ng-if="motion.isAmendment" ui-sref="motions.motion.amendment-list({id: motion.getParentMotion().id })"
class="btn btn-sm btn-default">
<i class="fa fa-angle-double-left fa-lg"></i> <i class="fa fa-angle-double-left fa-lg"></i>
<translate>Back to overview</translate> <translate>Back to overview</translate>
</a> </a>
<a ng-if="!motion.isAmendment" ui-sref="motions.motion.list"
class="btn btn-sm btn-default">
<i class="fa fa-angle-double-left fa-lg"></i>
<translate>Back to overview</translate>
</a>
<a ng-if="motion.hasAmendments()" ui-sref="motions.motion.amendment-list({id: motion.id})"
class="btn btn-sm btn-default">
<i class="fa fa-book fa-lg"></i>
<translate>Amendments</translate>
</a>
<!-- List of speakers --> <!-- List of speakers -->
<a ui-sref="agenda.item.detail({id: motion.agenda_item_id})" <a ui-sref="agenda.item.detail({id: motion.agenda_item_id})"
os-perms="agenda.can_see" class="btn btn-sm btn-default"> os-perms="agenda.can_see" class="btn btn-sm btn-default">
@ -23,20 +34,20 @@
<i class="fa fa-video-camera"></i> <i class="fa fa-video-camera"></i>
</button> </button>
<button type="button" class="btn btn-default btn-sm slimDropDown" uib-dropdown-toggle <button type="button" class="btn btn-default btn-sm slimDropDown" uib-dropdown-toggle
ng-if="projectors.length > 1 || change_recommendations.length" ng-if="projectors.length > 1 || has_proposed_changes"
ng-class="{ 'btn-primary': motion.isProjected().length && !inArray(motion.isProjected(), defaultProjectorId)}"> ng-class="{ 'btn-primary': motion.isProjected().length && !inArray(motion.isProjected(), defaultProjectorId)}">
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu" role="menu" aria-labelledby="split-button" <ul class="dropdown-menu" role="menu" aria-labelledby="split-button"
ng-if="projectors.length > 1 || change_recommendations.length"> ng-if="projectors.length > 1 || has_proposed_changes">
<li role="menuitem" ng-repeat="mode in projectionModes" ng-if="change_recommendations.length"> <li role="menuitem" ng-repeat="mode in projectionModes" ng-if="has_proposed_changes">
<a href="" ng-click="setProjectionMode(mode); $event.stopPropagation();"> <a href="" ng-click="setProjectionMode(mode); $event.stopPropagation();">
<i class="fa" ng-class="mode.mode == $parent.projectionMode.mode ? 'fa-check-square-o' : 'fa-square-o'"></i> <i class="fa" ng-class="mode.mode == $parent.projectionMode.mode ? 'fa-check-square-o' : 'fa-square-o'"></i>
<span ng-if="mode.mode!='agreed'">{{ mode.label | translate }}</span> <span ng-if="mode.mode!='agreed'">{{ mode.label | translate }}</span>
<span ng-if="mode.mode=='agreed'"><translate translate-context="resolution">Final version</translate></span> <span ng-if="mode.mode=='agreed'"><translate translate-context="resolution">Final version</translate></span>
</a> </a>
</li> </li>
<li class="divider" ng-show="projectors.length > 1 && change_recommendations.length > 0"></li> <li class="divider" ng-show="projectors.length > 1 && has_proposed_changes"></li>
<li role="menuitem" ng-repeat="projector in projectors | orderBy:'id'" ng-show="projectors.length > 1"> <li role="menuitem" ng-repeat="projector in projectors | orderBy:'id'" ng-show="projectors.length > 1">
<a href="" ng-click="motion.project(projector.id, projectionMode.mode)" <a href="" ng-click="motion.project(projector.id, projectionMode.mode)"
ng-class="{ 'projected': inArray(motion.isProjected(), projector.id) }"> ng-class="{ 'projected': inArray(motion.isProjected(), projector.id) }">
@ -62,13 +73,18 @@
</div> </div>
<h1 class="motion-title"> <h1 class="motion-title">
<span class="title-change-indicator" <span ng-if="!motion.isAmendment && viewChangeRecommendations.mode == 'original'">
ng-if="viewChangeRecommendations.mode == 'original' && title_change_recommendation" <span class="title-change-indicator" ng-if="title_change_recommendation"
ng-click="viewChangeRecommendations.scrollToDiffBox(title_change_recommendation.id)"></span> ng-click="viewChangeRecommendations.scrollToDiffBox(title_change_recommendation.id)"></span>
<span class="change-title" <span class="change-title" ng-if="motion.isAllowed('update') && !title_change_recommendation"></span>
ng-if="motion.isAllowed('update') && viewChangeRecommendations.mode == 'original' && !title_change_recommendation"></span> </span>
<span>{{ motion.getTitleWithChanges(viewChangeRecommendations.mode) }}</span> <a ui-sref="motions.motion.detail({id: motion.getParentMotion().id })" ng-if="motion.isAmendment">
{{ motion.getTitleWithChanges(viewChangeRecommendations.mode) }}
</a>
<span ng-if="!motion.isAmendment">
{{ 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"
@ -79,10 +95,6 @@
<div class="col-sm-6"> <div class="col-sm-6">
<h2> <h2>
<translate>Motion</translate> {{ motion.identifier }} <translate>Motion</translate> {{ motion.identifier }}
<span ng-if="parent">
(<translate>Amendment of motion</translate>
<a ui-sref="motions.motion.detail({id: parent.id})">{{ parent.identifier || parent.getTitle() }}</a>)
</span>
<span ng-if="motion.versions.length > 1" >| Version {{ motion.getVersion(version).version_number }}</span> <span ng-if="motion.versions.length > 1" >| Version {{ motion.getVersion(version).version_number }}</span>
<span ng-if="motion.active_version != version" class="label label-warning"> <span ng-if="motion.active_version != version" class="label label-warning">
<i class="fa fa-exclamation-triangle"></i> <i class="fa fa-exclamation-triangle"></i>
@ -154,15 +166,13 @@
<translate>Unsupport motion</translate> <translate>Unsupport motion</translate>
</button> </button>
</div> </div>
<!-- Amendments -->
<div ng-if="motion.isAllowed('can_see_amendments')"> <!-- Amendments -->
<div ng-if="!motion.isAmendment && motion.isAllowed('can_see_amendments')">
<h3 translate>Amendments</h3> <h3 translate>Amendments</h3>
<div ng-repeat="amendment in amendments | orderBy: 'identifier'"> <a ng-if="motion.hasAmendments()" ui-sref="motions.motion.amendment-list({id: motion.id})">
<a ui-sref="motions.motion.detail({id: amendment.id})"> {{ motion.getAmendments().length }} <translate>Amendments</translate><br>
<translate>Motion</translate> {{ amendment.identifier || amendment.getTitle() }}
</a> </a>
</div>
<button ng-if="motion.isAllowed('can_create_amendment')" ng-click="newAmendment()" class="btn btn-default btn-sm"> <button ng-if="motion.isAllowed('can_create_amendment')" ng-click="newAmendment()" class="btn btn-default btn-sm">
<i class="fa fa-plus"></i> <i class="fa fa-plus"></i>
<translate>New amendment</translate> <translate>New amendment</translate>
@ -180,13 +190,13 @@
</span> </span>
<ul uib-dropdown-menu class="dropdown-menu" aria-labelledby="state-dropdown"> <ul uib-dropdown-menu class="dropdown-menu" aria-labelledby="state-dropdown">
<li ng-repeat="state in motion.state.getNextStates()"> <li ng-repeat="state in motion.state.getNextStates()">
<a href ng-click="updateState(state.id)"> <a href ng-click="motion.setState(state.id)">
{{ state.action_word | translate }} {{ state.action_word | translate }}
<span ng-if="state.show_state_extension_field">...</span> <span ng-if="state.show_state_extension_field">...</span>
</a> </a>
<li class="divider" ng-if="motion.state.getNextStates().length && motion.isAllowed('reset_state')"> <li class="divider" ng-if="motion.state.getNextStates().length && motion.isAllowed('reset_state')">
<li ng-if="motion.isAllowed('reset_state')"> <li ng-if="motion.isAllowed('reset_state')">
<a href ng-click="reset_state()"> <a href ng-click="motion.setState(null)">
<i class="fa fa-exclamation-triangle"></i> <i class="fa fa-exclamation-triangle"></i>
<translate>Reset state</translate> <translate>Reset state</translate>
</a> </a>
@ -210,7 +220,7 @@
</div> </div>
<!-- Recommendation --> <!-- Recommendation -->
<div ng-if="config('motions_recommendations_by') != ''"> <div ng-if="config('motions_recommendations_by')">
<h3 ng-if="!motion.isAllowed('change_recommendation')" class="heading"> <h3 ng-if="!motion.isAllowed('change_recommendation')" class="heading">
{{ config('motions_recommendations_by') }} {{ config('motions_recommendations_by') }}
</h3> </h3>
@ -222,13 +232,13 @@
</span> </span>
<ul uib-dropdown-menu class="dropdown-menu" aria-labelledby="recommendation-dropdown"> <ul uib-dropdown-menu class="dropdown-menu" aria-labelledby="recommendation-dropdown">
<li ng-repeat="recommendation in motion.state.getRecommendations()"> <li ng-repeat="recommendation in motion.state.getRecommendations()">
<a href ng-click="updateRecommendation(recommendation.id)"> <a href ng-click="motion.setRecommendation(recommendation.id)">
{{ recommendation.recommendation_label | translate }} {{ recommendation.recommendation_label | translate }}
<span ng-if="recommendation.show_recommendation_extension_field">...</span> <span ng-if="recommendation.show_recommendation_extension_field">...</span>
</a> </a>
<li class="divider" ng-if="motion.state.getRecommendations().length && motion.recommendation"> <li class="divider" ng-if="motion.state.getRecommendations().length && motion.recommendation">
<li ng-if="motion.recommendation"> <li ng-if="motion.recommendation">
<a href ng-click="resetRecommendation()"> <a href ng-click="motion.setRecommendation(null)">
<i class="fa fa-exclamation-triangle"></i> <i class="fa fa-exclamation-triangle"></i>
<translate>Reset recommendation</translate> <translate>Reset recommendation</translate>
</a> </a>
@ -498,9 +508,10 @@
<div class="row"> <div class="row">
<!-- Motion toolbar --> <!-- Motion toolbar -->
<ng-include src="'static/templates/motions/motion-detail/toolbar.html'"></ng-include> <ng-include src="'static/templates/motions/motion-detail/toolbar.html'" ng-if="!motion.isParagraphBasedAmendment()"></ng-include>
<div ng-class="{'col-sm-8': (lineNumberMode != 'outside'), 'col-sm-12': (lineNumberMode == 'outside')}"> <div ng-class="{'col-sm-8': (lineNumberMode != 'outside'), 'col-sm-12': (lineNumberMode == 'outside')}"
ng-if="!motion.isParagraphBasedAmendment()">
<ng-include ng-if="viewChangeRecommendations.mode == 'diff'" <ng-include ng-if="viewChangeRecommendations.mode == 'diff'"
src="'static/templates/motions/motion-detail/change-summary.html'"></ng-include> src="'static/templates/motions/motion-detail/change-summary.html'"></ng-include>
@ -532,6 +543,15 @@
<!-- Agreed View --> <!-- Agreed View -->
<div ng-if="viewChangeRecommendations.mode == 'agreed'"> <div ng-if="viewChangeRecommendations.mode == 'agreed'">
<div class="alert alert-danger" ng-if="changed_version_has_accepted_collissions">
<i class="fa fa-warning"></i>
<translate>
At least two amendments or change recommendations affecting the same line are to be integrated.
This leads to undeterministic results.
Please resolve this conflict by not accepting multiple changes affecting the same line.
</translate>
</div>
<div ng-bind-html="motion.getTextByMode('agreed', version, highlight) | trusted" <div ng-bind-html="motion.getTextByMode('agreed', version, highlight) | trusted"
class="motion-text motion-text-changed line-numbers-{{ lineNumberMode }}"></div> class="motion-text motion-text-changed line-numbers-{{ lineNumberMode }}"></div>
@ -546,6 +566,10 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Paragraph-based Amendments -->
<ng-include src="'static/templates/motions/motion-detail/amendment-paragraph-diff.html'"></ng-include>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-8">

View File

@ -0,0 +1,43 @@
<div class="motion-toolbar" ng-if="motion.isParagraphBasedAmendment()">
<div class="toolbar-left {{ lineNumberMode }}">
<div class="btn-group pull-right" data-toggle="buttons">
<label class="btn btn-sm btn-default" ng-class="{active: showAmendmentContext}" ng-click="setShowAmendmentContext($event)">
<input type="checkbox" autocomplete="off" ng-model="showAmendmentContext" ng-checked="showAmendmentContext">
<translate>Show entire motion text</translate>
</label>
</div>
<ng-include src="'static/templates/motions/motion-detail/toolbar-line-numbering.html'"></ng-include>
</div>
</div>
<div ng-class="{'col-sm-8': (lineNumberMode != 'outside'), 'col-sm-12': (lineNumberMode == 'outside')}"
ng-if="motion.isParagraphBasedAmendment()">
<section class="motion-text-holder">
<div class="alert alert-info" ng-if="amendment_diff_paragraphs.length === 0">
<translate>No changes at the text</translate>
</div>
<div ng-repeat="paragraph in amendment_diff_paragraphs" class="motion-text motion-text-diff line-numbers-{{ lineNumberMode }}"
ng-class="{'amendment-context': showAmendmentContext}">
<div class="amendment-context" ng-if="showAmendmentContext">
<div ng-bind-html="motion.getParentMotion().getTextInLineRange(null, 1, paragraph.paragraphLineFrom) | trusted"
class="context"></div>
</div>
<h3 ng-if="paragraph.diffLineTo == paragraph.diffLineFrom + 1 && !showAmendmentContext" class="amendment-line-header">
<translate>Line</translate> {{ paragraph.diffLineFrom }}:
</h3>
<h3 ng-if="paragraph.diffLineTo != paragraph.diffLineFrom + 1 && !showAmendmentContext" class="amendment-line-header">
<translate>Line</translate> {{ paragraph.diffLineFrom }} - {{ paragraph.diffLineTo - 1 }}:
</h3>
<div class="paragraph-context" ng-bind-html="paragraph.textPre | trusted"></div>
<div ng-bind-html="paragraph.text | trusted"></div>
<div class="paragraph-context" ng-bind-html="paragraph.textPost | trusted"></div>
<div class="amendment-context" ng-if="showAmendmentContext">
<div ng-bind-html="motion.getParentMotion().getTextInLineRange(null, paragraph.paragraphLineTo, null) | trusted"></div>
</div>
</div>
</section>
</div>

View File

@ -1,17 +1,17 @@
<!-- A summary of all changes --> <!-- A summary of all changes -->
<section class="change-recommendation-overview"> <section class="change-recommendation-overview">
<strong> <strong>
<translate>Summary of change recommendations</translate>: <translate>Summary of changes</translate>:
</strong> </strong>
<button os-perms="motions.can_manage" class="btn btn-sm btn-default pull-right" <button os-perms="motions.can_manage" class="btn btn-sm btn-default pull-right"
uib-tooltip="{{ 'Note: You have to reject all change recommendations if the plenum does not follow the recommendation.' | translate }}" uib-tooltip="{{ 'Note: You have to reject all change recommendations if the plenum does not follow the recommendation. This does not affect amendments.' | translate }}"
ng-click="viewChangeRecommendations.rejectAll(motion)"> ng-click="viewChangeRecommendations.rejectAllChangeRecommendations(motion)">
<i class="fa fa-thumbs-down"></i> <i class="fa fa-thumbs-down"></i>
<translate>Reject all change recommendations</translate> <translate>Reject all change recommendations</translate>
</button> </button>
<ul ng-if="change_recommendations.length > 0 || title_change_recommendation"> <ul ng-if="has_proposed_changes">
<li ng-if="title_change_recommendation"> <li ng-if="title_change_recommendation">
<a href='' ng-click="viewChangeRecommendations.scrollToDiffBox(title_change_recommendation.id)"> <a href='' ng-click="viewChangeRecommendations.scrollToDiffBox(title_change_recommendation.id)">
<span class="line-number"><translate>Title</translate>:</span> <span class="line-number"><translate>Title</translate>:</span>
@ -21,30 +21,35 @@
</span> </span>
</a> </a>
</li> </li>
<li ng-repeat="change in (changes = (change_recommendations | filter:{motion_version_id:version}:true | orderBy: 'line_from')) "> <li ng-repeat="change in amendments_crs">
<a href='' ng-click="viewChangeRecommendations.scrollToDiffBox(change.id)"> <a href='' ng-click="viewChangeRecommendations.scrollToDiffBox(change.id)"
ng-class="{amendment: change.type === 'amendment', recommendation: change.type === 'recommendation'}">
<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">
<translate>Line</translate> {{ change.line_from }}: <translate>Line</translate> {{ change.line_from }}<span ng-if="change.type === 'recommendation'"></span>
</span> </span>
<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">
<translate>Line</translate> {{ change.line_from }} - {{ change.line_to - 1 }}: <translate>Line</translate> {{ change.line_from }} -
{{ change.line_to - 1 }}<span ng-if="change.type === 'recommendation'"></span>
</span> </span>
<span class="operation"> <span ng-if="change.type === 'recommendation'">(<translate>Change recommendation</translate>)</span>
<translate ng-if="change.getType(motion.getVersion(version).text) == 0">Replacement</translate> <span ng-if="change.type === 'amendment'">({{ change.original.identifier }})</span>
<translate ng-if="change.getType(motion.getVersion(version).text) == 1">Insertion</translate> <span class="operation" ng-if="change.type === 'recommendation'">
<translate ng-if="change.getType(motion.getVersion(version).text) == 2">Deletion</translate> <translate ng-if="change.original.getType(motion.getVersion(version).text) == 0">Replacement</translate>
<span ng-if="change.getType(motion.getVersion(version).text) == 3"> <translate ng-if="change.original.getType(motion.getVersion(version).text) == 1">Insertion</translate>
<translate ng-if="change.original.getType(motion.getVersion(version).text) == 2">Deletion</translate>
<span ng-if="change.original.getType(motion.getVersion(version).text) == 3">
{{ change.other_description }} {{ change.other_description }}
</span> </span>
</span> </span>
<span class="status"> <span class="status">
<translate ng-if="change.rejected">Rejected</translate> <translate ng-if="change.rejected">Rejected</translate>
<translate ng-if="change.accepted && change.type === 'amendment'">Accepted</translate>
</span> </span>
</a> </a>
</li> </li>
</ul> </ul>
<div ng-if="change_recommendations.length == 0 && !title_change_recommendation" class="no-changes"> <div ng-if="!has_proposed_changes" class="no-changes">
<translate>No change recommendations yet</translate> <translate>No change recommendations yet</translate>
</div> </div>
</section> </section>

View File

@ -0,0 +1,53 @@
<!-- line number mode for resonsive size medium/large (button group) -->
<div class="btn-group hidden-sm hidden-xs" data-toggle="buttons">
<span class="btn btn-sm btn-default disabled">
<i class="fa fa-list-ol" aria-hidden="true"></i>
<translate>Line numbering</translate>:
</span>
<label class="btn btn-sm btn-default" ng-class="{active: (lineNumberMode == 'none')}"
ng-click="setLineNumberMode('none')">
<input type="radio" name="lineNumberMode" value="none" ng-model="lineNumberMode"
ng-checked="lineNumberMode == 'none'">
<translate>none</translate>
</label>
<label class="btn btn-sm btn-default" ng-class="{active: (lineNumberMode == 'inline')}"
ng-click="setLineNumberMode('inline')">
<input type="radio" name="lineNumberMode" value="inline" ng-model="lineNumberMode"
ng-checked="lineNumberMode == 'inline'">
<translate>inline</translate>
</label>
<label class="btn btn-sm btn-default" ng-class="{active: (lineNumberMode == 'outside')}"
ng-click="setLineNumberMode('outside')">
<input type="radio" name="lineNumberMode" value="outside" ng-model="lineNumberMode"
ng-checked="lineNumberMode == 'outside'">
<translate>outside</translate>
</label>
</div>
<!-- line number mode for resonsive size small/extra small (dropdown) -->
<div class="dropdown hidden-md hidden-lg" uib-dropdown>
<button type="button" class="btn btn-default btn-sm" id="dropdownLineMode" uib-dropdown-toggle>
<i class="fa fa-list-ol" aria-hidden="true"></i>
<translate>Line numbering</translate>
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-left" aria-labelledby="dropdownLineMode">
<li>
<a href="" ng-click="setLineNumberMode('none')">
<i class="fa fa-check" ng-if="lineNumberMode == 'none'"></i>
<translate>none</translate>
</a>
</li>
<li>
<a href="" ng-click="setLineNumberMode('inline')">
<i class="fa fa-check" ng-if="lineNumberMode == 'inline'"></i>
<translate>inline</translate>
</a>
</li>
<li>
<a href="" ng-click="setLineNumberMode('outside')">
<i class="fa fa-check" ng-if="lineNumberMode == 'outside'"></i>
<translate>outside</translate>
</a>
</li>
</ul>
</div>

View File

@ -2,17 +2,17 @@
<!-- inline editing --> <!-- inline editing -->
<div class="pull-right inline-editing-activator" <div class="pull-right inline-editing-activator"
ng-if="motion.isAllowed('update') && version == motion.getVersion(-1).id && viewChangeRecommendations.mode == 'original'"> ng-if="motion.isAllowed('update') && version == motion.getVersion(-1).id && viewChangeRecommendations.mode == 'original'">
<button ng-if="!inlineEditing.active && change_recommendations.length == 0" ng-click="enableMotionInlineEditing()" <button ng-if="!inlineEditing.active && !has_proposed_changes" ng-click="enableMotionInlineEditing()"
class="btn btn-sm btn-default"> class="btn btn-sm btn-default">
<i class="fa fa-pencil-square-o"></i> <i class="fa fa-pencil-square-o"></i>
<translate>Inline editing</translate> <translate>Inline editing</translate>
</button> </button>
<button ng-if="inlineEditing.active && change_recommendations.length == 0" ng-click="disableMotionInlineEditing()" <button ng-if="inlineEditing.active && !has_proposed_changes" ng-click="disableMotionInlineEditing()"
class="btn btn-sm btn-default"> class="btn btn-sm btn-default">
<i class="fa fa-times-circle"></i> <i class="fa fa-times-circle"></i>
<translate>Inline editing</translate> <translate>Inline editing</translate>
</button> </button>
<button ng-if="change_recommendations.length > 0" class="btn btn-sm btn-default" disabled <button ng-if="has_proposed_changes" class="btn btn-sm btn-default" disabled
title="{{ 'Editing the text is not possible anymore once there are change recommendations.' | translate }}"> title="{{ 'Editing the text is not possible anymore once there are change recommendations.' | translate }}">
<i class="fa fa-pencil-square-o"></i> <i class="fa fa-pencil-square-o"></i>
<translate>Inline editing</translate> <translate>Inline editing</translate>
@ -21,56 +21,7 @@
<div class="toolbar-left {{ lineNumberMode }}"> <div class="toolbar-left {{ lineNumberMode }}">
<!-- line number mode for resonsive size medium/large (button group) --> <ng-include src="'static/templates/motions/motion-detail/toolbar-line-numbering.html'"></ng-include>
<div class="btn-group hidden-sm hidden-xs" data-toggle="buttons">
<span class="btn btn-sm btn-default disabled">
<i class="fa fa-list-ol" aria-hidden="true"></i>
<translate>Line numbering</translate>:
</span>
<label class="btn btn-sm btn-default" ng-class="{active: (lineNumberMode == 'none')}"
ng-click="setLineNumberMode('none')">
<input type="radio" name="lineNumberMode" value="none" ng-model="lineNumberMode"
ng-checked="lineNumberMode == 'none'">
<translate>none</translate>
</label>
<label class="btn btn-sm btn-default" ng-class="{active: (lineNumberMode == 'inline')}"
ng-click="setLineNumberMode('inline')">
<input type="radio" name="lineNumberMode" value="inline" ng-model="lineNumberMode"
ng-checked="lineNumberMode == 'inline'">
<translate>inline</translate>
</label>
<label class="btn btn-sm btn-default" ng-class="{active: (lineNumberMode == 'outside')}"
ng-click="setLineNumberMode('outside')">
<input type="radio" name="lineNumberMode" value="outside" ng-model="lineNumberMode"
ng-checked="lineNumberMode == 'outside'">
<translate>outside</translate>
</label>
</div>
<!-- line number mode for resonsive size small/extra small (dropdown) -->
<div class="dropdown hidden-md hidden-lg inline" uib-dropdown>
<button type="button" class="btn btn-default btn-sm" id="dropdownLineMode" uib-dropdown-toggle>
<i class="fa fa-list-ol" aria-hidden="true"></i>
<translate>Line numbering</translate>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownLineMode">
<li>
<a href="" ng-click="setLineNumberMode('none')">
<i class="fa fa-check" ng-if="lineNumberMode == 'none'"></i>
<translate>none</translate>
</a>
<li>
<a href="" ng-click="setLineNumberMode('inline')">
<i class="fa fa-check" ng-if="lineNumberMode == 'inline'"></i>
<translate>inline</translate>
</a>
<li>
<a href="" ng-click="setLineNumberMode('outside')">
<i class="fa fa-check" ng-if="lineNumberMode == 'outside'"></i>
<translate>outside</translate>
</a>
</ul>
</div>
<!-- go to line number --> <!-- go to line number -->
<div class="popover-wrapper"> <div class="popover-wrapper">
@ -98,7 +49,7 @@
</div> </div>
<!-- View Modes (Original, Diff, Changed) --> <!-- View Modes (Original, Diff, Changed) -->
<div class="motion-toolbar" ng-if="change_recommendations.length > 0 || title_change_recommendation"> <div class="motion-toolbar" ng-if="has_proposed_changes">
<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) -->
@ -144,7 +95,7 @@
<translate>Change recommendations</translate> <translate>Change recommendations</translate>
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownChangeVersion"> <ul class="dropdown-menu dropdown-menu-left" aria-labelledby="dropdownChangeVersion">
<li> <li>
<a href="" ng-click="viewChangeRecommendations.mode = 'original'"> <a href="" ng-click="viewChangeRecommendations.mode = 'original'">
<i class="fa fa-check" ng-if="viewChangeRecommendations.mode == 'original'"></i> <i class="fa fa-check" ng-if="viewChangeRecommendations.mode == 'original'"></i>

View File

@ -46,39 +46,55 @@
<!-- 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 }}">
<div ng-repeat="change in (changes = (change_recommendations | filter:{motion_version_id:version}:true | orderBy: 'line_from')) "> <div ng-repeat="change in amendments_crs">
<div class="motion-text original-text line-numbers-{{ lineNumberMode }}" <div class="motion-text original-text line-numbers-{{ lineNumberMode }}"
ng-bind-html="motion.getTextBetweenChangeRecommendations(version, changes[$index - 1], change, highlight) | trusted"></div> ng-if="$index === 0 || amendments_crs[$index - 1].line_to < change.line_from"
ng-bind-html="motion.getTextBetweenChanges(version, amendments_crs[$index - 1], change, highlight) | trusted"></div>
<div ng-class="motion.isAllowed('can_manage') ? 'diff-box' : ''" <div ng-class="{'collides': change.getCollissions().length > 0}"
class="diff-box-{{ change.id }} clearfix"> class="diff-box diff-box-{{ change.id }} clearfix">
<div class="action-row" ng-if="motion.isAllowed('can_manage')"> <div class="collission-hint" ng-if="change.getCollissions().length > 0">
<div class="btn-group" data-toggle="buttons"> <i class="fa fa-warning" uib-tooltip="{{ 'This change collides with another one.' | translate }}"></i>
<label class="btn btn-sm btn-default" ng-class="{active: !change.rejected}" </div>
title="{{ 'Not rejected' | translate }}" ng-click="change.rejected = false; change.saveStatus();"> <div class="action-row">
<span ng-if="motion.isAllowed('can_manage')">
<div class="btn-group" data-toggle="buttons" ng-if="change.type == 'recommendation'">
<label class="btn btn-sm btn-default"
ng-class="{active: change.accepted, disabled: change.getAcceptedCollissions().length > 0}"
title="{{ 'Not rejected' | translate }}" ng-click="change.setAccepted($event)">
<input type="radio" name="changeRecommendationRejected[{{ change.id }}]" value="0" <input type="radio" name="changeRecommendationRejected[{{ change.id }}]" value="0"
ng-change="change.saveStatus()" ng-model="change.rejected" ng-checked="change.rejected == false"> ng-disabled="change.getAcceptedCollissions().length > 0"
ng-change="change.saveStatus()" ng-model="change.rejected" ng-checked="change.accepted == true">
<i class="fa fa-thumbs-up"></i> <i class="fa fa-thumbs-up"></i>
</label> </label>
<label class="btn btn-sm btn-default" ng-class="{active: change.rejected}" <label class="btn btn-sm btn-default" ng-class="{active: change.rejected}"
title="{{ 'Rejected' | translate }}" ng-click="change.rejected = true; change.saveStatus();"> title="{{ 'Rejected' | translate }}" ng-click="change.setRejected($event)">
<input type="radio" name="changeRecommendationRejected[{{ change.id }}]" value="1" <input type="radio" name="changeRecommendationRejected[{{ change.id }}]" value="1"
ng-change="change.saveStatus()" ng-model="change.rejected" ng-checked="change.rejected == true"> ng-change="change.saveStatus()" ng-model="change.rejected" ng-checked="change.rejected == true">
<i class="fa fa-thumbs-down"></i> <i class="fa fa-thumbs-down"></i>
</label> </label>
</div> </div>
<button class="btn btn-default btn-sm pull-right btn-delete" <button class="btn btn-default btn-sm pull-right btn-delete"
ng-if="change.type == 'recommendation'"
ng-bootbox-confirm="{{ 'Are you sure you want to delete this change recommendation?' | translate }}" ng-bootbox-confirm="{{ 'Are you sure you want to delete this change recommendation?' | translate }}"
ng-bootbox-confirm-action="viewChangeRecommendations.delete(change.id)" ng-bootbox-confirm-action="viewChangeRecommendations.delete(change.original.id)"
title="{{ 'Delete' | translate }}"> title="{{ 'Delete' | translate }}">
<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"
<button class="btn btn-default btn-sm pull-right btn-edit" ng-click="createChangeRecommendation.editTextDialog(change)" ng-if="change.type == 'recommendation'"
ng-click="createChangeRecommendation.editTextDialog(change.original)"
title="{{ 'Edit' | translate }}"> title="{{ 'Edit' | translate }}">
<i class="fa fa-pencil"></i> <i class="fa fa-pencil"></i>
</button> </button>
</span>
<a ng-if="change.type == 'amendment'" ui-sref="motions.motion.detail({id: change.original.id})"
uib-tooltip="{{ 'Open amendment' | translate }}"
class="btn btn-default btn-sm pull-right btn-amend-info">
<i class="fa fa-info"></i>
{{ change.original.identifier }}
</a>
</div> </div>
<div class="status-row" ng-if="!motion.isAllowed('can_manage') && change.rejected"> <div class="status-row" ng-if="!motion.isAllowed('can_manage') && change.rejected">
<i class="grey"><translate>Rejected</translate>:</i> <i class="grey"><translate>Rejected</translate>:</i>
@ -90,6 +106,6 @@
</div> </div>
<div class="motion-text original-text line-numbers-{{ lineNumberMode }}" <div class="motion-text original-text line-numbers-{{ lineNumberMode }}"
ng-bind-html="motion.getTextRemainderAfterLastChangeRecommendation(version, changes, highlight) | trusted"></div> ng-bind-html="motion.getTextRemainderAfterLastChange(version, amendments_crs, highlight) | trusted"></div>
</div> </div>
</div> </div>

View File

@ -5,6 +5,10 @@
<i class="fa fa-plus fa-lg"></i> <i class="fa fa-plus fa-lg"></i>
<translate>New</translate> <translate>New</translate>
</a> </a>
<a ui-sref="motions.motion.allamendments" class="btn btn-default btn-sm">
<i class="fa fa-book fa-lg"></i>
<translate>Amendments</translate>
</a>
<a ui-sref="motions.category.list" os-perms="motions.can_manage" class="btn btn-default btn-sm"> <a ui-sref="motions.category.list" os-perms="motions.can_manage" class="btn btn-default btn-sm">
<i class="fa fa-sitemap fa-lg"></i> <i class="fa fa-sitemap fa-lg"></i>
<translate>Categories</translate> <translate>Categories</translate>
@ -39,7 +43,7 @@
</button> </button>
<!-- Export button --> <!-- Export button -->
<button type="button" class="btn btn-default btn-sm pull-right" <button type="button" class="btn btn-default btn-sm pull-right"
os-perms="motions.can_manage" ng-click="openExportDialog()"> os-perms="motions.can_manage" ng-click="openExportDialog(motionsFiltered)">
<i class="fa fa-upload"></i> <i class="fa fa-upload"></i>
<span ng-if="motionsFiltered.length === motions.length" translate> <span ng-if="motionsFiltered.length === motions.length" translate>
Export all Export all
@ -49,7 +53,7 @@
</span> </span>
</button> </button>
<button type="button" class="btn btn-default btn-sm pull-right" <button type="button" class="btn btn-default btn-sm pull-right"
os-perms="!motions.can_manage" ng-click="pdfExport()"> os-perms="!motions.can_manage" ng-click="pdfExport(motionsFiltered)">
<i class="fa fa-file-pdf-o"></i> <i class="fa fa-file-pdf-o"></i>
<span ng-if="motionsFiltered.length === motions.length" translate> <span ng-if="motionsFiltered.length === motions.length" translate>
Export all Export all
@ -74,13 +78,13 @@
<!-- state select --> <!-- state select -->
<select ng-show="selectedAction == 'setStatus'" ng-model="selectedState" class="form-control input-sm"> <select ng-show="selectedAction == 'setStatus'" ng-model="selectedState" class="form-control input-sm">
<option value="" translate>--- Select state ---</option> <option value="" translate>--- Select state ---</option>
<option ng-repeat="state in states" ng-disabled="state.workflowHeader" value="{{ state.id }}"> <option ng-repeat="state in states" ng-if="!state.divider" ng-disabled="state.workflowHeader" value="{{ state.id }}">
{{ (state.workflowHeader ? state.headername : state.name) | translate }} {{ (state.workflowHeader ? state.headername : state.name) | translate }}
</option> </option>
</select> </select>
<!-- set state button --> <!-- set state button -->
<a ng-show="selectedAction == 'setStatus' && selectedState" <a ng-show="selectedAction == 'setStatus' && selectedState"
ng-click="setStatusMultiple(selectedState)" class="btn btn-default btn-sm"> ng-click="setStatusMultiple(motionsFiltered, selectedState)" class="btn btn-default btn-sm">
<translate>Set status</translate> <translate>Set status</translate>
</a> </a>
<!-- category select --> <!-- category select -->
@ -94,7 +98,7 @@
</select> </select>
<!-- set category button --> <!-- set category button -->
<a ng-show="selectedAction == 'setCategory' && selectedCategory" <a ng-show="selectedAction == 'setCategory' && selectedCategory"
ng-click="setCategoryMultiple(selectedCategory)" class="btn btn-default btn-sm"> ng-click="setCategoryMultiple(motionsFiltered, selectedCategory)" class="btn btn-default btn-sm">
<translate>Set category</translate> <translate>Set category</translate>
</a> </a>
<!-- motionBlock select --> <!-- motionBlock select -->
@ -107,13 +111,13 @@
</select> </select>
<!-- set motion block button --> <!-- set motion block button -->
<a ng-show="selectedAction == 'setMotionBlock' && selectedMotionBlock" <a ng-show="selectedAction == 'setMotionBlock' && selectedMotionBlock"
ng-click="setMotionBlockMultiple(selectedMotionBlock)" class="btn btn-default btn-sm"> ng-click="setMotionBlockMultiple(motionsFilterd, selectedMotionBlock)" class="btn btn-default btn-sm">
<translate>Set motion block</translate> <translate>Set motion block</translate>
</a> </a>
<!-- delete button --> <!-- delete button -->
<a ng-show="selectedAction == 'delete'" <a ng-show="selectedAction == 'delete'"
ng-bootbox-confirm="{{ 'Are you sure you want to delete all selected motions?' | translate }}" ng-bootbox-confirm="{{ 'Are you sure you want to delete all selected motions?' | translate }}"
ng-bootbox-confirm-action="deleteMultiple()" ng-bootbox-confirm-action="deleteMultiple(motionsFiltered)"
class="btn btn-default btn-sm btn-danger"> class="btn btn-default btn-sm btn-danger">
<i class="fa fa-trash fa-lg"></i> <i class="fa fa-trash fa-lg"></i>
<translate>Delete selected motions</translate> <translate>Delete selected motions</translate>
@ -124,7 +128,8 @@
<div class="spacer-top-lg italic row"> <div class="spacer-top-lg italic row">
<div class="col-md-6"> <div class="col-md-6">
{{ motionsFiltered.length }} / {{ motionsFiltered.length }} /
{{ motions.length }} {{ "motions" | translate }}<span ng-if="(motions|filter:{selected:true}).length > 0">, {{ motions.length }}
<translate>motions</translate><span ng-if="(motions|filter:{selected:true}).length > 0">,
{{(motions|filter:{selected:true}).length}} {{ "selected" | translate }}</span> {{(motions|filter:{selected:true}).length}} {{ "selected" | translate }}</span>
</div> </div>
<div class="col-md-6" ng-show="motionsFiltered.length > pagination.itemsPerPage"> <div class="col-md-6" ng-show="motionsFiltered.length > pagination.itemsPerPage">
@ -147,382 +152,10 @@
<div class="row header-row"> <div class="row header-row">
<div class="col-xs-1 centered" ng-if="isSelectMode"> <div class="col-xs-1 centered" ng-if="isSelectMode">
<i class="fa text-danger pointer" ng-class=" selectedAll ? 'fa-check-square-o' : 'fa-square-o'" <i class="fa text-danger pointer" ng-class=" selectedAll ? 'fa-check-square-o' : 'fa-square-o'"
ng-click="checkAll()"></i> ng-click="checkAll(motionsFiltered)"></i>
</div> </div>
<div class="col-xs-11 main-header" ng-style="{'width': isSelectMode ? '' : '100%'}"> <div class="col-xs-11 main-header" ng-style="{'width': isSelectMode ? '' : '100%'}">
<div class="form-inline text-right pull-right"> <ng-include src="'static/templates/motions/motion-table-filters.html'"></ng-include>
<!-- State filter -->
<span uib-dropdown>
<span class="pointer" id="dropdownState" uib-dropdown-toggle
ng-class="{'bold': filter.multiselectFilters.state.length > 0, 'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>State</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-left" aria-labelledby="dropdownState">
<li ng-repeat="state in states" ng-class="{'dropdown-header': state.workflowHeader, 'divider': state.divider}">
<a ng-if="state.workflowHeader">
{{ state.headername | translate }}
</a>
<a href ng-if="!state.workflowHeader && !state.divider"
ng-click="operateStateFilter(state.id, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.state.indexOf(state.id) > -1"></i>
{{ state.name | translate }}
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="operateStateFilter(-1, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.state.indexOf(-1) > -1"></i>
<translate>done</translate>
</a>
</li>
<li>
<a href ng-click="operateStateFilter(-2, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.state.indexOf(-2) > -1"></i>
<translate>undone</translate>
</a>
</li>
</ul>
</span>
<!-- recommendation filter -->
<span uib-dropdown ng-if="config('motions_recommendations_by') != ''">
<span class="pointer" id="dropdownRecommendation" uib-dropdown-toggle
ng-class="{'bold': filter.multiselectFilters.recommendation.length > 0, 'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>Recommendation</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-left" aria-labelledby="dropdownRecommentation">
<li ng-repeat="recommendation in recommendations" ng-class="recommendation.workflowHeader ? 'dropdown-header' : ''">
<a ng-if="recommendation.workflowHeader">
{{ recommendation.headername | translate }}
</a>
<a href ng-if="!recommendation.workflowHeader"
ng-click="filter.operateMultiselectFilter('recommendation', recommendation.id, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.recommendation.indexOf(recommendation.id) > -1"></i>
{{ recommendation.recommendation_label | translate }}
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="filter.operateMultiselectFilter('recommendation', -1, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.recommendation.indexOf(-1) > -1"></i>
<translate>No recommendation set</translate>
</a>
</li>
</ul>
</span>
<!-- Category filter -->
<span uib-dropdown ng-if="categories.length > 0">
<span class="pointer" id="dropdownCategory" uib-dropdown-toggle
ng-class="{'bold': filter.multiselectFilters.category.length > 0, 'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>Category</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-left" aria-labelledby="dropdownCategory">
<li ng-repeat="category in categories | orderBy: config('motions_export_category_sorting')">
<a href ng-click="filter.operateMultiselectFilter('category', category.id, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.category.indexOf(category.id) > -1"></i>
{{ category.prefix }} &ndash; {{ category.name }}
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="filter.operateMultiselectFilter('category', -1, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.category.indexOf(-1) > -1"></i>
<translate>No category set</translate>
</a>
</li>
</ul>
</span>
<!-- Motion block filter -->
<span uib-dropdown ng-if="motionBlocks.length > 0">
<span class="pointer" id="dropdownBlock" uib-dropdown-toggle
ng-class="{'bold': filter.multiselectFilters.motionBlock.length > 0, 'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>Motion block</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-left" aria-labelledby="dropdownBlock">
<li ng-repeat="block in motionBlocks | orderBy: 'title'">
<a href ng-click="filter.operateMultiselectFilter('motionBlock', block.id, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.motionBlock.indexOf(block.id) > -1"></i>
{{ block.title }}
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="filter.operateMultiselectFilter('motionBlock', -1, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.motionBlock.indexOf(-1) > -1"></i>
<translate>No motion block set</translate>
</a>
</li>
</ul>
</span>
<!-- Comment filter -->
<span uib-dropdown ng-if="showCommentsFilter()">
<span class="pointer" id="dropdownComment" uib-dropdown-toggle
ng-class="{'bold': filter.multiselectFilters.comment.length > 0, 'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>Comment</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-left" aria-labelledby="dropdownComment">
<li ng-repeat="(index, commentsField) in noSpecialCommentsFields">
<a href ng-click="filter.operateMultiselectFilter('comment', index, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.comment.indexOf(index) > -1"></i>
{{ commentsField.name }} <translate>is set</translate>
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="filter.operateMultiselectFilter('comment', -1, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.comment.indexOf(-1) > -1"></i>
<translate>No comments set</translate>
</a>
</li>
</ul>
</span>
<!-- Tag filter -->
<span uib-dropdown ng-if="tags.length > 0">
<span class="pointer" id="dropdownTag" uib-dropdown-toggle
ng-class="{'bold': filter.multiselectFilters.tag.length > 0, 'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>Tag</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownTag">
<li ng-repeat="tag in tags">
<a href ng-click="filter.operateMultiselectFilter('tag', tag.id, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.tag.indexOf(tag.id) > -1"></i>
{{ tag.name }}
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="filter.operateMultiselectFilter('tag', -1, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.tag.indexOf(-1) > -1"></i>
<translate>No tag set</translate>
</a>
</li>
</ul>
</span>
<!-- boolean Filters (customized!) -->
<span ng-if="operator.user.id" uib-dropdown>
<span class="pointer" id="dropdownMisc" uib-dropdown-toggle
ng-class="{'bold': (filter.booleanFilters.isFavorite.value !== undefined) ||
(filter.booleanFilters.hasPersonalNote.value !== undefined) ||
(filter.booleanFilters.isAmendment.value !== undefined), 'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>Misc</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMisc">
<li>
<a href ng-click="filter.booleanFilters.isAmendment.value = (filter.booleanFilters.isAmendment.value ? undefined : true); filter.save();">
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.isAmendment.value === true}"></i>
{{ filter.booleanFilters.isAmendment.choiceYes | translate }}
</a>
</li>
<li>
<a href ng-click="filter.booleanFilters.isAmendment.value = (filter.booleanFilters.isAmendment.value === false) ? undefined : false; filter.save();">
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.isAmendment.value === false}"></i>
{{ filter.booleanFilters.isAmendment.choiceNo | translate }}
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="filter.booleanFilters.isFavorite.value = (filter.booleanFilters.isFavorite.value ? undefined : true); filter.save();">
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.isFavorite.value === true}"></i>
{{ filter.booleanFilters.isFavorite.choiceYes | translate }}
</a>
</li>
<li>
<a href ng-click="filter.booleanFilters.isFavorite.value = (filter.booleanFilters.isFavorite.value === false) ? undefined : false; filter.save();">
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.isFavorite.value === false}"></i>
{{ filter.booleanFilters.isFavorite.choiceNo | translate }}
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="filter.booleanFilters.hasPersonalNote.value = (filter.booleanFilters.hasPersonalNote.value ? undefined : true); filter.save();">
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.hasPersonalNote.value === true}"></i>
{{ filter.booleanFilters.hasPersonalNote.choiceYes | translate }}
</a>
</li>
<li>
<a href ng-click="filter.booleanFilters.hasPersonalNote.value = (filter.booleanFilters.hasPersonalNote.value === false) ? undefined : false; filter.save();">
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.hasPersonalNote.value === false}"></i>
{{ filter.booleanFilters.hasPersonalNote.choiceNo | translate }}
</a>
</li>
</ul>
</span>
<!-- dropdown sort -->
<span uib-dropdown>
<span class="pointer" id="dropdownSort" uib-dropdown-toggle
ng-class="{'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>Sort</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownSort">
<!-- item -->
<li>
<a href ng-click="sort.toggle('agenda_item.getItemNumberWithAncestors()')">
<translate translate-comment="short form of agenda item">Item</translate>
<span class="spacer-right pull-right"></span>
<i class="pull-right fa"
ng-style="{'visibility': sort.column === 'agenda_item.getItemNumberWithAncestors()' ? 'visible' : 'hidden'}"
ng-class="sort.reverse ? 'fa-sort-amount-desc' : 'fa-sort-amount-asc'">
</i>
</a>
</li>
<!-- all other sortOptions -->
<li ng-repeat="option in sortOptions">
<a href ng-click="sort.toggle(option.name)">
<span ng-style="{'font-weight': sort.column === option.name ? 'bold' : 'normal'}">
{{ option.display_name | translate }}
</span>
<span class="spacer-right pull-right"></span>
<i class="pull-right fa"
ng-style="{'visibility': sort.column === option.name ? 'visible' : 'hidden'}"
ng-class="sort.reverse ? 'fa-sort-amount-desc' : 'fa-sort-amount-asc'">
</i>
</a>
</li>
</ul>
</span>
<!-- search field -->
<span class="form-group">
<span class="input-group">
<span class="input-group-addon"><i class="fa fa-search"></i></span>
<input type="text" ng-model="filter.filterString" class="form-control"
placeholder="{{ 'Search' | translate}}" ng-disabled="isSelectMode"
ng-change="filter.save()">
</span>
</span>
</div>
<!-- show all selected multiselectoptions -->
<div>
<!-- clear all filters -->
<span class="spacer-left-lg pointer" ng-click="resetFilters(isSelectMode)"
ng-if="filter.areFiltersSet()" ng-disabled="isSelectMode"
ng-class="{'disabled': isSelectMode}">
<i class="fa fa-window-close"></i>
<strong translate>All Filters</strong>
</span>
<!-- state -->
<span ng-repeat="state in states" class="pointer spacer-left-lg"
ng-if="!state.workflowHeader && filter.multiselectFilters.state.indexOf(state.id) > -1"
ng-click="operateStateFilter(state.id, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
{{ state.name | translate }}
</span>
</span>
<span ng-if="filter.multiselectFilters.state.indexOf(-1) > -1" class="pointer spacer-left-lg"
ng-click="operateStateFilter(-1, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i>
<translate>done</translate>
</span>
<span ng-if="filter.multiselectFilters.state.indexOf(-2) > -1" class="pointer spacer-left-lg"
ng-click="operateStateFilter(-2, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i>
<translate>undone</translate>
</span>
<!-- category -->
<span ng-repeat="category in categories | orderBy: config('motions_export_category_sorting')"
class="pointer spacer-left-lg"
ng-if="filter.multiselectFilters.category.indexOf(category.id) > -1"
ng-click="filter.operateMultiselectFilter('category', category.id, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
{{ category.prefix }} &ndash; {{ category.name }}
</span>
</span>
<span ng-if="filter.multiselectFilters.category.indexOf(-1) > -1" class="pointer spacer-left-lg"
ng-click="filter.operateMultiselectFilter('category', -1, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i>
<translate>No category set</translate>
</span>
<!-- motion block -->
<span ng-repeat="motionBlock in motionBlocks | orderBy: 'title'" class="pointer spacer-left-lg"
ng-if="filter.multiselectFilters.motionBlock.indexOf(motionBlock.id) > -1"
ng-click="filter.operateMultiselectFilter('motionBlock', motionBlock.id, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
{{ motionBlock.title }}
</span>
</span>
<!-- comment -->
<span ng-repeat="(index, commentsField) in noSpecialCommentsFields" class="pointer spacer-left-lg"
ng-if="filter.multiselectFilters.comment.indexOf(index) > -1"
ng-click="filter.operateMultiselectFilter('comment', index, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
{{ commentsField.name }}
</span>
</span>
<span ng-if="filter.multiselectFilters.comment.indexOf(-1) > -1" class="pointer spacer-left-lg"
ng-click="filter.operateMultiselectFilter('comment', -1, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i>
<translate>No comments set</translate>
</span>
<!-- recommendation -->
<span ng-repeat="recommendation in recommendations" class="pointer spacer-left-lg"
ng-if="filter.multiselectFilters.recommendation.indexOf(recommendation.id) > -1"
ng-click="filter.operateMultiselectFilter('recommendation', recommendation.id, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
{{ recommendation.recommendation_label | translate }}
</span>
</span>
<span ng-if="filter.multiselectFilters.motionBlock.indexOf(-1) > -1" class="pointer spacer-left-lg"
ng-click="filter.operateMultiselectFilter('motionBlock', -1, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i>
<translate>No motion block set</translate>
</span>
<!-- tags -->
<span ng-repeat="tag in tags" class="pointer spacer-left-lg"
ng-if="filter.multiselectFilters.tag.indexOf(tag.id) > -1"
ng-click="filter.operateMultiselectFilter('tag', tag.id, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
{{ tag.name }}
</span>
</span>
<span ng-if="filter.multiselectFilters.tag.indexOf(-1) > -1" class="pointer spacer-left-lg"
ng-click="filter.operateMultiselectFilter('tag', -1, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i>
<translate>No tag set</translate>
</span>
<!-- for all boolean Filters -->
<span ng-repeat="(name, booleanFilter) in filter.booleanFilters"
ng-hide="booleanFilter.value === undefined"
class="pointer spacer-left-lg"
ng-click="booleanFilter.value = undefined; filter.save();"
ng-class="{'disabled': isSelectMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
{{ booleanFilter.value ? booleanFilter.choiceYes : booleanFilter.choiceNo | translate }}
</span>
</span>
</div>
</div> </div>
</div> </div>
@ -541,7 +174,6 @@
| MultiselectFilter: filter.multiselectFilters.tag : getItemId.tag | MultiselectFilter: filter.multiselectFilters.tag : getItemId.tag
| filter: {star: filter.booleanFilters.isFavorite.value} | filter: {star: filter.booleanFilters.isFavorite.value}
| filter: {hasPersonalNote: filter.booleanFilters.hasPersonalNote.value} | filter: {hasPersonalNote: filter.booleanFilters.hasPersonalNote.value}
| filter: {isAmendment: filter.booleanFilters.isAmendment.value}
| toArray | toArray
| orderByEmptyLast: sort.column : sort.reverse) | orderByEmptyLast: sort.column : sort.reverse)
| limitTo : pagination.itemsPerPage : pagination.limitBegin"> | limitTo : pagination.itemsPerPage : pagination.limitBegin">
@ -585,11 +217,11 @@
<i class="fa fa-cog pointer" uib-dropdown-toggle id="stateDropdown{{ motion.id }}"></i> <i class="fa fa-cog pointer" uib-dropdown-toggle id="stateDropdown{{ motion.id }}"></i>
<ul class="dropdown-menu" aria-labelledby="stateDropdown{{ motion.id }}"> <ul class="dropdown-menu" aria-labelledby="stateDropdown{{ motion.id }}">
<li ng-repeat="state in motion.state.getNextStates()"> <li ng-repeat="state in motion.state.getNextStates()">
<a href ng-click="updateState(motion, state.id)">{{ state.action_word | translate }}</a> <a href ng-click="motion.setState(state.id)">{{ state.action_word | translate }}</a>
</li> </li>
<li class="divider" ng-if="motion.state.getNextStates().length"></li> <li class="divider" ng-if="motion.state.getNextStates().length"></li>
<li> <li>
<a href ng-if="motion.isAllowed('reset_state')" ng-click="resetState(motion)"> <a href ng-if="motion.isAllowed('reset_state')" ng-click="motion.setState(null)">
<i class="fa fa-exclamation-triangle"></i> <i class="fa fa-exclamation-triangle"></i>
<translate>Reset state</translate> <translate>Reset state</translate>
</a> </a>
@ -599,7 +231,7 @@
</span> </span>
</div> </div>
<!-- recommendation --> <!-- recommendation -->
<div ng-if="motion.recommendation"> <div ng-if="motion.recommendation && config('motions_recommendations_by')">
<span ng-mouseover="motion.recommendationHover=true" ng-mouseleave="motion.recommendationHover=false" <span ng-mouseover="motion.recommendationHover=true" ng-mouseleave="motion.recommendationHover=false"
class="dropdown-hover-space"> class="dropdown-hover-space">
<span class="label" ng-class="'label-'+motion.recommendation.css_class" uib-tooltip="{{ config('motions_recommendations_by') }}"> <span class="label" ng-class="'label-'+motion.recommendation.css_class" uib-tooltip="{{ config('motions_recommendations_by') }}">
@ -609,13 +241,13 @@
<i class="fa fa-cog pointer" uib-dropdown-toggle id="recommendationDropdown{{ motion.id }}"></i> <i class="fa fa-cog pointer" uib-dropdown-toggle id="recommendationDropdown{{ motion.id }}"></i>
<ul class="dropdown-menu" aria-labelledby="recommendationDropdown{{ motion.id }}"> <ul class="dropdown-menu" aria-labelledby="recommendationDropdown{{ motion.id }}">
<li ng-repeat="recommendation in motion.state.getRecommendations()"> <li ng-repeat="recommendation in motion.state.getRecommendations()">
<a href ng-click="updateRecommendation(motion, recommendation.id)"> <a href ng-click="motion.setRecommendation(recommendation.id)">
{{ recommendation.recommendation_label | translate }} {{ recommendation.recommendation_label | translate }}
</a> </a>
</li> </li>
<li class="divider" ng-if="motion.state.getRecommendations().length && motion.recommendation"></li> <li class="divider" ng-if="motion.state.getRecommendations().length && motion.recommendation"></li>
<li ng-if="motion.recommendation"> <li ng-if="motion.recommendation">
<a href ng-click="resetRecommendation(motion)"> <a href ng-click="motion.setRecommendation(null)">
<i class="fa fa-exclamation-triangle"></i> <i class="fa fa-exclamation-triangle"></i>
<translate>Reset recommendation</translate> <translate>Reset recommendation</translate>
</a> </a>
@ -657,7 +289,7 @@
</div> </div>
<!-- additional content column --> <!-- additional content column -->
<div class="col-xs-4 content" ng-style="{'width': isSelectMode ? 'calc(50% - 120px)' : 'calc(50% - 70px)'}"> <div class="col-xs-4 content" ng-style="{'width': isSelectMode ? 'calc(50% - 120px)' : 'calc(50% - 70px)'}">
<div style="width: 50%;" class="optional"> <div style="width: 45%;" class="optional">
<small> <small>
<!-- Category dropdown for manage user --> <!-- Category dropdown for manage user -->
<div os-perms="motions.can_manage" ng-show="categories.length > 0" <div os-perms="motions.can_manage" ng-show="categories.length > 0"
@ -788,7 +420,7 @@
</span> </span>
</div> </div>
</div> </div>
<div style="width: 10%;" class="pull-right"> <div style="width: 15%;" class="pull-right">
<div class="centered" ng-if="(motion.agenda_item.speakers | filter: {'begin_time': null}).length" <div class="centered" ng-if="(motion.agenda_item.speakers | filter: {'begin_time': null}).length"
uib-tooltip="{{ (motion.agenda_item.speakers | filter: {'begin_time': null}).length }} {{ 'speakers' | translate }}" uib-tooltip="{{ (motion.agenda_item.speakers | filter: {'begin_time': null}).length }} {{ 'speakers' | translate }}"
tooltip-class="nobr"> tooltip-class="nobr">
@ -797,6 +429,14 @@
{{ (motion.agenda_item.speakers | filter: {'begin_time': null}).length }} {{ (motion.agenda_item.speakers | filter: {'begin_time': null}).length }}
</a> </a>
</div> </div>
<div class="centered" ng-if="motion.hasAmendments()"
uib-tooltip="{{ motion.getAmendments().length }} {{ 'amendments' | translate }}"
tooltip-class="nobr">
<a ui-sref="motions.motion.amendment-list({id: motion.id})" class="badge">
<i class="fa fa-book"></i>
{{ motion.getAmendments().length }}
</a>
</div>
</div> </div>
<div style="width: 30%;" class="pull-right"> <div style="width: 30%;" class="pull-right">
<div class="centered">{{ motion.agenda_item.getItemNumberWithAncestors() }}</div> <div class="centered">{{ motion.agenda_item.getItemNumberWithAncestors() }}</div>

View File

@ -0,0 +1,357 @@
<div class="form-inline text-right pull-right">
<!-- State filter -->
<span uib-dropdown>
<span class="pointer" id="dropdownState" uib-dropdown-toggle
ng-class="{'bold': filter.multiselectFilters.state.length > 0, 'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>State</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-left" aria-labelledby="dropdownState">
<li ng-repeat="state in states" ng-class="{'dropdown-header': state.workflowHeader, 'divider': state.divider}">
<a ng-if="state.workflowHeader">
{{ state.headername | translate }}
</a>
<a href ng-if="!state.workflowHeader && !state.divider"
ng-click="operateStateFilter(state.id, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.state.indexOf(state.id) > -1"></i>
{{ state.name | translate }}
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="operateStateFilter(-1, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.state.indexOf(-1) > -1"></i>
<translate>done</translate>
</a>
</li>
<li>
<a href ng-click="operateStateFilter(-2, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.state.indexOf(-2) > -1"></i>
<translate>undone</translate>
</a>
</li>
</ul>
</span>
<!-- recommendation filter -->
<span uib-dropdown ng-if="config('motions_recommendations_by') != ''">
<span class="pointer" id="dropdownRecommendation" uib-dropdown-toggle
ng-class="{'bold': filter.multiselectFilters.recommendation.length > 0, 'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>Recommendation</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-left" aria-labelledby="dropdownRecommentation">
<li ng-repeat="recommendation in recommendations" ng-class="recommendation.workflowHeader ? 'dropdown-header' : ''">
<a ng-if="recommendation.workflowHeader">
{{ recommendation.headername | translate }}
</a>
<a href ng-if="!recommendation.workflowHeader"
ng-click="filter.operateMultiselectFilter('recommendation', recommendation.id, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.recommendation.indexOf(recommendation.id) > -1"></i>
{{ recommendation.recommendation_label | translate }}
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="filter.operateMultiselectFilter('recommendation', -1, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.recommendation.indexOf(-1) > -1"></i>
<translate>No recommendation set</translate>
</a>
</li>
</ul>
</span>
<!-- Category filter -->
<span uib-dropdown ng-if="categories.length > 0">
<span class="pointer" id="dropdownCategory" uib-dropdown-toggle
ng-class="{'bold': filter.multiselectFilters.category.length > 0, 'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>Category</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownCategory">
<li ng-repeat="category in categories | orderBy: config('motions_export_category_sorting')">
<a href ng-click="filter.operateMultiselectFilter('category', category.id, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.category.indexOf(category.id) > -1"></i>
{{ category.prefix }} &ndash; {{ category.name }}
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="filter.operateMultiselectFilter('category', -1, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.category.indexOf(-1) > -1"></i>
<translate>No category set</translate>
</a>
</li>
</ul>
</span>
<!-- Motion block filter -->
<span uib-dropdown ng-if="motionBlocks.length > 0">
<span class="pointer" id="dropdownBlock" uib-dropdown-toggle
ng-class="{'bold': filter.multiselectFilters.motionBlock.length > 0, 'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>Motion block</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownBlock">
<li ng-repeat="block in motionBlocks | orderBy: 'title'">
<a href ng-click="filter.operateMultiselectFilter('motionBlock', block.id, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.motionBlock.indexOf(block.id) > -1"></i>
{{ block.title }}
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="filter.operateMultiselectFilter('motionBlock', -1, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.motionBlock.indexOf(-1) > -1"></i>
<translate>No motion block set</translate>
</a>
</li>
</ul>
</span>
<!-- Comment filter -->
<span uib-dropdown ng-if="showCommentsFilter()">
<span class="pointer" id="dropdownComment" uib-dropdown-toggle
ng-class="{'bold': filter.multiselectFilters.comment.length > 0, 'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>Comment</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-left" aria-labelledby="dropdownComment">
<li ng-repeat="(index, commentsField) in noSpecialCommentsFields">
<a href ng-click="filter.operateMultiselectFilter('comment', index, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.comment.indexOf(index) > -1"></i>
{{ commentsField.name }} <translate>is set</translate>
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="filter.operateMultiselectFilter('comment', -1, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.comment.indexOf(-1) > -1"></i>
<translate>No comments set</translate>
</a>
</li>
</ul>
</span>
<!-- Tag filter -->
<span uib-dropdown ng-if="tags.length > 0">
<span class="pointer" id="dropdownTag" uib-dropdown-toggle
ng-class="{'bold': filter.multiselectFilters.tag.length > 0, 'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>Tag</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownTag">
<li ng-repeat="tag in tags">
<a href ng-click="filter.operateMultiselectFilter('tag', tag.id, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.tag.indexOf(tag.id) > -1"></i>
{{ tag.name }}
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="filter.operateMultiselectFilter('tag', -1, isSelectMode)">
<i class="fa fa-check" ng-if="filter.multiselectFilters.tag.indexOf(-1) > -1"></i>
<translate>No tag set</translate>
</a>
</li>
</ul>
</span>
<!-- boolean Filters (customized!) -->
<span ng-if="operator.user.id" uib-dropdown>
<span class="pointer" id="dropdownMisc" uib-dropdown-toggle
ng-class="{'bold': (filter.booleanFilters.isFavorite.value !== undefined) ||
(filter.booleanFilters.hasPersonalNote.value !== undefined), 'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>Misc</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMisc">
<li>
<a href ng-click="filter.booleanFilters.isFavorite.value = (filter.booleanFilters.isFavorite.value ? undefined : true); filter.save();">
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.isFavorite.value === true}"></i>
{{ filter.booleanFilters.isFavorite.choiceYes | translate }}
</a>
</li>
<li>
<a href ng-click="filter.booleanFilters.isFavorite.value = (filter.booleanFilters.isFavorite.value === false) ? undefined : false; filter.save();">
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.isFavorite.value === false}"></i>
{{ filter.booleanFilters.isFavorite.choiceNo | translate }}
</a>
</li>
<li class="divider"></li>
<li>
<a href ng-click="filter.booleanFilters.hasPersonalNote.value = (filter.booleanFilters.hasPersonalNote.value ? undefined : true); filter.save();">
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.hasPersonalNote.value === true}"></i>
{{ filter.booleanFilters.hasPersonalNote.choiceYes | translate }}
</a>
</li>
<li>
<a href ng-click="filter.booleanFilters.hasPersonalNote.value = (filter.booleanFilters.hasPersonalNote.value === false) ? undefined : false; filter.save();">
<i class="fa" ng-class="{'fa-check': filter.booleanFilters.hasPersonalNote.value === false}"></i>
{{ filter.booleanFilters.hasPersonalNote.choiceNo | translate }}
</a>
</li>
</ul>
</span>
<!-- dropdown sort -->
<span uib-dropdown>
<span class="pointer" id="dropdownSort" uib-dropdown-toggle
ng-class="{'disabled': isSelectMode}"
ng-disabled="isSelectMode">
<translate>Sort</translate>
<span class="caret"></span>
</span>
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownSort">
<!-- item -->
<li>
<a href ng-click="sort.toggle('agenda_item.getItemNumberWithAncestors()')">
<translate translate-comment="short form of agenda item">Item</translate>
<span class="spacer-right pull-right"></span>
<i class="pull-right fa"
ng-style="{'visibility': sort.column === 'agenda_item.getItemNumberWithAncestors()' ? 'visible' : 'hidden'}"
ng-class="sort.reverse ? 'fa-sort-desc' : 'fa-sort-asc'">
</i>
</a>
</li>
<!-- all other sortOptions -->
<li ng-repeat="option in sortOptions">
<a href ng-click="sort.toggle(option.name)">
{{ option.display_name | translate }}
<span class="spacer-right pull-right"></span>
<i class="pull-right fa"
ng-style="{'visibility': sort.column === option.name ? 'visible' : 'hidden'}"
ng-class="sort.reverse ? 'fa-sort-amount-desc' : 'fa-sort-amount-asc'">
</i>
</a>
</li>
</ul>
</span>
<!-- search field -->
<span class="form-group">
<span class="input-group">
<span class="input-group-addon"><i class="fa fa-search"></i></span>
<input type="text" ng-model="filter.filterString" class="form-control"
placeholder="{{ 'Search' | translate}}" ng-disabled="isSelectMode"
ng-change="filter.save()">
</span>
</span>
</div>
<!-- show all selected multiselectoptions -->
<div>
<!-- clear all filters -->
<span class="spacer-left-lg pointer" ng-click="resetFilters(isSelectMode)"
ng-if="filter.areFiltersSet()" ng-disabled="isSelectMode"
ng-class="{'disabled': isSelectMode}">
<i class="fa fa-window-close"></i>
<strong translate>All Filters</strong>
</span>
<!-- state -->
<span ng-repeat="state in states" class="pointer spacer-left-lg"
ng-if="!state.workflowHeader && filter.multiselectFilters.state.indexOf(state.id) > -1"
ng-click="operateStateFilter(state.id, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
{{ state.name | translate }}
</span>
</span>
<span ng-if="filter.multiselectFilters.state.indexOf(-1) > -1" class="pointer spacer-left-lg"
ng-click="operateStateFilter(-1, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i>
<translate>done</translate>
</span>
<span ng-if="filter.multiselectFilters.state.indexOf(-2) > -1" class="pointer spacer-left-lg"
ng-click="operateStateFilter(-2, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i>
<translate>undone</translate>
</span>
<!-- category -->
<span ng-repeat="category in categories | orderBy: config('motions_export_category_sorting')"
class="pointer spacer-left-lg"
ng-if="filter.multiselectFilters.category.indexOf(category.id) > -1"
ng-click="filter.operateMultiselectFilter('category', category.id, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
{{ category.prefix }} &ndash; {{ category.name }}
</span>
</span>
<span ng-if="filter.multiselectFilters.category.indexOf(-1) > -1" class="pointer spacer-left-lg"
ng-click="filter.operateMultiselectFilter('category', -1, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i>
<translate>No category set</translate>
</span>
<!-- motion block -->
<span ng-repeat="motionBlock in motionBlocks | orderBy: 'title'" class="pointer spacer-left-lg"
ng-if="filter.multiselectFilters.motionBlock.indexOf(motionBlock.id) > -1"
ng-click="filter.operateMultiselectFilter('motionBlock', motionBlock.id, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
{{ motionBlock.title }}
</span>
</span>
<!-- comment -->
<span ng-repeat="(index, commentsField) in noSpecialCommentsFields" class="pointer spacer-left-lg"
ng-if="filter.multiselectFilters.comment.indexOf(index) > -1"
ng-click="filter.operateMultiselectFilter('comment', index, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
{{ commentsField.name }}
</span>
</span>
<span ng-if="filter.multiselectFilters.comment.indexOf(-1) > -1" class="pointer spacer-left-lg"
ng-click="filter.operateMultiselectFilter('comment', -1, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i>
<translate>No comments set</translate>
</span>
<!-- recommendation -->
<span ng-repeat="recommendation in recommendations" class="pointer spacer-left-lg"
ng-if="filter.multiselectFilters.recommendation.indexOf(recommendation.id) > -1"
ng-click="filter.operateMultiselectFilter('recommendation', recommendation.id, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
{{ recommendation.recommendation_label | translate }}
</span>
</span>
<span ng-if="filter.multiselectFilters.motionBlock.indexOf(-1) > -1" class="pointer spacer-left-lg"
ng-click="filter.operateMultiselectFilter('motionBlock', -1, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i>
<translate>No motion block set</translate>
</span>
<!-- tags -->
<span ng-repeat="tag in tags" class="pointer spacer-left-lg"
ng-if="filter.multiselectFilters.tag.indexOf(tag.id) > -1"
ng-click="filter.operateMultiselectFilter('tag', tag.id, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
{{ tag.name }}
</span>
</span>
<span ng-if="filter.multiselectFilters.tag.indexOf(-1) > -1" class="pointer spacer-left-lg"
ng-click="filter.operateMultiselectFilter('tag', -1, isSelectMode)"
ng-class="{'disabled': isSelectMode}">
<i class="fa fa-times-circle"></i>
<translate>No tag set</translate>
</span>
<!-- for all boolean Filters -->
<span ng-repeat="(name, booleanFilter) in filter.booleanFilters"
ng-hide="booleanFilter.value === undefined"
class="pointer spacer-left-lg"
ng-click="booleanFilter.value = undefined; filter.save();"
ng-class="{'disabled': isSelectMode}">
<span class="nobr">
<i class="fa fa-times-circle"></i>
{{ booleanFilter.value ? booleanFilter.choiceYes : booleanFilter.choiceNo | translate }}
</span>
</span>
</div>

View File

@ -73,10 +73,21 @@
<h2> <h2>
<translate>Motion</translate> {{ motion.identifier }} <translate>Motion</translate> {{ motion.identifier }}
<span ng-if="motion.versions.length > 1" >| Version {{ motion.getVersion().version_number }}</span> <span ng-if="motion.versions.length > 1" >| Version {{ motion.getVersion().version_number }}</span>
<span ng-if="motion.isParagraphBasedAmendment()"
ng-init="paragraph = motion.getAmendmentParagraphsLinesDiff()[0]">
<span ng-if="paragraph">
<span ng-if="paragraph.diffLineTo == paragraph.diffLineFrom + 1">
(<translate>Line</translate> {{ paragraph.diffLineFrom }})
</span>
<span ng-if="paragraph.diffLineTo != paragraph.diffLineFrom + 1">
(<translate>Line</translate> {{ paragraph.diffLineFrom }}-{{ paragraph.diffLineTo }})
</span>
</span>
</span>
</h2> </h2>
</div> </div>
<div class="zoomcontent"> <div class="zoomcontent" ng-if="!motion.isParagraphBasedAmendment()">
<!-- Preamble --> <!-- Preamble -->
<div><p>{{ config('motions_preamble') | translate }}</p></div><br> <div><p>{{ config('motions_preamble') | translate }}</p></div><br>
@ -101,17 +112,17 @@
<!-- The actual diff view --> <!-- The actual diff view -->
<div class="motion-text-with-diffs line-numbers-{{ config('motions_default_line_numbering') }}"> <div class="motion-text-with-diffs line-numbers-{{ config('motions_default_line_numbering') }}">
<div ng-repeat="change in (changes = (change_recommendations | orderBy: 'line_from')) "> <div ng-repeat="change in amendments_crs">
<div class="motion-text original-text line-numbers-{{ config('motions_default_line_numbering') }}" <div class="motion-text original-text line-numbers-{{ config('motions_default_line_numbering') }}"
ng-bind-html="motion.getTextBetweenChangeRecommendations(null, changes[$index - 1], change, line) | trusted"> ng-if="$index === 0 || amendments_crs[$index - 1].line_to < change.line_from"
ng-bind-html="motion.getTextBetweenChanges(version, amendments_crs[$index - 1], change, highlight) | trusted">
</div> </div>
<div class="diff-box diff-box-{{ change.id }} clearfix motion-text motion-text-diff line-numbers-{{ config('motions_default_line_numbering') }}"> <div class="diff-box diff-box-{{ change.id }} clearfix motion-text motion-text-diff line-numbers-{{ config('motions_default_line_numbering') }}">
<div ng-bind-html="change.getDiff(motion, null, line) | trusted"></div> <div ng-bind-html="change.getDiff(motion, null, highlight) | trusted"></div>
</div> </div>
</div> </div>
<div class="motion-text original-text line-numbers-{{ config('motions_default_line_numbering') }}" <div class="motion-text original-text line-numbers-{{ config('motions_default_line_numbering') }}"
ng-bind-html="motion.getTextRemainderAfterLastChangeRecommendation(null, changes, line) | trusted"> ng-bind-html="motion.getTextRemainderAfterLastChange(version, amendments_crs, highlight) | trusted">
</div> </div>
</div> </div>
@ -125,6 +136,15 @@
<!-- Agreed View --> <!-- Agreed View -->
<div ng-if="mode == 'agreed'"> <div ng-if="mode == 'agreed'">
<div class="alert alert-danger" ng-if="changed_version_has_accepted_collissions">
<i class="fa fa-warning"></i>
<translate>
At least two amendments or change recommendations affecting the same line are to be integrated.
This leads to undeterministic results.
Please resolve this conflict by not accepting multiple changes affecting the same line.
</translate>
</div>
<div ng-bind-html="motion.getTextByMode('agreed', null, line) | trusted" <div ng-bind-html="motion.getTextByMode('agreed', null, line) | trusted"
class="motion-text motion-text-changed line-numbers-{{ config('motions_default_line_numbering') }}"></div> class="motion-text motion-text-changed line-numbers-{{ config('motions_default_line_numbering') }}"></div>
</div> </div>
@ -135,5 +155,20 @@
<div ng-bind-html="motion.getReason() | trusted"></div> <div ng-bind-html="motion.getReason() | trusted"></div>
</div> </div>
</div> </div>
<!-- Amendments -->
<div class="zoomcontent" ng-if="motion.isParagraphBasedAmendment()">
<div ng-repeat="paragraph in amendment_diff_paragraphs" class="motion-text motion-text-diff line-numbers-{{ lineNumberMode }}"
ng-class="{'amendment-context': showAmendmentContext}">
<div ng-bind-html="paragraph.text | trusted"></div>
</div>
<!-- Reason -->
<div ng-if="motion.getReason() && !config('motions_disable_reason_on_projector')">
<h3 translate>Reason</h3>
<div ng-bind-html="motion.getReason() | trusted"></div>
</div>
</div>
</div> </div>
</div> </div>

View File

@ -131,6 +131,7 @@ class MotionViewSet(ModelViewSet):
# For creating amendments. # For creating amendments.
whitelist.extend([ whitelist.extend([
'parent_id', 'parent_id',
'amendment_paragraphs',
'category_id', # This will be set to the matching 'category_id', # This will be set to the matching
'motion_block_id', # values from parent_motion. 'motion_block_id', # values from parent_motion.
]) ])

View File

@ -546,6 +546,19 @@ describe('linenumbering', function () {
expect(diff).toBe(expected); expect(diff).toBe(expected);
}); });
it('handles inserted paragraphs (4)', function () {
var before = "<p>This is a random first line that remains unchanged.</p>",
after = "<p>This is a random first line that remains unchanged.</p>" +
'<p style="text-align: justify;"><span style="color: #000000;">Inserting this line should not make any troubles, especially not affect the first line</span></p>' +
'<p style="text-align: justify;"><span style="color: #000000;">Neither should this line</span></p>',
expected = "<p>This is a random first line that remains unchanged.</p>" +
'<p style="text-align: justify;"><ins><span style="color: #000000;">Inserting this line should not make any troubles, especially not affect the first line</span></ins></p>' +
'<p style="text-align: justify;"><ins><span style="color: #000000;">Neither should this line</span></ins></p>';
var diff = diffService.diff(before, after);
expect(diff).toBe(expected);
});
it('handles completely deleted paragraphs', function () { it('handles completely deleted paragraphs', function () {
var before = "<P>Ihr könnt ohne Sorge fortgehen.'Da meckerte die Alte und machte sich getrost auf den Weg.</P>", var before = "<P>Ihr könnt ohne Sorge fortgehen.'Da meckerte die Alte und machte sich getrost auf den Weg.</P>",
after = ""; after = "";
@ -630,6 +643,25 @@ describe('linenumbering', function () {
var diff = diffService.diff(before, after); var diff = diffService.diff(before, after);
expect(diff).toBe('elitr<del>. einsetzt. VERSCHLUCKT noch die sog.</del><ins>, Einfügung durch Änderung der</ins> Gleichbleibend<del> (Wird gelöscht).</del><ins>, einsetzt.</ins>'); expect(diff).toBe('elitr<del>. einsetzt. VERSCHLUCKT noch die sog.</del><ins>, Einfügung durch Änderung der</ins> Gleichbleibend<del> (Wird gelöscht).</del><ins>, einsetzt.</ins>');
}); });
it('does not fall back to block level replacement when BRs are inserted/deleted', function() {
var before = '<p>Lorem ipsum dolor sit amet, consetetur <br>sadipscing elitr.<br>Bavaria ipsum dolor sit amet oha wea nia ausgähd<br>kummt nia hoam i hob di narrisch gean</p>',
after = '<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr. Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua..<br>\n' +
'Bavaria ipsum dolor sit amet oha wea nia ausgähd<br>\n' +
'Autonomie erfährt ihre Grenzen</p>';
var diff = diffService.diff(before, after);
expect(diff).toBe('<p>Lorem ipsum dolor sit amet, consetetur <del><br></del>sadipscing elitr.<ins> Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua..</ins><br>Bavaria ipsum dolor sit amet oha wea nia ausgähd<br><del>kummt nia hoam i hob di narrisch gean</del><ins>Autonomie erfährt ihre Grenzen</ins></p>');
});
it('does not a change in a very specific case', function() {
// See diff._fixWrongChangeDetection
var inHtml = '<p>Test 123<br>wir strikt ab. lehnen wir ' + brMarkup(1486) + 'ab.<br>' + noMarkup(1487) + 'Gegenüber</p>',
outHtml = '<p>Test 123<br>\n' +
'wir strikt ab. lehnen wir ab.<br>\n' +
'Gegenüber</p>';
var diff = diffService.diff(inHtml, outHtml);
expect(diff).toBe('<p>Test 123<br>wir strikt ab. lehnen wir ' + brMarkup(1486) + 'ab.<br>' + noMarkup(1487) + 'Gegenüber</p>')
});
}); });
describe('ignoring line numbers', function () { describe('ignoring line numbers', function () {
@ -715,4 +747,53 @@ describe('linenumbering', function () {
expect(cleaned).toBe('<UL class="os-split-before os-split-after"><LI class="os-split-before"><UL class="os-split-before os-split-after"><LI class="os-split-before">...here it goes on</LI><LI>This has been added</LI></UL></LI></UL>'); expect(cleaned).toBe('<UL class="os-split-before os-split-after"><LI class="os-split-before"><UL class="os-split-before os-split-after"><LI class="os-split-before">...here it goes on</LI><LI>This has been added</LI></UL></LI></UL>');
}); });
}); });
describe('detecting changed line number range', function () {
it('detects changed line numbers in the middle', function () {
var before = '<p>' + noMarkup(1) + 'foo &amp; bar' + brMarkup(2) + 'Another line' +
brMarkup(3) + 'This will be changed' + brMarkup(4) + 'This, too' + brMarkup(5) + 'End</p>',
after = '<p>' + noMarkup(1) + 'foo &amp; bar' + brMarkup(2) + 'Another line' +
brMarkup(3) + 'This has been changed' + brMarkup(4) + 'End</p>';
var diff = diffService.diff(before, after);
var affected = diffService.detectAffectedLineRange(diff);
expect(affected).toEqual({"from": 3, "to": 5});
});
it('detects changed line numbers at the beginning', function () {
var before = '<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat</p>',
after = '<p>sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat</p>';
before = lineNumberingService.insertLineNumbers(before, 20);
var diff = diffService.diff(before, after);
var affected = diffService.detectAffectedLineRange(diff);
expect(affected).toEqual({"from": 1, "to": 2});
});
});
describe('stripping ins/del-styles/tags', function () {
it('deletes to be deleted nodes', function () {
var inHtml = '<p>Test <span class="delete">Test 2</span> Another test <del>Test 3</del></p><p class="delete">Test 4</p>';
var stripped = diffService.diffHtmlToFinalText(inHtml);
expect(stripped).toBe('<P>Test Another test </P>');
});
it('produces empty paragraphs, if necessary', function () {
var inHtml = '<p class="delete">Test <span class="delete">Test 2</span> Another test <del>Test 3</del></p><p class="delete">Test 4</p>';
var stripped = diffService.diffHtmlToFinalText(inHtml);
expect(stripped).toBe('');
});
it('Removes INS-tags', function () {
var inHtml = '<p>Test <ins>Test <strong>2</strong></ins> Another test</p>';
var stripped = diffService.diffHtmlToFinalText(inHtml);
expect(stripped).toBe('<P>Test Test <STRONG>2</STRONG> Another test</P>');
});
it('Removes .insert-classes', function () {
var inHtml = '<p class="insert">Test <strong>1</strong></p><p class="insert anotherclass">Test <strong>2</strong></p>';
var stripped = diffService.diffHtmlToFinalText(inHtml);
expect(stripped).toBe('<P>Test <STRONG>1</STRONG></P><P class="anotherclass">Test <STRONG>2</STRONG></P>');
});
});
}); });

View File

@ -22,6 +22,46 @@ describe('linenumbering', function () {
lineNumberingService = _lineNumberingService_; lineNumberingService = _lineNumberingService_;
})); }));
describe('paragraph splitting', function () {
it('breaks simple DIVs', function () {
var htmlIn = '<DIV class="testclass">Test <strong>1</strong></DIV>' + "\n" + '<p>Test <em>2</em> 3</p>';
var out = lineNumberingService.splitToParagraphs(htmlIn);
expect(out.length).toBe(2);
expect(out[0]).toBe('<div class="testclass">Test <strong>1</strong></div>');
expect(out[1]).toBe('<p>Test <em>2</em> 3</p>');
});
it('ignores root-level text-nodes', function () {
var htmlIn = '<DIV class="testclass">Test <strong>3</strong></DIV>' + "\n New line";
var out = lineNumberingService.splitToParagraphs(htmlIn);
expect(out.length).toBe(1);
expect(out[0]).toBe('<div class="testclass">Test <strong>3</strong></div>');
});
it('splits UL-Lists', function () {
var htmlIn = "<UL class='testclass'>\n<li>Node 1</li>\n <li class='second'>Node <strong>2</strong></li><li><p>Node 3</p></li></UL>";
var out = lineNumberingService.splitToParagraphs(htmlIn);
expect(out.length).toBe(3);
expect(out[0]).toBe('<ul class="testclass"><li>Node 1</li></ul>');
expect(out[1]).toBe('<ul class="testclass"><li class="second">Node <strong>2</strong></li></ul>');
expect(out[2]).toBe('<ul class="testclass"><li><p>Node 3</p></li></ul>');
});
it('splits OL-Lists', function () {
var htmlIn = "<OL start='2' class='testclass'>\n<li>Node 1</li>\n <li class='second'>Node <strong>2</strong></li><li><p>Node 3</p></li></OL>";
var out = lineNumberingService.splitToParagraphs(htmlIn);
expect(out.length).toBe(3);
expect(out[0]).toBe('<ol start="2" class="testclass"><li>Node 1</li></ol>');
expect(out[1]).toBe('<ol start="3" class="testclass"><li class="second">Node <strong>2</strong></li></ol>');
expect(out[2]).toBe('<ol start="4" class="testclass"><li><p>Node 3</p></li></ol>');
});
});
describe('getting line number range', function () {
it('extracts the line number range, example 1', function () {
var html = '<p>' + noMarkup(2) + 'et accusam et justo duo dolores et ea <span style="color: #ff0000;"><strike>rebum </strike></span><span style="color: #006400;">Inserted Text</span>. Stet clita kasd ' + brMarkup(3) + 'gubergren,</p>';
var range = lineNumberingService.getLineNumberRange(html);
expect(range).toEqual({"from": 2, "to": 4});
});
});
describe('line numbering: test nodes', function () { describe('line numbering: test nodes', function () {
it('breaks very short lines', function () { it('breaks very short lines', function () {
var textNode = document.createTextNode("0123"); var textNode = document.createTextNode("0123");
@ -138,6 +178,12 @@ describe('linenumbering', function () {
expect(lineNumberingService.insertLineBreaksWithoutNumbers(outHtml, 80)).toBe(outHtml); expect(lineNumberingService.insertLineBreaksWithoutNumbers(outHtml, 80)).toBe(outHtml);
}); });
it('counts after DEL/INS-nodes', function () {
var inHtml = "<P>leo Testelefantgeweih Buchstabenwut als Achzehnzahlunginer. Hierbei <DEL>darf</DEL><INS>setzen</INS> bist der Deifi <DEL>das </DEL><INS>Dor Reh Wachtel da </INS>Subjunktivier <DEL>als Derftige Aal</DEL><INS>san</INS> Orthopädische<DEL>, der Arbeitsnachweisdiskus Bass der Tastatur </DEL><DEL>Weiter schreiben wie Tasse Wasser als</DEL><INS> dienen</INS>.</P>";
var outHtml = lineNumberingService.insertLineNumbers(inHtml, 95);
expect(outHtml).toBe('<p>' + noMarkup(1) + 'leo Testelefantgeweih Buchstabenwut als Achzehnzahlunginer. Hierbei <del>darf</del><ins>setzen</ins> bist der Deifi <del>das ' + brMarkup(2) + '</del><ins>Dor Reh Wachtel da </ins>Subjunktivier <del>als Derftige Aal</del><ins>san</ins> Orthopädische<del>, der Arbeitsnachweisdiskus Bass der Tastatur </del>' + brMarkup(3) + '<del>Weiter schreiben wie Tasse Wasser als</del><ins> dienen</ins>.</p>');
});
it('handles STRIKE-tags', function () { it('handles STRIKE-tags', function () {
var inHtml = '<p>et accusam et justo duo dolores et ea <span style="color: #ff0000;"><strike>rebum </strike></span><span style="color: #006400;">Inserted Text</span>. Stet clita kasd gubergren,</p>'; var inHtml = '<p>et accusam et justo duo dolores et ea <span style="color: #ff0000;"><strike>rebum </strike></span><span style="color: #006400;">Inserted Text</span>. Stet clita kasd gubergren,</p>';
var outHtml = lineNumberingService.insertLineNumbers(inHtml, 80); var outHtml = lineNumberingService.insertLineNumbers(inHtml, 80);
@ -408,6 +454,23 @@ describe('linenumbering', function () {
}); });
}); });
describe('document structure parsing', function () {
it('detects the line numbers of headings', function () {
var inHtml = '<p>Line 1</p>' +
'<h1>Heading 1</h1><p>Line 2</p><h2>Heading 1.1</h2><p>Line 3</p><h2>Heading 1.2</h2><p>Line 4</p>' +
'<h1>Heading 2</h1><h2>Heading 2.1</h2><p>Line 5</p>';
inHtml = lineNumberingService.insertLineNumbers(inHtml, 80);
var structure = lineNumberingService.getHeadingsWithLineNumbers(inHtml);
expect(structure).toEqual([
{lineNumber: 2, level: 1, text: 'Heading 1'},
{lineNumber: 4, level: 2, text: 'Heading 1.1'},
{lineNumber: 6, level: 2, text: 'Heading 1.2'},
{lineNumber: 8, level: 1, text: 'Heading 2'},
{lineNumber: 9, level: 2, text: 'Heading 2.1'}
]);
});
});
describe('caching', function() { describe('caching', function() {
it('caches based on line length', function () { it('caches based on line length', function () {
var inHtml = '<p>' +longstr(100) + '</p>'; var inHtml = '<p>' +longstr(100) + '</p>';