New Feature: Paragraph based amendments
With new amendment list table: - Removed title from table, leadmotion can be selected now - rename the new list, added the export dialog, multiselect actions and supporter badge in the amendment list view - Moved collission detection to own factory, compute collissions in the amendment list view - Delegates can now enter paragraph based amendments - new amendment list as pdf/csv export - improved caching of amendments - Parse styles in headings and removed all double-quotes - Performance improvements: * Removed ng-mouseover/mouseleave actions in amendment-list * disable collission detection in amendment list view. * Improved state/recommendation dropdown in amendment list.
This commit is contained in:
parent
5735cebcf9
commit
d9c08b65b7
@ -10,6 +10,8 @@ Version 2.3 (unreleased)
|
||||
Motions:
|
||||
- New feature to scroll the projector to a specific line [#3748].
|
||||
- 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)
|
||||
|
@ -18,6 +18,10 @@
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.col-space {
|
||||
padding: 5px 7px 5px 7px;
|
||||
}
|
||||
|
||||
// TODO: Isn't this defined in the _helper.scss?
|
||||
.centered {
|
||||
text-align: center;
|
||||
|
@ -229,6 +229,11 @@ strong, b, th {
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
#content .containerOSExpanded {
|
||||
height: 100%;
|
||||
margin: 0 auto 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
/** Content **/
|
||||
#content {
|
||||
|
@ -20,7 +20,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
PDFLayout.createTitle = function(title) {
|
||||
return {
|
||||
text: title,
|
||||
style: "title"
|
||||
style: 'title'
|
||||
};
|
||||
};
|
||||
|
||||
@ -28,7 +28,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
PDFLayout.createSubtitle = function(subtitle) {
|
||||
return {
|
||||
text: subtitle.join('\n'),
|
||||
style: "subtitle"
|
||||
style: 'subtitle'
|
||||
};
|
||||
};
|
||||
|
||||
@ -45,9 +45,9 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
// table row style
|
||||
PDFLayout.flipTableRowStyle = function(currentTableSize) {
|
||||
if (currentTableSize % 2 === 0) {
|
||||
return "tableEven";
|
||||
return 'tableEven';
|
||||
} else {
|
||||
return "tableOdd";
|
||||
return 'tableOdd';
|
||||
}
|
||||
};
|
||||
|
||||
@ -76,7 +76,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
BallotCircleDimensions.size)
|
||||
},
|
||||
{
|
||||
width: "auto",
|
||||
width: 'auto',
|
||||
text: decision
|
||||
}
|
||||
],
|
||||
@ -92,7 +92,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
PDFLayout.imageURLtoBase64 = function(url) {
|
||||
var promise = new Promise(function(resolve, reject) {
|
||||
var img = new Image();
|
||||
img.crossOrigin = "Anonymous";
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.onerror = function () {
|
||||
reject({
|
||||
msg: '<i class="fa fa-exclamation-triangle fa-lg spacer-right"></i>' +
|
||||
@ -101,12 +101,12 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
});
|
||||
};
|
||||
img.onload = function () {
|
||||
var canvas = document.createElement("canvas");
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
var ctx = canvas.getContext("2d");
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
var dataURL = canvas.toDataURL("image/png");
|
||||
var dataURL = canvas.toDataURL('image/png');
|
||||
var imageData = {
|
||||
data: dataURL,
|
||||
width: img.width,
|
||||
@ -154,7 +154,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
});
|
||||
return '<p>' + str + '</p>';
|
||||
} else {
|
||||
return ''; //needed for blank "reasons" field
|
||||
return ''; //needed for blank 'reasons' field
|
||||
}
|
||||
};
|
||||
return HTMLValidizer;
|
||||
@ -460,25 +460,25 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
*/
|
||||
convertHTML = function(html, lineNumberMode) {
|
||||
var elementStyles = {
|
||||
"b": ["font-weight:bold"],
|
||||
"strong": ["font-weight:bold"],
|
||||
"u": ["text-decoration:underline"],
|
||||
"em": ["font-style:italic"],
|
||||
"i": ["font-style:italic"],
|
||||
"h1": ["font-size:14", "font-weight:bold"],
|
||||
"h2": ["font-size:12", "font-weight:bold"],
|
||||
"h3": ["font-size:10", "font-weight:bold"],
|
||||
"h4": ["font-size:10", "font-style:italic"],
|
||||
"h5": ["font-size:10"],
|
||||
"h6": ["font-size:10"],
|
||||
"a": ["color:blue", "text-decoration:underline"],
|
||||
"strike": ["text-decoration:line-through"],
|
||||
"del": ["color:red", "text-decoration:line-through"],
|
||||
"ins": ["color:green", "text-decoration:underline"]
|
||||
'b': ['font-weight:bold'],
|
||||
'strong': ['font-weight:bold'],
|
||||
'u': ['text-decoration:underline'],
|
||||
'em': ['font-style:italic'],
|
||||
'i': ['font-style:italic'],
|
||||
'h1': ['font-size:14', 'font-weight:bold'],
|
||||
'h2': ['font-size:12', 'font-weight:bold'],
|
||||
'h3': ['font-size:10', 'font-weight:bold'],
|
||||
'h4': ['font-size:10', 'font-style:italic'],
|
||||
'h5': ['font-size:10'],
|
||||
'h6': ['font-size:10'],
|
||||
'a': ['color:blue', 'text-decoration:underline'],
|
||||
'strike': ['text-decoration:line-through'],
|
||||
'del': ['color:red', 'text-decoration:line-through'],
|
||||
'ins': ['color:green', 'text-decoration:underline']
|
||||
},
|
||||
classStyles = {
|
||||
"delete": ["color:red", "text-decoration:line-through"],
|
||||
"insert": ["color:green", "text-decoration:underline"]
|
||||
'delete': ['color:red', 'text-decoration:line-through'],
|
||||
'insert': ['color:green', 'text-decoration:underline']
|
||||
},
|
||||
getLineNumber = function (element) {
|
||||
if (element && element.nodeName == 'SPAN' && element.getAttribute('class') &&
|
||||
@ -595,54 +595,54 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
*/
|
||||
ComputeStyle = function(o, styles) {
|
||||
styles.forEach(function(singleStyle) {
|
||||
var styleDefinition = singleStyle.trim().toLowerCase().split(":");
|
||||
var styleDefinition = singleStyle.trim().toLowerCase().split(':');
|
||||
var style = styleDefinition[0];
|
||||
var value = styleDefinition[1];
|
||||
if (styleDefinition.length === 2) {
|
||||
switch (style) {
|
||||
case "padding-left":
|
||||
case 'padding-left':
|
||||
o.margin = [parseInt(value), 0, 0, 0];
|
||||
break;
|
||||
case "font-size":
|
||||
case 'font-size':
|
||||
o.fontSize = parseInt(value);
|
||||
break;
|
||||
case "text-align":
|
||||
case 'text-align':
|
||||
switch (value) {
|
||||
case "right":
|
||||
case "center":
|
||||
case "justify":
|
||||
case 'right':
|
||||
case 'center':
|
||||
case 'justify':
|
||||
o.alignment = value;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "font-weight":
|
||||
case 'font-weight':
|
||||
switch (value) {
|
||||
case "bold":
|
||||
case 'bold':
|
||||
o.bold = true;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "text-decoration":
|
||||
case 'text-decoration':
|
||||
switch (value) {
|
||||
case "underline":
|
||||
o.decoration = "underline";
|
||||
case 'underline':
|
||||
o.decoration = 'underline';
|
||||
break;
|
||||
case "line-through":
|
||||
o.decoration = "lineThrough";
|
||||
case 'line-through':
|
||||
o.decoration = 'lineThrough';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "font-style":
|
||||
case 'font-style':
|
||||
switch (value) {
|
||||
case "italic":
|
||||
case 'italic':
|
||||
o.italics = true;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "color":
|
||||
case 'color':
|
||||
o.color = parseColor(value);
|
||||
break;
|
||||
case "background-color":
|
||||
case 'background-color':
|
||||
o.background = parseColor(value);
|
||||
break;
|
||||
}
|
||||
@ -683,16 +683,16 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
styles = styles ? _.clone(styles) : [];
|
||||
var classes = [];
|
||||
if (element.getAttribute) {
|
||||
var nodeStyle = element.getAttribute("style");
|
||||
var nodeStyle = element.getAttribute('style');
|
||||
if (nodeStyle) {
|
||||
nodeStyle.split(";").forEach(function(nodeStyle) {
|
||||
nodeStyle.split(';').forEach(function(nodeStyle) {
|
||||
var tmp = nodeStyle.replace(/\s/g, '');
|
||||
styles.push(tmp);
|
||||
});
|
||||
}
|
||||
var nodeClass = element.getAttribute("class");
|
||||
var nodeClass = element.getAttribute('class');
|
||||
if (nodeClass) {
|
||||
classes = nodeClass.toLowerCase().split(" ");
|
||||
classes = nodeClass.toLowerCase().split(' ');
|
||||
classes.forEach(function(nodeClass) {
|
||||
if (typeof(classStyles[nodeClass]) != 'undefined') {
|
||||
classStyles[nodeClass].forEach(function(style) {
|
||||
@ -710,37 +710,40 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
}
|
||||
var nodeName = element.nodeName.toLowerCase();
|
||||
switch (nodeName) {
|
||||
case "h1":
|
||||
case "h2":
|
||||
case "h3":
|
||||
case "h4":
|
||||
case "h5":
|
||||
case "h6":
|
||||
if (lineNumberMode === "outside" &&
|
||||
case 'h1':
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
case 'h4':
|
||||
case 'h5':
|
||||
case 'h6':
|
||||
if (lineNumberMode === 'outside' &&
|
||||
element.childNodes.length > 0 &&
|
||||
element.childNodes[0].getAttribute) {
|
||||
// A heading may have multiple lines, so handle line by line separated by line number elements
|
||||
var outerStack = create("stack");
|
||||
var currentCol;
|
||||
var outerStack = create('stack');
|
||||
var currentCol, currentText;
|
||||
_.forEach(element.childNodes, function (node) {
|
||||
if (node.getAttribute && node.getAttribute('data-line-number')) {
|
||||
if (currentCol) {
|
||||
ComputeStyle(currentCol, elementStyles[nodeName]);
|
||||
outerStack.stack.push(currentCol);
|
||||
}
|
||||
currentText = create('text');
|
||||
currentCol = {
|
||||
columns: [
|
||||
getLineNumberObject({
|
||||
lineNumber: node.getAttribute('data-line-number')
|
||||
}),
|
||||
currentText,
|
||||
],
|
||||
margin: [0, 2, 0, 0],
|
||||
};
|
||||
} else if (node.textContent) {
|
||||
var HeaderText = {
|
||||
text: node.textContent,
|
||||
};
|
||||
currentCol.columns.push(HeaderText);
|
||||
} else {
|
||||
var parsedText = ParseElement([], node, create('text'), styles, diff_mode);
|
||||
// append the parsed text to the currentText
|
||||
_.forEach(parsedText.text, function (text) {
|
||||
currentText.text.push(text);
|
||||
});
|
||||
}
|
||||
});
|
||||
ComputeStyle(currentCol, elementStyles[nodeName]);
|
||||
@ -751,30 +754,30 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
}
|
||||
alreadyConverted.push(outerStack);
|
||||
} else {
|
||||
currentParagraph = create("text");
|
||||
currentParagraph = create('text');
|
||||
currentParagraph.marginBottom = 4;
|
||||
currentParagraph.marginTop = 10;
|
||||
currentParagraph = parseChildren(alreadyConverted, element, currentParagraph, styles.concat(elementStyles[nodeName]), diff_mode);
|
||||
alreadyConverted.push(currentParagraph);
|
||||
}
|
||||
break;
|
||||
case "a":
|
||||
case "b":
|
||||
case "strong":
|
||||
case "u":
|
||||
case "em":
|
||||
case "i":
|
||||
case "ins":
|
||||
case "del":
|
||||
case "strike":
|
||||
case 'a':
|
||||
case 'b':
|
||||
case 'strong':
|
||||
case 'u':
|
||||
case 'em':
|
||||
case 'i':
|
||||
case 'ins':
|
||||
case 'del':
|
||||
case 'strike':
|
||||
currentParagraph = parseChildren(alreadyConverted, element, currentParagraph, styles.concat(elementStyles[nodeName]), diff_mode);
|
||||
break;
|
||||
case "table":
|
||||
var t = create("table", {
|
||||
case 'table':
|
||||
var t = create('table', {
|
||||
widths: [],
|
||||
body: []
|
||||
});
|
||||
var border = element.getAttribute("border");
|
||||
var border = element.getAttribute('border');
|
||||
var isBorder = false;
|
||||
if (border) {
|
||||
isBorder = (parseInt(border) === 1);
|
||||
@ -782,58 +785,58 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
t.layout = 'noBorders';
|
||||
}
|
||||
currentParagraph = parseChildren(t.table.body, element, currentParagraph, styles, diff_mode);
|
||||
var widths = element.getAttribute("widths");
|
||||
var widths = element.getAttribute('widths');
|
||||
if (!widths) {
|
||||
if (t.table.body.length !== 0) {
|
||||
if (t.table.body[0].length !== 0)
|
||||
for (var k = 0; k < t.table.body[0].length; k++)
|
||||
t.table.widths.push("*");
|
||||
t.table.widths.push('*');
|
||||
}
|
||||
} else {
|
||||
var w = widths.split(",");
|
||||
var w = widths.split(',');
|
||||
for (var ko = 0; ko < w.length; ko++) t.table.widths.push(w[ko]);
|
||||
}
|
||||
alreadyConverted.push(t);
|
||||
break;
|
||||
case "tbody":
|
||||
case 'tbody':
|
||||
currentParagraph = parseChildren(alreadyConverted, element, currentParagraph, styles, diff_mode);
|
||||
break;
|
||||
case "tr":
|
||||
case 'tr':
|
||||
var row = [];
|
||||
currentParagraph = parseChildren(row, element, currentParagraph, styles, diff_mode);
|
||||
alreadyConverted.push(row);
|
||||
break;
|
||||
case "td":
|
||||
currentParagraph = create("text");
|
||||
var st = create("stack");
|
||||
case 'td':
|
||||
currentParagraph = create('text');
|
||||
var st = create('stack');
|
||||
st.stack.push(currentParagraph);
|
||||
var rspan = element.getAttribute("rowspan");
|
||||
var rspan = element.getAttribute('rowspan');
|
||||
if (rspan)
|
||||
st.rowSpan = parseInt(rspan);
|
||||
var cspan = element.getAttribute("colspan");
|
||||
var cspan = element.getAttribute('colspan');
|
||||
if (cspan)
|
||||
st.colSpan = parseInt(cspan);
|
||||
currentParagraph = parseChildren(st.stack, element, currentParagraph, styles, diff_mode);
|
||||
alreadyConverted.push(st);
|
||||
break;
|
||||
case "span":
|
||||
if (element.getAttribute("data-line-number")) {
|
||||
if (lineNumberMode === "inline") {
|
||||
case 'span':
|
||||
if (element.getAttribute('data-line-number')) {
|
||||
if (lineNumberMode === 'inline') {
|
||||
if (diff_mode !== DIFF_MODE_INSERT) {
|
||||
var lineNumberInline = element.getAttribute("data-line-number"),
|
||||
var lineNumberInline = element.getAttribute('data-line-number'),
|
||||
lineNumberObjInline = {
|
||||
text: lineNumberInline,
|
||||
color: "gray",
|
||||
color: 'gray',
|
||||
fontSize: 5
|
||||
};
|
||||
currentParagraph.text.push(lineNumberObjInline);
|
||||
}
|
||||
} else if (lineNumberMode === "outside") {
|
||||
} else if (lineNumberMode === 'outside') {
|
||||
var lineNumberOutline;
|
||||
if (diff_mode === DIFF_MODE_INSERT) {
|
||||
lineNumberOutline = "";
|
||||
lineNumberOutline = '';
|
||||
} else {
|
||||
lineNumberOutline = element.getAttribute("data-line-number");
|
||||
lineNumberOutline = element.getAttribute('data-line-number');
|
||||
}
|
||||
var col = {
|
||||
columns: [
|
||||
@ -842,7 +845,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
}),
|
||||
]
|
||||
};
|
||||
currentParagraph = create("text");
|
||||
currentParagraph = create('text');
|
||||
currentParagraph.lineHeight = 1.25;
|
||||
col.columns.push(currentParagraph);
|
||||
alreadyConverted.push(col);
|
||||
@ -852,7 +855,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
currentParagraph = parseChildren(alreadyConverted, element, currentParagraph, styles, diff_mode);
|
||||
}
|
||||
break;
|
||||
case "br":
|
||||
case 'br':
|
||||
var brParent = element.parentNode;
|
||||
var brParentNodeName = brParent.nodeName;
|
||||
//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')) {
|
||||
break;
|
||||
} else {
|
||||
currentParagraph = create("text");
|
||||
if (lineNumberMode === "outside" &&
|
||||
brParentNodeName !== "LI" &&
|
||||
element.parentNode.parentNode.nodeName !== "LI") {
|
||||
currentParagraph = create('text');
|
||||
if (lineNumberMode === 'outside' &&
|
||||
brParentNodeName !== 'LI' &&
|
||||
element.parentNode.parentNode.nodeName !== 'LI') {
|
||||
if (brParentNodeName === 'INS' || brParentNodeName === 'DEL') {
|
||||
|
||||
var hasPrevSiblingALineNumber = function (element) {
|
||||
@ -896,11 +899,11 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
alreadyConverted.push(currentParagraph);
|
||||
}
|
||||
break;
|
||||
case "li":
|
||||
case "div":
|
||||
currentParagraph = create("text");
|
||||
case 'li':
|
||||
case 'div':
|
||||
currentParagraph = create('text');
|
||||
currentParagraph.lineHeight = 1.25;
|
||||
var stackDiv = create("stack");
|
||||
var stackDiv = create('stack');
|
||||
if (_.indexOf(classes, 'os-split-before') > -1) {
|
||||
stackDiv.listType = 'none';
|
||||
}
|
||||
@ -912,9 +915,9 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
currentParagraph = parseChildren(stackDiv.stack, element, currentParagraph, [], diff_mode);
|
||||
alreadyConverted.push(stackDiv);
|
||||
break;
|
||||
case "p":
|
||||
case 'p':
|
||||
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
|
||||
// and margins. Just leave the paragraph there..
|
||||
if (!isInsideAList(element)) {
|
||||
@ -927,19 +930,19 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
}
|
||||
}
|
||||
currentParagraph.lineHeight = 1.25;
|
||||
var stackP = create("stack");
|
||||
var stackP = create('stack');
|
||||
stackP.stack.push(currentParagraph);
|
||||
ComputeStyle(stackP, styles);
|
||||
currentParagraph = parseChildren(stackP.stack, element, currentParagraph, [], diff_mode);
|
||||
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
|
||||
// add empty line number column for inline diff or pragraph diff mode
|
||||
if (element.childNodes[0].tagName === "INS" ||
|
||||
element.childNodes[0].tagName === "DEL") {
|
||||
if (element.childNodes[0].tagName === 'INS' ||
|
||||
element.childNodes[0].tagName === 'DEL') {
|
||||
var pLineNumberPlaceholder = {
|
||||
width: 20,
|
||||
text: "",
|
||||
text: '',
|
||||
fontSize: 8,
|
||||
margin: [0, 2, 0, 0]
|
||||
};
|
||||
@ -955,7 +958,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
}
|
||||
alreadyConverted.push(pObjectToPush);
|
||||
break;
|
||||
case "img":
|
||||
case 'img':
|
||||
var path = element.getAttribute('src');
|
||||
var height = images[path].height;
|
||||
var width = images[path].width;
|
||||
@ -989,8 +992,8 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
height: height,
|
||||
});
|
||||
break;
|
||||
case "ul":
|
||||
case "ol":
|
||||
case 'ul':
|
||||
case 'ol':
|
||||
var list = create(nodeName);
|
||||
if (nodeName == 'ol') {
|
||||
var start = element.getAttribute('start');
|
||||
@ -999,7 +1002,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
}
|
||||
}
|
||||
ComputeStyle(list, styles);
|
||||
if (lineNumberMode === "outside") {
|
||||
if (lineNumberMode === 'outside') {
|
||||
var lines = extractLineNumbers(element);
|
||||
currentParagraph = parseChildren(list[nodeName], element, currentParagraph, styles, diff_mode);
|
||||
if (lines.length > 0) {
|
||||
@ -1028,7 +1031,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
}
|
||||
break;
|
||||
default:
|
||||
var defaultText = create("text", element.textContent.replace(/\n/g, ""));
|
||||
var defaultText = create('text', element.textContent.replace(/\n/g, ''));
|
||||
ComputeStyle(defaultText, styles);
|
||||
if (!currentParagraph) {
|
||||
currentParagraph = {};
|
||||
@ -1047,8 +1050,8 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
*/
|
||||
ParseHtml = function(converted, htmlText) {
|
||||
var html = HTMLValidizer.validize(htmlText);
|
||||
html = $(html.replace(/\t/g, "").replace(/\n/g, ""));
|
||||
var emptyParagraph = create("text");
|
||||
html = $(html.replace(/\t/g, '').replace(/\n/g, ''));
|
||||
var emptyParagraph = create('text');
|
||||
slice(html).forEach(function(element) {
|
||||
ParseElement(converted, element, null, [], DIFF_MODE_NORMAL);
|
||||
});
|
||||
@ -1067,7 +1070,7 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
},
|
||||
{
|
||||
text: line.lineNumber,
|
||||
color: "gray",
|
||||
color: 'gray',
|
||||
fontSize: standardFontsize - 2,
|
||||
decoration: '',
|
||||
},
|
||||
@ -1082,8 +1085,8 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
},
|
||||
/**
|
||||
* Creates containerelements for pdfMake
|
||||
* e.g create("text":"MyText") result in { text: "MyText" }
|
||||
* or complex objects create("stack", [{text:"MyText"}, {text:"MyText2"}])
|
||||
* e.g create('text':'MyText') result in { text: 'MyText' }
|
||||
* or complex objects create('stack', [{text:'MyText'}, {text:'MyText2'}])
|
||||
*for units / paragraphs of text
|
||||
*
|
||||
* @function
|
||||
@ -1160,8 +1163,8 @@ angular.module('OpenSlidesApp.core.pdf', [])
|
||||
|
||||
/*
|
||||
* 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",
|
||||
* the map fould be "fonts/myFont.ttf": ["OSFont-regular.ttf", "OSFont-bold.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']
|
||||
*/
|
||||
var getUrlMapping = function () {
|
||||
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([
|
||||
'mainMenuProvider',
|
||||
'gettext',
|
||||
|
@ -178,9 +178,10 @@
|
||||
|
||||
<!-- Content -->
|
||||
<div id="content" ng-controller="ProjectorSidebarCtrl">
|
||||
<div class="containerOS">
|
||||
<div ng-class="expandContent ? 'containerOSExpanded' : 'containerOS'">
|
||||
|
||||
<!-- col2 sidebar-xs (for small devices)-->
|
||||
<div ng-if="!expandContent">
|
||||
<div id="sidebar-xs" class="col2" os-perms="core.can_see_projector" ng-class="{
|
||||
'sidebar-max': isProjectorSidebar && operator.hasPerms('core.can_see_projector'),
|
||||
'sidebar-min': !isProjectorSidebar && operator.hasPerms('core.can_see_projector'),
|
||||
@ -211,12 +212,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- col1 -->
|
||||
<div id="main-column" class="col1" ng-class="{
|
||||
'sidebar-max': isProjectorSidebar && operator.hasPerms('core.can_see_projector'),
|
||||
'sidebar-min': !isProjectorSidebar && operator.hasPerms('core.can_see_projector'),
|
||||
'sidebar-none': !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') && !expandContent,
|
||||
'sidebar-none': !operator.hasPerms('core.can_see_projector') || expandContent}">
|
||||
<!-- dynamic views -->
|
||||
<div ui-view ng-if="openslidesBootstrapDone && baseViewPermissionsGranted"></div>
|
||||
<!-- footer -->
|
||||
@ -228,6 +230,7 @@
|
||||
</div>
|
||||
|
||||
<!-- col2 normal sidebar -->
|
||||
<div ng-if="!expandContent">
|
||||
<div id="sidebar" class="col2" os-perms="core.can_see_projector" ng-class="{
|
||||
'sidebar-max': 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><!--end content-container-->
|
||||
|
@ -166,11 +166,15 @@ def get_config_variables():
|
||||
subgroup='Amendments')
|
||||
|
||||
yield ConfigVariable(
|
||||
name='motions_amendments_apply_text',
|
||||
default_value=False,
|
||||
input_type='boolean',
|
||||
label='Apply text for new amendments',
|
||||
help_text='The title of the motion is always applied.',
|
||||
name='motions_amendments_text_mode',
|
||||
default_value='freestyle',
|
||||
input_type='choice',
|
||||
label='How to create new amendments',
|
||||
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,
|
||||
group='Motions',
|
||||
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.
|
||||
|
||||
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
|
||||
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:
|
||||
self.reset_state()
|
||||
@ -261,8 +264,8 @@ class Motion(RESTModelMixin, models.Model):
|
||||
return
|
||||
elif use_version is None:
|
||||
use_version = self.get_last_version()
|
||||
# Save title, text and reason into the version object.
|
||||
for attr in ['title', 'text', 'reason']:
|
||||
# Save title, text, amendment paragraphs and reason into the version object.
|
||||
for attr in ['title', 'text', 'amendment_paragraphs', 'reason']:
|
||||
_attr = '_%s' % attr
|
||||
data = getattr(self, _attr, None)
|
||||
if data is not None:
|
||||
@ -323,7 +326,7 @@ class Motion(RESTModelMixin, models.Model):
|
||||
return True
|
||||
|
||||
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):
|
||||
return True
|
||||
return False
|
||||
@ -460,7 +463,33 @@ class Motion(RESTModelMixin, models.Model):
|
||||
|
||||
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.
|
||||
"""
|
||||
@ -496,7 +525,7 @@ class Motion(RESTModelMixin, models.Model):
|
||||
Return a version object, not saved in the database.
|
||||
|
||||
The version data of the new version object is populated with the data
|
||||
set via motion.title, motion.text, motion.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
|
||||
attributes, it is populated with the data from the last version
|
||||
object if such object exists.
|
||||
@ -510,7 +539,7 @@ class Motion(RESTModelMixin, models.Model):
|
||||
last_version = self.get_last_version()
|
||||
else:
|
||||
last_version = None
|
||||
for attr in ['title', 'text', 'reason']:
|
||||
for attr in ['title', 'text', 'amendment_paragraphs', 'reason']:
|
||||
if attr in kwargs:
|
||||
continue
|
||||
_attr = '_%s' % attr
|
||||
@ -693,6 +722,13 @@ class Motion(RESTModelMixin, models.Model):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Generator that yields all amendments of this motion including all
|
||||
@ -702,6 +738,12 @@ class Motion(RESTModelMixin, models.Model):
|
||||
yield amendment
|
||||
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):
|
||||
"""
|
||||
@ -789,6 +831,15 @@ class MotionVersion(RESTModelMixin, models.Model):
|
||||
text = models.TextField()
|
||||
"""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)
|
||||
"""The reason for a motion."""
|
||||
|
||||
|
@ -29,10 +29,13 @@ class MotionSlide(ProjectorElement):
|
||||
yield motion.agenda_item
|
||||
yield motion.state.workflow
|
||||
yield from self.required_motions_for_state_and_recommendation(motion)
|
||||
yield from motion.get_paragraph_based_amendments()
|
||||
for submitter in motion.submitters.all():
|
||||
yield submitter.user
|
||||
yield from motion.supporters.all()
|
||||
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):
|
||||
"""
|
||||
|
@ -134,6 +134,28 @@ class MotionCommentsJSONSerializerField(Field):
|
||||
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):
|
||||
"""
|
||||
Serializer for motion.models.MotionLog objects.
|
||||
@ -250,6 +272,8 @@ class MotionPollSerializer(ModelSerializer):
|
||||
|
||||
|
||||
class MotionVersionSerializer(ModelSerializer):
|
||||
amendment_paragraphs = AmendmentParagraphsJSONSerializerField(required=False)
|
||||
|
||||
"""
|
||||
Serializer for motion.models.MotionVersion objects.
|
||||
"""
|
||||
@ -261,6 +285,7 @@ class MotionVersionSerializer(ModelSerializer):
|
||||
'creation_time',
|
||||
'title',
|
||||
'text',
|
||||
'amendment_paragraphs',
|
||||
'reason',)
|
||||
|
||||
|
||||
@ -315,8 +340,9 @@ class MotionSerializer(ModelSerializer):
|
||||
polls = MotionPollSerializer(many=True, read_only=True)
|
||||
reason = CharField(allow_blank=True, required=False, write_only=True)
|
||||
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)
|
||||
amendment_paragraphs = AmendmentParagraphsJSONSerializerField(required=False, write_only=True)
|
||||
versions = MotionVersionSerializer(many=True, read_only=True)
|
||||
workflow_id = IntegerField(
|
||||
min_value=1,
|
||||
@ -334,6 +360,7 @@ class MotionSerializer(ModelSerializer):
|
||||
'identifier',
|
||||
'title',
|
||||
'text',
|
||||
'amendment_paragraphs',
|
||||
'reason',
|
||||
'versions',
|
||||
'active_version',
|
||||
@ -360,12 +387,25 @@ class MotionSerializer(ModelSerializer):
|
||||
def validate(self, data):
|
||||
if 'text'in data:
|
||||
data['text'] = validate_html(data['text'])
|
||||
|
||||
if 'reason' in data:
|
||||
data['reason'] = validate_html(data['reason'])
|
||||
|
||||
validated_comments = dict()
|
||||
for id, comment in data.get('comments', {}).items():
|
||||
validated_comments[id] = validate_html(comment)
|
||||
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
|
||||
|
||||
@transaction.atomic
|
||||
@ -379,6 +419,7 @@ class MotionSerializer(ModelSerializer):
|
||||
motion = Motion()
|
||||
motion.title = validated_data['title']
|
||||
motion.text = validated_data['text']
|
||||
motion.amendment_paragraphs = validated_data.get('amendment_paragraphs')
|
||||
motion.reason = validated_data.get('reason', '')
|
||||
motion.identifier = validated_data.get('identifier')
|
||||
motion.category = validated_data.get('category')
|
||||
@ -418,7 +459,7 @@ class MotionSerializer(ModelSerializer):
|
||||
version = motion.get_last_version()
|
||||
|
||||
# Title, text, reason.
|
||||
for key in ('title', 'text', 'reason'):
|
||||
for key in ('title', 'text', 'amendment_paragraphs', 'reason'):
|
||||
if key in validated_data.keys():
|
||||
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;
|
||||
}
|
||||
}
|
||||
.collission-hint {
|
||||
color: red;
|
||||
float: left;
|
||||
margin-left: -19px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.motion-text-diff {
|
||||
@ -51,4 +57,15 @@ ul.os-split-after, ol.os-split-after {
|
||||
&.line-numbers-inline .insert .os-line-number {
|
||||
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;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-bottom: 0px;
|
||||
ol, ul {
|
||||
margin-left: 15px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
|
@ -1,3 +1,4 @@
|
||||
@import "amendments";
|
||||
@import "diff";
|
||||
@import "change-recommendation-overview";
|
||||
@import "inline-editing";
|
||||
@ -72,6 +73,10 @@
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
ng-include {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn.disabled {
|
||||
cursor: default;
|
||||
opacity: 1;
|
||||
@ -224,6 +229,11 @@
|
||||
.btn-edit {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.btn-amend-info {
|
||||
margin-left: 5px;
|
||||
min-width: 68px;
|
||||
}
|
||||
}
|
||||
.status-row {
|
||||
font-style: italic;
|
||||
|
@ -229,6 +229,7 @@ angular.module('OpenSlidesApp.motions', [
|
||||
.factory('Motion', [
|
||||
'DS',
|
||||
'$http',
|
||||
'$cacheFactory',
|
||||
'MotionPoll',
|
||||
'MotionStateAndRecommendationParser',
|
||||
'MotionChangeRecommendation',
|
||||
@ -243,9 +244,13 @@ angular.module('OpenSlidesApp.motions', [
|
||||
'Projector',
|
||||
'ProjectHelper',
|
||||
'operator',
|
||||
function(DS, $http, MotionPoll, MotionStateAndRecommendationParser, MotionChangeRecommendation,
|
||||
'UnifiedChangeObjectCollission',
|
||||
function(DS, $http, $cacheFactory, MotionPoll, MotionStateAndRecommendationParser, MotionChangeRecommendation,
|
||||
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';
|
||||
return DS.defineResource({
|
||||
name: name,
|
||||
@ -277,6 +282,10 @@ angular.module('OpenSlidesApp.motions', [
|
||||
}
|
||||
return this.versions[index] || {};
|
||||
},
|
||||
isParagraphBasedAmendment: function () {
|
||||
var version = this.getVersion();
|
||||
return this.isAmendment && version.amendment_paragraphs;
|
||||
},
|
||||
getTitle: function (versionId) {
|
||||
return this.getVersion(versionId).title;
|
||||
},
|
||||
@ -331,19 +340,31 @@ angular.module('OpenSlidesApp.motions', [
|
||||
|
||||
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),
|
||||
line_to = (change2 ? change2.line_from : null);
|
||||
|
||||
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 this.getTextInLineRange(versionId, line_from, line_to, highlight);
|
||||
},
|
||||
getTextInLineRange: function (versionId, line_from, line_to, highlight) {
|
||||
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;
|
||||
|
||||
try {
|
||||
@ -362,9 +383,11 @@ angular.module('OpenSlidesApp.motions', [
|
||||
data.html + data.innerContextEnd + data.outerContextEnd;
|
||||
html = lineNumberingService.insertLineNumbers(html, lineLength, highlight, null, line_from);
|
||||
|
||||
diffCache.put(cacheKey, html);
|
||||
|
||||
return html;
|
||||
},
|
||||
getTextRemainderAfterLastChangeRecommendation: function(versionId, changes, highlight) {
|
||||
getTextRemainderAfterLastChange: function(versionId, changes, highlight) {
|
||||
var maxLine = 0;
|
||||
for (var i = 0; i < changes.length; i++) {
|
||||
if (changes[i].line_to > maxLine) {
|
||||
@ -398,18 +421,37 @@ angular.module('OpenSlidesApp.motions', [
|
||||
}
|
||||
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,
|
||||
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 change = changes[i];
|
||||
if (typeof statusCompareCb === 'undefined' || statusCompareCb(change.rejected)) {
|
||||
var allChanges = [];
|
||||
change_recommendations.filter(recommendation_filter).forEach(function(change) {
|
||||
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 = diffService.replaceLines(html, change.text, change.line_from, change.line_to);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (lineBreaks) {
|
||||
html = lineNumberingService.insertLineNumbers(html, lineLength, highlight, null, 1);
|
||||
@ -418,13 +460,23 @@ angular.module('OpenSlidesApp.motions', [
|
||||
return html;
|
||||
},
|
||||
getTextWithAllChangeRecommendations: function (versionId, highlight, lineBreaks) {
|
||||
return this._getTextWithChangeRecommendations(versionId, highlight, lineBreaks, function() {
|
||||
return true;
|
||||
return this._getTextWithChanges(versionId, highlight, lineBreaks, function() {
|
||||
return true; // All change recommendations
|
||||
}, function() {
|
||||
return false; // No amendments
|
||||
});
|
||||
},
|
||||
getTextWithoutRejectedChangeRecommendations: function (versionId, highlight, lineBreaks) {
|
||||
return this._getTextWithChangeRecommendations(versionId, highlight, lineBreaks, function(rejected) {
|
||||
return !rejected;
|
||||
getTextWithAgreedChanges: function (versionId, highlight, lineBreaks) {
|
||||
return this._getTextWithChanges(versionId, highlight, lineBreaks, function(recommendation) {
|
||||
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) {
|
||||
@ -447,13 +499,34 @@ angular.module('OpenSlidesApp.motions', [
|
||||
}
|
||||
break;
|
||||
case 'diff':
|
||||
var changes = this.getTextChangeRecommendations(versionId, 'ASC');
|
||||
text = '';
|
||||
for (var i = 0; i < changes.length; i++) {
|
||||
text += this.getTextBetweenChangeRecommendations(versionId, (i === 0 ? null : changes[i - 1]), changes[i], highlight);
|
||||
text += changes[i].getDiff(this, versionId, highlight);
|
||||
var amendments_crs = this.getTextChangeRecommendations(versionId, 'ASC').map(function (cr) {
|
||||
return cr.getUnifiedChangeObject();
|
||||
}).concat(
|
||||
this.getParagraphBasedAmendmentsForDiffView().map(function (amendment) {
|
||||
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) {
|
||||
text = lineNumberingService.stripLineNumbers(text);
|
||||
@ -463,11 +536,313 @@ angular.module('OpenSlidesApp.motions', [
|
||||
text = this.getTextWithAllChangeRecommendations(versionId, highlight, lineBreaks);
|
||||
break;
|
||||
case 'agreed':
|
||||
text = this.getTextWithoutRejectedChangeRecommendations(versionId, highlight, lineBreaks);
|
||||
text = this.getTextWithAgreedChanges(versionId, highlight, lineBreaks);
|
||||
break;
|
||||
}
|
||||
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) {
|
||||
this.text = lineNumberingService.stripLineNumbers(text);
|
||||
},
|
||||
@ -490,6 +865,14 @@ angular.module('OpenSlidesApp.motions', [
|
||||
}
|
||||
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
|
||||
// depended by state and provided by a custom comment field
|
||||
getRecommendationName: function () {
|
||||
@ -506,6 +889,14 @@ angular.module('OpenSlidesApp.motions', [
|
||||
}
|
||||
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
|
||||
getSearchResultName: function () {
|
||||
return this.getTitle();
|
||||
@ -578,9 +969,42 @@ angular.module('OpenSlidesApp.motions', [
|
||||
});
|
||||
return (changes.length > 0 ? changes[0] : null);
|
||||
},
|
||||
getAmendments: function () {
|
||||
return DS.filter('motions/motion', {parent_id: this.id});
|
||||
},
|
||||
hasAmendments: function () {
|
||||
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) {
|
||||
/*
|
||||
* 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) {
|
||||
// Populate content of motion.comments to the single comment
|
||||
var fields = this.getCommentsFields();
|
||||
@ -915,8 +1356,10 @@ angular.module('OpenSlidesApp.motions', [
|
||||
'jsDataModel',
|
||||
'diffService',
|
||||
'lineNumberingService',
|
||||
'UnifiedChangeObjectCollission',
|
||||
'gettextCatalog',
|
||||
function (DS, Config, jsDataModel, diffService, lineNumberingService, gettextCatalog) {
|
||||
function (DS, Config, jsDataModel, diffService, lineNumberingService,
|
||||
UnifiedChangeObjectCollission, gettextCatalog) {
|
||||
return DS.defineResource({
|
||||
name: 'motions/motion-change-recommendation',
|
||||
useClass: jsDataModel,
|
||||
@ -993,12 +1436,89 @@ angular.module('OpenSlidesApp.motions', [
|
||||
}
|
||||
title = title.replace('%FROM%', this.line_from).replace('%TO%', (this.line_to - 1));
|
||||
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([
|
||||
'Motion',
|
||||
'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);
|
||||
};
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
var context = [],
|
||||
currNode = node;
|
||||
@ -169,14 +205,14 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
};
|
||||
|
||||
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
|
||||
return '';
|
||||
}
|
||||
var html = '<' + node.nodeName;
|
||||
for (var i = 0; i < node.attributes.length; i++) {
|
||||
var attr = node.attributes[i];
|
||||
if (attr.name != 'os-li-number') {
|
||||
if (attr.name !== 'os-li-number') {
|
||||
html += ' ' + attr.name + '="' + attr.value + '"';
|
||||
}
|
||||
}
|
||||
@ -226,21 +262,21 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
if (lineNumberingService._isOsLineNumberNode(node) || lineNumberingService._isOsLineBreakNode(node)) {
|
||||
return '';
|
||||
}
|
||||
if (node.nodeName == 'OS-LINEBREAK') {
|
||||
if (node.nodeName === 'OS-LINEBREAK') {
|
||||
return '';
|
||||
}
|
||||
|
||||
var html = this._serializeTag(node);
|
||||
|
||||
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;
|
||||
var remainingTrace = toChildTrace;
|
||||
remainingTrace.shift();
|
||||
if (!lineNumberingService._isOsLineNumberNode(node.childNodes[i])) {
|
||||
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;
|
||||
} else {
|
||||
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)) {
|
||||
return '';
|
||||
}
|
||||
if (node.nodeName == 'OS-LINEBREAK') {
|
||||
if (node.nodeName === 'OS-LINEBREAK') {
|
||||
return '';
|
||||
}
|
||||
|
||||
var html = '';
|
||||
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;
|
||||
var remainingTrace = fromChildTrace;
|
||||
remainingTrace.shift();
|
||||
@ -277,7 +313,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
html += this._serializePartialDomFromChild(node.childNodes[i], remainingTrace, stripLineNumbers);
|
||||
}
|
||||
} else if (found) {
|
||||
if (node.childNodes[i].nodeType == TEXT_NODE) {
|
||||
if (node.childNodes[i].nodeType === TEXT_NODE) {
|
||||
html += node.childNodes[i].nodeValue;
|
||||
} else {
|
||||
if (!stripLineNumbers || (!lineNumberingService._isOsLineNumberNode(node.childNodes[i]) &&
|
||||
@ -291,12 +327,16 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
console.trace();
|
||||
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 + '>';
|
||||
}
|
||||
return html;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
* @return {DocumentFragment}
|
||||
*/
|
||||
this.htmlToFragment = function(html) {
|
||||
var fragment = document.createDocumentFragment(),
|
||||
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.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* 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 = '',
|
||||
fakeOl, offset;
|
||||
|
||||
|
||||
fromChildTraceAbs.shift();
|
||||
var previousHtml = this._serializePartialDomToChild(fragment, fromChildTraceAbs, false);
|
||||
toChildTraceAbs.shift();
|
||||
@ -526,6 +565,16 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
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
|
||||
* first word of the following line.
|
||||
@ -537,13 +586,13 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
this._insertDanglingSpace = function(element) {
|
||||
if (element.childNodes.length > 0) {
|
||||
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,
|
||||
// like a line break between </LI> and </UL>
|
||||
lastChild = element.childNodes[element.childNodes.length - 2];
|
||||
}
|
||||
if (lastChild.nodeType == TEXT_NODE) {
|
||||
if (lastChild.nodeValue === '' || lastChild.nodeValue.substr(-1) != ' ') {
|
||||
if (lastChild.nodeType === TEXT_NODE) {
|
||||
if (lastChild.nodeValue === '' || lastChild.nodeValue.substr(-1) !== ' ') {
|
||||
lastChild.nodeValue += ' ';
|
||||
}
|
||||
} 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>');
|
||||
@ -693,6 +748,111 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
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} htmlNew
|
||||
@ -702,13 +862,13 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
htmlOld = this._normalizeHtmlForDiff(htmlOld);
|
||||
htmlNew = this._normalizeHtmlForDiff(htmlNew);
|
||||
|
||||
if (htmlOld == htmlNew) {
|
||||
if (htmlOld === htmlNew) {
|
||||
return this.TYPE_REPLACEMENT;
|
||||
}
|
||||
|
||||
var i, foundDiff;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -718,11 +878,11 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
type = this.TYPE_REPLACEMENT;
|
||||
|
||||
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;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
@ -867,7 +1027,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
}
|
||||
|
||||
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]};
|
||||
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
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
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
|
||||
// The "!!(found=...)"-construction is only used to make jshint happy :)
|
||||
var findDel = /<del>(.*?)<\/del>/gi,
|
||||
@ -1069,7 +1243,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
}
|
||||
while (!!(found = findIns.exec(html))) {
|
||||
inner = found[1].replace(/<br[^>]*>/gi, '');
|
||||
if (inner.match(/<[^>]*>/)) {
|
||||
if (!this._isValidInlineHtml(inner)) {
|
||||
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.
|
||||
* If lineLength and firstLineNumber is given, line numbers will be returned es well
|
||||
*
|
||||
* @param {number} lineLength
|
||||
* @param {number} firstLineNumber
|
||||
* @param {string} htmlOld
|
||||
* @param {string} htmlNew
|
||||
* @param {number} lineLength - optional
|
||||
* @param {number} firstLineNumber - optional
|
||||
* @returns {string}
|
||||
*/
|
||||
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),
|
||||
diffUnnormalized = str.replace(/^\s+/g, '').replace(/\s+$/g, '').replace(/ {2,}/g, ' ');
|
||||
|
||||
|
||||
diffUnnormalized = this._fixWrongChangeDetection(diffUnnormalized);
|
||||
|
||||
// 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
|
||||
diffUnnormalized = diffUnnormalized.replace(
|
||||
@ -1287,7 +1505,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
remainderOld = oldText, remainderNew = newText;
|
||||
|
||||
while (remainderOld.length > 0 && remainderNew.length > 0 && !foundDiff) {
|
||||
if (remainderOld[0] == remainderNew[0]) {
|
||||
if (remainderOld[0] === remainderNew[0]) {
|
||||
commonStart += remainderOld[0];
|
||||
remainderOld = remainderOld.substr(1);
|
||||
remainderNew = remainderNew.substr(1);
|
||||
@ -1298,7 +1516,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
|
||||
foundDiff = false;
|
||||
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;
|
||||
remainderNew = remainderNew.substr(0, remainderNew.length - 1);
|
||||
remainderOld = remainderOld.substr(0, remainderOld.length - 1);
|
||||
|
@ -75,7 +75,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
|
||||
|
||||
this._isOsLineBreakNode = function (node) {
|
||||
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(' ');
|
||||
if (classes.indexOf('os-line-break') > -1) {
|
||||
isLineBreak = true;
|
||||
@ -86,7 +86,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
|
||||
|
||||
this._isOsLineNumberNode = function (node) {
|
||||
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(' ');
|
||||
if (classes.indexOf('os-line-number') > -1) {
|
||||
isLineNumber = true;
|
||||
@ -189,13 +189,16 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
|
||||
out.push(node);
|
||||
return node;
|
||||
};
|
||||
var addLinebreakToPreviousNode = function (node, offset, highlight) {
|
||||
var addLinebreakToPreviousNode = function (node, offset) {
|
||||
var firstText = node.nodeValue.substr(0, offset + 1),
|
||||
secondText = node.nodeValue.substr(offset + 1);
|
||||
var lineBreak = service._createLineBreak();
|
||||
var firstNode = document.createTextNode(firstText);
|
||||
node.parentNode.insertBefore(firstNode, node);
|
||||
node.parentNode.insertBefore(lineBreak, node);
|
||||
if (service._currentLineNumber !== null) {
|
||||
node.parentNode.insertBefore(service._createLineNumber(), node);
|
||||
}
|
||||
node.nodeValue = secondText;
|
||||
};
|
||||
|
||||
@ -244,7 +247,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
|
||||
} else {
|
||||
// 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;
|
||||
addLinebreakToPreviousNode(lineBreakAt.node, lineBreakAt.offset, highlight);
|
||||
addLinebreakToPreviousNode(lineBreakAt.node, lineBreakAt.offset);
|
||||
|
||||
this._currentInlineOffset = i + remainderOfPrev;
|
||||
this._lastInlineBreakablePoint = null;
|
||||
@ -298,7 +301,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
|
||||
if (!node.firstChild) {
|
||||
return 0;
|
||||
}
|
||||
if (node.firstChild.nodeType == TEXT_NODE) {
|
||||
if (node.firstChild.nodeType === TEXT_NODE) {
|
||||
var parts = node.firstChild.nodeValue.split(' ');
|
||||
return parts[0].length;
|
||||
} else {
|
||||
@ -317,12 +320,12 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
|
||||
}
|
||||
|
||||
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);
|
||||
for (var j = 0; j < ret.length; 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]),
|
||||
overlength = ((this._currentInlineOffset + firstword) > length && this._currentInlineOffset > 0);
|
||||
if (overlength && this._isInlineElement(oldChildren[i])) {
|
||||
@ -399,7 +402,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
|
||||
}
|
||||
|
||||
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/)) {
|
||||
// White space nodes between block elements should be ignored
|
||||
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++) {
|
||||
node.appendChild(ret[j]);
|
||||
}
|
||||
} else if (oldChildren[i].nodeType == ELEMENT_NODE) {
|
||||
} else if (oldChildren[i].nodeType === ELEMENT_NODE) {
|
||||
var firstword = this._lengthOfFirstInlineWord(oldChildren[i]),
|
||||
overlength = ((this._currentInlineOffset + firstword) > length && this._currentInlineOffset > 0);
|
||||
if (overlength && this._isInlineElement(oldChildren[i]) && !this._isIgnoredByLineNumbering(oldChildren[i])) {
|
||||
@ -491,7 +494,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
|
||||
/**
|
||||
*
|
||||
* @param {string} html
|
||||
* @param {number} lineLength
|
||||
* @param {number|string} lineLength
|
||||
* @param {number|null} highlight - optional
|
||||
* @param {number|null} firstLine
|
||||
*/
|
||||
@ -588,6 +591,115 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
|
||||
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
|
||||
*
|
||||
|
@ -391,10 +391,11 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
|
||||
'$interval',
|
||||
'$timeout',
|
||||
function (Motion, MotionChangeRecommendation, Config, lineNumberingService, diffService, $interval, $timeout) {
|
||||
var $scope;
|
||||
var $scope, motion;
|
||||
|
||||
var obj = {
|
||||
mode: 'original'
|
||||
mode: 'original',
|
||||
context: null
|
||||
};
|
||||
|
||||
obj.diffFormatterCb = function (change, oldFragment, newFragment) {
|
||||
@ -430,7 +431,7 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
|
||||
MotionChangeRecommendation.destroy(changeId);
|
||||
};
|
||||
|
||||
obj.rejectAll = function (motion) {
|
||||
obj.rejectAllChangeRecommendations = function (motion) {
|
||||
var changeRecommendations = MotionChangeRecommendation.filter({
|
||||
'where': {'motion_version_id': {'==': motion.active_version}}
|
||||
});
|
||||
@ -508,8 +509,99 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
|
||||
}, 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;
|
||||
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() {
|
||||
obj.repositionOriginalAnnotations();
|
||||
});
|
||||
@ -518,12 +610,12 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
|
||||
}, obj.repositionOriginalAnnotations);
|
||||
|
||||
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';
|
||||
}
|
||||
};
|
||||
$scope.$watch(function () {
|
||||
return $scope.change_recommendations.length;
|
||||
return $scope.amendments_crs.length;
|
||||
}, checkGotoOriginal);
|
||||
$scope.$watch(function () {
|
||||
return $scope.title_change_recommendation;
|
||||
@ -535,7 +627,7 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
|
||||
var $holder = $(".motion-text-original"),
|
||||
newHeight = $holder.height(),
|
||||
classes = $holder.attr("class");
|
||||
if (newHeight != sizeCheckerLastSize || sizeCheckerLastClass != classes) {
|
||||
if (newHeight !== sizeCheckerLastSize || sizeCheckerLastClass !== classes) {
|
||||
sizeCheckerLastSize = newHeight;
|
||||
sizeCheckerLastClass = classes;
|
||||
obj.repositionOriginalAnnotations();
|
||||
|
@ -327,7 +327,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
|
||||
|
||||
// motion title
|
||||
var motionTitle = function() {
|
||||
if (params.include.text) {
|
||||
if (params.include.text && !motion.isParagraphBasedAmendment()) {
|
||||
return [{
|
||||
text: titlePlain,
|
||||
style: 'heading3'
|
||||
@ -337,30 +337,45 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
|
||||
|
||||
// motion preamble
|
||||
var motionPreamble = function () {
|
||||
if (params.include.text) {
|
||||
return {
|
||||
text: Config.translate(Config.get('motions_preamble').value),
|
||||
margin: [0, 10, 0, 0]
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var escapeHtml = function(text) {
|
||||
return text.replace(/&/, "&").replace(/</, "<").replace(/>/, ">");
|
||||
return text.replace(/&/, '&').replace(/</, '<').replace(/>/, '>');
|
||||
};
|
||||
|
||||
// motion text (with line-numbers)
|
||||
var motionText = function() {
|
||||
var content = [];
|
||||
if (params.include.text) {
|
||||
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();
|
||||
if (params.changeRecommendationMode === 'diff' && titleChange) {
|
||||
motionTextContent += '<p><strong>' + gettextCatalog.getString('New title') + ':</strong> ' +
|
||||
escapeHtml(titleChange.text) + '</p>';
|
||||
}
|
||||
motionTextContent += motion.getTextByMode(params.changeRecommendationMode, motionVersion);
|
||||
return converter.convertHTML(motionTextContent, params.lineNumberMode);
|
||||
}
|
||||
content.push(converter.convertHTML(motionTextContent, params.lineNumberMode));
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
// motion reason heading
|
||||
@ -421,10 +436,10 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
|
||||
title,
|
||||
subtitle,
|
||||
metaTable(),
|
||||
motionTitle(),
|
||||
motionPreamble(),
|
||||
motionText(),
|
||||
motionTitle()
|
||||
];
|
||||
content = content.concat(motionText());
|
||||
|
||||
var reason = motionReason();
|
||||
if (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', [
|
||||
'$http',
|
||||
'$q',
|
||||
@ -980,6 +1199,7 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
|
||||
'PollContentProvider',
|
||||
'PdfMakeBallotPaperProvider',
|
||||
'MotionPartialContentProvider',
|
||||
'AmendmentContentProvider',
|
||||
'PdfCreate',
|
||||
'PDFLayout',
|
||||
'PersonalNoteManager',
|
||||
@ -988,8 +1208,8 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
|
||||
'FileSaver',
|
||||
function ($http, $q, operator, Config, gettextCatalog, MotionChangeRecommendation, HTMLValidizer,
|
||||
PdfMakeConverter, MotionContentProvider, MotionCatalogContentProvider, PdfMakeDocumentProvider,
|
||||
PollContentProvider, PdfMakeBallotPaperProvider, MotionPartialContentProvider, PdfCreate,
|
||||
PDFLayout, PersonalNoteManager, MotionComment, Messaging, FileSaver) {
|
||||
PollContentProvider, PdfMakeBallotPaperProvider, MotionPartialContentProvider, AmendmentContentProvider,
|
||||
PdfCreate, PDFLayout, PersonalNoteManager, MotionComment, Messaging, FileSaver) {
|
||||
return {
|
||||
getDocumentProvider: function (motions, params, singleMotion) {
|
||||
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', [
|
||||
'OpenSlidesApp.motions',
|
||||
'OpenSlidesApp.motions.motionservices',
|
||||
'OpenSlidesApp.motions.motionBlockProjector',
|
||||
])
|
||||
|
||||
@ -18,17 +19,20 @@ angular.module('OpenSlidesApp.motions.projector', [
|
||||
|
||||
.controller('SlideMotionCtrl', [
|
||||
'$scope',
|
||||
'Config',
|
||||
'Motion',
|
||||
'MotionChangeRecommendation',
|
||||
'ChangeRecommendationView',
|
||||
'User',
|
||||
'Notify',
|
||||
'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.
|
||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
||||
// class.
|
||||
var id = $scope.element.id;
|
||||
var motionId = $scope.element.id;
|
||||
$scope.mode = $scope.element.mode || 'original';
|
||||
$scope.lineNumberMode = Config.get('motions_default_line_numbering').value;
|
||||
|
||||
var notifyNamePrefix = 'projector_' + ProjectorID() + '_motion_line_';
|
||||
var callbackId = Notify.registerCallback(notifyNamePrefix + 'request', function (params) {
|
||||
@ -55,27 +59,19 @@ angular.module('OpenSlidesApp.motions.projector', [
|
||||
Notify.deregisterCallback(callbackId);
|
||||
});
|
||||
|
||||
Motion.bindOne(id, $scope, 'motion');
|
||||
User.bindAll({}, $scope, 'users');
|
||||
|
||||
$scope.$watch(function () {
|
||||
return MotionChangeRecommendation.lastModified();
|
||||
return Motion.lastModified(motionId);
|
||||
}, function () {
|
||||
$scope.change_recommendations = [];
|
||||
$scope.title_change_recommendation = null;
|
||||
if ($scope.motion) {
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
$scope.motion = Motion.get(motionId);
|
||||
$scope.amendment_diff_paragraphs = $scope.motion.getAmendmentParagraphsLinesDiff();
|
||||
$scope.viewChangeRecommendations.setVersion($scope.motion, $scope.motion.active_version);
|
||||
});
|
||||
|
||||
// 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',
|
||||
},
|
||||
})
|
||||
.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', {
|
||||
url: '/import',
|
||||
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)
|
||||
.factory('MotionForm', [
|
||||
'$filter',
|
||||
@ -385,7 +422,11 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
Config, Mediafile, MotionBlock, Tag, User, Workflow, Agenda, AgendaTree) {
|
||||
return {
|
||||
// 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 {
|
||||
template: 'static/templates/motions/motion-form.html',
|
||||
controller: motion ? 'MotionUpdateCtrl' : 'MotionCreateCtrl',
|
||||
@ -394,11 +435,14 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
closeByDocument: false,
|
||||
resolve: {
|
||||
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
|
||||
getFormFields: function (isCreateForm) {
|
||||
getFormFields: function (isCreateForm, isParagraphBasedAmendment) {
|
||||
var workflows = Workflow.getAll();
|
||||
var images = Mediafile.getAllImages();
|
||||
var formFields = [];
|
||||
@ -432,7 +476,8 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Title'),
|
||||
required: true
|
||||
}
|
||||
},
|
||||
hide: isParagraphBasedAmendment && isCreateForm
|
||||
},
|
||||
{
|
||||
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',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Text'),
|
||||
required: true
|
||||
required: !isParagraphBasedAmendment // Deleting the whole paragraph in an amendment should be possible
|
||||
},
|
||||
data: {
|
||||
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', [
|
||||
'gettextCatalog',
|
||||
function (gettextCatalog) {
|
||||
@ -739,6 +812,9 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
getFormFields: function (singleMotion, motions, formatChangeCallback) {
|
||||
var fields = [];
|
||||
var commentsAvailable = _.keys(noSpecialCommentsFields).length !== 0;
|
||||
var someMotionsHaveAmendments = _.some(motions, function (motion) {
|
||||
return motion.hasAmendments();
|
||||
});
|
||||
var getMetaInformationOptions = function (disabled) {
|
||||
if (!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')) {
|
||||
fields.push.apply(fields, [
|
||||
{
|
||||
@ -952,6 +1041,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
pdfFormat: 'pdf',
|
||||
changeRecommendationMode: Config.get('motions_recommendation_text_mode').value,
|
||||
lineNumberMode: Config.get('motions_default_line_numbering').value,
|
||||
amendments: false,
|
||||
include: {
|
||||
text: true,
|
||||
reason: true,
|
||||
@ -967,7 +1057,24 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
$scope.motions = motions;
|
||||
$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 () {
|
||||
if ($scope.params.amendments) {
|
||||
motions = prepareAmendments(motions);
|
||||
}
|
||||
switch ($scope.params.format) {
|
||||
case 'pdf':
|
||||
if ($scope.params.pdfFormat === 'pdf') {
|
||||
@ -1078,8 +1185,8 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
$scope.$watch(function () {
|
||||
return Motion.lastModified();
|
||||
}, function () {
|
||||
// always order by identifier (after custom ordering)
|
||||
$scope.motions = _.orderBy(Motion.getAll(), ['identifier']);
|
||||
// get all main motions and order by identifier (after custom ordering)
|
||||
$scope.motions = _.orderBy(Motion.filter({parent_id: undefined}), ['identifier']);
|
||||
_.forEach($scope.motions, function (motion) {
|
||||
MotionComment.populateFields(motion);
|
||||
motion.personalNote = PersonalNoteManager.getNote(motion);
|
||||
@ -1172,8 +1279,12 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
$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
|
||||
$scope.filter = osTableFilter.createInstance('MotionTableFilter');
|
||||
$scope.filter = osTableFilter.createInstance(osTablePrefix + 'Filter');
|
||||
|
||||
if (!$scope.filter.existsStorageEntry()) {
|
||||
$scope.filter.multiselectFilters = {
|
||||
@ -1185,11 +1296,6 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
comment: [],
|
||||
};
|
||||
$scope.filter.booleanFilters = {
|
||||
isAmendment: {
|
||||
value: undefined,
|
||||
choiceYes: gettext('Is an amendment'),
|
||||
choiceNo: gettext('Is not an amendment'),
|
||||
},
|
||||
isFavorite: {
|
||||
value: undefined,
|
||||
choiceYes: gettext('Marked as favorite'),
|
||||
@ -1245,7 +1351,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
updateStateFilter();
|
||||
};
|
||||
// Sorting
|
||||
$scope.sort = osTableSort.createInstance('MotionTableSort');
|
||||
$scope.sort = osTableSort.createInstance(osTablePrefix + 'Sort');
|
||||
if (!$scope.sort.column) {
|
||||
$scope.sort.column = 'identifier';
|
||||
}
|
||||
@ -1269,24 +1375,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
];
|
||||
|
||||
// pagination
|
||||
$scope.pagination = osTablePagination.createInstance('MotionTablePagination');
|
||||
|
||||
// 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.pagination = osTablePagination.createInstance(osTablePrefix + 'Pagination');
|
||||
|
||||
$scope.hasTag = function (motion, tag) {
|
||||
return _.indexOf(motion.tags_id, tag.id) > -1;
|
||||
@ -1340,19 +1429,19 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
ngDialog.open(MotionForm.getDialog(motion));
|
||||
};
|
||||
// Export dialog
|
||||
$scope.openExportDialog = function () {
|
||||
ngDialog.open(MotionExportForm.getDialog($scope.motionsFiltered));
|
||||
$scope.openExportDialog = function (motions) {
|
||||
ngDialog.open(MotionExportForm.getDialog(motions));
|
||||
};
|
||||
$scope.pdfExport = function () {
|
||||
MotionPdfExport.export($scope.motionsFiltered);
|
||||
$scope.pdfExport = function (motions) {
|
||||
MotionPdfExport.export(motions);
|
||||
};
|
||||
|
||||
// *** select mode functions ***
|
||||
$scope.isSelectMode = false;
|
||||
// check all checkboxes from filtered motions
|
||||
$scope.checkAll = function () {
|
||||
$scope.checkAll = function (motions) {
|
||||
$scope.selectedAll = !$scope.selectedAll;
|
||||
angular.forEach($scope.motionsFiltered, function (motion) {
|
||||
_.forEach(motions, function (motion) {
|
||||
motion.selected = $scope.selectedAll;
|
||||
});
|
||||
};
|
||||
@ -1360,13 +1449,13 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
$scope.uncheckAll = function () {
|
||||
if (!$scope.isSelectMode) {
|
||||
$scope.selectedAll = false;
|
||||
angular.forEach($scope.motions, function (motion) {
|
||||
_.forEach($scope.motions, function (motion) {
|
||||
motion.selected = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
var selectModeAction = function (predicate) {
|
||||
angular.forEach($scope.motionsFiltered, function (motion) {
|
||||
var selectModeAction = function (motions, predicate) {
|
||||
angular.forEach(motions, function (motion) {
|
||||
if (motion.selected) {
|
||||
predicate(motion);
|
||||
}
|
||||
@ -1375,27 +1464,27 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
$scope.uncheckAll();
|
||||
};
|
||||
// delete selected motions
|
||||
$scope.deleteMultiple = function () {
|
||||
selectModeAction(function (motion) {
|
||||
$scope.deleteMultiple = function (motions) {
|
||||
selectModeAction(motions, function (motion) {
|
||||
$scope.delete(motion);
|
||||
});
|
||||
};
|
||||
// set status for selected motions
|
||||
$scope.setStatusMultiple = function (stateId) {
|
||||
selectModeAction(function (motion) {
|
||||
$scope.updateState(motion, stateId);
|
||||
$scope.setStatusMultiple = function (motions, stateId) {
|
||||
selectModeAction(motions, function (motion) {
|
||||
$http.put('/rest/motions/motion/' + motion.id + '/set_state/', {'state': stateId});
|
||||
});
|
||||
};
|
||||
// set category for selected motions
|
||||
$scope.setCategoryMultiple = function (categoryId) {
|
||||
selectModeAction(function (motion) {
|
||||
$scope.setCategoryMultiple = function (motions, categoryId) {
|
||||
selectModeAction(motions, function (motion) {
|
||||
motion.category_id = categoryId === 'no_category_selected' ? null : categoryId;
|
||||
$scope.save(motion);
|
||||
});
|
||||
};
|
||||
// set status for selected motions
|
||||
$scope.setMotionBlockMultiple = function (motionBlockId) {
|
||||
selectModeAction(function (motion) {
|
||||
$scope.setMotionBlockMultiple = function (motions, motionBlockId) {
|
||||
selectModeAction(motions, function (motion) {
|
||||
motion.motion_block_id = motionBlockId === 'no_motionBlock_selected' ? null : motionBlockId;
|
||||
$scope.save(motion);
|
||||
});
|
||||
@ -1413,6 +1502,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
'ngDialog',
|
||||
'gettextCatalog',
|
||||
'MotionForm',
|
||||
'AmendmentParagraphChooseForm',
|
||||
'ChangeRecommendationCreate',
|
||||
'ChangeRecommendationView',
|
||||
'MotionStateAndRecommendationParser',
|
||||
@ -1438,7 +1528,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
'WebpageTitle',
|
||||
'EditingWarning',
|
||||
function($scope, $http, $timeout, $window, $filter, operator, ngDialog, gettextCatalog,
|
||||
MotionForm, ChangeRecommendationCreate, ChangeRecommendationView,
|
||||
MotionForm, AmendmentParagraphChooseForm, ChangeRecommendationCreate, ChangeRecommendationView,
|
||||
MotionStateAndRecommendationParser, MotionChangeRecommendation, Motion, MotionComment,
|
||||
Category, Mediafile, Tag, User, Workflow, Config, motionId, MotionInlineEditing,
|
||||
MotionCommentsInlineEditing, Editor, Projector, ProjectionDefault, MotionBlock,
|
||||
@ -1451,29 +1541,8 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
Workflow.bindAll({}, $scope, 'workflows');
|
||||
MotionBlock.bindAll({}, $scope, 'motionBlocks');
|
||||
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 () {
|
||||
return Projector.lastModified();
|
||||
}, function () {
|
||||
@ -1487,6 +1556,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
return Motion.lastModified(motionId);
|
||||
}, function () {
|
||||
$scope.motion = Motion.get(motionId);
|
||||
$scope.amendment_diff_paragraphs = $scope.motion.getAmendmentParagraphsLinesDiff();
|
||||
MotionComment.populateFields($scope.motion);
|
||||
if (motion.comments) {
|
||||
$scope.stateExtension = $scope.motion.comments[$scope.commentFieldForStateId];
|
||||
@ -1503,6 +1573,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
WebpageTitle.updateTitle(webpageTitle);
|
||||
|
||||
$scope.createChangeRecommendation.setVersion(motion, motion.active_version);
|
||||
$scope.viewChangeRecommendations.setVersion(motion, motion.active_version);
|
||||
});
|
||||
$scope.$watch(function () {
|
||||
return Motion.lastModified();
|
||||
@ -1554,6 +1625,13 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
$scope.lineNumberMode = mode;
|
||||
};
|
||||
|
||||
$scope.showAmendmentContext = false;
|
||||
$scope.setShowAmendmentContext = function($event) {
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
$scope.showAmendmentContext = !$scope.showAmendmentContext;
|
||||
};
|
||||
|
||||
if (motion.parent_id) {
|
||||
Motion.bindOne(motion.parent_id, $scope, 'parent');
|
||||
}
|
||||
@ -1641,27 +1719,26 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
};
|
||||
// open dialog for new amendment
|
||||
$scope.newAmendment = function () {
|
||||
var dialog = MotionForm.getDialog();
|
||||
if (typeof dialog.scope === 'undefined') {
|
||||
dialog.scope = {};
|
||||
}
|
||||
var openMainDialog = function (paragraphNo) {
|
||||
var dialog = MotionForm.getDialog(null, motion, paragraphNo);
|
||||
dialog.scope = $scope;
|
||||
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
|
||||
$scope.followRecommendation = function () {
|
||||
$http.post('/rest/motions/motion/' + motion.id + '/follow_recommendation/', {
|
||||
'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
|
||||
$scope.toggleCategory = function (category) {
|
||||
if ($scope.motion.category_id == category.id) {
|
||||
@ -1706,14 +1783,6 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
$scope.addMotionToRecommendationField = function (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
|
||||
$scope.create_poll = function () {
|
||||
$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.reasonInlineEditing.setVersion(motion, version.id);
|
||||
$scope.createChangeRecommendation.setVersion(motion, version.id);
|
||||
$scope.viewChangeRecommendations.setVersion(motion, motion.active_version);
|
||||
};
|
||||
// permit specific version
|
||||
$scope.permitVersion = function (version) {
|
||||
@ -1882,9 +1952,9 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
$scope.createChangeRecommendation = ChangeRecommendationCreate;
|
||||
$scope.createChangeRecommendation.init($scope, motion);
|
||||
|
||||
// Change recommendation viewing
|
||||
// Change recommendation and amendment viewing
|
||||
$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
|
||||
$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', [
|
||||
'$scope',
|
||||
'$state',
|
||||
@ -2060,6 +2156,9 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
'operator',
|
||||
'Motion',
|
||||
'MotionForm',
|
||||
'parentMotion',
|
||||
'paragraphNo',
|
||||
'paragraphTextPre',
|
||||
'Category',
|
||||
'Config',
|
||||
'Mediafile',
|
||||
@ -2068,8 +2167,9 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
'Workflow',
|
||||
'Agenda',
|
||||
'ErrorMessage',
|
||||
function($scope, $state, gettext, gettextCatalog, operator, Motion, MotionForm,
|
||||
Category, Config, Mediafile, Tag, User, Workflow, Agenda, ErrorMessage) {
|
||||
function($scope, $state, gettext, gettextCatalog, operator, Motion, MotionForm, parentMotion,
|
||||
paragraphNo, paragraphTextPre, Category, Config, Mediafile, Tag, User, Workflow,
|
||||
Agenda, ErrorMessage) {
|
||||
Category.bindAll({}, $scope, 'categories');
|
||||
Mediafile.bindAll({}, $scope, 'mediafiles');
|
||||
Tag.bindAll({}, $scope, 'tags');
|
||||
@ -2080,30 +2180,54 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
$scope.alert = {};
|
||||
|
||||
// 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
|
||||
// ... for amendments add parent_id
|
||||
if (isAmendment) {
|
||||
if (Config.get('motions_amendments_apply_text').value) {
|
||||
$scope.model.text = $scope.$parent.motion.getText();
|
||||
if (Config.get('motions_amendments_text_mode').value === 'fulltext') {
|
||||
$scope.model.text = parentMotion.getText();
|
||||
}
|
||||
$scope.model.title = $scope.$parent.motion.getTitle();
|
||||
$scope.model.parent_id = $scope.$parent.motion.id;
|
||||
$scope.model.category_id = $scope.$parent.motion.category_id;
|
||||
$scope.model.motion_block_id = $scope.$parent.motion.motion_block_id;
|
||||
if (Config.get('motions_amendments_text_mode').value === 'paragraph' &&
|
||||
paragraphNo !== undefined) {
|
||||
var paragraphs = parentMotion.getTextParagraphs(parentMotion.active_version, false);
|
||||
$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');
|
||||
}
|
||||
// ... preselect default workflow
|
||||
if (operator.hasPerms('motions.can_manage')) {
|
||||
$scope.model.workflow_id = Config.get('motions_workflow').value;
|
||||
}
|
||||
|
||||
// get all form fields
|
||||
$scope.formFields = MotionForm.getFormFields(true);
|
||||
$scope.formFields = MotionForm.getFormFields(true, isParagraphBasedAmendment);
|
||||
|
||||
// save motion
|
||||
$scope.save = function (motion, gotoDetailView) {
|
||||
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.
|
||||
Motion.create(motion).then(
|
||||
function(success) {
|
||||
@ -2149,12 +2273,48 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
// 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);
|
||||
// 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.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
|
||||
$scope.formFields = MotionForm.getFormFields();
|
||||
$scope.formFields = MotionForm.getFormFields(false, motion.isParagraphBasedAmendment());
|
||||
// override default values for update form
|
||||
for (var i = 0; i < $scope.formFields.length; i++) {
|
||||
if ($scope.formFields[i].key == "identifier") {
|
||||
@ -2190,15 +2350,90 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
$scope.$on('$destroy', editingStoppedCallback);
|
||||
|
||||
// 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
|
||||
Motion.inject(motion);
|
||||
// save changed motion object on server
|
||||
Motion.save(motion).then(
|
||||
function(success) {
|
||||
if (gotoDetailView) {
|
||||
$state.go('motions.motion.detail', {id: success.id});
|
||||
}
|
||||
$scope.closeThisDialog();
|
||||
},
|
||||
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', [
|
||||
'$scope',
|
||||
'$q',
|
||||
@ -2581,7 +3012,6 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
.controller('CategoryListCtrl', [
|
||||
'$scope',
|
||||
'Category',
|
||||
@ -2740,6 +3170,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
gettext('Name of recommender');
|
||||
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('Edit comment %%comment%% of motion %%motion%%');
|
||||
|
||||
// subgroup Amendments
|
||||
gettext('Amendments');
|
||||
@ -2747,6 +3178,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
gettext('Prefix for the identifier for amendments');
|
||||
gettext('Apply text for new amendments');
|
||||
gettext('The title of the motion is always applied.');
|
||||
gettext('Amendment to');
|
||||
|
||||
// subgroup 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="title">
|
||||
<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>
|
||||
<translate>Back to overview</translate>
|
||||
</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 -->
|
||||
<a ui-sref="agenda.item.detail({id: motion.agenda_item_id})"
|
||||
os-perms="agenda.can_see" class="btn btn-sm btn-default">
|
||||
@ -23,20 +34,20 @@
|
||||
<i class="fa fa-video-camera"></i>
|
||||
</button>
|
||||
<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)}">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="split-button"
|
||||
ng-if="projectors.length > 1 || change_recommendations.length">
|
||||
<li role="menuitem" ng-repeat="mode in projectionModes" ng-if="change_recommendations.length">
|
||||
ng-if="projectors.length > 1 || has_proposed_changes">
|
||||
<li role="menuitem" ng-repeat="mode in projectionModes" ng-if="has_proposed_changes">
|
||||
<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>
|
||||
<span ng-if="mode.mode!='agreed'">{{ mode.label | translate }}</span>
|
||||
<span ng-if="mode.mode=='agreed'"><translate translate-context="resolution">Final version</translate></span>
|
||||
</a>
|
||||
</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">
|
||||
<a href="" ng-click="motion.project(projector.id, projectionMode.mode)"
|
||||
ng-class="{ 'projected': inArray(motion.isProjected(), projector.id) }">
|
||||
@ -62,13 +73,18 @@
|
||||
</div>
|
||||
|
||||
<h1 class="motion-title">
|
||||
<span class="title-change-indicator"
|
||||
ng-if="viewChangeRecommendations.mode == 'original' && title_change_recommendation"
|
||||
<span ng-if="!motion.isAmendment && viewChangeRecommendations.mode == 'original'">
|
||||
<span class="title-change-indicator" ng-if="title_change_recommendation"
|
||||
ng-click="viewChangeRecommendations.scrollToDiffBox(title_change_recommendation.id)"></span>
|
||||
<span class="change-title"
|
||||
ng-if="motion.isAllowed('update') && viewChangeRecommendations.mode == 'original' && !title_change_recommendation"></span>
|
||||
<span class="change-title" ng-if="motion.isAllowed('update') && !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'"
|
||||
ng-if="operator.user"
|
||||
@ -79,10 +95,6 @@
|
||||
<div class="col-sm-6">
|
||||
<h2>
|
||||
<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.active_version != version" class="label label-warning">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
@ -154,15 +166,13 @@
|
||||
<translate>Unsupport motion</translate>
|
||||
</button>
|
||||
</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>
|
||||
<div ng-repeat="amendment in amendments | orderBy: 'identifier'">
|
||||
<a ui-sref="motions.motion.detail({id: amendment.id})">
|
||||
<translate>Motion</translate> {{ amendment.identifier || amendment.getTitle() }}
|
||||
<a ng-if="motion.hasAmendments()" ui-sref="motions.motion.amendment-list({id: motion.id})">
|
||||
{{ motion.getAmendments().length }} <translate>Amendments</translate><br>
|
||||
</a>
|
||||
</div>
|
||||
<button ng-if="motion.isAllowed('can_create_amendment')" ng-click="newAmendment()" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-plus"></i>
|
||||
<translate>New amendment</translate>
|
||||
@ -180,13 +190,13 @@
|
||||
</span>
|
||||
<ul uib-dropdown-menu class="dropdown-menu" aria-labelledby="state-dropdown">
|
||||
<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 }}
|
||||
<span ng-if="state.show_state_extension_field">...</span>
|
||||
</a>
|
||||
<li class="divider" ng-if="motion.state.getNextStates().length && 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>
|
||||
<translate>Reset state</translate>
|
||||
</a>
|
||||
@ -210,7 +220,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Recommendation -->
|
||||
<div ng-if="config('motions_recommendations_by') != ''">
|
||||
<div ng-if="config('motions_recommendations_by')">
|
||||
<h3 ng-if="!motion.isAllowed('change_recommendation')" class="heading">
|
||||
{{ config('motions_recommendations_by') }}
|
||||
</h3>
|
||||
@ -222,13 +232,13 @@
|
||||
</span>
|
||||
<ul uib-dropdown-menu class="dropdown-menu" aria-labelledby="recommendation-dropdown">
|
||||
<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 }}
|
||||
<span ng-if="recommendation.show_recommendation_extension_field">...</span>
|
||||
</a>
|
||||
<li class="divider" ng-if="motion.state.getRecommendations().length && 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>
|
||||
<translate>Reset recommendation</translate>
|
||||
</a>
|
||||
@ -498,9 +508,10 @@
|
||||
<div class="row">
|
||||
|
||||
<!-- 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'"
|
||||
src="'static/templates/motions/motion-detail/change-summary.html'"></ng-include>
|
||||
@ -532,6 +543,15 @@
|
||||
|
||||
<!-- Agreed View -->
|
||||
<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"
|
||||
class="motion-text motion-text-changed line-numbers-{{ lineNumberMode }}"></div>
|
||||
|
||||
@ -546,6 +566,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paragraph-based Amendments -->
|
||||
<ng-include src="'static/templates/motions/motion-detail/amendment-paragraph-diff.html'"></ng-include>
|
||||
|
||||
</div>
|
||||
<div class="row">
|
||||
<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 -->
|
||||
<section class="change-recommendation-overview">
|
||||
<strong>
|
||||
<translate>Summary of change recommendations</translate>:
|
||||
<translate>Summary of changes</translate>:
|
||||
</strong>
|
||||
|
||||
<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 }}"
|
||||
ng-click="viewChangeRecommendations.rejectAll(motion)">
|
||||
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.rejectAllChangeRecommendations(motion)">
|
||||
<i class="fa fa-thumbs-down"></i>
|
||||
<translate>Reject all change recommendations</translate>
|
||||
</button>
|
||||
|
||||
<ul ng-if="change_recommendations.length > 0 || title_change_recommendation">
|
||||
<ul ng-if="has_proposed_changes">
|
||||
<li ng-if="title_change_recommendation">
|
||||
<a href='' ng-click="viewChangeRecommendations.scrollToDiffBox(title_change_recommendation.id)">
|
||||
<span class="line-number"><translate>Title</translate>:</span>
|
||||
@ -21,30 +21,35 @@
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-repeat="change in (changes = (change_recommendations | filter:{motion_version_id:version}:true | orderBy: 'line_from')) ">
|
||||
<a href='' ng-click="viewChangeRecommendations.scrollToDiffBox(change.id)">
|
||||
<li ng-repeat="change in amendments_crs">
|
||||
<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">
|
||||
<translate>Line</translate> {{ change.line_from }}:
|
||||
<translate>Line</translate> {{ change.line_from }}<span ng-if="change.type === 'recommendation'"></span>
|
||||
</span>
|
||||
<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 class="operation">
|
||||
<translate ng-if="change.getType(motion.getVersion(version).text) == 0">Replacement</translate>
|
||||
<translate ng-if="change.getType(motion.getVersion(version).text) == 1">Insertion</translate>
|
||||
<translate ng-if="change.getType(motion.getVersion(version).text) == 2">Deletion</translate>
|
||||
<span ng-if="change.getType(motion.getVersion(version).text) == 3">
|
||||
<span ng-if="change.type === 'recommendation'">(<translate>Change recommendation</translate>)</span>
|
||||
<span ng-if="change.type === 'amendment'">({{ change.original.identifier }})</span>
|
||||
<span class="operation" ng-if="change.type === 'recommendation'">–
|
||||
<translate ng-if="change.original.getType(motion.getVersion(version).text) == 0">Replacement</translate>
|
||||
<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 }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="status">
|
||||
<translate ng-if="change.rejected">Rejected</translate>
|
||||
<translate ng-if="change.accepted && change.type === 'amendment'">Accepted</translate>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</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>
|
||||
</div>
|
||||
</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 -->
|
||||
<div class="pull-right inline-editing-activator"
|
||||
ng-if="motion.isAllowed('update') && version == motion.getVersion(-1).id && viewChangeRecommendations.mode == 'original'">
|
||||
<button ng-if="!inlineEditing.active && change_recommendations.length == 0" ng-click="enableMotionInlineEditing()"
|
||||
<button ng-if="!inlineEditing.active && !has_proposed_changes" ng-click="enableMotionInlineEditing()"
|
||||
class="btn btn-sm btn-default">
|
||||
<i class="fa fa-pencil-square-o"></i>
|
||||
<translate>Inline editing</translate>
|
||||
</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">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
<translate>Inline editing</translate>
|
||||
</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 }}">
|
||||
<i class="fa fa-pencil-square-o"></i>
|
||||
<translate>Inline editing</translate>
|
||||
@ -21,56 +21,7 @@
|
||||
|
||||
<div class="toolbar-left {{ lineNumberMode }}">
|
||||
|
||||
<!-- 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 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>
|
||||
<ng-include src="'static/templates/motions/motion-detail/toolbar-line-numbering.html'"></ng-include>
|
||||
|
||||
<!-- go to line number -->
|
||||
<div class="popover-wrapper">
|
||||
@ -98,7 +49,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
|
||||
<!-- change recommendations for resonsive size medium/large (button group) -->
|
||||
@ -144,7 +95,7 @@
|
||||
<translate>Change recommendations</translate>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownChangeVersion">
|
||||
<ul class="dropdown-menu dropdown-menu-left" aria-labelledby="dropdownChangeVersion">
|
||||
<li>
|
||||
<a href="" ng-click="viewChangeRecommendations.mode = 'original'">
|
||||
<i class="fa fa-check" ng-if="viewChangeRecommendations.mode == 'original'"></i>
|
||||
|
@ -46,39 +46,55 @@
|
||||
|
||||
<!-- The actual diff view -->
|
||||
<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 }}"
|
||||
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' : ''"
|
||||
class="diff-box-{{ change.id }} clearfix">
|
||||
<div class="action-row" ng-if="motion.isAllowed('can_manage')">
|
||||
<div class="btn-group" data-toggle="buttons">
|
||||
<label class="btn btn-sm btn-default" ng-class="{active: !change.rejected}"
|
||||
title="{{ 'Not rejected' | translate }}" ng-click="change.rejected = false; change.saveStatus();">
|
||||
<div ng-class="{'collides': change.getCollissions().length > 0}"
|
||||
class="diff-box diff-box-{{ change.id }} clearfix">
|
||||
<div class="collission-hint" ng-if="change.getCollissions().length > 0">
|
||||
<i class="fa fa-warning" uib-tooltip="{{ 'This change collides with another one.' | translate }}"></i>
|
||||
</div>
|
||||
<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"
|
||||
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>
|
||||
</label>
|
||||
<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"
|
||||
ng-change="change.saveStatus()" ng-model="change.rejected" ng-checked="change.rejected == true">
|
||||
<i class="fa fa-thumbs-down"></i>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<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-action="viewChangeRecommendations.delete(change.id)"
|
||||
ng-bootbox-confirm-action="viewChangeRecommendations.delete(change.original.id)"
|
||||
title="{{ 'Delete' | translate }}">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-default btn-sm pull-right btn-edit" ng-click="createChangeRecommendation.editTextDialog(change)"
|
||||
<button class="btn btn-default btn-sm pull-right btn-edit"
|
||||
ng-if="change.type == 'recommendation'"
|
||||
ng-click="createChangeRecommendation.editTextDialog(change.original)"
|
||||
title="{{ 'Edit' | translate }}">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</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 class="status-row" ng-if="!motion.isAllowed('can_manage') && change.rejected">
|
||||
<i class="grey"><translate>Rejected</translate>:</i>
|
||||
@ -90,6 +106,6 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
@ -5,6 +5,10 @@
|
||||
<i class="fa fa-plus fa-lg"></i>
|
||||
<translate>New</translate>
|
||||
</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">
|
||||
<i class="fa fa-sitemap fa-lg"></i>
|
||||
<translate>Categories</translate>
|
||||
@ -39,7 +43,7 @@
|
||||
</button>
|
||||
<!-- Export button -->
|
||||
<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>
|
||||
<span ng-if="motionsFiltered.length === motions.length" translate>
|
||||
Export all
|
||||
@ -49,7 +53,7 @@
|
||||
</span>
|
||||
</button>
|
||||
<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>
|
||||
<span ng-if="motionsFiltered.length === motions.length" translate>
|
||||
Export all
|
||||
@ -74,13 +78,13 @@
|
||||
<!-- 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-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 }}
|
||||
</option>
|
||||
</select>
|
||||
<!-- set state button -->
|
||||
<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>
|
||||
</a>
|
||||
<!-- category select -->
|
||||
@ -94,7 +98,7 @@
|
||||
</select>
|
||||
<!-- set category button -->
|
||||
<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>
|
||||
</a>
|
||||
<!-- motionBlock select -->
|
||||
@ -107,13 +111,13 @@
|
||||
</select>
|
||||
<!-- set motion block button -->
|
||||
<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>
|
||||
</a>
|
||||
<!-- delete button -->
|
||||
<a ng-show="selectedAction == 'delete'"
|
||||
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">
|
||||
<i class="fa fa-trash fa-lg"></i>
|
||||
<translate>Delete selected motions</translate>
|
||||
@ -124,7 +128,8 @@
|
||||
<div class="spacer-top-lg italic row">
|
||||
<div class="col-md-6">
|
||||
{{ 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>
|
||||
</div>
|
||||
<div class="col-md-6" ng-show="motionsFiltered.length > pagination.itemsPerPage">
|
||||
@ -147,382 +152,10 @@
|
||||
<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()"></i>
|
||||
ng-click="checkAll(motionsFiltered)"></i>
|
||||
</div>
|
||||
<div class="col-xs-11 main-header" ng-style="{'width': isSelectMode ? '' : '100%'}">
|
||||
<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-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>
|
||||
<ng-include src="'static/templates/motions/motion-table-filters.html'"></ng-include>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -541,7 +174,6 @@
|
||||
| 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">
|
||||
@ -585,11 +217,11 @@
|
||||
<i class="fa fa-cog pointer" uib-dropdown-toggle id="stateDropdown{{ motion.id }}"></i>
|
||||
<ul class="dropdown-menu" aria-labelledby="stateDropdown{{ motion.id }}">
|
||||
<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 class="divider" ng-if="motion.state.getNextStates().length"></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>
|
||||
<translate>Reset state</translate>
|
||||
</a>
|
||||
@ -599,7 +231,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<!-- 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"
|
||||
class="dropdown-hover-space">
|
||||
<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>
|
||||
<ul class="dropdown-menu" aria-labelledby="recommendationDropdown{{ motion.id }}">
|
||||
<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 }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="divider" ng-if="motion.state.getRecommendations().length && motion.recommendation"></li>
|
||||
<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>
|
||||
<translate>Reset recommendation</translate>
|
||||
</a>
|
||||
@ -657,7 +289,7 @@
|
||||
</div>
|
||||
<!-- additional content column -->
|
||||
<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>
|
||||
<!-- Category dropdown for manage user -->
|
||||
<div os-perms="motions.can_manage" ng-show="categories.length > 0"
|
||||
@ -788,7 +420,7 @@
|
||||
</span>
|
||||
</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"
|
||||
uib-tooltip="{{ (motion.agenda_item.speakers | filter: {'begin_time': null}).length }} {{ 'speakers' | translate }}"
|
||||
tooltip-class="nobr">
|
||||
@ -797,6 +429,14 @@
|
||||
{{ (motion.agenda_item.speakers | filter: {'begin_time': null}).length }}
|
||||
</a>
|
||||
</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 style="width: 30%;" class="pull-right">
|
||||
<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>
|
||||
<translate>Motion</translate> {{ motion.identifier }}
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="zoomcontent">
|
||||
<div class="zoomcontent" ng-if="!motion.isParagraphBasedAmendment()">
|
||||
<!-- Preamble -->
|
||||
<div><p>{{ config('motions_preamble') | translate }}</p></div><br>
|
||||
|
||||
@ -101,17 +112,17 @@
|
||||
|
||||
<!-- The actual diff view -->
|
||||
<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') }}"
|
||||
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 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 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>
|
||||
@ -125,6 +136,15 @@
|
||||
|
||||
<!-- Agreed View -->
|
||||
<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"
|
||||
class="motion-text motion-text-changed line-numbers-{{ config('motions_default_line_numbering') }}"></div>
|
||||
</div>
|
||||
@ -135,5 +155,20 @@
|
||||
<div ng-bind-html="motion.getReason() | trusted"></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>
|
||||
|
@ -131,6 +131,7 @@ class MotionViewSet(ModelViewSet):
|
||||
# For creating amendments.
|
||||
whitelist.extend([
|
||||
'parent_id',
|
||||
'amendment_paragraphs',
|
||||
'category_id', # This will be set to the matching
|
||||
'motion_block_id', # values from parent_motion.
|
||||
])
|
||||
|
@ -546,6 +546,19 @@ describe('linenumbering', function () {
|
||||
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 () {
|
||||
var before = "<P>Ihr könnt ohne Sorge fortgehen.'Da meckerte die Alte und machte sich getrost auf den Weg.</P>",
|
||||
after = "";
|
||||
@ -630,6 +643,25 @@ describe('linenumbering', function () {
|
||||
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>');
|
||||
});
|
||||
|
||||
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 () {
|
||||
@ -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>');
|
||||
});
|
||||
});
|
||||
|
||||
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_;
|
||||
}));
|
||||
|
||||
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 () {
|
||||
it('breaks very short lines', function () {
|
||||
var textNode = document.createTextNode("0123");
|
||||
@ -138,6 +178,12 @@ describe('linenumbering', function () {
|
||||
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 () {
|
||||
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);
|
||||
@ -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() {
|
||||
it('caches based on line length', function () {
|
||||
var inHtml = '<p>' +longstr(100) + '</p>';
|
||||
|
Loading…
Reference in New Issue
Block a user