Project change recommendations

This commit is contained in:
FinnStutzenstein 2016-12-19 10:40:55 +01:00
parent 11c0b0cc3f
commit e8fa488d60
17 changed files with 304 additions and 24 deletions

View File

@ -38,6 +38,7 @@ Motions:
motion text.
- Allowed to add change recommendations for special motion text lines
(with diff mode).
- Added projection support for change recommendations.
- Added button to sort and number all motions in a category.
- Added recommendations for motions.
- Added options to calculate percentages on different bases.

View File

@ -28,7 +28,7 @@
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu" aria-labelledby="split-button">
<li role="menuitem" ng-repeat="projector in projectors">
<li role="menuitem" ng-repeat="projector in projectors | orderBy:'id'">
<a href="" ng-click="projectCurrentLoS(projector.id)"
ng-class="{ 'projected': inArray(isCurrentLoSProjected(), projector.id) }">
<i class="fa fa-video-camera" ng-show="inArray(isCurrentLoSProjected(), projector.id) "></i>

View File

@ -27,7 +27,7 @@
<span class="caret"></span>
</button>
<ul class="dropdown-menu" ng-if="projectors.length > 1">
<li role="menuitem" ng-repeat="projector in projectors">
<li role="menuitem" ng-repeat="projector in projectors | orderBy:'id'">
<a href="" ng-click="item.projectListOfSpeakers(projector.id)"
ng-class="{ 'projected': inArray(item.isListOfSpeakersProjected(), projector.id) }">
<i class="fa fa-video-camera" ng-show="inArray(item.isListOfSpeakersProjected(), projector.id) "></i>

View File

@ -44,7 +44,7 @@
</a>
</li>
<li class="divider" ng-show="agendaHasSubitems && projectors.length > 1"></li>
<li role="menuitem" ng-repeat="projector in projectors" ng-show="projectors.length > 1">
<li role="menuitem" ng-repeat="projector in projectors | orderBy:'id'" ng-show="projectors.length > 1">
<a href="" ng-click="projectAgenda(projectorId=projector.id, tree=mainListTree)"
ng-class="{ 'projected': inArray(isAgendaProjected(mainListTree), projector.id) }">
<i class="fa fa-video-camera" ng-show="inArray(isAgendaProjected(mainListTree), projector.id) "></i>
@ -245,7 +245,7 @@
</a>
</li>
<li class="divider" ng-show="item.hasSubitems(items)"></li>
<li role="menuitem" ng-repeat="projector in projectors">
<li role="menuitem" ng-repeat="projector in projectors | orderBy:'id'">
<a href="" ng-click="item.project(projector.id, item.tree)"
ng-class="{ 'projected': inArray(item.isProjected(item.tree), projector.id) }">
<i class="fa fa-video-camera" ng-show="inArray(item.isProjected(item.tree), projector.id)"></i>

View File

