Added controls for projector elements

countdowns, messages, scale/scroll/edit current slide
Use global serverOffset.
Fixed #1645 (wrong time of projector clock).
Show countdown description on projector slide.
This commit is contained in:
Emanuel Schuetze 2015-09-05 23:32:10 +02:00
parent 0c9d4b2627
commit dda16af532
17 changed files with 655 additions and 85 deletions

View File

@ -25,6 +25,7 @@ Users:
- Used authentication frontend via AngularJS.
Other:
- New OpenSlides logo.
- Added multiple countdown support.
- Changed supported Python version to >= 3.3.
- Used Django 1.7 as lowest requirement.
- Added Django's application configuration. Refactored loading of signals

View File

@ -0,0 +1,36 @@
import uuid
from django.db import migrations
def clear_all_and_make_it_new_2(apps, schema_editor):
"""
Clear all projector elements and them write new.
"""
# We get the model from the versioned app registry;
# if we directly import it, it will be the wrong version.
Projector = apps.get_model('core', 'Projector')
projector = Projector.objects.get()
projector.config = {}
projector.config[uuid.uuid4().hex] = {
'name': 'core/clock',
'stable': True}
projector.config[uuid.uuid4().hex] = {
'name': 'core/customslide',
'id': 1} # TODO: Use ID from model here. Do not guess.
projector.save()
class Migration(migrations.Migration):
dependencies = [
('core', '0006_auto_20150914_2232'),
]
operations = [
migrations.RunPython(
code=clear_all_and_make_it_new_2,
reverse_code=None,
atomic=True,
),
]

View File

@ -82,6 +82,7 @@ class Projector(RESTModelMixin, models.Model):
for key, value in self.config.items():
# Use a copy here not to change the origin value in the config field.
result[key] = value.copy()
result[key]['uuid'] = key
element = elements.get(value['name'])
if element is None:
result[key]['error'] = _('Projector element does not exist.')

View File

