diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py index 12b921a67..6896afe20 100644 --- a/openslides/agenda/models.py +++ b/openslides/agenda/models.py @@ -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): """ diff --git a/openslides/core/access_permissions.py b/openslides/core/access_permissions.py index 0ffb49cba..90cec0f01 100644 --- a/openslides/core/access_permissions.py +++ b/openslides/core/access_permissions.py @@ -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 diff --git a/openslides/core/apps.py b/openslides/core/apps.py index c0f68c1f5..c7dc9ceb7 100644 --- a/openslides/core/apps.py +++ b/openslides/core/apps.py @@ -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( diff --git a/openslides/core/migrations/0008_countdown_message.py b/openslides/core/migrations/0008_countdown_message.py new file mode 100644 index 000000000..fe31ad351 --- /dev/null +++ b/openslides/core/migrations/0008_countdown_message.py @@ -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), + ] diff --git a/openslides/core/models.py b/openslides/core/models.py index 583071f35..c74581c28 100644 --- a/openslides/core/models.py +++ b/openslides/core/models.py @@ -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 diff --git a/openslides/core/projector.py b/openslides/core/projector.py index 296d7f2f7..a0b46c1e0 100644 --- a/openslides/core/projector.py +++ b/openslides/core/projector.py @@ -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": , - } - - 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": - } - - 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 diff --git a/openslides/core/serializers.py b/openslides/core/serializers.py index a3c45dd89..19fbc4042 100644 --- a/openslides/core/serializers.py +++ b/openslides/core/serializers.py @@ -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', ) diff --git a/openslides/core/signals.py b/openslides/core/signals.py index bb08c9918..033626341 100644 --- a/openslides/core/signals.py +++ b/openslides/core/signals.py @@ -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) diff --git a/openslides/core/static/css/app.css b/openslides/core/static/css/app.css index 6ea0da188..a8e30f1b4 100644 --- a/openslides/core/static/css/app.css +++ b/openslides/core/static/css/app.css @@ -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; diff --git a/openslides/core/static/js/core/base.js b/openslides/core/static/js/core/base.js index f0a48bbf6..5141d87c4 100644 --- a/openslides/core/static/js/core/base.js +++ b/openslides/core/static/js/core/base.js @@ -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) {} ]); }()); diff --git a/openslides/core/static/js/core/projector.js b/openslides/core/static/js/core/projector.js index 00b423e1c..5b60097d6 100644 --- a/openslides/core/static/js/core/projector.js +++ b/openslides/core/static/js/core/projector.js @@ -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'); + } } ]); diff --git a/openslides/core/static/js/core/site.js b/openslides/core/static/js/core/site.js index c4ef29d96..4f9805ad1 100644 --- a/openslides/core/static/js/core/site.js +++ b/openslides/core/static/js/core/site.js @@ -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; }; } diff --git a/openslides/core/static/templates/core/projector-controls.html b/openslides/core/static/templates/core/projector-controls.html index f6ecfedee..9de254fca 100644 --- a/openslides/core/static/templates/core/projector-controls.html +++ b/openslides/core/static/templates/core/projector-controls.html @@ -141,7 +141,7 @@

Countdowns

-
+
{{ countdown.description }} Countdown {{ $index +1 }} @@ -153,63 +153,34 @@
- -
- - - -
+    - @@ -220,7 +191,7 @@ ng-submit="editCountdown(countdown)">
- +
@@ -281,39 +252,7 @@
-
- -
- - - -
- -
+   
@@ -344,7 +283,7 @@

List of speakers

- +
-
-
- {{ seconds | osSecondsToTime}} -
{{ description }}
-
+
+ {{ countdown.seconds | osSecondsToTime}} +
{{ countdown.description }}
diff --git a/openslides/core/static/templates/core/slide_message.html b/openslides/core/static/templates/core/slide_message.html index a3debfeda..80f757212 100644 --- a/openslides/core/static/templates/core/slide_message.html +++ b/openslides/core/static/templates/core/slide_message.html @@ -1,4 +1,4 @@
-
-
+
+
diff --git a/openslides/core/views.py b/openslides/core/views.py index 859ebb4c8..b4352db45 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -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): diff --git a/openslides/motions/migrations/0005_motionchangerecommendation.py b/openslides/motions/migrations/0005_motionchangerecommendation.py index 8ed9758c1..44d34cd1a 100644 --- a/openslides/motions/migrations/0005_motionchangerecommendation.py +++ b/openslides/motions/migrations/0005_motionchangerecommendation.py @@ -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 diff --git a/openslides/motions/migrations/0006_auto_20161017_2020.py b/openslides/motions/migrations/0006_auto_20161017_2020.py index 90a0fa6d0..2070ebc63 100644 --- a/openslides/motions/migrations/0006_auto_20161017_2020.py +++ b/openslides/motions/migrations/0006_auto_20161017_2020.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import django.db.models.deletion from django.db import migrations, models + import openslides.utils.models diff --git a/openslides/motions/views.py b/openslides/motions/views.py index 7fb9cb167..04e30c93f 100644 --- a/openslides/motions/views.py +++ b/openslides/motions/views.py @@ -19,7 +19,6 @@ from ..utils.rest_api import ( detail_route, ) from ..utils.views import APIView - from .access_permissions import ( CategoryAccessPermissions, MotionAccessPermissions, diff --git a/tests/integration/agenda/test_viewsets.py b/tests/integration/agenda/test_viewsets.py index e72d6313b..1e80cc27a 100644 --- a/tests/integration/agenda/test_viewsets.py +++ b/tests/integration/agenda/test_viewsets.py @@ -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):