Merge pull request #2529 from FinnStutzenstein/Issue2464

countdown and message models (closes #2464)
This commit is contained in:
Emanuel Schütze 2016-11-20 22:39:12 +01:00 committed by GitHub
commit 701387984b
20 changed files with 576 additions and 562 deletions

View File

@ -10,7 +10,7 @@ from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from openslides.core.config import config
from openslides.core.projector import Countdown
from openslides.core.models import Countdown
from openslides.utils.exceptions import OpenSlidesError
from openslides.utils.models import RESTModelMixin
from openslides.utils.utils import to_roman
@ -412,8 +412,12 @@ class Speaker(RESTModelMixin, models.Model):
self.begin_time = timezone.now()
self.save()
if config['agenda_couple_countdown_and_speakers']:
Countdown.control(action='reset')
Countdown.control(action='start')
countdown, created = Countdown.objects.get_or_create(pk=1, defaults={
'default_time': config['projector_default_countdown'],
'countdown_time': config['projector_default_countdown']})
if not created:
countdown.control(action='reset')
countdown.control(action='start')
def end_speech(self):
"""
@ -422,7 +426,12 @@ class Speaker(RESTModelMixin, models.Model):
self.end_time = timezone.now()
self.save()
if config['agenda_couple_countdown_and_speakers']:
Countdown.control(action='stop')
try:
countdown = Countdown.objects.get(pk=1)
except Countdown.DoesNotExist:
pass # Do not create a new countdown on stop action
else:
countdown.control(action='stop')
def get_root_rest_element(self):
"""

View File

@ -64,6 +64,44 @@ class ChatMessageAccessPermissions(BaseAccessPermissions):
return ChatMessageSerializer
class ProjectorMessageAccessPermissions(BaseAccessPermissions):
"""
Access permissions for ProjectorMessage.
"""
def check_permissions(self, user):
"""
Returns True if the user has read access model instances.
"""
return user.has_perm('core.can_see_projector')
def get_serializer_class(self, user=None):
"""
Returns serializer class.
"""
from .serializers import ProjectorMessageSerializer
return ProjectorMessageSerializer
class CountdownAccessPermissions(BaseAccessPermissions):
"""
Access permissions for Countdown.
"""
def check_permissions(self, user):
"""
Returns True if the user has read access model instances.
"""
return user.has_perm('core.can_see_projector')
def get_serializer_class(self, user=None):
"""
Returns serializer class.
"""
from .serializers import CountdownSerializer
return CountdownSerializer
class ConfigAccessPermissions(BaseAccessPermissions):
"""
Access permissions container for the config (ConfigStore and

View File

@ -19,10 +19,12 @@ 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, create_builtin_projection_defaults
from .signals import delete_django_app_permissions
from .views import (
ChatMessageViewSet,
ConfigViewSet,
CountdownViewSet,
ProjectorMessageViewSet,
ProjectorViewSet,
TagViewSet,
)
@ -34,15 +36,14 @@ 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)
router.register(self.get_model('ChatMessage').get_collection_string(), ChatMessageViewSet)
router.register(self.get_model('Tag').get_collection_string(), TagViewSet)
router.register(self.get_model('ConfigStore').get_collection_string(), ConfigViewSet, 'config')
router.register(self.get_model('ProjectorMessage').get_collection_string(), ProjectorMessageViewSet)
router.register(self.get_model('Countdown').get_collection_string(), CountdownViewSet)
# Update the search when a model is saved or deleted
signals.post_save.connect(

View File

@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-10-21 09:07
from __future__ import unicode_literals
from django.db import migrations, models
import openslides.utils.models
def add_projection_defaults(apps, schema_editor):
"""
Adds projectiondefaults for messages and countdowns.
"""
Projector = apps.get_model('core', 'Projector')
ProjectionDefault = apps.get_model('core', 'ProjectionDefault')
# the default projector (pk=1) is always available.
default_projector = Projector.objects.get(pk=1)
projectiondefaults = []
# It is possible that already some projectiondefaults exist if this
# is a database created with an older version of OS.
if not ProjectionDefault.objects.all().exists():
projectiondefaults.append(ProjectionDefault(
name='agenda_all_items',
display_name='Agenda',
projector=default_projector))
projectiondefaults.append(ProjectionDefault(
name='topics',
display_name='Topics',
projector=default_projector))
projectiondefaults.append(ProjectionDefault(
name='agenda_list_of_speakers',
display_name='List of speakers',
projector=default_projector))
projectiondefaults.append(ProjectionDefault(
name='agenda_current_list_of_speakers',
display_name='Current list of speakers',
projector=default_projector))
projectiondefaults.append(ProjectionDefault(
name='motions',
display_name='Motions',
projector=default_projector))
projectiondefaults.append(ProjectionDefault(
name='motionBlocks',
display_name='Motion Blocks',
projector=default_projector))
projectiondefaults.append(ProjectionDefault(
name='assignments',
display_name='Elections',
projector=default_projector))
projectiondefaults.append(ProjectionDefault(
name='users',
display_name='Participants',
projector=default_projector))
projectiondefaults.append(ProjectionDefault(
name='mediafiles',
display_name='Files',
projector=default_projector))
# Now, these are new projectiondefaults
projectiondefaults.append(ProjectionDefault(
name='messages',
display_name='Messages',
projector=default_projector))
projectiondefaults.append(ProjectionDefault(
name='countdowns',
display_name='Countdowns',
projector=default_projector))
# Create all new projectiondefaults
ProjectionDefault.objects.bulk_create(projectiondefaults)
class Migration(migrations.Migration):
dependencies = [
('core', '0007_manage_chat_permission'),
]
operations = [
migrations.CreateModel(
name='Countdown',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('description', models.CharField(max_length=256, blank=True)),
('running', models.BooleanField(default=False)),
('default_time', models.PositiveIntegerField(default=60)),
('countdown_time', models.FloatField(default=60)),
],
options={
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model),
),
migrations.CreateModel(
name='ProjectorMessage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('message', models.TextField(blank=True)),
],
options={
'default_permissions': (),
},
bases=(openslides.utils.models.RESTModelMixin, models.Model),
),
migrations.RunPython(add_projection_defaults),
]

View File

@ -1,6 +1,7 @@
from django.conf import settings
from django.contrib.sessions.models import Session as DjangoSession
from django.db import models
from django.utils.timezone import now
from jsonfield import JSONField
from ..utils.collection import CollectionElement
@ -9,7 +10,9 @@ from ..utils.projector import ProjectorElement
from .access_permissions import (
ChatMessageAccessPermissions,
ConfigAccessPermissions,
CountdownAccessPermissions,
ProjectorAccessPermissions,
ProjectorMessageAccessPermissions,
TagAccessPermissions,
)
from .exceptions import ProjectorException
@ -294,6 +297,51 @@ class ChatMessage(RESTModelMixin, models.Model):
return 'Message {}'.format(self.timestamp)
class ProjectorMessage(RESTModelMixin, models.Model):
"""
Model for ProjectorMessages.
"""
access_permissions = ProjectorMessageAccessPermissions()
message = models.TextField(blank=True)
class Meta:
default_permissions = ()
class Countdown(RESTModelMixin, models.Model):
"""
Model for countdowns.
"""
access_permissions = CountdownAccessPermissions()
description = models.CharField(max_length=256, blank=True)
running = models.BooleanField(default=False)
default_time = models.PositiveIntegerField(default=60)
countdown_time = models.FloatField(default=60)
class Meta:
default_permissions = ()
def control(self, action):
if action not in ('start', 'stop', 'reset'):
raise ValueError("Action must be 'start', 'stop' or 'reset', not {}.".format(action))
if action == 'start':
self.running = True
self.countdown_time = now().timestamp() + self.default_time
elif action == 'stop' and self.running:
self.running = False
self.countdown_time = self.countdown_time - now().timestamp()
else: # reset
self.running = False
self.countdown_time = self.default_time
self.save()
class Session(DjangoSession):
"""
Model like the Django db session, which saves the user as ForeignKey instead

View File

@ -1,11 +1,6 @@
import uuid
from django.utils.timezone import now
from ..utils.projector import ProjectorElement
from .config import config
from .exceptions import ProjectorException
from .models import Projector
from .models import Countdown, ProjectorMessage
class Clock(ProjectorElement):
@ -15,134 +10,41 @@ class Clock(ProjectorElement):
name = 'core/clock'
class Countdown(ProjectorElement):
class CountdownElement(ProjectorElement):
"""
Countdown on the projector.
To start the countdown write into the config field:
{
"running": True,
"countdown_time": <timestamp>,
}
The timestamp is a POSIX timestamp (seconds) calculated from client
time, server time offset and countdown duration (countdown_time = now -
serverTimeOffset + duration).
To stop the countdown set the countdown time to the current value of the
countdown (countdown_time = countdown_time - now + serverTimeOffset)
and set running to False.
To reset the countdown (it is not a reset in a functional way) just
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.
The countdown backend supports an extra keyword "default".
{
"default": <seconds>
}
This is used for the internal reset method if the countdown is coupled
with the list of speakers. The default of this default value can be
customized in OpenSlides config 'projector_default_countdown'.
Use additional keywords to control view behavior like "visable" and
"label". These keywords are not handles by the backend.
Countdown slide for the projector.
"""
name = 'core/countdown'
def check_data(self):
self.validate_config(self.config_entry)
if not Countdown.objects.filter(pk=self.config_entry.get('id')).exists():
raise ProjectorException('Countdown does not exists.')
@classmethod
def validate_config(cls, config_data):
"""
Raises ProjectorException if the given data are invalid.
"""
if not isinstance(config_data.get('countdown_time'), (int, float)):
raise ProjectorException('Invalid countdown time. Use integer or float.')
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):
if action not in ('start', 'stop', 'reset'):
raise ValueError("Action must be 'start', 'stop' or 'reset', not {}.".format(action))
# 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()
def get_requirements(self, config_entry):
try:
countdown = Countdown.objects.get(pk=config_entry.get('id'))
except Countdown.DoesNotExist:
# Just do nothing if message does not exist
pass
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()
yield countdown
class Message(ProjectorElement):
class ProjectorMessageElement(ProjectorElement):
"""
Short message on the projector. Rendered as overlay.
"""
name = 'core/message'
name = 'core/projectormessage'
def check_data(self):
if self.config_entry.get('message') is None:
raise ProjectorException('No message given.')
if not ProjectorMessage.objects.filter(pk=self.config_entry.get('id')).exists():
raise ProjectorException('Message does not exists.')
def get_requirements(self, config_entry):
try:
message = ProjectorMessage.objects.get(pk=config_entry.get('id'))
except ProjectorMessage.DoesNotExist:
# Just do nothing if message does not exist
pass
else:
yield message