@ -23,6 +23,32 @@ body {
border: 2px dashed #bed2db;
box-sizing: border-box;
}
.countdown.panel, .message.panel {
margin-bottom: 7px;
}
.countdown .panel-heading {
padding: 3px 15px;
}
.countdown .panel-body {
padding: 5px 15px;
}
.message .panel-body {
padding: 10px 15px;
}
.countdown_timer {
font-size: 2.2em;
font-weight: bold;
}
.countdown .editicon, .message .editicon {
padding-right: 10px;
}
.vcenter {
vertical-align: middle;
}
.notNull {
color: red;
font-weight: bold;
}
/* TODO: used by ng-fab-forms */
.validation-success {
@ -184,7 +210,7 @@ a:hover {
}
/* List tables */
th.sortable:hover, tr.pointer:hover {
th.sortable:hover, tr.pointer:hover, .pointer {
cursor: pointer;
}
@ -245,7 +271,7 @@ div.import > div > input[type="text"] {
tr.offline td, li.offline {
background-color: #EAEAEA !important;
}
tr.activeline td, li.activeline {
tr.activeline td, li.activeline, .projected {
background-color: #bed4de;
}
.nopadding {

View File

@ -130,7 +130,28 @@ hr {
/*** Overlay ***/
#overlay_transparent {
.countdown {
position: fixed;
margin: 0;
top: 0;
right: 0px;
padding: 19px 20px 5px 19px;
min-height: 72px;
font-size: 4em;
font-weight: bold;
text-align: center;
border-radius: 0.2em 0 0 0.2em;
z-index: 200;
}
.countdown .description {
font-weight: normal;
font-size: 0.2em;
margin-top: 20px;
}
.countdown.negative {
color: #CC0000;
}
.message_background {
background-color: #777777;
opacity: 0.6;
position: absolute;
@ -138,23 +159,9 @@ hr {
left: 0;
width: 100%;
height: 100%;
z-index: 200;
}
#overlay_countdown_inner {
position: fixed;
right: 40px;
height: 30px;
width: 215px;
margin: 0;
top: 0;
font-size: 4em;
font-weight: bold;
text-align: center;
border-radius: 0 0 0.2em 0.2em;
}
#overlay_countdown_inner.negative {
color: #CC0000;
}
#overlay_message_inner {
.message {
position: fixed;
top: 35%;
left: 10%;
@ -165,6 +172,7 @@ hr {
font-size: 2.75em;
padding: 0.2em 0;
line-height: normal !important;
z-index: 201;
}

View File

@ -77,9 +77,10 @@ angular.module('OpenSlidesApp.core', [
.factory('loadGlobalData', [
'$rootScope',
'$http',
'Config',
'Projector',
function ($rootScope, Config, Projector) {
function ($rootScope, $http, Config, Projector) {
return function () {
// Puts the config object into each scope.
Config.findAll().then(function() {
@ -96,6 +97,11 @@ angular.module('OpenSlidesApp.core', [
// Loads all projector data
Projector.findAll();
// Loads server time and calculates server offset
$http.get('/core/servertime/').then(function(data) {
$rootScope.serverOffset = Math.floor( Date.now() / 1000 - data.data );
});
}
}
])
@ -176,6 +182,34 @@ angular.module('OpenSlidesApp.core', [
});
}])
/* Converts number of seconds into string "hh:mm:ss" or "mm:ss" */
.filter('osSecondsToTime', [
function () {
return function (totalseconds) {
var time;
var total = Math.abs(totalseconds);
if (parseInt(totalseconds)) {
var hh = Math.floor(total / 3600);
var mm = Math.floor(total % 3600 / 60);
var ss = Math.floor(total % 60);
var zero = "0";
// Add leading "0" for double digit values
hh = (zero+hh).slice(-2);
mm = (zero+mm).slice(-2);
ss = (zero+ss).slice(-2);
if (hh == "00")
time = mm + ':' + ss;
else
time = hh + ":" + mm + ":" + ss;
if (totalseconds < 0)
time = "-"+time;
} else {
time = "--:--";
}
return time;
};
}
])
// Make sure that the DS factories are loaded by making them a dependency
.run(['Projector', 'Config', 'Tag', 'Customslide', function(Projector, Config, Tag, Customslide){}]);
@ -572,12 +606,16 @@ angular.module('OpenSlidesApp.core.site', [
])
// Version Controller
.controller('VersionCtrl', function ($scope, $http) {
$http.get('/core/version/').success(function(data) {
$scope.core_version = data.openslides_version;
$scope.plugins = data.plugins;
});
})
.controller('VersionCtrl', [
'$scope',
'$http',
function ($scope, $http) {
$http.get('/core/version/').success(function(data) {
$scope.core_version = data.openslides_version;
$scope.plugins = data.plugins;
});
}
])
// Config Controller
.controller('ConfigCtrl', function($scope, Config, configOption) {
@ -592,33 +630,231 @@ angular.module('OpenSlidesApp.core.site', [
})
// Customslide Controller
.controller('CustomslideListCtrl', function($scope, Customslide) {
Customslide.bindAll({}, $scope, 'customslides');
.controller('CustomslideListCtrl', [
'$scope',
'$http',
'Customslide',
function($scope, $http, Customslide) {
Customslide.bindAll({}, $scope, 'customslides');
// setup table sorting
$scope.sortColumn = 'title';
$scope.reverse = false;
// function to sort by clicked column
$scope.toggleSort = function ( column ) {
if ( $scope.sortColumn === column ) {
$scope.reverse = !$scope.reverse;
}
$scope.sortColumn = column;
};
// save changed customslide
$scope.save = function (customslide) {
Customslide.save(customslide);
};
$scope.delete = function (customslide) {
//TODO: add confirm message
Customslide.destroy(customslide.id).then(
function(success) {
//TODO: success message
// setup table sorting
$scope.sortColumn = 'title';
$scope.reverse = false;
// function to sort by clicked column
$scope.toggleSort = function ( column ) {
if ( $scope.sortColumn === column ) {
$scope.reverse = !$scope.reverse;
}
);
};
})
$scope.sortColumn = column;
};
// save changed customslide
$scope.save = function (customslide) {
Customslide.save(customslide);
};
$scope.delete = function (customslide) {
//TODO: add confirm message
Customslide.destroy(customslide.id).then(
function(success) {
//TODO: success message
}
);
};
}
])
// Projector Control Controller
.controller('ProjectorControlCtrl', [
'$scope',
'$http',
'$interval',
'$state',
'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);
}
}
// rebuild all variables after projector update
$scope.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);
}
}
});
// *** 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;
}
}
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/', [{
name: 'core/countdown',
status: 'stop',
visible: false,
index: $scope.countdowns.length,
countdown_time: defaultvalue,
default: defaultvalue,
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.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.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.resetCountdown = function (countdown) {
var data = {};
data[countdown.uuid] = {
"status": "stop",
"countdown_time": countdown.default,
};
$http.post('/rest/core/projector/1/update_elements/', data);
};
// *** message functions ***
$scope.addMessage = function () {
$http.post('/rest/core/projector/1/activate_elements/', [{
name: 'core/message',
visible: false,
index: $scope.messages.length,
message: '',
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 };
}
}
} 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});
};
$scope.editCurrentSlide = function () {
$.each(Projector.get(1).elements, function(key, value) {
if (value.name != 'core/clock' &&
value.name != 'core/countdown' &&
value.name != 'core/message' ) {
$state.go(value.name.replace('/', '.')+'.detail.update', {id: value.id });
}
});
};
}
])
.controller('CustomslideDetailCtrl', function($scope, Customslide, customslide) {
Customslide.bindOne(customslide.id, $scope, 'customslide');
@ -748,13 +984,14 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
slidesProvider.registerSlide('core/clock', {
template: 'static/templates/core/slide_clock.html',
});
})
.filter('osServertime',function() {
return function(serverOffset) {
var date = new Date();
return date.setTime(date.getTime() - serverOffset);
};
slidesProvider.registerSlide('core/countdown', {
template: 'static/templates/core/slide_countdown.html',
});
slidesProvider.registerSlide('core/message', {
template: 'static/templates/core/slide_message.html',
});
})
.controller('ProjectorCtrl', function($scope, Projector, slides) {
@ -770,22 +1007,70 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
console.error("Error for slide " + element.name + ": " + element.error)
}
});
$scope.scroll = -10 * Projector.get(1).scroll;
$scope.scale = 100 + 20 * Projector.get(1).scale;
});
});
})
.controller('SlideCustomSlideCtrl', function($scope, Customslide) {
// Attention! Each object that is used here has to be dealt on server side.
// Add it to the coresponding get_requirements method of the ProjectorElement
// class.
var id = $scope.element.id;
Customslide.find(id);
Customslide.bindOne(id, $scope, 'customslide');
})
.controller('SlideCustomSlideCtrl', [
'$scope',
'Customslide',
function($scope, Customslide) {
// Attention! Each object that is used here has to be dealt on server side.
// Add it to the coresponding get_requirements method of the ProjectorElement
// class.
var id = $scope.element.id;
Customslide.find(id);
Customslide.bindOne(id, $scope, 'customslide');
}
])
.controller('SlideClockCtrl', function($scope) {
// 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.
$scope.serverOffset = Date.parse(new Date().toUTCString()) - $scope.element.context.server_time;
});
.controller('SlideClockCtrl', [
'$scope',
function($scope) {
// 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.
$scope.servertime = ( Date.now() / 1000 - $scope.serverOffset ) * 1000;
}
])
.controller('SlideCountdownCtrl', [
'$scope',
'$interval',
function($scope, $interval) {
// 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.
$scope.seconds = Math.floor( $scope.element.countdown_time - Date.now() / 1000 + $scope.serverOffset );
$scope.status = $scope.element.status;
$scope.visible = $scope.element.visible;
$scope.index = $scope.element.index;
$scope.description = $scope.element.description;
// start interval timer if countdown status is running
var interval;
if ($scope.status == "running") {
interval = $interval( function() {
$scope.seconds = Math.floor( $scope.element.countdown_time - Date.now() / 1000 + $scope.serverOffset );
}, 1000);
} else {
$scope.seconds = $scope.element.countdown_time;
}
$scope.$on('$destroy', function() {
// Cancel the interval if the controller is destroyed
$interval.cancel(interval);
});
}
])
.controller('SlideMessageCtrl', [
'$scope',
function($scope) {
// 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.
$scope.message = $scope.element.message;
$scope.visible = $scope.element.visible;
}
]);

