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:
parent
0c9d4b2627
commit
dda16af532
@ -25,6 +25,7 @@ Users:
|
|||||||
- Used authentication frontend via AngularJS.
|
- Used authentication frontend via AngularJS.
|
||||||
Other:
|
Other:
|
||||||
- New OpenSlides logo.
|
- New OpenSlides logo.
|
||||||
|
- Added multiple countdown support.
|
||||||
- Changed supported Python version to >= 3.3.
|
- Changed supported Python version to >= 3.3.
|
||||||
- Used Django 1.7 as lowest requirement.
|
- Used Django 1.7 as lowest requirement.
|
||||||
- Added Django's application configuration. Refactored loading of signals
|
- Added Django's application configuration. Refactored loading of signals
|
||||||
|
36
openslides/core/migrations/0007_clear_default_countdown.py
Normal file
36
openslides/core/migrations/0007_clear_default_countdown.py
Normal 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,
|
||||||
|
),
|
||||||
|
]
|
@ -82,6 +82,7 @@ class Projector(RESTModelMixin, models.Model):
|
|||||||
for key, value in self.config.items():
|
for key, value in self.config.items():
|
||||||
# Use a copy here not to change the origin value in the config field.
|
# Use a copy here not to change the origin value in the config field.
|
||||||
result[key] = value.copy()
|
result[key] = value.copy()
|
||||||
|
result[key]['uuid'] = key
|
||||||
element = elements.get(value['name'])
|
element = elements.get(value['name'])
|
||||||
if element is None:
|
if element is None:
|
||||||
result[key]['error'] = _('Projector element does not exist.')
|
result[key]['error'] = _('Projector element does not exist.')
|
||||||
|
@ -23,6 +23,32 @@ body {
|
|||||||
border: 2px dashed #bed2db;
|
border: 2px dashed #bed2db;
|
||||||
box-sizing: border-box;
|
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 */
|
/* TODO: used by ng-fab-forms */
|
||||||
.validation-success {
|
.validation-success {
|
||||||
@ -184,7 +210,7 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* List tables */
|
/* List tables */
|
||||||
th.sortable:hover, tr.pointer:hover {
|
th.sortable:hover, tr.pointer:hover, .pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,7 +271,7 @@ div.import > div > input[type="text"] {
|
|||||||
tr.offline td, li.offline {
|
tr.offline td, li.offline {
|
||||||
background-color: #EAEAEA !important;
|
background-color: #EAEAEA !important;
|
||||||
}
|
}
|
||||||
tr.activeline td, li.activeline {
|
tr.activeline td, li.activeline, .projected {
|
||||||
background-color: #bed4de;
|
background-color: #bed4de;
|
||||||
}
|
}
|
||||||
.nopadding {
|
.nopadding {
|
||||||
|
@ -130,7 +130,28 @@ hr {
|
|||||||
|
|
||||||
|
|
||||||
/*** Overlay ***/
|
/*** 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;
|
background-color: #777777;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -138,23 +159,9 @@ hr {
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
z-index: 200;
|
||||||
}
|
}
|
||||||
#overlay_countdown_inner {
|
.message {
|
||||||
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 {
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 35%;
|
top: 35%;
|
||||||
left: 10%;
|
left: 10%;
|
||||||
@ -165,6 +172,7 @@ hr {
|
|||||||
font-size: 2.75em;
|
font-size: 2.75em;
|
||||||
padding: 0.2em 0;
|
padding: 0.2em 0;
|
||||||
line-height: normal !important;
|
line-height: normal !important;
|
||||||
|
z-index: 201;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,9 +77,10 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
|
|
||||||
.factory('loadGlobalData', [
|
.factory('loadGlobalData', [
|
||||||
'$rootScope',
|
'$rootScope',
|
||||||
|
'$http',
|
||||||
'Config',
|
'Config',
|
||||||
'Projector',
|
'Projector',
|
||||||
function ($rootScope, Config, Projector) {
|
function ($rootScope, $http, Config, Projector) {
|
||||||
return function () {
|
return function () {
|
||||||
// Puts the config object into each scope.
|
// Puts the config object into each scope.
|
||||||
Config.findAll().then(function() {
|
Config.findAll().then(function() {
|
||||||
@ -96,6 +97,11 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
|
|
||||||
// Loads all projector data
|
// Loads all projector data
|
||||||
Projector.findAll();
|
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
|
// Make sure that the DS factories are loaded by making them a dependency
|
||||||
.run(['Projector', 'Config', 'Tag', 'Customslide', function(Projector, Config, Tag, Customslide){}]);
|
.run(['Projector', 'Config', 'Tag', 'Customslide', function(Projector, Config, Tag, Customslide){}]);
|
||||||
|
|
||||||
@ -572,12 +606,16 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
])
|
])
|
||||||
|
|
||||||
// Version Controller
|
// Version Controller
|
||||||
.controller('VersionCtrl', function ($scope, $http) {
|
.controller('VersionCtrl', [
|
||||||
$http.get('/core/version/').success(function(data) {
|
'$scope',
|
||||||
$scope.core_version = data.openslides_version;
|
'$http',
|
||||||
$scope.plugins = data.plugins;
|
function ($scope, $http) {
|
||||||
});
|
$http.get('/core/version/').success(function(data) {
|
||||||
})
|
$scope.core_version = data.openslides_version;
|
||||||
|
$scope.plugins = data.plugins;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
// Config Controller
|
// Config Controller
|
||||||
.controller('ConfigCtrl', function($scope, Config, configOption) {
|
.controller('ConfigCtrl', function($scope, Config, configOption) {
|
||||||
@ -592,33 +630,231 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Customslide Controller
|
// Customslide Controller
|
||||||
.controller('CustomslideListCtrl', function($scope, Customslide) {
|
.controller('CustomslideListCtrl', [
|
||||||
Customslide.bindAll({}, $scope, 'customslides');
|
'$scope',
|
||||||
|
'$http',
|
||||||
|
'Customslide',
|
||||||
|
function($scope, $http, Customslide) {
|
||||||
|
Customslide.bindAll({}, $scope, 'customslides');
|
||||||
|
|
||||||
// setup table sorting
|
// setup table sorting
|
||||||
$scope.sortColumn = 'title';
|
$scope.sortColumn = 'title';
|
||||||
$scope.reverse = false;
|
$scope.reverse = false;
|
||||||
// function to sort by clicked column
|
// function to sort by clicked column
|
||||||
$scope.toggleSort = function ( column ) {
|
$scope.toggleSort = function ( column ) {
|
||||||
if ( $scope.sortColumn === column ) {
|
if ( $scope.sortColumn === column ) {
|
||||||
$scope.reverse = !$scope.reverse;
|
$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
|
|
||||||
}
|
}
|
||||||
);
|
$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) {
|
.controller('CustomslideDetailCtrl', function($scope, Customslide, customslide) {
|
||||||
Customslide.bindOne(customslide.id, $scope, 'customslide');
|
Customslide.bindOne(customslide.id, $scope, 'customslide');
|
||||||
@ -748,13 +984,14 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
|
|||||||
slidesProvider.registerSlide('core/clock', {
|
slidesProvider.registerSlide('core/clock', {
|
||||||
template: 'static/templates/core/slide_clock.html',
|
template: 'static/templates/core/slide_clock.html',
|
||||||
});
|
});
|
||||||
})
|
|
||||||
|
|
||||||
.filter('osServertime',function() {
|
slidesProvider.registerSlide('core/countdown', {
|
||||||
return function(serverOffset) {
|
template: 'static/templates/core/slide_countdown.html',
|
||||||
var date = new Date();
|
});
|
||||||
return date.setTime(date.getTime() - serverOffset);
|
|
||||||
};
|
slidesProvider.registerSlide('core/message', {
|
||||||
|
template: 'static/templates/core/slide_message.html',
|
||||||
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
.controller('ProjectorCtrl', function($scope, Projector, slides) {
|
.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)
|
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) {
|
.controller('SlideCustomSlideCtrl', [
|
||||||
// Attention! Each object that is used here has to be dealt on server side.
|
'$scope',
|
||||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
'Customslide',
|
||||||
// class.
|
function($scope, Customslide) {
|
||||||
var id = $scope.element.id;
|
// Attention! Each object that is used here has to be dealt on server side.
|
||||||
Customslide.find(id);
|
// Add it to the coresponding get_requirements method of the ProjectorElement
|
||||||
Customslide.bindOne(id, $scope, 'customslide');
|
// class.
|
||||||
})
|
var id = $scope.element.id;
|
||||||
|
Customslide.find(id);
|
||||||
|
Customslide.bindOne(id, $scope, 'customslide');
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
.controller('SlideClockCtrl', function($scope) {
|
.controller('SlideClockCtrl', [
|
||||||
// Attention! Each object that is used here has to be dealt on server side.
|
'$scope',
|
||||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
function($scope) {
|
||||||
// class.
|
// Attention! Each object that is used here has to be dealt on server side.
|
||||||
$scope.serverOffset = Date.parse(new Date().toUTCString()) - $scope.element.context.server_time;
|
// 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;
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
@ -48,15 +48,8 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<!-- projector live view -->
|
|
||||||
<div class="well">
|
<div class="well">
|
||||||
<h4 translate>Projector live view</h4>
|
<div ng-include src="'static/templates/core/projector-controls.html'"></div>
|
||||||
<a ui-sref="projector" target="_blank">
|
|
||||||
<div id="iframewrapper">
|
|
||||||
<iframe id="iframe" src="/projector" frameborder="0"></iframe>
|
|
||||||
<div id="iframeoverlay"></div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
177
openslides/core/static/templates/core/projector-controls.html
Normal file
177
openslides/core/static/templates/core/projector-controls.html
Normal 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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
{{ 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>
|
@ -1,4 +1,4 @@
|
|||||||
<div ng-controller="SlideClockCtrl" id="currentTime">
|
<div ng-controller="SlideClockCtrl" id="currentTime">
|
||||||
<i class="fa fa-clock-o"></i>
|
<i class="fa fa-clock-o"></i>
|
||||||
{{ serverOffset | osServertime | date:'HH:mm' }}
|
{{ servertime | date:'HH:mm' }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -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>
|
@ -1,4 +1,4 @@
|
|||||||
<div ng-controller="SlideCustomSlideCtrl" class="content">
|
<div ng-controller="SlideCustomSlideCtrl" class="content scrollcontent">
|
||||||
<h1>{{ customslide.title }}</h1>
|
<h1>{{ customslide.title }}</h1>
|
||||||
<div class="white-space-pre-line">{{ customslide.text }}</div>
|
<div class="white-space-pre-line">{{ customslide.text }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
4
openslides/core/static/templates/core/slide_message.html
Normal file
4
openslides/core/static/templates/core/slide_message.html
Normal 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>
|
@ -54,6 +54,7 @@
|
|||||||
<!-- Login dialog (modal) -->
|
<!-- Login dialog (modal) -->
|
||||||
<div ng-controller="LoginFormCtrl" ng-if="!operator.isAuthenticated()">
|
<div ng-controller="LoginFormCtrl" ng-if="!operator.isAuthenticated()">
|
||||||
<script type="text/ng-template" id="LoginForm.html">
|
<script type="text/ng-template" id="LoginForm.html">
|
||||||
|
<form ng-submit="login(username, password)">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 class="modal-title" translate>Please sign in!</h3>
|
<h3 class="modal-title" translate>Please sign in!</h3>
|
||||||
</div>
|
</div>
|
||||||
@ -62,7 +63,7 @@
|
|||||||
<strong translate>Username or password is not correct.</strong>
|
<strong translate>Username or password is not correct.</strong>
|
||||||
<div class="input-group form-group">
|
<div class="input-group form-group">
|
||||||
<div class="input-group-addon"><i class="fa fa-user"></i></div>
|
<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 }}">
|
placeholder="{{ 'Username' | translate }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group form-group">
|
<div class="input-group form-group">
|
||||||
@ -73,8 +74,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<button type="submit" ng-click="login(username, password)"
|
<button type="submit" class="btn btn-primary btn-lg btn-block" translate>
|
||||||
class="btn btn-primary btn-lg btn-block" translate>
|
|
||||||
Login
|
Login
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -88,6 +88,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
</script>
|
</script>
|
||||||
<button class="btn btn-default" ng-click="open()">
|
<button class="btn btn-default" ng-click="open()">
|
||||||
<i class="fa fa-sign-in"></i>
|
<i class="fa fa-sign-in"></i>
|
||||||
|
@ -43,6 +43,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-controller="ProjectorCtrl">
|
<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-repeat="element in elements">
|
||||||
<div ng-include="element.template"></div>
|
<div ng-include="element.template"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -247,7 +247,7 @@ class ProjectorViewSet(ReadOnlyModelViewSet):
|
|||||||
for key, value in request.data.items():
|
for key, value in request.data.items():
|
||||||
if key not in projector_config:
|
if key not in projector_config:
|
||||||
raise ValidationError({'data': 'Invalid projector element. Wrong UUID.'})
|
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 = self.get_serializer(projector_instance, data={'config': projector_config}, partial=False)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
@ -183,6 +183,17 @@ class Speak(TestCase):
|
|||||||
|
|
||||||
def test_begin_speech_with_countdown(self):
|
def test_begin_speech_with_countdown(self):
|
||||||
config['agenda_couple_countdown_and_speakers'] = True
|
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.objects.add(self.user, self.item)
|
||||||
speaker = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item)
|
speaker = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item)
|
||||||
self.client.put(
|
self.client.put(
|
||||||
@ -199,6 +210,17 @@ class Speak(TestCase):
|
|||||||
|
|
||||||
def test_end_speech_with_countdown(self):
|
def test_end_speech_with_countdown(self):
|
||||||
config['agenda_couple_countdown_and_speakers'] = True
|
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 = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item)
|
||||||
speaker.begin_speech()
|
speaker.begin_speech()
|
||||||
self.client.delete(reverse('item-speak', args=[self.item.pk]))
|
self.client.delete(reverse('item-speak', args=[self.item.pk]))
|
||||||
|
@ -33,6 +33,7 @@ class ProjectorAPI(TestCase):
|
|||||||
'elements': {
|
'elements': {
|
||||||
'aae4a07b26534cfb9af4232f361dce73':
|
'aae4a07b26534cfb9af4232f361dce73':
|
||||||
{'id': customslide.id,
|
{'id': customslide.id,
|
||||||
|
'uuid': 'aae4a07b26534cfb9af4232f361dce73',
|
||||||
'name': 'core/customslide',
|
'name': 'core/customslide',
|
||||||
'context': None}},
|
'context': None}},
|
||||||
'scale': 0,
|
'scale': 0,
|
||||||
@ -53,6 +54,7 @@ class ProjectorAPI(TestCase):
|
|||||||
'elements': {
|
'elements': {
|
||||||
'fc6ef43b624043068c8e6e7a86c5a1b0':
|
'fc6ef43b624043068c8e6e7a86c5a1b0':
|
||||||
{'name': 'invalid_slide',
|
{'name': 'invalid_slide',
|
||||||
|
'uuid': 'fc6ef43b624043068c8e6e7a86c5a1b0',
|
||||||
'error': 'Projector element does not exist.'}},
|
'error': 'Projector element does not exist.'}},
|
||||||
'scale': 0,
|
'scale': 0,
|
||||||
'scroll': 0})
|
'scroll': 0})
|
||||||
|
Loading…
Reference in New Issue
Block a user