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 django.utils.translation import ugettext_lazy
|
||||||
|
|
||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.core.projector import Countdown
|
from openslides.core.models import Countdown
|
||||||
from openslides.utils.exceptions import OpenSlidesError
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
from openslides.utils.models import RESTModelMixin
|
from openslides.utils.models import RESTModelMixin
|
||||||
from openslides.utils.utils import to_roman
|
from openslides.utils.utils import to_roman
|
||||||
@ -412,8 +412,12 @@ class Speaker(RESTModelMixin, models.Model):
|
|||||||
self.begin_time = timezone.now()
|
self.begin_time = timezone.now()
|
||||||
self.save()
|
self.save()
|
||||||
if config['agenda_couple_countdown_and_speakers']:
|
if config['agenda_couple_countdown_and_speakers']:
|
||||||
Countdown.control(action='reset')
|
countdown, created = Countdown.objects.get_or_create(pk=1, defaults={
|
||||||
Countdown.control(action='start')
|
'default_time': config['projector_default_countdown'],
|
||||||
|
'countdown_time': config['projector_default_countdown']})
|
||||||
|
if not created:
|
||||||
|
countdown.control(action='reset')
|
||||||
|
countdown.control(action='start')
|
||||||
|
|
||||||
def end_speech(self):
|
def end_speech(self):
|
||||||
"""
|
"""
|
||||||
@ -422,7 +426,12 @@ class Speaker(RESTModelMixin, models.Model):
|
|||||||
self.end_time = timezone.now()
|
self.end_time = timezone.now()
|
||||||
self.save()
|
self.save()
|
||||||
if config['agenda_couple_countdown_and_speakers']:
|
if config['agenda_couple_countdown_and_speakers']:
|
||||||
Countdown.control(action='stop')
|
try:
|
||||||
|
countdown = Countdown.objects.get(pk=1)
|
||||||
|
except Countdown.DoesNotExist:
|
||||||
|
pass # Do not create a new countdown on stop action
|
||||||
|
else:
|
||||||
|
countdown.control(action='stop')
|
||||||
|
|
||||||
def get_root_rest_element(self):
|
def get_root_rest_element(self):
|
||||||
"""
|
"""
|
||||||
|
@ -64,6 +64,44 @@ class ChatMessageAccessPermissions(BaseAccessPermissions):
|
|||||||
return ChatMessageSerializer
|
return ChatMessageSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectorMessageAccessPermissions(BaseAccessPermissions):
|
||||||
|
"""
|
||||||
|
Access permissions for ProjectorMessage.
|
||||||
|
"""
|
||||||
|
def check_permissions(self, user):
|
||||||
|
"""
|
||||||
|
Returns True if the user has read access model instances.
|
||||||
|
"""
|
||||||
|
return user.has_perm('core.can_see_projector')
|
||||||
|
|
||||||
|
def get_serializer_class(self, user=None):
|
||||||
|
"""
|
||||||
|
Returns serializer class.
|
||||||
|
"""
|
||||||
|
from .serializers import ProjectorMessageSerializer
|
||||||
|
|
||||||
|
return ProjectorMessageSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class CountdownAccessPermissions(BaseAccessPermissions):
|
||||||
|
"""
|
||||||
|
Access permissions for Countdown.
|
||||||
|
"""
|
||||||
|
def check_permissions(self, user):
|
||||||
|
"""
|
||||||
|
Returns True if the user has read access model instances.
|
||||||
|
"""
|
||||||
|
return user.has_perm('core.can_see_projector')
|
||||||
|
|
||||||
|
def get_serializer_class(self, user=None):
|
||||||
|
"""
|
||||||
|
Returns serializer class.
|
||||||
|
"""
|
||||||
|
from .serializers import CountdownSerializer
|
||||||
|
|
||||||
|
return CountdownSerializer
|
||||||
|
|
||||||
|
|
||||||
class ConfigAccessPermissions(BaseAccessPermissions):
|
class ConfigAccessPermissions(BaseAccessPermissions):
|
||||||
"""
|
"""
|
||||||
Access permissions container for the config (ConfigStore and
|
Access permissions container for the config (ConfigStore and
|
||||||
|
@ -19,10 +19,12 @@ class CoreAppConfig(AppConfig):
|
|||||||
from openslides.utils.rest_api import router
|
from openslides.utils.rest_api import router
|
||||||
from openslides.utils.search import index_add_instance, index_del_instance
|
from openslides.utils.search import index_add_instance, index_del_instance
|
||||||
from .config_variables import get_config_variables
|
from .config_variables import get_config_variables
|
||||||
from .signals import delete_django_app_permissions, create_builtin_projection_defaults
|
from .signals import delete_django_app_permissions
|
||||||
from .views import (
|
from .views import (
|
||||||
ChatMessageViewSet,
|
ChatMessageViewSet,
|
||||||
ConfigViewSet,
|
ConfigViewSet,
|
||||||
|
CountdownViewSet,
|
||||||
|
ProjectorMessageViewSet,
|
||||||
ProjectorViewSet,
|
ProjectorViewSet,
|
||||||
TagViewSet,
|
TagViewSet,
|
||||||
)
|
)
|
||||||
@ -34,15 +36,14 @@ class CoreAppConfig(AppConfig):
|
|||||||
post_permission_creation.connect(
|
post_permission_creation.connect(
|
||||||
delete_django_app_permissions,
|
delete_django_app_permissions,
|
||||||
dispatch_uid='delete_django_app_permissions')
|
dispatch_uid='delete_django_app_permissions')
|
||||||
post_permission_creation.connect(
|
|
||||||
create_builtin_projection_defaults,
|
|
||||||
dispatch_uid='create_builtin_projection_defaults')
|
|
||||||
|
|
||||||
# Register viewsets.
|
# Register viewsets.
|
||||||
router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet)
|
router.register(self.get_model('Projector').get_collection_string(), ProjectorViewSet)
|
||||||
router.register(self.get_model('ChatMessage').get_collection_string(), ChatMessageViewSet)
|
router.register(self.get_model('ChatMessage').get_collection_string(), ChatMessageViewSet)
|
||||||
router.register(self.get_model('Tag').get_collection_string(), TagViewSet)
|
router.register(self.get_model('Tag').get_collection_string(), TagViewSet)
|
||||||
router.register(self.get_model('ConfigStore').get_collection_string(), ConfigViewSet, 'config')
|
router.register(self.get_model('ConfigStore').get_collection_string(), ConfigViewSet, 'config')
|
||||||
|
router.register(self.get_model('ProjectorMessage').get_collection_string(), ProjectorMessageViewSet)
|
||||||
|
router.register(self.get_model('Countdown').get_collection_string(), CountdownViewSet)
|
||||||
|
|
||||||
# Update the search when a model is saved or deleted
|
# Update the search when a model is saved or deleted
|
||||||
signals.post_save.connect(
|
signals.post_save.connect(
|
||||||
|
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.conf import settings
|
||||||
from django.contrib.sessions.models import Session as DjangoSession
|
from django.contrib.sessions.models import Session as DjangoSession
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.timezone import now
|
||||||
from jsonfield import JSONField
|
from jsonfield import JSONField
|
||||||
|
|
||||||
from ..utils.collection import CollectionElement
|
from ..utils.collection import CollectionElement
|
||||||
@ -9,7 +10,9 @@ from ..utils.projector import ProjectorElement
|
|||||||
from .access_permissions import (
|
from .access_permissions import (
|
||||||
ChatMessageAccessPermissions,
|
ChatMessageAccessPermissions,
|
||||||
ConfigAccessPermissions,
|
ConfigAccessPermissions,
|
||||||
|
CountdownAccessPermissions,
|
||||||
ProjectorAccessPermissions,
|
ProjectorAccessPermissions,
|
||||||
|
ProjectorMessageAccessPermissions,
|
||||||
TagAccessPermissions,
|
TagAccessPermissions,
|
||||||
)
|
)
|
||||||
from .exceptions import ProjectorException
|
from .exceptions import ProjectorException
|
||||||
@ -294,6 +297,51 @@ class ChatMessage(RESTModelMixin, models.Model):
|
|||||||
return 'Message {}'.format(self.timestamp)
|
return 'Message {}'.format(self.timestamp)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectorMessage(RESTModelMixin, models.Model):
|
||||||
|
"""
|
||||||
|
Model for ProjectorMessages.
|
||||||
|
"""
|
||||||
|
access_permissions = ProjectorMessageAccessPermissions()
|
||||||
|
|
||||||
|
message = models.TextField(blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
|
||||||
|
|
||||||
|
class Countdown(RESTModelMixin, models.Model):
|
||||||
|
"""
|
||||||
|
Model for countdowns.
|
||||||
|
"""
|
||||||
|
access_permissions = CountdownAccessPermissions()
|
||||||
|
|
||||||
|
description = models.CharField(max_length=256, blank=True)
|
||||||
|
|
||||||
|
running = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
default_time = models.PositiveIntegerField(default=60)
|
||||||
|
|
||||||
|
countdown_time = models.FloatField(default=60)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
default_permissions = ()
|
||||||
|
|
||||||
|
def control(self, action):
|
||||||
|
if action not in ('start', 'stop', 'reset'):
|
||||||
|
raise ValueError("Action must be 'start', 'stop' or 'reset', not {}.".format(action))
|
||||||
|
|
||||||
|
if action == 'start':
|
||||||
|
self.running = True
|
||||||
|
self.countdown_time = now().timestamp() + self.default_time
|
||||||
|
elif action == 'stop' and self.running:
|
||||||
|
self.running = False
|
||||||
|
self.countdown_time = self.countdown_time - now().timestamp()
|
||||||
|
else: # reset
|
||||||
|
self.running = False
|
||||||
|
self.countdown_time = self.default_time
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
class Session(DjangoSession):
|
class Session(DjangoSession):
|
||||||
"""
|
"""
|
||||||
Model like the Django db session, which saves the user as ForeignKey instead
|
Model like the Django db session, which saves the user as ForeignKey instead
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
import uuid
|
|
||||||
|
|
||||||
from django.utils.timezone import now
|
|
||||||
|
|
||||||
from ..utils.projector import ProjectorElement
|
from ..utils.projector import ProjectorElement
|
||||||
from .config import config
|
|
||||||
from .exceptions import ProjectorException
|
from .exceptions import ProjectorException
|
||||||
from .models import Projector
|
from .models import Countdown, ProjectorMessage
|
||||||
|
|
||||||
|
|
||||||
class Clock(ProjectorElement):
|
class Clock(ProjectorElement):
|
||||||
@ -15,134 +10,41 @@ class Clock(ProjectorElement):
|
|||||||
name = 'core/clock'
|
name = 'core/clock'
|
||||||
|
|
||||||
|
|
||||||
class Countdown(ProjectorElement):
|
class CountdownElement(ProjectorElement):
|
||||||
"""
|
"""
|
||||||
Countdown on the projector.
|
Countdown slide for the projector.
|
||||||
|
|
||||||
To start the countdown write into the config field:
|
|
||||||
|
|
||||||
{
|
|
||||||
"running": True,
|
|
||||||
"countdown_time": <timestamp>,
|
|
||||||
}
|
|
||||||
|
|
||||||
The timestamp is a POSIX timestamp (seconds) calculated from client
|
|
||||||
time, server time offset and countdown duration (countdown_time = now -
|
|
||||||
serverTimeOffset + duration).
|
|
||||||
|
|
||||||
To stop the countdown set the countdown time to the current value of the
|
|
||||||
countdown (countdown_time = countdown_time - now + serverTimeOffset)
|
|
||||||
and set running to False.
|
|
||||||
|
|
||||||
To reset the countdown (it is not a reset in a functional way) just
|
|
||||||
change the countdown time. The running value remains False.
|
|
||||||
|
|
||||||
Do not forget to send values for additional keywords like "stable" if
|
|
||||||
you do not want to use the default.
|
|
||||||
|
|
||||||
The countdown backend supports an extra keyword "default".
|
|
||||||
|
|
||||||
{
|
|
||||||
"default": <seconds>
|
|
||||||
}
|
|
||||||
|
|
||||||
This is used for the internal reset method if the countdown is coupled
|
|
||||||
with the list of speakers. The default of this default value can be
|
|
||||||
customized in OpenSlides config 'projector_default_countdown'.
|
|
||||||
|
|
||||||
Use additional keywords to control view behavior like "visable" and
|
|
||||||
"label". These keywords are not handles by the backend.
|
|
||||||
"""
|
"""
|
||||||
name = 'core/countdown'
|
name = 'core/countdown'
|
||||||
|
|
||||||
def check_data(self):
|
def check_data(self):
|
||||||
self.validate_config(self.config_entry)
|
if not Countdown.objects.filter(pk=self.config_entry.get('id')).exists():
|
||||||
|
raise ProjectorException('Countdown does not exists.')
|
||||||
|
|
||||||
@classmethod
|
def get_requirements(self, config_entry):
|
||||||
def validate_config(cls, config_data):
|
try:
|
||||||
"""
|
countdown = Countdown.objects.get(pk=config_entry.get('id'))
|
||||||
Raises ProjectorException if the given data are invalid.
|
except Countdown.DoesNotExist:
|
||||||
"""
|
# Just do nothing if message does not exist
|
||||||
if not isinstance(config_data.get('countdown_time'), (int, float)):
|
pass
|
||||||
raise ProjectorException('Invalid countdown time. Use integer or float.')
|
|
||||||
if not isinstance(config_data.get('running'), bool):
|
|
||||||
raise ProjectorException("Invalid running status. Has to be a boolean.")
|
|
||||||
if config_data.get('default') is not None and not isinstance(config_data.get('default'), int):
|
|
||||||
raise ProjectorException('Invalid default value. Use integer.')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def control(cls, action):
|
|
||||||
if action not in ('start', 'stop', 'reset'):
|
|
||||||
raise ValueError("Action must be 'start', 'stop' or 'reset', not {}.".format(action))
|
|
||||||
|
|
||||||
# Use the countdown with the lowest index
|
|
||||||
projectors = Projector.objects.all()
|
|
||||||
lowest_index = None
|
|
||||||
if projectors[0]:
|
|
||||||
for key, value in projectors[0].config.items():
|
|
||||||
if value['name'] == cls.name:
|
|
||||||
if lowest_index is None or value['index'] < lowest_index:
|
|
||||||
lowest_index = value['index']
|
|
||||||
|
|
||||||
if lowest_index is None:
|
|
||||||
# create a countdown
|
|
||||||
for projector in projectors:
|
|
||||||
projector_config = {}
|
|
||||||
for key, value in projector.config.items():
|
|
||||||
projector_config[key] = value
|
|
||||||
# new countdown
|
|
||||||
countdown = {
|
|
||||||
'name': 'core/countdown',
|
|
||||||
'stable': True,
|
|
||||||
'index': 1,
|
|
||||||
'default_time': config['projector_default_countdown'],
|
|
||||||
'visible': False,
|
|
||||||
'selected': True,
|
|
||||||
}
|
|
||||||
if action == 'start':
|
|
||||||
countdown['running'] = True
|
|
||||||
countdown['countdown_time'] = now().timestamp() + countdown['default_time']
|
|
||||||
elif action == 'reset' or action == 'stop':
|
|
||||||
countdown['running'] = False
|
|
||||||
countdown['countdown_time'] = countdown['default_time']
|
|
||||||
projector_config[uuid.uuid4().hex] = countdown
|
|
||||||
projector.config = projector_config
|
|
||||||
projector.save()
|
|
||||||
else:
|
else:
|
||||||
# search for the countdown and modify it.
|
yield countdown
|
||||||
for projector in projectors:
|
|
||||||
projector_config = {}
|
|
||||||
found = False
|
|
||||||
for key, value in projector.config.items():
|
|
||||||
if value['name'] == cls.name and value['index'] == lowest_index:
|
|
||||||
try:
|
|
||||||
cls.validate_config(value)
|
|
||||||
except ProjectorException:
|
|
||||||
# Do not proceed if the specific procjector config data is invalid.
|
|
||||||
# The variable found remains False.
|
|
||||||
break
|
|
||||||
found = True
|
|
||||||
if action == 'start':
|
|
||||||
value['running'] = True
|
|
||||||
value['countdown_time'] = now().timestamp() + value['default_time']
|
|
||||||
elif action == 'stop' and value['running']:
|
|
||||||
value['running'] = False
|
|
||||||
value['countdown_time'] = value['countdown_time'] - now().timestamp()
|
|
||||||
elif action == 'reset':
|
|
||||||
value['running'] = False
|
|
||||||
value['countdown_time'] = value['default_time']
|
|
||||||
projector_config[key] = value
|
|
||||||
if found:
|
|
||||||
projector.config = projector_config
|
|
||||||
projector.save()
|
|
||||||
|
|
||||||
|
|
||||||
class Message(ProjectorElement):
|
class ProjectorMessageElement(ProjectorElement):
|
||||||
"""
|
"""
|
||||||
Short message on the projector. Rendered as overlay.
|
Short message on the projector. Rendered as overlay.
|
||||||
"""
|
"""
|
||||||
name = 'core/message'
|
name = 'core/projectormessage'
|
||||||
|
|
||||||
def check_data(self):
|
def check_data(self):
|
||||||
if self.config_entry.get('message') is None:
|
if not ProjectorMessage.objects.filter(pk=self.config_entry.get('id')).exists():
|
||||||
raise ProjectorException('No message given.')
|
raise ProjectorException('Message does not exists.')
|
||||||
|
|
||||||
|
def get_requirements(self, config_entry):
|
||||||
|
try:
|
||||||
|
message = ProjectorMessage.objects.get(pk=config_entry.get('id'))
|
||||||
|
except ProjectorMessage.DoesNotExist:
|
||||||
|
# Just do nothing if message does not exist
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
yield message
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
from openslides.utils.rest_api import Field, ModelSerializer, ValidationError
|
from openslides.utils.rest_api import Field, ModelSerializer, ValidationError
|
||||||
|
|
||||||
from .models import ChatMessage, ProjectionDefault, Projector, Tag
|
from .models import (
|
||||||
|
ChatMessage,
|
||||||
|
Countdown,
|
||||||
|
ProjectionDefault,
|
||||||
|
Projector,
|
||||||
|
ProjectorMessage,
|
||||||
|
Tag,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class JSONSerializerField(Field):
|
class JSONSerializerField(Field):
|
||||||
@ -61,3 +68,21 @@ class ChatMessageSerializer(ModelSerializer):
|
|||||||
model = ChatMessage
|
model = ChatMessage
|
||||||
fields = ('id', 'message', 'timestamp', 'user', )
|
fields = ('id', 'message', 'timestamp', 'user', )
|
||||||
read_only_fields = ('user', )
|
read_only_fields = ('user', )
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectorMessageSerializer(ModelSerializer):
|
||||||
|
"""
|
||||||
|
Serializer for core.models.ProjectorMessage objects.
|
||||||
|
"""
|
||||||
|
class Meta:
|
||||||
|
model = ProjectorMessage
|
||||||
|
fields = ('id', 'message', )
|
||||||
|
|
||||||
|
|
||||||
|
class CountdownSerializer(ModelSerializer):
|
||||||
|
"""
|
||||||
|
Serializer for core.models.Countdown objects.
|
||||||
|
"""
|
||||||
|
class Meta:
|
||||||
|
model = Countdown
|
||||||
|
fields = ('id', 'description', 'default_time', 'countdown_time', 'running', )
|
||||||
|
@ -3,8 +3,6 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.dispatch import Signal
|
from django.dispatch import Signal
|
||||||
|
|
||||||
from .models import ProjectionDefault, Projector
|
|
||||||
|
|
||||||
# This signal is sent when the migrate command is done. That means it is sent
|
# This signal is sent when the migrate command is done. That means it is sent
|
||||||
# after post_migrate sending and creating all Permission objects. Don't use it
|
# after post_migrate sending and creating all Permission objects. Don't use it
|
||||||
# for other things than dealing with Permission objects.
|
# for other things than dealing with Permission objects.
|
||||||
@ -21,62 +19,3 @@ def delete_django_app_permissions(sender, **kwargs):
|
|||||||
Q(app_label='contenttypes') |
|
Q(app_label='contenttypes') |
|
||||||
Q(app_label='sessions'))
|
Q(app_label='sessions'))
|
||||||
Permission.objects.filter(content_type__in=contenttypes).delete()
|
Permission.objects.filter(content_type__in=contenttypes).delete()
|
||||||
|
|
||||||
|
|
||||||
def create_builtin_projection_defaults(**kwargs):
|
|
||||||
"""
|
|
||||||
Creates the builtin defaults:
|
|
||||||
- agenda_all_items, agenda_list_of_speakers, agenda_current_list_of_speakers
|
|
||||||
- topics
|
|
||||||
- assignments
|
|
||||||
- mediafiles
|
|
||||||
- motion
|
|
||||||
- users
|
|
||||||
|
|
||||||
These strings have to be used in the controllers where you want to
|
|
||||||
define a projector button. Use the string to get the id of the
|
|
||||||
responsible projector and pass this id to the projector button directive.
|
|
||||||
"""
|
|
||||||
# Check whether ProjectionDefault objects exist.
|
|
||||||
if ProjectionDefault.objects.all().exists():
|
|
||||||
# Do completely nothing if some defaults are already in the database.
|
|
||||||
return
|
|
||||||
|
|
||||||
default_projector = Projector.objects.get(pk=1)
|
|
||||||
|
|
||||||
ProjectionDefault.objects.create(
|
|
||||||
name='agenda_all_items',
|
|
||||||
display_name='Agenda',
|
|
||||||
projector=default_projector)
|
|
||||||
ProjectionDefault.objects.create(
|
|
||||||
name='topics',
|
|
||||||
display_name='Topics',
|
|
||||||
projector=default_projector)
|
|
||||||
ProjectionDefault.objects.create(
|
|
||||||
name='agenda_list_of_speakers',
|
|
||||||
display_name='List of speakers',
|
|
||||||
projector=default_projector)
|
|
||||||
ProjectionDefault.objects.create(
|
|
||||||
name='agenda_current_list_of_speakers',
|
|
||||||
display_name='Current list of speakers',
|
|
||||||
projector=default_projector)
|
|
||||||
ProjectionDefault.objects.create(
|
|
||||||
name='motions',
|
|
||||||
display_name='Motions',
|
|
||||||
projector=default_projector)
|
|
||||||
ProjectionDefault.objects.create(
|
|
||||||
name='motionBlocks',
|
|
||||||
display_name='Motion Blocks',
|
|
||||||
projector=default_projector)
|
|
||||||
ProjectionDefault.objects.create(
|
|
||||||
name='assignments',
|
|
||||||
display_name='Elections',
|
|
||||||
projector=default_projector)
|
|
||||||
ProjectionDefault.objects.create(
|
|
||||||
name='users',
|
|
||||||
display_name='Participants',
|
|
||||||
projector=default_projector)
|
|
||||||
ProjectionDefault.objects.create(
|
|
||||||
name='mediafiles',
|
|
||||||
display_name='Files',
|
|
||||||
projector=default_projector)
|
|
||||||
|
@ -86,9 +86,7 @@ img {
|
|||||||
margin: 0 auto 0 auto;
|
margin: 0 auto 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Header **/
|
/** Header **/
|
||||||
|
|
||||||
#header {
|
#header {
|
||||||
float: left;
|
float: left;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -799,7 +797,7 @@ img {
|
|||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col2 .message .projectorbtn {
|
.col2 .message projector-button {
|
||||||
float: left;
|
float: left;
|
||||||
width: auto;
|
width: auto;
|
||||||
margin: 5px 10px 5px 0px;
|
margin: 5px 10px 5px 0px;
|
||||||
|
@ -287,12 +287,16 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
'ChatMessage',
|
'ChatMessage',
|
||||||
'Config',
|
'Config',
|
||||||
'Projector',
|
'Projector',
|
||||||
function (ChatMessage, Config, Projector) {
|
'ProjectorMessage',
|
||||||
|
'Countdown',
|
||||||
|
function (ChatMessage, Config, Projector, ProjectorMessage, Countdown) {
|
||||||
return function () {
|
return function () {
|
||||||
Config.findAll();
|
Config.findAll();
|
||||||
|
|
||||||
// Loads all projector data and the projectiondefaults
|
// Loads all projector data and the projectiondefaults
|
||||||
Projector.findAll();
|
Projector.findAll();
|
||||||
|
ProjectorMessage.findAll();
|
||||||
|
Countdown.findAll();
|
||||||
|
|
||||||
// Loads all chat messages data and their user_ids
|
// Loads all chat messages data and their user_ids
|
||||||
// TODO: add permission check if user has required chat permission
|
// TODO: add permission check if user has required chat permission
|
||||||
@ -640,6 +644,112 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
/* Model for ProjectorMessages */
|
||||||
|
.factory('ProjectorMessage', [
|
||||||
|
'DS',
|
||||||
|
'jsDataModel',
|
||||||
|
'gettext',
|
||||||
|
'$http',
|
||||||
|
'Projector',
|
||||||
|
function(DS, jsDataModel, gettext, $http, Projector) {
|
||||||
|
var name = 'core/projectormessage';
|
||||||
|
return DS.defineResource({
|
||||||
|
name: name,
|
||||||
|
useClass: jsDataModel,
|
||||||
|
verboseName: gettext('Message'),
|
||||||
|
verbosenamePlural: gettext('Messages'),
|
||||||
|
methods: {
|
||||||
|
getResourceName: function () {
|
||||||
|
return name;
|
||||||
|
},
|
||||||
|
// Override the BaseModel.project function
|
||||||
|
project: function(projectorId) {
|
||||||
|
// if this object is already projected on projectorId, delete this element from this projector
|
||||||
|
var isProjectedIds = this.isProjected();
|
||||||
|
var self = this;
|
||||||
|
var predicate = function (element) {
|
||||||
|
return element.name == name && element.id == self.id;
|
||||||
|
};
|
||||||
|
_.forEach(isProjectedIds, function (id) {
|
||||||
|
var uuid = _.findKey(Projector.get(id).elements, predicate);
|
||||||
|
$http.post('/rest/core/projector/' + id + '/deactivate_elements/', [uuid]);
|
||||||
|
});
|
||||||
|
// if it was the same projector before, just delete it but not show again
|
||||||
|
if (_.indexOf(isProjectedIds, projectorId) == -1) {
|
||||||
|
return $http.post(
|
||||||
|
'/rest/core/projector/' + projectorId + '/activate_elements/',
|
||||||
|
[{name: name, id: self.id, stable: true}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
/* Model for Countdowns */
|
||||||
|
.factory('Countdown', [
|
||||||
|
'DS',
|
||||||
|
'jsDataModel',
|
||||||
|
'gettext',
|
||||||
|
'$rootScope',
|
||||||
|
'$http',
|
||||||
|
'Projector',
|
||||||
|
function(DS, jsDataModel, gettext, $rootScope, $http, Projector) {
|
||||||
|
var name = 'core/countdown';
|
||||||
|
return DS.defineResource({
|
||||||
|
name: name,
|
||||||
|
useClass: jsDataModel,
|
||||||
|
verboseName: gettext('Countdown'),
|
||||||
|
verbosenamePlural: gettext('Countdowns'),
|
||||||
|
methods: {
|
||||||
|
getResourceName: function () {
|
||||||
|
return name;
|
||||||
|
},
|
||||||
|
start: function () {
|
||||||
|
// calculate end point of countdown (in seconds!)
|
||||||
|
var endTimestamp = Date.now() / 1000 - $rootScope.serverOffset + this.countdown_time;
|
||||||
|
this.running = true;
|
||||||
|
this.countdown_time = endTimestamp;
|
||||||
|
DS.save(name, this.id);
|
||||||
|
},
|
||||||
|
stop: function () {
|
||||||
|
// calculate rest duration of countdown (in seconds!)
|
||||||
|
var newDuration = Math.floor( this.countdown_time - Date.now() / 1000 + $rootScope.serverOffset );
|
||||||
|
this.running = false;
|
||||||
|
this.countdown_time = newDuration;
|
||||||
|
DS.save(name, this.id);
|
||||||
|
},
|
||||||
|
reset: function () {
|
||||||
|
this.running = false;
|
||||||
|
this.countdown_time = this.default_time;
|
||||||
|
DS.save(name, this.id);
|
||||||
|
},
|
||||||
|
// Override the BaseModel.project function
|
||||||
|
project: function(projectorId) {
|
||||||
|
// if this object is already projected on projectorId, delete this element from this projector
|
||||||
|
var isProjectedIds = this.isProjected();
|
||||||
|
var self = this;
|
||||||
|
var predicate = function (element) {
|
||||||
|
return element.name == name && element.id == self.id;
|
||||||
|
};
|
||||||
|
_.forEach(isProjectedIds, function (id) {
|
||||||
|
var uuid = _.findKey(Projector.get(id).elements, predicate);
|
||||||
|
$http.post('/rest/core/projector/' + id + '/deactivate_elements/', [uuid]);
|
||||||
|
});
|
||||||
|
// if it was the same projector before, just delete it but not show again
|
||||||
|
if (_.indexOf(isProjectedIds, projectorId) == -1) {
|
||||||
|
return $http.post(
|
||||||
|
'/rest/core/projector/' + projectorId + '/activate_elements/',
|
||||||
|
[{name: name, id: self.id, stable: true}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
/* Converts number of seconds into string "h:mm:ss" or "mm:ss" */
|
/* Converts number of seconds into string "h:mm:ss" or "mm:ss" */
|
||||||
.filter('osSecondsToTime', [
|
.filter('osSecondsToTime', [
|
||||||
function () {
|
function () {
|
||||||
@ -733,10 +843,12 @@ angular.module('OpenSlidesApp.core', [
|
|||||||
.run([
|
.run([
|
||||||
'ChatMessage',
|
'ChatMessage',
|
||||||
'Config',
|
'Config',
|
||||||
|
'Countdown',
|
||||||
|
'ProjectorMessage',
|
||||||
'Projector',
|
'Projector',
|
||||||
'ProjectionDefault',
|
'ProjectionDefault',
|
||||||
'Tag',
|
'Tag',
|
||||||
function (ChatMessage, Config, Projector, ProjectionDefault, Tag) {}
|
function (ChatMessage, Config, Countdown, ProjectorMessage, Projector, ProjectionDefault, Tag) {}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
}());
|
}());
|
||||||
|
@ -50,7 +50,7 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
|
|||||||
template: 'static/templates/core/slide_countdown.html',
|
template: 'static/templates/core/slide_countdown.html',
|
||||||
});
|
});
|
||||||
|
|
||||||
slidesProvider.registerSlide('core/message', {
|
slidesProvider.registerSlide('core/projectormessage', {
|
||||||
template: 'static/templates/core/slide_message.html',
|
template: 'static/templates/core/slide_message.html',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -214,42 +214,60 @@ angular.module('OpenSlidesApp.core.projector', ['OpenSlidesApp.core'])
|
|||||||
.controller('SlideCountdownCtrl', [
|
.controller('SlideCountdownCtrl', [
|
||||||
'$scope',
|
'$scope',
|
||||||
'$interval',
|
'$interval',
|
||||||
function($scope, $interval) {
|
'Countdown',
|
||||||
|
function($scope, $interval, Countdown) {
|
||||||
// Attention! Each object that is used here has to be dealt on server side.
|
// Attention! Each object that is used here has to be dealt on server side.
|
||||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
// Add it to the coresponding get_requirements method of the ProjectorElement
|
||||||
// class.
|
// class.
|
||||||
$scope.seconds = Math.floor( $scope.element.countdown_time - Date.now() / 1000 + $scope.serverOffset );
|
var id = $scope.element.id;
|
||||||
$scope.running = $scope.element.running;
|
|
||||||
$scope.visible = $scope.element.visible;
|
|
||||||
$scope.selected = $scope.element.selected;
|
|
||||||
$scope.index = $scope.element.index;
|
|
||||||
$scope.description = $scope.element.description;
|
|
||||||
// start interval timer if countdown status is running
|
|
||||||
var interval;
|
var interval;
|
||||||
if ($scope.running) {
|
var calculateCountdownTime = function (countdown) {
|
||||||
interval = $interval( function() {
|
countdown.seconds = Math.floor( $scope.countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset );
|
||||||
$scope.seconds = Math.floor( $scope.element.countdown_time - Date.now() / 1000 + $scope.serverOffset );
|
};
|
||||||
}, 1000);
|
$scope.$watch(function () {
|
||||||
} else {
|
return Countdown.lastModified(id);
|
||||||
$scope.seconds = $scope.element.countdown_time;
|
}, function () {
|
||||||
}
|
$scope.countdown = Countdown.get(id);
|
||||||
|
if (interval) {
|
||||||
|
$interval.cancel(interval);
|
||||||
|
}
|
||||||
|
if ($scope.countdown) {
|
||||||
|
if ($scope.countdown.running) {
|
||||||
|
calculateCountdownTime($scope.countdown);
|
||||||
|
interval = $interval(function () { calculateCountdownTime($scope.countdown); }, 1000);
|
||||||
|
} else {
|
||||||
|
$scope.countdown.seconds = $scope.countdown.countdown_time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
$scope.$on('$destroy', function() {
|
$scope.$on('$destroy', function() {
|
||||||
// Cancel the interval if the controller is destroyed
|
// Cancel the interval if the controller is destroyed
|
||||||
$interval.cancel(interval);
|
if (interval) {
|
||||||
|
$interval.cancel(interval);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
.controller('SlideMessageCtrl', [
|
.controller('SlideMessageCtrl', [
|
||||||
'$scope',
|
'$scope',
|
||||||
function($scope) {
|
'ProjectorMessage',
|
||||||
|
'Projector',
|
||||||
|
'ProjectorID',
|
||||||
|
'gettextCatalog',
|
||||||
|
function($scope, ProjectorMessage, Projector, ProjectorID, gettextCatalog) {
|
||||||
// Attention! Each object that is used here has to be dealt on server side.
|
// Attention! Each object that is used here has to be dealt on server side.
|
||||||
// Add it to the coresponding get_requirements method of the ProjectorElement
|
// Add it to the coresponding get_requirements method of the ProjectorElement
|
||||||
// class.
|
// class.
|
||||||
$scope.message = $scope.element.message;
|
var id = $scope.element.id;
|
||||||
$scope.visible = $scope.element.visible;
|
|
||||||
$scope.selected = $scope.element.selected;
|
if ($scope.element.identify) {
|
||||||
$scope.type = $scope.element.type;
|
var projector = Projector.get(ProjectorID());
|
||||||
|
$scope.identifyMessage = gettextCatalog.getString('Projector') + ' ' + projector.id + ': ' + projector.name;
|
||||||
|
} else {
|
||||||
|
$scope.message = ProjectorMessage.get(id);
|
||||||
|
ProjectorMessage.bindOne(id, $scope, 'message');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -805,50 +805,44 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
'CurrentListOfSpeakersItem',
|
'CurrentListOfSpeakersItem',
|
||||||
'ListOfSpeakersOverlay',
|
'ListOfSpeakersOverlay',
|
||||||
'ProjectionDefault',
|
'ProjectionDefault',
|
||||||
function($scope, $http, $interval, $state, $q, Config, Projector, CurrentListOfSpeakersItem, ListOfSpeakersOverlay, ProjectionDefault) {
|
'ProjectorMessage',
|
||||||
$scope.countdowns = [];
|
'Countdown',
|
||||||
$scope.highestCountdownIndex = 0;
|
'gettextCatalog',
|
||||||
$scope.messages = [];
|
function($scope, $http, $interval, $state, $q, Config, Projector, CurrentListOfSpeakersItem,
|
||||||
$scope.highestMessageIndex = 0;
|
ListOfSpeakersOverlay, ProjectionDefault, ProjectorMessage, Countdown, gettextCatalog) {
|
||||||
$scope.listofspeakers = ListOfSpeakersOverlay;
|
ProjectorMessage.bindAll({}, $scope, 'messages');
|
||||||
|
|
||||||
|
var intervals = [];
|
||||||
|
var calculateCountdownTime = function (countdown) {
|
||||||
|
countdown.seconds = Math.floor( countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset );
|
||||||
|
};
|
||||||
var cancelIntervalTimers = function () {
|
var cancelIntervalTimers = function () {
|
||||||
$scope.countdowns.forEach(function (countdown) {
|
intervals.forEach(function (interval) {
|
||||||
$interval.cancel(countdown.interval);
|
$interval.cancel(interval);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
$scope.$watch(function () {
|
||||||
|
return Countdown.lastModified();
|
||||||
|
}, function () {
|
||||||
|
$scope.countdowns = Countdown.getAll();
|
||||||
|
|
||||||
// Get all message and countdown data from the defaultprojector (id=1)
|
// stop ALL interval timer
|
||||||
var rebuildAllElements = function () {
|
cancelIntervalTimers();
|
||||||
$scope.countdowns = [];
|
$scope.countdowns.forEach(function (countdown) {
|
||||||
$scope.messages = [];
|
if (countdown.running) {
|
||||||
|
calculateCountdownTime(countdown);
|
||||||
_.forEach(Projector.get(1).elements, function (element, uuid) {
|
intervals.push($interval(function () { calculateCountdownTime(countdown); }, 1000));
|
||||||
if (element.name == 'core/countdown') {
|
} else {
|
||||||
$scope.countdowns.push(element);
|
countdown.seconds = countdown.countdown_time;
|
||||||
|
|
||||||
if (element.running) {
|
|
||||||
// calculate remaining seconds directly because interval starts with 1 second delay
|
|
||||||
$scope.calculateCountdownTime(element);
|
|
||||||
// start interval timer (every second)
|
|
||||||
element.interval = $interval(function () { $scope.calculateCountdownTime(element); }, 1000);
|
|
||||||
} else {
|
|
||||||
element.seconds = element.countdown_time;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.index > $scope.highestCountdownIndex) {
|
|
||||||
$scope.highestCountdownIndex = element.index;
|
|
||||||
}
|
|
||||||
} else if (element.name == 'core/message') {
|
|
||||||
$scope.messages.push(element);
|
|
||||||
|
|
||||||
if (element.index > $scope.highestMessageIndex) {
|
|
||||||
$scope.highestMessageIndex = element.index;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
|
$scope.$on('$destroy', function() {
|
||||||
|
// Cancel all intervals if the controller is destroyed
|
||||||
|
cancelIntervalTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.listofspeakers = ListOfSpeakersOverlay;
|
||||||
$scope.$watch(function () {
|
$scope.$watch(function () {
|
||||||
return Projector.lastModified();
|
return Projector.lastModified();
|
||||||
}, function () {
|
}, function () {
|
||||||
@ -856,26 +850,20 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
if (!$scope.active_projector) {
|
if (!$scope.active_projector) {
|
||||||
$scope.changeProjector($scope.projectors[0]);
|
$scope.changeProjector($scope.projectors[0]);
|
||||||
}
|
}
|
||||||
$scope.getDefaultOverlayProjector();
|
|
||||||
// stop ALL interval timer
|
|
||||||
cancelIntervalTimers();
|
|
||||||
|
|
||||||
rebuildAllElements();
|
$scope.messageDefaultProjectorId = ProjectionDefault.filter({name: 'messages'})[0].projector_id;
|
||||||
|
$scope.countdownDefaultProjectorId = ProjectionDefault.filter({name: 'countdowns'})[0].projector_id;
|
||||||
|
$scope.getDefaultOverlayProjector();
|
||||||
});
|
});
|
||||||
// gets the default projector where the current list of speakers overlay will be displayed
|
// gets the default projector where the current list of speakers overlay will be displayed
|
||||||
$scope.getDefaultOverlayProjector = function () {
|
$scope.getDefaultOverlayProjector = function () {
|
||||||
var projectiondefault = ProjectionDefault.filter({name: 'agenda_current_list_of_speakers'})[0];
|
var projectiondefault = ProjectionDefault.filter({name: 'agenda_current_list_of_speakers'})[0];
|
||||||
if (projectiondefault) {
|
if (projectiondefault) {
|
||||||
$scope.defaultProjectorId = projectiondefault.projector_id;
|
$scope.listofSpeakersDefaultProjectorId = projectiondefault.projector_id;
|
||||||
} else {
|
} else {
|
||||||
$scope.defaultProjectorId = 1;
|
$scope.listOfSpeakersDefaultProjectorId = 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
$scope.$on('$destroy', function() {
|
|
||||||
// Cancel all intervals if the controller is destroyed
|
|
||||||
cancelIntervalTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
// watch for changes in projector_broadcast and currentListOfSpeakersReference
|
// watch for changes in projector_broadcast and currentListOfSpeakersReference
|
||||||
var last_broadcast;
|
var last_broadcast;
|
||||||
$scope.$watch(function () {
|
$scope.$watch(function () {
|
||||||
@ -908,205 +896,45 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
};
|
};
|
||||||
$scope.editCountdown = function (countdown) {
|
$scope.editCountdown = function (countdown) {
|
||||||
countdown.editFlag = false;
|
countdown.editFlag = false;
|
||||||
$scope.projectors.forEach(function (projector) {
|
countdown.description = countdown.new_description;
|
||||||
_.forEach(projector.elements, function (element, uuid) {
|
Countdown.save(countdown);
|
||||||
if (element.name == 'core/countdown' && element.index == countdown.index) {
|
|
||||||
var data = {};
|
|
||||||
data[uuid] = {
|
|
||||||
"description": countdown.description,
|
|
||||||
"default_time": parseInt(countdown.default_time)
|
|
||||||
};
|
|
||||||
if (!countdown.running) {
|
|
||||||
data[uuid].countdown_time = parseInt(countdown.default_time);
|
|
||||||
}
|
|
||||||
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
$scope.addCountdown = function () {
|
$scope.addCountdown = function () {
|
||||||
var default_time = parseInt($scope.config('projector_default_countdown'));
|
var default_time = parseInt($scope.config('projector_default_countdown'));
|
||||||
$scope.highestCountdownIndex++;
|
var countdown = {
|
||||||
// select all projectors on creation, so write the countdown to all projectors
|
description: '',
|
||||||
$scope.projectors.forEach(function (projector) {
|
default_time: default_time,
|
||||||
$http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{
|
countdown_time: default_time,
|
||||||
name: 'core/countdown',
|
running: false,
|
||||||
countdown_time: default_time,
|
};
|
||||||
default_time: default_time,
|
Countdown.create(countdown);
|
||||||
visible: false,
|
|
||||||
selected: true,
|
|
||||||
index: $scope.highestCountdownIndex,
|
|
||||||
running: false,
|
|
||||||
stable: true,
|
|
||||||
}]);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
$scope.removeCountdown = function (countdown) {
|
$scope.removeCountdown = function (countdown) {
|
||||||
$scope.projectors.forEach(function (projector) {
|
var isProjectedIds = countdown.isProjected();
|
||||||
var countdowns = [];
|
_.forEach(isProjectedIds, function(id) {
|
||||||
_.forEach(projector.elements, function (element, uuid) {
|
countdown.project(id);
|
||||||
if (element.name == 'core/countdown' && element.index == countdown.index) {
|
|
||||||
$http.post('/rest/core/projector/' + projector.id + '/deactivate_elements/', [uuid]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
$scope.startCountdown = function (countdown) {
|
|
||||||
$scope.projectors.forEach(function (projector) {
|
|
||||||
_.forEach(projector.elements, function (element, uuid) {
|
|
||||||
if (element.name == 'core/countdown' && element.index == countdown.index) {
|
|
||||||
var data = {};
|
|
||||||
// calculate end point of countdown (in seconds!)
|
|
||||||
var endTimestamp = Date.now() / 1000 - $scope.serverOffset + countdown.countdown_time;
|
|
||||||
data[uuid] = {
|
|
||||||
'running': true,
|
|
||||||
'countdown_time': endTimestamp
|
|
||||||
};
|
|
||||||
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
$scope.stopCountdown = function (countdown) {
|
|
||||||
$scope.projectors.forEach(function (projector) {
|
|
||||||
_.forEach(projector.elements, function (element, uuid) {
|
|
||||||
if (element.name == 'core/countdown' && element.index == countdown.index) {
|
|
||||||
var data = {};
|
|
||||||
// calculate rest duration of countdown (in seconds!)
|
|
||||||
var newDuration = Math.floor( countdown.countdown_time - Date.now() / 1000 + $scope.serverOffset );
|
|
||||||
data[uuid] = {
|
|
||||||
'running': false,
|
|
||||||
'countdown_time': newDuration
|
|
||||||
};
|
|
||||||
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
$scope.resetCountdown = function (countdown) {
|
|
||||||
$scope.projectors.forEach(function (projector) {
|
|
||||||
_.forEach(projector.elements, function (element, uuid) {
|
|
||||||
if (element.name == 'core/countdown' && element.index == countdown.index) {
|
|
||||||
var data = {};
|
|
||||||
data[uuid] = {
|
|
||||||
'running': false,
|
|
||||||
'countdown_time': countdown.default_time,
|
|
||||||
};
|
|
||||||
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
Countdown.destroy(countdown.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
// *** message functions ***
|
// *** message functions ***
|
||||||
$scope.editMessage = function (message) {
|
$scope.editMessage = function (message) {
|
||||||
message.editFlag = false;
|
message.editFlag = false;
|
||||||
$scope.projectors.forEach(function (projector) {
|
ProjectorMessage.save(message);
|
||||||
_.forEach(projector.elements, function (element, uuid) {
|
|
||||||
if (element.name == 'core/message' && element.index == message.index) {
|
|
||||||
var data = {};
|
|
||||||
data[uuid] = {
|
|
||||||
message: message.message,
|
|
||||||
};
|
|
||||||
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
$scope.addMessage = function () {
|
$scope.addMessage = function () {
|
||||||
$scope.highestMessageIndex++;
|
var message = {message: ''};
|
||||||
// select all projectors on creation, so write the countdown to all projectors
|
ProjectorMessage.create(message);
|
||||||
$scope.projectors.forEach(function (projector) {
|
|
||||||
$http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{
|
|
||||||
name: 'core/message',
|
|
||||||
visible: false,
|
|
||||||
selected: true,
|
|
||||||
index: $scope.highestMessageIndex,
|
|
||||||
message: '',
|
|
||||||
stable: true,
|
|
||||||
}]);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
$scope.removeMessage = function (message) {
|
$scope.removeMessage = function (message) {
|
||||||
$scope.projectors.forEach(function (projector) {
|
var isProjectedIds = message.isProjected();
|
||||||
_.forEach(projector.elements, function (element, uuid) {
|
_.forEach(isProjectedIds, function(id) {
|
||||||
if (element.name == 'core/message' && element.index == message.index) {
|
message.project(id);
|
||||||
$http.post('/rest/core/projector/' + projector.id + '/deactivate_elements/', [uuid]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
ProjectorMessage.destroy(message.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
/* project functions*/
|
// go to the list of speakers(management) of the currently displayed list of speakers reference slide
|
||||||
$scope.project = function (element) {
|
|
||||||
$scope.projectors.forEach(function (projector) {
|
|
||||||
_.forEach(projector.elements, function (projectorElement, uuid) {
|
|
||||||
if (element.name == projectorElement.name && element.index == projectorElement.index) {
|
|
||||||
var data = {};
|
|
||||||
data[uuid] = {visible: !projectorElement.visible};
|
|
||||||
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
$scope.isProjected = function (element) {
|
|
||||||
var projectorIds = [];
|
|
||||||
$scope.projectors.forEach(function (projector) {
|
|
||||||
_.forEach(projector.elements, function (projectorElement, uuid) {
|
|
||||||
if (element.name == projectorElement.name && element.index == projectorElement.index) {
|
|
||||||
if (projectorElement.visible && projectorElement.selected) {
|
|
||||||
projectorIds.push(projector.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return projectorIds;
|
|
||||||
};
|
|
||||||
$scope.isProjectedOn = function (element, projector) {
|
|
||||||
var projectedIds = $scope.isProjected(element);
|
|
||||||
return _.indexOf(projectedIds, projector.id) > -1;
|
|
||||||
};
|
|
||||||
$scope.hasProjector = function (element, projector) {
|
|
||||||
var hasProjector = false;
|
|
||||||
_.forEach(projector.elements, function (projectorElement, uuid) {
|
|
||||||
if (element.name == projectorElement.name && element.index == projectorElement.index) {
|
|
||||||
if (projectorElement.selected) {
|
|
||||||
hasProjector = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return hasProjector;
|
|
||||||
};
|
|
||||||
$scope.toggleProjector = function (element, projector) {
|
|
||||||
_.forEach(projector.elements, function (projectorElement, uuid) {
|
|
||||||
if (element.name == projectorElement.name && element.index == projectorElement.index) {
|
|
||||||
var data = {};
|
|
||||||
data[uuid] = {
|
|
||||||
'selected': !projectorElement.selected,
|
|
||||||
};
|
|
||||||
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
$scope.selectAll = function (element, value) {
|
|
||||||
$scope.projectors.forEach(function (projector) {
|
|
||||||
_.forEach(projector.elements, function (projectorElement, uuid) {
|
|
||||||
if (element.name == projectorElement.name && element.index == projectorElement.index) {
|
|
||||||
var data = {};
|
|
||||||
data[uuid] = {
|
|
||||||
'selected': value,
|
|
||||||
};
|
|
||||||
$http.post('/rest/core/projector/' + projector.id + '/update_elements/', data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.preventClose = function (e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
/* go to the list of speakers(management) of the currently displayed list of speakers reference slide*/
|
|
||||||
$scope.goToListOfSpeakers = function() {
|
$scope.goToListOfSpeakers = function() {
|
||||||
CurrentListOfSpeakersItem.getItem($scope.currentListOfSpeakersReference).then(function (success) {
|
CurrentListOfSpeakersItem.getItem($scope.currentListOfSpeakersReference).then(function (success) {
|
||||||
$state.go('agenda.item.detail', {id: success.id});
|
$state.go('agenda.item.detail', {id: success.id});
|
||||||
@ -1123,8 +951,8 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
'Projector',
|
'Projector',
|
||||||
'ProjectionDefault',
|
'ProjectionDefault',
|
||||||
'Config',
|
'Config',
|
||||||
'gettextCatalog',
|
'ProjectorMessage',
|
||||||
function ($scope, $http, $state, $timeout, Projector, ProjectionDefault, Config, gettextCatalog) {
|
function ($scope, $http, $state, $timeout, Projector, ProjectionDefault, Config, ProjectorMessage) {
|
||||||
ProjectionDefault.bindAll({}, $scope, 'projectiondefaults');
|
ProjectionDefault.bindAll({}, $scope, 'projectiondefaults');
|
||||||
|
|
||||||
// watch for changes in projector_broadcast
|
// watch for changes in projector_broadcast
|
||||||
@ -1233,27 +1061,33 @@ angular.module('OpenSlidesApp.core.site', [
|
|||||||
$timeout.cancel($scope.identifyPromise);
|
$timeout.cancel($scope.identifyPromise);
|
||||||
$scope.removeIdentifierMessages();
|
$scope.removeIdentifierMessages();
|
||||||
} else {
|
} else {
|
||||||
$scope.projectors.forEach(function (projector) {
|
// Create new Message
|
||||||
$http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{
|
var message = {
|
||||||
name: 'core/message',
|
message: '',
|
||||||
stable: true,
|
};
|
||||||
selected: true,
|
ProjectorMessage.create(message).then(function(message){
|
||||||
visible: true,
|
$scope.projectors.forEach(function (projector) {
|
||||||
message: gettextCatalog.getString('Projector') + ' ' + projector.id + ': ' + projector.name,
|
$http.post('/rest/core/projector/' + projector.id + '/activate_elements/', [{
|
||||||
type: 'identify'
|
name: 'core/projectormessage',
|
||||||
}]);
|
stable: true,
|
||||||
|
id: message.id,
|
||||||
|
identify: true,
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
$scope.identifierMessage = message;
|
||||||
});
|
});
|
||||||
$scope.identifyPromise = $timeout($scope.removeIdentifierMessages, 3000);
|
$scope.identifyPromise = $timeout($scope.removeIdentifierMessages, 3000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
$scope.removeIdentifierMessages = function () {
|
$scope.removeIdentifierMessages = function () {
|
||||||
Projector.getAll().forEach(function (projector) {
|
Projector.getAll().forEach(function (projector) {
|
||||||
angular.forEach(projector.elements, function (uuid, value) {
|
_.forEach(projector.elements, function (element, uuid) {
|
||||||
if (value.name == 'core/message' && value.type == 'identify') {
|
if (element.name == 'core/projectormessage' && element.id == $scope.identifierMessage.id) {
|
||||||
$http.post('/rest/core/projector/' + projector.id + '/deactivate_elements/', [uuid]);
|
$http.post('/rest/core/projector/' + projector.id + '/deactivate_elements/', [uuid]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
ProjectorMessage.destroy($scope.identifierMessage.id);
|
||||||
$scope.identifyPromise = null;
|
$scope.identifyPromise = null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -141,7 +141,7 @@
|
|||||||
<h4 translate>Countdowns</h4>
|
<h4 translate>Countdowns</h4>
|
||||||
</a>
|
</a>
|
||||||
<div uib-collapse="!isCountdowns" ng-cloak>
|
<div uib-collapse="!isCountdowns" ng-cloak>
|
||||||
<div ng-repeat="countdown in countdowns | orderBy: 'index'" id="countdown{{countdown.uuid}}" class="countdown panel panel-default">
|
<div ng-repeat="countdown in countdowns | orderBy: 'index'" id="countdown{{countdown.id}}" class="countdown panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<span ng-if="countdown.description">{{ countdown.description }}</span>
|
<span ng-if="countdown.description">{{ countdown.description }}</span>
|
||||||
<span ng-if="!countdown.description">Countdown {{ $index +1 }}</span>
|
<span ng-if="!countdown.description">Countdown {{ $index +1 }}</span>
|
||||||
@ -153,63 +153,34 @@
|
|||||||
</button>
|
</button>
|
||||||
<!-- edit countdown button -->
|
<!-- edit countdown button -->
|
||||||
<button type="button" class="close editicon"
|
<button type="button" class="close editicon"
|
||||||
ng-click="countdown.editFlag=true;"
|
ng-click="countdown.editFlag=true; countdown.new_description = countdown.description;"
|
||||||
title="{{ 'Edit countdown' | translate}}">
|
title="{{ 'Edit countdown' | translate}}">
|
||||||
<i class="fa fa-pencil"></i>
|
<i class="fa fa-pencil"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body"
|
<div class="panel-body"
|
||||||
ng-class="{ 'projected': isProjected(countdown).length }">
|
ng-class="{ 'projected': isProjected(countdown).length }">
|
||||||
<!-- project countdown button -->
|
<projector-button model="countdown" default-projector-id="countdownDefaultProjectorId"></projector-button>
|
||||||
<div class="btn-group" style="width:54px;" uib-dropdown>
|
|
||||||
<button type="button" class="btn btn-default btn-sm"
|
|
||||||
ng-click="project(countdown)"
|
|
||||||
ng-class="{ 'btn-primary': isProjected(countdown).length }">
|
|
||||||
<i class="fa fa-video-camera"></i>
|
|
||||||
</button>
|
|
||||||
<button ng-if="projectors.length > 1" type="button" class="btn btn-default btn-sm slimDropDown" uib-dropdown-toggle>
|
|
||||||
<span class="caret"></span>
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li role="menuitem" ng-if="projectors.length > 1" style="text-align: center;">
|
|
||||||
<span class="pointer" ng-click="selectAll(countdown, true); preventClose($event)" translate>
|
|
||||||
All
|
|
||||||
</span>
|
|
||||||
| <span class="pointer" ng-click="selectAll(countdown, false); preventClose($event)" translate>
|
|
||||||
None
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class="divider" ng-if="projectors.length > 1"></li>
|
|
||||||
<li role="menuitem" ng-repeat="projector in projectors">
|
|
||||||
<a href="" ng-click="toggleProjector(countdown, projector); preventClose($event)"
|
|
||||||
ng-class="{ 'projected': isProjectedOn(countdown, projector) }">
|
|
||||||
<i class="fa fa-square-o" ng-hide="hasProjector(countdown, projector)"></i>
|
|
||||||
<i class="fa fa-check-square-o" ng-show="hasProjector(countdown, projector)"></i>
|
|
||||||
{{ projector.name }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- countdown controls -->
|
<!-- countdown controls -->
|
||||||
<a class="btn btn-default vcenter"
|
<a class="btn btn-default vcenter"
|
||||||
ng-click="resetCountdown(countdown)"
|
ng-click="countdown.reset()"
|
||||||
ng-class="{ 'disabled': !countdown.running && countdown.default_time == countdown.countdown_time }"
|
ng-class="{ 'disabled': !countdown.running && countdown.default_time == countdown.countdown_time }"
|
||||||
title="{{ 'Reset countdown' | translate}}">
|
title="{{ 'Reset countdown' | translate}}">
|
||||||
<i class="fa fa-stop"></i>
|
<i class="fa fa-stop"></i>
|
||||||
</a>
|
</a>
|
||||||
<a ng-if="!countdown.running" class="btn btn-default vcenter"
|
<a ng-if="!countdown.running" class="btn btn-default vcenter"
|
||||||
ng-click="startCountdown(countdown)"
|
ng-click="countdown.start()"
|
||||||
title="{{ 'Start' | translate}}">
|
title="{{ 'Start' | translate}}">
|
||||||
<i class="fa fa-play"></i>
|
<i class="fa fa-play"></i>
|
||||||
<i ng-if="countdown.running" class="fa fa-pause"></i>
|
<i ng-if="countdown.running" class="fa fa-pause"></i>
|
||||||
</a>
|
</a>
|
||||||
<a ng-if="countdown.running" class="btn btn-default vcenter"
|
<a ng-if="countdown.running" class="btn btn-default vcenter"
|
||||||
ng-click="stopCountdown(countdown)"
|
ng-click="countdown.stop()"
|
||||||
title="{{ 'Pause' | translate}}">
|
title="{{ 'Pause' | translate}}">
|
||||||
<i class="fa fa-pause"></i>
|
<i class="fa fa-pause"></i>
|
||||||
</a>
|
</a>
|
||||||
<span ng-if="!editTime" class="countdown_timer vcenter"
|
<span ng-if="!countdown.editTime" class="countdown_timer vcenter"
|
||||||
ng-class="{
|
ng-class="{
|
||||||
'negative': countdown.seconds <= 0,
|
'negative': countdown.seconds <= 0,
|
||||||
'warning': countdown.seconds <= config('agenda_countdown_warning_time') && countdown.seconds > 0 }">
|
'warning': countdown.seconds <= config('agenda_countdown_warning_time') && countdown.seconds > 0 }">
|
||||||
@ -220,7 +191,7 @@
|
|||||||
ng-submit="editCountdown(countdown)">
|
ng-submit="editCountdown(countdown)">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label translate>Description</label>
|
<label translate>Description</label>
|
||||||
<input ng-model="countdown.description" type="text" class="form-control input-sm">
|
<input ng-model="countdown.new_description" type="text" class="form-control input-sm">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label translate>Start time</label>
|
<label translate>Start time</label>
|
||||||
@ -281,39 +252,7 @@
|
|||||||
|
|
||||||
<div class="panel-body"
|
<div class="panel-body"
|
||||||
ng-class="{ 'projected': isProjected(message).length }">
|
ng-class="{ 'projected': isProjected(message).length }">
|
||||||
<div class="projectorbtn">
|
<projector-button model="message" default-projector-id="messageDefaultProjectorId"></projector-button>
|
||||||
<!-- project message button -->
|
|
||||||
<div class="btn-group" style="width:54px;" uib-dropdown>
|
|
||||||
<button type="button" class="btn btn-default btn-sm"
|
|
||||||
ng-click="project(message)"
|
|
||||||
ng-class="{ 'btn-primary': isProjected(message).length }">
|
|
||||||
<i class="fa fa-video-camera"></i>
|
|
||||||
</button>
|
|
||||||
<button type="button" ng-if="projectors.length > 1" class="btn btn-default btn-sm slimDropDown" uib-dropdown-toggle>
|
|
||||||
<span class="caret"></span>
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li role="menuitem" ng-if="projectors.length > 1" style="text-align: center;">
|
|
||||||
<span class="pointer" ng-click="selectAll(message, true); preventClose($event)" translate>
|
|
||||||
All
|
|
||||||
</span>
|
|
||||||
| <span class="pointer" ng-click="selectAll(message, false); preventClose($event)" translate>
|
|
||||||
None
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class="divider" ng-if="projectors.length > 1"></li>
|
|
||||||
<li role="menuitem" ng-repeat="projector in projectors">
|
|
||||||
<a href="" ng-click="toggleProjector(message, projector); preventClose($event)"
|
|
||||||
ng-class="{ 'projected': isProjectedOn(message, projector) }">
|
|
||||||
<i class="fa fa-square-o" ng-hide="hasProjector(message, projector)"></i>
|
|
||||||
<i class="fa fa-check-square-o" ng-show="hasProjector(message, projector)"></i>
|
|
||||||
{{ projector.name }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="innermessage" ng-bind-html="message.message"> </div>
|
<div class="innermessage" ng-bind-html="message.message"> </div>
|
||||||
|
|
||||||
@ -344,7 +283,7 @@
|
|||||||
<h4 translate>List of speakers</h4>
|
<h4 translate>List of speakers</h4>
|
||||||
</a>
|
</a>
|
||||||
<div uib-collapse="!isSpeakerList" ng-cloak>
|
<div uib-collapse="!isSpeakerList" ng-cloak>
|
||||||
<projector-button model="listofspeakers" default-projector-id="defaultProjectorId">
|
<projector-button model="listofspeakers" default-projector-id="listOfSpeakersDefaultProjectorId">
|
||||||
</projector-button>
|
</projector-button>
|
||||||
<div class="btn-group" os-perms="agenda.can_manage">
|
<div class="btn-group" os-perms="agenda.can_manage">
|
||||||
<a ng-click="goToListOfSpeakers()" class="btn btn-default btn-sm"
|
<a ng-click="goToListOfSpeakers()" class="btn btn-default btn-sm"
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
<div ng-controller="SlideCountdownCtrl">
|
<div ng-controller="SlideCountdownCtrl">
|
||||||
<div ng-if="visible && selected">
|
<div class="countdown well pull-right"
|
||||||
<div class="countdown well pull-right"
|
ng-class="{
|
||||||
ng-class="{
|
'negative': countdown.seconds <= 0,
|
||||||
'negative': seconds <= 0,
|
'warning': countdown.seconds <= config('agenda_countdown_warning_time') && countdown.seconds > 0 }">
|
||||||
'warning': seconds <= config('agenda_countdown_warning_time') && seconds > 0 }">
|
{{ countdown.seconds | osSecondsToTime}}
|
||||||
{{ seconds | osSecondsToTime}}
|
<div class="description">{{ countdown.description }}</div>
|
||||||
<div class="description">{{ description }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div ng-controller="SlideMessageCtrl">
|
<div ng-controller="SlideMessageCtrl">
|
||||||
<div ng-if="visible && selected" class="message_background"></div>
|
<div class="message_background"></div>
|
||||||
<div ng-if="visible && selected" class="message well" ng-class="{'identify': type=='identify'}" ng-bind-html="message"></div>
|
<div class="message well" ng-class="{'identify': element.identify}" ng-bind-html="element.identify ? identifyMessage : message.message"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -39,12 +39,22 @@ from ..utils.search import search
|
|||||||
from .access_permissions import (
|
from .access_permissions import (
|
||||||
ChatMessageAccessPermissions,
|
ChatMessageAccessPermissions,
|
||||||
ConfigAccessPermissions,
|
ConfigAccessPermissions,
|
||||||
|
CountdownAccessPermissions,
|
||||||
ProjectorAccessPermissions,
|
ProjectorAccessPermissions,
|
||||||
|
ProjectorMessageAccessPermissions,
|
||||||
TagAccessPermissions,
|
TagAccessPermissions,
|
||||||
)
|
)
|
||||||
from .config import config
|
from .config import config
|
||||||
from .exceptions import ConfigError, ConfigNotFound
|
from .exceptions import ConfigError, ConfigNotFound
|
||||||
from .models import ChatMessage, ConfigStore, ProjectionDefault, Projector, Tag
|
from .models import (
|
||||||
|
ChatMessage,
|
||||||
|
ConfigStore,
|
||||||
|
Countdown,
|
||||||
|
ProjectionDefault,
|
||||||
|
Projector,
|
||||||
|
ProjectorMessage,
|
||||||
|
Tag,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Special Django views
|
# Special Django views
|
||||||
@ -709,6 +719,50 @@ class ChatMessageViewSet(ModelViewSet):
|
|||||||
return Response({'detail': _('All chat messages deleted successfully.')})
|
return Response({'detail': _('All chat messages deleted successfully.')})
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectorMessageViewSet(ModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoint for messages.
|
||||||
|
|
||||||
|
There are the following views: list, retrieve, create, update and destroy.
|
||||||
|
"""
|
||||||
|
access_permissions = ProjectorMessageAccessPermissions()
|
||||||
|
queryset = ProjectorMessage.objects.all()
|
||||||
|
|
||||||
|
def check_view_permissions(self):
|
||||||
|
"""
|
||||||
|
Returns True if the user has required permissions.
|
||||||
|
"""
|
||||||
|
if self.action in ('list', 'retrieve'):
|
||||||
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
|
elif self.action in ('create', 'update', 'destroy'):
|
||||||
|
result = self.request.user.has_perm('core.can_manage_projector')
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class CountdownViewSet(ModelViewSet):
|
||||||
|
"""
|
||||||
|
API endpoint for Countdown.
|
||||||
|
|
||||||
|
There are the following views: list, retrieve, create, update and destroy.
|
||||||
|
"""
|
||||||
|
access_permissions = CountdownAccessPermissions()
|
||||||
|
queryset = Countdown.objects.all()
|
||||||
|
|
||||||
|
def check_view_permissions(self):
|
||||||
|
"""
|
||||||
|
Returns True if the user has required permissions.
|
||||||
|
"""
|
||||||
|
if self.action in ('list', 'retrieve'):
|
||||||
|
result = self.get_access_permissions().check_permissions(self.request.user)
|
||||||
|
elif self.action in ('create', 'update', 'destroy'):
|
||||||
|
result = self.request.user.has_perm('core.can_manage_projector')
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
# Special API views
|
# Special API views
|
||||||
|
|
||||||
class UrlPatternsView(utils_views.APIView):
|
class UrlPatternsView(utils_views.APIView):
|
||||||
|
@ -5,6 +5,7 @@ from __future__ import unicode_literals
|
|||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
import openslides.utils.models
|
import openslides.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
import openslides.utils.models
|
import openslides.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ from ..utils.rest_api import (
|
|||||||
detail_route,
|
detail_route,
|
||||||
)
|
)
|
||||||
from ..utils.views import APIView
|
from ..utils.views import APIView
|
||||||
|
|
||||||
from .access_permissions import (
|
from .access_permissions import (
|
||||||
CategoryAccessPermissions,
|
CategoryAccessPermissions,
|
||||||
MotionAccessPermissions,
|
MotionAccessPermissions,
|
||||||
|
@ -6,7 +6,7 @@ from rest_framework.test import APIClient
|
|||||||
from openslides.agenda.models import Item, Speaker
|
from openslides.agenda.models import Item, Speaker
|
||||||
from openslides.assignments.models import Assignment
|
from openslides.assignments.models import Assignment
|
||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.core.models import Projector
|
from openslides.core.models import Countdown
|
||||||
from openslides.motions.models import Motion
|
from openslides.motions.models import Motion
|
||||||
from openslides.topics.models import Topic
|
from openslides.topics.models import Topic
|
||||||
from openslides.users.models import User
|
from openslides.users.models import User
|
||||||
@ -278,29 +278,20 @@ class Speak(TestCase):
|
|||||||
self.client.put(
|
self.client.put(
|
||||||
reverse('item-speak', args=[self.item.pk]),
|
reverse('item-speak', args=[self.item.pk]),
|
||||||
{'speaker': speaker.pk})
|
{'speaker': speaker.pk})
|
||||||
for key, value in Projector.objects.get().config.items():
|
# Countdown should be created with pk=1 and running
|
||||||
if value['name'] == 'core/countdown':
|
self.assertEqual(Countdown.objects.all().count(), 1)
|
||||||
self.assertTrue(value['running'])
|
countdown = Countdown.objects.get(pk=1)
|
||||||
# If created, the countdown should have index 1
|
self.assertTrue(countdown.running)
|
||||||
created = value['index'] == 1
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
created = False
|
|
||||||
self.assertTrue(created)
|
|
||||||
|
|
||||||
def test_end_speech_with_countdown(self):
|
def test_end_speech_with_countdown(self):
|
||||||
config['agenda_couple_countdown_and_speakers'] = True
|
config['agenda_couple_countdown_and_speakers'] = True
|
||||||
speaker = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item)
|
speaker = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item)
|
||||||
speaker.begin_speech()
|
speaker.begin_speech()
|
||||||
self.client.delete(reverse('item-speak', args=[self.item.pk]))
|
self.client.delete(reverse('item-speak', args=[self.item.pk]))
|
||||||
for key, value in Projector.objects.get().config.items():
|
# Countdown should be created with pk=1 and stopped
|
||||||
if value['name'] == 'core/countdown':
|
self.assertEqual(Countdown.objects.all().count(), 1)
|
||||||
self.assertFalse(value['running'])
|
countdown = Countdown.objects.get(pk=1)
|
||||||
success = True
|
self.assertFalse(countdown.running)
|
||||||
break
|
|
||||||
else:
|
|
||||||
success = False
|
|
||||||
self.assertTrue(success)
|
|
||||||
|
|
||||||
|
|
||||||
class Numbering(TestCase):
|
class Numbering(TestCase):
|
||||||
|
Loading…
Reference in New Issue
Block a user