countdown and message models (closes #2464)
This commit is contained in:
parent
577d0bf3cc
commit
0cc8a81320
@ -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):
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
107
openslides/core/migrations/0008_countdown_message.py
Normal file
107
openslides/core/migrations/0008_countdown_message.py
Normal 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),
|
||||
]
|
@ -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
|
||||
|
@ -1,11 +1,6 @@
|
||||
import uuid
|
||||
|
||||
from django.utils.timezone import now
|
||||
|
||||
from ..utils.projector import ProjectorElement
|
||||
from .config import config
|
||||
from .exceptions import ProjectorException
|
||||
from .models import Projector
|
||||
from .models import Countdown, ProjectorMessage
|
||||
|
||||
|
||||
class Clock(ProjectorElement):
|
||||
@ -15,134 +10,41 @@ class Clock(ProjectorElement):
|
||||
name = 'core/clock'
|
||||
|
||||
|
||||
class Countdown(ProjectorElement):
|
||||
class CountdownElement(ProjectorElement):
|
||||
"""
|
||||
Countdown on the projector.
|
||||
|
||||
To start the countdown write into the config field:
|
||||
|
||||
{
|
||||
"running": True,
|
||||
"countdown_time": <timestamp>,
|
||||
}
|
||||
|
||||
The timestamp is a POSIX timestamp (seconds) calculated from client
|
||||
time, server time offset and countdown duration (countdown_time = now -
|
||||
serverTimeOffset + duration).
|
||||
|
||||
To stop the countdown set the countdown time to the current value of the
|
||||
countdown (countdown_time = countdown_time - now + serverTimeOffset)
|
||||
and set running to False.
|
||||
|
||||
To reset the countdown (it is not a reset in a functional way) just
|
||||
change the countdown time. The running value remains False.
|
||||
|
||||
Do not forget to send values for additional keywords like "stable" if
|
||||
you do not want to use the default.
|
||||
|
||||
The countdown backend supports an extra keyword "default".
|
||||
|
||||
{
|
||||
"default": <seconds>
|
||||
}
|
||||
|
||||
This is used for the internal reset method if the countdown is coupled
|
||||
with the list of speakers. The default of this default value can be
|
||||
customized in OpenSlides config 'projector_default_countdown'.
|
||||
|
||||
Use additional keywords to control view behavior like "visable" and
|
||||
"label". These keywords are not handles by the backend.
|
||||
Countdown slide for the projector.
|
||||
"""
|
||||
name = 'core/countdown'
|
||||
|
||||
def check_data(self):
|
||||
self.validate_config(self.config_entry)
|
||||
if not Countdown.objects.filter(pk=self.config_entry.get('id')).exists():
|
||||
raise ProjectorException('Countdown does not exists.')
|
||||
|
||||
@classmethod
|
||||
def validate_config(cls, config_data):
|
||||
"""
|
||||
Raises ProjectorException if the given data are invalid.
|
||||
"""
|
||||
if not isinstance(config_data.get('countdown_time'), (int, float)):
|
||||
raise ProjectorException('Invalid countdown time. Use integer or float.')
|
||||
if not isinstance(config_data.get('running'), bool):
|
||||
raise ProjectorException("Invalid running status. Has to be a boolean.")
|
||||
if config_data.get('default') is not None and not isinstance(config_data.get('default'), int):
|
||||
raise ProjectorException('Invalid default value. Use integer.')
|
||||
|
||||
@classmethod
|
||||
def control(cls, action):
|
||||
if action not in ('start', 'stop', 'reset'):
|
||||
raise ValueError("Action must be 'start', 'stop' or 'reset', not {}.".format(action))
|
||||
|
||||
# Use the countdown with the lowest index
|
||||
projectors = Projector.objects.all()
|
||||
lowest_index = None
|
||||
if projectors[0]:
|
||||
for key, value in projectors[0].config.items():
|
||||
if value['name'] == cls.name:
|
||||
if lowest_index is None or value['index'] < lowest_index:
|
||||
lowest_index = value['index']
|
||||
|
||||
if lowest_index is None:
|
||||
# create a countdown
|
||||
for projector in projectors:
|
||||
projector_config = {}
|
||||
for key, value in projector.config.items():
|
||||
projector_config[key] = value
|
||||
# new countdown
|
||||
countdown = {
|
||||
'name': 'core/countdown',
|
||||
'stable': True,
|
||||
'index': 1,
|
||||
'default_time': config['projector_default_countdown'],
|
||||
'visible': False,
|
||||
'selected': True,
|
||||
}
|
||||
if action == 'start':
|
||||
countdown['running'] = True
|
||||
countdown['countdown_time'] = now().timestamp() + countdown['default_time']
|
||||
elif action == 'reset' or action == 'stop':
|
||||
countdown['running'] = False
|
||||
countdown['countdown_time'] = countdown['default_time']
|
||||
projector_config[uuid.uuid4().hex] = countdown
|
||||
projector.config = projector_config
|
||||
projector.save()
|
||||
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:
|
||||
def get_requirements(self, config_entry):
|
||||
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()
|
||||
countdown = Countdown.objects.get(pk=config_entry.get('id'))
|
||||
except Countdown.DoesNotExist:
|
||||
# Just do nothing if message does not exist
|
||||
pass
|
||||
else:
|
||||
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
|
||||
|
@ -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', )
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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) {}
|
||||
]);
|
||||
|
||||
}());
|
||||
|
@ -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
|
||||
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');
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
|
@ -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 () {
|
||||
intervals.forEach(function (interval) {
|
||||
$interval.cancel(interval);
|
||||
});
|
||||
};
|
||||
$scope.$watch(function () {
|
||||
return Countdown.lastModified();
|
||||
}, function () {
|
||||
$scope.countdowns = Countdown.getAll();
|
||||
|
||||
// stop ALL interval timer
|
||||
cancelIntervalTimers();
|
||||
$scope.countdowns.forEach(function (countdown) {
|
||||
$interval.cancel(countdown.interval);
|
||||
});
|
||||
};
|
||||
|
||||
// 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);
|
||||
if (countdown.running) {
|
||||
calculateCountdownTime(countdown);
|
||||
intervals.push($interval(function () { calculateCountdownTime(countdown); }, 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;
|
||||
}
|
||||
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,
|
||||
var countdown = {
|
||||
description: '',
|
||||
default_time: default_time,
|
||||
visible: false,
|
||||
selected: true,
|
||||
index: $scope.highestCountdownIndex,
|
||||
countdown_time: default_time,
|
||||
running: false,
|
||||
stable: true,
|
||||
}]);
|
||||
});
|
||||
};
|
||||
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 {
|
||||
// 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/message',
|
||||
name: 'core/projectormessage',
|
||||
stable: true,
|
||||
selected: true,
|
||||
visible: true,
|
||||
message: gettextCatalog.getString('Projector') + ' ' + projector.id + ': ' + projector.name,
|
||||
type: 'identify'
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
@ -141,7 +141,7 @@
|
||||
<h4 translate>Countdowns</h4>
|
||||
</a>
|
||||
<div uib-collapse="!isCountdowns" ng-cloak>
|
||||
<div ng-repeat="countdown in countdowns | orderBy: 'index'" id="countdown{{countdown.uuid}}" class="countdown panel panel-default">
|
||||
<div ng-repeat="countdown in countdowns | orderBy: 'index'" id="countdown{{countdown.id}}" class="countdown panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span ng-if="countdown.description">{{ countdown.description }}</span>
|
||||
<span ng-if="!countdown.description">Countdown {{ $index +1 }}</span>
|
||||
@ -153,63 +153,34 @@
|
||||
</button>
|
||||
<!-- edit countdown button -->
|
||||
<button type="button" class="close editicon"
|
||||
ng-click="countdown.editFlag=true;"
|
||||
ng-click="countdown.editFlag=true; countdown.new_description = countdown.description;"
|
||||
title="{{ 'Edit countdown' | translate}}">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-body"
|
||||
ng-class="{ 'projected': isProjected(countdown).length }">
|
||||
<!-- project countdown button -->
|
||||
<div class="btn-group" style="width:54px;" uib-dropdown>
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
ng-click="project(countdown)"
|
||||
ng-class="{ 'btn-primary': isProjected(countdown).length }">
|
||||
<i class="fa fa-video-camera"></i>
|
||||
</button>
|
||||
<button ng-if="projectors.length > 1" type="button" class="btn btn-default btn-sm slimDropDown" uib-dropdown-toggle>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li role="menuitem" ng-if="projectors.length > 1" style="text-align: center;">
|
||||
<span class="pointer" ng-click="selectAll(countdown, true); preventClose($event)" translate>
|
||||
All
|
||||
</span>
|
||||
| <span class="pointer" ng-click="selectAll(countdown, false); preventClose($event)" translate>
|
||||
None
|
||||
</span>
|
||||
</li>
|
||||
<li class="divider" ng-if="projectors.length > 1"></li>
|
||||
<li role="menuitem" ng-repeat="projector in projectors">
|
||||
<a href="" ng-click="toggleProjector(countdown, projector); preventClose($event)"
|
||||
ng-class="{ 'projected': isProjectedOn(countdown, projector) }">
|
||||
<i class="fa fa-square-o" ng-hide="hasProjector(countdown, projector)"></i>
|
||||
<i class="fa fa-check-square-o" ng-show="hasProjector(countdown, projector)"></i>
|
||||
{{ projector.name }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<projector-button model="countdown" default-projector-id="countdownDefaultProjectorId"></projector-button>
|
||||
|
||||
<!-- countdown controls -->
|
||||
<a class="btn btn-default vcenter"
|
||||
ng-click="resetCountdown(countdown)"
|
||||
ng-click="countdown.reset()"
|
||||
ng-class="{ 'disabled': !countdown.running && countdown.default_time == countdown.countdown_time }"
|
||||
title="{{ 'Reset countdown' | translate}}">
|
||||
<i class="fa fa-stop"></i>
|
||||
</a>
|
||||
<a ng-if="!countdown.running" class="btn btn-default vcenter"
|
||||
ng-click="startCountdown(countdown)"
|
||||
ng-click="countdown.start()"
|
||||
title="{{ 'Start' | translate}}">
|
||||
<i class="fa fa-play"></i>
|
||||
<i ng-if="countdown.running" class="fa fa-pause"></i>
|
||||
</a>
|
||||
<a ng-if="countdown.running" class="btn btn-default vcenter"
|
||||
ng-click="stopCountdown(countdown)"
|
||||
ng-click="countdown.stop()"
|
||||
title="{{ 'Pause' | translate}}">
|
||||
<i class="fa fa-pause"></i>
|
||||
</a>
|
||||
<span ng-if="!editTime" class="countdown_timer vcenter"
|
||||
<span ng-if="!countdown.editTime" class="countdown_timer vcenter"
|
||||
ng-class="{
|
||||
'negative': countdown.seconds <= 0,
|
||||
'warning': countdown.seconds <= config('agenda_countdown_warning_time') && countdown.seconds > 0 }">
|
||||
@ -220,7 +191,7 @@
|
||||
ng-submit="editCountdown(countdown)">
|
||||
<div class="form-group">
|
||||
<label translate>Description</label>
|
||||
<input ng-model="countdown.description" type="text" class="form-control input-sm">
|
||||
<input ng-model="countdown.new_description" type="text" class="form-control input-sm">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate>Start time</label>
|
||||
@ -281,39 +252,7 @@
|
||||
|
||||
<div class="panel-body"
|
||||
ng-class="{ 'projected': isProjected(message).length }">
|
||||
<div class="projectorbtn">
|
||||
<!-- project message button -->
|
||||
<div class="btn-group" style="width:54px;" uib-dropdown>
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
ng-click="project(message)"
|
||||
ng-class="{ 'btn-primary': isProjected(message).length }">
|
||||
<i class="fa fa-video-camera"></i>
|
||||
</button>
|
||||
<button type="button" ng-if="projectors.length > 1" class="btn btn-default btn-sm slimDropDown" uib-dropdown-toggle>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li role="menuitem" ng-if="projectors.length > 1" style="text-align: center;">
|
||||
<span class="pointer" ng-click="selectAll(message, true); preventClose($event)" translate>
|
||||
All
|
||||
</span>
|
||||
| <span class="pointer" ng-click="selectAll(message, false); preventClose($event)" translate>
|
||||
None
|
||||
</span>
|
||||
</li>
|
||||
<li class="divider" ng-if="projectors.length > 1"></li>
|
||||
<li role="menuitem" ng-repeat="projector in projectors">
|
||||
<a href="" ng-click="toggleProjector(message, projector); preventClose($event)"
|
||||
ng-class="{ 'projected': isProjectedOn(message, projector) }">
|
||||
<i class="fa fa-square-o" ng-hide="hasProjector(message, projector)"></i>
|
||||
<i class="fa fa-check-square-o" ng-show="hasProjector(message, projector)"></i>
|
||||
{{ projector.name }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<projector-button model="message" default-projector-id="messageDefaultProjectorId"></projector-button>
|
||||
|
||||
<div class="innermessage" ng-bind-html="message.message"> </div>
|
||||
|
||||
@ -344,7 +283,7 @@
|
||||
<h4 translate>List of speakers</h4>
|
||||
</a>
|
||||
<div uib-collapse="!isSpeakerList" ng-cloak>
|
||||
<projector-button model="listofspeakers" default-projector-id="defaultProjectorId">
|
||||
<projector-button model="listofspeakers" default-projector-id="listOfSpeakersDefaultProjectorId">
|
||||
</projector-button>
|
||||
<div class="btn-group" os-perms="agenda.can_manage">
|
||||
<a ng-click="goToListOfSpeakers()" class="btn btn-default btn-sm"
|
||||
|
@ -1,11 +1,9 @@
|
||||
<div ng-controller="SlideCountdownCtrl">
|
||||
<div ng-if="visible && selected">
|
||||
<div class="countdown well pull-right"
|
||||
ng-class="{
|
||||
'negative': seconds <= 0,
|
||||
'warning': seconds <= config('agenda_countdown_warning_time') && seconds > 0 }">
|
||||
{{ seconds | osSecondsToTime}}
|
||||
<div class="description">{{ description }}</div>
|
||||
</div>
|
||||
'negative': countdown.seconds <= 0,
|
||||
'warning': countdown.seconds <= config('agenda_countdown_warning_time') && countdown.seconds > 0 }">
|
||||
{{ countdown.seconds | osSecondsToTime}}
|
||||
<div class="description">{{ countdown.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div ng-controller="SlideMessageCtrl">
|
||||
<div ng-if="visible && selected" class="message_background"></div>
|
||||
<div ng-if="visible && selected" class="message well" ng-class="{'identify': type=='identify'}" ng-bind-html="message"></div>
|
||||
<div class="message_background"></div>
|
||||
<div class="message well" ng-class="{'identify': element.identify}" ng-bind-html="element.identify ? identifyMessage : message.message"></div>
|
||||
</div>
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import openslides.utils.models
|
||||
|
||||
|
||||
|
@ -19,7 +19,6 @@ from ..utils.rest_api import (
|
||||
detail_route,
|
||||
)
|
||||
from ..utils.views import APIView
|
||||
|
||||
from .access_permissions import (
|
||||
CategoryAccessPermissions,
|
||||
MotionAccessPermissions,
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user