Added support for multiple projectors.

This commit is contained in:
Finn Stutzenstein 2016-09-12 11:05:34 +02:00 committed by Norman Jäckel
parent 447d475321
commit e6b9b21d41
51 changed files with 2272 additions and 813 deletions

View File

@ -20,6 +20,7 @@ Core:
- Added support for big assemblies with lots of users.
- Added HTML support for messages on the projector.
- Moved custom slides to own app "topics". Renamed it to "Topic".
- Added support for multiple projectors
Motions:
- Added origin field.

View File

@ -64,3 +64,32 @@ class ListOfSpeakersSlide(ProjectorElement):
# Full update if item changes because then we may have new speakers
# and therefor need new users.
return collection_element.collection_string == Item.get_collection_string()
class CurrentListOfSpeakersSlide(ProjectorElement):
"""
Slide for the current list of speakers.
Nothing special to check.
"""
name = 'agenda/current-list-of-speakers'
def get_requirements(self, config_entry):
pk = config['projector_currentListOfSpeakers_reference']
if pk is not None:
# List of speakers slide.
try:
item = Item.objects.get(pk=pk)
except Item.DoesNotExist:
# Item does not exist. Just do nothing.
pass
else:
yield item
for speaker in item.speakers.filter(end_time=None):
# Yield current speaker and next speakers
yield speaker.user
query = (item.speakers.exclude(end_time=None)
.order_by('-end_time')[:config['agenda_show_last_speakers']])
for speaker in query:
# Yield last speakers
yield speaker.user

View File

