countdown and message models (closes #2464)

This commit is contained in:
FinnStutzenstein 2016-10-21 11:05:24 +02:00
parent 577d0bf3cc
commit 0cc8a81320
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 django.utils.translation import ugettext_lazy
from openslides.core.config import config 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.exceptions import OpenSlidesError
from openslides.utils.models import RESTModelMixin from openslides.utils.models import RESTModelMixin
from openslides.utils.utils import to_roman from openslides.utils.utils import to_roman
@ -412,8 +412,12 @@ class Speaker(RESTModelMixin, models.Model):
self.begin_time = timezone.now() self.begin_time = timezone.now()
self.save() self.save()
if config['agenda_couple_countdown_and_speakers']: if config['agenda_couple_countdown_and_speakers']:
Countdown.control(action='reset') countdown, created = Countdown.objects.get_or_create(pk=1, defaults={
Countdown.control(action='start') '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): def end_speech(self):
""" """
@ -422,7 +426,12 @@ class Speaker(RESTModelMixin, models.Model):
self.end_time = timezone.now() self.end_time = timezone.now()
self.save() self.save()
if config['agenda_couple_countdown_and_speakers']: 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): def get_root_rest_element(self):
""" """

View File

@ -64,6 +64,44 @@ class ChatMessageAccessPermissions(BaseAccessPermissions):
return ChatMessageSerializer 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): class ConfigAccessPermissions(BaseAccessPermissions):
""" """
Access permissions container for the config (ConfigStore and 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.rest_api import router
from openslides.utils.search import index_add_instance, index_del_instance from openslides.utils.search import index_add_instance, index_del_instance
from .config_variables import get_config_variables 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 ( from .views import (
ChatMessageViewSet, ChatMessageViewSet,
ConfigViewSet, ConfigViewSet,
CountdownViewSet,
ProjectorMessageViewSet,
ProjectorViewSet, ProjectorViewSet,
TagViewSet, TagViewSet,
) )
@ -34,15 +36,14 @@ class CoreAppConfig(AppConfig):
post_permission_creation.connect( post_permission_creation.connect(
delete_django_app_permissions, delete_django_app_permissions,
dispatch_uid='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. # Register viewsets.
router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet) 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('ChatMessage').get_collection_string(), ChatMessageViewSet)
router.register(self.get_model('Tag').get_collection_string(), TagViewSet) 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('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 # Update the search when a model is saved or deleted
signals.post_save.connect( 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.conf import settings
from django.contrib.sessions.models import Session as DjangoSession from django.contrib.sessions.models import Session as DjangoSession
from django.db import models from django.db import models
from django.utils.timezone import now
from jsonfield import JSONField from jsonfield import JSONField
from ..utils.collection import CollectionElement from ..utils.collection import CollectionElement
@ -9,7 +10,9 @@ from ..utils.projector import ProjectorElement
from .access_permissions import ( from .access_permissions import (
ChatMessageAccessPermissions, ChatMessageAccessPermissions,
ConfigAccessPermissions, ConfigAccessPermissions,
CountdownAccessPermissions,
ProjectorAccessPermissions, ProjectorAccessPermissions,
ProjectorMessageAccessPermissions,
TagAccessPermissions, TagAccessPermissions,
) )
from .exceptions import ProjectorException from .exceptions import ProjectorException
@ -294,6 +297,51 @@ class ChatMessage(RESTModelMixin, models.Model):
return 'Message {}'.format(self.timestamp) 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): class Session(DjangoSession):
""" """
Model like the Django db session, which saves the user as ForeignKey instead 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 ..utils.projector import ProjectorElement
from .config import config
from .exceptions import ProjectorException from .exceptions import ProjectorException
from .models import Projector from .models import Countdown, ProjectorMessage
class Clock(ProjectorElement): class Clock(ProjectorElement):
@ -15,134 +10,41 @@ class Clock(ProjectorElement):
name = 'core/clock' name = 'core/clock'
class Countdown(ProjectorElement): class CountdownElement(ProjectorElement):
""" """
Countdown on the projector. Countdown slide for 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.
""" """
name = 'core/countdown' name = 'core/countdown'
def check_data(self): 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 get_requirements(self, config_entry):
def validate_config(cls, config_data): try:
""" countdown = Countdown.objects.get(pk=config_entry.get('id'))
Raises ProjectorException if the given data are invalid. except Countdown.DoesNotExist:
""" # Just do nothing if message does not exist
if not isinstance(config_data.get('countdown_time'), (int, float)): pass
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()
else: else:
# search for the countdown and modify it. yield countdown
for projector in projectors:
projector_config = {}
found = False
for key, value in projector.config.items():
if value['name'] == cls.name and value['index'] == lowest_index:
try:
cls.validate_config(value)
except ProjectorException:
# Do not proceed if the specific procjector config data is invalid.
# The variable found remains False.
break
found = True
if action == 'start':
value['running'] = True
value['countdown_time'] = now().timestamp() + value['default_time']
elif action == 'stop' and value['running']:
value['running'] = False
value['countdown_time'] = value['countdown_time'] - now().timestamp()
elif action == 'reset':
value['running'] = False
value['countdown_time'] = value['default_time']
projector_config[key] = value
if found:
projector.config = projector_config
projector.save()
class Message(ProjectorElement): class ProjectorMessageElement(ProjectorElement):
""" """
Short message on the projector. Rendered as overlay. Short message on the projector. Rendered as overlay.
""" """
name = 'core/message' name = 'core/projectormessage'
def check_data(self): def check_data(self):
if self.config_entry.get('message') is None: if not ProjectorMessage.objects.filter(pk=self.config_entry.get('id')).exists():
raise ProjectorException('No message given.') 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 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): class JSONSerializerField(Field):
@ -61,3 +68,21 @@ class ChatMessageSerializer(ModelSerializer):
model = ChatMessage model = ChatMessage
fields = ('id', 'message', 'timestamp', 'user', ) fields = ('id', 'message', 'timestamp', 'user', )
read_only_fields = ('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.db.models import Q
from django.dispatch import Signal 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 # 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 # after post_migrate sending and creating all Permission objects. Don't use it
# for other things than dealing with Permission objects. # 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='contenttypes') |
Q(app_label='sessions')) Q(app_label='sessions'))
Permission.objects.filter(content_type__in=contenttypes).delete() 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; margin: 0 auto 0 auto;
} }
/** Header **/ /** Header **/
#header { #header {
float: left; float: left;
width: 100%; width: 100%;
@ -799,7 +797,7 @@ img {
padding: 10px 15px; padding: 10px 15px;
} }
.col2 .message .projectorbtn { .col2 .message projector-button {
float: left; float: left;
width: auto; width: auto;
margin: 5px 10px 5px 0px; margin: 5px 10px 5px 0px;

View File

@ -287,12 +287,16 @@ angular.module('OpenSlidesApp.core', [
'ChatMessage', 'ChatMessage',
'Config', 'Config',
'Projector', 'Projector',
function (ChatMessage, Config, Projector) { 'ProjectorMessage',
'Countdown',
function (ChatMessage, Config, Projector, ProjectorMessage, Countdown) {
return function () { return function () {
Config.findAll(); Config.findAll();
// Loads all projector data and the projectiondefaults // Loads all projector data and the projectiondefaults
Projector.findAll(); Projector.findAll();
ProjectorMessage.findAll();
Countdown.findAll();
// Loads all chat messages data and their user_ids // Loads all chat messages data and their user_ids
// TODO: add permission check if user has required chat permission // 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" */ /* Converts number of seconds into string "h:mm:ss" or "mm:ss" */
.filter('osSecondsToTime', [ .filter('osSecondsToTime', [
function () { function () {
@ -733,10 +843,12 @@ angular.module('OpenSlidesApp.core', [
.run([ .run([
'ChatMessage', 'ChatMessage',
'Config', 'Config',
'Countdown',
'ProjectorMessage',
'Projector', 'Projector',
'ProjectionDefault', 'ProjectionDefault',
'Tag', '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', template: 'static/templates/core/slide_countdown.html',
}); });
slidesProvider.registerSlide('core/message', { slidesProvider.registerSlide('core/projectormessage', {
template: 'static/templates/core/slide_message.html', template: 'static/templates/core/slide_message.html',
}); });
} }
@ -214,42 +214,60 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
.controller('SlideCountdownCtrl', [ .controller('SlideCountdownCtrl', [
'$scope', '$scope',
'$interval', '$interval',
function($scope, $interval) { 'Countdown',
function($scope, $interval, Countdown) {
// Attention! Each object that is used here has to be dealt on server side. // 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 // Add it to the coresponding get_requirements method of the ProjectorElement
// class. // class.
$scope.seconds = Math.floor( $scope.element.countdown_time - Date.now() / 1000 + $scope.serverOffset ); var id = $scope.element.id;
$scope.running = $scope.element.running;
$scope.visible = $scope.element.visible;
$scope.selected = $scope.element.selected;
$scope.index = $scope.element.index;
$scope.description = $scope.element.description;
// start interval timer if countdown status is running
var interval; var interval;
if ($scope.running) { var calculateCountdownTime = function (countdown) {
interval = $interval( function() { countdown.seconds = Math.floor( $scope.countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset );
$scope.seconds = Math.floor( $scope.element.countdown_time - Date.now() / 1000 + $scope.serverOffset ); };
}, 1000); $scope.$watch(function () {
} else { return Countdown.lastModified(id);
$scope.seconds = $scope.element.countdown_time; }, 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() { $scope.$on('$destroy', function() {
// Cancel the interval if the controller is destroyed // Cancel the interval if the controller is destroyed
$interval.cancel(interval); if (interval) {
$interval.cancel(interval);
}
}); });
} }
]) ])
.controller('SlideMessageCtrl', [ .controller('SlideMessageCtrl', [
'$scope', '$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. // 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 // Add it to the coresponding get_requirements method of the ProjectorElement
// class. // class.
$scope.message = $scope.element.message; var id = $scope.element.id;
$scope.visible = $scope.element.visible;
$scope.selected = $scope.element.selected; if ($scope.element.identify) {
$scope.type = $scope.element.type; 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', 'CurrentListOfSpeakersItem',
'ListOfSpeakersOverlay', 'ListOfSpeakersOverlay',
'ProjectionDefault', 'ProjectionDefault',
function($scope, $http, $interval, $state, $q, Config, Projector, CurrentListOfSpeakersItem, ListOfSpeakersOverlay, ProjectionDefault) { 'ProjectorMessage',
$scope.countdowns = []; 'Countdown',
$scope.highestCountdownIndex = 0; 'gettextCatalog',
$scope.messages = []; function($scope, $http, $interval, $state, $q, Config, Projector, CurrentListOfSpeakersItem,
$scope.highestMessageIndex = 0; ListOfSpeakersOverlay, ProjectionDefault, ProjectorMessage, Countdown, gettextCatalog) {
$scope.listofspeakers = ListOfSpeakersOverlay; 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 () { var cancelIntervalTimers = function () {
$scope.countdowns.forEach(function (countdown) { intervals.forEach(function (interval) {
$interval.cancel(countdown.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) // stop ALL interval timer
var rebuildAllElements = function () { cancelIntervalTimers();
$scope.countdowns = []; $scope.countdowns.forEach(function (countdown) {
$scope.messages = []; if (countdown.running) {
calculateCountdownTime(countdown);
_.forEach(Projector.get(1).elements, function (element, uuid) { intervals.push($interval(function () { calculateCountdownTime(countdown); }, 1000));
if (element.name == 'core/countdown') { } else {
$scope.countdowns.push(element); countdown.seconds = countdown.countdown_time;
if (element.running) {
// calculate remaining seconds directly because interval starts with 1 second delay
$scope.calculateCountdownTime(element);
// start interval timer (every second)
element.interval = $interval(function () { $scope.calculateCountdownTime(element); }, 1000);
} else {
element.seconds = element.countdown_time;
}
if (element.index > $scope.highestCountdownIndex) {
$scope.highestCountdownIndex = element.index;
}
} else if (element.name == 'core/message') {
$scope.messages.push(element);
if (element.index > $scope.highestMessageIndex) {
$scope.highestMessageIndex = element.index;
}
} }
}); });
}; });
$scope.$on('$destroy', function() {
// Cancel all intervals if the controller is destroyed
cancelIntervalTimers();
});
$scope.listofspeakers = ListOfSpeakersOverlay;
$scope.$watch(function () { $scope.$watch(function () {
return Projector.lastModified(); return Projector.lastModified();
}, function () { }, function () {
@ -856,26 +850,20 @@ angular.module('OpenSlidesApp.core.site', [
if (!$scope.active_projector) { if (!$scope.active_projector) {
$scope.changeProjector($scope.projectors[0]); $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 // gets the default projector where the current list of speakers overlay will be displayed
$scope.getDefaultOverlayProjector = function () { $scope.getDefaultOverlayProjector = function () {
var projectiondefault = ProjectionDefault.filter({name: 'agenda_current_list_of_speakers'})[0]; var projectiondefault = ProjectionDefault.filter({name: 'agenda_current_list_of_speakers'})[0];
if (projectiondefault) { if (projectiondefault) {
$scope.defaultProjectorId = projectiondefault.projector_id; $scope.listofSpeakersDefaultProjectorId = projectiondefault.projector_id;
} else { } 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 // watch for changes in projector_broadcast and currentListOfSpeakersReference
var last_broadcast; var last_broadcast;
$scope.$watch(function () { $scope.$watch(function () {
@ -908,205 +896,45 @@ angular.module('OpenSlidesApp.core.site', [
}; };
$scope.editCountdown = function (countdown) { $scope.editCountdown = function (countdown) {
countdown.editFlag = false; countdown.editFlag = false;
$scope.projectors.forEach(function (projector) { countdown.description = countdown.new_description;
_.forEach(projector.elements, function (element, uuid) { Countdown.save(countdown);
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);
}
});
});
}; };
$scope.addCountdown = function () { $scope.addCountdown = function () {
var default_time = parseInt($scope.config('projector_default_countdown')); var default_time = parseInt($scope.config('projector_default_countdown'));
$scope.highestCountdownIndex++; var countdown = {
// select all projectors on creation, so write the countdown to all projectors description: '',
$scope.projectors.forEach(function (projector) { default_time: default_time,
$http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{ countdown_time: default_time,
name: 'core/countdown', running: false,
countdown_time: default_time, };
default_time: default_time, Countdown.create(countdown);
visible: false,
selected: true,
index: $scope.highestCountdownIndex,
running: false,
stable: true,
}]);
});
}; };
$scope.removeCountdown = function (countdown) { $scope.removeCountdown = function (countdown) {
$scope.projectors.forEach(function (projector) { var isProjectedIds = countdown.isProjected();
var countdowns = []; _.forEach(isProjectedIds, function(id) {
_.forEach(projector.elements, function (element, uuid) { countdown.project(id);
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);
}
});
}); });
Countdown.destroy(countdown.id);
}; };
// *** message functions *** // *** message functions ***
$scope.editMessage = function (message) { $scope.editMessage = function (message) {
message.editFlag = false; message.editFlag = false;
$scope.projectors.forEach(function (projector) { ProjectorMessage.save(message);
_.forEach(projector.elements, function (element, uuid) {
if (element.name == 'core/message' && element.index == message.index) {
var data = {};
data[uuid] = {
message: message.message,
};
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
}
});
});
}; };
$scope.addMessage = function () { $scope.addMessage = function () {
$scope.highestMessageIndex++; var message = {message: ''};
// select all projectors on creation, so write the countdown to all projectors ProjectorMessage.create(message);
$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,
}]);
});
}; };
$scope.removeMessage = function (message) { $scope.removeMessage = function (message) {
$scope.projectors.forEach(function (projector) { var isProjectedIds = message.isProjected();
_.forEach(projector.elements, function (element, uuid) { _.forEach(isProjectedIds, function(id) {
if (element.name == 'core/message' && element.index == message.index) { message.project(id);
$http.post('/rest/core/projector/' + projector.id + '/deactivate_elements/', [uuid]);
}
});
}); });
ProjectorMessage.destroy(message.id);
}; };
/* project functions*/ // go to the list of speakers(management) of the currently displayed list of speakers reference slide
$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*/
$scope.goToListOfSpeakers = function() { $scope.goToListOfSpeakers = function() {
CurrentListOfSpeakersItem.getItem($scope.currentListOfSpeakersReference).then(function (success) { CurrentListOfSpeakersItem.getItem($scope.currentListOfSpeakersReference).then(function (success) {
$state.go('agenda.item.detail', {id: success.id}); $state.go('agenda.item.detail', {id: success.id});
@ -1123,8 +951,8 @@ angular.module('OpenSlidesApp.core.site', [
'Projector', 'Projector',
'ProjectionDefault', 'ProjectionDefault',
'Config', 'Config',
'gettextCatalog', 'ProjectorMessage',
function ($scope, $http, $state, $timeout, Projector, ProjectionDefault, Config, gettextCatalog) { function ($scope, $http, $state, $timeout, Projector, ProjectionDefault, Config, ProjectorMessage) {
ProjectionDefault.bindAll({}, $scope, 'projectiondefaults'); ProjectionDefault.bindAll({}, $scope, 'projectiondefaults');
// watch for changes in projector_broadcast // watch for changes in projector_broadcast
@ -1233,27 +1061,33 @@ angular.module('OpenSlidesApp.core.site', [
$timeout.cancel($scope.identifyPromise); $timeout.cancel($scope.identifyPromise);
$scope.removeIdentifierMessages(); $scope.removeIdentifierMessages();
} else { } else {
$scope.projectors.forEach(function (projector) { // Create new Message
$http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{ var message = {
name: 'core/message', message: '',
stable: true, };
selected: true, ProjectorMessage.create(message).then(function(message){
visible: true, $scope.projectors.forEach(function (projector) {
message: gettextCatalog.getString('Projector') + ' ' + projector.id + ': ' + projector.name, $http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{
type: 'identify' name: 'core/projectormessage',
}]); stable: true,
id: message.id,
identify: true,
}]);
});
$scope.identifierMessage = message;
}); });
$scope.identifyPromise = $timeout($scope.removeIdentifierMessages, 3000); $scope.identifyPromise = $timeout($scope.removeIdentifierMessages, 3000);
} }
}; };
$scope.removeIdentifierMessages = function () { $scope.removeIdentifierMessages = function () {
Projector.getAll().forEach(function (projector) { Projector.getAll().forEach(function (projector) {
angular.forEach(projector.elements, function (uuid, value) { _.forEach(projector.elements, function (element, uuid) {
if (value.name == 'core/message' && value.type == 'identify') { if (element.name == 'core/projectormessage' && element.id == $scope.identifierMessage.id) {
$http.post('/rest/core/projector/' + projector.id + '/deactivate_elements/', [uuid]); $http.post('/rest/core/projector/' + projector.id + '/deactivate_elements/', [uuid]);
} }
}); });
}); });
ProjectorMessage.destroy($scope.identifierMessage.id);
$scope.identifyPromise = null; $scope.identifyPromise = null;
}; };
} }

View File

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

View File

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

View File

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

View File

@ -39,12 +39,22 @@ from ..utils.search import search
from .access_permissions import ( from .access_permissions import (
ChatMessageAccessPermissions, ChatMessageAccessPermissions,
ConfigAccessPermissions, ConfigAccessPermissions,
CountdownAccessPermissions,
ProjectorAccessPermissions, ProjectorAccessPermissions,
ProjectorMessageAccessPermissions,
TagAccessPermissions, TagAccessPermissions,
) )
from .config import config from .config import config
from .exceptions import ConfigError, ConfigNotFound 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 # Special Django views
@ -709,6 +719,50 @@ class ChatMessageViewSet(ModelViewSet):
return Response({'detail': _('All chat messages deleted successfully.')}) 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 # Special API views
class UrlPatternsView(utils_views.APIView): class UrlPatternsView(utils_views.APIView):

View File

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

View File

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

View File

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

View File

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