Merge pull request #3637 from CatoTH/Paragraph-Based-Amendments
Paragraph based amendments / Diff
This commit is contained in:
commit
5a5475299c
@ -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)
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -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 = {};
|
||||||
|
@ -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',
|
||||||
|
@ -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-->
|
||||||
|
@ -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')
|
||||||
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -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."""
|
||||||
|
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
@ -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])
|
||||||
|
|
||||||
|
94
openslides/motions/static/css/motions/_amendments.scss
Normal file
94
openslides/motions/static/css/motions/_amendments.scss
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,9 @@
|
|||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ol, ul {
|
||||||
margin-bottom: 0px;
|
margin-left: 15px;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight {
|
.highlight {
|
||||||
|
@ -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;
|
||||||
|
@ -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',
|
||||||
|
@ -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');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
}());
|
}());
|
||||||
|
@ -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
|
|||||||
'Ä': 'Ä',
|
'Ä': 'Ä',
|
||||||
'Ö': 'Ö',
|
'Ö': 'Ö',
|
||||||
'Ü': 'Ü',
|
'Ü': 'Ü',
|
||||||
'ß': 'ß'
|
'ß': 'ß',
|
||||||
|
'„': '„',
|
||||||
|
'“': '“',
|
||||||
|
'•': '•',
|
||||||
|
'§': '§',
|
||||||
|
'é': 'é',
|
||||||
|
'€': '€'
|
||||||
};
|
};
|
||||||
|
|
||||||
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);
|
||||||
|
@ -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
|
||||||
*
|
*
|
||||||
|
@ -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();
|
||||||
|
@ -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(/&/, "&").replace(/</, "<").replace(/>/, ">");
|
return text.replace(/&/, '&').replace(/</, '<').replace(/>/, '>');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -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');
|
||||||
|
@ -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>
|
@ -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 }} —
|
||||||
|
</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 }} —
|
||||||
|
</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 }} – {{ 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')"> ·
|
||||||
|
<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="«"
|
||||||
|
last-text="»">
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div> <!-- container -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -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>
|
@ -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">
|
||||||
|
@ -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>
|
@ -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>
|
||||||
|
@ -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>
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 }} – {{ 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 }} – {{ 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>
|
||||||
|
@ -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 }} – {{ 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 }} – {{ 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>
|
@ -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>
|
||||||
|
@ -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.
|
||||||
])
|
])
|
||||||
|
@ -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 o’ha 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 o’ha 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 o’ha 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 & bar' + brMarkup(2) + 'Another line' +
|
||||||
|
brMarkup(3) + 'This will be changed' + brMarkup(4) + 'This, too' + brMarkup(5) + 'End</p>',
|
||||||
|
after = '<p>' + noMarkup(1) + 'foo & 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>');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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>';
|
||||||
|
Loading…
Reference in New Issue
Block a user