View File

@ -48,15 +48,8 @@
</table>
</div>
<div class="col-sm-4">
<!-- projector live view -->
<div class="well">
<h4 translate>Projector live view</h4>
<a ui-sref="projector" target="_blank">
<div id="iframewrapper">
<iframe id="iframe" src="/projector" frameborder="0"></iframe>
<div id="iframeoverlay"></div>
</div>
</a>
<div ng-include src="'static/templates/core/projector-controls.html'"></div>
</div>
</div>
</div>

View File

@ -0,0 +1,177 @@
<!-- projector live view -->
<h3 translate>Projector</h4>
<a ui-sref="projector" target="_blank">
<div id="iframewrapper">
<iframe id="iframe" src="/projector" frameborder="0"></iframe>
<div id="iframeoverlay"></div>
</div>
</a>
<div ng-controller="ProjectorControlCtrl">
<!-- projector control buttons -->
<p os-perms="core.can_manage_projector">
<a ng-click="editCurrentSlide()"
class="btn btn-default btn-sm"
title="{{ 'Edit current slide' | translate}}">
<i class="fa fa-pencil"></i>
</a>
&nbsp;
<a ng-click="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')"
class="btn btn-default btn-sm"
title="{{ 'Bigger' | translate}}">
<i class="fa fa-search-plus"></i>
</a>
<span ng-class="{ 'notNull': scaleLevel != 0 }">{{ scaleLevel }}</span>
&nbsp;
<a ng-click="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')"
class="btn btn-default btn-sm"
title="{{ 'Scroll down' | translate}}">
<i class="fa fa-arrow-down"></i>
</a>
<a ng-click="controlProjector('scroll', 'reset')"
class="btn btn-default btn-sm"
title="{{ 'Reset scrolling' | translate}}">
<i class="fa fa-undo"></i>
</a>
<span ng-class="{ 'notNull': scrollLevel != 0 }">{{ scrollLevel }}</span>
</p>
<!-- countdowns -->
<div os-perms-lite="core.can_manage_projector">
<div ng-repeat="countdown in countdowns | orderBy: 'index'" id="{{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>
<!-- remove countdown button -->
<button type="button" class="close"
ng-click="removeCountdown(countdown)"
title="{{ 'Remove countdown' | translate}}">
<i class="fa fa-times"></i>
</button>
<!-- edit countdown button -->
<button ng-show="countdown.status=='stop'"
type="button" class="close editicon"
ng-click="editCountdownFlag=true;"
title="{{ 'Edit countdown' | translate}}">
<i class="fa fa-pencil"></i>
</button>
</div>
<div class="panel-body"
ng-class="{ 'projected': countdown.visible }">
<!-- 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>
&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 }"
title="{{ 'Reset countdown' | translate}}">
<i class="fa fa-stop"></i>
</a>
<a ng-if="countdown.status=='stop'" 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>
</a>
<a ng-if="countdown.status=='running'" class="btn btn-default vcenter"
ng-click="stopCountdown(countdown)"
title="{{ 'Pause' | translate}}">
<i class="fa fa-pause"></i>
</a>
<span ng-if="!editTime" class="countdown_timer vcenter"
ng-class="{ 'negative': countdown.seconds < 0 }">
{{ countdown.seconds | osSecondsToTime }}
</span>
<!-- edit countdown form -->
<form ng-show="editCountdownFlag" 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 ng-model="countdown.default" type="number" class="form-control input-sm">
</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;"
title="{{ 'Cancel' | translate}}"
class="btn btn-default btn-sm">
<i class="fa fa-times"></i>
</button>
</form>
</div>
</div>
<!-- Add countdown button -->
<a ng-click="addCountdown()"
class="btn btn-default btn-sm"
title="{{ 'Add countdown' | translate}}">
<i class="fa fa-plus"></i> <translate>Add new countdown</translate>
</a>
</div>
<!-- messages -->
<div os-perms-lite="core.can_manage_projector">
<h3 translate>Messages</h3>
<div ng-repeat="message in messages | orderBy: 'index'" id="{{message.uuid}}" class="message panel panel-default">
<div class="panel-body"
ng-class="{ 'projected': message.visible }">
<!-- 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 }}">
<i class="fa fa-video-camera"></i>
</a>
&nbsp;&nbsp;
{{ message.message }}
<!-- remove message button -->
<button type="button" class="close"
ng-click="removeMessage(message)"
title="{{ 'Remove message' | translate}}">
<i class="fa fa-times"></i>
</button>
<button type="button" class="close editicon"
ng-click="editMessageFlag=true;"
title="{{ 'Edit message' | translate}}">
<i class="fa fa-pencil"></i>
</button>
<div ng-if="editMessageFlag" class="input-group">
<input ng-model="message.message" type="text" class="form-control input-sm">
<a ng-click="editMessage(message)"
title="{{ 'Save' | translate}}"
class="btn btn-sm btn-primary input-group-addon">
<i class="fa fa-check"></i>
</a>
</div>
</div>
</div>
<!-- Add message button -->
<a ng-click="addMessage()"
class="btn btn-default btn-sm"
title="{{ 'Add message' | translate}}">
<i class="fa fa-plus"></i> <translate>Add new message</translate>
</a>
</div>
</div>