@ -112,61 +112,84 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
}
},
// override project function of jsDataModel factory
project: function() {
return $http.post(
'/rest/core/projector/1/prune_elements/',
[{name: this.content_object.collection, id: this.content_object.id}]
);
project: function (projectorId, tree) {
var isProjectedId = this.isProjected(tree);
if (isProjectedId > 0) {
// Deactivate
$http.post('/rest/core/projector/' + isProjectedId + '/clear_elements/');
}
// Activate, if the projector_id is a new projector.
if (isProjectedId != projectorId) {
var name = tree ? 'agenda/item-list' : this.content_object.collection;
var id = tree ? this.id : this.content_object.id;
return $http.post(
'/rest/core/projector/' + projectorId + '/prune_elements/',
[{name: name, tree: tree, id: id}]
);
}
},
// override isProjected function of jsDataModel factory
isProjected: function (list) {
// Returns true if there is a projector element with the same
// name and the same id.
var projector = Projector.get(1);
var isProjected;
if (typeof projector !== 'undefined') {
var self = this;
var predicate = function (element) {
var value;
if (typeof list === 'undefined') {
// Releated item detail slide
value = element.name == self.content_object.collection &&
typeof element.id !== 'undefined' &&
element.id == self.content_object.id;
} else {
// Item list slide for sub tree
value = element.name == 'agenda/item-list' &&
typeof element.id !== 'undefined' &&
element.id == self.id;
}
return value;
};
isProjected = typeof _.findKey(projector.elements, predicate) === 'string';
} else {
isProjected = false;
isProjected: function (tree) {
// Returns the id of the last projector with an agenda-item element. Else return 0.
if (typeof tree === 'undefined') {
tree = false;
}
var self = this;
var predicate = function (element) {
var value;
if (tree) {
// Item tree slide for sub tree
value = element.name == 'agenda/item-list' &&
typeof element.id !== 'undefined' &&
element.id == self.id;
} else {
// Releated item detail slide
value = element.name == self.content_object.collection &&
typeof element.id !== 'undefined' &&
element.id == self.content_object.id;
}
return value;
};
var isProjected = 0;
Projector.getAll().forEach(function (projector) {
if (typeof _.findKey(projector.elements, predicate) === 'string') {
isProjected = projector.id;
}
});
return isProjected;
},
// project list of speakers
projectListOfSpeakers: function() {
return $http.post(
'/rest/core/projector/1/prune_elements/',
[{name: 'agenda/list-of-speakers', id: this.id}]
);
projectListOfSpeakers: function(projectorId) {
var isProjectedId = this.isListOfSpeakersProjected();
if (isProjectedId > 0) {
// Deactivate
$http.post('/rest/core/projector/' + isProjectedId + '/clear_elements/');
}
// Activate
if (isProjectedId != projectorId) {
return $http.post(
'/rest/core/projector/' + projectorId + '/prune_elements/',
[{name: 'agenda/list-of-speakers', id: this.id}]
);
}
},
// check if list of speakers is projected
isListOfSpeakersProjected: function () {
// Returns true if there is a projector element with the
// Returns the id of the last projector with an element with the
// name 'agenda/list-of-speakers' and the same id.
var projector = Projector.get(1);
if (typeof projector === 'undefined') return false;
var self = this;
var predicate = function (element) {
return element.name == 'agenda/list-of-speakers' &&
typeof element.id !== 'undefined' &&
element.id == self.id;
};
return typeof _.findKey(projector.elements, predicate) === 'string';
var isProjected = 0;
Projector.getAll().forEach(function (projector) {
if (typeof _.findKey(projector.elements, predicate) === 'string') {
isProjected = projector.id;
}
});
return isProjected;
},
hasSubitems: function(items) {
var self = this;
@ -265,6 +288,56 @@ angular.module('OpenSlidesApp.agenda', ['OpenSlidesApp.users'])
}
])
// TODO: Remove all find() calls from the projector logic. It is also used on the site so this has to be
// changed with the refactoring of the site autoupdate.
.factory('CurrentListOfSpeakersItem', [
'Projector',
'Assignment', // TODO: Remove this after refactoring of data loading on start.
'Topic', // TODO: Remove this after refactoring of data loading on start.
'Motion', // TODO: Remove this after refactoring of data loading on start.
'Agenda',
function (Projector, Assignment, Topic, Motion, Agenda) {
return {
getItem: function (projectorId) {
var elementPromise;
return Projector.find(projectorId).then(function (projector) {
// scan all elements
_.forEach(projector.elements, function(element) {
switch(element.name) {
case 'motions/motion':
elementPromise = Motion.find(element.id).then(function(motion) {
return Motion.loadRelations(motion, 'agenda_item').then(function() {
return motion.agenda_item;
});
});
break;
case 'topics/topic':
elementPromise = Topic.find(element.id).then(function(topic) {
return Topic.loadRelations(topic, 'agenda_item').then(function() {
return topic.agenda_item;
});
});
break;
case 'assignments/assignment':
elementPromise = Assignment.find(element.id).then(function(assignment) {
return Assignment.loadRelations(assignment, 'agenda_item').then(function() {
return assignment.agenda_item;
});
});
break;
case 'agenda/list-of-speakers':
elementPromise = Agenda.find(element.id).then(function(item) {
return item;
});
}
});
return elementPromise;
});
}
};
}
])
// Make sure that the Agenda resource is loaded.
.run(['Agenda', function(Agenda) {}]);

View File

@ -13,6 +13,39 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda'])
slidesProvider.registerSlide('agenda/item-list', {
template: 'static/templates/agenda/slide-item-list.html',
});
slidesProvider.registerSlide('agenda/current-list-of-speakers', {
template: 'static/templates/agenda/slide-current-list-of-speakers.html',
});
}
])
.controller('SlideCurrentListOfSpeakersCtrl', [
'$scope',
'Agenda',
'CurrentListOfSpeakersItem',
'Config',
function ($scope, Agenda, CurrentListOfSpeakersItem, Config) {
// Watch for changes in the current list of speakers reference
$scope.$watch(function () {
return Config.lastModified('projector_currentListOfSpeakers_reference');
}, function () {
$scope.currentListOfSpeakersReference = $scope.config('projector_currentListOfSpeakers_reference');
$scope.updateCurrentListOfSpeakers();
});
// Watch for changes in the current item.
$scope.$watch(function () {
return Agenda.lastModified();
}, function () {
$scope.updateCurrentListOfSpeakers();
});
$scope.updateCurrentListOfSpeakers = function () {
var itemPromise = CurrentListOfSpeakersItem.getItem($scope.currentListOfSpeakersReference);
if (itemPromise) {
itemPromise.then(function(item) {
$scope.agendaItem = item;
});
}
};
}
])
@ -20,7 +53,7 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda'])
'$scope',
'Agenda',
'User',
function($scope, Agenda, User) {
function ($scope, Agenda, User) {
// 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.
@ -35,7 +68,7 @@ angular.module('OpenSlidesApp.agenda.projector', ['OpenSlidesApp.agenda'])
'$filter',
'Agenda',
'AgendaTree',
function($scope, $http, $filter, Agenda, AgendaTree) {
function ($scope, $http, $filter, Agenda, AgendaTree) {
// 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.

View File

@ -99,7 +99,8 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
'TopicForm', // TODO: Remove this dependency. Use template hook for "New" and "Import" buttons.
'AgendaTree',
'Projector',
function($scope, $filter, $http, $state, DS, operator, ngDialog, Agenda, TopicForm, AgendaTree, Projector) {
'ProjectionDefault',
function($scope, $filter, $http, $state, DS, operator, ngDialog, Agenda, TopicForm, AgendaTree, Projector, ProjectionDefault) {
// Bind agenda tree to the scope
$scope.$watch(function () {
return Agenda.lastModified();
@ -110,6 +111,17 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
$scope.agendaHasSubitems = true;
}
});
Projector.bindAll({}, $scope, 'projectors');
$scope.mainListTree = true;
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var projectiondefault = ProjectionDefault.filter({name: 'agenda_all_items'})[0];
if (projectiondefault) {
$scope.defaultProjectorId_all_items = projectiondefault.projector_id;
}
$scope.projectionDefaults = ProjectionDefault.getAll();
});
$scope.alert = {};
$scope.sumDurations = function () {
@ -218,34 +230,78 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
$scope.uncheckAll();
};
/** Project functions **/
// get ProjectionDefault for item
$scope.getProjectionDefault = function (item) {
if (item.tree) {
return $scope.defaultProjectorId_all_items;
} else {
var app_name = item.content_object.collection.split('/')[0];
var id = 1;
$scope.projectionDefaults.forEach(function (projectionDefault) {
if (projectionDefault.name == app_name) {
id = projectionDefault.projector_id;
}
});
return id;
}
};
// project agenda
$scope.projectAgenda = function (tree, id) {
$http.post('/rest/core/projector/1/prune_elements/',
$scope.projectAgenda = function (projectorId, tree, id) {
var isAgendaProjectedId = $scope.isAgendaProjected($scope.mainListTree);
if (isAgendaProjectedId > 0) {
// Deactivate
$http.post('/rest/core/projector/' + isAgendaProjectedId + '/prune_elements/', []);
}
if (isAgendaProjectedId != projectorId) {
$http.post('/rest/core/projector/' + projectorId + '/prune_elements/',
[{name: 'agenda/item-list', tree: tree, id: id}]);
}
};
// change whether all items or only main items should be projected
$scope.changeMainListTree = function () {
var isAgendaProjectedId = $scope.isAgendaProjected($scope.mainListTree);
$scope.mainListTree = !$scope.mainListTree;
if (isAgendaProjectedId > 0) {
$scope.projectAgenda(isAgendaProjectedId, $scope.mainListTree);
}
};
// change whether one item or all subitems should be projected
$scope.changeItemTree = function (item) {
var isProjected = item.isProjected(item.tree);
if (isProjected > 0) {
// Deactivate and reactivate
item.project(isProjected, item.tree);
item.project(isProjected, !item.tree);
}
item.tree = !item.tree;
};
// check if agenda is projected
$scope.isAgendaProjected = function (tree) {
// Returns true if there is a projector element with the name
// 'agenda/item-list'.
var projector = Projector.get(1);
if (typeof projector === 'undefined') return false;
var self = this;
var predicate = function (element) {
var value;
if (typeof tree === 'undefined') {
// only main agenda items
value = element.name == 'agenda/item-list' &&
typeof element.id === 'undefined' &&
!element.tree;
} else {
if (tree) {
// tree with all agenda items
value = element.name == 'agenda/item-list' &&
typeof element.id === 'undefined' &&
element.tree;
} else {
// only main agenda items
value = element.name == 'agenda/item-list' &&
typeof element.id === 'undefined' &&
!element.tree;
}
return value;
};
return typeof _.findKey(projector.elements, predicate) === 'string';
var projectorId = 0;
$scope.projectors.forEach(function (projector) {
if (typeof _.findKey(projector.elements, predicate) === 'string') {
projectorId = projector.id;
}
});
return projectorId;
};
// auto numbering of agenda items
$scope.autoNumbering = function() {
@ -263,9 +319,24 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
'Agenda',
'User',
'item',
function ($scope, $filter, $http, $state, operator, Agenda, User, item) {
'Projector',
'ProjectionDefault',
function ($scope, $filter, $http, $state, operator, Agenda, User, item, Projector, ProjectionDefault) {
Agenda.bindOne(item.id, $scope, 'item');
User.bindAll({}, $scope, 'users');
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var item_app_name = item.content_object.collection.split('/')[0];
var projectiondefaultItem = ProjectionDefault.filter({name: item_app_name})[0];
if (projectiondefaultItem) {
$scope.defaultProjectorItemId = projectiondefaultItem.projector_id;
}
var projectiondefaultListOfSpeakers = ProjectionDefault.filter({name: 'list_of_speakers'})[0];
if (projectiondefaultListOfSpeakers) {
$scope.defaultProjectorListOfSpeakersId = projectiondefaultListOfSpeakers.projector_id;
}
});
$scope.speakerSelectBox = {};
$scope.alert = {};
$scope.speakers = $filter('orderBy')(item.speakers, 'weight');
@ -428,50 +499,69 @@ angular.module('OpenSlidesApp.agenda.site', ['OpenSlidesApp.agenda'])
'$state',
'$http',
'Projector',
'Assignment', // TODO: Remove this after refactoring of data loading on start.
'Topic', // TODO: Remove this after refactoring of data loading on start.
'Motion', // TODO: Remove this after refactoring of data loading on start.
'Agenda',
function($scope, $state, $http, Projector, Assignment, Topic, Motion, Agenda) {
$scope.$watch(
function() {
return Projector.lastModified(1);
},
function() {
Projector.find(1).then( function(projector) {
$scope.AgendaItem = null;
_.forEach(projector.elements, function(element) {
switch(element.name) {
case 'motions/motion':
Motion.find(element.id).then(function(motion) {
Motion.loadRelations(motion, 'agenda_item').then(function() {
$scope.AgendaItem = motion.agenda_item;
});
});
break;
case 'topics/topic':
Topic.find(element.id).then(function(topic) {
Topic.loadRelations(topic, 'agenda_item').then(function() {
$scope.AgendaItem = topic.agenda_item;
});
});
break;
case 'assignments/assignment':
Assignment.find(element.id).then(function(assignment) {
Assignment.loadRelations(assignment, 'agenda_item').then(function() {
$scope.AgendaItem = assignment.agenda_item;
});
});
break;
case 'agenda/list-of-speakers':
Agenda.find(element.id).then(function(item) {
$scope.AgendaItem = item;
});
}
});
'ProjectionDefault',
'Config',
'CurrentListOfSpeakersItem',
function($scope, $state, $http, Projector, ProjectionDefault, Config, CurrentListOfSpeakersItem) {
// Watch for changes in the current list of speakers reference
$scope.$watch(function () {
return Config.lastModified('projector_currentListOfSpeakers_reference');
}, function () {
$scope.currentListOfSpeakersReference = $scope.config('projector_currentListOfSpeakers_reference');
$scope.updateCurrentListOfSpeakers();
});
$scope.$watch(function() {
return Projector.lastModified();
}, function() {
$scope.projectors = Projector.getAll();
$scope.updateCurrentListOfSpeakers();
});
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var projectiondefault = ProjectionDefault.filter({name: 'current_list_of_speakers'})[0];
if (projectiondefault) {
$scope.defaultProjectorId = projectiondefault.projector_id;
}
});
$scope.updateCurrentListOfSpeakers = function () {
var itemPromise = CurrentListOfSpeakersItem.getItem($scope.currentListOfSpeakersReference);
if (itemPromise) {
itemPromise.then(function(item) {
$scope.AgendaItem = item;
});
}
);
};
// Project current list of speakers
// same logic as in core/base.js
$scope.projectCurrentLoS = function (projectorId) {
var isCurrentLoSProjectedId = $scope.isCurrentLoSProjected($scope.mainListTree);
if (isCurrentLoSProjectedId > 0) {
// Deactivate
$http.post('/rest/core/projector/' + isCurrentLoSProjectedId + '/prune_elements/', []);
}
if (isCurrentLoSProjectedId != projectorId) {
$http.post('/rest/core/projector/' + projectorId + '/prune_elements/',
[{name: 'agenda/current-list-of-speakers'}]);
}
};
// same logic as in core/base.js
$scope.isCurrentLoSProjected = function () {
// Returns the projector id if there is a projector element with the name
// 'agenda/current-list-of-speakers'. Elsewise returns 0.
var projectorId = 0;
$scope.projectors.forEach(function (projector) {
var key = _.findKey(projector.elements, function (element) {
return element.name == 'agenda/current-list-of-speakers';
});
if (typeof key === 'string') {
projectorId = projector.id;
}
});
return projectorId;
};
// go to the list of speakers (management) of the currently
// displayed projector slide
$scope.goToListOfSpeakers = function() {

View File

@ -1,48 +1,61 @@
<div class="header">
<div class="title">
<div class="submenu">
<button ng-click="isFullScreen = !isFullScreen"
class="btn btn-sm btn-default">
<i class="fa fa-expand fa-lg"></i>
<translate>Fullscreen</translate>
</button>
<button os-perms="agenda.can_manage"
ng-click="goToListOfSpeakers()"
class="btn btn-sm btn-default">
<i class="fa fa-microphone"></i>
<translate>Manage list</translate>
</button>
<div class="form-inline">
<div os-perms="core.can_manage_projector" class="btn-group" uib-dropdown
uib-tooltip="{{ 'Projector' | translate }} {{ isCurrentLoSProjected() }}"
tooltip-enable="isCurrentLoSProjected() > 0">
<button type="button" class="btn btn-default btn-sm"
title="{{ 'Project current list of speakers' | translate }}"
ng-click="projectCurrentLoS(defaultProjectorId)"
ng-class="{ 'btn-primary': isCurrentLoSProjected() > 0 && isCurrentLoSProjected() == defaultProjectorId}">
<i class="fa fa-video-camera"></i>
<translate>Current list of speakers</translate>
</button>
<button type="button" class="btn btn-default btn-sm" uib-dropdown-toggle
ng-class="{ 'btn-primary': isCurrentLoSProjected() > 0 && isCurrentLoSProjected() != defaultProjectorId}">
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu" aria-labelledby="split-button">
<li role="menuitem" ng-repeat="projector in projectors">
<a href="" ng-click="projectCurrentLoS(projector.id)"
ng-class="{ 'projected': isCurrentLoSProjected() == projector.id }">
<i class="fa fa-video-camera" ng-show="isCurrentLoSProjected() == projector.id"></i>
{{ projector.name }}
<span ng-if="projector.id == defaultProjectorId">(<translate>Standard</translate>)</span>
</a>
</li>
</ul>
</div>
<button os-perms="agenda.can_manage"
ng-click="goToListOfSpeakers()"
class="btn btn-sm btn-default">
<i class="fa fa-microphone"></i>
<translate>Manage list</translate>
</button>
</div>
</div>
<h1 translate>List of speakers</h1>
<h1 translate>Current list of speakers</h1>
<h2> {{ AgendaItem.getTitle() }}
<span class="slimlabel label label-danger ng-scope" style="" ng-if="AgendaItem.speaker_list_closed" translate>
Closed
</span>
</h2>
</div>
</div>
<div class="content" ng-class="isFullScreen ? 'fullscreendiv' : 'details'"
ng-click="isFullScreen? (isFullScreen = !isFullScreen) : a">
<div ng-if="isFullScreen" class="fullscreendiv-title">
<h1 translate>List of speakers</h1>
<h2> {{ AgendaItem.getTitle() }}
<span class="slimlabel label label-danger ng-scope" style=""
ng-if="AgendaItem.speaker_list_closed" translate>Closed</span>
</h2>
</div>
<div class="content">
<!-- Last speakers -->
<p ng-repeat="speaker in lastSpeakers = (AgendaItem.speakers | filter: {end_time: '!!', begin_time: '!!'}) |
limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))" class="lastSpeakers">
{{ speaker.user.get_full_name() }}
<!-- Current speaker -->
<p ng-repeat="speaker in currentspeakers = (AgendaItem.speakers| filter: {end_time: null, begin_time: '!!'})"
class="currentSpeaker">
<i class="fa fa-microphone fa-lg"></i> {{ speaker.user.get_full_name() }}
<!-- Next speakers -->
<ol class="nextSpeakers">
<li ng-repeat="speaker in AgendaItem.speakers | filter: {begin_time: null} | orderBy:'weight'">
{{ speaker.user.get_full_name() }}
</ol>
</div>
<div class="details">
<!-- Last speakers -->
<p ng-repeat="speaker in lastSpeakers = (AgendaItem.speakers | filter: {end_time: '!!', begin_time: '!!'}) |
limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))" class="lastSpeakers">
{{ speaker.user.get_full_name() }}
<!-- Current speaker -->
<p ng-repeat="speaker in currentspeakers = (AgendaItem.speakers| filter: {end_time: null, begin_time: '!!'})"
class="currentSpeaker">
<i class="fa fa-microphone fa-lg"></i> {{ speaker.user.get_full_name() }}
<!-- Next speakers -->
<ol class="nextSpeakers">
<li ng-repeat="speaker in AgendaItem.speakers | filter: {begin_time: null} | orderBy:'weight'">
{{ speaker.user.get_full_name() }}
</ol>
</div>

View File

@ -10,20 +10,37 @@
{{ item.getContentResource().verboseName | translate }}
</a>
<!-- project list of speakers -->
<a os-perms="core.can_manage_projector" class="btn btn-default btn-sm"
ng-class="{ 'btn-primary': item.isListOfSpeakersProjected() }"
ng-click="item.projectListOfSpeakers()">
<i class="fa fa-video-camera"></i>
<translate>List of speakers</translate>
</a>
<span class="btn-group" style="min-width:54px;" uib-dropdown
uib-tooltip="{{ 'Projektor' | translate }} {{ item.isListOfSpeakersProjected() }}"
tooltip-enable="item.isListofSpeakersProjected() > 0"
os-perms="core.can_manage_projector">
<button type="button" class="btn btn-default btn-sm"
ng-click="item.projectListOfSpeakers(defaultProjectorListOfSpeakersId)"
ng-class="{ 'btn-primary': item.isListOfSpeakersProjected() == defaultProjectorListOfSpeakersId }">
<i class="fa fa-video-camera"></i>
<translate>List of speakers</translate>
</button>
<button type="button" class="btn btn-default btn-sm slimDropDown"
ng-class="{ 'btn-primary': (item.isListOfSpeakersProjected() > 0 && item.isListOfSpeakersProjected() != defaultProjectorListOfSpeakersId) }"
ng-if="projectors.length > 1"
uib-dropdown-toggle>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li role="menuitem" ng-repeat="projector in projectors">
<a href="" ng-click="item.projectListOfSpeakers(projector.id)"
ng-class="{ 'projected': (item.isListOfSpeakersProjected() == projector.id) }">
<i class="fa fa-video-camera" ng-show="item.isListOfSpeakersProjected() == projector.id"></i>
{{ projector.name }}
<span ng-if="defaultProjectorListOfSpeakersId == projector.id">(<translate>Standard</translate>)</span>
</a>
</li>
</ul>
</span>
<!-- project -->
<a os-perms="core.can_manage_projector" class="btn btn-default btn-sm"
ng-class="{ 'btn-primary': item.isProjected() }"
ng-click="item.project()"
title="{{ 'Project item' | translate }}">
<i class="fa fa-video-camera"></i>
{{ item.getContentResource().verboseName | translate }}
</a>
<projector-button model="item" default-projector-id="defaultProjectorItemId"
content="{{ item.getContentResource().verboseName | translate }}">
</projector-button>
</div>
<h1>{{ item.getTitle() }}</h1>
<h2>

View File

@ -38,26 +38,37 @@
<translate>Select ...</translate>
</button>
<!-- project agenda button -->
<div os-perms="core.can_manage_projector" class="btn-group" uib-dropdown>
<button
id="project-agenda-button"
type="button"
class="btn btn-default"
<div class="btn-group" uib-dropdown
uib-tooltip="{{ 'Projector' | translate }} {{ isAgendaProjected(mainListTree) }}"
tooltip-enable="isAgendaProjected(mainListTree) > 0"
os-perms="core.can_manage_projector">
<button type="button" class="btn btn-default"
title="{{ 'Project agenda' | translate }}"
ng-click="projectAgenda(tree=true)"
ng-class="{ 'btn-primary': isAgendaProjected(tree=true) }">
ng-click="projectAgenda(defaultProjectorId_all_items, mainListTree)"
ng-class="{ 'btn-primary': isAgendaProjected(mainListTree) > 0 && isAgendaProjected(mainListTree) == defaultProjectorId_all_items}">
<i class="fa fa-video-camera"></i>
<translate>Agenda</translate>
</button>
<button type="button" class="btn btn-default"
ng-if="agendaHasSubitems"
ng-class="{ 'btn-primary': isAgendaProjected() }"
uib-dropdown-toggle>
<button type="button" class="btn btn-default" uib-dropdown-toggle
ng-class="{ 'btn-primary': isAgendaProjected(mainListTree) > 0 && isAgendaProjected(mainListTree) != defaultProjectorId_all_items}">
<span class="caret"></span>
</button>
<ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="project-agenda-button">
<li role="menuitem"><a href="" ng-click="projectAgenda(tree=true)" translate>All agenda items (Default)</a>
<li role="menuitem"><a href="" ng-click="projectAgenda(tree=false)" translate>Only main agenda items</a>
<ul class="dropdown-menu" role="menu" aria-labelledby="split-button">
<li role="menuitem" ng-show="agendaHasSubitems">
<a href="" ng-click="changeMainListTree(); $event.stopPropagation();">
<i class="fa" ng-class="mainListTree ? 'fa-square-o' : 'fa-check-square-o'"></i>
<translate>Only main agenda items</translate>
</a>
</li>
<li class="divider" ng-show="agendaHasSubitems"></li>
<li role="menuitem" ng-repeat="projector in projectors">
<a href="" ng-click="projectAgenda(projectorId=projector.id, tree=mainListTree)"
ng-class="{ 'projected': isAgendaProjected(mainListTree) == projector.id }">
<i class="fa fa-video-camera" ng-show="isAgendaProjected(mainListTree) == projector.id"></i>
{{ projector.name }}
<span ng-if="projector.id == defaultProjectorId_all_items">(<translate>Standard</translate>)</span>
</a>
</li>
</ul>
</div>
<!-- auto numbering button -->
@ -71,7 +82,7 @@
<a os-perms="users.can_see_name" class="btn btn-default"
ui-sref="agenda.current-list-of-speakers">
<i class="fa fa-microphone"></i>
<translate>List of speakers</translate>
<translate>Current list of speakers</translate>
</a>
</div>
</div>
@ -150,25 +161,37 @@
ng-class="{ 'activeline': item.isProjected(), 'selected': item.selected, 'hiddenrow': item.is_hidden}">
<!-- projector column -->
<td ng-show="!isDeleteMode" os-perms="core.can_manage_projector">
<div class="btn-group" style="width:54px;" uib-dropdown>
<button os-perms="core.can_manage_projector"
id="project-item"
type="button"
class="btn btn-default btn-sm"
<div class="btn-group" style="min-width:54px;" uib-dropdown
uib-tooltip="{{ 'Projector' | translate }} {{ item.isProjected(item.tree) }}"
tooltip-enable="item.isProjected(item.tree) > 0">
<button class="btn btn-default btn-sm"
title="{{ 'Project item' | translate }}"
ng-click="item.project()"
ng-class="{ 'btn-primary': item.isProjected() }">
ng-click="item.project(getProjectionDefault(item), item.tree)"
ng-class="{ 'btn-primary': item.isProjected(item.tree) > 0 && item.isProjected(item.tree) == getProjectionDefault(item)}">
<i class="fa fa-video-camera"></i>
</button>
<button type="button" class="btn btn-default btn-sm slimDropDown"
ng-if="item.hasSubitems(items)"
ng-class="{ 'btn-primary': item.isProjected(list=true) }"
ng-class="{ 'btn-primary': item.isProjected(item.tree) > 0 && item.isProjected(item.tree) != getProjectionDefault(item)}"
ng-show="item.hasSubitems(items) || projectors.length > 1"
uib-dropdown-toggle>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="project-item">
<li role="menuitem"><a href="" ng-click="item.project()" translate>Project item (Default)</a>
<li role="menuitem"><a href="" ng-click="projectAgenda(tree=true, id=item.id)" translate>Project all sub items</a>
<ul class="dropdown-menu" role="menu" aria-labelledby="split-button">
<li role="menuitem" ng-show="item.hasSubitems(items)">
<a href="" ng-click="changeItemTree(item); $event.stopPropagation();">
<i class="fa" ng-class="item.tree ? 'fa-check-square-o' : 'fa-square-o'"></i>
<translate>Include all sub items</translate>
</a>
</li>
<li class="divider" ng-show="item.hasSubitems(items)"></li>
<li role="menuitem" ng-repeat="projector in projectors">
<a href="" ng-click="item.project(projector.id, item.tree)"
ng-class="{ 'projected': item.isProjected(item.tree) == projector.id }">
<i class="fa fa-video-camera" ng-show="item.isProjected(item.tree) == projector.id"></i>
{{ projector.name }}
<span ng-if="projector.id == getProjectionDefault(item)">(<translate>Standard</translate>)</span>
</a>
</li>
</ul>
</div>
<!-- delete selection column -->

View File

@ -0,0 +1,23 @@
<div ng-controller="SlideCurrentListOfSpeakersCtrl" class="content scrollcontent">
<div class="title">
<h1 translate>Current List of speakers</h1>
<h2> {{ agendaItem.getTitle() }}
<span class="slimlabel label label-danger ng-scope" style=""
ng-if="agendaItem.speaker_list_closed" translate>Closed</span>
</h2>
</div>
<!-- Last speakers -->
<p ng-repeat="speaker in lastSpeakers = (agendaItem.speakers | filter: {end_time: '!!', begin_time: '!!'}) |
limitTo: config('agenda_show_last_speakers') : (lastSpeakers.length - config('agenda_show_last_speakers'))" class="lastSpeakers">
{{ speaker.user.get_full_name() }}
<!-- Current speaker -->
<p ng-repeat="speaker in currentspeakers = (agendaItem.speakers| filter: {end_time: null, begin_time: '!!'})"
class="currentSpeaker">
<i class="fa fa-microphone fa-lg"></i> {{ speaker.user.get_full_name() }}
<!-- Next speakers -->
<ol class="nextSpeakers">
<li ng-repeat="speaker in agendaItem.speakers | filter: {begin_time: null} | orderBy:'weight'">
{{ speaker.user.get_full_name() }}
</ol>
</div>

View File

@ -267,42 +267,47 @@ angular.module('OpenSlidesApp.assignments', [])
return "Election";
},
// override project function of jsDataModel factory
project: function (poll_id) {
return $http.post(
'/rest/core/projector/1/prune_elements/',
[{name: 'assignments/assignment', id: this.id, poll: poll_id}]
);
project: function (projectorId, pollId) {
var isProjectedId = this.isProjected(pollId);
if (isProjectedId > 0) {
$http.post('/rest/core/projector/' + isProjectedId + '/clear_elements/');
}
if (isProjectedId != projectorId) {
return $http.post(
'/rest/core/projector/' + projectorId + '/prune_elements/',
[{name: 'assignments/assignment', id: this.id, poll: pollId}]
);
}
},
// override isProjected function of jsDataModel factory
isProjected: function (poll_id) {
// Returns true if there is a projector element with the name
// 'assignments/assignment'.
var projector = Projector.get(1);
var isProjected;
if (typeof projector !== 'undefined') {
var self = this;
var predicate = function (element) {
var value;
if (typeof poll_id === 'undefined') {
// Assignment detail slide without poll
value = element.name == 'assignments/assignment' &&
typeof element.id !== 'undefined' &&
element.id == self.id &&
typeof element.poll === 'undefined';
} else {
// Assignment detail slide with specific poll
value = element.name == 'assignments/assignment' &&
typeof element.id !== 'undefined' &&
element.id == self.id &&
typeof element.poll !== 'undefined' &&
element.poll == poll_id;
}
return value;
};
isProjected = typeof _.findKey(projector.elements, predicate) === 'string';
} else {
isProjected = false;
}
// Returns the id of the last projector found with an element
// with the name 'assignments/assignment'.
var self = this;
var predicate = function (element) {
var value;
if (typeof poll_id === 'undefined') {
// Assignment detail slide without poll
value = element.name == 'assignments/assignment' &&
typeof element.id !== 'undefined' &&
element.id == self.id &&
typeof element.poll === 'undefined';
} else {
// Assignment detail slide with specific poll
value = element.name == 'assignments/assignment' &&
typeof element.id !== 'undefined' &&
element.id == self.id &&
typeof element.poll !== 'undefined' &&
element.poll == poll_id;
}
return value;
};
var isProjected = 0;
Projector.getAll().forEach(function (projector) {
if (typeof _.findKey(projector.elements, predicate) === 'string') {
isProjected = projector.id;
}
});
return isProjected;
}
},

View File

@ -233,9 +233,19 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
'Tag',
'Agenda',
'phases',
function($scope, ngDialog, AssignmentForm, Assignment, Tag, Agenda, phases) {
'Projector',
'ProjectionDefault',
function($scope, ngDialog, AssignmentForm, Assignment, Tag, Agenda, phases, Projector, ProjectionDefault) {
Assignment.bindAll({}, $scope, 'assignments');
Tag.bindAll({}, $scope, 'tags');
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var projectiondefault = ProjectionDefault.filter({name: 'assignments'})[0];
if (projectiondefault) {
$scope.defaultProjectorId = projectiondefault.projector_id;
}
});
$scope.phases = phases;
$scope.alert = {};
@ -339,10 +349,21 @@ angular.module('OpenSlidesApp.assignments.site', ['OpenSlidesApp.assignments'])
'User',
'assignment',
'phases',
function($scope, $http, filterFilter, gettext, ngDialog, AssignmentForm, operator, Assignment, User, assignment, phases) {
'Projector',
'ProjectionDefault',
function($scope, $http, filterFilter, gettext, ngDialog, AssignmentForm, operator, Assignment, User,
assignment, phases, Projector, ProjectionDefault) {
User.bindAll({}, $scope, 'users');
Assignment.bindOne(assignment.id, $scope, 'assignment');
Assignment.loadRelations(assignment, 'agenda_item');
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var projectiondefault = ProjectionDefault.filter({name: 'assignments'})[0];
if (projectiondefault) {
$scope.defaultProjectorId = projectiondefault.projector_id;
}
});
$scope.candidateSelectBox = {};
$scope.phases = phases;
$scope.alert = {};

View File

@ -16,12 +16,8 @@
<translate>List of speakers</translate>
</a>
<!-- project -->
<a os-perms="core.can_manage_projector" class="btn btn-default btn-sm"
ng-class="{ 'btn-primary': assignment.isProjected() }"
ng-click="assignment.project()"
title="{{ 'Project election' | translate }}">
<i class="fa fa-video-camera"></i>
</a>
<projector-button model="assignment", default-projector-id="defaultProjectorId">
</projector-button>
<!-- edit -->
<a os-perms="assignments.can_manage" ng-click="openDialog(assignment)"
class="btn btn-default btn-sm"
@ -155,13 +151,9 @@
3. <translate>Published</translate>
</button>
<i class="fa fa-arrow-right"></i>
<button os-perms="core.can_manage_projector" class="btn btn-default btn-sm"
ng-class="{ 'btn-primary': assignment.isProjected(poll.id) }"
ng-click="assignment.project(poll.id)"
title="{{ 'Project ballot' | translate }}">
<i class="fa fa-video-camera"></i>
4. <translate>Project</translate>
</button>
<projector-button model="assignment" default-projector-id="defaultProjectorId"
additional-id="poll.id" content="4. {{ 'Project' | translate }}">
</projector-button>
<a class="btn btn-danger btn-sm"
ng-bootbox-confirm="{{ 'Are you sure you want to delete this ballot?' | translate }}"
ng-bootbox-confirm-action="deleteBallot(poll)">

View File

@ -121,12 +121,8 @@
<!-- projector -->
<td ng-show="!isDeleteMode" os-perms="core.can_manage_projector">
<a class="btn btn-default btn-sm"
ng-class="{ 'btn-primary': assignment.isProjected() }"
ng-click="assignment.project()"
title="{{ 'Project election' | translate }}">
<i class="fa fa-video-camera"></i>
</a>
<projector-button model="assignment" default-projector-id="defaultProjectorId">
</projector-button>
<!-- delete selection -->
<td ng-show="isDeleteMode" os-perms="assignments.can_manage" class="deleteColumn">

View File

@ -20,7 +20,7 @@ class CoreAppConfig(AppConfig):
from openslides.utils.rest_api import router
from openslides.utils.search import index_add_instance, index_del_instance
from .config_variables import get_config_variables
from .signals import delete_django_app_permissions
from .signals import delete_django_app_permissions, create_builtin_projection_defaults
from .views import (
ChatMessageViewSet,
ConfigViewSet,
@ -35,6 +35,9 @@ class CoreAppConfig(AppConfig):
post_permission_creation.connect(
delete_django_app_permissions,
dispatch_uid='delete_django_app_permissions')
post_permission_creation.connect(
create_builtin_projection_defaults,
dispatch_uid='create_builtin_projection_defaults')
# Register viewsets.
router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet)

View File

@ -4,16 +4,14 @@ from django.utils.translation import ugettext as _
from .exceptions import ConfigError, ConfigNotFound
from .models import ConfigStore
# remove resolution when changing to multiprojector
INPUT_TYPE_MAPPING = {
'string': str,
'text': str,
'integer': int,
'boolean': bool,
'choice': str,
'colorpicker': str,
'comments': list,
'resolution': dict}
'colorpicker': str}
class ConfigHandler:
@ -89,16 +87,6 @@ class ConfigHandler:
except DjangoValidationError as e:
raise ConfigError(e.messages[0])
# remove this block when changing to multiprojector
if config_variable.input_type == 'resolution':
if value.get('width') is None or value.get('height') is None:
raise ConfigError(_('A width and a height have to be given.'))
if not isinstance(value['width'], int) or not isinstance(value['height'], int):
raise ConfigError(_('Data has to be integers.'))
if (value['width'] < 800 or value['width'] > 3840 or
value['height'] < 600 or value['height'] > 2160):
raise ConfigError(_('The Resolution have to be between 800x600 and 3840x2160.'))
if config_variable.input_type == 'comments':
if not isinstance(value, list):
raise ConfigError(_('motions_comments has to be a list.'))

View File

@ -157,11 +157,28 @@ def get_config_variables():
weight=185,
group='Projector')
# set the resolution for one projector. It can be removed with the multiprojector feature.
yield ConfigVariable(
name='projector_resolution',
default_value={'width': 1024, 'height': 768},
input_type='resolution',
label='Projector Resolution',
weight=200,
name='projector_blank_color',
default_value='#FFFFFF',
input_type='colorpicker',
label='Color for blanked projector',
weight=190,
group='Projector')
yield ConfigVariable(
name='projector_broadcast',
default_value=0,
input_type='integer',
label='Projector which is broadcasted',
weight=200,
group='Projector',
hidden=True)
yield ConfigVariable(
name='projector_currentListOfSpeakers_reference',
default_value=1,
input_type='integer',
label='Projector reference for list of speakers',
weight=201,
group='Projector',
hidden=True)

View File

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.9 on 2016-08-29 09:37
from __future__ import unicode_literals
import django.db.models.deletion
from django.db import migrations, models
import openslides.utils.models
def name_default_projector(apps, schema_editor):
"""
Set the name of the default projector to 'Defaultprojector'
"""
Projector = apps.get_model('core', 'Projector')
Projector.objects.filter(pk=1).update(name='Defaultprojector')
class Migration(migrations.Migration):
dependencies = [
('core', '0005_auto_20160918_2104'),
]
operations = [
migrations.CreateModel(
name='ProjectionDefault',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=256)),
('display_name', models.CharField(max_length=256)),
],
options={
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model),
),
migrations.AddField(
model_name='projector',
name='name',
field=models.CharField(blank=True, max_length=255, unique=True),
),
migrations.AddField(
model_name='projector',
name='blank',
field=models.BooleanField(blank=False, default=False),
),
migrations.AddField(
model_name='projectiondefault',
name='projector',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projectiondefaults', to='core.Projector'),
),
migrations.RunPython(name_default_projector),
]

View File

@ -63,11 +63,19 @@ class Projector(RESTModelMixin, models.Model):
scroll = models.IntegerField(default=0)
# currently unused, but important for the multiprojector.
width = models.PositiveIntegerField(default=1024)
height = models.PositiveIntegerField(default=768)
name = models.CharField(
max_length=255,
unique=True,
blank=True)
blank = models.BooleanField(
blank=False,
default=False)
class Meta:
"""
Contains general permissions that can not be placed in a specific app.
@ -169,6 +177,33 @@ class Projector(RESTModelMixin, models.Model):
return result
class ProjectionDefault(RESTModelMixin, models.Model):
"""
Model for the ProjectionDefaults like Motion, Agenda, List of speakers,...
The name is the technical name like 'topics', 'motions'. For apps the name should
be the app name to get keep the ProjectionDefault for apps generic. But it is
possible to give some special name like 'list_of_speakers'.
The display_name is the shown name on the front end for the user.
"""
name = models.CharField(max_length=256)
display_name = models.CharField(max_length=256)
projector = models.ForeignKey(
'Projector',
on_delete=models.CASCADE,
related_name='projectiondefaults')
def get_root_rest_element(self):
return self.projector
class Meta:
default_permissions = ()
def __str__(self):
return self.display_name
class Tag(RESTModelMixin, models.Model):
"""
Model for tags. This tags can be used for other models like agenda items,

View File

@ -1,3 +1,5 @@
import uuid
from django.utils.timezone import now
from ..utils.projector import ProjectorElement
@ -20,7 +22,7 @@ class Countdown(ProjectorElement):
To start the countdown write into the config field:
{
"status": "running",
"running": True,
"countdown_time": <timestamp>,
}
@ -30,10 +32,10 @@ class Countdown(ProjectorElement):
To stop the countdown set the countdown time to the current value of the
countdown (countdown_time = countdown_time - now + serverTimeOffset)
and set status to "stop".
and set running to False.
To reset the countdown (it is not a reset in a functional way) just
change the countdown time. The status value remains "stop".
change the countdown time. The running value remains False.
Do not forget to send values for additional keywords like "stable" if
you do not want to use the default.
@ -63,50 +65,76 @@ class Countdown(ProjectorElement):
"""
if not isinstance(config_data.get('countdown_time'), (int, float)):
raise ProjectorException('Invalid countdown time. Use integer or float.')
if config_data.get('status') not in ('running', 'stop'):
raise ProjectorException("Invalid status. Use 'running' or 'stop'.")
if not isinstance(config_data.get('running'), bool):
raise ProjectorException("Invalid running status. Has to be a boolean.")
if config_data.get('default') is not None and not isinstance(config_data.get('default'), int):
raise ProjectorException('Invalid default value. Use integer.')
@classmethod
def control(cls, action, projector_id=1, index=0):
"""
Starts, stops or resets the countdown with the given index on the
given projector.
Action must be 'start', 'stop' or 'reset'.
"""
def control(cls, action):
if action not in ('start', 'stop', 'reset'):
raise ValueError("Action must be 'start', 'stop' or 'reset', not {}.".format(action))
raise ValueError("Action must be 'start', 'stop' or 'reset', not {}.".format(action))
projector_instance = Projector.objects.get(pk=projector_id)
projector_config = {}
found = False
for key, value in projector_instance.config.items():
if value['name'] == cls.name:
if index == 0:
try:
cls.validate_config(value)
except ProjectorException:
# Do not proceed if the specific procjector config data is invalid.
# The variable found remains False.
break
found = True
if action == 'start' and value['status'] == 'stop':
value['status'] = 'running'
value['countdown_time'] = now().timestamp() + value['countdown_time']
elif action == 'stop' and value['status'] == 'running':
value['status'] = 'stop'
value['countdown_time'] = value['countdown_time'] - now().timestamp()
elif action == 'reset':
value['status'] = 'stop'
value['countdown_time'] = value.get('default', config['projector_default_countdown'])
else:
index += -1
projector_config[key] = value
if found:
projector_instance.config = projector_config
projector_instance.save()
# Use the countdown with the lowest index
projectors = Projector.objects.all()
lowest_index = None
if projectors[0]:
for key, value in projectors[0].config.items():
if value['name'] == cls.name:
if lowest_index is None or value['index'] < lowest_index:
lowest_index = value['index']
if lowest_index is None:
# create a countdown
for projector in projectors:
projector_config = {}
for key, value in projector.config.items():
projector_config[key] = value
# new countdown
countdown = {
'name': 'core/countdown',
'stable': True,
'index': 1,
'default_time': config['projector_default_countdown'],
'visible': False,
'selected': True,
}
if action == 'start':
countdown['running'] = True
countdown['countdown_time'] = now().timestamp() + countdown['default_time']
elif action == 'reset' or action == 'stop':
countdown['running'] = False
countdown['countdown_time'] = countdown['default_time']
projector_config[uuid.uuid4().hex] = countdown
projector.config = projector_config
projector.save()
else:
# search for the countdown and modify it.
for projector in projectors:
projector_config = {}
found = False
for key, value in projector.config.items():
if value['name'] == cls.name and value['index'] == lowest_index:
try:
cls.validate_config(value)
except ProjectorException:
# Do not proceed if the specific procjector config data is invalid.
# The variable found remains False.
break
found = True
if action == 'start':
value['running'] = True
value['countdown_time'] = now().timestamp() + value['default_time']
elif action == 'stop' and value['running']:
value['running'] = False
value['countdown_time'] = value['countdown_time'] - now().timestamp()
elif action == 'reset':
value['running'] = False
value['countdown_time'] = value['default_time']
projector_config[key] = value
if found:
projector.config = projector_config
projector.save()
class Message(ProjectorElement):

View File

@ -1,6 +1,6 @@
from openslides.utils.rest_api import Field, ModelSerializer, ValidationError
from .models import ChatMessage, Projector, Tag
from .models import ChatMessage, ProjectionDefault, Projector, Tag
class JSONSerializerField(Field):
@ -22,15 +22,26 @@ class JSONSerializerField(Field):
return data
class ProjectionDefaultSerializer(ModelSerializer):
"""
Serializer for core.models.ProjectionDefault objects.
"""
class Meta:
model = ProjectionDefault
fields = ('id', 'name', 'display_name', 'projector', )
class ProjectorSerializer(ModelSerializer):
"""
Serializer for core.models.Projector objects.
"""
config = JSONSerializerField(write_only=True)
projectiondefaults = ProjectionDefaultSerializer(many=True, read_only=True)
class Meta:
model = Projector
fields = ('id', 'config', 'elements', 'scale', 'scroll', 'width', 'height',)
fields = ('id', 'config', 'elements', 'scale', 'scroll', 'name', 'blank', 'width', 'height', 'projectiondefaults')
read_only_fields = ('scale', 'scroll', 'blank', 'width', 'height', 'projectiondefaults')
class TagSerializer(ModelSerializer):

View File

@ -3,6 +3,8 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.dispatch import Signal
from .models import ProjectionDefault, Projector
# This signal is sent when the migrate command is done. That means it is sent
# after post_migrate sending and creating all Permission objects. Don't use it
# for other things than dealing with Permission objects.
@ -20,3 +22,56 @@ def delete_django_app_permissions(sender, **kwargs):
Q(app_label='sessions'))
for permission in Permission.objects.filter(content_type__in=contenttypes):
permission.delete()
def create_builtin_projection_defaults(**kwargs):
"""
Creates the builtin defaults:
- agenda_all_items, agenda_item
- assignments
- mediafiles
- motion
- users
- list_of_speakers
- current_list_of_speakers
"""
# Check whether ProjectionDefaults exists
if ProjectionDefault.objects.all().exists():
# Do completely nothing if the defaults are already in the database.
return
default_projector = Projector.objects.get(pk=1)
ProjectionDefault.objects.create(
name='agenda_all_items',
display_name='Agenda',
projector=default_projector)
ProjectionDefault.objects.create(
name='topics',
display_name='Topics',
projector=default_projector)
ProjectionDefault.objects.create(
name='list_of_speakers',
display_name='List of speakers',
projector=default_projector)
ProjectionDefault.objects.create(
name='current_list_of_speakers',
display_name='Current list of speakers',
projector=default_projector)
ProjectionDefault.objects.create(
name='motions',
display_name='Motions',
projector=default_projector)
ProjectionDefault.objects.create(
name='assignments',
display_name='Elections',
projector=default_projector)
ProjectionDefault.objects.create(
name='users',
display_name='Participants',
projector=default_projector)
ProjectionDefault.objects.create(
name='mediafiles',
display_name='Files',
projector=default_projector)

View File

@ -270,6 +270,12 @@ img {
float: right;
}
.col1 .header .submenu > div {
display: inline-block;
float: left;
margin-left: 5px;
}
.col1 .meta .title {
width: 100%;
cursor: pointer;
@ -331,15 +337,6 @@ img {
margin-left: 20px;
}
/* .resolution can be removed with the multiprojector, but maybe
* it could be reused for the settings in the projectormanage-view */
.col1 .input-group .resolution {
float: none;
display: inline-block;
width: 80px;
border-radius: 4px !important;
}
/* Toolbar to save motion in inline editing mode */
.motion-save-toolbar {
position: fixed;
@ -586,7 +583,6 @@ img {
#content .col2 .section a:hover {
text-decoration: none;
opacity: 0.5;
}
#content .toggle-icon {
@ -652,20 +648,20 @@ img {
color: #CC0000;
}
.col2 .notNull {
.notNull {
color: red;
font-weight: bold;
}
/* iframe for live view */
.col2 #iframe {
.iframe {
-moz-transform-origin: 0 0;
-webkit-transform-origin: 0 0;
-o-transform-origin: 0 0;
transform-origin: 0 0 0;
}
.col2 #iframewrapper {
.iframewrapper {
width: 256px;
position: relative;
overflow: hidden;
@ -673,7 +669,7 @@ img {
margin-bottom: 10px;
}
.col2 #iframeoverlay {
.iframeoverlay {
width: 256px;
position: absolute;
top: 0px;
@ -782,6 +778,77 @@ img {
margin-bottom: 5px
}
/** Pojector sidebar **/
.col2 .projectorSelector {
margin-top: 10px;
margin-bottom: 10px;
}
.col2 .projectorSelector > div > div {
width: 65%;
padding-right: 5px;
float: left;
}
.col2 .projectorSelector > div > div > button {
width: 100%;
}
.col2 .projectorSelector .manageBtn {
width: 35%;
}
.col2 .projectorSelector .btn-group {
margin-left: auto;
margin-right: auto;
display: table;
}
.col2 .projectorSelector > div {
margin-bottom: 10px;
}
/** manage-projectors **/
.projectorContainer > div {
display: inline-table;
width: 256px;
margin: 10px 20px 35px 10px;
}
.projectorContainer > div > div {
margin-bottom: 10px;
}
.projectorContainer .middle {
width: 100%;
}
.projectorContainer .middle > div {
margin-left: auto;
margin-right: auto;
display: table;
}
.projectorContainer .dropdown {
width: 65%;
padding-right: 5px;
float: left;
}
.projectorContainer .dropdown > button {
width: 100%;
}
.projectorContainer .btn-danger {
width: 35%;
}
.projectorContainer .resolution {
display: inline-block;
width: 120px;
}
/** Footer **/
#footer {
float: left;
@ -988,21 +1055,6 @@ img {
}
/* List of speakers view */
.fullscreendiv {
position: absolute;
font-size:120%;
top:0;
bottom:0;
right:0;
left:0;
padding: 50px;
background: #fff;
overflow: hidden;
width: 100vw;
height: 100vh;
z-index: 99;
}
.fullscreendiv-title {
border-bottom: 5px solid #d3d3d3;
margin-bottom: 40px;

View File

@ -3,7 +3,11 @@
*
*/
body{
html, body {
height: 100%;
}
body {
font-size: 20px !important;
line-height: 24px !important;
overflow: hidden;
@ -34,7 +38,7 @@ body{
transform-origin: 0 0 0;
}
.pContainer #iframewrapper {
.pContainer #iframewrapper, .pContainer .error {
position: relative;
overflow: hidden;
margin-left: auto;
@ -49,6 +53,12 @@ body{
z-index: 1;
}
.pContainer .error > p {
color: #f00;
font-size: 150%;
text-align: center;
}
/*** HEADER ***/
#header {
box-shadow: 0 0 7px rgba(0,0,0,0.6);
@ -278,6 +288,10 @@ hr {
line-height: normal !important;
z-index: 301;
}
.identify {
background-color: #D9F8C4;
z-index: 400;
}
/*** PDF presentation ***/
.rotate0 {
@ -379,7 +393,6 @@ tr.elected td {
}
/*** Line numbers ***/
.motion-text .highlight {
background-color: #ff0;

View File

@ -33,11 +33,20 @@ angular.module('OpenSlidesApp.core', [
}
])
.factory('ProjectorID', [
function () {
return function () {
return /projector\/(\d+)\//.exec(location.pathname)[1];
};
}
])
.factory('autoupdate', [
'DS',
'$rootScope',
'REALM',
function (DS, $rootScope, REALM) {
'ProjectorID',
function (DS, $rootScope, REALM, ProjectorID) {
var socket = null;
var recInterval = null;
$rootScope.connected = false;
@ -46,8 +55,7 @@ angular.module('OpenSlidesApp.core', [
if (REALM == 'site') {
websocketPath = '/ws/site/';
} else if (REALM == 'projector') {
// TODO: At the moment there is only one projector. Find out which one is requested
websocketPath = '/ws/projector/1/';
websocketPath = '/ws/projector/' + ProjectorID() + '/';
} else {
console.error('The constant REALM is not set properly.');
}
@ -246,7 +254,7 @@ angular.module('OpenSlidesApp.core', [
return function () {
Config.findAll();
// Loads all projector data
// Loads all projector data and the projectiondefaults
Projector.findAll();
// Loads all chat messages data and their user_ids
@ -305,34 +313,91 @@ angular.module('OpenSlidesApp.core', [
}
])
// This places a Projectorbutton in the document. Example:
// <projector-button model="motion" default-projector.id="defPrId" additional-id="2"
// content="{{ 'project' | translate }}"></projector-button>
//
// This button references to model (in this case 'motion'). Also a defaultProjectionId has to
// be given. In the Exable its a scope variable. The next two parameters are additional:
// - additional-id: Then the model.project and model.isProjected will be called whith this
// argument (ex.: model.project(2))
// - content: A not trusted text placed behind the projector symbol.
.directive('projectorButton', [
'Projector',
function (Projector) {
return {
restrict: 'E',
templateUrl: 'static/templates/projector-button.html',
link: function (scope, element, attributes) {
if (!attributes.model) {
throw 'A model has to be given!';
} else if (!attributes.defaultProjectorId) {
throw 'A default-projector-id has to be given!';
}
Projector.bindAll({}, scope, 'projectors');
scope.$watch(attributes.model, function (model) {
scope.model = model;
});
scope.$watch(attributes.defaultProjectorId, function (defaultProjectorId) {
scope.defaultProjectorId = defaultProjectorId;
});
if (attributes.additionalId) {
scope.$watch(attributes.additionalId, function (id) {
scope.additionalId = id;
});
}
if (attributes.content) {
attributes.$observe('content', function (content) {
scope.content = content;
});
}
}
};
}
])
.factory('jsDataModel', [
'$http',
'Projector',
function($http, Projector) {
var BaseModel = function() {};
BaseModel.prototype.project = function() {
return $http.post(
'/rest/core/projector/1/prune_elements/',
[{name: this.getResourceName(), id: this.id}]
);
BaseModel.prototype.project = function(projectorId) {
// if this object is already projected on projectorId, delete this element from this projector
var isProjectedId = this.isProjected();
if (isProjectedId > 0) {
$http.post('/rest/core/projector/' + isProjectedId + '/prune_elements/');
}
// if it was the same projector before, just delete it but not show again
if (isProjectedId != projectorId) {
return $http.post(
'/rest/core/projector/' + projectorId + '/prune_elements/',
[{name: this.getResourceName(), id: this.id}]
);
}
};
BaseModel.prototype.isProjected = function() {
// Returns true if there is a projector element with the same
// name and the same id.
var projector = Projector.get(1);
var isProjected;
if (typeof projector !== 'undefined') {
var self = this;
var predicate = function (element) {
return element.name == self.getResourceName() &&
typeof element.id !== 'undefined' &&
element.id == self.id;
};
isProjected = typeof _.findKey(projector.elements, predicate) === 'string';
} else {
isProjected = false;
}
return isProjected;
// Returns the projector id if there is a projector element
// with the same name and the same id. Else returns 0.
// Attention: if this element is projected multiple times, only the
// id of the last projector is returned.
var self = this;
var predicate = function (element) {
return element.name == self.getResourceName() &&
typeof element.id !== 'undefined' &&
element.id == self.id;
};
var isProjectedId = 0;
Projector.getAll().forEach(function (projector) {
if (typeof _.findKey(projector.elements, predicate) === 'string') {
isProjectedId = projector.id;
}
});
return isProjectedId;
};
return BaseModel;
}
@ -396,10 +461,74 @@ angular.module('OpenSlidesApp.core', [
*/
.factory('Projector', [
'DS',
function(DS) {
'$http',
'Config',
function(DS, $http, Config) {
return DS.defineResource({
name: 'core/projector',
onConflict: 'replace',
relations: {
hasMany: {
'core/projectiondefault': {
localField: 'projectiondefaults',
foreignKey: 'projector_id',
}
},
},
methods: {
controlProjector: function(action, direction) {
$http.post('/rest/core/projector/' + this.id + '/control_view/',
{"action": action, "direction": direction}
);
},
getStateForCurrentSlide: function () {
var return_dict;
$.each(this.elements, function(key, value) {
if (value.name == 'agenda/list-of-speakers') {
return_dict = {
'state': 'agenda.item.detail',
'param': {id: value.id}
};
} else if (
value.name != 'agenda/item-list' &&
value.name != 'core/clock' &&
value.name != 'core/countdown' &&
value.name != 'core/message' ) {
return_dict = {
'state': value.name.replace('/', '.')+'.detail.update',
'param': {id: value.id}
};
}
});
return return_dict;
},
toggleBlank: function () {
$http.post('/rest/core/projector/' + this.id + '/control_blank/',
!this.blank
);
},
toggleBroadcast: function () {
$http.post('/rest/core/projector/' + this.id + '/broadcast/');
}
},
});
}
])
/* Model for all projection defaults */
.factory('ProjectionDefault', [
'DS',
function(DS) {
return DS.defineResource({
name: 'core/projectiondefault',
relations: {
belongsTo: {
'core/projector': {
localField: 'projector',
localKey: 'projector_id',
}
}
}
});
}
])
@ -522,8 +651,9 @@ angular.module('OpenSlidesApp.core', [
'ChatMessage',
'Config',
'Projector',
'ProjectionDefault',
'Tag',
function (ChatMessage, Config, Projector, Tag) {}
function (ChatMessage, Config, Projector, ProjectionDefault, Tag) {}
]);
}());

View File

@ -59,25 +59,31 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
// Projector Container Controller
.controller('ProjectorContainerCtrl', [
'$scope',
'Config',
'$location',
'loadGlobalData',
function($scope, Config, loadGlobalData) {
'Projector',
'ProjectorID',
function($scope, $location, loadGlobalData, Projector, ProjectorID) {
loadGlobalData();
// watch for changes in Config
var last_conf;
$scope.projector_id = ProjectorID();
$scope.error = '';
// watch for changes in Projector
$scope.$watch(function () {
return Config.lastModified();
return Projector.lastModified($scope.projector_id);
}, function () {
// With multiprojector, get the resolution from Prjector.get(pk).{width; height}
if (typeof $scope.config === 'function') {
var conf = $scope.config('projector_resolution');
if(!last_conf || last_conf.width != conf.width || last_conf.height != conf.height) {
last_conf = conf;
$scope.projectorWidth = conf.width;
$scope.projectorHeight = conf.height;
$scope.recalculateIframe();
Projector.find($scope.projector_id).then(function (projector) {
$scope.projectorWidth = projector.width;
$scope.projectorHeight = projector.height;
$scope.recalculateIframe();
}, function (error) {
if (error.status == 404) {
$scope.error = 'Projector not found.';
} else if (error.status == 403) {
$scope.error = 'You have to login to see the projector.';
}
}
});
});
// recalculate the actual Iframesize and scale
@ -114,27 +120,84 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
.controller('ProjectorCtrl', [
'$scope',
'$location',
'Projector',
'slides',
function($scope, Projector, slides) {
'Config',
'ProjectorID',
function($scope, $location, Projector, slides, Config, ProjectorID) {
var projector_id = ProjectorID();
$scope.broadcast = 0;
var setElements = function (projector) {
$scope.elements = [];
_.forEach(slides.getElements(projector), function(element) {
if (!element.error) {
$scope.elements.push(element);
} else {
console.error("Error for slide " + element.name + ": " + element.error);
}
});
};
$scope.$watch(function () {
// TODO: Use the current projector. At the moment there is only one.
return Projector.lastModified(1);
return Projector.lastModified(projector_id);
}, function () {
// TODO: Use the current projector. At the moment there is only one
var projector = Projector.get(1);
if (projector) {
$scope.projector = Projector.get(projector_id);
if ($scope.projector) {
if ($scope.broadcast === 0) {
setElements($scope.projector);
$scope.blank = $scope.projector.blank;
}
} else {
// Blank projector on error
$scope.elements = [];
_.forEach(slides.getElements(projector), function(element) {
if (!element.error) {
$scope.elements.push(element);
} else {
console.error("Error for slide " + element.name + ": " + element.error);
$scope.projector = {
scroll: 0,
scale: 0,
blank: true
};
}
});
$scope.$watch(function () {
return Config.lastModified('projector_broadcast');
}, function () {
Config.findAll().then(function () {
var bc = Config.get('projector_broadcast').value;
if ($scope.broadcast != bc) {
$scope.broadcast = bc;
if ($scope.broadcastDeregister) {
// revert to original $scope.projector
$scope.broadcastDeregister();
$scope.broadcastDeregister = null;
setElements($scope.projector);
$scope.blank = $scope.projector.blank;
}
});
// TODO: Use the current projector. At the moment there is only one
$scope.scroll = -80 * Projector.get(1).scroll;
$scope.scale = 100 + 20 * Projector.get(1).scale;
}
if ($scope.broadcast > 0) {
// get elements and blank from broadcast projector
$scope.broadcastDeregister = $scope.$watch(function () {
return Projector.lastModified($scope.broadcast);
}, function () {
if ($scope.broadcast > 0) {
// var broadcast_projector = Projector.get($scope.broadcast);
Projector.find($scope.broadcast).then(function (broadcast_projector) {
setElements(broadcast_projector);
$scope.blank = broadcast_projector.blank;
});
}
});
}
});
});
$scope.$on('$destroy', function() {
if ($scope.broadcastDeregister) {
$scope.broadcastDeregister();
$scope.broadcastDeregister = null;
}
});
}
@ -158,13 +221,14 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
// Add it to the coresponding get_requirements method of the ProjectorElement
// class.
$scope.seconds = Math.floor( $scope.element.countdown_time - Date.now() / 1000 + $scope.serverOffset );
$scope.status = $scope.element.status;
$scope.running = $scope.element.running;
$scope.visible = $scope.element.visible;
$scope.selected = $scope.element.selected;
$scope.index = $scope.element.index;
$scope.description = $scope.element.description;
// start interval timer if countdown status is running
var interval;
if ($scope.status == "running") {
if ($scope.running) {
interval = $interval( function() {
$scope.seconds = Math.floor( $scope.element.countdown_time - Date.now() / 1000 + $scope.serverOffset );
}, 1000);
@ -186,6 +250,8 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
// class.
$scope.message = $scope.element.message;
$scope.visible = $scope.element.visible;
$scope.selected = $scope.element.selected;
$scope.type = $scope.element.type;
}
]);

View File

@ -717,7 +717,7 @@ angular.module('OpenSlidesApp.core.site', [
templateUrl: 'static/templates/home.html'
})
.state('projector', {
url: '/projector',
url: '/projector/{id:int}',
templateUrl: 'static/templates/projector-container.html',
data: {extern: true},
onEnter: function($window) {
@ -725,13 +725,18 @@ angular.module('OpenSlidesApp.core.site', [
}
})
.state('real-projector', {
url: '/real-projector',
url: '/real-projector/{id:int}',
templateUrl: 'static/templates/projector.html',
data: {extern: true},
onEnter: function($window) {
$window.location.href = this.url;
}
})
.state('manage-projectors', {
url: '/manage-projectors',
templateUrl: 'static/templates/core/manage-projectors.html',
controller: 'ManageProjectorsCtrl'
})
.state('core', {
url: '/core',
abstract: true,
@ -901,7 +906,6 @@ angular.module('OpenSlidesApp.core.site', [
'Config',
'gettextCatalog',
function($parse, Config, gettextCatalog) {
// remove resolution when changing to multiprojector
function getHtmlType(type) {
return {
string: 'text',
@ -911,7 +915,6 @@ angular.module('OpenSlidesApp.core.site', [
choice: 'choice',
colorpicker: 'colorpicker',
comments: 'comments',
resolution: 'resolution',
}[type];
}
@ -1187,209 +1190,439 @@ angular.module('OpenSlidesApp.core.site', [
'$http',
'$interval',
'$state',
'$q',
'Config',
'Projector',
function($scope, $http, $interval, $state, Config, Projector) {
// bind projector elements to the scope, update after projector changed
$scope.$watch(function () {
return Projector.lastModified(1);
}, function () {
// stop ALL interval timer
for (var i=0; i<$scope.countdowns.length; i++) {
if ( $scope.countdowns[i].interval ) {
$interval.cancel($scope.countdowns[i].interval);
function($scope, $http, $interval, $state, $q, Config, Projector) {
$scope.countdowns = [];
$scope.highestCountdownIndex = 0;
$scope.messages = [];
$scope.highestMessageIndex = 0;
var cancelIntervalTimers = function () {
$scope.countdowns.forEach(function (countdown) {
$interval.cancel(countdown.interval);
});
};
// Get all message and countdown data from the defaultprojector (id=1)
var rebuildAllElements = function () {
$scope.countdowns = [];
$scope.messages = [];
_.forEach(Projector.get(1).elements, function (element, uuid) {
if (element.name == 'core/countdown') {
$scope.countdowns.push(element);
if (element.running) {
// calculate remaining seconds directly because interval starts with 1 second delay
$scope.calculateCountdownTime(element);
// start interval timer (every second)
element.interval = $interval(function () { $scope.calculateCountdownTime(element); }, 1000);
} else {
element.seconds = element.countdown_time;
}
if (element.index > $scope.highestCountdownIndex) {
$scope.highestCountdownIndex = element.index;
}
} else if (element.name == 'core/message') {
$scope.messages.push(element);
if (element.index > $scope.highestMessageIndex) {
$scope.highestMessageIndex = element.index;
}
}
});
};
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
$scope.projectors = Projector.getAll();
if (!$scope.active_projector) {
$scope.changeProjector($scope.projectors[0]);
}
// rebuild all variables after projector update
$scope.rebuildAllElements();
// stop ALL interval timer
cancelIntervalTimers();
rebuildAllElements();
});
$scope.$on('$destroy', function() {
// Cancel all intervals if the controller is destroyed
for (var i=0; i<$scope.countdowns.length; i++) {
if ( $scope.countdowns[i].interval ) {
$interval.cancel($scope.countdowns[i].interval);
}
}
cancelIntervalTimers();
});
// watch for changes in Config
var last_conf;
// watch for changes in projector_broadcast
var last_broadcast;
$scope.$watch(function () {
return Config.lastModified();
}, function () {
var conf = Config.get('projector_resolution').value;
// With multiprojector, get the resolution from Prjector.get(pk).{width; height}
if(!last_conf || last_conf.width != conf.width || last_conf.height != conf.height) {
last_conf = conf;
$scope.projectorWidth = conf.width;
$scope.projectorHeight = conf.height;
$scope.scale = 256.0 / $scope.projectorWidth;
$scope.iframeHeight = $scope.scale * $scope.projectorHeight;
var broadcast = Config.get('projector_broadcast').value;
if (!last_broadcast || last_broadcast != broadcast) {
last_broadcast = broadcast;
$scope.broadcast = broadcast;
}
});
$scope.changeProjector = function (projector) {
$scope.active_projector = projector;
$scope.scale = 256.0 / projector.width;
$scope.iframeHeight = $scope.scale * projector.height;
};
$scope.editCurrentSlide = function (projector) {
var state = projector.getStateForCurrentSlide();
if (state) {
$state.go(state.state, state.param);
}
};
// *** countdown functions ***
$scope.calculateCountdownTime = function (countdown) {
countdown.seconds = Math.floor( countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset );
};
$scope.rebuildAllElements = function () {
$scope.countdowns = [];
$scope.messages = [];
// iterate via all projector elements and catch all countdowns and messages
$.each(Projector.get(1).elements, function(key, value) {
if (value.name == 'core/countdown') {
$scope.countdowns.push(value);
if (value.status == "running") {
// calculate remaining seconds directly because interval starts with 1 second delay
$scope.calculateCountdownTime(value);
// start interval timer (every second)
value.interval = $interval( function() { $scope.calculateCountdownTime(value); }, 1000);
} else {
value.seconds = value.countdown_time;
$scope.editCountdown = function (countdown) {
countdown.editFlag = false;
$scope.projectors.forEach(function (projector) {
_.forEach(projector.elements, function (element, uuid) {
if (element.name == 'core/countdown' && element.index == countdown.index) {
var data = {};
data[uuid] = {
"description": countdown.description,
"default_time": parseInt(countdown.default_time)
};
if (!countdown.running) {
data[uuid].countdown_time = parseInt(countdown.default_time);
}
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
}
if (value.name == 'core/message') {
$scope.messages.push(value);
}
});
});
$scope.scrollLevel = Projector.get(1).scroll;
$scope.scaleLevel = Projector.get(1).scale;
};
// get initial values for $scope.countdowns, $scope.messages, $scope.scrollLevel
// and $scope.scaleLevel (after page reload)
$scope.rebuildAllElements();
$scope.addCountdown = function () {
var defaultvalue = parseInt(Config.get('projector_default_countdown').value);
$http.post('/rest/core/projector/1/activate_elements/', [{
var default_time = parseInt($scope.config('projector_default_countdown'));
$scope.highestCountdownIndex++;
// select all projectors on creation, so write the countdown to all projectors
$scope.projectors.forEach(function (projector) {
$http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{
name: 'core/countdown',
status: 'stop',
countdown_time: default_time,
default_time: default_time,
visible: false,
index: $scope.countdowns.length,
countdown_time: defaultvalue,
default: defaultvalue,
stable: true
}]);
selected: true,
index: $scope.highestCountdownIndex,
running: false,
stable: true,
}]);
});
};
$scope.removeCountdown = function (countdown) {
var data = {};
var delta = 0;
// rebuild index for all countdowns after the selected (deleted) countdown
for (var i=0; i<$scope.countdowns.length; i++) {
if ( $scope.countdowns[i].uuid == countdown.uuid ) {
delta = 1;
} else if (delta > 0) {
data[$scope.countdowns[i].uuid] = { "index": i - delta };
}
}
$http.post('/rest/core/projector/1/deactivate_elements/', [countdown.uuid]);
if (Object.keys(data).length > 0) {
$http.post('/rest/core/projector/1/update_elements/', data);
}
};
$scope.showCountdown = function (countdown) {
var data = {};
data[countdown.uuid] = { "visible": !countdown.visible };
$http.post('/rest/core/projector/1/update_elements/', data);
};
$scope.editCountdown = function (countdown) {
var data = {};
data[countdown.uuid] = {
"description": countdown.description,
"default": parseInt(countdown.default)
};
if (countdown.status == "stop") {
data[countdown.uuid].countdown_time = parseInt(countdown.default);
}
$http.post('/rest/core/projector/1/update_elements/', data);
$scope.projectors.forEach(function (projector) {
var countdowns = [];
_.forEach(projector.elements, function (element, uuid) {
if (element.name == 'core/countdown' && element.index == countdown.index) {
$http.post('/rest/core/projector/' + projector.id + '/deactivate_elements/', [uuid]);
}
});
});
};
$scope.startCountdown = function (countdown) {
var data = {};
// calculate end point of countdown (in seconds!)
var endTimestamp = Date.now() / 1000 - $scope.serverOffset + countdown.countdown_time;
data[countdown.uuid] = {
"status": "running",
"countdown_time": endTimestamp
};
$http.post('/rest/core/projector/1/update_elements/', data);
$scope.projectors.forEach(function (projector) {
_.forEach(projector.elements, function (element, uuid) {
if (element.name == 'core/countdown' && element.index == countdown.index) {
var data = {};
// calculate end point of countdown (in seconds!)
var endTimestamp = Date.now() / 1000 - $scope.serverOffset + countdown.countdown_time;
data[uuid] = {
'running': true,
'countdown_time': endTimestamp
};
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
});
});
};
$scope.stopCountdown = function (countdown) {
var data = {};
// calculate rest duration of countdown (in seconds!)
var newDuration = Math.floor( countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset );
data[countdown.uuid] = {
"status": "stop",
"countdown_time": newDuration
};
$http.post('/rest/core/projector/1/update_elements/', data);
$scope.projectors.forEach(function (projector) {
_.forEach(projector.elements, function (element, uuid) {
if (element.name == 'core/countdown' && element.index == countdown.index) {
var data = {};
// calculate rest duration of countdown (in seconds!)
var newDuration = Math.floor( countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset );
data[uuid] = {
'running': false,
'countdown_time': newDuration
};
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
});
});
};
$scope.resetCountdown = function (countdown) {
var data = {};
data[countdown.uuid] = {
"status": "stop",
"countdown_time": countdown.default,
};
$http.post('/rest/core/projector/1/update_elements/', data);
$scope.projectors.forEach(function (projector) {
_.forEach(projector.elements, function (element, uuid) {
if (element.name == 'core/countdown' && element.index == countdown.index) {
var data = {};
data[uuid] = {
'running': false,
'countdown_time': countdown.default_time,
};
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
});
});
};
// *** message functions ***
$scope.editMessage = function (message) {
message.editFlag = false;
$scope.projectors.forEach(function (projector) {
_.forEach(projector.elements, function (element, uuid) {
if (element.name == 'core/message' && element.index == message.index) {
var data = {};
data[uuid] = {
message: message.message,
};
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
});
});
};
$scope.addMessage = function () {
$http.post('/rest/core/projector/1/activate_elements/', [{
$scope.highestMessageIndex++;
// select all projectors on creation, so write the countdown to all projectors
$scope.projectors.forEach(function (projector) {
$http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{
name: 'core/message',
visible: false,
index: $scope.messages.length,
selected: true,
index: $scope.highestMessageIndex,
message: '',
stable: true
}]);
stable: true,
}]);
});
};
$scope.removeMessage = function (message) {
$http.post('/rest/core/projector/1/deactivate_elements/', [message.uuid]);
};
$scope.showMessage = function (message) {
var data = {};
// if current message is activated, deactivate all other messages
if ( !message.visible ) {
for (var i=0; i<$scope.messages.length; i++) {
if ( $scope.messages[i].uuid == message.uuid ) {
data[$scope.messages[i].uuid] = { "visible": true };
} else {
data[$scope.messages[i].uuid] = { "visible": false };
$scope.projectors.forEach(function (projector) {
_.forEach(projector.elements, function (element, uuid) {
if (element.name == 'core/message' && element.index == message.index) {
$http.post('/rest/core/projector/' + projector.id + '/deactivate_elements/', [uuid]);
}
}
} else {
data[message.uuid] = { "visible": false };
}
$http.post('/rest/core/projector/1/update_elements/', data);
};
$scope.editMessage = function (message) {
var data = {};
data[message.uuid] = {
"message": message.message,
};
$http.post('/rest/core/projector/1/update_elements/', data);
message.editMessageFlag = false;
});
});
};
// *** projector controls ***
$scope.scrollLevel = Projector.get(1).scroll;
$scope.scaleLevel = Projector.get(1).scale;
$scope.controlProjector = function (action, direction) {
$http.post('/rest/core/projector/1/control_view/', {"action": action, "direction": direction});
/* project functions*/
$scope.project = function (element) {
$scope.projectors.forEach(function (projector) {
_.forEach(projector.elements, function (projectorElement, uuid) {
if (element.name == projectorElement.name && element.index == projectorElement.index) {
var data = {};
data[uuid] = {visible: !projectorElement.visible};
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
});
});
};
$scope.editCurrentSlide = function () {
$.each(Projector.get(1).elements, function(key, value) {
if (value.name == 'agenda/list-of-speakers') {
$state.go('agenda.item.detail', {id: value.id});
} else if (
value.name != 'agenda/item-list' &&
value.name != 'core/clock' &&
value.name != 'core/countdown' &&
value.name != 'core/message' ) {
$state.go(value.name.replace('/', '.')+'.detail.update', {id: value.id});
$scope.isProjected = function (element) {
var projectorIds = [];
$scope.projectors.forEach(function (projector) {
_.forEach(projector.elements, function (projectorElement, uuid) {
if (element.name == projectorElement.name && element.index == projectorElement.index) {
if (projectorElement.visible && projectorElement.selected) {
projectorIds.push(projector.id);
}
}
});
});
return projectorIds;
};
$scope.isProjectedOn = function (element, projector) {
var projectedIds = $scope.isProjected(element);
return _.indexOf(projectedIds, projector.id) > -1;
};
$scope.hasProjector = function (element, projector) {
var hasProjector = false;
_.forEach(projector.elements, function (projectorElement, uuid) {
if (element.name == projectorElement.name && element.index == projectorElement.index) {
if (projectorElement.selected) {
hasProjector = true;
}
}
});
return hasProjector;
};
$scope.toggleProjector = function (element, projector) {
_.forEach(projector.elements, function (projectorElement, uuid) {
if (element.name == projectorElement.name && element.index == projectorElement.index) {
var data = {};
data[uuid] = {
'selected': !projectorElement.selected,
};
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
});
};
$scope.selectAll = function (element, value) {
$scope.projectors.forEach(function (projector) {
_.forEach(projector.elements, function (projectorElement, uuid) {
if (element.name == projectorElement.name && element.index == projectorElement.index) {
var data = {};
data[uuid] = {
'selected': value,
};
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
});
});
};
$scope.preventClose = function (e) {
e.stopPropagation();
};
}
])
.controller('ManageProjectorsCtrl', [
'$scope',
'$http',
'$state',
'$timeout',
'Projector',
'ProjectionDefault',
'Config',
'gettextCatalog',
function ($scope, $http, $state, $timeout, Projector, ProjectionDefault, Config, gettextCatalog) {
ProjectionDefault.bindAll({}, $scope, 'projectiondefaults');
// watch for changes in projector_broadcast
// and projector_currentListOfSpeakers_reference
var last_broadcast, last_clos;
$scope.$watch(function () {
return Config.lastModified();
}, function () {
var broadcast = $scope.config('projector_broadcast'),
currentListOfSpeakers = $scope.config('projector_currentListOfSpeakers_reference');
if (!last_broadcast || last_broadcast != broadcast) {
last_broadcast = broadcast;
$scope.broadcast = broadcast;
}
if (!last_clos || last_clos != currentListOfSpeakers) {
last_clos = currentListOfSpeakers;
$scope.currentListOfSpeakers = currentListOfSpeakers;
}
});
// watch for changes in Projector, and recalc scale and iframeHeight
var first_watch = true;
$scope.resolutions = [];
$scope.edit = [];
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
$scope.projectors = Projector.getAll();
$scope.projectors.forEach(function (projector) {
projector.iframeScale = 256.0 / projector.width;
projector.iframeHeight = projector.iframeScale * projector.height;
if (first_watch) {
$scope.resolutions[projector.id] = {
width: projector.width,
height: projector.height
};
$scope.edit[projector.id] = false;
}
});
if ($scope.projectors.length) {
first_watch = false;
}
});
// Set list of speakers reference
$scope.setListOfSpeakers = function (projector) {
Config.get('projector_currentListOfSpeakers_reference').value = projector.id;
Config.save('projector_currentListOfSpeakers_reference');
};
// Projector functions
$scope.setProjectionDefault = function (projector, def) {
$http.post('/rest/core/projector/' + projector.id + '/set_projectiondefault/', def.id);
};
$scope.createProjector = function (name) {
var projector = {
name: name,
config: {},
scale: 0,
scroll: 0,
blank: false,
projectiondefaults: [],
};
Projector.create(projector).then(function (projector) {
$http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{
name: 'core/clock',
stable: true
}]);
$scope.resolutions[projector.id] = {
width: projector.width,
height: projector.height
};
});
};
$scope.deleteProjector = function (projector) {
if (projector.id != 1) {
Projector.destroy(projector.id);
}
};
$scope.editCurrentSlide = function (projector) {
var state = projector.getStateForCurrentSlide();
if (state) {
$state.go(state.state, state.param);
}
};
$scope.editName = function (projector) {
projector.config = projector.elements;
Projector.save(projector);
};
$scope.changeResolution = function (projectorId) {
$http.post(
'/rest/core/projector/' + projectorId + '/set_resolution/',
$scope.resolutions[projectorId]
).then(function (success) {
$scope.resolutions[projectorId].error = null;
}, function (error) {
$scope.resolutions[projectorId].error = error.data.detail;
});
};
// Identify projectors
$scope.identifyProjectors = function () {
if ($scope.identifyPromise) {
$timeout.cancel($scope.identifyPromise);
$scope.removeIdentifierMessages();
} else {
$scope.projectors.forEach(function (projector) {
$http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{
name: 'core/message',
stable: true,
selected: true,
visible: true,
message: gettextCatalog.getString('Projector') + ' ' + projector.id + ': ' + projector.name,
type: 'identify'
}]);
});
$scope.identifyPromise = $timeout($scope.removeIdentifierMessages, 3000);
}
};
$scope.removeIdentifierMessages = function () {
Projector.getAll().forEach(function (projector) {
$.each(projector.elements, function (uuid, value) {
if (value.name == 'core/message' && value.type == 'identify') {
$http.post('/rest/core/projector/' + projector.id + '/deactivate_elements/', [uuid]);
}
});
});
$scope.identifyPromise = null;
};
}
])

View File

@ -11,26 +11,6 @@
id="{{ key }}"
type="{{ type }}">
<!-- resolution -->
<!-- Can be removed with multiprojector, but maybe it could be reused in the projectormanage-view -->
<!-- if removed, remember to delete the class resolution -->
<span ng-if="type == 'resolution'">
<translate>Width</translate>:
<input ng-model="$parent.value.width"
ng-model-option="{debounce: 1000}"
ng-change="save(configOption.key, $parent.value)"
class="form-control resolution"
id="{{ key }}_width"
type="number">
<translate>Height</translate>:
<input ng-model="$parent.value.height"
ng-model-option="{debounce: 1000}"
ng-change="save(configOption.key, $parent.value)"
class="form-control resolution"
id="{{ key }}_height"
type="number">
</span>
<!-- comments -->
<div class="input-comments" ng-if="type == 'comments'">
<div ng-repeat="entry in $parent.value" class="input-group">

View File

@ -0,0 +1,200 @@
<div class="header">
<div class="title">
<div class="submenu">
<div>
<button class="btn btn-primary" ng-bootbox-prompt="{{ 'Please enter a name for the new projector' | translate }}"
ng-bootbox-prompt-action="createProjector(result)">
<i class="fa fa-plus"></i>
<translate>New</translate>
</button>
</div>
<div class="dropdown">
<button class="btn btn-default dropdown-toggle" id="menuListofSpeakers" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
<translate>Current list of speakers reference</translate>
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-entries" aria-labelledby="menuListOfSpeakers">
<li ng-repeat="projector in projectors"
ng-click="setListOfSpeakers(projector)">
<i class="fa fa-check" ng-if="projector.id == currentListOfSpeakers"></i>
{{ projector.name }}
</li>
</ul>
</div>
<div>
<button class="btn" ng-click="identifyProjectors()" ng-class="identifyPromise ? 'btn-primary' : 'btn-default'">
<i class="fa fa-binoculars"></i>
<translate>Identify</translate>
</button>
</div>
</div>
<h1 translate>Projektoren verwalten</h1>
</div>
</div>
<div class="details">
<div class="projectorContainer">
<div ng-repeat="projector in projectors">
<div>
<a ui-sref="projector({id: projector.id})">
{{ projector.id }}:
<strong>{{ projector.name }}</strong>
</a>
<a href="" class="pull-right" ng-click="edit[projector.id] = !edit[projector.id]"><i class="fa" ng-class="edit[projector.id] ? 'fa-times' : 'fa-pencil'"></i></a>
</div>
<div ng-show="edit[projector.id]" style="margin-bottom: -20px;">
<div>
<div class="dropdown" uib-dropdown>
<button class="btn btn-default btn-sm" id="menuProjector{{ pr.id }}" uib-dropdown-toggle>
<translate>Projection defaults</translate>
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-entries" aria-labelledby="menuProjector{{ pr.id }}">
<li ng-repeat="projectiondefault in projectiondefaults | orderBy:'id'"
ng-click="setProjectionDefault(projector, projectiondefault)">
<i class="fa fa-check" ng-if="projectiondefault.projector_id === projector.id"></i>
<translate>{{ projectiondefault.display_name }}</translate>
</li>
</ul>
</div>
<button type="button" class="btn btn-danger btn-sm"
ng-hide="projector.id==1"
ng-bootbox-confirm="{{ 'Are you sure you want to delete this entry?' | translate }}<br>
<b>{{ projector.name }}</b>"
ng-bootbox-confirm-action="deleteProjector(projector)">
<i class="fa fa-trash"></i>
<translate>Delete</translate>
</button>
<div ng-show="projector.id==1" style="height: 30px"><!-- Placeholder --></div>
</div>
<div>
<label for="name{{ projector.id }}" class="control-label"><translate>Name</translate>:</label>
<input type="text" class="form-control" id="name{{ projector.id }}"
ng-model="projector.name" ng-change="editName(projector)"
ng-model-options="{debounce: 2000}"></input>
</div>
<div>
<label for="resolution{{ projector.id }}" class="control-label"><translate>Resolution</translate>:</label>
<div id="resolution{{ projector.id }}">
<input ng-model="resolutions[projector.id].width"
ng-model-option="{debounce: 2000}"
ng-change="changeResolution(projector.id)"
class="form-control resolution"
id="{{ projector.id }}_width"
type="number">
x
<input ng-model="resolutions[projector.id].height"
ng-model-option="{debounce: 2000}"
ng-change="changeResolution(projector.id)"
class="form-control resolution"
id="{{ projector.id }}_height"
type="number">
</div>
<p class="help-block">
{{ resolutions[projector.id].error }}
</p>
</div>
</div>
<style>
#iframe_{{ projector.id }} {
width: {{ projector.width }}px;
height: {{ projector.height }}px;
-moz-transform: scale({{ projector.iframeScale }});
-webkit-transform: scale({{ projector.iframeScale }});
-o-transform: scale({{ projector.iframeScale }});
transform: scale({{ projector.iframeScale }});
/* IE8+ - must be on one line, unfortunately */
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11={{ projector.iframeScale }}, M12=0, M21=0, M22={{ projector.iframeScale }}, SizingMethod='auto expand')";
}
#iframewrapper_{{ projector.id }} {
height: {{ projector.iframeHeight }}px;
}
#iframeoverlay_{{ projector.id }} {
height: {{ projector.iframeHeight }}px;
}
</style>
<a ui-sref="projector({id: projector.id})" target="_blank">
<div class="iframewrapper" id="iframewrapper_{{ projector.id }}">
<iframe class="iframe" id="iframe_{{ projector.id }}" ng-src="{{ '/real-projector/' + projector.id }}" frameborder="0"></iframe>
<div class="iframeoverlay" id="iframeoverlay_{{ projector.id }}"></div>
</div>
</a>
<!-- projector control buttons -->
<div os-perms="core.can_manage_projector">
<!-- edit -->
<a ng-click="editCurrentSlide(projector)"
class="btn btn-default btn-sm"
title="{{ 'Edit current slide' | translate}}">
<i class="fa fa-pencil"></i>
</a>
<!-- scale -->
<div class="btn-group">
<a ng-click="projector.controlProjector('scale', 'down')"
class="btn btn-default btn-sm"
title="{{ 'Smaller' | translate}}">
<i class="fa fa-search-minus"></i>
</a>
<a ng-click="projector.controlProjector('scale', 'up')"
class="btn btn-default btn-sm"
title="{{ 'Bigger' | translate}}">
<i class="fa fa-search-plus"></i>
</a>
<a ng-click="projector.controlProjector('scale', 'reset')"
class="btn btn-default btn-sm"
title="{{ 'Reset scaling' | translate}}">
<i class="fa fa-undo"></i>
</a>
</div>
<span ng-class="{'notNull': projector.scale != 0}">{{ projector.scale }}</span>
<!-- scroll -->
<div class="btn-group">
<a ng-click="projector.controlProjector('scroll', 'down')"
class="btn btn-default btn-sm"
title="{{ 'Scroll up' | translate}}">
<i class="fa fa-arrow-up"></i>
</a>
<a ng-click="projector.controlProjector('scroll', 'up')"
class="btn btn-default btn-sm"
title="{{ 'Scroll down' | translate}}">
<i class="fa fa-arrow-down"></i>
</a>
<a ng-click="projector.controlProjector('scroll', 'reset')"
class="btn btn-default btn-sm"
title="{{ 'Reset scrolling' | translate}}">
<i class="fa fa-undo"></i>
</a>
</div>
<span ng-class="{'notNull': projector.scroll != 0}">{{ projector.scroll }}</span>
</div>
<!-- Default, BC, Blank -->
<div class="middle">
<div class="btn-group">
<button class="btn btn-sm" ng-class="broadcast == projector.id ? 'btn-primary' : 'btn-default'"
ng-click="projector.toggleBroadcast(projector)"
ng-disabled="broadcast > 0 && broadcast != projector.id">
<i class="fa" ng-class="broadcast == projector.id ? 'fa-star' : 'fa-star-o'"></i>
<translate>Broadcast</translate>
</button>
<button class="btn btn-sm" ng-class="projector.blank ? 'btn-primary' : 'btn-default'"
ng-click="projector.toggleBlank(projector)"
ng-disabled="broadcast > 0 && broadcast != projector.id">
<i class="fa" ng-class="projector.blank ? 'fa-square' : 'fa-square-o'"></i>
<translate>Blank</translate>
</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -10,10 +10,9 @@
</a>
<div uib-collapse="isLiveViewClosed" ng-cloak>
<style>
/* iframe for live view */
.col2 #iframe {
width: {{ projectorWidth }}px;
height: {{ projectorHeight }}px;
.col2 #iframe_sidebar {
width: {{ active_projector.width }}px;
height: {{ active_projector.height }}px;
-moz-transform: scale({{ scale }});
-webkit-transform: scale({{ scale }});
-o-transform: scale({{ scale }});
@ -22,25 +21,72 @@
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11={{ scale }}, M12=0, M21=0, M22={{ scale }}, SizingMethod='auto expand')";
}
.col2 #iframewrapper {
.col2 #iframewrapper_sidebar {
height: {{ iframeHeight }}px;
}
.col2 #iframeoverlay {
.col2 #iframeoverlay_sidebar {
height: {{ iframeHeight }}px;
}
</style>
<a ui-sref="projector" target="_blank">
<div id="iframewrapper">
<iframe id="iframe" src="/real-projector" frameborder="0"></iframe>
<div id="iframeoverlay"></div>
<div class="projectorSelector">
<div>
<div class="dropdown" ng-show="projectors.length > 1">
<button class="btn btn-default btn-sm dropdown-toggle" id="menuProjector" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
<i class="fa fa-video-camera"></i>
{{ active_projector.name }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-entries" aria-labelledby="menuProjector">
<li ng-repeat="projector in projectors"
ng-class="{'projected': projector === active_projector}"
ng-click="changeProjector(projector)">
<i ng-show="projector === active_projector" class="fa fa-video-camera"></i>
{{ projector.name }}
<i ng-show="projector.id == broadcast" class="fa fa-star-o spacer-left"></i>
</li>
</ul>
</div>
<div>
<button class="btn btn-sm" ng-click="active_projector.toggleBlank()" ng-hide="projectors.length > 1"
ng-class="active_projector.blank ? 'btn-primary' : 'btn-default'">
<i class="fa" ng-class="active_projector.blank ? 'fa-square' : 'fa-square-o'"></i>
<translate>Blank</translate>
</button>
</div>
<a class="btn btn-primary btn-sm manageBtn" ui-sref="manage-projectors">
<i class="fa fa-cog fa-lg"></i>
<translate>Manage</translate>
</a>
</div>
<div class="btn-group" ng-show="projectors.length > 1">
<button class="btn btn-sm" ng-class="broadcast == active_projector.id ? 'btn-primary' : 'btn-default'"
ng-click="active_projector.toggleBroadcast()" ng-disabled="broadcast > 0 && broadcast != active_projector.id">
<i class="fa" ng-class="broadcast == active_projector.id ? 'fa-star' : 'fa-star-o'"></i>
<translate>Broadcast</translate>
</button>
<button class="btn btn-sm" ng-click="active_projector.toggleBlank()"
ng-class="active_projector.blank ? 'btn-primary' : 'btn-default'"
ng-disabled="broadcast > 0 && broadcast != active_projector.id">
<i class="fa" ng-class="active_projector.blank ? 'fa-square' : 'fa-square-o'"></i>
<translate>Blank</translate>
</button>
</div>
</div>
<a ui-sref="projector({id: active_projector.id })" target="_blank">
<div class="iframewrapper" id="iframewrapper_sidebar">
<iframe class="iframe" id="iframe_sidebar" ng-src="{{ '/real-projector/' + active_projector.id }}" frameborder="0"></iframe>
<div class="iframeoverlay" id="iframeoverlay_sidebar"></div>
</div>
</a>
<!-- projector control buttons -->
<div os-perms="core.can_manage_projector">
<!-- edit -->
<a ng-click="editCurrentSlide()"
<a ng-click="editCurrentSlide(active_projector)"
class="btn btn-default btn-sm"
title="{{ 'Edit current slide' | translate}}">
<i class="fa fa-pencil"></i>
@ -48,43 +94,43 @@
<!-- scale -->
<div class="btn-group">
<a ng-click="controlProjector('scale', 'down')"
<a ng-click="active_projector.controlProjector('scale', 'down')"
class="btn btn-default btn-sm"
title="{{ 'Smaller' | translate}}">
<i class="fa fa-search-minus"></i>
</a>
<a ng-click="controlProjector('scale', 'up')"
<a ng-click="active_projector.controlProjector('scale', 'up')"
class="btn btn-default btn-sm"
title="{{ 'Bigger' | translate}}">
<i class="fa fa-search-plus"></i>
</a>
<a ng-click="controlProjector('scale', 'reset')"
<a ng-click="active_projector.controlProjector('scale', 'reset')"
class="btn btn-default btn-sm"
title="{{ 'Reset scaling' | translate}}">
<i class="fa fa-undo"></i>
</a>
</div>
<span ng-class="{ 'notNull': scaleLevel != 0 }">{{ scaleLevel }}</span>
<span ng-class="{ 'notNull': active_projector.scale != 0 }">{{ active_projector.scale }}</span>
<!-- scroll -->
<div class="btn-group">
<a ng-click="controlProjector('scroll', 'down')"
<a ng-click="active_projector.controlProjector('scroll', 'down')"
class="btn btn-default btn-sm"
title="{{ 'Scroll up' | translate}}">
<i class="fa fa-arrow-up"></i>
</a>
<a ng-click="controlProjector('scroll', 'up')"
<a ng-click="active_projector.controlProjector('scroll', 'up')"
class="btn btn-default btn-sm"
title="{{ 'Scroll down' | translate}}">
<i class="fa fa-arrow-down"></i>
</a>
<a ng-click="controlProjector('scroll', 'reset')"
<a ng-click="active_projector.controlProjector('scroll', 'reset')"
class="btn btn-default btn-sm"
title="{{ 'Reset scrolling' | translate}}">
<i class="fa fa-undo"></i>
</a>
</div>
<span ng-class="{ 'notNull': scrollLevel != 0 }">{{ scrollLevel }}</span>
<span ng-class="{ 'notNull': active_projector.scroll != 0 }">{{ active_projector.scroll }}</span>
</div>
</div>
</div>
@ -96,11 +142,10 @@
<h4 translate>Countdowns</h4>
</a>
<div uib-collapse="!isCountdowns" ng-cloak>
<div ng-repeat="countdown in countdowns | orderBy: 'index'" id="{{countdown.uuid}}"
class="countdown panel panel-default">
<div ng-repeat="countdown in countdowns | orderBy: 'index'" id="countdown{{countdown.uuid}}" class="countdown panel panel-default">
<div class="panel-heading">
<span ng-if="countdown.description">{{ countdown.description }}</span>
<span ng-if="!countdown.description">Countdown {{ countdown.index +1 }}</span>
<span ng-if="!countdown.description">Countdown {{ $index +1 }}</span>
<!-- remove countdown button -->
<button type="button" class="close"
ng-click="removeCountdown(countdown)"
@ -109,36 +154,58 @@
</button>
<!-- edit countdown button -->
<button type="button" class="close editicon"
ng-click="editCountdownFlag=true;"
ng-click="countdown.editFlag=true;"
title="{{ 'Edit countdown' | translate}}">
<i class="fa fa-pencil"></i>
</button>
</div>
<div class="panel-body"
ng-class="{ 'projected': countdown.visible }">
ng-class="{ 'projected': isProjected(countdown).length }">
<!-- project countdown button -->
<a class="btn btn-default btn-sm"
ng-model="countdown.visible"
ng-click="showCountdown(countdown)"
ng-class="{ 'btn-primary': countdown.visible }"
title="{{ 'Project countdown' | translate }}">
<i class="fa fa-video-camera"></i>
</a>
<div class="btn-group" style="width:54px;" uib-dropdown>
<button type="button" class="btn btn-default btn-sm"
ng-click="project(countdown)"
ng-class="{ 'btn-primary': isProjected(countdown).length }">
<i class="fa fa-video-camera"></i>
</button>
<button ng-if="projectors.length > 1" type="button" class="btn btn-default btn-sm slimDropDown" uib-dropdown-toggle>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li role="menuitem" ng-if="projectors.length > 1" style="text-align: center;">
<span class="pointer" ng-click="selectAll(countdown, true); preventClose($event)" translate>
All
</span>
| <span class="pointer" ng-click="selectAll(countdown, false); preventClose($event)" translate>
None
</span>
</li>
<li class="divider" ng-if="projectors.length > 1"></li>
<li role="menuitem" ng-repeat="projector in projectors">
<a href="" ng-click="toggleProjector(countdown, projector); preventClose($event)"
ng-class="{ 'projected': isProjectedOn(countdown, projector) }">
<i class="fa fa-square-o" ng-hide="hasProjector(countdown, projector)"></i>
<i class="fa fa-check-square-o" ng-show="hasProjector(countdown, projector)"></i>
{{ projector.name }}
</a>
</li>
</ul>
</div>
&nbsp;&nbsp;
<!-- countdown controls -->
<a class="btn btn-default vcenter"
ng-click="resetCountdown(countdown)"
ng-class="{ 'disabled': countdown.status == 'stop' && countdown.default == countdown.countdown_time }"
ng-class="{ 'disabled': !countdown.running && countdown.default_time == countdown.countdown_time }"
title="{{ 'Reset countdown' | translate}}">
<i class="fa fa-stop"></i>
</a>
<a ng-if="countdown.status=='stop'" class="btn btn-default vcenter"
<a ng-if="!countdown.running" class="btn btn-default vcenter"
ng-click="startCountdown(countdown)"
title="{{ 'Start' | translate}}">
<i class="fa fa-play"></i>
<i ng-if="countdown.status=='running'" class="fa fa-pause"></i>
<i ng-if="countdown.running" class="fa fa-pause"></i>
</a>
<a ng-if="countdown.status=='running'" class="btn btn-default vcenter"
<a ng-if="countdown.running" class="btn btn-default vcenter"
ng-click="stopCountdown(countdown)"
title="{{ 'Pause' | translate}}">
<i class="fa fa-pause"></i>
@ -150,22 +217,29 @@
{{ countdown.seconds | osSecondsToTime }}
</span>
<!-- edit countdown form -->
<form ng-show="editCountdownFlag" ng-submit="editCountdown(countdown)">
<form ng-show="countdown.editFlag"
ng-submit="editCountdown(countdown)">
<div class="form-group">
<label translate>Description</label>
<input ng-model="countdown.description" type="text" class="form-control input-sm">
</div>
<div class="form-group">
<label translate>Start time</label>
<input data-ng-model="countdown.default" min-sec-format
<div class="input-group">
<input data-ng-model="countdown.default_time" min-sec-format
type="text" placeholder="mm:ss" class="form-control input-sm">
<div class="input-group-addon pointer" uib-tooltip="{{ 'Reset countdown' | translate }}"
ng-click="countdown.reset()">
<i class="fa fa-undo"></i>
</div>
</div>
</div>
<button type="submit"
title="{{ 'Save' | translate}}"
class="btn btn-sm btn-primary">
<i class="fa fa-check"></i>
</button>
<button ng-click="editCountdownFlag=false;"
<button ng-click="countdown.editFlag=false;"
title="{{ 'Cancel' | translate}}"
class="btn btn-default btn-sm">
<i class="fa fa-times"></i>
@ -182,7 +256,6 @@
</div>
</div>
<!-- messages -->
<div class="section" os-perms="core.can_manage_projector">
<a href="#" ng-click="isMessages = !isMessages">
@ -190,10 +263,10 @@
<h4 translate>Messages</h4>
</a>
<div uib-collapse="!isMessages" ng-cloak>
<div ng-repeat="message in messages | orderBy: 'index'" id="{{message.uuid}}" class="message panel panel-default">
<div ng-repeat="message in messages" id="message{{message.id}}" class="message panel panel-default">
<div class="panel-heading">
<span>{{ 'Message' | translate }} {{ message.index + 1 }}</span>
<span>{{ 'Message' | translate }} {{ $index + 1 }}</span>
<!-- remove message button -->
<button type="button" class="close"
ng-click="removeMessage(message)"
@ -201,29 +274,52 @@
<i class="fa fa-times"></i>
</button>
<button type="button" class="close editicon"
ng-click="editMessageFlag=true;"
ng-click="message.editFlag=true"
title="{{ 'Edit message' | translate}}">
<i class="fa fa-pencil"></i>
</button>
</div>
<div class="panel-body"
ng-class="{ 'projected': message.visible }">
ng-class="{ 'projected': isProjected(message).length }">
<div class="projectorbtn">
<!-- project message button -->
<a class="btn btn-default btn-sm"
ng-model="message.visible"
ng-click="showMessage(message)"
ng-class="{ 'btn-primary': message.visible }"
title="{{ 'Project message' | translate }}" float="left">
<i class="fa fa-video-camera"></i>
</a>
<div class="btn-group" style="width:54px;" uib-dropdown>
<button type="button" class="btn btn-default btn-sm"
ng-click="project(message)"
ng-class="{ 'btn-primary': isProjected(message).length }">
<i class="fa fa-video-camera"></i>
</button>
<button type="button" ng-if="projectors.length > 1" class="btn btn-default btn-sm slimDropDown" uib-dropdown-toggle>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li role="menuitem" ng-if="projectors.length > 1" style="text-align: center;">
<span class="pointer" ng-click="selectAll(message, true); preventClose($event)" translate>
All
</span>
| <span class="pointer" ng-click="selectAll(message, false); preventClose($event)" translate>
None
</span>
</li>
<li class="divider" ng-if="projectors.length > 1"></li>
<li role="menuitem" ng-repeat="projector in projectors">
<a href="" ng-click="toggleProjector(message, projector); preventClose($event)"
ng-class="{ 'projected': isProjectedOn(message, projector) }">
<i class="fa fa-square-o" ng-hide="hasProjector(message, projector)"></i>
<i class="fa fa-check-square-o" ng-show="hasProjector(message, projector)"></i>
{{ projector.name }}
</a>
</li>
</ul>
</div>
</div>
&nbsp;&nbsp;
<div class="innermessage" ng-bind-html="message.message"> </div>
<div class="panel-input">
<div ng-if="editMessageFlag" class="input-group">
<div ng-if="message.editFlag" class="input-group">
<input ng-model="message.message" type="text" class="form-control input-sm">
<a ng-click="editMessage(message)"
title="{{ 'Save' | translate}}"

View File

@ -1,5 +1,5 @@
<div ng-controller="SlideCountdownCtrl">
<div ng-if="visible">
<div ng-if="visible && selected">
<div class="countdown well pull-right"
ng-class="{
'negative': seconds <= 0,

View File

@ -1,4 +1,4 @@
<div ng-controller="SlideMessageCtrl">
<div ng-if="visible" class="message_background"></div>
<div ng-if="visible" class="message well" ng-bind-html="message"></div>
<div ng-if="visible && selected" class="message_background"></div>
<div ng-if="visible && selected" class="message well" ng-class="{'identify': type=='identify'}" ng-bind-html="message"></div>
</div>

View File

@ -0,0 +1,27 @@
<div class="btn-group" style="min-width:{{ projectors.length > 1 ? '54' : '34' }}px;" uib-dropdown
uib-tooltip="{{ 'Projector' | translate }} {{ model.isProjected(additionalId) }}"
tooltip-enable="model.isProjected(additionalId) > 0"
os-perms="core.can_manage_projector">
<button type="button" class="btn btn-default btn-sm"
ng-click="model.project(defaultProjectorId, additionalId)"
ng-class="{ 'btn-primary': model.isProjected(additionalId) == defaultProjectorId }">
<i class="fa fa-video-camera"></i>
{{ content }}
</button>
<button type="button" class="btn btn-default btn-sm slimDropDown"
ng-class="{ 'btn-primary': (model.isProjected(additionalId) > 0 && model.isProjected(additionalId) != defaultProjectorId) }"
ng-if="projectors.length > 1"
uib-dropdown-toggle>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li role="menuitem" ng-repeat="projector in projectors">
<a href="" ng-click="model.project(projector.id, additionalId)"
ng-class="{ 'projected': (model.isProjected(additionalId) == projector.id) }">
<i class="fa fa-video-camera" ng-show="model.isProjected(additionalId) == projector.id"></i>
{{ projector.name }}
<span ng-if="defaultProjectorId == projector.id">(<translate>Standard</translate>)</span>
</a>
</li>
</ul>
</div>

View File

@ -37,10 +37,14 @@
}
</style>
<div id="iframewrapper">
<iframe id="iframe" src="/real-projector" frameborder="0"></iframe>
<div id="iframewrapper" ng-hide="error">
<iframe id="iframe" ng-src="{{ '/real-projector/' + projector_id }}" frameborder="0"></iframe>
<div id="iframeoverlay"></div>
</div>
<div class="error" ng-show="error">
<p>{{ error | translate }}</p>
</div>
</div>
</div>

View File

@ -11,34 +11,41 @@
<script src="static/js/openslides.js"></script>
<script src="static/js/openslides-templates.js"></script>
<style type="text/css">
#header, #footer {
background-color: {{ config('projector_header_backgroundcolor') }};
}
#header, #footer, #currentTime {
color: {{ config('projector_header_fontcolor') }};
}
h1 {
color: {{ config('projector_h1_fontcolor') }};
}
</style>
<div id="projectorContainer" ng-controller="ProjectorCtrl">
<style type="text/css">
#header, #footer {
background-color: {{ config('projector_header_backgroundcolor') }};
}
#header, #footer, #currentTime {
color: {{ config('projector_header_fontcolor') }};
}
#header, #footer, .contentContainer {
visibility: {{ blank ? 'hidden' : 'visible' }};
}
#projectorContainer {
background-color: {{ blank ? config('projector_blank_color') : '#fff' }};
height: {{ blank ? '100%' : 'auto' }};
}
h1 {
color: {{ config('projector_h1_fontcolor') }};
}
</style>
<div id="header">
<img ng-if="config('projector_enable_logo')" id="logo" src="/static/img/logo-projector.png" alt="OpenSlides" />
<div ng-if="config('projector_enable_title')" id="eventdata">
<div class="title" ng-class="{ 'titleonly': !config('general_event_description') }"
ng-bind-html="config('general_event_name')"></div>
<div ng-if="config('general_event_description')" class="description"
ng-bind-html="config('general_event_description')"></div>
<div id="header">
<img ng-if="config('projector_enable_logo')" id="logo" src="/static/img/logo-projector.png" alt="OpenSlides" />
<div ng-if="config('projector_enable_title')" id="eventdata">
<div class="title" ng-class="{ 'titleonly': !config('general_event_description') }"
ng-bind-html="config('general_event_name')"></div>
<div ng-if="config('general_event_description')" class="description"
ng-bind-html="config('general_event_description')"></div>
</div>
</div>
</div>
<div ng-controller="ProjectorCtrl">
<style type="text/css">
.scrollcontent {
margin-top: {{scroll}}px !important;
font-size: {{scale}}%;
margin-top: {{ -80 * projector.scroll }}px !important;
font-size: {{ 100 + 20 * projector.scale }}%;
}
.mediascrollcontent {
margin-top: {{scroll/2}}em !important;
@ -47,21 +54,21 @@
transform: scale({{scale/100}});
}
</style>
<div ng-repeat="element in elements | orderBy:'index'">
<div class="contentContainer" ng-repeat="element in elements | orderBy:'index'">
<div ng-include="element.template"></div>
</div>
<div id="footer">
<span ng-if="config('general_event_date')">
{{ config('general_event_date') }}
</span>
<span ng-if="config('general_event_date') && config('general_event_location')">
|
</span>
<span ng-if="config('general_event_location')">
{{ config('general_event_location') }}
</span>
</div>
</div>
<div id="footer">
<span ng-if="config('general_event_date')">
{{ config('general_event_date') }}
</span>
<span ng-if="config('general_event_date') && config('general_event_location')">
|
</span>
<span ng-if="config('general_event_location')">
{{ config('general_event_location') }}
</span>
</div>
<script src="/webclient/projector/"></script>

View File

@ -28,10 +28,10 @@ urlpatterns = [
name='core_webclient_javascript'),
# View for the projectors are handled by angular.
url(r'^projector.*$', views.ProjectorView.as_view()),
url(r'^projector/(\d+)/$', views.ProjectorView.as_view()),
# Original view without resolutioncontrol for the projectors are handled by angular.
url(r'^real-projector.*$', views.RealProjectorView.as_view()),
url(r'^real-projector/(\d+)/$', views.RealProjectorView.as_view()),
# Main entry point for all angular pages.
# Has to be the last entry in the urls.py

View File

@ -18,6 +18,7 @@ from django.utils.timezone import now
from openslides import __version__ as version
from openslides.utils import views as utils_views
from openslides.utils.autoupdate import inform_changed_data
from openslides.utils.plugins import (
get_plugin_description,
get_plugin_verbose_name,
@ -25,7 +26,6 @@ from openslides.utils.plugins import (
)
from openslides.utils.rest_api import (
ModelViewSet,
ReadOnlyModelViewSet,
Response,
SimpleMetadata,
ValidationError,
@ -42,7 +42,7 @@ from .access_permissions import (
)
from .config import config
from .exceptions import ConfigError, ConfigNotFound
from .models import ChatMessage, Projector, Tag
from .models import ChatMessage, ProjectionDefault, Projector, Tag
# Special Django views
@ -171,7 +171,7 @@ class WebclientJavaScriptView(utils_views.View):
# Viewsets for the REST API
class ProjectorViewSet(ReadOnlyModelViewSet):
class ProjectorViewSet(ModelViewSet):
"""
API endpoint for the projector slide info.
@ -192,13 +192,23 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
result = self.request.user.has_perm('core.can_see_projector')
elif self.action in ('activate_elements', 'prune_elements', 'update_elements',
'deactivate_elements', 'clear_elements', 'control_view',
'set_resolution', 'set_scroll'):
'set_resolution', 'set_scroll', 'control_blank', 'destroy',
'create', 'update', 'broadcast', 'set_projectiondefault'):
result = (self.request.user.has_perm('core.can_see_projector') and
self.request.user.has_perm('core.can_manage_projector'))
else:
result = False
return result
# Assign all projectionDefaults from this projector to the default projector (pk=1)
def destroy(self, *args, **kwargs):
projector_instance = self.get_object()
for a in ProjectionDefault.objects.all():
if a.projector.id == projector_instance.id:
a.projector = Projector.objects.get(pk=1)
a.save()
return super(ProjectorViewSet, self).destroy(*args, **kwargs)
@detail_route(methods=['post'])
def activate_elements(self, request, pk):
"""
@ -447,6 +457,70 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
scroll=request.data)
return Response({'detail': message})
@detail_route(methods=['post'])
def control_blank(self, request, pk):
"""
REST API operation to blank the projector.
It expects a POST request to
/rest/core/projector/<pk>/control_blank/ with a value for blank.
"""
if not isinstance(request.data, bool):
raise ValidationError({'detail': 'Data must be a bool.'})
projector_instance = self.get_object()
projector_instance.blank = request.data
projector_instance.save()
message = "Setting 'blank' to {blank} was successful.".format(
blank=request.data)
return Response({'detail': message})
@detail_route(methods=['post'])
def broadcast(self, request, pk):
"""
REST API operation to (un-)broadcast the given projector.
This method takes care, that all other projectors get the new requirements.
It expects a POST request to
/rest/core/projector/<pk>/broadcast/ without an argument
"""
if config['projector_broadcast'] == 0:
config['projector_broadcast'] = pk
projector_instance = self.get_object()
inform_changed_data(projector_instance)
message = "Setting projector {id} as broadcast projector was successful.".format(
id=pk)
else:
config['projector_broadcast'] = 0
message = "Disabling broadcast was successful."
return Response({'detail': message})
@detail_route(methods=['post'])
def set_projectiondefault(self, request, pk):
"""
REST API operation to set a projectiondefault to the requested projector. The argument
has to be an int representing the pk from the projectiondefault to be set.
It expects a POST request to
/rest/core/projector/<pk>/set_projectiondefault/ with the projectiondefault id as the argument
"""
if not isinstance(request.data, int):
raise ValidationError({'detail': 'Data must be an int.'})
try:
projectiondefault = ProjectionDefault.objects.get(pk=request.data)
except ProjectionDefault.DoesNotExist:
raise ValidationError({'detail': 'The projectiondefault with pk={pk} was not found.'.format(
pk=request.data)})
else:
projector_instance = self.get_object()
projectiondefault.projector = projector_instance
projectiondefault.save()
return Response('Setting projectiondefault "{name}" to projector {projector_id} was successful.'.format(
name=projectiondefault.display_name,
projector_id=projector_instance.pk))
class TagViewSet(ModelViewSet):
"""