@ -623,7 +623,6 @@ img {
color: grey;
}
.diff-box {
background-color: #f9f9f9;
border: solid 1px #eee;

View File

@ -433,7 +433,14 @@ tr.elected td {
.motion-text .highlight {
background-color: #ff0;
}
.motion-text ins {
color: green;
text-decoration: underline;
}
.motion-text del {
color: red;
text-decoration: line-through;
}
.motion-text.line-numbers-outside {
padding-left: 0;
margin-left: 25px;
@ -478,6 +485,122 @@ tr.elected td {
visibility: hidden;
}
/** 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: 5px;
color: red;
}
.diff-box .action-row .btn-edit {
margin-left: 5px;
}
.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;
}
/*** Video and Image projection ***/
img.projector-image {
width: 100%;

View File

@ -1021,6 +1021,7 @@ angular.module('OpenSlidesApp.core.site', [
'$interval',
'$state',
'$q',
'$filter',
'Config',
'Projector',
'CurrentListOfSpeakersItem',
@ -1031,7 +1032,7 @@ angular.module('OpenSlidesApp.core.site', [
'gettextCatalog',
'ngDialog',
'ProjectorMessageForm',
function($scope, $http, $interval, $state, $q, Config, Projector, CurrentListOfSpeakersItem,
function($scope, $http, $interval, $state, $q, $filter, Config, Projector, CurrentListOfSpeakersItem,
ListOfSpeakersOverlay, ProjectionDefault, ProjectorMessage, Countdown, gettextCatalog,
ngDialog, ProjectorMessageForm) {
ProjectorMessage.bindAll({}, $scope, 'messages');
@ -1072,7 +1073,7 @@ angular.module('OpenSlidesApp.core.site', [
}, function () {
$scope.projectors = Projector.getAll();
if (!$scope.active_projector) {
$scope.changeProjector($scope.projectors[0]);
$scope.changeProjector($filter('orderBy')($scope.projectors, 'id')[0]);
}
$scope.messageDefaultProjectorId = ProjectionDefault.filter({name: 'messages'})[0].projector_id;

View File

@ -17,7 +17,7 @@
<span class="caret"></span>
</button>
<ul class="dropdown-menu" uib-dropdown-menu aria-labelledby="menuListOfSpeakers">
<li ng-repeat="projector in projectors">
<li ng-repeat="projector in projectors | orderBy:'id'">
<a href ng-click="setListOfSpeakers(projector)">
<i class="fa fa-check" ng-if="projector.id == currentListOfSpeakers"></i>
{{ projector.name | translate }}

View File

@ -38,7 +38,7 @@
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-entries" aria-labelledby="menuProjector">
<li ng-repeat="projector in projectors">
<li ng-repeat="projector in projectors | orderBy:'id'">
<a href ng-class="{'projected': projector === active_projector}"
ng-click="changeProjector(projector)">
<i ng-show="projector === active_projector" class="fa fa-check"></i>

View File

@ -15,7 +15,7 @@
<span class="caret"></span>
</button>
<ul class="dropdown-menu" ng-if="projectors.length > 1">
<li role="menuitem" ng-repeat="projector in projectors">
<li role="menuitem" ng-repeat="projector in projectors | orderBy:'id'">
<a href="" ng-click="model.project(projector.id, arg)"
ng-class="{ 'projected': inArray(model.isProjected(arg), projector.id) }">
<i class="fa fa-video-camera" ng-show="inArray(model.isProjected(arg), projector.id)"></i>

View File

@ -265,7 +265,7 @@
<span class="caret"></span>
</button>
<ul class="dropdown-menu" ng-if="projectors.length > 1">
<li role="menuitem" ng-repeat="projector in projectors">
<li role="menuitem" ng-repeat="projector in projectors | orderBy:'id'">
<a href="" ng-click="showMediafile(projector.id, mediafile)"
ng-class="{ 'projected': inArray(mediafile.isProjected(), projector.id) }">
<i class="fa fa-video-camera" ng-show="inArray(mediafile.isProjected(), projector.id) "></i>

View File

@ -1,7 +1,7 @@
from ..core.exceptions import ProjectorException
from ..utils.collection import CollectionElement
from ..utils.projector import ProjectorElement
from .models import Motion, MotionBlock
from .models import Motion, MotionBlock, MotionChangeRecommendation
class MotionSlide(ProjectorElement):
@ -26,6 +26,7 @@ class MotionSlide(ProjectorElement):
yield motion.state.workflow
yield from motion.submitters.all()
yield from motion.supporters.all()
yield from MotionChangeRecommendation.objects.filter(motion_version=motion.get_active_version().id)
def get_collection_elements_required_for_this(self, collection_element, config_entry):
output = super().get_collection_elements_required_for_this(collection_element, config_entry)

View File

@ -191,6 +191,7 @@ angular.module('OpenSlidesApp.motions', [
.factory('Motion', [
'DS',
'$http',
'MotionPoll',
'MotionChangeRecommendation',
'MotionComment',
@ -201,8 +202,9 @@ angular.module('OpenSlidesApp.motions', [
'Config',
'lineNumberingService',
'diffService',
function(DS, MotionPoll, MotionChangeRecommendation, MotionComment, jsDataModel, gettext, gettextCatalog, operator, Config,
lineNumberingService, diffService) {
'Projector',
function(DS, $http, MotionPoll, MotionChangeRecommendation, MotionComment, jsDataModel, gettext, gettextCatalog,
operator, Config, lineNumberingService, diffService, Projector) {
var name = 'motions/motion';
return DS.defineResource({
name: name,
@ -469,8 +471,44 @@ angular.module('OpenSlidesApp.motions', [
default:
return false;
}
},
// Overrides from jsDataModel factory
project: function (projectorId, mode) {
// if this object is already projected on projectorId, delete this element from this projector
var isProjectedIds = this.isProjected(mode);
_.forEach(isProjectedIds, function (id) {
$http.post('/rest/core/projector/' + id + '/clear_elements/');
});
mode = mode || 'original';
// Show the element, if it was not projected before on the given projector
if (_.indexOf(isProjectedIds, projectorId) === -1) {
return $http.post(
'/rest/core/projector/' + projectorId + '/prune_elements/',
[{name: name,
id: this.id,
mode: mode}]
);
}
},
isProjected: function (mode) {
var self = this;
var predicate = function (element) {
var value = element.name === name &&
element.id === self.id;
if (mode) {
value = value && (element.mode === mode);
}
return value;
};
var projectorIds = [];
_.forEach(Projector.getAll(), function (projector) {
if (typeof _.findKey(projector.elements, predicate) === 'string') {
projectorIds.push(projector.id);
}
});
return projectorIds;
},
},
relations: {
belongsTo: {
'motions/category': {

View File

@ -21,12 +21,13 @@ angular.module('OpenSlidesApp.motions.projector', [
'$rootScope',
'$http',
'Motion',
'MotionChangeRecommendation',
'User',
'Config',
'Projector',
'$timeout',
'ProjectorID',
function($scope, $rootScope, $http, Motion, User, Config, Projector, $timeout, ProjectorID) {
function($scope, $rootScope, $http, Motion, MotionChangeRecommendation, User, Config, Projector, $timeout, ProjectorID) {
// Attention! Each object that is used here has to be dealt on server side.
// Add it to the coresponding get_requirements method of the ProjectorElement
// class.
@ -58,9 +59,11 @@ angular.module('OpenSlidesApp.motions.projector', [
}
};
$scope.mode = $scope.element.mode || 'original';
Motion.bindOne(id, $scope, 'motion');
User.bindAll({}, $scope, 'users');
MotionChangeRecommendation.bindAll({}, $scope, 'change_recommendations');
}
]);

View File

@ -1116,6 +1116,7 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
$scope.projectors = Projector.getAll();
$scope.defaultProjectorId = ProjectionDefault.filter({name: 'motions'})[0].projector_id;
});
$scope.$watch(function () {
@ -1124,6 +1125,44 @@ angular.module('OpenSlidesApp.motions.site', [
$scope.motion = Motion.get(motion.id);
MotionComment.populateFields($scope.motion);
});
$scope.projectionModes = [
{mode: 'original',
label: 'Original version'},
{mode: 'changed',
label: 'Changed version'},
{mode: 'diff',
label: 'Diff version'},
{mode: 'agreed',
label: 'Resolution'},
];
var getProjectionMode = function () {
var projectedIds = motion.isProjected();
if (projectedIds.length) {
var element = _.find(Projector.get(projectedIds[0]).elements, function (element) {
return element.name === 'motions/motion' && element.id === motion.id;
});
var modeName = element.mode || 'original', mode;
_.forEach($scope.projectionModes, function (_mode) {
if (_mode.mode === modeName) {
mode = _mode;
}
});
return mode || $scope.projectionModes[0];
} else {
return $scope.projectionModes[0];
}
};
$scope.projectionMode = getProjectionMode();
$scope.setProjectionMode = function (mode, event) {
$scope.projectionMode = mode;
var projectedIds = motion.isProjected();
_.forEach(projectedIds, function (id) {
motion.project(id, mode.mode);
});
event.stopPropagation();
};
$scope.commentsFields = Config.get('motions_comments').value;
$scope.commentFieldForState = MotionComment.getFieldNameForFlag('forState');
$scope.commentFieldForRecommendation = MotionComment.getFieldNameForFlag('forRecommendation');

View File

@ -11,9 +11,42 @@
<i class="fa fa-microphone fa-lg"></i>
<translate>List of speakers</translate>
</a>
<!-- project -->
<projector-button model="motion" default-projector-id="defaultProjectorId">
</projector-button>
<!-- project button -->
<div class="btn-group button" uib-dropdown
uib-tooltip="{{ 'Projector' | translate }} {{ motion.isProjected()[0] || '' }} ({{ projectionMode.label | translate }})"
tooltip-enable="motion.isProjected().length"
os-perms="core.can_manage_projector">
<button type="button" class="btn btn-default btn-sm"
title="{{ 'Project motion' | translate }}"
ng-click="motion.project(defaultProjectorId, projectionMode.mode)"
ng-class="{ 'btn-primary': motion.isProjected().length && inArray(motion.isProjected(), defaultProjectorId)}">
<i class="fa fa-video-camera"></i>
</button>
<button type="button" class="btn btn-default btn-sm slimDropDown" uib-dropdown-toggle
ng-if="projectors.length > 1 || change_recommendations.length"
ng-class="{ 'btn-primary': motion.isProjected().length && !inArray(motion.isProjected(), defaultProjectorId)}">
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu" aria-labelledby="split-button"
ng-if="projectors.length > 1 || change_recommendations.length">
<li role="menuitem" ng-repeat="mode in projectionModes" ng-if="change_recommendations.length">
<a href="" ng-click="setProjectionMode(mode, $event);">
<i class="fa" ng-class="mode.mode == $parent.projectionMode.mode ? 'fa-check-square-o' : 'fa-square-o'"></i>
<span ng-if="mode.mode!='agreed'">{{ mode.label | translate }}</span>
<span ng-if="mode.mode=='agreed'"><translate translate-context="decision making">Resolution</translate></span
</a>
</li>
<li class="divider" ng-show="projectors.length > 1 && change_recommendations.length > 0"></li>
<li role="menuitem" ng-repeat="projector in projectors | orderBy:'id'" ng-show="projectors.length > 1">
<a href="" ng-click="motion.project(projector.id, projectionMode.mode)"
ng-class="{ 'projected': inArray(motion.isProjected(), projector.id) }">
<i class="fa fa-video-camera" ng-show="inArray(motion.isProjected(), projector.id) "></i>
{{ projector.name | translate }}
<span ng-if="projector.id === defaultProjectorId">(<translate>Default</translate>)</span>
</a>
</li>
</ul>
</div>
<!-- edit -->
<a ng-if="motion.isAllowed('update')" ng-click="openDialog(motion)"
class="btn btn-default btn-sm"

View File

@ -76,11 +76,53 @@
</div>
<!-- Preamble -->
<div><p>{{ config('motions_preamble') | translate }}</p></div>
<div><p>{{ config('motions_preamble') | translate }}</p></div><br>
<!-- Text -->
<div ng-bind-html="motion.getTextWithLineBreaks(null, line, scroll) | trusted"
class="motion-text line-numbers-{{ config('motions_default_line_numbering') }}"></div>
<!-- <div ng-bind-html="motion.getTextWithLineBreaks(null, line, scroll) | trusted"
class="motion-text line-numbers-{{ config('motions_default_line_numbering') }}"></div> -->
<!-- Original view -->
<div ng-if="mode == 'original'">
<div id="view-original-inline-editor" ng-bind-html="motion.getTextWithLineBreaks(null, line, scroll) | trusted"
class="motion-text motion-text-original line-numbers-{{ config('motions_default_line_numbering') }}"
contenteditable="false"></div>
</div>
<!-- Diff View -->
<div ng-if="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-{{ config('motions_default_line_numbering') }}">
<div ng-repeat="change in (changes = (change_recommendations | orderBy: 'line_from')) ">
<div class="motion-text original-text line-numbers-{{ config('motions_default_line_numbering') }}"
ng-bind-html="motion.getTextBetweenChangeRecommendations(null, changes[$index - 1], change, line) | trusted">
</div>
<div class="diff-box diff-box-{{ change.id }} clearfix">
<div class="motion-text motion-text-diff line-numbers-{{ lineNumberMode }}"
ng-bind-html="change.getDiff(motion, null, line) | trusted">
</div>
</div>
<div class="motion-text original-text line-numbers-{{ config('motions_default_line_numbering') }}"
ng-bind-html="motion.getTextRemainderAfterLastChangeRecommendation(null, changes, line) | trusted">
</div>
</div>
</div>
</div>
<!-- Changed View -->
<div ng-if="mode == 'changed'">
<div ng-bind-html="motion.getTextByMode('changed', null, line) | trusted"
class="motion-text motion-text-changed line-numbers-{{ config('motions_default_line_numbering') }}"></div>
</div>
<!-- Agreed View -->
<div ng-if="mode == 'agreed'">
<div ng-bind-html="motion.getTextByMode('agreed', null, line) | trusted"
class="motion-text motion-text-changed line-numbers-{{ config('motions_default_line_numbering') }}"></div>
</div>
<!-- Reason -->
<h3 ng-if="motion.getReason()" translate>Reason</h3>