View File

@ -1,4 +1,4 @@
<div ng-controller="SlideClockCtrl" id="currentTime">
<i class="fa fa-clock-o"></i>
{{ serverOffset | osServertime | date:'HH:mm' }}
{{ servertime | date:'HH:mm' }}
</div>

View File

@ -0,0 +1,8 @@
<div ng-controller="SlideCountdownCtrl">
<div ng-if="visible">
<div class="countdown well" style="margin-top: calc({{index}}*100px);">
{{ seconds | osSecondsToTime}}
<div class="description">{{ description }}</div>
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
<div ng-controller="SlideCustomSlideCtrl" class="content">
<div ng-controller="SlideCustomSlideCtrl" class="content scrollcontent">
<h1>{{ customslide.title }}</h1>
<div class="white-space-pre-line">{{ customslide.text }}</div>
</div>

View File

@ -0,0 +1,4 @@
<div ng-controller="SlideMessageCtrl">
<div ng-if="visible" class="message_background"></div>
<div ng-if="visible" class="message well">{{ message }}</div>
</div>

View File

@ -54,6 +54,7 @@
<!-- Login dialog (modal) -->
<div ng-controller="LoginFormCtrl" ng-if="!operator.isAuthenticated()">
<script type="text/ng-template" id="LoginForm.html">
<form ng-submit="login(username, password)">
<div class="modal-header">
<h3 class="modal-title" translate>Please sign in!</h3>
</div>
@ -62,7 +63,7 @@
<strong translate>Username or password is not correct.</strong>
<div class="input-group form-group">
<div class="input-group-addon"><i class="fa fa-user"></i></div>
<input type="text" ng-model="username" class="form-control input-lg"
<input os-focus-me type="text" ng-model="username" class="form-control input-lg"
placeholder="{{ 'Username' | translate }}">
</div>
<div class="input-group form-group">
@ -73,8 +74,7 @@
</div>
<div class="modal-footer">
<div class="form-group">
<button type="submit" ng-click="login(username, password)"
class="btn btn-primary btn-lg btn-block" translate>
<button type="submit" class="btn btn-primary btn-lg btn-block" translate>
Login
</button>
</div>
@ -88,6 +88,7 @@
</button>
</div>
</div>
</form>
</script>
<button class="btn btn-default" ng-click="open()">
<i class="fa fa-sign-in"></i>

