Merge pull request #2501 from CatoTH/change-recommendations-pr
Change recommendations (WIP)
This commit is contained in:
commit
34b074faec
@ -302,22 +302,25 @@ img {
|
|||||||
border: 1px solid #d3d3d3;
|
border: 1px solid #d3d3d3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col1 .details .line-number-setter {
|
.col1 .details .motion-toolbar .toolbar-left {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 55px;
|
margin-bottom: 55px;
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col1 .details .line-number-setter > span {
|
.col1 .details .motion-toolbar .toolbar-left > * {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col1 .details .line-number-setter .btn.disabled {
|
.col1 .details .motion-toolbar .toolbar-left .btn.disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
.col1 .details .motion-toolbar .toolbar-left .goto-line-number {
|
||||||
|
max-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
.col1 .details .inline-editing-activator {
|
.col1 .details .inline-editing-activator {
|
||||||
margin-right: 13px;
|
margin-right: 13px;
|
||||||
@ -395,23 +398,29 @@ img {
|
|||||||
background-color: #ff0;
|
background-color: #ff0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.motion-text li {
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
.motion-text.line-numbers-outside {
|
.motion-text.line-numbers-outside {
|
||||||
padding-left: 35px;
|
padding-left: 40px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.motion-text.line-numbers-outside .os-line-number {
|
.motion-text.line-numbers-outside .os-line-number {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
width: 0;
|
width: 22px;
|
||||||
height: 0;
|
height: 22px;
|
||||||
|
position: absolute;
|
||||||
|
left: -20px;
|
||||||
|
padding-right: 55px;
|
||||||
}
|
}
|
||||||
.motion-text.line-numbers-outside .os-line-number:after {
|
.motion-text.line-numbers-outside .os-line-number:after {
|
||||||
content: attr(data-line-number);
|
content: attr(data-line-number);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 20px;
|
||||||
|
top: 12px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
margin-top: -5px;
|
|
||||||
color: gray;
|
color: gray;
|
||||||
font-family: Courier, serif;
|
font-family: Courier, serif;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@ -445,6 +454,7 @@ img {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.os-line-number {
|
.os-line-number {
|
||||||
|
position: relative;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-khtml-user-select: none;
|
-khtml-user-select: none;
|
||||||
@ -452,6 +462,7 @@ img {
|
|||||||
-o-user-select: none;
|
-o-user-select: none;
|
||||||
}
|
}
|
||||||
.os-line-number:after {
|
.os-line-number:after {
|
||||||
|
position: relative;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-khtml-user-select: none;
|
-khtml-user-select: none;
|
||||||
@ -459,6 +470,174 @@ img {
|
|||||||
-o-user-select: none;
|
-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 **/
|
/** Projector sidebar column **/
|
||||||
|
|
||||||
#content .col2 {
|
#content .col2 {
|
||||||
@ -704,13 +883,16 @@ img {
|
|||||||
border-top: 1px solid #ddd;
|
border-top: 1px solid #ddd;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
.linenumber-toolbar, .speakers-toolbar {
|
.motion-toolbar, .speakers-toolbar {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
padding: 12px 0 10px 0;
|
padding: 12px 0 10px 0;
|
||||||
height: 54px;
|
height: 54px;
|
||||||
margin: -20px -5px 50px -5px;
|
margin: -20px -5px 50px -5px;
|
||||||
}
|
}
|
||||||
|
.motion-toolbar:first-child {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.speakers-toolbar {
|
.speakers-toolbar {
|
||||||
margin: -20px -20px 30px -20px;
|
margin: -20px -20px 30px -20px;
|
||||||
|
@ -55,6 +55,25 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
|||||||
return data
|
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):
|
class CategoryAccessPermissions(BaseAccessPermissions):
|
||||||
"""
|
"""
|
||||||
Access permissions container for Category and CategoryViewSet.
|
Access permissions container for Category and CategoryViewSet.
|
||||||
|
@ -18,7 +18,7 @@ class MotionsAppConfig(AppConfig):
|
|||||||
from openslides.utils.rest_api import router
|
from openslides.utils.rest_api import router
|
||||||
from .config_variables import get_config_variables
|
from .config_variables import get_config_variables
|
||||||
from .signals import create_builtin_workflows
|
from .signals import create_builtin_workflows
|
||||||
from .views import CategoryViewSet, MotionViewSet, MotionPollViewSet, WorkflowViewSet
|
from .views import CategoryViewSet, MotionViewSet, MotionPollViewSet, MotionChangeRecommendationViewSet, WorkflowViewSet
|
||||||
|
|
||||||
# Define config variables
|
# Define config variables
|
||||||
config.update_config_variables(get_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('Category').get_collection_string(), CategoryViewSet)
|
||||||
router.register(self.get_model('Motion').get_collection_string(), MotionViewSet)
|
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('Workflow').get_collection_string(), WorkflowViewSet)
|
||||||
|
router.register(self.get_model('MotionChangeRecommendation').get_collection_string(),
|
||||||
|
MotionChangeRecommendationViewSet)
|
||||||
router.register('motions/motionpoll', MotionPollViewSet)
|
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 (
|
from .access_permissions import (
|
||||||
CategoryAccessPermissions,
|
CategoryAccessPermissions,
|
||||||
MotionAccessPermissions,
|
MotionAccessPermissions,
|
||||||
|
MotionChangeRecommendationAccessPermissions,
|
||||||
WorkflowAccessPermissions,
|
WorkflowAccessPermissions,
|
||||||
)
|
)
|
||||||
from .exceptions import WorkflowError
|
from .exceptions import WorkflowError
|
||||||
@ -697,6 +698,68 @@ class MotionVersion(RESTModelMixin, models.Model):
|
|||||||
return self.motion
|
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):
|
class Category(RESTModelMixin, models.Model):
|
||||||
"""
|
"""
|
||||||
Model for categories of motions.
|
Model for categories of motions.
|
||||||
|
@ -16,6 +16,7 @@ from openslides.utils.rest_api import (
|
|||||||
from .models import (
|
from .models import (
|
||||||
Category,
|
Category,
|
||||||
Motion,
|
Motion,
|
||||||
|
MotionChangeRecommendation,
|
||||||
MotionLog,
|
MotionLog,
|
||||||
MotionPoll,
|
MotionPoll,
|
||||||
MotionVersion,
|
MotionVersion,
|
||||||
@ -228,6 +229,22 @@ class MotionVersionSerializer(ModelSerializer):
|
|||||||
'reason',)
|
'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):
|
class MotionSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
Serializer for motion.models.Motion objects.
|
Serializer for motion.models.Motion objects.
|
||||||
|
@ -142,13 +142,16 @@ angular.module('OpenSlidesApp.motions', [
|
|||||||
.factory('Motion', [
|
.factory('Motion', [
|
||||||
'DS',
|
'DS',
|
||||||
'MotionPoll',
|
'MotionPoll',
|
||||||
|
'MotionChangeRecommendation',
|
||||||
'MotionComment',
|
'MotionComment',
|
||||||
'jsDataModel',
|
'jsDataModel',
|
||||||
'gettext',
|
'gettext',
|
||||||
'operator',
|
'operator',
|
||||||
'Config',
|
'Config',
|
||||||
'lineNumberingService',
|
'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';
|
var name = 'motions/motion';
|
||||||
return DS.defineResource({
|
return DS.defineResource({
|
||||||
name: name,
|
name: name,
|
||||||
@ -187,7 +190,98 @@ angular.module('OpenSlidesApp.motions', [
|
|||||||
|
|
||||||
return lineNumberingService.insertLineNumbers(html, lineLength, highlight, callback);
|
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);
|
this.text = lineNumberingService.stripLineNumbers(text);
|
||||||
},
|
},
|
||||||
getReason: function (versionId) {
|
getReason: function (versionId) {
|
||||||
@ -201,6 +295,24 @@ angular.module('OpenSlidesApp.motions', [
|
|||||||
getSearchResultSubtitle: function () {
|
getSearchResultSubtitle: function () {
|
||||||
return "Motion";
|
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) {
|
isAllowed: function (action) {
|
||||||
/*
|
/*
|
||||||
* Return true if the requested user is allowed to do the specific action.
|
* Return true if the requested user is allowed to do the specific action.
|
||||||
@ -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([
|
.run([
|
||||||
'Motion',
|
'Motion',
|
||||||
'Category',
|
'Category',
|
||||||
'Workflow',
|
'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,
|
TEXT_NODE = 3,
|
||||||
DOCUMENT_FRAGMENT_NODE = 11;
|
DOCUMENT_FRAGMENT_NODE = 11;
|
||||||
|
|
||||||
|
this.TYPE_REPLACEMENT = 0;
|
||||||
|
this.TYPE_INSERTION = 1;
|
||||||
|
this.TYPE_DELETION = 2;
|
||||||
|
|
||||||
this.getLineNumberNode = function(fragment, lineNumber) {
|
this.getLineNumberNode = function(fragment, lineNumber) {
|
||||||
return fragment.querySelector('os-linebreak.os-line-number.line-number-' + lineNumber);
|
return fragment.querySelector('os-linebreak.os-line-number.line-number-' + lineNumber);
|
||||||
@ -24,22 +27,91 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
|||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Adds elements like <OS-LINEBREAK class="os-line-number line-number-23" data-line-number="23"/>
|
||||||
this._insertInternalLineMarkers = function(fragment) {
|
this._insertInternalLineMarkers = function(fragment) {
|
||||||
if (fragment.querySelectorAll('OS-LINEBREAK').length > 0) {
|
if (fragment.querySelectorAll('OS-LINEBREAK').length > 0) {
|
||||||
// Prevent duplicate calls
|
// Prevent duplicate calls
|
||||||
return;
|
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++) {
|
for (var i = 0; i < lineNumbers.length; i++) {
|
||||||
var insertBefore = lineNumbers[i];
|
var insertBefore = lineNumbers[i];
|
||||||
while (insertBefore.parentNode.nodeType != DOCUMENT_FRAGMENT_NODE && insertBefore.parentNode.childNodes[0] == insertBefore) {
|
while (insertBefore.parentNode.nodeType != DOCUMENT_FRAGMENT_NODE && insertBefore.parentNode.childNodes[0] == insertBefore) {
|
||||||
insertBefore = insertBefore.parentNode;
|
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('data-line-number', lineNumbers[i].getAttribute('data-line-number'));
|
||||||
lineMarker.setAttribute('class', lineNumbers[i].getAttribute('class'));
|
lineMarker.setAttribute('class', lineNumbers[i].getAttribute('class'));
|
||||||
insertBefore.parentNode.insertBefore(lineMarker, insertBefore);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -85,7 +157,9 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
|||||||
var html = '<' + node.nodeName;
|
var html = '<' + node.nodeName;
|
||||||
for (var i = 0; i < node.attributes.length; i++) {
|
for (var i = 0; i < node.attributes.length; i++) {
|
||||||
var attr = node.attributes[i];
|
var attr = node.attributes[i];
|
||||||
html += " " + attr.name + "=\"" + attr.value + "\"";
|
if (attr.name != 'os-li-number') {
|
||||||
|
html += ' ' + attr.name + '="' + attr.value + '"';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
html += '>';
|
html += '>';
|
||||||
return html;
|
return html;
|
||||||
@ -158,7 +232,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
|||||||
}
|
}
|
||||||
if (!found) {
|
if (!found) {
|
||||||
console.trace();
|
console.trace();
|
||||||
throw "Inconsistency or invalid call of this function detected";
|
throw "Inconsistency or invalid call of this function detected (to)";
|
||||||
}
|
}
|
||||||
return html;
|
return html;
|
||||||
};
|
};
|
||||||
@ -196,7 +270,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
|||||||
}
|
}
|
||||||
if (!found) {
|
if (!found) {
|
||||||
console.trace();
|
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) {
|
if (node.nodeType != DOCUMENT_FRAGMENT_NODE) {
|
||||||
html += '</' + node.nodeName + '>';
|
html += '</' + node.nodeName + '>';
|
||||||
@ -221,6 +295,7 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
|||||||
*
|
*
|
||||||
* Hint:
|
* Hint:
|
||||||
* - The last line (toLine) is not included anymore, as the number refers to the line breaking element
|
* - The last line (toLine) is not included anymore, as the number refers to the line breaking element
|
||||||
|
* - if toLine === null, then everything from fromLine to the end of the fragment is returned
|
||||||
*
|
*
|
||||||
* In addition to the HTML snippet, additional information is provided regarding the most specific DOM element
|
* In addition to the HTML snippet, additional information is provided regarding the most specific DOM element
|
||||||
* that contains the whole section specified by the line numbers (like a P-element if only one paragraph is selected
|
* 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"
|
* - 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._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),
|
var fromLineNode = this.getLineNumberNode(fragment, fromLine),
|
||||||
toLineNode = this.getLineNumberNode(fragment, toLine),
|
toLineNode = (toLine ? this.getLineNumberNode(fragment, toLine) : null),
|
||||||
ancestorData = this._getCommonAncestor(fromLineNode, toLineNode);
|
ancestorData = this._getCommonAncestor(fromLineNode, toLineNode);
|
||||||
|
|
||||||
var fromChildTraceRel = ancestorData.trace1,
|
var fromChildTraceRel = ancestorData.trace1,
|
||||||
@ -264,7 +347,8 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
|||||||
innerContextStart = '',
|
innerContextStart = '',
|
||||||
innerContextEnd = '',
|
innerContextEnd = '',
|
||||||
previousHtmlEndSnippet = '',
|
previousHtmlEndSnippet = '',
|
||||||
followingHtmlStartSnippet = '';
|
followingHtmlStartSnippet = '',
|
||||||
|
fakeOl;
|
||||||
|
|
||||||
|
|
||||||
fromChildTraceAbs.shift();
|
fromChildTraceAbs.shift();
|
||||||
@ -287,10 +371,16 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
|||||||
for (var i = 0; i < fromChildTraceRel.length && !found; i++) {
|
for (var i = 0; i < fromChildTraceRel.length && !found; i++) {
|
||||||
if (fromChildTraceRel[i].nodeName == 'OS-LINEBREAK') {
|
if (fromChildTraceRel[i].nodeName == 'OS-LINEBREAK') {
|
||||||
found = true;
|
found = true;
|
||||||
|
} else {
|
||||||
|
if (fromChildTraceRel[i].nodeName == 'OL') {
|
||||||
|
fakeOl = fromChildTraceRel[i].cloneNode(false);
|
||||||
|
fakeOl.setAttribute('start', this._isWithinNthLIOfOL(fromChildTraceRel[i], fromLineNode));
|
||||||
|
innerContextStart += this._serializeTag(fakeOl);
|
||||||
} else {
|
} else {
|
||||||
innerContextStart += this._serializeTag(fromChildTraceRel[i]);
|
innerContextStart += this._serializeTag(fromChildTraceRel[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
found = false;
|
found = false;
|
||||||
for (i = 0; i < toChildTraceRel.length && !found; i++) {
|
for (i = 0; i < toChildTraceRel.length && !found; i++) {
|
||||||
if (toChildTraceRel[i].nodeName == 'OS-LINEBREAK') {
|
if (toChildTraceRel[i].nodeName == 'OS-LINEBREAK') {
|
||||||
@ -317,7 +407,13 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
|||||||
|
|
||||||
currNode = ancestor;
|
currNode = ancestor;
|
||||||
while (currNode.parentNode) {
|
while (currNode.parentNode) {
|
||||||
|
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;
|
outerContextStart = this._serializeTag(currNode) + outerContextStart;
|
||||||
|
}
|
||||||
outerContextEnd += '</' + currNode.nodeName + '>';
|
outerContextEnd += '</' + currNode.nodeName + '>';
|
||||||
currNode = currNode.parentNode;
|
currNode = currNode.parentNode;
|
||||||
}
|
}
|
||||||
@ -334,9 +430,16 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
|||||||
'followingHtml': followingHtml,
|
'followingHtml': followingHtml,
|
||||||
'followingHtmlStartSnippet': followingHtmlStartSnippet
|
'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) {
|
this._replaceLinesMergeNodeArrays = function(nodes1, nodes2) {
|
||||||
if (nodes1.length === 0) {
|
if (nodes1.length === 0) {
|
||||||
return nodes2;
|
return nodes2;
|
||||||
@ -350,56 +453,179 @@ angular.module('OpenSlidesApp.motions.diff', ['OpenSlidesApp.motions.lineNumberi
|
|||||||
out.push(nodes1[i]);
|
out.push(nodes1[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
out.push(nodes1[nodes1.length - 1]);
|
var lastNode = nodes1[nodes1.length - 1],
|
||||||
out.push(nodes2[0]);
|
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++) {
|
for (i = 1; i < nodes2.length; i++) {
|
||||||
out.push(nodes2[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;
|
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) {
|
this.replaceLines = function (fragment, newHTML, fromLine, toLine) {
|
||||||
var data = this.extractRangeByLineNumbers(fragment, 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),
|
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),
|
followingFragment = this.htmlToFragment(followingHtml),
|
||||||
newFragment = this.htmlToFragment(newHTML),
|
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) {
|
var mergedFragment = document.createDocumentFragment();
|
||||||
child = previousFragment.children[0];
|
while (previousFragment.firstChild) {
|
||||||
previousFragment.removeChild(child);
|
el = previousFragment.firstChild;
|
||||||
merged.appendChild(child);
|
previousFragment.removeChild(el);
|
||||||
|
mergedFragment.appendChild(el);
|
||||||
}
|
}
|
||||||
while (newFragment.children.length > 0) {
|
while (diffFragment.firstChild) {
|
||||||
child = newFragment.children[0];
|
el = diffFragment.firstChild;
|
||||||
newFragment.removeChild(child);
|
diffFragment.removeChild(el);
|
||||||
merged.appendChild(child);
|
mergedFragment.appendChild(el);
|
||||||
}
|
}
|
||||||
while (followingFragment.children.length > 0) {
|
while (followingFragment.firstChild) {
|
||||||
child = followingFragment.children[0];
|
el = followingFragment.firstChild;
|
||||||
followingFragment.removeChild(child);
|
followingFragment.removeChild(el);
|
||||||
merged.appendChild(child);
|
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 node
|
||||||
* @param length
|
* @param length
|
||||||
|
* @param highlight
|
||||||
* @returns Array
|
* @returns Array
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
@ -89,7 +90,7 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
|
|||||||
var addLine = function (text, highlight) {
|
var addLine = function (text, highlight) {
|
||||||
var node;
|
var node;
|
||||||
if (typeof highlight === 'undefined') {
|
if (typeof highlight === 'undefined') {
|
||||||
highlight = 0;
|
highlight = -1;
|
||||||
}
|
}
|
||||||
if (firstTextNode) {
|
if (firstTextNode) {
|
||||||
if (highlight == service._currentLineNumber - 1) {
|
if (highlight == service._currentLineNumber - 1) {
|
||||||
@ -344,19 +345,23 @@ angular.module('OpenSlidesApp.motions.lineNumbering', [])
|
|||||||
return root.innerHTML;
|
return root.innerHTML;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.insertLineNumbersNode = function (html, lineLength, highlight) {
|
this.insertLineNumbersNode = function (html, lineLength, highlight, firstLine) {
|
||||||
var root = document.createElement('div');
|
var root = document.createElement('div');
|
||||||
root.innerHTML = html;
|
root.innerHTML = html;
|
||||||
|
|
||||||
this._currentInlineOffset = 0;
|
this._currentInlineOffset = 0;
|
||||||
|
if (firstLine) {
|
||||||
|
this._currentLineNumber = firstLine;
|
||||||
|
} else {
|
||||||
this._currentLineNumber = 1;
|
this._currentLineNumber = 1;
|
||||||
|
}
|
||||||
this._prependLineNumberToFirstText = true;
|
this._prependLineNumberToFirstText = true;
|
||||||
|
|
||||||
return this._insertLineNumbersToNode(root, lineLength, highlight);
|
return this._insertLineNumbersToNode(root, lineLength, highlight);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.insertLineNumbers = function (html, lineLength, highlight, callback) {
|
this.insertLineNumbers = function (html, lineLength, highlight, callback, firstLine) {
|
||||||
var newRoot = this.insertLineNumbersNode(html, lineLength, highlight);
|
var newRoot = this.insertLineNumbersNode(html, lineLength, highlight, firstLine);
|
||||||
|
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback();
|
callback();
|
||||||
|
@ -4,6 +4,57 @@
|
|||||||
|
|
||||||
angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions', 'OpenSlidesApp.motions.lineNumbering'])
|
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', [
|
.factory('MotionInlineEditing', [
|
||||||
'Editor',
|
'Editor',
|
||||||
'Motion',
|
'Motion',
|
||||||
@ -37,29 +88,28 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
|
|||||||
obj.tinymceOptions.readonly = 1;
|
obj.tinymceOptions.readonly = 1;
|
||||||
obj.tinymceOptions.setup = function (editor) {
|
obj.tinymceOptions.setup = function (editor) {
|
||||||
obj.editor = editor;
|
obj.editor = editor;
|
||||||
editor.on("init", function () {
|
editor.on('init', function () {
|
||||||
obj.lineBrokenText = motion.getTextWithLineBreaks($scope.version);
|
obj.lineBrokenText = motion.getTextWithLineBreaks($scope.version);
|
||||||
obj.editor.setContent(obj.lineBrokenText);
|
obj.editor.setContent(obj.lineBrokenText);
|
||||||
obj.originalHtml = obj.editor.getContent();
|
obj.originalHtml = obj.editor.getContent();
|
||||||
obj.changed = false;
|
obj.changed = false;
|
||||||
});
|
});
|
||||||
editor.on("change", function () {
|
editor.on('change', function () {
|
||||||
obj.changed = (editor.getContent() != obj.originalHtml);
|
obj.changed = (editor.getContent() != obj.originalHtml);
|
||||||
});
|
});
|
||||||
editor.on("undo", function () {
|
editor.on('undo', function () {
|
||||||
obj.changed = (editor.getContent() != obj.originalHtml);
|
obj.changed = (editor.getContent() != obj.originalHtml);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
obj.setVersion = function (_motion, versionId) {
|
obj.setVersion = function (_motion, versionId) {
|
||||||
motion = _motion; // If this is not updated,
|
motion = _motion; // If this is not updated,
|
||||||
console.log(versionId, motion.getTextWithLineBreaks(versionId));
|
|
||||||
obj.lineBrokenText = motion.getTextWithLineBreaks(versionId);
|
obj.lineBrokenText = motion.getTextWithLineBreaks(versionId);
|
||||||
obj.changed = false;
|
obj.changed = false;
|
||||||
obj.active = false;
|
obj.active = false;
|
||||||
if (obj.editor) {
|
if (obj.editor) {
|
||||||
obj.editor.setContent(obj.lineBrokenText);
|
obj.editor.setContent(obj.lineBrokenText);
|
||||||
obj.editor.setMode("readonly");
|
obj.editor.setMode('readonly');
|
||||||
obj.originalHtml = obj.editor.getContent();
|
obj.originalHtml = obj.editor.getContent();
|
||||||
} else {
|
} else {
|
||||||
obj.originalHtml = obj.lineBrokenText;
|
obj.originalHtml = obj.lineBrokenText;
|
||||||
@ -67,7 +117,7 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
|
|||||||
};
|
};
|
||||||
|
|
||||||
obj.enable = function () {
|
obj.enable = function () {
|
||||||
obj.editor.setMode("design");
|
obj.editor.setMode('design');
|
||||||
obj.active = true;
|
obj.active = true;
|
||||||
obj.changed = false;
|
obj.changed = false;
|
||||||
|
|
||||||
@ -80,7 +130,7 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
|
|||||||
};
|
};
|
||||||
|
|
||||||
obj.disable = function () {
|
obj.disable = function () {
|
||||||
obj.editor.setMode("readonly");
|
obj.editor.setMode('readonly');
|
||||||
obj.active = false;
|
obj.active = false;
|
||||||
obj.changed = false;
|
obj.changed = false;
|
||||||
obj.lineBrokenText = obj.originalHtml;
|
obj.lineBrokenText = obj.originalHtml;
|
||||||
@ -89,10 +139,10 @@ angular.module('OpenSlidesApp.motions.motionservices', ['OpenSlidesApp.motions',
|
|||||||
|
|
||||||
obj.save = function () {
|
obj.save = function () {
|
||||||
if (!motion.isAllowed('update')) {
|
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.disable_versioning = (obj.trivialChange && Config.get('motions_allow_disable_versioning').value);
|
||||||
|
|
||||||
Motion.inject(motion);
|
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;
|
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.
|
* other project that might want to use this HTML to PDF parser.
|
||||||
* https://github.com/OpenSlides/OpenSlides/issues/2361
|
* 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 {
|
} else {
|
||||||
return converter.convertHTML(motion.getText($scope.version), $scope);
|
return converter.convertHTML(motion.getText($scope.version), $scope);
|
||||||
}
|
}
|
||||||
|
@ -104,6 +104,9 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
},
|
},
|
||||||
tags: function(Tag) {
|
tags: function(Tag) {
|
||||||
return Tag.findAll();
|
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)
|
// Service for generic motion form (create and update)
|
||||||
.factory('MotionForm', [
|
.factory('MotionForm', [
|
||||||
'gettextCatalog',
|
'gettextCatalog',
|
||||||
@ -818,6 +908,10 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
'operator',
|
'operator',
|
||||||
'ngDialog',
|
'ngDialog',
|
||||||
'MotionForm',
|
'MotionForm',
|
||||||
|
'ChangeRecommmendationCreate',
|
||||||
|
'ChangeRecommmendationView',
|
||||||
|
'MotionChangeRecommendation',
|
||||||
|
'MotionPDFExport',
|
||||||
'Motion',
|
'Motion',
|
||||||
'Category',
|
'Category',
|
||||||
'Mediafile',
|
'Mediafile',
|
||||||
@ -826,24 +920,21 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
'Workflow',
|
'Workflow',
|
||||||
'Config',
|
'Config',
|
||||||
'motion',
|
'motion',
|
||||||
'MotionContentProvider',
|
|
||||||
'PollContentProvider',
|
|
||||||
'PdfMakeConverter',
|
|
||||||
'PdfMakeDocumentProvider',
|
|
||||||
'MotionInlineEditing',
|
'MotionInlineEditing',
|
||||||
'gettextCatalog',
|
'gettextCatalog',
|
||||||
'Projector',
|
'Projector',
|
||||||
'HTMLValidizer',
|
|
||||||
'ProjectionDefault',
|
'ProjectionDefault',
|
||||||
function($scope, $http, operator, ngDialog, MotionForm, Motion, Category, Mediafile, Tag, User, Workflow, Config,
|
function($scope, $http, operator, ngDialog, MotionForm,
|
||||||
motion, MotionContentProvider, PollContentProvider, PdfMakeConverter, PdfMakeDocumentProvider,
|
ChangeRecommmendationCreate, ChangeRecommmendationView, MotionChangeRecommendation, MotionPDFExport,
|
||||||
MotionInlineEditing, gettextCatalog, Projector, HTMLValidizer, ProjectionDefault) {
|
Motion, Category, Mediafile, Tag, User, Workflow, Config, motion, MotionInlineEditing, gettextCatalog,
|
||||||
|
Projector, ProjectionDefault) {
|
||||||
Motion.bindOne(motion.id, $scope, 'motion');
|
Motion.bindOne(motion.id, $scope, 'motion');
|
||||||
Category.bindAll({}, $scope, 'categories');
|
Category.bindAll({}, $scope, 'categories');
|
||||||
Mediafile.bindAll({}, $scope, 'mediafiles');
|
Mediafile.bindAll({}, $scope, 'mediafiles');
|
||||||
Tag.bindAll({}, $scope, 'tags');
|
Tag.bindAll({}, $scope, 'tags');
|
||||||
User.bindAll({}, $scope, 'users');
|
User.bindAll({}, $scope, 'users');
|
||||||
Workflow.bindAll({}, $scope, 'workflows');
|
Workflow.bindAll({}, $scope, 'workflows');
|
||||||
|
MotionChangeRecommendation.bindAll({'where': {'motion_version_id': {'==': motion.active_version}}}, $scope, 'change_recommendations');
|
||||||
Motion.loadRelations(motion, 'agenda_item');
|
Motion.loadRelations(motion, 'agenda_item');
|
||||||
$scope.$watch(function () {
|
$scope.$watch(function () {
|
||||||
return Projector.lastModified();
|
return Projector.lastModified();
|
||||||
@ -853,7 +944,12 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
$scope.version = motion.active_version;
|
$scope.version = motion.active_version;
|
||||||
$scope.isCollapsed = true;
|
$scope.isCollapsed = true;
|
||||||
$scope.commentsFields = Config.get('motions_comments').value;
|
$scope.commentsFields = Config.get('motions_comments').value;
|
||||||
|
|
||||||
$scope.lineNumberMode = Config.get('motions_default_line_numbering').value;
|
$scope.lineNumberMode = Config.get('motions_default_line_numbering').value;
|
||||||
|
$scope.setLineNumberMode = function(mode) {
|
||||||
|
$scope.lineNumberMode = mode;
|
||||||
|
};
|
||||||
|
|
||||||
if (motion.parent_id) {
|
if (motion.parent_id) {
|
||||||
Motion.bindOne(motion.parent_id, $scope, 'parent');
|
Motion.bindOne(motion.parent_id, $scope, 'parent');
|
||||||
}
|
}
|
||||||
@ -891,32 +987,6 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
setHighlightOnProjector($scope.linesForProjector ? $scope.highlight : 0);
|
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
|
// open edit dialog
|
||||||
$scope.openDialog = function (motion) {
|
$scope.openDialog = function (motion) {
|
||||||
if ($scope.inlineEditing.active) {
|
if ($scope.inlineEditing.active) {
|
||||||
@ -987,6 +1057,7 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
$scope.showVersion = function (version) {
|
$scope.showVersion = function (version) {
|
||||||
$scope.version = version.id;
|
$scope.version = version.id;
|
||||||
$scope.inlineEditing.setVersion(motion, version.id);
|
$scope.inlineEditing.setVersion(motion, version.id);
|
||||||
|
$scope.createChangeRecommendation.setVersion(motion, version.id);
|
||||||
};
|
};
|
||||||
// permit specific version
|
// permit specific version
|
||||||
$scope.permitVersion = function (version) {
|
$scope.permitVersion = function (version) {
|
||||||
@ -1022,6 +1093,59 @@ angular.module('OpenSlidesApp.motions.site', [
|
|||||||
// Inline editing functions
|
// Inline editing functions
|
||||||
$scope.inlineEditing = MotionInlineEditing;
|
$scope.inlineEditing = MotionInlineEditing;
|
||||||
$scope.inlineEditing.init($scope, motion);
|
$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>
|
<i class="fa fa-pencil"></i>
|
||||||
</a>
|
</a>
|
||||||
<!-- pdf -->
|
<!-- 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>
|
<i class="fa fa-file-pdf-o fa-lg"></i>
|
||||||
<translate>PDF</translate>
|
<translate>PDF</translate>
|
||||||
</a>
|
</a>
|
||||||
@ -182,7 +182,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Print poll PDF -->
|
<!-- 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 }}">
|
title="{{ 'Print ballot paper' | translate }}">
|
||||||
<i class="fa fa-file-pdf-o"></i>
|
<i class="fa fa-file-pdf-o"></i>
|
||||||
</a>
|
</a>
|
||||||
@ -289,94 +289,48 @@
|
|||||||
|
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<div class="row">
|
<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 -->
|
<!-- Motion toolbar -->
|
||||||
<div class="line-number-setter {{ lineNumberMode }}">
|
<ng-include src="'static/templates/motions/motion-detail/toolbar.html'"></ng-include>
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 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>
|
|
||||||
</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-class="{'col-sm-8': (lineNumberMode != 'outside'), 'col-sm-12': (lineNumberMode == 'outside')}">
|
||||||
|
<div class="motion-text-holder">
|
||||||
|
|
||||||
<div ng-if="motion.isAllowed('update') && version == motion.getVersion(-1).id">
|
<!-- Original view -->
|
||||||
<div ng-show="inlineEditing.active">
|
<ng-include src="'static/templates/motions/motion-detail/view-original.html'"></ng-include>
|
||||||
<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 line-numbers-{{ lineNumberMode }}"></div>
|
|
||||||
|
|
||||||
<div class="motion-save-toolbar" ng-class="{ 'visible': (inlineEditing.changed && inlineEditing.active) }">
|
<!-- Diff View -->
|
||||||
<div class="changed-hint" translate>The text has been changed.</div>
|
<ng-include src="'static/templates/motions/motion-detail/view-diff.html'"></ng-include>
|
||||||
<button type="button" ng-click="inlineEditing.save()" class="btn btn-primary">Save</button>
|
|
||||||
<label ng-if="motion.state.versioning && config('motions_allow_disable_versioning')">
|
<!-- Changed View -->
|
||||||
<input type="checkbox" ng-model="inlineEditing.trivialChange" value="1">
|
<div ng-if="viewChangeRecommendations.mode == 'changed'">
|
||||||
<span translate>Trivial change</span>
|
<div ng-bind-html="motion.getTextWithChangeRecommendations(version) | trusted"
|
||||||
</label>
|
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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<!-- reason -->
|
<!-- reason -->
|
||||||
|
@ -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 (
|
from .access_permissions import (
|
||||||
CategoryAccessPermissions,
|
CategoryAccessPermissions,
|
||||||
MotionAccessPermissions,
|
MotionAccessPermissions,
|
||||||
|
MotionChangeRecommendationAccessPermissions,
|
||||||
WorkflowAccessPermissions,
|
WorkflowAccessPermissions,
|
||||||
)
|
)
|
||||||
from .exceptions import WorkflowError
|
from .exceptions import WorkflowError
|
||||||
from .models import (
|
from .models import (
|
||||||
Category,
|
Category,
|
||||||
Motion,
|
Motion,
|
||||||
|
MotionChangeRecommendation,
|
||||||
MotionPoll,
|
MotionPoll,
|
||||||
MotionVersion,
|
MotionVersion,
|
||||||
State,
|
State,
|
||||||
@ -341,6 +343,31 @@ class MotionPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet):
|
|||||||
self.request.user.has_perm('motions.can_manage'))
|
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):
|
class CategoryViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
API endpoint for categories.
|
API endpoint for categories.
|
||||||
|
@ -2,7 +2,7 @@ describe('linenumbering', function () {
|
|||||||
|
|
||||||
beforeEach(module('OpenSlidesApp.motions.diff'));
|
beforeEach(module('OpenSlidesApp.motions.diff'));
|
||||||
|
|
||||||
var diffService, baseHtmlDom1, baseHtmlDom2,
|
var diffService, baseHtmlDom1, baseHtmlDom2, baseHtmlDom3,
|
||||||
brMarkup = function (no) {
|
brMarkup = function (no) {
|
||||||
return '<br class="os-line-break">' +
|
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 + '" contenteditable="false"> </span>';
|
||||||
@ -26,26 +26,36 @@ describe('linenumbering', function () {
|
|||||||
'</ul>' +
|
'</ul>' +
|
||||||
'<p>' + noMarkup(10) + 'Line 10 ' + brMarkup(11) + 'Line 11</p>');
|
'<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>\
|
baseHtmlDom2 = diffService.htmlToFragment('<p>' + noMarkup(1) + '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">' +
|
<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, ' +
|
||||||
'<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">' +
|
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 ' +
|
||||||
'<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>\
|
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>\
|
<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>' + 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><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>' + noMarkup(10) + '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>' + noMarkup(11) + 'Ned Mamalad auffi i bin a woschechta Bayer greaßt eich nachad, umananda gwiss nia need ' + brMarkup(12) + '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(13) + 'Woibbadinga noch da Giasinga Heiwog Biazelt mechad mim Spuiratz, soi zwoa.</li>\
|
||||||
</ul>\
|
</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">' +
|
<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 ' +
|
||||||
'<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>\
|
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>\
|
<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">' +
|
<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 ' +
|
||||||
'<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>\
|
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>\
|
</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">' +
|
<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 ' +
|
||||||
'<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">' +
|
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 ' +
|
||||||
'<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">' +
|
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 ' +
|
||||||
'<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>');
|
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(baseHtmlDom1);
|
||||||
diffService._insertInternalLineMarkers(baseHtmlDom2);
|
diffService._insertInternalLineMarkers(baseHtmlDom2);
|
||||||
@ -131,7 +141,7 @@ describe('linenumbering', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('extracts lines from a more complex example', 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.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');
|
expect(diff.ancestor.nodeName).toBe('#document-fragment');
|
||||||
@ -143,20 +153,135 @@ describe('linenumbering', function () {
|
|||||||
expect(diff.followingHtmlStartSnippet).toBe('<UL>');
|
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('');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('merging lines into the original motion', function () {
|
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 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 () {
|
it('replaces LIs by a P', function () {
|
||||||
var merged = diffService.replaceLines(baseHtmlDom1, '<p>Replaced a UL by a P</p>', 6, 9);
|
var merged = diffService.replaceLines(baseHtmlDom1, '<p>Replaced a UL by a P</p>', 6, 9);
|
||||||
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>');
|
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 () {
|
it('replaces LIs by another LI', function () {
|
||||||
var merged = diffService.replaceLines(baseHtmlDom1, '<UL class="ul-class"><LI>A new LI</LI></UL>', 6, 9);
|
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,
|
var lineNumberingService,
|
||||||
brMarkup = function (no) {
|
brMarkup = function (no) {
|
||||||
return '<br class="os-line-break">' +
|
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) {
|
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) {
|
longstr = function (length) {
|
||||||
var outstr = '';
|
var outstr = '';
|
||||||
@ -26,6 +26,7 @@ describe('linenumbering', function () {
|
|||||||
it('breaks very short lines', function () {
|
it('breaks very short lines', function () {
|
||||||
var textNode = document.createTextNode("0123");
|
var textNode = document.createTextNode("0123");
|
||||||
lineNumberingService._currentInlineOffset = 0;
|
lineNumberingService._currentInlineOffset = 0;
|
||||||
|
lineNumberingService._currentLineNumber = 1;
|
||||||
var out = lineNumberingService._textNodeToLines(textNode, 5);
|
var out = lineNumberingService._textNodeToLines(textNode, 5);
|
||||||
var outHtml = lineNumberingService._nodesToHtml(out);
|
var outHtml = lineNumberingService._nodesToHtml(out);
|
||||||
expect(outHtml).toBe('0123');
|
expect(outHtml).toBe('0123');
|
||||||
|
Loading…
Reference in New Issue
Block a user