Change recommendations
This commit is contained in:
parent
e944f6368b
commit
228abf5cee
@ -302,22 +302,25 @@ img {
|
||||
border: 1px solid #d3d3d3;
|
||||
}
|
||||
|
||||
.col1 .details .line-number-setter {
|
||||
.col1 .details .motion-toolbar .toolbar-left {
|
||||
margin-top: 0;
|
||||
margin-bottom: 55px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.col1 .details .line-number-setter > span {
|
||||
.col1 .details .motion-toolbar .toolbar-left > * {
|
||||
margin-right: 5px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.col1 .details .line-number-setter .btn.disabled {
|
||||
.col1 .details .motion-toolbar .toolbar-left .btn.disabled {
|
||||
cursor: default;
|
||||
opacity: 1;
|
||||
background-color: #eee;
|
||||
}
|
||||
.col1 .details .motion-toolbar .toolbar-left .goto-line-number {
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.col1 .details .inline-editing-activator {
|
||||
margin-right: 13px;
|
||||
@ -395,23 +398,29 @@ img {
|
||||
background-color: #ff0;
|
||||
}
|
||||
|
||||
.motion-text li {
|
||||
margin-left: 30px;
|
||||
}
|
||||
.motion-text.line-numbers-outside {
|
||||
padding-left: 35px;
|
||||
padding-left: 40px;
|
||||
position: relative;
|
||||
}
|
||||
.motion-text.line-numbers-outside .os-line-number {
|
||||
display: inline-block;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
position: absolute;
|
||||
left: -20px;
|
||||
padding-right: 55px;
|
||||
}
|
||||
.motion-text.line-numbers-outside .os-line-number:after {
|
||||
content: attr(data-line-number);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
left: 20px;
|
||||
top: 12px;
|
||||
vertical-align: top;
|
||||
margin-top: -5px;
|
||||
color: gray;
|
||||
font-family: Courier, serif;
|
||||
font-size: 13px;
|
||||
@ -445,6 +454,7 @@ img {
|
||||
}
|
||||
|
||||
.os-line-number {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
@ -452,6 +462,7 @@ img {
|
||||
-o-user-select: none;
|
||||
}
|
||||
.os-line-number:after {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
@ -459,6 +470,174 @@ img {
|
||||
-o-user-select: none;
|
||||
}
|
||||
|
||||
.line-numbers-outside .os-line-number.selectable:hover:before, .line-numbers-outside .os-line-number.selected:before {
|
||||
cursor: pointer;
|
||||
content: "\f067";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 0.25em;
|
||||
top: 4px;
|
||||
left: 43px;
|
||||
cursor: pointer;
|
||||
font-family: FontAwesome;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
background-color: #337ab7;
|
||||
}
|
||||
|
||||
|
||||
/** Styles for annotating the original motion text with change recommendations */
|
||||
|
||||
.motion-text-holder {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.motion-text-holder .change-recommendation-list {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -10px;
|
||||
width: 4px;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.motion-text-holder .change-recommendation-list > li {
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.motion-text-holder .change-recommendation-list > li.insert {
|
||||
background-color: #00aa00;
|
||||
}
|
||||
.motion-text-holder .change-recommendation-list > li.delete {
|
||||
background-color: #aa0000;
|
||||
}
|
||||
.motion-text-holder .change-recommendation-list > li.replace {
|
||||
background-color: #0333ff;
|
||||
}
|
||||
|
||||
.motion-text-holder .change-recommendation-list .tooltip {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/** Diff view */
|
||||
|
||||
.change-recommendation-overview {
|
||||
background-color: #eee;
|
||||
border: solid 1px #ddd;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 5px;
|
||||
margin-top: -15px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
.change-recommendation-overview {
|
||||
margin-bottom: 50px;
|
||||
padding: 10px;
|
||||
}
|
||||
.change-recommendation-overview h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.change-recommendation-overview ul {
|
||||
list-style: none;
|
||||
display: table;
|
||||
}
|
||||
.change-recommendation-overview li {
|
||||
display: table-row;
|
||||
cursor: pointer;
|
||||
}
|
||||
.change-recommendation-overview li:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.change-recommendation-overview li > * {
|
||||
display: table-cell;
|
||||
padding: 4px;
|
||||
}
|
||||
.change-recommendation-overview .status {
|
||||
color: gray;
|
||||
font-style: italic;
|
||||
}
|
||||
.change-recommendation-overview .status > *:before {
|
||||
content: '(';
|
||||
}
|
||||
.change-recommendation-overview .status > *:after {
|
||||
content: ')';
|
||||
}
|
||||
.change-recommendation-overview .no-changes {
|
||||
font-style: italic;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
|
||||
.diff-box {
|
||||
background-color: #f9f9f9;
|
||||
border: solid 1px #eee;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 0;
|
||||
margin-top: -25px;
|
||||
padding-top: 0;
|
||||
padding-right: 155px;
|
||||
}
|
||||
.motion-text-with-diffs .original-text {
|
||||
min-height: 30px; // Spacer between .diff-box, in case .original-text is empty
|
||||
}
|
||||
.motion-text-with-diffs .original-text ul:last-child {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.motion-text-with-diffs.line-numbers-inline .diff-box, .motion-text-with-diffs.line-numbers-none .diff-box {
|
||||
margin-right: -220px;
|
||||
}
|
||||
.diff-box:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.diff-box .action-row {
|
||||
font-size: 0.8em;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
float: right;
|
||||
width: 150px;
|
||||
text-align: right;
|
||||
margin-right: -150px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.diff-box:hover .action-row {
|
||||
opacity: 1;
|
||||
}
|
||||
.diff-box .action-row .btn-delete {
|
||||
margin-left: 10px;
|
||||
color: red;
|
||||
}
|
||||
.diff-box .status-row {
|
||||
font-style: italic;
|
||||
color: gray;
|
||||
}
|
||||
.diff-box .status-row > *:after {
|
||||
content: ':';
|
||||
}
|
||||
|
||||
.motion-text-diff .delete {
|
||||
color: red;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.motion-text-diff .insert {
|
||||
color: green;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.motion-text-diff p {
|
||||
padding-bottom: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.motion-text-diff.line-numbers-outside .insert .os-line-number {
|
||||
display: none;
|
||||
}
|
||||
.motion-text-diff.line-numbers-inline .insert .os-line-number {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/** Projector sidebar column **/
|
||||
|
||||
#content .col2 {
|
||||
@ -704,13 +883,16 @@ img {
|
||||
border-top: 1px solid #ddd;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.linenumber-toolbar, .speakers-toolbar {
|
||||
.motion-toolbar, .speakers-toolbar {
|
||||
background-color: #f5f5f5;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 12px 0 10px 0;
|
||||
height: 54px;
|
||||
margin: -20px -5px 50px -5px;
|
||||
}
|
||||
.motion-toolbar:first-child {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.speakers-toolbar {
|
||||
margin: -20px -20px 30px -20px;
|
||||
|
@ -55,6 +55,25 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
||||
return data
|
||||
|
||||
|
||||
class MotionChangeRecommendationAccessPermissions(BaseAccessPermissions):
|
||||
"""
|
||||
Access permissions container for MotionChangeRecommendation and MotionChangeRecommendationViewSet.
|
||||
"""
|
||||
def check_permissions(self, user):
|
||||
"""
|
||||
Returns True if the user has read access model instances.
|
||||
"""
|
||||
return user.has_perm('motions.can_see')
|
||||
|
||||
def get_serializer_class(self, user=None):
|
||||
"""
|
||||
Returns serializer class.
|
||||
"""
|
||||
from .serializers import MotionChangeRecommendationSerializer
|
||||
|
||||
return MotionChangeRecommendationSerializer
|
||||
|
||||
|
||||
class CategoryAccessPermissions(BaseAccessPermissions):
|
||||
"""
|
||||
Access permissions container for Category and CategoryViewSet.
|
||||
|
@ -18,7 +18,7 @@ class MotionsAppConfig(AppConfig):
|
||||
from openslides.utils.rest_api import router
|
||||
from .config_variables import get_config_variables
|
||||
from .signals import create_builtin_workflows
|
||||
from .views import CategoryViewSet, MotionViewSet, MotionPollViewSet, WorkflowViewSet
|
||||
from .views import CategoryViewSet, MotionViewSet, MotionPollViewSet, MotionChangeRecommendationViewSet, WorkflowViewSet
|
||||
|
||||
# Define config variables
|
||||
config.update_config_variables(get_config_variables())
|
||||
@ -30,4 +30,6 @@ class MotionsAppConfig(AppConfig):
|
||||
router.register(self.get_model('Category').get_collection_string(), CategoryViewSet)
|
||||
router.register(self.get_model('Motion').get_collection_string(), MotionViewSet)
|
||||
router.register(self.get_model('Workflow').get_collection_string(), WorkflowViewSet)
|
||||
router.register(self.get_model('MotionChangeRecommendation').get_collection_string(),
|
||||
MotionChangeRecommendationViewSet)
|
||||
router.register('motions/motionpoll', MotionPollViewSet)
|
||||
|
@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.1 on 2016-10-12 18:10
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import openslides.utils.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('motions', '0004_auto_20160907_2343'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MotionChangeRecommendation',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('status', models.PositiveIntegerField(default=0)),
|
||||
('line_from', models.PositiveIntegerField()),
|
||||
('line_to', models.PositiveIntegerField()),
|
||||
('text', models.TextField(blank=True)),
|
||||
('creation_time', models.DateTimeField(auto_now=True)),
|
||||
('author', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
('motion_version', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='change_recommendations', to='motions.MotionVersion')),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
bases=(openslides.utils.models.RESTModelMixin, models.Model),
|
||||
),
|
||||
]
|
@ -23,6 +23,7 @@ from openslides.utils.search import user_name_helper
|
||||
from .access_permissions import (
|
||||
CategoryAccessPermissions,
|
||||
MotionAccessPermissions,
|
||||
MotionChangeRecommendationAccessPermissions,
|
||||
WorkflowAccessPermissions,
|
||||
)
|
||||
from .exceptions import WorkflowError
|
||||
@ -697,6 +698,68 @@ class MotionVersion(RESTModelMixin, models.Model):
|
||||
return self.motion
|
||||
|
||||
|
||||
class MotionChangeRecommendationManager(models.Manager):
|
||||
"""
|
||||
Customized model manager to support our get_full_queryset method.
|
||||
"""
|
||||
def get_full_queryset(self):
|
||||
"""
|
||||
Returns the normal queryset with all change recommendations. In the background we
|
||||
join and prefetch all related models.
|
||||
"""
|
||||
return self.get_queryset()
|
||||
|
||||
|
||||
class MotionChangeRecommendation(RESTModelMixin, models.Model):
|
||||
"""
|
||||
A MotionChangeRecommendation object saves change recommendations for a specific MotionVersion
|
||||
"""
|
||||
|
||||
access_permissions = MotionChangeRecommendationAccessPermissions()
|
||||
|
||||
objects = MotionChangeRecommendationManager()
|
||||
|
||||
motion_version = models.ForeignKey(
|
||||
MotionVersion,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='change_recommendations')
|
||||
"""The motion version to which the change recommendation belongs."""
|
||||
|
||||
status = models.PositiveIntegerField(default=0)
|
||||
"""Proposed (0), Accepted (1), Rejected (2)"""
|
||||
|
||||
line_from = models.PositiveIntegerField()
|
||||
"""The number or the first affected line"""
|
||||
|
||||
line_to = models.PositiveIntegerField()
|
||||
"""The number or the last affected line (inclusive)"""
|
||||
|
||||
text = models.TextField(blank=True)
|
||||
"""The replacement for the section of the original text specified by version, line_from and line_to"""
|
||||
|
||||
author = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True)
|
||||
"""A user object, who created this change recommendation. Optional."""
|
||||
|
||||
creation_time = models.DateTimeField(auto_now=True)
|
||||
"""Time when the change recommendation was saved."""
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string, representing this object."""
|
||||
return "Recommendation for Version %s, line %s - %s" % (self.motion_version_id, self.line_from, self.line_to)
|
||||
|
||||
def get_root_rest_element(self):
|
||||
"""
|
||||
Returns this instance, which is the root REST element.
|
||||
"""
|
||||
return self
|
||||
|
||||
|
||||
class Category(RESTModelMixin, models.Model):
|
||||
"""
|
||||
Model for categories of motions.
|
||||
|
@ -16,6 +16,7 @@ from openslides.utils.rest_api import (
|
||||
from .models import (
|
||||
Category,
|
||||
Motion,
|
||||
MotionChangeRecommendation,
|
||||
MotionLog,
|
||||
MotionPoll,
|
||||
MotionVersion,
|
||||
@ -228,6 +229,22 @@ class MotionVersionSerializer(ModelSerializer):
|
||||
'reason',)
|
||||
|
||||
|
||||
class MotionChangeRecommendationSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.MotionChangeRecommendation objects.
|
||||
"""
|
||||
class Meta:
|
||||
model = MotionChangeRecommendation
|
||||
fields = (
|
||||
'id',
|
||||
'motion_version',
|
||||
'status',
|
||||
'line_from',
|
||||
'line_to',
|
||||
'text',
|
||||
'creation_time',)
|
||||
|
||||
|
||||
class MotionSerializer(ModelSerializer):
|
||||
"""
|
||||
Serializer for motion.models.Motion objects.
|
||||
|
@ -142,13 +142,16 @@ angular.module('OpenSlidesApp.motions', [
|
||||
.factory('Motion', [
|
||||
'DS',
|
||||
'MotionPoll',
|
||||
'MotionChangeRecommendation',
|
||||
'MotionComment',
|
||||
'jsDataModel',
|
||||
'gettext',
|
||||
'operator',
|
||||
'Config',
|
||||
'lineNumberingService',
|
||||
function(DS, MotionPoll, MotionComment, jsDataModel, gettext, operator, Config, lineNumberingService) {
|
||||
'diffService',
|
||||
function(DS, MotionPoll, MotionChangeRecommendation, MotionComment, jsDataModel, gettext, operator, Config,
|
||||
lineNumberingService, diffService) {
|
||||
var name = 'motions/motion';
|
||||
return DS.defineResource({
|
||||
name: name,
|
||||
@ -187,7 +190,98 @@ angular.module('OpenSlidesApp.motions', [
|
||||
|
||||
return lineNumberingService.insertLineNumbers(html, lineLength, highlight, callback);
|
||||
},
|
||||
setTextStrippingLineBreaks: function (versionId, text) {
|
||||
getTextBetweenChangeRecommendations: function (versionId, change1, change2) {
|
||||
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';
|
||||
}
|
||||
if (line_from == line_to) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var lineLength = Config.get('motions_line_length').value,
|
||||
html = lineNumberingService.insertLineNumbers(this.getVersion(versionId).text, lineLength);
|
||||
|
||||
var data = diffService.extractRangeByLineNumbers(html, line_from, line_to);
|
||||
|
||||
html = data.outerContextStart + data.innerContextStart + data.html +
|
||||
data.innerContextEnd + data.outerContextEnd;
|
||||
html = lineNumberingService.insertLineNumbers(html, lineLength, null, null, line_from);
|
||||
|
||||
return html;
|
||||
},
|
||||
getTextRemainderAfterLastChangeRecommendation: function(versionId, changes) {
|
||||
var maxLine = 0;
|
||||
for (var i = 0; i < changes.length; i++) {
|
||||
if (changes[i].line_to > maxLine) {
|
||||
maxLine = changes[i].line_to;
|
||||
}
|
||||
}
|
||||
|
||||
var lineLength = Config.get('motions_line_length').value,
|
||||
html = lineNumberingService.insertLineNumbers(this.getVersion(versionId).text, lineLength);
|
||||
|
||||
var data = diffService.extractRangeByLineNumbers(html, maxLine, null);
|
||||
html = data.outerContextStart + data.innerContextStart +
|
||||
data.html +
|
||||
data.innerContextEnd + data.outerContextEnd;
|
||||
html = lineNumberingService.insertLineNumbers(html, lineLength, null, null, maxLine);
|
||||
|
||||
return html;
|
||||
},
|
||||
getTextWithChangeRecommendations: function (versionId, statusCompareCb) {
|
||||
var lineLength = Config.get('motions_line_length').value,
|
||||
html = this.getVersion(versionId).text,
|
||||
changes = this.getChangeRecommendations(versionId, 'DESC'),
|
||||
fragment;
|
||||
|
||||
for (var i = 0; i < changes.length; i++) {
|
||||
var change = changes[i];
|
||||
if (statusCompareCb === undefined || statusCompareCb(change.status)) {
|
||||
html = lineNumberingService.insertLineNumbers(html, lineLength);
|
||||
fragment = diffService.htmlToFragment(html);
|
||||
html = diffService.replaceLines(fragment, change.text, change.line_from, change.line_to);
|
||||
}
|
||||
}
|
||||
|
||||
return lineNumberingService.insertLineNumbers(html, lineLength);
|
||||
},
|
||||
getTextWithAcceptedChangeRecommendations: function (versionId) {
|
||||
return this.getTextWithChangeRecommendations(versionId, function(status) {
|
||||
return (status == 1);
|
||||
});
|
||||
},
|
||||
getTextByMode: function(mode, versionId) {
|
||||
/*
|
||||
* @param mode ['original', 'diff', 'changed', 'agreed']
|
||||
* @param versionId [if undefined, active_version will be used]
|
||||
*/
|
||||
var text;
|
||||
switch (mode) {
|
||||
case 'original':
|
||||
text = this.getTextWithLineBreaks(versionId);
|
||||
break;
|
||||
case 'diff':
|
||||
var changes = this.getChangeRecommendations(versionId, 'ASC');
|
||||
text = '';
|
||||
for (var i = 0; i < changes.length; i++) {
|
||||
text += this.getTextBetweenChangeRecommendations(versionId, (i === 0 ? null : changes[i - 1]), changes[i]);
|
||||
text += changes[i].format(this, versionId);
|
||||
}
|
||||
text += this.getTextRemainderAfterLastChangeRecommendation(versionId, changes);
|
||||
break;
|
||||
case 'changed':
|
||||
text = this.getTextWithChangeRecommendations(versionId);
|
||||
break;
|
||||
case 'agreed':
|
||||
text = this.getTextWithAcceptedChangeRecommendations(versionId);
|
||||
break;
|
||||
}
|
||||
return text;
|
||||
},
|
||||
setTextStrippingLineBreaks: function (text) {
|
||||
this.text = lineNumberingService.stripLineNumbers(text);
|
||||
},
|
||||
getReason: function (versionId) {
|
||||
@ -201,6 +295,24 @@ angular.module('OpenSlidesApp.motions', [
|
||||
getSearchResultSubtitle: function () {
|
||||
return "Motion";
|
||||
},
|
||||
getChangeRecommendations: function (versionId, order) {
|
||||
/*
|
||||
* Returns all change recommendations for this given version, sorted by line
|
||||
* @param versionId
|
||||
* @param order ['DESC' or 'ASC' (default)]
|
||||
* @returns {*}
|
||||
*/
|
||||
versionId = versionId || this.active_version;
|
||||
order = order || 'ASC';
|
||||
return MotionChangeRecommendation.filter({
|
||||
where: {
|
||||
motion_version_id: versionId
|
||||
},
|
||||
orderBy: [
|
||||
['line_from', order]
|
||||
]
|
||||
});
|
||||
},
|
||||
isAllowed: function (action) {
|
||||
/*
|
||||
* Return true if the requested user is allowed to do the specific action.
|
||||
@ -392,11 +504,95 @@ angular.module('OpenSlidesApp.motions', [
|
||||
}
|
||||
])
|
||||
|
||||
.factory('MotionChangeRecommendation', [
|
||||
'DS',
|
||||
'Config',
|
||||
'jsDataModel',
|
||||
'diffService',
|
||||
'lineNumberingService',
|
||||
'gettextCatalog',
|
||||
function (DS, Config, jsDataModel, diffService, lineNumberingService, gettextCatalog) {
|
||||
return DS.defineResource({
|
||||
name: 'motions/motionchangerecommendation',
|
||||
useClass: jsDataModel,
|
||||
methods: {
|
||||
saveStatus: function() {
|
||||
this.DSSave();
|
||||
},
|
||||
format: function(motion, version) {
|
||||
var lineLength = Config.get('motions_line_length').value,
|
||||
html = lineNumberingService.insertLineNumbers(motion.getVersion(version).text, lineLength);
|
||||
|
||||
var data = diffService.extractRangeByLineNumbers(html, this.line_from, this.line_to),
|
||||
oldText = data.outerContextStart + data.innerContextStart +
|
||||
data.html + data.innerContextEnd + data.outerContextEnd,
|
||||
oldTextWithBreaks = lineNumberingService.insertLineNumbersNode(oldText, lineLength, null, this.line_from),
|
||||
newTextWithBreaks = lineNumberingService.insertLineNumbersNode(this.text, lineLength, null, this.line_from);
|
||||
|
||||
for (var i = 0; i < oldTextWithBreaks.childNodes.length; i++) {
|
||||
diffService.addCSSClass(oldTextWithBreaks.childNodes[i], 'delete');
|
||||
}
|
||||
for (i = 0; i < newTextWithBreaks.childNodes.length; i++) {
|
||||
diffService.addCSSClass(newTextWithBreaks.childNodes[i], 'insert');
|
||||
}
|
||||
|
||||
var mergedFragment = document.createDocumentFragment(),
|
||||
el;
|
||||
while (oldTextWithBreaks.firstChild) {
|
||||
el = oldTextWithBreaks.firstChild;
|
||||
oldTextWithBreaks.removeChild(el);
|
||||
mergedFragment.appendChild(el);
|
||||
}
|
||||
while (newTextWithBreaks.firstChild) {
|
||||
el = newTextWithBreaks.firstChild;
|
||||
newTextWithBreaks.removeChild(el);
|
||||
mergedFragment.appendChild(el);
|
||||
}
|
||||
|
||||
return diffService._serializeDom(mergedFragment);
|
||||
},
|
||||
getType: function(original_full_html) {
|
||||
var lineLength = Config.get('motions_line_length').value,
|
||||
html = lineNumberingService.insertLineNumbers(original_full_html, lineLength);
|
||||
|
||||
var data = diffService.extractRangeByLineNumbers(html, this.line_from, this.line_to),
|
||||
oldText = data.outerContextStart + data.innerContextStart +
|
||||
data.html + data.innerContextEnd + data.outerContextEnd;
|
||||
|
||||
return diffService.detectReplacementType(oldText, this.text);
|
||||
},
|
||||
getTitle: function(original_full_html) {
|
||||
var title;
|
||||
if (this.line_to > (this.line_from + 1)) {
|
||||
title = gettextCatalog.getString('%TYPE% from line %FROM% to %TO%');
|
||||
} else {
|
||||
title = gettextCatalog.getString('%TYPE% in line %FROM%');
|
||||
}
|
||||
switch (this.getType(original_full_html)) {
|
||||
case diffService.TYPE_INSERTION:
|
||||
title = title.replace('%TYPE%', gettextCatalog.getString('Insertion'));
|
||||
break;
|
||||
case diffService.TYPE_DELETION:
|
||||
title = title.replace('%TYPE%', gettextCatalog.getString('Deletion'));
|
||||
break;
|
||||
case diffService.TYPE_REPLACEMENT:
|
||||
title = title.replace('%TYPE%', gettextCatalog.getString('Replacement'));
|
||||
break;
|
||||
}
|
||||
title = title.replace('%FROM%', this.line_from).replace('%TO%', (this.line_to - 1));
|
||||
return title;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
])
|
||||
|
||||
.run([
|
||||
'Motion',
|
||||
'Category',
|
||||
'Workflow',
|
||||
function(Motion, Category, Workflow) {}
|
||||
'MotionChangeRecommendation',
|
||||
function(Motion, Category, Workflow, MotionChangeRecommendation) {}
|
||||
])
|
||||
|
||||
|
||||
|
@ -9,6 +9,9 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
TEXT_NODE = 3,
|
||||
DOCUMENT_FRAGMENT_NODE = 11;
|
||||
|
||||
this.TYPE_REPLACEMENT = 0;
|
||||
this.TYPE_INSERTION = 1;
|
||||
this.TYPE_DELETION = 2;
|
||||
|
||||
this.getLineNumberNode = function(fragment, lineNumber) {
|
||||
return fragment.querySelector('os-linebreak.os-line-number.line-number-' + lineNumber);
|
||||
@ -24,31 +27,100 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
return context;
|
||||
};
|
||||
|
||||
// Adds elements like <OS-LINEBREAK class="os-line-number line-number-23" data-line-number="23"/>
|
||||
this._insertInternalLineMarkers = function(fragment) {
|
||||
if (fragment.querySelectorAll('OS-LINEBREAK').length > 0) {
|
||||
// Prevent duplicate calls
|
||||
return;
|
||||
}
|
||||
var lineNumbers = fragment.querySelectorAll('span.os-line-number');
|
||||
var lineNumbers = fragment.querySelectorAll('span.os-line-number'),
|
||||
lineMarker, maxLineNumber;
|
||||
|
||||
for (var i = 0; i < lineNumbers.length; i++) {
|
||||
var insertBefore = lineNumbers[i];
|
||||
while (insertBefore.parentNode.nodeType != DOCUMENT_FRAGMENT_NODE && insertBefore.parentNode.childNodes[0] == insertBefore) {
|
||||
insertBefore = insertBefore.parentNode;
|
||||
}
|
||||
var lineMarker = document.createElement('OS-LINEBREAK');
|
||||
lineMarker = document.createElement('OS-LINEBREAK');
|
||||
lineMarker.setAttribute('data-line-number', lineNumbers[i].getAttribute('data-line-number'));
|
||||
lineMarker.setAttribute('class', lineNumbers[i].getAttribute('class'));
|
||||
insertBefore.parentNode.insertBefore(lineMarker, insertBefore);
|
||||
maxLineNumber = lineNumbers[i].getAttribute('data-line-number');
|
||||
}
|
||||
|
||||
// Add one more "fake" line number at the end and beginning, so we can select the last line as well
|
||||
lineMarker = document.createElement('OS-LINEBREAK');
|
||||
lineMarker.setAttribute('data-line-number', (parseInt(maxLineNumber) + 1));
|
||||
lineMarker.setAttribute('class', 'os-line-number line-number-' + (parseInt(maxLineNumber) + 1));
|
||||
fragment.appendChild(lineMarker);
|
||||
|
||||
lineMarker = document.createElement('OS-LINEBREAK');
|
||||
lineMarker.setAttribute('data-line-number', '0');
|
||||
lineMarker.setAttribute('class', 'os-line-number line-number-0');
|
||||
fragment.insertBefore(lineMarker, fragment.firstChild);
|
||||
};
|
||||
|
||||
// @TODO Check if this is actually necessary
|
||||
this._insertInternalLiNumbers = function(fragment) {
|
||||
if (fragment.querySelectorAll('LI[os-li-number]').length > 0) {
|
||||
// Prevent duplicate calls
|
||||
return;
|
||||
}
|
||||
var ols = fragment.querySelectorAll('OL');
|
||||
for (var i = 0; i < ols.length; i++) {
|
||||
var ol = ols[i],
|
||||
liNo = 0;
|
||||
for (var j = 0; j < ol.childNodes.length; j++) {
|
||||
if (ol.childNodes[j].nodeName == 'LI') {
|
||||
liNo++;
|
||||
ol.childNodes[j].setAttribute('os-li-number', liNo);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Returns an array with the following values:
|
||||
* 0: the most specific DOM-node that contains both line numbers
|
||||
* 1: the context of node1 (an array of dom-elements; 0 is the document fragment)
|
||||
* 2: the context of node2 (an array of dom-elements; 0 is the document fragment)
|
||||
* 3: the index of [0] in the two arrays
|
||||
*/
|
||||
this._addStartToOlIfNecessary = function(node) {
|
||||
var firstLiNo = null;
|
||||
for (var i = 0; i < node.childNodes.length && firstLiNo === null; i++) {
|
||||
if (node.childNode[i].nodeName == 'LI') {
|
||||
var lineNo = node.childNode[i].getAttribute('ol-li-number');
|
||||
if (lineNo) {
|
||||
firstLiNo = parseInt(lineNo);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (firstLiNo > 1) {
|
||||
node.setAttribute('start', firstLiNo);
|
||||
}
|
||||
};
|
||||
|
||||
this._isWithinNthLIOfOL = function(olNode, descendantNode) {
|
||||
var nthLIOfOL = null;
|
||||
while (descendantNode.parentNode) {
|
||||
if (descendantNode.parentNode == olNode) {
|
||||
var lisBeforeOl = 0,
|
||||
foundMe = false;
|
||||
for (var i = 0; i < olNode.childNodes.length && !foundMe; i++) {
|
||||
if (olNode.childNodes[i] == descendantNode) {
|
||||
foundMe = true;
|
||||
} else if (olNode.childNodes[i].nodeName == 'LI') {
|
||||
lisBeforeOl++;
|
||||
}
|
||||
}
|
||||
nthLIOfOL = lisBeforeOl + 1;
|
||||
}
|
||||
descendantNode = descendantNode.parentNode;
|
||||
}
|
||||
return nthLIOfOL;
|
||||
};
|
||||
|
||||
/*
|
||||
* Returns an array with the following values:
|
||||
* 0: the most specific DOM-node that contains both line numbers
|
||||
* 1: the context of node1 (an array of dom-elements; 0 is the document fragment)
|
||||
* 2: the context of node2 (an array of dom-elements; 0 is the document fragment)
|
||||
* 3: the index of [0] in the two arrays
|
||||
*/
|
||||
this._getCommonAncestor = function(node1, node2) {
|
||||
var trace1 = this._getNodeContextTrace(node1),
|
||||
trace2 = this._getNodeContextTrace(node2),
|
||||
@ -84,8 +156,10 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
}
|
||||
var html = '<' + node.nodeName;
|
||||
for (var i = 0; i < node.attributes.length; i++) {
|
||||
var attr = node.attributes[i];
|
||||
html += " " + attr.name + "=\"" + attr.value + "\"";
|
||||
var attr = node.attributes[i];
|
||||
if (attr.name != 'os-li-number') {
|
||||
html += ' ' + attr.name + '="' + attr.value + '"';
|
||||
}
|
||||
}
|
||||
html += '>';
|
||||
return html;
|
||||
@ -158,7 +232,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
}
|
||||
if (!found) {
|
||||
console.trace();
|
||||
throw "Inconsistency or invalid call of this function detected";
|
||||
throw "Inconsistency or invalid call of this function detected (to)";
|
||||
}
|
||||
return html;
|
||||
};
|
||||
@ -196,7 +270,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
}
|
||||
if (!found) {
|
||||
console.trace();
|
||||
throw "Inconsistency or invalid call of this function detected";
|
||||
throw "Inconsistency or invalid call of this function detected (from)";
|
||||
}
|
||||
if (node.nodeType != DOCUMENT_FRAGMENT_NODE) {
|
||||
html += '</' + node.nodeName + '>';
|
||||
@ -221,6 +295,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
*
|
||||
* Hint:
|
||||
* - The last line (toLine) is not included anymore, as the number refers to the line breaking element
|
||||
* - 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
|
||||
* that contains the whole section specified by the line numbers (like a P-element if only one paragraph is selected
|
||||
@ -246,11 +321,19 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
* - followingHtmlStartSnippet: A HTML snippet that opens all HTML tags necessary to render "followingHtml"
|
||||
*
|
||||
*/
|
||||
this.extractRangeByLineNumbers = function(fragment, fromLine, toLine) {
|
||||
this.extractRangeByLineNumbers = function(fragment, fromLine, toLine, debug) {
|
||||
if (typeof(fragment) == 'string') {
|
||||
fragment = this.htmlToFragment(fragment);
|
||||
}
|
||||
this._insertInternalLineMarkers(fragment);
|
||||
this._insertInternalLiNumbers(fragment);
|
||||
if (toLine === null) {
|
||||
var internalLineMarkers = fragment.querySelectorAll('OS-LINEBREAK');
|
||||
toLine = parseInt(internalLineMarkers[internalLineMarkers.length - 1].getAttribute("data-line-number"));
|
||||
}
|
||||
|
||||
var fromLineNode = this.getLineNumberNode(fragment, fromLine),
|
||||
toLineNode = this.getLineNumberNode(fragment, toLine),
|
||||
toLineNode = (toLine ? this.getLineNumberNode(fragment, toLine) : null),
|
||||
ancestorData = this._getCommonAncestor(fromLineNode, toLineNode);
|
||||
|
||||
var fromChildTraceRel = ancestorData.trace1,
|
||||
@ -264,7 +347,8 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
innerContextStart = '',
|
||||
innerContextEnd = '',
|
||||
previousHtmlEndSnippet = '',
|
||||
followingHtmlStartSnippet = '';
|
||||
followingHtmlStartSnippet = '',
|
||||
fakeOl;
|
||||
|
||||
|
||||
fromChildTraceAbs.shift();
|
||||
@ -288,7 +372,13 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
if (fromChildTraceRel[i].nodeName == 'OS-LINEBREAK') {
|
||||
found = true;
|
||||
} else {
|
||||
innerContextStart += this._serializeTag(fromChildTraceRel[i]);
|
||||
if (fromChildTraceRel[i].nodeName == 'OL') {
|
||||
fakeOl = fromChildTraceRel[i].cloneNode(false);
|
||||
fakeOl.setAttribute('start', this._isWithinNthLIOfOL(fromChildTraceRel[i], fromLineNode));
|
||||
innerContextStart += this._serializeTag(fakeOl);
|
||||
} else {
|
||||
innerContextStart += this._serializeTag(fromChildTraceRel[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
found = false;
|
||||
@ -317,7 +407,13 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
|
||||
currNode = ancestor;
|
||||
while (currNode.parentNode) {
|
||||
outerContextStart = this._serializeTag(currNode) + outerContextStart;
|
||||
if (currNode.nodeName == 'OL') {
|
||||
fakeOl = currNode.cloneNode(false);
|
||||
fakeOl.setAttribute('start', this._isWithinNthLIOfOL(currNode, fromLineNode));
|
||||
outerContextStart = this._serializeTag(fakeOl) + outerContextStart;
|
||||
} else {
|
||||
outerContextStart = this._serializeTag(currNode) + outerContextStart;
|
||||
}
|
||||
outerContextEnd += '</' + currNode.nodeName + '>';
|
||||
currNode = currNode.parentNode;
|
||||
}
|
||||
@ -334,9 +430,16 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
'followingHtml': followingHtml,
|
||||
'followingHtmlStartSnippet': followingHtmlStartSnippet
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
/*
|
||||
* This functions merges to arrays of nodes. The last element of nodes1 and the first element of nodes2
|
||||
* are merged, if they are of the same type.
|
||||
*
|
||||
* This is done recursively until a TEMPLATE-Tag is is found, which was inserted in this.replaceLines.
|
||||
* Using a TEMPLATE-Tag is a rather dirty hack, as it is allowed inside of any other element, including <ul>.
|
||||
*
|
||||
*/
|
||||
this._replaceLinesMergeNodeArrays = function(nodes1, nodes2) {
|
||||
if (nodes1.length === 0) {
|
||||
return nodes2;
|
||||
@ -350,56 +453,179 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
||||
out.push(nodes1[i]);
|
||||
}
|
||||
|
||||
out.push(nodes1[nodes1.length - 1]);
|
||||
out.push(nodes2[0]);
|
||||
var lastNode = nodes1[nodes1.length - 1],
|
||||
firstNode = nodes2[0];
|
||||
if (lastNode.nodeType == TEXT_NODE && firstNode.nodeType == TEXT_NODE) {
|
||||
var newTextNode = lastNode.ownerDocument.createTextNode(lastNode.nodeValue + firstNode.nodeValue);
|
||||
out.push(newTextNode);
|
||||
} else if (lastNode.nodeName == firstNode.nodeName) {
|
||||
var newNode = lastNode.ownerDocument.createElement(lastNode.nodeName);
|
||||
for (i = 0; i < lastNode.attributes.length; i++) {
|
||||
var attr = lastNode.attributes[i];
|
||||
newNode.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
|
||||
// Remove #text nodes inside of List elements, as they are confusing
|
||||
var lastChildren, firstChildren;
|
||||
if (lastNode.nodeName == 'OL' || lastNode.nodeName == 'UL') {
|
||||
lastChildren = [];
|
||||
firstChildren = [];
|
||||
for (i = 0; i < firstNode.childNodes.length; i++) {
|
||||
if (firstNode.childNodes[i].nodeType == ELEMENT_NODE) {
|
||||
firstChildren.push(firstNode.childNodes[i]);
|
||||
}
|
||||
}
|
||||
for (i = 0; i < lastNode.childNodes.length; i++) {
|
||||
if (lastNode.childNodes[i].nodeType == ELEMENT_NODE) {
|
||||
lastChildren.push(lastNode.childNodes[i]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lastChildren = lastNode.childNodes;
|
||||
firstChildren = firstNode.childNodes;
|
||||
}
|
||||
|
||||
var children = this._replaceLinesMergeNodeArrays(lastChildren, firstChildren);
|
||||
for (i = 0; i < children.length; i++) {
|
||||
newNode.appendChild(children[i]);
|
||||
}
|
||||
out.push(newNode);
|
||||
} else {
|
||||
if (lastNode.nodeName != 'TEMPLATE') {
|
||||
out.push(lastNode);
|
||||
}
|
||||
if (firstNode.nodeName != 'TEMPLATE') {
|
||||
out.push(firstNode);
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 1; i < nodes2.length; i++) {
|
||||
out.push(nodes2[i]);
|
||||
}
|
||||
|
||||
/*
|
||||
if (node1.nodeName != node2.nodeName) {
|
||||
return null;
|
||||
}
|
||||
var newNode = node1.ownerDocument.createElement(node1.nodeName);
|
||||
for (var i = 0; i < node1.attributes.length; i++) {
|
||||
var attr = node1.attributes[i];
|
||||
newNode.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
return newNode;
|
||||
*/
|
||||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} htmlOld
|
||||
* @param {string} htmlNew
|
||||
* @returns {number}
|
||||
*/
|
||||
this.detectReplacementType = function (htmlOld, htmlNew) {
|
||||
// Convert all HTML tags to uppercase, strip trailing whitespaces
|
||||
var normalizeHtml = function(html) {
|
||||
html = html.replace(/<[^>]+>/g, function(tag) { return tag.toUpperCase(); });
|
||||
html = html.replace(/\s+<\/P>/gi, '</P>').replace(/\s+<\/DIV>/gi, '</DIV>').replace(/\s+<\/LI>/gi, '</LI>');
|
||||
html = html.replace(/\s+<LI>/gi, '<LI>').replace(/<\/LI>\s+/gi, '</LI>');
|
||||
html = html.replace(/ /gi, ' ').replace(/\u00A0/g, ' '); // non-breaking spaces
|
||||
return html;
|
||||
};
|
||||
htmlOld = normalizeHtml(htmlOld);
|
||||
htmlNew = normalizeHtml(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]) {
|
||||
foundDiff = true;
|
||||
}
|
||||
}
|
||||
|
||||
var remainderOld = htmlOld.substr(i - 1),
|
||||
remainderNew = htmlNew.substr(i - 1),
|
||||
type = this.TYPE_REPLACEMENT;
|
||||
|
||||
if (remainderOld.length > remainderNew.length) {
|
||||
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) {
|
||||
type = this.TYPE_INSERTION;
|
||||
}
|
||||
}
|
||||
|
||||
return type;
|
||||
};
|
||||
|
||||
this.replaceLines = function (fragment, newHTML, fromLine, toLine) {
|
||||
var data = this.extractRangeByLineNumbers(fragment, fromLine, toLine),
|
||||
previousHtml = data.previousHtml + data.previousHtmlEndSnippet,
|
||||
previousHtml = data.previousHtml + '<TEMPLATE></TEMPLATE>' + data.previousHtmlEndSnippet,
|
||||
previousFragment = this.htmlToFragment(previousHtml),
|
||||
followingHtml = data.followingHtmlStartSnippet + data.followingHtml,
|
||||
followingHtml = data.followingHtmlStartSnippet + '<TEMPLATE></TEMPLATE>' + data.followingHtml,
|
||||
followingFragment = this.htmlToFragment(followingHtml),
|
||||
newFragment = this.htmlToFragment(newHTML);
|
||||
|
||||
var merged = this._replaceLinesMergeNodeArrays(previousFragment.childNodes, newFragment.childNodes);
|
||||
merged = this._replaceLinesMergeNodeArrays(merged, followingFragment.childNodes);
|
||||
|
||||
var mergedFragment = document.createDocumentFragment();
|
||||
for (var i = 0; i < merged.length; i++) {
|
||||
mergedFragment.appendChild(merged[i]);
|
||||
}
|
||||
|
||||
var forgottenTemplates = mergedFragment.querySelectorAll("TEMPLATE");
|
||||
for (i = 0; i < forgottenTemplates.length; i++) {
|
||||
var el = forgottenTemplates[i];
|
||||
el.parentNode.removeChild(el);
|
||||
}
|
||||
|
||||
return this._serializeDom(mergedFragment, true);
|
||||
};
|
||||
|
||||
this.addCSSClass = function (node, className) {
|
||||
if (node.nodeType != ELEMENT_NODE) {
|
||||
return;
|
||||
}
|
||||
var classes = node.getAttribute('class');
|
||||
classes = (classes ? classes.split(' ') : []);
|
||||
if (classes.indexOf(className) == -1) {
|
||||
classes.push(className);
|
||||
}
|
||||
node.setAttribute('class', classes);
|
||||
};
|
||||
|
||||
this.addDiffMarkup = function (fragment, newHTML, fromLine, toLine, diffFormatterCb) {
|
||||
var data = this.extractRangeByLineNumbers(fragment, fromLine, toLine),
|
||||
previousHtml = data.previousHtml + '<TEMPLATE></TEMPLATE>' + data.previousHtmlEndSnippet,
|
||||
previousFragment = this.htmlToFragment(previousHtml),
|
||||
followingHtml = data.followingHtmlStartSnippet + '<TEMPLATE></TEMPLATE>' + data.followingHtml,
|
||||
followingFragment = this.htmlToFragment(followingHtml),
|
||||
newFragment = this.htmlToFragment(newHTML),
|
||||
child;
|
||||
oldHTML = data.outerContextStart + data.innerContextStart + data.html +
|
||||
data.innerContextEnd + data.outerContextEnd,
|
||||
oldFragment = this.htmlToFragment(oldHTML),
|
||||
el;
|
||||
|
||||
var merged = document.createDocumentFragment();
|
||||
var diffFragment = diffFormatterCb(oldFragment, newFragment);
|
||||
|
||||
while (previousFragment.children.length > 0) {
|
||||
child = previousFragment.children[0];
|
||||
previousFragment.removeChild(child);
|
||||
merged.appendChild(child);
|
||||
var mergedFragment = document.createDocumentFragment();
|
||||
while (previousFragment.firstChild) {
|
||||
el = previousFragment.firstChild;
|
||||
previousFragment.removeChild(el);
|
||||
mergedFragment.appendChild(el);
|
||||
}
|
||||
while (newFragment.children.length > 0) {
|
||||
child = newFragment.children[0];
|
||||
newFragment.removeChild(child);
|
||||
merged.appendChild(child);
|
||||
while (diffFragment.firstChild) {
|
||||
el = diffFragment.firstChild;
|
||||
diffFragment.removeChild(el);
|
||||
mergedFragment.appendChild(el);
|
||||
}
|
||||
while (followingFragment.children.length > 0) {
|
||||
child = followingFragment.children[0];
|
||||
followingFragment.removeChild(child);
|
||||
merged.appendChild(child);
|
||||
while (followingFragment.firstChild) {
|
||||
el = followingFragment.firstChild;
|
||||
followingFragment.removeChild(el);
|
||||
mergedFragment.appendChild(el);
|
||||
}
|
||||
//var merged = this._replaceLinesAttemptMerge(lastOfPrevious, firstOfReplaced);
|
||||
|
||||
return this._serializeDom(merged, true);
|
||||
var forgottenTemplates = mergedFragment.querySelectorAll("TEMPLATE");
|
||||
for (var i = 0; i < forgottenTemplates.length; i++) {
|
||||
el = forgottenTemplates[i];
|
||||
el.parentNode.removeChild(el);
|
||||
}
|
||||
|
||||
return this._serializeDom(mergedFragment, true);
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -76,6 +76,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
|
||||
*
|
||||
* @param node
|
||||
* @param length
|
||||
* @param highlight
|
||||
* @returns Array
|
||||
* @private
|
||||
*/
|
||||
@ -89,7 +90,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
|
||||
var addLine = function (text, highlight) {
|
||||
var node;
|
||||
if (typeof highlight === 'undefined') {
|
||||
highlight = 0;
|
||||
highlight = -1;
|
||||
}
|
||||
if (firstTextNode) {
|
||||
if (highlight == service._currentLineNumber - 1) {
|
||||
@ -344,19 +345,23 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
|
||||
return root.innerHTML;
|
||||
};
|
||||
|
||||
this.insertLineNumbersNode = function (html, lineLength, highlight) {
|
||||
this.insertLineNumbersNode = function (html, lineLength, highlight, firstLine) {
|
||||
var root = document.createElement('div');
|
||||
root.innerHTML = html;
|
||||
|
||||
this._currentInlineOffset = 0;
|
||||
this._currentLineNumber = 1;
|
||||
if (firstLine) {
|
||||
this._currentLineNumber = firstLine;
|
||||
} else {
|
||||
this._currentLineNumber = 1;
|
||||
}
|
||||
this._prependLineNumberToFirstText = true;
|
||||
|
||||
return this._insertLineNumbersToNode(root, lineLength, highlight);
|
||||
};
|
||||
|
||||
this.insertLineNumbers = function (html, lineLength, highlight, callback) {
|
||||
var newRoot = this.insertLineNumbersNode(html, lineLength, highlight);
|
||||
this.insertLineNumbers = function (html, lineLength, highlight, callback, firstLine) {
|
||||
var newRoot = this.insertLineNumbersNode(html, lineLength, highlight, firstLine);
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
|
@ -4,6 +4,57 @@
|
||||
|
||||
angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions', 'OpenSlidesApp.motions.lineNumbering'])
|
||||
|
||||
.factory('MotionPDFExport', [
|
||||
'HTMLValidizer',
|
||||
'Motion',
|
||||
'User',
|
||||
'PdfMakeConverter',
|
||||
'PdfMakeDocumentProvider',
|
||||
'MotionContentProvider',
|
||||
'PollContentProvider',
|
||||
'gettextCatalog',
|
||||
'$http',
|
||||
function (HTMLValidizer, Motion, User, PdfMakeConverter, PdfMakeDocumentProvider, MotionContentProvider,
|
||||
PollContentProvider, gettextCatalog, $http) {
|
||||
var obj = {};
|
||||
|
||||
var $scope;
|
||||
|
||||
obj.createMotion = function() {
|
||||
var text = $scope.motion.getTextByMode($scope.viewChangeRecommendations.mode, $scope.version);
|
||||
var content = HTMLValidizer.validize(text) + HTMLValidizer.validize($scope.motion.getReason($scope.version));
|
||||
var map = Function.prototype.call.bind([].map);
|
||||
var image_sources = map($(content).find("img"), function(element) {
|
||||
return element.getAttribute("src");
|
||||
});
|
||||
|
||||
$http.post('/core/encode_media/', JSON.stringify(image_sources)).success(function(data) {
|
||||
var converter = PdfMakeConverter.createInstance(data.images, data.fonts, pdfMake);
|
||||
var motionContentProvider = MotionContentProvider.createInstance(converter, $scope.motion, $scope, User, $http);
|
||||
var documentProvider = PdfMakeDocumentProvider.createInstance(motionContentProvider, data.defaultFont);
|
||||
var filename = gettextCatalog.getString("Motion") + "-" + $scope.motion.identifier + ".pdf";
|
||||
pdfMake.createPdf(documentProvider.getDocument()).download(filename);
|
||||
});
|
||||
};
|
||||
|
||||
//make PDF for polls
|
||||
obj.createPoll = function() {
|
||||
var id = $scope.motion.identifier.replace(" ", ""),
|
||||
title = $scope.motion.getTitle($scope.version),
|
||||
filename = gettextCatalog.getString("Motion") + "-" + id + "-" + gettextCatalog.getString("ballot-paper") + ".pdf",
|
||||
content = PollContentProvider.createInstance(title, id, gettextCatalog);
|
||||
|
||||
pdfMake.createPdf(content).download(filename);
|
||||
};
|
||||
|
||||
obj.init = function (_scope) {
|
||||
$scope = _scope;
|
||||
};
|
||||
|
||||
return obj;
|
||||
}
|
||||
])
|
||||
|
||||
.factory('MotionInlineEditing', [
|
||||
'Editor',
|
||||
'Motion',
|
||||
@ -37,29 +88,28 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
|
||||
obj.tinymceOptions.readonly = 1;
|
||||
obj.tinymceOptions.setup = function (editor) {
|
||||
obj.editor = editor;
|
||||
editor.on("init", function () {
|
||||
editor.on('init', function () {
|
||||
obj.lineBrokenText = motion.getTextWithLineBreaks($scope.version);
|
||||
obj.editor.setContent(obj.lineBrokenText);
|
||||
obj.originalHtml = obj.editor.getContent();
|
||||
obj.changed = false;
|
||||
});
|
||||
editor.on("change", function () {
|
||||
editor.on('change', function () {
|
||||
obj.changed = (editor.getContent() != obj.originalHtml);
|
||||
});
|
||||
editor.on("undo", function () {
|
||||
editor.on('undo', function () {
|
||||
obj.changed = (editor.getContent() != obj.originalHtml);
|
||||
});
|
||||
};
|
||||
|
||||
obj.setVersion = function (_motion, versionId) {
|
||||
motion = _motion; // If this is not updated,
|
||||
console.log(versionId, motion.getTextWithLineBreaks(versionId));
|
||||
obj.lineBrokenText = motion.getTextWithLineBreaks(versionId);
|
||||
obj.changed = false;
|
||||
obj.active = false;
|
||||
if (obj.editor) {
|
||||
obj.editor.setContent(obj.lineBrokenText);
|
||||
obj.editor.setMode("readonly");
|
||||
obj.editor.setMode('readonly');
|
||||
obj.originalHtml = obj.editor.getContent();
|
||||
} else {
|
||||
obj.originalHtml = obj.lineBrokenText;
|
||||
@ -67,7 +117,7 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
|
||||
};
|
||||
|
||||
obj.enable = function () {
|
||||
obj.editor.setMode("design");
|
||||
obj.editor.setMode('design');
|
||||
obj.active = true;
|
||||
obj.changed = false;
|
||||
|
||||
@ -80,7 +130,7 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
|
||||
};
|
||||
|
||||
obj.disable = function () {
|
||||
obj.editor.setMode("readonly");
|
||||
obj.editor.setMode('readonly');
|
||||
obj.active = false;
|
||||
obj.changed = false;
|
||||
obj.lineBrokenText = obj.originalHtml;
|
||||
@ -89,10 +139,10 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
|
||||
|
||||
obj.save = function () {
|
||||
if (!motion.isAllowed('update')) {
|
||||
throw "No permission to update motion";
|
||||
throw 'No permission to update motion';
|
||||
}
|
||||
|
||||
motion.setTextStrippingLineBreaks(motion.active_version, obj.editor.getContent());
|
||||
motion.setTextStrippingLineBreaks(obj.editor.getContent());
|
||||
motion.disable_versioning = (obj.trivialChange && Config.get('motions_allow_disable_versioning').value);
|
||||
|
||||
Motion.inject(motion);
|
||||
@ -114,6 +164,321 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
|
||||
);
|
||||
};
|
||||
|
||||
return obj;
|
||||
}
|
||||
])
|
||||
|
||||
.factory('ChangeRecommmendationCreate', [
|
||||
'ngDialog',
|
||||
'ChangeRecommendationForm',
|
||||
function(ngDialog, ChangeRecommendationForm) {
|
||||
var MODE_INACTIVE = 0,
|
||||
MODE_SELECTING_FROM = 1,
|
||||
MODE_SELECTING_TO = 2;
|
||||
|
||||
var obj = {
|
||||
mode: MODE_INACTIVE,
|
||||
lineFrom: 1,
|
||||
lineTo: 2,
|
||||
html: '',
|
||||
reviewingHtml: ''
|
||||
};
|
||||
|
||||
var $scope, motion, version;
|
||||
|
||||
obj._getAffectedLineNumbers = function () {
|
||||
var changeRecommendations = motion.getChangeRecommendations(version),
|
||||
affectedLines = [];
|
||||
for (var i = 0; i < changeRecommendations.length; i++) {
|
||||
var change = changeRecommendations[i];
|
||||
for (var j = change.line_from; j < change.line_to; j++) {
|
||||
affectedLines.push(j);
|
||||
}
|
||||
}
|
||||
return affectedLines;
|
||||
};
|
||||
|
||||
obj.startCreating = function () {
|
||||
if (obj.mode > MODE_SELECTING_FROM || !motion.isAllowed('can_manage')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var $lineNumbers = $(".motion-text-original .os-line-number");
|
||||
if ($lineNumbers.filter(".selectable").length === 0) {
|
||||
obj.mode = MODE_SELECTING_FROM;
|
||||
var alreadyAffectedLines = obj._getAffectedLineNumbers();
|
||||
$lineNumbers.each(function () {
|
||||
var $this = $(this),
|
||||
lineNumber = $this.data("line-number");
|
||||
if (alreadyAffectedLines.indexOf(lineNumber) == -1) {
|
||||
$(this).addClass("selectable");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
obj.cancelCreating = function (ev) {
|
||||
var $target = $(ev.target),
|
||||
query = ".line-numbers-outside .os-line-number.selectable";
|
||||
if (!$target.is(query) && $target.parents(query).length === 0) {
|
||||
obj.mode = MODE_INACTIVE;
|
||||
obj.lineFrom = 0;
|
||||
obj.lineTo = 0;
|
||||
$(".motion-text-original .os-line-number").removeClass("selected selectable");
|
||||
obj.startCreating();
|
||||
}
|
||||
};
|
||||
|
||||
obj.setFromLine = function (line) {
|
||||
obj.mode = MODE_SELECTING_TO;
|
||||
obj.lineFrom = line;
|
||||
|
||||
var alreadyAffectedLines = obj._getAffectedLineNumbers(),
|
||||
foundCollission = false;
|
||||
|
||||
$(".motion-text-original .os-line-number").each(function () {
|
||||
|
||||
var $this = $(this);
|
||||
if ($this.data("line-number") >= line && !foundCollission) {
|
||||
if (alreadyAffectedLines.indexOf($this.data("line-number")) == -1) {
|
||||
$(this).addClass("selectable");
|
||||
} else {
|
||||
$(this).removeClass("selectable");
|
||||
foundCollission = true;
|
||||
}
|
||||
} else {
|
||||
$(this).removeClass("selectable");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
obj.setToLine = function (line) {
|
||||
if (line < obj.lineFrom) {
|
||||
return;
|
||||
}
|
||||
obj.mode = MODE_INACTIVE;
|
||||
obj.lineTo = line + 1;
|
||||
ngDialog.open(ChangeRecommendationForm.getCreateDialog(
|
||||
motion,
|
||||
version,
|
||||
obj.lineFrom,
|
||||
obj.lineTo
|
||||
));
|
||||
|
||||
obj.lineFrom = 0;
|
||||
obj.lineTo = 0;
|
||||
$(".motion-text-original .os-line-number").removeClass("selected selectable");
|
||||
obj.startCreating();
|
||||
};
|
||||
|
||||
obj.lineClicked = function (ev) {
|
||||
if (obj.mode == MODE_INACTIVE) {
|
||||
return;
|
||||
}
|
||||
if (obj.mode == MODE_SELECTING_FROM) {
|
||||
obj.setFromLine($(ev.target).data("line-number"));
|
||||
$(ev.target).addClass("selected");
|
||||
} else if (obj.mode == MODE_SELECTING_TO) {
|
||||
obj.setToLine($(ev.target).data("line-number"));
|
||||
}
|
||||
};
|
||||
|
||||
obj.mouseOver = function (ev) {
|
||||
if (obj.mode != MODE_SELECTING_TO) {
|
||||
return;
|
||||
}
|
||||
var hoverLine = $(ev.target).data("line-number");
|
||||
$(".motion-text-original .os-line-number").each(function () {
|
||||
var line = $(this).data("line-number");
|
||||
if (line >= obj.lineFrom && line <= hoverLine) {
|
||||
$(this).addClass("selected");
|
||||
} else {
|
||||
$(this).removeClass("selected");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
obj.setVersion = function (_motion, _version) {
|
||||
motion = _motion;
|
||||
version = _version;
|
||||
};
|
||||
|
||||
obj.init = function (_scope, _motion) {
|
||||
$scope = _scope;
|
||||
motion = _motion;
|
||||
version = $scope.version;
|
||||
|
||||
var $content = $("#content");
|
||||
$content.on("click", ".line-numbers-outside .os-line-number.selectable", obj.lineClicked);
|
||||
$content.on("click", obj.cancelCreating);
|
||||
$content.on("mouseover", ".line-numbers-outside .os-line-number.selectable", obj.mouseOver);
|
||||
$content.on("mouseover", ".motion-text-original", obj.startCreating);
|
||||
|
||||
$scope.$on("$destroy", function () {
|
||||
obj.destroy();
|
||||
});
|
||||
};
|
||||
|
||||
obj.destroy = function () {
|
||||
var $content = $("#content");
|
||||
$content.off("click", ".line-numbers-outside .os-line-number.selectable", obj.lineClicked);
|
||||
$content.off("click", obj.cancelCreating);
|
||||
$content.off("mouseover", ".line-numbers-outside .os-line-number.selectable", obj.mouseOver);
|
||||
$content.off("mouseover", ".motion-text-original", obj.startCreating);
|
||||
};
|
||||
|
||||
return obj;
|
||||
}
|
||||
])
|
||||
|
||||
.factory('ChangeRecommmendationView', [
|
||||
'Motion',
|
||||
'MotionChangeRecommendation',
|
||||
'Config',
|
||||
'lineNumberingService',
|
||||
'diffService',
|
||||
'$interval',
|
||||
'$timeout',
|
||||
function (Motion, MotionChangeRecommendation, Config, lineNumberingService, diffService, $interval, $timeout) {
|
||||
var $scope;
|
||||
|
||||
var obj = {
|
||||
mode: 'original'
|
||||
};
|
||||
|
||||
obj.diffFormatterCb = function (change, oldFragment, newFragment) {
|
||||
for (var i = 0; i < oldFragment.childNodes.length; i++) {
|
||||
diffService.addCSSClass(oldFragment.childNodes[i], 'delete');
|
||||
}
|
||||
for (i = 0; i < newFragment.childNodes.length; i++) {
|
||||
diffService.addCSSClass(newFragment.childNodes[i], 'insert');
|
||||
}
|
||||
var mergedFragment = document.createDocumentFragment(),
|
||||
diffSection = document.createElement('SECTION'),
|
||||
el;
|
||||
|
||||
mergedFragment.appendChild(diffSection);
|
||||
diffSection.setAttribute('class', 'diff');
|
||||
diffSection.setAttribute('data-change-id', change.id);
|
||||
|
||||
while (oldFragment.firstChild) {
|
||||
el = oldFragment.firstChild;
|
||||
oldFragment.removeChild(el);
|
||||
diffSection.appendChild(el);
|
||||
}
|
||||
while (newFragment.firstChild) {
|
||||
el = newFragment.firstChild;
|
||||
newFragment.removeChild(el);
|
||||
diffSection.appendChild(el);
|
||||
}
|
||||
|
||||
return mergedFragment;
|
||||
};
|
||||
|
||||
obj.delete = function (changeId) {
|
||||
MotionChangeRecommendation.destroy(changeId);
|
||||
};
|
||||
|
||||
obj.repositionOriginalAnnotations = function () {
|
||||
var $changeRecommendationList = $('.change-recommendation-list'),
|
||||
$lineNumberReference = $('.motion-text-original');
|
||||
|
||||
$changeRecommendationList.children().each(function() {
|
||||
var $this = $(this),
|
||||
lineFrom = $this.data('line-from'),
|
||||
lineTo = ($this.data('line-to') - 1),
|
||||
$lineFrom = $lineNumberReference.find('.line-number-' + lineFrom),
|
||||
$lineTo = $lineNumberReference.find('.line-number-' + lineTo),
|
||||
fromTop = $lineFrom.position().top + 3,
|
||||
toTop = $lineTo.position().top + 20,
|
||||
height = (toTop - fromTop);
|
||||
|
||||
if (height < 10) {
|
||||
height = 10;
|
||||
}
|
||||
|
||||
// $lineFrom.position().top seems to depend on the scrolling position when the line numbers
|
||||
// have position: absolute. Maybe a bug in the used version of jQuery?
|
||||
// This cancels the effect.
|
||||
/*
|
||||
if ($lineNumberReference.hasClass('line-numbers-outside')) {
|
||||
fromTop += window.scrollY;
|
||||
}
|
||||
*/
|
||||
|
||||
$this.css({ 'top': fromTop, 'height': height });
|
||||
});
|
||||
};
|
||||
|
||||
obj.newVersionIncludingChanges = function (motion, version, includeProposed) {
|
||||
if (!motion.isAllowed('update')) {
|
||||
throw 'No permission to update motion';
|
||||
}
|
||||
|
||||
var newHtml = (
|
||||
includeProposed ?
|
||||
motion.getTextWithoutRejectedChangeRecommendations(version) :
|
||||
motion.getTextWithAcceptedChangeRecommendations(version)
|
||||
);
|
||||
|
||||
motion.setTextStrippingLineBreaks(newHtml);
|
||||
|
||||
Motion.inject(motion);
|
||||
// save change motion object on server
|
||||
Motion.save(motion, {method: 'PATCH'}).then(
|
||||
function (success) {
|
||||
$scope.showVersion(motion.getVersion(-1));
|
||||
},
|
||||
function (error) {
|
||||
// save error: revert all changes by restore
|
||||
// (refresh) original motion object from server
|
||||
Motion.refresh(motion);
|
||||
var message = '';
|
||||
for (var e in error.data) {
|
||||
message += e + ': ' + error.data[e] + ' ';
|
||||
}
|
||||
$scope.alert = {type: 'danger', msg: message, show: true};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
obj.scrollToDiffBox = function (changeId) {
|
||||
obj.mode = 'diff';
|
||||
$timeout(function() {
|
||||
var $diffBox = $('.diff-box-' + changeId);
|
||||
$('html, body').animate({
|
||||
scrollTop: $diffBox.offset().top - 50
|
||||
}, 300);
|
||||
}, 0, false);
|
||||
};
|
||||
|
||||
obj.init = function (_scope) {
|
||||
$scope = _scope;
|
||||
$scope.$evalAsync(function() {
|
||||
obj.repositionOriginalAnnotations();
|
||||
});
|
||||
$scope.$watch(function() {
|
||||
return $('.change-recommendation-list').children().length;
|
||||
}, obj.repositionOriginalAnnotations);
|
||||
|
||||
var sizeCheckerLastSize = null,
|
||||
sizeCheckerLastClass = null,
|
||||
sizeChecker = $interval(function() {
|
||||
var $holder = $(".motion-text-original"),
|
||||
newHeight = $holder.height(),
|
||||
classes = $holder.attr("class");
|
||||
if (newHeight != sizeCheckerLastSize || sizeCheckerLastClass != classes) {
|
||||
sizeCheckerLastSize = newHeight;
|
||||
sizeCheckerLastClass = classes;
|
||||
obj.repositionOriginalAnnotations();
|
||||
}
|
||||
}, 100, 0, false);
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
$interval.cancel(sizeChecker);
|
||||
});
|
||||
};
|
||||
|
||||
return obj;
|
||||
}
|
||||
]);
|
||||
|
@ -27,7 +27,8 @@ angular.module('OpenSlidesApp.motions.pdf', ['OpenSlidesApp.core.pdf'])
|
||||
* other project that might want to use this HTML to PDF parser.
|
||||
* https://github.com/OpenSlides/OpenSlides/issues/2361
|
||||
*/
|
||||
return converter.convertHTML(motion.getTextWithLineBreaks($scope.version), $scope);
|
||||
var text = motion.getTextByMode($scope.viewChangeRecommendations.mode, $scope.version);
|
||||
return converter.convertHTML(text, $scope);
|
||||
} else {
|
||||
return converter.convertHTML(motion.getText($scope.version), $scope);
|
||||
}
|
||||
|
@ -104,6 +104,9 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
},
|
||||
tags: function(Tag) {
|
||||
return Tag.findAll();
|
||||
},
|
||||
change_recommendations: function(MotionChangeRecommendation, motion) {
|
||||
return MotionChangeRecommendation.findAll({'where': {'motion_version_id': {'==': motion.active_version}}});
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -207,6 +210,93 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
}
|
||||
])
|
||||
|
||||
.factory('ChangeRecommendationForm', [
|
||||
'gettextCatalog',
|
||||
'Editor',
|
||||
'Config',
|
||||
function(gettextCatalog, Editor, Config) {
|
||||
return {
|
||||
// ngDialog for motion form
|
||||
getCreateDialog: function (motion, version, lineFrom, lineTo) {
|
||||
return {
|
||||
template: 'static/templates/motions/change-recommendation-form.html',
|
||||
controller: 'ChangeRecommendationCreateCtrl',
|
||||
className: 'ngdialog-theme-default wide-form',
|
||||
closeByEscape: false,
|
||||
closeByDocument: false,
|
||||
resolve: {
|
||||
motion: function() {
|
||||
return motion;
|
||||
},
|
||||
version: function() {
|
||||
return version;
|
||||
},
|
||||
lineFrom: function() {
|
||||
return lineFrom;
|
||||
},
|
||||
lineTo: function() {
|
||||
return lineTo;
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
// angular-formly fields for motion form
|
||||
getFormFields: function (line_from, line_to) {
|
||||
return [
|
||||
{
|
||||
key: 'identifier',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Identifier')
|
||||
},
|
||||
hide: true
|
||||
},
|
||||
{
|
||||
key: 'motion_version_id',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('Motion')
|
||||
},
|
||||
hide: true
|
||||
},
|
||||
{
|
||||
key: 'line_from',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('From Line')
|
||||
},
|
||||
hide: true
|
||||
},
|
||||
{
|
||||
key: 'line_to',
|
||||
type: 'input',
|
||||
templateOptions: {
|
||||
label: gettextCatalog.getString('To Line')
|
||||
},
|
||||
hide: true
|
||||
},
|
||||
{
|
||||
key: 'text',
|
||||
type: 'editor',
|
||||
templateOptions: {
|
||||
label: (
|
||||
line_from == line_to - 1 ?
|
||||
gettextCatalog.getString('Text in line %from%').replace(/%from%/, line_from) :
|
||||
gettextCatalog.getString('Text from line %from% to %to%')
|
||||
.replace(/%from%/, line_from).replace(/%to%/, line_to - 1)
|
||||
),
|
||||
required: false
|
||||
},
|
||||
data: {
|
||||
tinymceOption: Editor.getOptions()
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
// Service for generic motion form (create and update)
|
||||
.factory('MotionForm', [
|
||||
'gettextCatalog',
|
||||
@ -818,6 +908,10 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
'operator',
|
||||
'ngDialog',
|
||||
'MotionForm',
|
||||
'ChangeRecommmendationCreate',
|
||||
'ChangeRecommmendationView',
|
||||
'MotionChangeRecommendation',
|
||||
'MotionPDFExport',
|
||||
'Motion',
|
||||
'Category',
|
||||
'Mediafile',
|
||||
@ -826,24 +920,21 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
'Workflow',
|
||||
'Config',
|
||||
'motion',
|
||||
'MotionContentProvider',
|
||||
'PollContentProvider',
|
||||
'PdfMakeConverter',
|
||||
'PdfMakeDocumentProvider',
|
||||
'MotionInlineEditing',
|
||||
'gettextCatalog',
|
||||
'Projector',
|
||||
'HTMLValidizer',
|
||||
'ProjectionDefault',
|
||||
function($scope, $http, operator, ngDialog, MotionForm, Motion, Category, Mediafile, Tag, User, Workflow, Config,
|
||||
motion, MotionContentProvider, PollContentProvider, PdfMakeConverter, PdfMakeDocumentProvider,
|
||||
MotionInlineEditing, gettextCatalog, Projector, HTMLValidizer, ProjectionDefault) {
|
||||
function($scope, $http, operator, ngDialog, MotionForm,
|
||||
ChangeRecommmendationCreate, ChangeRecommmendationView, MotionChangeRecommendation, MotionPDFExport,
|
||||
Motion, Category, Mediafile, Tag, User, Workflow, Config, motion, MotionInlineEditing, gettextCatalog,
|
||||
Projector, ProjectionDefault) {
|
||||
Motion.bindOne(motion.id, $scope, 'motion');
|
||||
Category.bindAll({}, $scope, 'categories');
|
||||
Mediafile.bindAll({}, $scope, 'mediafiles');
|
||||
Tag.bindAll({}, $scope, 'tags');
|
||||
User.bindAll({}, $scope, 'users');
|
||||
Workflow.bindAll({}, $scope, 'workflows');
|
||||
MotionChangeRecommendation.bindAll({'where': {'motion_version_id': {'==': motion.active_version}}}, $scope, 'change_recommendations');
|
||||
Motion.loadRelations(motion, 'agenda_item');
|
||||
$scope.$watch(function () {
|
||||
return Projector.lastModified();
|
||||
@ -853,7 +944,12 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
$scope.version = motion.active_version;
|
||||
$scope.isCollapsed = true;
|
||||
$scope.commentsFields = Config.get('motions_comments').value;
|
||||
|
||||
$scope.lineNumberMode = Config.get('motions_default_line_numbering').value;
|
||||
$scope.setLineNumberMode = function(mode) {
|
||||
$scope.lineNumberMode = mode;
|
||||
};
|
||||
|
||||
if (motion.parent_id) {
|
||||
Motion.bindOne(motion.parent_id, $scope, 'parent');
|
||||
}
|
||||
@ -891,32 +987,6 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
setHighlightOnProjector($scope.linesForProjector ? $scope.highlight : 0);
|
||||
};
|
||||
|
||||
$scope.makePDF = function() {
|
||||
var content = HTMLValidizer.validize(motion.getText($scope.version)) + HTMLValidizer.validize(motion.getReason($scope.version));
|
||||
var map = Function.prototype.call.bind([].map);
|
||||
var image_sources = map($(content).find("img"), function(element) {
|
||||
return element.getAttribute("src");
|
||||
});
|
||||
|
||||
$http.post('/core/encode_media/', JSON.stringify(image_sources)).success(function(data) {
|
||||
var converter = PdfMakeConverter.createInstance(data.images, data.fonts, pdfMake);
|
||||
var motionContentProvider = MotionContentProvider.createInstance(converter, motion, $scope, User, $http);
|
||||
var documentProvider = PdfMakeDocumentProvider.createInstance(motionContentProvider, data.defaultFont);
|
||||
var filename = gettextCatalog.getString("Motion") + "-" + motion.identifier + ".pdf";
|
||||
pdfMake.createPdf(documentProvider.getDocument()).download(filename);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
//make PDF for polls
|
||||
$scope.makePollPDF = function() {
|
||||
var id = motion.identifier.replace(" ", ""),
|
||||
title = motion.getTitle($scope.version),
|
||||
filename = gettextCatalog.getString("Motion") + "-" + id + "-" + gettextCatalog.getString("ballot-paper") + ".pdf",
|
||||
content = PollContentProvider.createInstance(title, id, gettextCatalog);
|
||||
pdfMake.createPdf(content).download(filename);
|
||||
};
|
||||
|
||||
// open edit dialog
|
||||
$scope.openDialog = function (motion) {
|
||||
if ($scope.inlineEditing.active) {
|
||||
@ -987,6 +1057,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
$scope.showVersion = function (version) {
|
||||
$scope.version = version.id;
|
||||
$scope.inlineEditing.setVersion(motion, version.id);
|
||||
$scope.createChangeRecommendation.setVersion(motion, version.id);
|
||||
};
|
||||
// permit specific version
|
||||
$scope.permitVersion = function (version) {
|
||||
@ -1022,6 +1093,59 @@ angular.module('OpenSlidesApp.motions.site', [
|
||||
// Inline editing functions
|
||||
$scope.inlineEditing = MotionInlineEditing;
|
||||
$scope.inlineEditing.init($scope, motion);
|
||||
|
||||
// Change recommendation creation functions
|
||||
$scope.createChangeRecommendation = ChangeRecommmendationCreate;
|
||||
$scope.createChangeRecommendation.init($scope, motion);
|
||||
|
||||
// Change recommendation viewing
|
||||
$scope.viewChangeRecommendations = ChangeRecommmendationView;
|
||||
$scope.viewChangeRecommendations.init($scope);
|
||||
|
||||
// PDF creating functions
|
||||
$scope.pdfExport = MotionPDFExport;
|
||||
$scope.pdfExport.init($scope);
|
||||
}
|
||||
])
|
||||
|
||||
.controller('ChangeRecommendationCreateCtrl', [
|
||||
'$scope',
|
||||
'Motion',
|
||||
'MotionChangeRecommendation',
|
||||
'ChangeRecommendationForm',
|
||||
'Config',
|
||||
'diffService',
|
||||
'motion',
|
||||
'version',
|
||||
'lineFrom',
|
||||
'lineTo',
|
||||
function($scope, Motion, MotionChangeRecommendation, ChangeRecommendationForm, Config, diffService, motion,
|
||||
version, lineFrom, lineTo) {
|
||||
$scope.alert = {};
|
||||
|
||||
var html = motion.getTextWithLineBreaks(version),
|
||||
fragment = diffService.htmlToFragment(html),
|
||||
lineData = diffService.extractRangeByLineNumbers(fragment, lineFrom, lineTo);
|
||||
|
||||
$scope.model = {
|
||||
text: lineData.outerContextStart + lineData.innerContextStart +
|
||||
lineData.html + lineData.innerContextEnd + lineData.outerContextEnd,
|
||||
line_from: lineFrom,
|
||||
line_to: lineTo,
|
||||
motion_version_id: version,
|
||||
type: 0
|
||||
};
|
||||
|
||||
// get all form fields
|
||||
$scope.formFields = ChangeRecommendationForm.getFormFields(lineFrom, lineTo);
|
||||
// save motion
|
||||
$scope.save = function (motion) {
|
||||
MotionChangeRecommendation.create(motion).then(
|
||||
function(success) {
|
||||
$scope.closeThisDialog();
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
])
|
||||
|
||||
|
@ -0,0 +1,17 @@
|
||||
<h1 ng-if="model.id" translate>Edit change recommendation</h1>
|
||||
<h1 ng-if="!model.id" translate>New change recommendation</h1>
|
||||
|
||||
<uib-alert ng-show="alert.show" type="{{ alert.type }}" ng-click="alert={}" close="alert={}">
|
||||
{{ alert.msg }}
|
||||
</uib-alert>
|
||||
|
||||
<form name="changeRecommendationForm" ng-submit="save(model)">
|
||||
<formly-form model="model" fields="formFields">
|
||||
<button type="submit" ng-disabled="changeRecommendation.$invalid" class="btn btn-primary" translate>
|
||||
Save
|
||||
</button>
|
||||
<button ng-click="closeThisDialog()" class="btn btn-default" translate>
|
||||
Cancel
|
||||
</button>
|
||||
</formly-form>
|
||||
</form>
|
@ -21,7 +21,7 @@
|
||||
<i class="fa fa-pencil"></i>
|
||||
</a>
|
||||
<!-- pdf -->
|
||||
<a ng-click="makePDF()" class="btn btn-default btn-sm">
|
||||
<a ng-click="pdfExport.createMotion()" class="btn btn-default btn-sm">
|
||||
<i class="fa fa-file-pdf-o fa-lg"></i>
|
||||
<translate>PDF</translate>
|
||||
</a>
|
||||
@ -182,7 +182,7 @@
|
||||
</button>
|
||||
|
||||
<!-- Print poll PDF -->
|
||||
<a os-perms="motions.can_manage" ng-click="makePollPDF()" class="btn btn-default btn-xs"
|
||||
<a os-perms="motions.can_manage" ng-click="pdfExport.createPoll()" class="btn btn-default btn-xs"
|
||||
title="{{ 'Print ballot paper' | translate }}">
|
||||
<i class="fa fa-file-pdf-o"></i>
|
||||
</a>
|
||||
@ -289,95 +289,49 @@
|
||||
|
||||
<div class="details">
|
||||
<div class="row">
|
||||
<div class="linenumber-toolbar">
|
||||
<!-- inline editing -->
|
||||
<div class="pull-right inline-editing-activator"
|
||||
ng-if="motion.isAllowed('update') && version == motion.getVersion(-1).id">
|
||||
<button ng-if="!inlineEditing.active" ng-click="inlineEditing.enable()" class="btn btn-sm btn-default">
|
||||
<i class="fa fa-pencil-square-o"></i>
|
||||
<translate>Inline editing</translate>
|
||||
</button>
|
||||
<button ng-if="inlineEditing.active" ng-click="inlineEditing.disable()" class="btn btn-sm btn-default">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
<translate>Inline editing</translate>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- line number mode -->
|
||||
<div class="line-number-setter {{ lineNumberMode }}">
|
||||
<span class="btn-group" data-toggle="buttons">
|
||||
<div class="btn btn-sm btn-default disabled">
|
||||
<i class="fa fa-list-ol" aria-hidden="true"></i>
|
||||
<translate>Line numbering</translate>:
|
||||
</div>
|
||||
<label class="btn btn-sm btn-default" ng-class="{active: (lineNumberMode == 'none')}"
|
||||
ng-click="lineNumberMode = '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="lineNumberMode = '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="lineNumberMode = 'outside'">
|
||||
<input type="radio" name="lineNumberMode" value="outside" ng-model="lineNumberMode"
|
||||
ng-checked="lineNumberMode == 'outside'">
|
||||
<translate>outside</translate>
|
||||
</label>
|
||||
</span>
|
||||
<!-- Motion toolbar -->
|
||||
<ng-include src="'static/templates/motions/motion-detail/toolbar.html'"></ng-include>
|
||||
|
||||
<!-- go to line number -->
|
||||
<span>
|
||||
<form class="input-group" style="max-width: 220px;" ng-if="lineNumberMode != 'none'" ng-submit="scrollToAndHighlight(gotoLinenumber)">
|
||||
<input type="number" class="form-control input-sm" ng-model="gotoLinenumber" placeholder="{{ 'Line' | translate }}"></input>
|
||||
<span class="input-group-btn">
|
||||
<button type="button" class="btn btn-sm btn-default btn-slim" ng-show="gotoLinenumber"
|
||||
ng-click="gotoLinenumber = ''; scrollToAndHighlight(0);">
|
||||
<i class="fa fa-times text-danger"></i>
|
||||
<div ng-class="{'col-sm-8': (lineNumberMode != 'outside'), 'col-sm-12': (lineNumberMode == 'outside')}">
|
||||
<div class="motion-text-holder">
|
||||
|
||||
<!-- Original view -->
|
||||
<ng-include src="'static/templates/motions/motion-detail/view-original.html'"></ng-include>
|
||||
|
||||
<!-- Diff View -->
|
||||
<ng-include src="'static/templates/motions/motion-detail/view-diff.html'"></ng-include>
|
||||
|
||||
<!-- Changed View -->
|
||||
<div ng-if="viewChangeRecommendations.mode == 'changed'">
|
||||
<div ng-bind-html="motion.getTextWithChangeRecommendations(version) | trusted"
|
||||
class="motion-text motion-text-changed line-numbers-{{ lineNumberMode }}"></div>
|
||||
|
||||
<div style="text-align: right;" ng-if="motion.state.versioning && (change_recommendations | filter:{motion_version_id:version}:true).length > 0">
|
||||
<button class="btn btn-default"
|
||||
ng-bootbox-confirm="{{ 'Do you want to create a new version of this motion based on this changes?' | translate }}"
|
||||
ng-bootbox-confirm-action="viewChangeRecommendations.newVersionIncludingChanges(motion, version, true)">
|
||||
<i class="fa fa-file-text"></i>
|
||||
<translate>New version on these changes</translate>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-sm btn-default">
|
||||
<i class="fa fa-share"></i>
|
||||
<translate>go</translate>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default" os-perms="core.can_manage_projector"
|
||||
ng-show="lineNumberMode != 'none' && motion.isProjected()" ng-click="toggleLinesForProjector()"
|
||||
uib-tooltip="{{ 'Show highlighted line also on projector.' | translate }}">
|
||||
<i class="fa" ng-class="linesForProjector ? 'fa-check-square-o' : 'fa-square-o'"></i>
|
||||
<i class="fa fa-video-camera"></i>
|
||||
</button>
|
||||
</span>
|
||||
</form>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-class="{'col-sm-8': (lineNumberMode != 'outside'), 'col-sm-12': (lineNumberMode == 'outside')}">
|
||||
|
||||
<div ng-if="motion.isAllowed('update') && version == motion.getVersion(-1).id">
|
||||
<div ng-show="inlineEditing.active">
|
||||
<div ui-tinymce="inlineEditing.tinymceOptions" ng-model="inlineEditing.lineBrokenText"
|
||||
class="motion-text line-numbers-{{ lineNumberMode }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="!inlineEditing.active" ng-bind-html="motion.getTextWithLineBreaks(version, highlight) | trusted"
|
||||
class="motion-text line-numbers-{{ lineNumberMode }}"></div>
|
||||
|
||||
<div class="motion-save-toolbar" ng-class="{ 'visible': (inlineEditing.changed && inlineEditing.active) }">
|
||||
<div class="changed-hint" translate>The text has been changed.</div>
|
||||
<button type="button" ng-click="inlineEditing.save()" class="btn btn-primary">Save</button>
|
||||
<label ng-if="motion.state.versioning && config('motions_allow_disable_versioning')">
|
||||
<input type="checkbox" ng-model="inlineEditing.trivialChange" value="1">
|
||||
<span translate>Trivial change</span>
|
||||
</label>
|
||||
<!-- Agreed View -->
|
||||
<div ng-if="viewChangeRecommendations.mode == 'agreed'">
|
||||
<div ng-bind-html="motion.getTextWithAcceptedChangeRecommendations(version) | trusted"
|
||||
class="motion-text motion-text-changed line-numbers-{{ lineNumberMode }}"></div>
|
||||
|
||||
<div style="text-align: right;" ng-if="motion.state.versioning && (change_recommendations | filter:{motion_version_id:version}:true).length > 0">
|
||||
<button class="btn btn-default"
|
||||
ng-bootbox-confirm="{{ 'Do you want to create a new version of this motion based on this changes?' | translate }}"
|
||||
ng-bootbox-confirm-action="viewChangeRecommendations.newVersionIncludingChanges(motion, version, false)">
|
||||
<i class="fa fa-file-text"></i>
|
||||
<translate>New version on these changes</translate>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="!(motion.isAllowed('update') && version == motion.getVersion(-1).id)">
|
||||
<div ng-bind-html="motion.getTextWithLineBreaks(version, highlight) | trusted"
|
||||
class="motion-text line-numbers-{{ lineNumberMode }}"></div>
|
||||
</div>
|
||||
|
||||
<!-- reason -->
|
||||
<div ng-if="motion.getReason(version) != ''">
|
||||
|
@ -0,0 +1,30 @@
|
||||
<!-- A summary of all changes -->
|
||||
<section class="change-recommendation-overview">
|
||||
<h2>
|
||||
<translate>Change recommendation summary</translate>
|
||||
</h2>
|
||||
<ul ng-if="change_recommendations.length > 0">
|
||||
<li ng-repeat="change in (changes = (change_recommendations | filter:{motion_version_id:version}:true | orderBy: 'line_from')) "
|
||||
ng-click="viewChangeRecommendations.scrollToDiffBox(change.id)">
|
||||
<span ng-if="change.line_from >= change.line_to - 1" class="line-number">
|
||||
Line {{ change.line_from }}:
|
||||
</span>
|
||||
<span ng-if="change.line_from < change.line_to - 1" class="line-number">
|
||||
Line {{ change.line_from }} - {{ change.line_to - 1 }}:
|
||||
</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>
|
||||
<span class="status">
|
||||
<translate ng-if="change.status == 0">Suggested</translate>
|
||||
<translate ng-if="change.status == 1">Accepted</translate>
|
||||
<translate ng-if="change.status == 2">Rejected</translate>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div ng-if="change_recommendations.length == 0" class="no-changes">
|
||||
<translate>No change recommendations yet</translate>
|
||||
</div>
|
||||
</section>
|
@ -0,0 +1,116 @@
|
||||
<div class="motion-toolbar">
|
||||
<!-- 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="inlineEditing.enable()"
|
||||
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="inlineEditing.disable()"
|
||||
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
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-left {{ lineNumberMode }}">
|
||||
|
||||
<!-- line number mode -->
|
||||
<div class="btn-group" 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>
|
||||
|
||||
<!-- go to line number -->
|
||||
<div class="goto-line-number">
|
||||
<form class="input-group" ng-if="viewChangeRecommendations.mode == 'original' && lineNumberMode != 'none'"
|
||||
ng-submit="scrollToAndHighlight(gotoLinenumber)">
|
||||
<input type="number" class="form-control input-sm" ng-model="gotoLinenumber"
|
||||
placeholder="{{ 'Line' | translate }}"/>
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-sm btn-default btn-slim" ng-show="gotoLinenumber"
|
||||
ng-click="gotoLinenumber = ''; scrollToAndHighlight(0);">
|
||||
<i class="fa fa-times text-danger"></i>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-sm btn-default">
|
||||
<i class="fa fa-share"></i>
|
||||
<translate>go</translate>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default" os-perms="core.can_manage_projector"
|
||||
ng-show="lineNumberMode != 'none' && motion.isProjected()"
|
||||
ng-click="toggleLinesForProjector()"
|
||||
uib-tooltip="{{ 'Show highlighted line also on projector.' | translate }}">
|
||||
<i class="fa" ng-class="linesForProjector ? 'fa-check-square-o' : 'fa-square-o'"></i>
|
||||
<i class="fa fa-video-camera"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- View Modes (Original, Diff, Changed) -->
|
||||
<div class="motion-toolbar" ng-if="change_recommendations.length > 0">
|
||||
<div class="toolbar-left">
|
||||
<div class="btn-group" data-toggle="buttons">
|
||||
<span class="btn btn-sm btn-default disabled">
|
||||
<i class="fa fa-edit" aria-hidden="true"></i>
|
||||
<translate>Change recommendations</translate>:
|
||||
</span>
|
||||
<label class="btn btn-sm btn-default" ng-class="{active: (viewChangeRecommendations.mode == 'original')}"
|
||||
ng-click="viewChangeRecommendations.mode = 'original';">
|
||||
<input type="radio" name="viewChangeRecommendations.mode" value="none"
|
||||
ng-model="viewChangeRecommendations.mode"
|
||||
ng-checked="viewChangeRecommendations.mode == 'original'">
|
||||
<translate>Original</translate>
|
||||
|
||||
</label>
|
||||
<label class="btn btn-sm btn-default" ng-class="{active: (viewChangeRecommendations.mode == 'changed')}"
|
||||
ng-click="viewChangeRecommendations.mode = 'changed'">
|
||||
<input type="radio" name="viewChangeRecommendations.mode" value="changed"
|
||||
ng-model="viewChangeRecommendations.mode"
|
||||
ng-checked="viewChangeRecommendations.mode == 'changed'">
|
||||
<translate>Changed</translate>
|
||||
</label>
|
||||
<label class="btn btn-sm btn-default" ng-class="{active: (viewChangeRecommendations.mode == 'diff')}"
|
||||
ng-click="viewChangeRecommendations.mode = 'diff'">
|
||||
<input type="radio" name="viewChangeRecommendations.mode" value="diff" ng-model="lineNumberMode"
|
||||
ng-checked="viewChangeRecommendations.mode == 'diff'">
|
||||
<translate>Diff</translate>
|
||||
</label>
|
||||
<label class="btn btn-sm btn-default" ng-class="{active: (viewChangeRecommendations.mode == 'agreed')}"
|
||||
ng-click="viewChangeRecommendations.mode = 'agreed'">
|
||||
<input type="radio" name="viewChangeRecommendations.mode" value="agreed"
|
||||
ng-model="viewChangeRecommendations.mode"
|
||||
ng-checked="viewChangeRecommendations.mode == 'agreed'">
|
||||
<translate>Agreed</translate>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,54 @@
|
||||
<div ng-if="viewChangeRecommendations.mode == 'diff'">
|
||||
<ng-include src="'static/templates/motions/motion-detail/change-summary.html'"></ng-include>
|
||||
|
||||
<!-- 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 class="motion-text original-text line-numbers-{{ lineNumberMode }}"
|
||||
ng-bind-html="motion.getTextBetweenChangeRecommendations(version, changes[$index - 1], change) | trusted"></div>
|
||||
|
||||
<div class="diff-box 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.status == 0)}"
|
||||
title="{{ 'Suggested' | translate }}" ng-click="change.status = 0; change.saveStatus();">
|
||||
<input type="radio" name="changeRecommendationStatus[{{ change.id }}]" value="0"
|
||||
ng-change="change.saveStatus()" ng-model="change.status" ng-checked="change.status == 0">
|
||||
<i class="fa fa-question"></i>
|
||||
</label>
|
||||
<label class="btn btn-sm btn-default" ng-class="{active: (change.status == 1)}"
|
||||
title="{{ 'Accepted' | translate }}" ng-click="change.status = 1; change.saveStatus();">
|
||||
<input type="radio" name="changeRecommendationStatus[{{ change.id }}]" value="1"
|
||||
ng-change="change.saveStatus()" ng-model="change.status" ng-checked="change.status == 1">
|
||||
<i class="fa fa-thumbs-up"></i>
|
||||
</label>
|
||||
<label class="btn btn-sm btn-default" ng-class="{active: (change.status == 2)}"
|
||||
title="{{ 'Rejected' | translate }}" ng-click="change.status = 2; change.saveStatus();">
|
||||
<input type="radio" name="changeRecommendationStatus[{{ change.id }}]" value="2"
|
||||
ng-change="change.saveStatus()" ng-model="change.status" ng-checked="change.status == 2">
|
||||
<i class="fa fa-thumbs-down"></i>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-default btn-sm pull-right btn-delete"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to delete this change recommendation?' | translate }}"
|
||||
ng-bootbox-confirm-action="viewChangeRecommendations.delete(change.id)"
|
||||
title="{{ 'Delete' | translate }}">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="status-row" ng-if="!motion.isAllowed('can_manage')">
|
||||
<span ng-if="change.status == 0"><translate>Suggested</translate></span>
|
||||
<span ng-if="change.status == 1"><translate>Accepted</translate></span>
|
||||
<span ng-if="change.status == 2"><translate>Rejected</translate></span>
|
||||
</div>
|
||||
|
||||
<div class="motion-text motion-text-diff line-numbers-{{ lineNumberMode }}"
|
||||
ng-bind-html="change.format(motion, version) | trusted"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="motion-text original-text line-numbers-{{ lineNumberMode }}"
|
||||
ng-bind-html="motion.getTextRemainderAfterLastChangeRecommendation(version, changes) | trusted"></div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,46 @@
|
||||
<!-- Original view, Inline Editing is possible -->
|
||||
<div ng-if="viewChangeRecommendations.mode == 'original' && motion.isAllowed('update') && version == motion.getVersion(-1).id">
|
||||
<div ng-show="inlineEditing.active">
|
||||
<div ui-tinymce="inlineEditing.tinymceOptions" ng-model="inlineEditing.lineBrokenText"
|
||||
class="motion-text line-numbers-{{ lineNumberMode }}"></div>
|
||||
</div>
|
||||
<div ng-show="!inlineEditing.active" ng-bind-html="motion.getTextWithLineBreaks(version, highlight) | trusted"
|
||||
class="motion-text motion-text-original line-numbers-{{ lineNumberMode }}"></div>
|
||||
|
||||
<div class="motion-save-toolbar" ng-class="{ 'visible': (inlineEditing.changed && inlineEditing.active) }">
|
||||
<div class="changed-hint" translate>The text has been changed.</div>
|
||||
<button type="button" ng-click="inlineEditing.save()" class="btn btn-primary">Save</button>
|
||||
<label ng-if="motion.state.versioning && config('motions_allow_disable_versioning')">
|
||||
<input type="checkbox" ng-model="inlineEditing.trivialChange" value="1">
|
||||
<span translate>Trivial change</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Original view, Inline Editing is NOT possible -->
|
||||
<div ng-if="viewChangeRecommendations.mode == 'original' && !(motion.isAllowed('update') && version == motion.getVersion(-1).id)">
|
||||
<div ng-bind-html="motion.getTextWithLineBreaks(version, highlight) | trusted"
|
||||
class="motion-text motion-text-original line-numbers-{{ lineNumberMode }}"></div>
|
||||
</div>
|
||||
|
||||
<!-- Original view, Change list -->
|
||||
<ul ng-if="viewChangeRecommendations.mode == 'original'" ng-show="lineNumberMode != 'none'"
|
||||
class="change-recommendation-list">
|
||||
<li ng-repeat="change in change_recommendations | filter:{motion_version_id:version}:true"
|
||||
ng-class="['replace', 'insert', 'delete'][change.getType(motion.getVersion(version).text)]"
|
||||
ng-click="viewChangeRecommendations.scrollToDiffBox(change.id)"
|
||||
data-line-from="{{ change.line_from }}" data-line-to="{{ change.line_to}}"
|
||||
title="{{ change.getTitle(motion.getVersion(version).text) }}">
|
||||
<div class="tooltip">
|
||||
<div class="text" ng-bind-html="change.text || trusted"></div>
|
||||
|
||||
<!-- delete change recommendation -->
|
||||
<button class="btn btn-default btn-xs"
|
||||
ng-bootbox-confirm="{{ 'Are you sure you want to delete this change recommendation?' | translate }}"
|
||||
ng-bootbox-confirm-action="viewChangeRecommendations.delete(change.id)"
|
||||
title="{{ 'Delete' | translate }}">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
@ -24,12 +24,14 @@ from openslides.utils.views import APIView, PDFView, SingleObjectMixin
|
||||
from .access_permissions import (
|
||||
CategoryAccessPermissions,
|
||||
MotionAccessPermissions,
|
||||
MotionChangeRecommendationAccessPermissions,
|
||||
WorkflowAccessPermissions,
|
||||
)
|
||||
from .exceptions import WorkflowError
|
||||
from .models import (
|
||||
Category,
|
||||
Motion,
|
||||
MotionChangeRecommendation,
|
||||
MotionPoll,
|
||||
MotionVersion,
|
||||
State,
|
||||
@ -341,6 +343,31 @@ class MotionPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet):
|
||||
self.request.user.has_perm('motions.can_manage'))
|
||||
|
||||
|
||||
class MotionChangeRecommendationViewSet(ModelViewSet):
|
||||
"""
|
||||
API endpoint for motion change recommendations.
|
||||
|
||||
There are the following views: metadata, list, retrieve, create,
|
||||
partial_update, update and destroy.
|
||||
"""
|
||||
access_permissions = MotionChangeRecommendationAccessPermissions()
|
||||
queryset = MotionChangeRecommendation.objects.all()
|
||||
|
||||
def check_view_permissions(self):
|
||||
"""
|
||||
Returns True if the user has required permissions.
|
||||
"""
|
||||
if self.action in ('list', 'retrieve'):
|
||||
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||
elif self.action == 'metadata':
|
||||
result = self.request.user.has_perm('motions.can_see')
|
||||
elif self.action in ('create', 'destroy', 'partial_update', 'update'):
|
||||
result = self.request.user.has_perm('motions.can_manage')
|
||||
else:
|
||||
result = False
|
||||
return result
|
||||
|
||||
|
||||
class CategoryViewSet(ModelViewSet):
|
||||
"""
|
||||
API endpoint for categories.
|
||||
|
@ -2,7 +2,7 @@ describe('linenumbering', function () {
|
||||
|
||||
beforeEach(module('OpenSlidesApp.motions.diff'));
|
||||
|
||||
var diffService, baseHtmlDom1, baseHtmlDom2,
|
||||
var diffService, baseHtmlDom1, baseHtmlDom2, baseHtmlDom3,
|
||||
brMarkup = function (no) {
|
||||
return '<br class="os-line-break">' +
|
||||
'<span class="os-line-number line-number-' + no + '" data-line-number="' + no + '" contenteditable="false"> </span>';
|
||||
@ -15,7 +15,7 @@ describe('linenumbering', function () {
|
||||
diffService = _diffService_;
|
||||
|
||||
baseHtmlDom1 = diffService.htmlToFragment('<p>' +
|
||||
noMarkup(1) + 'Line 1 ' + brMarkup(2) + 'Line 2' +
|
||||
noMarkup(1) + 'Line 1 ' + brMarkup(2) + 'Line 2 ' +
|
||||
brMarkup(3) + 'Line <strong>3<br>' + noMarkup(4) + 'Line 4 ' + brMarkup(5) + 'Line</strong> 5</p>' +
|
||||
'<ul class="ul-class">' +
|
||||
'<li class="li-class">' + noMarkup(6) + 'Line 6 ' + brMarkup(7) + 'Line 7' + '</li>' +
|
||||
@ -26,26 +26,36 @@ describe('linenumbering', function () {
|
||||
'</ul>' +
|
||||
'<p>' + noMarkup(10) + 'Line 10 ' + brMarkup(11) + 'Line 11</p>');
|
||||
|
||||
baseHtmlDom2 = diffService.htmlToFragment('<p><span class="os-line-number line-number-1" data-line-number="1" contenteditable="false"> </span>Single text line</p>\
|
||||
<p><span class="os-line-number line-number-2" data-line-number="2" contenteditable="false"> </span>sdfsdfsdfsdf dsfsdfsdfdsflkewjrl ksjfl ksdjf klnlkjBavaria ipsum dolor sit amet Biazelt Auffisteign <br class="os-line-break"><span class="os-line-number line-number-3" data-line-number="3" contenteditable="false"> </span>Schorsch mim Radl foahn Ohrwaschl Steckerleis wann griagd ma nacha wos z’dringa glacht Mamalad, <br class="os-line-break">' +
|
||||
'<span class="os-line-number line-number-4" data-line-number="4" contenteditable="false"> </span>muass? I bin a woschechta Bayer sowos oamoi und sei und glei wirds no fui lustiga: Jo mei khkhis des <br class="os-line-break"><span class="os-line-number line-number-5" data-line-number="5" contenteditable="false"> </span>schee middn ognudelt, Trachtnhuat Biawambn gscheid: Griasd eich midnand etza nix Gwiass woass ma ned <br class="os-line-break">' +
|
||||
'<span class="os-line-number line-number-6" data-line-number="6" contenteditable="false"> </span>owe. Dahoam gscheckate middn Spuiratz des is a gmahde Wiesn. Des is schee so Obazda san da, Haferl <br class="os-line-break"><span class="os-line-number line-number-7" data-line-number="7" contenteditable="false"> </span>pfenningguat schoo griasd eich midnand.</p>\
|
||||
baseHtmlDom2 = diffService.htmlToFragment('<p>' + noMarkup(1) + 'Single text line</p>\
|
||||
<p>' + noMarkup(2) + 'sdfsdfsdfsdf dsfsdfsdfdsflkewjrl ksjfl ksdjf klnlkjBavaria ipsum dolor sit amet Biazelt Auffisteign ' + brMarkup(3) + 'Schorsch mim Radl foahn Ohrwaschl Steckerleis wann griagd ma nacha wos z’dringa glacht Mamalad, ' +
|
||||
brMarkup(4) + 'muass? I bin a woschechta Bayer sowos oamoi und sei und glei wirds no fui lustiga: Jo mei khkhis des ' + brMarkup(5) + 'schee middn ognudelt, Trachtnhuat Biawambn gscheid: Griasd eich midnand etza nix Gwiass woass ma ned ' +
|
||||
brMarkup(6) + 'owe. Dahoam gscheckate middn Spuiratz des is a gmahde Wiesn. Des is schee so Obazda san da, Haferl ' + brMarkup(7) + 'pfenningguat schoo griasd eich midnand.</p>\
|
||||
<ul>\
|
||||
<li><span class="os-line-number line-number-8" data-line-number="8" contenteditable="false"> </span>Auffi Gamsbart nimma de Sepp Ledahosn Ohrwaschl um Godds wujn Wiesn Deandlgwand Mongdratzal! Jo <br class="os-line-break"><span class="os-line-number line-number-9" data-line-number="9" contenteditable="false"> </span>leck mi Mamalad i daad mechad?</li>\
|
||||
<li><span class="os-line-number line-number-10" data-line-number="10" contenteditable="false"> </span>Do nackata Wurscht i hob di narrisch gean, Diandldrahn Deandlgwand vui huift vui woaß?</li>\
|
||||
<li><span class="os-line-number line-number-11" data-line-number="11" contenteditable="false"> </span>Ned Mamalad auffi i bin a woschechta Bayer greaßt eich nachad, umananda gwiss nia need <br class="os-line-break"><span class="os-line-number line-number-12" data-line-number="12" contenteditable="false"> </span>Weiznglasl.</li>\
|
||||
<li><span class="os-line-number line-number-13" data-line-number="13" contenteditable="false"> </span>Woibbadinga noch da Giasinga Heiwog Biazelt mechad mim Spuiratz, soi zwoa.</li>\
|
||||
<li>' + noMarkup(8) + 'Auffi Gamsbart nimma de Sepp Ledahosn Ohrwaschl um Godds wujn Wiesn Deandlgwand Mongdratzal! Jo ' + brMarkup(9) + 'leck mi Mamalad i daad mechad?</li>\
|
||||
<li>' + noMarkup(10) + 'Do nackata Wurscht i hob di narrisch gean, Diandldrahn Deandlgwand vui huift vui woaß?</li>\
|
||||
<li>' + noMarkup(11) + 'Ned Mamalad auffi i bin a woschechta Bayer greaßt eich nachad, umananda gwiss nia need ' + brMarkup(12) + 'Weiznglasl.</li>\
|
||||
<li>' + noMarkup(13) + 'Woibbadinga noch da Giasinga Heiwog Biazelt mechad mim Spuiratz, soi zwoa.</li>\
|
||||
</ul>\
|
||||
<p><span class="os-line-number line-number-14" data-line-number="14" contenteditable="false"> </span>I waar soweid Blosmusi es nomoi. Broadwurschtbudn des is a gmahde Wiesn Kirwa mogsd a Bussal <br class="os-line-break"><span class="os-line-number line-number-15" data-line-number="15" contenteditable="false"> </span>Guglhupf schüds nei. Luja i moan oiwei Baamwach Watschnbaam, wiavui baddscher! Biakriagal a fescha <br class="os-line-break">' +
|
||||
'<span class="os-line-number line-number-16" data-line-number="16" contenteditable="false"> </span>1Bua Semmlkneedl iabaroi oba um Godds wujn Ledahosn wui Greichats. Geh um Godds wujn luja heid <br class="os-line-break"><span class="os-line-number line-number-17" data-line-number="17" contenteditable="false"> </span>greaßt eich nachad woaß Breihaus eam! De om auf’n Gipfe auf gehds beim Schichtl mehra Baamwach a <br class="os-line-break"><span class="os-line-number line-number-18" data-line-number="18" contenteditable="false"> </span>bissal wos gehd ollaweil gscheid:</p>\
|
||||
<p>' + noMarkup(14) + 'I waar soweid Blosmusi es nomoi. Broadwurschtbudn des is a gmahde Wiesn Kirwa mogsd a Bussal ' + brMarkup(15) + 'Guglhupf schüds nei. Luja i moan oiwei Baamwach Watschnbaam, wiavui baddscher! Biakriagal a fescha ' +
|
||||
brMarkup(16) + '1Bua Semmlkneedl iabaroi oba um Godds wujn Ledahosn wui Greichats. Geh um Godds wujn luja heid ' + brMarkup(17) + 'greaßt eich nachad woaß Breihaus eam! De om auf’n Gipfe auf gehds beim Schichtl mehra Baamwach a ' + brMarkup(18) + 'bissal wos gehd ollaweil gscheid:</p>\
|
||||
<blockquote>\
|
||||
<p><span class="os-line-number line-number-19" data-line-number="19" contenteditable="false"> </span>Scheans Schdarmbeaga See i hob di narrisch gean i jo mei is des schee! Nia eam <br class="os-line-break"><span class="os-line-number line-number-20" data-line-number="20" contenteditable="false"> </span>hod vasteh i sog ja nix, i red ja bloß sammawiedaguad, umma eana obandeln! Zwoa <br class="os-line-break"><span class="os-line-number line-number-21" data-line-number="21" contenteditable="false"> </span>jo mei scheans amoi, san und hoggd Milli barfuaßat gscheit. Foidweg vui huift <br class="os-line-break">' +
|
||||
'<span class="os-line-number line-number-22" data-line-number="22" contenteditable="false"> </span>vui singan, mehra Biakriagal om auf’n Gipfe! Ozapfa sodala Charivari greaßt eich <br class="os-line-break"><span class="os-line-number line-number-23" data-line-number="23" contenteditable="false"> </span>nachad Broadwurschtbudn do middn liberalitas Bavariae sowos Leonhardifahrt:</p>\
|
||||
<p>' + noMarkup(19) + 'Scheans Schdarmbeaga See i hob di narrisch gean i jo mei is des schee! Nia eam ' + brMarkup(20) + 'hod vasteh i sog ja nix, i red ja bloß sammawiedaguad, umma eana obandeln! Zwoa ' + brMarkup(21) + 'jo mei scheans amoi, san und hoggd Milli barfuaßat gscheit. Foidweg vui huift ' +
|
||||
brMarkup(22) + 'vui singan, mehra Biakriagal om auf’n Gipfe! Ozapfa sodala Charivari greaßt eich ' + brMarkup(23) + 'nachad Broadwurschtbudn do middn liberalitas Bavariae sowos Leonhardifahrt:</p>\
|
||||
</blockquote>\
|
||||
<p><span class="os-line-number line-number-24" data-line-number="24" contenteditable="false"> </span>Wui helfgod Wiesn, ognudelt schaugn: Dahoam gelbe Rüam Schneid singan wo hi sauba i moan scho aa no <br class="os-line-break"><span class="os-line-number line-number-25" data-line-number="25" contenteditable="false"> </span>a Maß a Maß und no a Maß nimma. Is umananda a ganze Hoiwe zwoa, Schneid. Vui huift vui Brodzeid kumm <br class="os-line-break">' +
|
||||
'<span class="os-line-number line-number-26" data-line-number="26" contenteditable="false"> </span>geh naa i daad vo de allerweil, gor. Woaß wia Gams, damischa. A ganze Hoiwe Ohrwaschl Greichats <br class="os-line-break"><span class="os-line-number line-number-27" data-line-number="27" contenteditable="false"> </span>iabaroi Prosd Engelgwand nix Reiwadatschi.Weibaleid ognudelt Ledahosn noch da Giasinga Heiwog i daad <br class="os-line-break">' +
|
||||
'<span class="os-line-number line-number-28" data-line-number="28" contenteditable="false"> </span>Almrausch, Ewig und drei Dog nackata wea ko, dea ko. Meidromml Graudwiggal nois dei, nackata. No <br class="os-line-break"><span class="os-line-number line-number-29" data-line-number="29" contenteditable="false"> </span>Diandldrahn nix Gwiass woass ma ned hod boarischer: Samma sammawiedaguad wos, i hoam Brodzeid. Jo <br class="os-line-break">' +
|
||||
'<span class="os-line-number line-number-30" data-line-number="30" contenteditable="false"> </span>mei Sepp Gaudi, is ma Wuascht do Hendl Xaver Prosd eana an a bravs. Sauwedda an Brezn, abfieseln.</p>');
|
||||
<p>' + noMarkup(24) + 'Wui helfgod Wiesn, ognudelt schaugn: Dahoam gelbe Rüam Schneid singan wo hi sauba i moan scho aa no ' + brMarkup(25) + 'a Maß a Maß und no a Maß nimma. Is umananda a ganze Hoiwe zwoa, Schneid. Vui huift vui Brodzeid kumm ' +
|
||||
brMarkup(26) + 'geh naa i daad vo de allerweil, gor. Woaß wia Gams, damischa. A ganze Hoiwe Ohrwaschl Greichats ' + brMarkup(27) + 'iabaroi Prosd Engelgwand nix Reiwadatschi.Weibaleid ognudelt Ledahosn noch da Giasinga Heiwog i daad ' +
|
||||
brMarkup(28) + 'Almrausch, Ewig und drei Dog nackata wea ko, dea ko. Meidromml Graudwiggal nois dei, nackata. No ' + brMarkup(29) + 'Diandldrahn nix Gwiass woass ma ned hod boarischer: Samma sammawiedaguad wos, i hoam Brodzeid. Jo ' +
|
||||
brMarkup(30) + 'mei Sepp Gaudi, is ma Wuascht do Hendl Xaver Prosd eana an a bravs. Sauwedda an Brezn, abfieseln.</p>');
|
||||
|
||||
baseHtmlDom3 = diffService.htmlToFragment('<ol>' +
|
||||
'<li>' + noMarkup(1) + 'Line 1</li>' +
|
||||
'<li>' + noMarkup(2) + 'Line 2</li>' +
|
||||
'<li><ol>' +
|
||||
'<li>' + noMarkup(3) + 'Line 3.1</li>' +
|
||||
'<li>' + noMarkup(4) + 'Line 3.2</li>' +
|
||||
'<li>' + noMarkup(5) + 'Line 3.3</li>' +
|
||||
'</ol></li>' +
|
||||
'<li>' + noMarkup(6) + ' Line 4</li></ol>');
|
||||
|
||||
diffService._insertInternalLineMarkers(baseHtmlDom1);
|
||||
diffService._insertInternalLineMarkers(baseHtmlDom2);
|
||||
@ -131,7 +141,7 @@ describe('linenumbering', function () {
|
||||
});
|
||||
|
||||
it('extracts lines from a more complex example', function () {
|
||||
var diff = diffService.extractRangeByLineNumbers(baseHtmlDom2, 6, 11, true);
|
||||
var diff = diffService.extractRangeByLineNumbers(baseHtmlDom2, 6, 11);
|
||||
|
||||
expect(diff.html).toBe('owe. Dahoam gscheckate middn Spuiratz des is a gmahde Wiesn. Des is schee so Obazda san da, Haferl pfenningguat schoo griasd eich midnand.</P><UL><LI>Auffi Gamsbart nimma de Sepp Ledahosn Ohrwaschl um Godds wujn Wiesn Deandlgwand Mongdratzal! Jo leck mi Mamalad i daad mechad?</LI><LI>Do nackata Wurscht i hob di narrisch gean, Diandldrahn Deandlgwand vui huift vui woaß?</LI>');
|
||||
expect(diff.ancestor.nodeName).toBe('#document-fragment');
|
||||
@ -143,20 +153,135 @@ describe('linenumbering', function () {
|
||||
expect(diff.followingHtmlStartSnippet).toBe('<UL>');
|
||||
});
|
||||
|
||||
it('extracts the end of a section', function () {
|
||||
var diff = diffService.extractRangeByLineNumbers(baseHtmlDom2, 29, null);
|
||||
|
||||
expect(diff.html).toBe('Diandldrahn nix Gwiass woass ma ned hod boarischer: Samma sammawiedaguad wos, i hoam Brodzeid. Jo mei Sepp Gaudi, is ma Wuascht do Hendl Xaver Prosd eana an a bravs. Sauwedda an Brezn, abfieseln.</P>');
|
||||
expect(diff.ancestor.nodeName).toBe('#document-fragment');
|
||||
expect(diff.outerContextStart).toBe('');
|
||||
expect(diff.outerContextEnd).toBe('');
|
||||
expect(diff.innerContextStart).toBe('<P>');
|
||||
expect(diff.innerContextEnd).toBe('');
|
||||
expect(diff.previousHtmlEndSnippet).toBe('</P>');
|
||||
expect(diff.followingHtml).toBe('');
|
||||
expect(diff.followingHtmlStartSnippet).toBe('');
|
||||
});
|
||||
|
||||
it('preserves the numbering of OLs (1)', function () {
|
||||
var diff = diffService.extractRangeByLineNumbers(baseHtmlDom3, 5, 7, true);
|
||||
|
||||
expect(diff.html).toBe('<LI>Line 3.3</LI></OL></LI><LI> Line 4</LI></OL>');
|
||||
expect(diff.ancestor.nodeName).toBe('#document-fragment');
|
||||
expect(diff.innerContextStart).toBe('<OL start="3"><LI><OL start="3">');
|
||||
expect(diff.innerContextEnd).toBe('');
|
||||
expect(diff.previousHtmlEndSnippet).toBe('</OL></LI></OL>');
|
||||
});
|
||||
|
||||
it('preserves the numbering of OLs (2)', function () {
|
||||
var diff = diffService.extractRangeByLineNumbers(baseHtmlDom3, 3, 5, true);
|
||||
|
||||
expect(diff.html).toBe('<LI><OL><LI>Line 3.1</LI><LI>Line 3.2</LI>');
|
||||
expect(diff.ancestor.nodeName).toBe('OL');
|
||||
expect(diff.outerContextStart).toBe('<OL start="3">');
|
||||
expect(diff.outerContextEnd).toBe('</OL>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('merging lines into the original motion', function () {
|
||||
describe('merging two sections', function () {
|
||||
it('merges OLs recursively, ignoring whitespaces between OL and LI', function () {
|
||||
var node1 = document.createElement('DIV');
|
||||
node1.innerHTML = '<OL><LI><OL><LI>Punkt 4.1</LI><TEMPLATE></TEMPLATE></OL></LI> </OL>';
|
||||
var node2 = document.createElement('DIV');
|
||||
node2.innerHTML = '<OL> <LI>\
|
||||
<OL start="2">\
|
||||
<LI>Punkt 4.2</LI>\
|
||||
<LI>Punkt 4.3</LI>\
|
||||
</OL></LI></OL>';
|
||||
var out = diffService._replaceLinesMergeNodeArrays([node1.childNodes[0]], [node2.childNodes[0]]);
|
||||
expect(out[0], '<ol><li><ol><li>Punkt 4.1</li><template></template><li>Punkt 4.2</li><li>Punkt 4.3</li></ol></li></ol>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('replacing lines in the original motion', function () {
|
||||
it('replaces LIs by a P', function () {
|
||||
var merged = diffService.replaceLines(baseHtmlDom1, '<p>Replaced a UL by a P</p>', 6, 9);
|
||||
expect(merged).toBe('<P>Line 1 Line 2Line <STRONG>3<BR>Line 4 Line</STRONG> 5</P><P>Replaced a UL by a P</P><UL class="ul-class"><LI class="li-class"><UL><LI>Level 2 LI 9</LI></UL></LI></UL><P>Line 10 Line 11</P>');
|
||||
expect(merged).toBe('<P>Line 1 Line 2 Line <STRONG>3<BR>Line 4 Line</STRONG> 5</P><P>Replaced a UL by a P</P><UL class="ul-class"><LI class="li-class"><UL><LI>Level 2 LI 9</LI></UL></LI></UL><P>Line 10 Line 11</P>');
|
||||
});
|
||||
/*
|
||||
|
||||
it('replaces LIs by another LI', function () {
|
||||
var merged = diffService.replaceLines(baseHtmlDom1, '<UL class="ul-class"><LI>A new LI</LI></UL>', 6, 9);
|
||||
expect(merged).toBe('');
|
||||
expect(merged).toBe('<P>Line 1 Line 2 Line <STRONG>3<BR>Line 4 Line</STRONG> 5</P><UL class="ul-class"><LI>A new LI<UL><LI>Level 2 LI 9</LI></UL></LI></UL><P>Line 10 Line 11</P>');
|
||||
});
|
||||
|
||||
it('breaks up a paragraph into two', function() {
|
||||
var merged = diffService.replaceLines(baseHtmlDom1, '<P>Replaced Line 10</P><P>Inserted Line 11 </P>', 10, 11);
|
||||
expect(merged).toBe('<P>Line 1 Line 2 Line <STRONG>3<BR>Line 4 Line</STRONG> 5</P><UL class="ul-class"><LI class="li-class">Line 6 Line 7</LI><LI class="li-class"><UL><LI>Level 2 LI 8</LI><LI>Level 2 LI 9</LI></UL></LI></UL><P>Replaced Line 10</P><P>Inserted Line 11 Line 11</P>');
|
||||
});
|
||||
*/
|
||||
|
||||
});
|
||||
|
||||
describe('detecting the type of change', function() {
|
||||
it('detects a simple insertion', function () {
|
||||
var htmlBefore = '<p>Test 1</p>',
|
||||
htmlAfter = '<p>Test 1 Test 2</p>' + "\n" + '<p>Test 3</p>';
|
||||
var calculatedType = diffService.detectReplacementType(htmlBefore, htmlAfter);
|
||||
expect(calculatedType).toBe(diffService.TYPE_INSERTION);
|
||||
});
|
||||
|
||||
it('detects a simple insertion, ignoring case of tags', function () {
|
||||
var htmlBefore = '<p>Test 1</p>',
|
||||
htmlAfter = '<P>Test 1 Test 2</P>' + "\n" + '<P>Test 3</P>';
|
||||
var calculatedType = diffService.detectReplacementType(htmlBefore, htmlAfter);
|
||||
expect(calculatedType).toBe(diffService.TYPE_INSERTION);
|
||||
});
|
||||
|
||||
it('detects a simple insertion, ignoring trailing whitespaces', function () {
|
||||
var htmlBefore = '<P>Lorem ipsum dolor sit amet, sed diam voluptua. At </P>',
|
||||
htmlAfter = '<P>Lorem ipsum dolor sit amet, sed diam voluptua. At2</P>';
|
||||
var calculatedType = diffService.detectReplacementType(htmlBefore, htmlAfter);
|
||||
expect(calculatedType).toBe(diffService.TYPE_INSERTION);
|
||||
});
|
||||
|
||||
it('detects a simple insertion, ignoring spaces between UL and LI', function () {
|
||||
var htmlBefore = '<UL><LI>accusam et justo duo dolores et ea rebum.</LI></UL>',
|
||||
htmlAfter = '<UL>' + "\n" + '<LI>accusam et justo duo dolores et ea rebum 123.</LI>' + "\n" + '</UL>';
|
||||
var calculatedType = diffService.detectReplacementType(htmlBefore, htmlAfter);
|
||||
expect(calculatedType).toBe(diffService.TYPE_INSERTION);
|
||||
});
|
||||
|
||||
it('detects a simple insertion, despite tags', function() {
|
||||
var htmlBefore = '<P>dsds dsfsdfsdf sdf sdfs dds sdf dsds dsfsdfsdf</P>',
|
||||
htmlAfter = '<P>dsds dsfsdfsdf sdf sdfs dds sd345 3453 45f dsds dsfsdfsdf</P>';
|
||||
var calculatedType = diffService.detectReplacementType(htmlBefore, htmlAfter);
|
||||
expect(calculatedType).toBe(diffService.TYPE_INSERTION);
|
||||
});
|
||||
|
||||
it('detects a simple deletion', function () {
|
||||
var htmlBefore = '<p>Test 1 Test 2</p>' + "\n" + '<p>Test 3</p>',
|
||||
htmlAfter = '<p>Test 1</p>';
|
||||
var calculatedType = diffService.detectReplacementType(htmlBefore, htmlAfter);
|
||||
expect(calculatedType).toBe(diffService.TYPE_DELETION);
|
||||
});
|
||||
|
||||
it('detects a simple deletion, ignoring case of tags', function () {
|
||||
var htmlBefore = '<p>Test 1 Test 2</p>' + "\n" + '<p>Test 3</p>',
|
||||
htmlAfter = '<P>Test 1</P>';
|
||||
var calculatedType = diffService.detectReplacementType(htmlBefore, htmlAfter);
|
||||
expect(calculatedType).toBe(diffService.TYPE_DELETION);
|
||||
});
|
||||
|
||||
it('detects a simple deletion, ignoring trailing whitespaces', function () {
|
||||
var htmlBefore = '<P>Lorem ipsum dolor sit amet, sed diam voluptua. At2</P>',
|
||||
htmlAfter = '<P>Lorem ipsum dolor sit amet, sed diam voluptua. At </P>';
|
||||
var calculatedType = diffService.detectReplacementType(htmlBefore, htmlAfter);
|
||||
expect(calculatedType).toBe(diffService.TYPE_DELETION);
|
||||
});
|
||||
|
||||
it('detects a simple replacement', function () {
|
||||
var htmlBefore = '<p>Test 1 Test 2</p>' + "\n" + '<p>Test 3</p>',
|
||||
htmlAfter = '<p>Test 1</p>' + "\n" + '<p>Test 2</p>' + "\n" + '<p>Test 3</p>';
|
||||
var calculatedType = diffService.detectReplacementType(htmlBefore, htmlAfter);
|
||||
expect(calculatedType).toBe(diffService.TYPE_REPLACEMENT);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -5,10 +5,10 @@ describe('linenumbering', function () {
|
||||
var lineNumberingService,
|
||||
brMarkup = function (no) {
|
||||
return '<br class="os-line-break">' +
|
||||
'<span class="os-line-number line-number-' + no + '" data-line-number="' + no + '" contenteditable="false"> </span>';
|
||||
'<span class="os-line-number line-number-' + no + '" data-line-number="' + no + '" name="L' + no + '" contenteditable="false"> </span>';
|
||||
},
|
||||
noMarkup = function (no) {
|
||||
return '<span class="os-line-number line-number-' + no + '" data-line-number="' + no + '" contenteditable="false"> </span>';
|
||||
return '<span class="os-line-number line-number-' + no + '" data-line-number="' + no + '" name="L' + no + '" contenteditable="false"> </span>';
|
||||
},
|
||||
longstr = function (length) {
|
||||
var outstr = '';
|
||||
@ -26,6 +26,7 @@ describe('linenumbering', function () {
|
||||
it('breaks very short lines', function () {
|
||||
var textNode = document.createTextNode("0123");
|
||||
lineNumberingService._currentInlineOffset = 0;
|
||||
lineNumberingService._currentLineNumber = 1;
|
||||
var out = lineNumberingService._textNodeToLines(textNode, 5);
|
||||
var outHtml = lineNumberingService._nodesToHtml(out);
|
||||
expect(outHtml).toBe('0123');
|
||||
|
Loading…
Reference in New Issue
Block a user