View File

@ -43,6 +43,12 @@
</div>
<div ng-controller="ProjectorCtrl">
<style type="text/css">
.scrollcontent {
margin-top: {{scroll}}em;
font-size: {{scale}}%;
}
</style>
<div ng-repeat="element in elements">
<div ng-include="element.template"></div>
</div>

View File

@ -247,7 +247,7 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
for key, value in request.data.items():
if key not in projector_config:
raise ValidationError({'data': 'Invalid projector element. Wrong UUID.'})
projector_config.update(request.data)
projector_config[key].update(request.data[key])
serializer = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False)
serializer.is_valid(raise_exception=True)

View File

@ -183,6 +183,17 @@ 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(
@ -199,6 +210,17 @@ class Speak(TestCase):
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]))

View File

@ -33,6 +33,7 @@ class ProjectorAPI(TestCase):
'elements': {
'aae4a07b26534cfb9af4232f361dce73':
{'id': customslide.id,
'uuid': 'aae4a07b26534cfb9af4232f361dce73',
'name': 'core/customslide',
'context': None}},
'scale': 0,
@ -53,6 +54,7 @@ class ProjectorAPI(TestCase):
'elements': {
'fc6ef43b624043068c8e6e7a86c5a1b0':
{'name': 'invalid_slide',
'uuid': 'fc6ef43b624043068c8e6e7a86c5a1b0',
'error': 'Projector element does not exist.'}},
'scale': 0,
'scroll': 0})