View File

@ -1,6 +1,13 @@
from openslides.utils.rest_api import Field, ModelSerializer, ValidationError
from .models import ChatMessage, ProjectionDefault, Projector, Tag
from .models import (
ChatMessage,
Countdown,
ProjectionDefault,
Projector,
ProjectorMessage,
Tag,
)
class JSONSerializerField(Field):
@ -61,3 +68,21 @@ class ChatMessageSerializer(ModelSerializer):
model = ChatMessage
fields = ('id', 'message', 'timestamp', 'user', )
read_only_fields = ('user', )
class ProjectorMessageSerializer(ModelSerializer):
"""
Serializer for core.models.ProjectorMessage objects.
"""
class Meta:
model = ProjectorMessage
fields = ('id', 'message', )
class CountdownSerializer(ModelSerializer):
"""
Serializer for core.models.Countdown objects.
"""
class Meta:
model = Countdown
fields = ('id', 'description', 'default_time', 'countdown_time', 'running', )

View File

@ -3,8 +3,6 @@ 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.
@ -21,62 +19,3 @@ def delete_django_app_permissions(sender, **kwargs):
Q(app_label='contenttypes') |
Q(app_label='sessions'))
Permission.objects.filter(content_type__in=contenttypes).delete()
def create_builtin_projection_defaults(**kwargs):
"""
Creates the builtin defaults:
- agenda_all_items, agenda_list_of_speakers, agenda_current_list_of_speakers
- topics
- assignments
- mediafiles
- motion
- users
These strings have to be used in the controllers where you want to
define a projector button. Use the string to get the id of the
responsible projector and pass this id to the projector button directive.
"""
# Check whether ProjectionDefault objects exist.
if ProjectionDefault.objects.all().exists():
# Do completely nothing if some 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='agenda_list_of_speakers',
display_name='List of speakers',
projector=default_projector)
ProjectionDefault.objects.create(
name='agenda_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='motionBlocks',
display_name='Motion Blocks',
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

@ -86,9 +86,7 @@ img {
margin: 0 auto 0 auto;
}
/** Header **/
#header {
float: left;
width: 100%;
@ -799,7 +797,7 @@ img {
padding: 10px 15px;
}
.col2 .message .projectorbtn {
.col2 .message projector-button {
float: left;
width: auto;
margin: 5px 10px 5px 0px;

View File

@ -287,12 +287,16 @@ angular.module('OpenSlidesApp.core', [
'ChatMessage',
'Config',
'Projector',
function (ChatMessage, Config, Projector) {
'ProjectorMessage',
'Countdown',
function (ChatMessage, Config, Projector, ProjectorMessage, Countdown) {
return function () {
Config.findAll();
// Loads all projector data and the projectiondefaults
Projector.findAll();
ProjectorMessage.findAll();
Countdown.findAll();
// Loads all chat messages data and their user_ids
// TODO: add permission check if user has required chat permission
@ -640,6 +644,112 @@ angular.module('OpenSlidesApp.core', [
}
])
/* Model for ProjectorMessages */
.factory('ProjectorMessage', [
'DS',
'jsDataModel',
'gettext',
'$http',
'Projector',
function(DS, jsDataModel, gettext, $http, Projector) {
var name = 'core/projectormessage';
return DS.defineResource({
name: name,
useClass: jsDataModel,
verboseName: gettext('Message'),
verbosenamePlural: gettext('Messages'),
methods: {
getResourceName: function () {
return name;
},
// Override the BaseModel.project function
project: function(projectorId) {
// if this object is already projected on projectorId, delete this element from this projector
var isProjectedIds = this.isProjected();
var self = this;
var predicate = function (element) {
return element.name == name && element.id == self.id;
};
_.forEach(isProjectedIds, function (id) {
var uuid = _.findKey(Projector.get(id).elements, predicate);
$http.post('/rest/core/projector/' + id + '/deactivate_elements/', [uuid]);
});
// if it was the same projector before, just delete it but not show again
if (_.indexOf(isProjectedIds, projectorId) == -1) {
return $http.post(
'/rest/core/projector/' + projectorId + '/activate_elements/',
[{name: name, id: self.id, stable: true}]
);
}
},
}
});
}
])
/* Model for Countdowns */
.factory('Countdown', [
'DS',
'jsDataModel',
'gettext',
'$rootScope',
'$http',
'Projector',
function(DS, jsDataModel, gettext, $rootScope, $http, Projector) {
var name = 'core/countdown';
return DS.defineResource({
name: name,
useClass: jsDataModel,
verboseName: gettext('Countdown'),
verbosenamePlural: gettext('Countdowns'),
methods: {
getResourceName: function () {
return name;
},
start: function () {
// calculate end point of countdown (in seconds!)
var endTimestamp = Date.now() / 1000 - $rootScope.serverOffset + this.countdown_time;
this.running = true;
this.countdown_time = endTimestamp;
DS.save(name, this.id);
},
stop: function () {
// calculate rest duration of countdown (in seconds!)
var newDuration = Math.floor( this.countdown_time - Date.now() / 1000 + $rootScope.serverOffset );
this.running = false;
this.countdown_time = newDuration;
DS.save(name, this.id);
},
reset: function () {
this.running = false;
this.countdown_time = this.default_time;
DS.save(name, this.id);
},
// Override the BaseModel.project function
project: function(projectorId) {
// if this object is already projected on projectorId, delete this element from this projector
var isProjectedIds = this.isProjected();
var self = this;
var predicate = function (element) {
return element.name == name && element.id == self.id;
};
_.forEach(isProjectedIds, function (id) {
var uuid = _.findKey(Projector.get(id).elements, predicate);
$http.post('/rest/core/projector/' + id + '/deactivate_elements/', [uuid]);
});
// if it was the same projector before, just delete it but not show again
if (_.indexOf(isProjectedIds, projectorId) == -1) {
return $http.post(
'/rest/core/projector/' + projectorId + '/activate_elements/',
[{name: name, id: self.id, stable: true}]
);
}
},
},
});
}
])
/* Converts number of seconds into string "h:mm:ss" or "mm:ss" */
.filter('osSecondsToTime', [
function () {
@ -733,10 +843,12 @@ angular.module('OpenSlidesApp.core', [
.run([
'ChatMessage',
'Config',
'Countdown',
'ProjectorMessage',
'Projector',
'ProjectionDefault',
'Tag',
function (ChatMessage, Config, Projector, ProjectionDefault, Tag) {}
function (ChatMessage, Config, Countdown, ProjectorMessage, Projector, ProjectionDefault, Tag) {}
]);
}());

View File

@ -50,7 +50,7 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
template: 'static/templates/core/slide_countdown.html',
});
slidesProvider.registerSlide('core/message', {
slidesProvider.registerSlide('core/projectormessage', {
template: 'static/templates/core/slide_message.html',
});
}
@ -214,42 +214,60 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
.controller('SlideCountdownCtrl', [
'$scope',
'$interval',
function($scope, $interval) {
'Countdown',
function($scope, $interval, Countdown) {
// 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.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 id = $scope.element.id;
var interval;
if ($scope.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;
}
var calculateCountdownTime = function (countdown) {
countdown.seconds = Math.floor( $scope.countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset );
};
$scope.$watch(function () {
return Countdown.lastModified(id);
}, function () {
$scope.countdown = Countdown.get(id);
if (interval) {
$interval.cancel(interval);
}
if ($scope.countdown) {
if ($scope.countdown.running) {
calculateCountdownTime($scope.countdown);
interval = $interval(function () { calculateCountdownTime($scope.countdown); }, 1000);
} else {
$scope.countdown.seconds = $scope.countdown.countdown_time;
}
}
});
$scope.$on('$destroy', function() {
// Cancel the interval if the controller is destroyed
$interval.cancel(interval);
if (interval) {
$interval.cancel(interval);
}
});
}
])
.controller('SlideMessageCtrl', [
'$scope',
function($scope) {
'ProjectorMessage',
'Projector',
'ProjectorID',
'gettextCatalog',
function($scope, ProjectorMessage, Projector, ProjectorID, gettextCatalog) {
// 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;
$scope.selected = $scope.element.selected;
$scope.type = $scope.element.type;
var id = $scope.element.id;
if ($scope.element.identify) {
var projector = Projector.get(ProjectorID());
$scope.identifyMessage = gettextCatalog.getString('Projector') + ' ' + projector.id + ': ' + projector.name;
} else {
$scope.message = ProjectorMessage.get(id);
ProjectorMessage.bindOne(id, $scope, 'message');
}
}
]);

View File

@ -805,50 +805,44 @@ angular.module('OpenSlidesApp.core.site', [
'CurrentListOfSpeakersItem',
'ListOfSpeakersOverlay',
'ProjectionDefault',
function($scope, $http, $interval, $state, $q, Config, Projector, CurrentListOfSpeakersItem, ListOfSpeakersOverlay, ProjectionDefault) {
$scope.countdowns = [];
$scope.highestCountdownIndex = 0;
$scope.messages = [];
$scope.highestMessageIndex = 0;
$scope.listofspeakers = ListOfSpeakersOverlay;
'ProjectorMessage',
'Countdown',
'gettextCatalog',
function($scope, $http, $interval, $state, $q, Config, Projector, CurrentListOfSpeakersItem,
ListOfSpeakersOverlay, ProjectionDefault, ProjectorMessage, Countdown, gettextCatalog) {
ProjectorMessage.bindAll({}, $scope, 'messages');
var intervals = [];
var calculateCountdownTime = function (countdown) {
countdown.seconds = Math.floor( countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset );
};
var cancelIntervalTimers = function () {
$scope.countdowns.forEach(function (countdown) {
$interval.cancel(countdown.interval);
intervals.forEach(function (interval) {
$interval.cancel(interval);
});
};
$scope.$watch(function () {
return Countdown.lastModified();
}, function () {
$scope.countdowns = Countdown.getAll();
// 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;
}
// stop ALL interval timer
cancelIntervalTimers();
$scope.countdowns.forEach(function (countdown) {
if (countdown.running) {
calculateCountdownTime(countdown);
intervals.push($interval(function () { calculateCountdownTime(countdown); }, 1000));
} else {
countdown.seconds = countdown.countdown_time;
}
});
};
});
$scope.$on('$destroy', function() {
// Cancel all intervals if the controller is destroyed
cancelIntervalTimers();
});
$scope.listofspeakers = ListOfSpeakersOverlay;
$scope.$watch(function () {
return Projector.lastModified();
}, function () {
@ -856,26 +850,20 @@ angular.module('OpenSlidesApp.core.site', [
if (!$scope.active_projector) {
$scope.changeProjector($scope.projectors[0]);
}
$scope.getDefaultOverlayProjector();
// stop ALL interval timer
cancelIntervalTimers();
rebuildAllElements();
$scope.messageDefaultProjectorId = ProjectionDefault.filter({name: 'messages'})[0].projector_id;
$scope.countdownDefaultProjectorId = ProjectionDefault.filter({name: 'countdowns'})[0].projector_id;
$scope.getDefaultOverlayProjector();
});
// gets the default projector where the current list of speakers overlay will be displayed
$scope.getDefaultOverlayProjector = function () {
var projectiondefault = ProjectionDefault.filter({name: 'agenda_current_list_of_speakers'})[0];
if (projectiondefault) {
$scope.defaultProjectorId = projectiondefault.projector_id;
$scope.listofSpeakersDefaultProjectorId = projectiondefault.projector_id;
} else {
$scope.defaultProjectorId = 1;
$scope.listOfSpeakersDefaultProjectorId = 1;
}
};
$scope.$on('$destroy', function() {
// Cancel all intervals if the controller is destroyed
cancelIntervalTimers();
});
// watch for changes in projector_broadcast and currentListOfSpeakersReference
var last_broadcast;
$scope.$watch(function () {
@ -908,205 +896,45 @@ angular.module('OpenSlidesApp.core.site', [
};
$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);
}
});
});
countdown.description = countdown.new_description;
Countdown.save(countdown);
};
$scope.addCountdown = function () {
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',
countdown_time: default_time,
default_time: default_time,
visible: false,
selected: true,
index: $scope.highestCountdownIndex,
running: false,
stable: true,
}]);
});
var countdown = {
description: '',
default_time: default_time,
countdown_time: default_time,
running: false,
};
Countdown.create(countdown);
};
$scope.removeCountdown = function (countdown) {
$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) {
$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) {
$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) {
$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);
}
});
var isProjectedIds = countdown.isProjected();
_.forEach(isProjectedIds, function(id) {
countdown.project(id);
});
Countdown.destroy(countdown.id);
};
// *** 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);
}
});
});
ProjectorMessage.save(message);
};
$scope.addMessage = function () {
$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,
selected: true,
index: $scope.highestMessageIndex,
message: '',
stable: true,
}]);
});
var message = {message: ''};
ProjectorMessage.create(message);
};
$scope.removeMessage = function (message) {
$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]);
}
});
var isProjectedIds = message.isProjected();
_.forEach(isProjectedIds, function(id) {
message.project(id);
});
ProjectorMessage.destroy(message.id);
};
/* 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.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();
};
/* go to the list of speakers(management) of the currently displayed list of speakers reference slide*/
// go to the list of speakers(management) of the currently displayed list of speakers reference slide
$scope.goToListOfSpeakers = function() {
CurrentListOfSpeakersItem.getItem($scope.currentListOfSpeakersReference).then(function (success) {
$state.go('agenda.item.detail', {id: success.id});
@ -1123,8 +951,8 @@ angular.module('OpenSlidesApp.core.site', [
'Projector',
'ProjectionDefault',
'Config',
'gettextCatalog',
function ($scope, $http, $state, $timeout, Projector, ProjectionDefault, Config, gettextCatalog) {
'ProjectorMessage',
function ($scope, $http, $state, $timeout, Projector, ProjectionDefault, Config, ProjectorMessage) {
ProjectionDefault.bindAll({}, $scope, 'projectiondefaults');
// watch for changes in projector_broadcast
@ -1233,27 +1061,33 @@ angular.module('OpenSlidesApp.core.site', [
$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'
}]);
// Create new Message
var message = {
message: '',
};
ProjectorMessage.create(message).then(function(message){
$scope.projectors.forEach(function (projector) {
$http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{
name: 'core/projectormessage',
stable: true,
id: message.id,
identify: true,
}]);
});
$scope.identifierMessage = message;
});
$scope.identifyPromise = $timeout($scope.removeIdentifierMessages, 3000);
}
};
$scope.removeIdentifierMessages = function () {
Projector.getAll().forEach(function (projector) {
angular.forEach(projector.elements, function (uuid, value) {
if (value.name == 'core/message' && value.type == 'identify') {
_.forEach(projector.elements, function (element, uuid) {
if (element.name == 'core/projectormessage' && element.id == $scope.identifierMessage.id) {
$http.post('/rest/core/projector/' + projector.id + '/deactivate_elements/', [uuid]);
}
});
});
ProjectorMessage.destroy($scope.identifierMessage.id);
$scope.identifyPromise = null;
};
}

View File

@ -141,7 +141,7 @@
<h4 translate>Countdowns</h4>
</a>
<div uib-collapse="!isCountdowns" ng-cloak>
<div ng-repeat="countdown in countdowns | orderBy: 'index'" id="countdown{{countdown.uuid}}" class="countdown panel panel-default">
<div ng-repeat="countdown in countdowns | orderBy: 'index'" id="countdown{{countdown.id}}" class="countdown panel panel-default">
<div class="panel-heading">
<span ng-if="countdown.description">{{ countdown.description }}</span>
<span ng-if="!countdown.description">Countdown {{ $index +1 }}</span>
@ -153,63 +153,34 @@
</button>
<!-- edit countdown button -->
<button type="button" class="close editicon"
ng-click="countdown.editFlag=true;"
ng-click="countdown.editFlag=true; countdown.new_description = countdown.description;"
title="{{ 'Edit countdown' | translate}}">
<i class="fa fa-pencil"></i>
</button>
</div>
<div class="panel-body"
ng-class="{ 'projected': isProjected(countdown).length }">
<!-- project countdown button -->
<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>
<projector-button model="countdown" default-projector-id="countdownDefaultProjectorId"></projector-button>
&nbsp;&nbsp;
<!-- countdown controls -->
<a class="btn btn-default vcenter"
ng-click="resetCountdown(countdown)"
ng-click="countdown.reset()"
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.running" class="btn btn-default vcenter"
ng-click="startCountdown(countdown)"
ng-click="countdown.start()"
title="{{ 'Start' | translate}}">
<i class="fa fa-play"></i>
<i ng-if="countdown.running" class="fa fa-pause"></i>
</a>
<a ng-if="countdown.running" class="btn btn-default vcenter"
ng-click="stopCountdown(countdown)"
ng-click="countdown.stop()"
title="{{ 'Pause' | translate}}">
<i class="fa fa-pause"></i>
</a>
<span ng-if="!editTime" class="countdown_timer vcenter"
<span ng-if="!countdown.editTime" class="countdown_timer vcenter"
ng-class="{
'negative': countdown.seconds <= 0,
'warning': countdown.seconds <= config('agenda_countdown_warning_time') && countdown.seconds > 0 }">
@ -220,7 +191,7 @@
ng-submit="editCountdown(countdown)">
<div class="form-group">
<label translate>Description</label>
<input ng-model="countdown.description" type="text" class="form-control input-sm">
<input ng-model="countdown.new_description" type="text" class="form-control input-sm">
</div>
<div class="form-group">
<label translate>Start time</label>
@ -281,39 +252,7 @@
<div class="panel-body"
ng-class="{ 'projected': isProjected(message).length }">
<div class="projectorbtn">
<!-- project message button -->
<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>
<projector-button model="message" default-projector-id="messageDefaultProjectorId"></projector-button>
&nbsp;&nbsp;
<div class="innermessage" ng-bind-html="message.message"> </div>
@ -344,7 +283,7 @@
<h4 translate>List of speakers</h4>
</a>
<div uib-collapse="!isSpeakerList" ng-cloak>
<projector-button model="listofspeakers" default-projector-id="defaultProjectorId">
<projector-button model="listofspeakers" default-projector-id="listOfSpeakersDefaultProjectorId">
</projector-button>
<div class="btn-group" os-perms="agenda.can_manage">
<a ng-click="goToListOfSpeakers()" class="btn btn-default btn-sm"

View File

@ -1,11 +1,9 @@
<div ng-controller="SlideCountdownCtrl">
<div ng-if="visible && selected">
<div class="countdown well pull-right"
ng-class="{
'negative': seconds <= 0,
'warning': seconds <= config('agenda_countdown_warning_time') && seconds > 0 }">
{{ seconds | osSecondsToTime}}
<div class="description">{{ description }}</div>
</div>
<div class="countdown well pull-right"
ng-class="{
'negative': countdown.seconds <= 0,
'warning': countdown.seconds <= config('agenda_countdown_warning_time') && countdown.seconds > 0 }">
{{ countdown.seconds | osSecondsToTime}}
<div class="description">{{ countdown.description }}</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
<div ng-controller="SlideMessageCtrl">
<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 class="message_background"></div>
<div class="message well" ng-class="{'identify': element.identify}" ng-bind-html="element.identify ? identifyMessage : message.message"></div>
</div>

View File

@ -39,12 +39,22 @@ from ..utils.search import search
from .access_permissions import (
ChatMessageAccessPermissions,
ConfigAccessPermissions,
CountdownAccessPermissions,
ProjectorAccessPermissions,
ProjectorMessageAccessPermissions,
TagAccessPermissions,
)
from .config import config
from .exceptions import ConfigError, ConfigNotFound
from .models import ChatMessage, ConfigStore, ProjectionDefault, Projector, Tag
from .models import (
ChatMessage,
ConfigStore,
Countdown,
ProjectionDefault,
Projector,
ProjectorMessage,
Tag,
)
# Special Django views
@ -709,6 +719,50 @@ class ChatMessageViewSet(ModelViewSet):
return Response({'detail': _('All chat messages deleted successfully.')})
class ProjectorMessageViewSet(ModelViewSet):
"""
API endpoint for messages.
There are the following views: list, retrieve, create, update and destroy.
"""
access_permissions = ProjectorMessageAccessPermissions()
queryset = ProjectorMessage.objects.all()
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ('create', 'update', 'destroy'):
result = self.request.user.has_perm('core.can_manage_projector')
else:
result = False
return result
class CountdownViewSet(ModelViewSet):
"""
API endpoint for Countdown.
There are the following views: list, retrieve, create, update and destroy.
"""
access_permissions = CountdownAccessPermissions()
queryset = Countdown.objects.all()
def check_view_permissions(self):
"""
Returns True if the user has required permissions.
"""
if self.action in ('list', 'retrieve'):
result = self.get_access_permissions().check_permissions(self.request.user)
elif self.action in ('create', 'update', 'destroy'):
result = self.request.user.has_perm('core.can_manage_projector')
else:
result = False
return result
# Special API views
class UrlPatternsView(utils_views.APIView):

View File

@ -5,6 +5,7 @@ from __future__ import unicode_literals
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import openslides.utils.models

View File

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

View File

@ -19,7 +19,6 @@ from ..utils.rest_api import (
detail_route,
)
from ..utils.views import APIView
from .access_permissions import (
CategoryAccessPermissions,
MotionAccessPermissions,

View File

@ -6,7 +6,7 @@ from rest_framework.test import APIClient
from openslides.agenda.models import Item, Speaker
from openslides.assignments.models import Assignment
from openslides.core.config import config
from openslides.core.models import Projector
from openslides.core.models import Countdown
from openslides.motions.models import Motion
from openslides.topics.models import Topic
from openslides.users.models import User
@ -278,29 +278,20 @@ class Speak(TestCase):
self.client.put(
reverse('item-speak', args=[self.item.pk]),
{'speaker': speaker.pk})
for key, value in Projector.objects.get().config.items():
if value['name'] == 'core/countdown':
self.assertTrue(value['running'])
# If created, the countdown should have index 1
created = value['index'] == 1
break
else:
created = False
self.assertTrue(created)
# Countdown should be created with pk=1 and running
self.assertEqual(Countdown.objects.all().count(), 1)
countdown = Countdown.objects.get(pk=1)
self.assertTrue(countdown.running)
def test_end_speech_with_countdown(self):
config['agenda_couple_countdown_and_speakers'] = True
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.assertFalse(value['running'])
success = True
break
else:
success = False
self.assertTrue(success)
# Countdown should be created with pk=1 and stopped
self.assertEqual(Countdown.objects.all().count(), 1)
countdown = Countdown.objects.get(pk=1)
self.assertFalse(countdown.running)
class Numbering(TestCase):