View File

@ -56,19 +56,35 @@ angular.module('OpenSlidesApp.mediafiles.site', ['ngFileUpload', 'OpenSlidesApp.
'MediafileForm',
'User',
'Projector',
function($scope, $http, ngDialog, Mediafile, MediafileForm, User, Projector) {
'ProjectionDefault',
function($scope, $http, ngDialog, Mediafile, MediafileForm, User, Projector, ProjectionDefault) {
Mediafile.bindAll({}, $scope, 'mediafiles');
User.bindAll({}, $scope, 'users');
$scope.$watch(function() {
return Projector.lastModified();
}, function() {
$scope.projectors = Projector.getAll();
updatePresentedMediafiles();
});
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var projectiondefault = ProjectionDefault.filter({name: 'mediafiles'})[0];
if (projectiondefault) {
$scope.defaultProjectorId = projectiondefault.projector_id;
}
});
// setup table sorting
$scope.sortColumn = 'title';
$scope.filterPresent = '';
$scope.reverse = false;
function updatePresentedMediafiles() {
var projectorElements = _.map(Projector.get(1).elements, function(element) { return element; });
$scope.presentedMediafiles = _.filter(projectorElements, function (element) {
return element.name === 'mediafiles/mediafile';
function updatePresentedMediafiles () {
$scope.presentedMediafiles = [];
Projector.getAll().forEach(function (projector) {
var projectorElements = _.map(projector.elements, function(element) { return element; });
var mediaElements = _.filter(projectorElements, function (element) {
return element.name === 'mediafiles/mediafile';
});
mediaElements.forEach(function (element) {
$scope.presentedMediafiles.push(element);
});
});
if ($scope.presentedMediafiles.length) {
$scope.isMeta = false;
@ -77,12 +93,13 @@ angular.module('OpenSlidesApp.mediafiles.site', ['ngFileUpload', 'OpenSlidesApp.
}
}
$scope.$watch(function() {
return Projector.get(1).elements;
}, updatePresentedMediafiles);
updatePresentedMediafiles();
// setup table sorting
$scope.sortColumn = 'title';
$scope.filterPresent = '';
$scope.reverse = false;
// function to sort by clicked column
$scope.toggleSort = function ( column ) {
if ( $scope.sortColumn === column ) {
@ -138,37 +155,47 @@ angular.module('OpenSlidesApp.mediafiles.site', ['ngFileUpload', 'OpenSlidesApp.
// ** PDF presentation functions **/
// show document on projector
$scope.showMediafile = function (mediafile) {
var postUrl = '/rest/core/projector/1/prune_elements/';
var data = [{
name: 'mediafiles/mediafile',
id: mediafile.id,
numPages: mediafile.mediafile.pages,
page: 1,
scale: 'page-fit',
rotate: 0,
visible: true,
playing: false,
fullscreen: mediafile.is_pdf
}];
$http.post(postUrl, data);
$scope.showMediafile = function (projectorId, mediafile) {
var isProjectedId = mediafile.isProjected();
if (isProjectedId > 0) {
$http.post('/rest/core/projector/' + isProjectedId + '/prune_elements/', []);
}
if (isProjectedId != projectorId) {
var postUrl = '/rest/core/projector/' + projectorId + '/prune_elements/';
var data = [{
name: 'mediafiles/mediafile',
id: mediafile.id,
numPages: mediafile.mediafile.pages,
page: 1,
scale: 'page-fit',
rotate: 0,
visible: true,
playing: false,
fullscreen: mediafile.is_pdf
}];
$http.post(postUrl, data);
}
};
function sendMediafileCommand(data) {
var mediafileElement = getCurrentlyPresentedMediafile();
var updateData = _.extend({}, mediafileElement);
// To avoid some kind of 60,000000000001% in template
$scope.round = function (val) {return Math.round(val);};
var sendMediafileCommand = function (mediafile, data) {
var updateData = _.extend({}, mediafile);
_.extend(updateData, data);
var postData = {};
postData[mediafileElement.uuid] = updateData;
$http.post('/rest/core/projector/1/update_elements/', postData);
}
postData[mediafile.uuid] = updateData;
// Find Projector where the mediafile is projected
$scope.projectors.forEach(function (projector) {
if (_.find(projector.elements, function (e) {return e.uuid == mediafile.uuid;})) {
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', postData);
}
});
};
function getCurrentlyPresentedMediafile() {
return $scope.presentedMediafiles[0];
}
$scope.getTitle = function (presentedMediafile) {
return Mediafile.get(presentedMediafile.id).title;
$scope.getTitle = function (mediafile) {
return Mediafile.get(mediafile.id).title;
};
$scope.getType = function(presentedMediafile) {
@ -176,55 +203,57 @@ angular.module('OpenSlidesApp.mediafiles.site', ['ngFileUpload', 'OpenSlidesApp.
return mediafile.is_pdf ? 'pdf' : mediafile.is_image ? 'image' : 'video';
};
$scope.mediafileGoToPage = function (page) {
var mediafileElement = getCurrentlyPresentedMediafile();
$scope.mediafileGoToPage = function (mediafile, page) {
if (parseInt(page) > 0) {
sendMediafileCommand({
page: parseInt(page)
});
sendMediafileCommand(
mediafile,
{page: parseInt(page)}
);
}
};
$scope.mediafileZoomIn = function () {
var mediafileElement = getCurrentlyPresentedMediafile();
$scope.mediafileZoomIn = function (mediafile) {
var scale = 1;
if (parseFloat(mediafileElement.scale)) {
scale = mediafileElement.scale;
if (parseFloat(mediafile.scale)) {
scale = mediafile.scale;
}
sendMediafileCommand({
scale: scale + 0.2
});
sendMediafileCommand(
mediafile,
{scale: scale + 0.2}
);
};
$scope.mediafileFit = function () {
sendMediafileCommand({
scale: 'page-fit'
});
$scope.mediafileFit = function (mediafile) {
sendMediafileCommand(
mediafile,
{scale: 'page-fit'}
);
};
$scope.mediafileZoomOut = function () {
var mediafileElement = getCurrentlyPresentedMediafile();
$scope.mediafileZoomOut = function (mediafile) {
var scale = 1;
if (parseFloat(mediafileElement.scale)) {
scale = mediafileElement.scale;
if (parseFloat(mediafile.scale)) {
scale = mediafile.scale;
}
sendMediafileCommand({
scale: scale - 0.2
});
sendMediafileCommand(
mediafile,
{scale: scale - 0.2}
);
};
$scope.mediafileChangePage = function(pageNum) {
sendMediafileCommand({
pageToDisplay: pageNum
});
$scope.mediafileChangePage = function(mediafile, pageNum) {
sendMediafileCommand(
mediafile,
{pageToDisplay: pageNum}
);
};
$scope.mediafileRotate = function () {
var mediafileElement = getCurrentlyPresentedMediafile();
var rotation = mediafileElement.rotate;
$scope.mediafileRotate = function (mediafile) {
var rotation = mediafile.rotate;
if (rotation === 270) {
rotation = 0;
} else {
rotation = rotation + 90;
}
sendMediafileCommand({
rotate: rotation
});
sendMediafileCommand(
mediafile,
{rotate: rotation}
);
};
$scope.mediafileScroll = function(scroll) {
var mediafileElement = getCurrentlyPresentedMediafile();

View File

@ -28,12 +28,12 @@
<h3>{{ getTitle(presentedMediafile) }}</h3>
<nav ng-show="getType(presentedMediafile) === 'pdf'" ng-class="getNavStyle(scroll)" class="form-inline">
<div class="btn-group">
<button class="btn btn-default" ng-click="mediafileGoToPage(presentedMediafile.page - 1)"
<button class="btn btn-default" ng-click="mediafileGoToPage(presentedMediafile, presentedMediafile.page - 1)"
ng-class="{ 'disabled': (presentedMediafile.page - 1) < 1 }"
title="{{ 'Previous page' | translate }}">
<i class="fa fa-backward"></i>
</button>
<button class="btn btn-default" ng-click="mediafileGoToPage(presentedMediafile.page + 1)"
<button class="btn btn-default" ng-click="mediafileGoToPage(presentedMediafile, presentedMediafile.page + 1)"
ng-class="{ 'disabled': (presentedMediafile.page + 1) > presentedMediafile.numPages }"
title="{{ 'Next page' | translate }}">
<i class="fa fa-forward"></i>
@ -42,23 +42,23 @@
<div class="input-group">
<span class="input-group-addon" translate>Page</span>
<input type="number" min=1 ng-model="presentedMediafile.page" class="form-control" style="width: 80px"
ng-change="mediafileGoToPage(presentedMediafile.page)">
ng-change="mediafileGoToPage(presentedMediafile, presentedMediafile.page)">
<span class="input-group-addon"><translate>of</translate> {{presentedMediafile.numPages}}</span>
</div>
<div class="btn-group">
<button class="btn btn-default" ng-click="mediafileRotate()" title="{{ 'Rotate clockwise' | translate }}">
<button class="btn btn-default" ng-click="mediafileRotate(presentedMediafile)" title="{{ 'Rotate clockwise' | translate }}">
<i class="fa fa-repeat"></i>
</button>
</div>
<div class="btn-group">
<button class="btn btn-default" ng-click="mediafileZoomOut()" title="{{ 'Zoom out' | translate }}">
<button class="btn btn-default" ng-click="mediafileZoomOut(presentedMediafile)" title="{{ 'Zoom out' | translate }}">
<i class="fa fa-search-minus"></i>
</button>
<button class="btn" ng-click="mediafileFit()" title="{{ 'Reset zoom' | translate }}"
<button class="btn" ng-click="mediafileFit(presentedMediafile)" title="{{ 'Reset zoom' | translate }}"
ng-class="presentedMediafile.scale=='page-fit' ? 'btn-primary' : 'btn-default'">
<i class="fa fa-arrows-alt"></i>
</button>
<button class="btn btn-default" ng-click="mediafileZoomIn()" title="{{ 'Zoom in' | translate }}">
<button class="btn btn-default" ng-click="mediafileZoomIn(presentedMediafile)" title="{{ 'Zoom in' | translate }}">
<i class="fa fa-search-plus"></i>
</button>
</div>
@ -195,13 +195,32 @@
<!-- projector column -->
<td ng-show="!isDeleteMode"
os-perms="core.can_manage_projector">
<a class="btn btn-default btn-sm"
ng-if="mediafile.is_presentable"
ng-class="{ 'btn-primary': mediafile.isProjected() }"
ng-click="showMediafile(mediafile)"
title="{{ 'Project mediafile' | translate }}">
<i class="fa fa-video-camera"></i>
</a>
<div class="btn-group" style="min-width:54px;" uib-dropdown
ng-if="mediafile.mediafile.is_presentable"
uib-tooltip="{{ 'Projektor' | translate }} {{ mediafile.isProjected() }}"
tooltip-enable="mediafile.isProjected() > 0">
<button type="button" class="btn btn-default btn-sm"
ng-click="showMediafile(defaultProjectorId, mediafile)"
ng-class="{ 'btn-primary': mediafile.isProjected() == defaultProjectorId }">
<i class="fa fa-video-camera"></i>
</button>
<button type="button" class="btn btn-default btn-sm slimDropDown"
ng-class="{ 'btn-primary': (mediafile.isProjected() > 0 && mediafile.isProjected() != defaultProjectorId) }"
ng-if="projectors.length > 1"
uib-dropdown-toggle>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li role="menuitem" ng-repeat="projector in projectors">
<a href="" ng-click="showMediafile(projector.id, mediafile)"
ng-class="{ 'projected': (mediafile.isProjected() == projector.id) }">
<i class="fa fa-video-camera" ng-show="mediafile.isProjected() == projector.id"></i>
{{ projector.name }}
<span ng-if="defaultProjectorId == projector.id">(<translate>Standard</translate>)</span>
</a>
</li>
</ul>
</div>
<!-- delete selection column -->
<td ng-show="isDeleteMode" os-perms="mediafiles.can_manage" class="deleteColumn">
<input type="checkbox" ng-model="mediafile.selected">

View File

@ -21,7 +21,8 @@ angular.module('OpenSlidesApp.motions.projector', ['OpenSlidesApp.motions'])
'User',
'Config',
'Projector',
function($scope, $rootScope, $http, Motion, User, Config, Projector) {
'$timeout',
function($scope, $rootScope, $http, Motion, User, Config, Projector, $timeout) {
// 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.
@ -78,8 +79,11 @@ angular.module('OpenSlidesApp.motions.projector', ['OpenSlidesApp.motions'])
}
};
Motion.bindOne(id, $scope, 'motion');
Motion.find(id).then(function (motion) {
$scope.motion = motion;
});
User.bindAll({}, $scope, 'users');
}
]);

View File

@ -917,14 +917,25 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
'PdfMakeDocumentProvider',
'gettextCatalog',
'HTMLValidizer',
'Projector',
'ProjectionDefault',
function($scope, $state, $http, ngDialog, MotionForm, Motion, Category, Tag, Workflow, User, Agenda, MotionDocxExport,
MotionContentProvider, MotionCatalogContentProvider, PdfMakeConverter,
PdfMakeDocumentProvider, gettextCatalog, HTMLValidizer) {
MotionContentProvider, MotionCatalogContentProvider, PdfMakeConverter, PdfMakeDocumentProvider,
gettextCatalog, HTMLValidizer, Projector, ProjectionDefault) {
Motion.bindAll({}, $scope, 'motions');
Category.bindAll({}, $scope, 'categories');
Tag.bindAll({}, $scope, 'tags');
Workflow.bindAll({}, $scope, 'workflows');
User.bindAll({}, $scope, 'users');
Projector.bindAll({}, $scope, 'projectors');
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var projectiondefault = ProjectionDefault.filter({name: 'motions'})[0];
if (projectiondefault) {
$scope.defaultProjectorId = projectiondefault.projector_id;
}
});
$scope.alert = {};
// setup table sorting
@ -1192,9 +1203,10 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
'gettextCatalog',
'Projector',
'HTMLValidizer',
'ProjectionDefault',
function($scope, $http, ngDialog, MotionForm, Motion, Category, Mediafile, Tag, User, Workflow, Config,
motion, MotionContentProvider, PollContentProvider,
PdfMakeConverter, PdfMakeDocumentProvider, MotionInlineEditing, gettextCatalog, Projector, HTMLValidizer) {
motion, MotionContentProvider, PollContentProvider, PdfMakeConverter, PdfMakeDocumentProvider,
MotionInlineEditing, gettextCatalog, Projector, HTMLValidizer, ProjectionDefault) {
Motion.bindOne(motion.id, $scope, 'motion');
Category.bindAll({}, $scope, 'categories');
Mediafile.bindAll({}, $scope, 'mediafiles');
@ -1202,6 +1214,12 @@ angular.module('OpenSlidesApp.motions.site', ['OpenSlidesApp.motions', 'OpenSlid
User.bindAll({}, $scope, 'users');
Workflow.bindAll({}, $scope, 'workflows');
Motion.loadRelations(motion, 'agenda_item');
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
console.log(ProjectionDefault.getAll());
$scope.defaultProjectorId = ProjectionDefault.filter({name: 'motions'})[0].projector_id;
});
$scope.version = motion.active_version;
$scope.isCollapsed = true;
$scope.commentsFields = Config.get('motions_comments').value;

View File

@ -16,12 +16,8 @@
<translate>List of speakers</translate>
</a>
<!-- project -->
<a os-perms="core.can_manage_projector" class="btn btn-default btn-sm"
ng-class="{ 'btn-primary': motion.isProjected() }"
ng-click="motion.project()"
title="{{ 'Project motion' | translate }}">
<i class="fa fa-video-camera"></i>
</a>
<projector-button model="motion" default-projector-id="defaultProjectorId">
</projector-button>
<!-- edit -->
<a ng-if="motion.isAllowed('update')" ng-click="openDialog(motion)"
class="btn btn-default btn-sm"

View File

@ -98,7 +98,6 @@
ng-click="checkAll()"></i>
</div>
<div class="col-xs-11 main-header">
<span class="form-inline text-right pull-right">
<span class="sort-spacer pointer" ng-click="reset_filters()"
ng-if="are_filters_set()" ng-disabled="isDeleteMode"
@ -304,12 +303,8 @@
</div>
<!-- projector column -->
<div class="col-xs-1 centered" os-perms="core.can_manage_projector">
<a class="btn btn-default btn-sm"
ng-class="{ 'btn-primary': motion.isProjected() }"
ng-click="motion.project()"
title="{{ 'Project motion' | translate }}">
<i class="fa fa-video-camera"></i>
</a>
<projector-button model="motion", default-projector-id="defaultProjectorId">
</projector-button>
</div>
<!-- main content column -->
<div class="col-xs-6 content">

View File

@ -3,6 +3,7 @@
from __future__ import unicode_literals
from django.db import migrations, models
import openslides.utils.models

View File

@ -156,8 +156,18 @@ angular.module('OpenSlidesApp.topics.site', ['OpenSlidesApp.topics'])
'TopicForm',
'Topic',
'topic',
function($scope, ngDialog, TopicForm, Topic, topic) {
'Projector',
'ProjectionDefault',
function($scope, ngDialog, TopicForm, Topic, topic, Projector, ProjectionDefault) {
Topic.bindOne(topic.id, $scope, 'topic');
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var projectiondefault = ProjectionDefault.filter({name: 'topics'})[0];
if (projectiondefault) {
$scope.defaultProjectorId = projectiondefault.projector_id;
}
});
Topic.loadRelations(topic, 'agenda_item');
$scope.openDialog = function (topic) {
ngDialog.open(TopicForm.getDialog(topic));

View File

@ -11,12 +11,8 @@
<translate>List of speakers</translate>
</a>
<!-- project -->
<a os-perms="core.can_manage_projector" class="btn btn-default btn-sm"
ng-class="{ 'btn-primary': topic.isProjected() }"
ng-click="topic.project()"
title="{{ 'Project topic' | translate }}">
<i class="fa fa-video-camera"></i>
</a>
<projector-button model="topic" default-projector-id="defaultProjectorId">
</projector-button>
<!-- edit -->
<a os-perms="agenda.can_manage" ng-click="openDialog(topic)"
class="btn btn-default btn-sm"

View File

@ -421,9 +421,19 @@ angular.module('OpenSlidesApp.users.site', ['OpenSlidesApp.users'])
'User',
'Group',
'PasswordGenerator',
function($scope, $state, $http, ngDialog, UserForm, User, Group, PasswordGenerator) {
'Projector',
'ProjectionDefault',
function($scope, $state, $http, ngDialog, UserForm, User, Group, PasswordGenerator, Projector, ProjectionDefault) {
User.bindAll({}, $scope, 'users');
Group.bindAll({where: {id: {'>': 1}}}, $scope, 'groups');
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
var projectiondefault = ProjectionDefault.filter({name: 'users'})[0];
if (projectiondefault) {
$scope.defaultProjectorId = projectiondefault.projector_id;
}
});
$scope.alert = {};
$scope.groupFilter = undefined;

View File

@ -184,12 +184,8 @@
ng-class="{ 'activeline': user.isProjected(), 'selected': user.selected }">
<!-- projector column -->
<td ng-show="!isSelectMode" os-perms="core.can_manage_projector">
<a class="btn btn-default btn-sm"
ng-class="{ 'btn-primary': user.isProjected() }"
ng-click="user.project()"
title="{{ 'Project user' | translate }}">
<i class="fa fa-video-camera"></i>
</a>
<projector-button model="user" default-projector-id="defaultProjectorId">
</projector-button>
<!-- selection column -->
<td ng-show="isSelectMode" os-perms="users.can_manage" class="deleteColumn">
<input type="checkbox" ng-model="user.selected">

View File

@ -138,11 +138,25 @@ def send_data(message):
projectors = Projector.get_projectors_that_show_this(collection_element)
send_all = None # The decission is done later
broadcast_id = config['projector_broadcast']
if broadcast_id > 0:
projectors = Projector.objects.all() # also the broadcasted projector should get data
broadcast_projector = Projector.objects.get(pk=broadcast_id)
send_all = True
# The data from the broadcasted projector
broadcast_projector_data = get_projector_element_data(broadcast_projector)
else:
broadcast_projector_data = None
for projector in projectors:
if send_all is None:
send_all = projector.need_full_update_for_this(collection_element)
if send_all:
output = get_projector_element_data(projector)
if broadcast_projector_data is None:
output = get_projector_element_data(projector)
else:
output = broadcast_projector_data
else:
output = []
output.append(collection_element.as_autoupdate_for_projector())

View File

@ -14,6 +14,7 @@ from rest_framework.routers import DefaultRouter
from rest_framework.serializers import ModelSerializer as _ModelSerializer
from rest_framework.serializers import ( # noqa
MANY_RELATION_KWARGS,
BooleanField,
CharField,
DictField,
Field,
@ -29,8 +30,6 @@ from rest_framework.serializers import ( # noqa
)
from rest_framework.viewsets import GenericViewSet as _GenericViewSet # noqa
from rest_framework.viewsets import ModelViewSet as _ModelViewSet # noqa
from rest_framework.viewsets import \
ReadOnlyModelViewSet as _ReadOnlyModelViewSet # noqa
from rest_framework.viewsets import ViewSet as _ViewSet # noqa
router = DefaultRouter()
@ -174,9 +173,5 @@ class ModelViewSet(PermissionMixin, _ModelViewSet):
pass
class ReadOnlyModelViewSet(PermissionMixin, _ReadOnlyModelViewSet):
pass
class ViewSet(PermissionMixin, _ViewSet):
pass

View File

@ -216,17 +216,6 @@ class Speak(TestCase):
def test_begin_speech_with_countdown(self):
config['agenda_couple_countdown_and_speakers'] = True
projector = Projector.objects.get(pk=1)
projector.config['03e87dea9c3f43c88b756c06a4c044fb'] = {
'name': 'core/countdown',
'status': 'stop',
'visible': True,
'default': 60,
'countdown_time': 60,
'stable': True,
'index': 0
}
projector.save()
Speaker.objects.add(self.user, self.item)
speaker = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item)
self.client.put(
@ -234,32 +223,22 @@ class Speak(TestCase):
{'speaker': speaker.pk})
for key, value in Projector.objects.get().config.items():
if value['name'] == 'core/countdown':
self.assertEqual(value['status'], 'running')
success = True
self.assertTrue(value['running'])
# If created, the countdown should have index 1
created = value['index'] == 1
break
else:
success = False
self.assertTrue(success)
created = False
self.assertTrue(created)
def test_end_speech_with_countdown(self):
config['agenda_couple_countdown_and_speakers'] = True
projector = Projector.objects.get(pk=1)
projector.config['03e87dea9c3f43c88b756c06a4c044fb'] = {
'name': 'core/countdown',
'status': 'stop',
'visible': True,
'default': 60,
'countdown_time': 60,
'stable': True,
'index': 0
}
projector.save()
speaker = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item)
speaker.begin_speech()
self.client.delete(reverse('item-speak', args=[self.item.pk]))
for key, value in Projector.objects.get().config.items():
if value['name'] == 'core/countdown':
self.assertEqual(value['status'], 'stop')
self.assertFalse(value['running'])
success = True
break
else:

View File

@ -25,9 +25,11 @@ class ProjectorAPI(TestCase):
default_projector.save()
response = self.client.get(reverse('projector-detail', args=['1']))
content = json.loads(response.content.decode())
del content['projectiondefaults']
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(json.loads(response.content.decode()), {
self.assertEqual(content, {
'id': 1,
'elements': {
'aae4a07b26534cfb9af4232f361dce73':
@ -36,6 +38,8 @@ class ProjectorAPI(TestCase):
'name': 'topics/topic'}},
'scale': 0,
'scroll': 0,
'name': 'Defaultprojector',
'blank': False,
'width': 1024,
'height': 768})
@ -47,9 +51,11 @@ class ProjectorAPI(TestCase):
default_projector.save()
response = self.client.get(reverse('projector-detail', args=['1']))
content = json.loads(response.content.decode())
del content['projectiondefaults']
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(json.loads(response.content.decode()), {
self.assertEqual(content, {
'id': 1,
'elements': {
'fc6ef43b624043068c8e6e7a86c5a1b0':
@ -58,6 +64,8 @@ class ProjectorAPI(TestCase):
'error': 'Projector element does not exist.'}},
'scale': 0,
'scroll': 0,
'name': 'Defaultprojector',
'blank': False,
'width': 1024,
'height': 768})