Bundle countdown with list of speakers. Fixed #1541.
This commit is contained in:
parent
948e776d33
commit
719b5ffedd
@ -11,6 +11,7 @@ from django.utils.translation import ugettext_lazy, ugettext_noop
|
|||||||
|
|
||||||
from openslides.core.config import config
|
from openslides.core.config import config
|
||||||
from openslides.core.models import Tag
|
from openslides.core.models import Tag
|
||||||
|
from openslides.core.projector import Countdown
|
||||||
from openslides.users.models import User
|
from openslides.users.models import User
|
||||||
from openslides.utils.exceptions import OpenSlidesError
|
from openslides.utils.exceptions import OpenSlidesError
|
||||||
from openslides.utils.models import RESTModelMixin
|
from openslides.utils.models import RESTModelMixin
|
||||||
@ -409,12 +410,9 @@ class Speaker(RESTModelMixin, models.Model):
|
|||||||
self.weight = None
|
self.weight = None
|
||||||
self.begin_time = datetime.now()
|
self.begin_time = datetime.now()
|
||||||
self.save()
|
self.save()
|
||||||
# start countdown
|
|
||||||
if config['agenda_couple_countdown_and_speakers']:
|
if config['agenda_couple_countdown_and_speakers']:
|
||||||
# TODO: Fix me with the new countdown api
|
Countdown.control(action='reset')
|
||||||
# reset_countdown()
|
Countdown.control(action='start')
|
||||||
# start_countdown()
|
|
||||||
pass
|
|
||||||
|
|
||||||
def end_speech(self):
|
def end_speech(self):
|
||||||
"""
|
"""
|
||||||
@ -422,11 +420,8 @@ class Speaker(RESTModelMixin, models.Model):
|
|||||||
"""
|
"""
|
||||||
self.end_time = datetime.now()
|
self.end_time = datetime.now()
|
||||||
self.save()
|
self.save()
|
||||||
# stop countdown
|
|
||||||
if config['agenda_couple_countdown_and_speakers']:
|
if config['agenda_couple_countdown_and_speakers']:
|
||||||
# TODO: Fix me with the new countdown api
|
Countdown.control(action='stop')
|
||||||
# stop_countdown()
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_root_rest_element(self):
|
def get_root_rest_element(self):
|
||||||
"""
|
"""
|
||||||
|
35
openslides/core/migrations/0002_countdown.py
Normal file
35
openslides/core/migrations/0002_countdown.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def add_default_projector_2(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Adds default projector, activates countdown.
|
||||||
|
"""
|
||||||
|
# We get the model from the versioned app registry;
|
||||||
|
# if we directly import it, it will be the wrong version.
|
||||||
|
Projector = apps.get_model('core', 'Projector')
|
||||||
|
projector = Projector.objects.get()
|
||||||
|
config = projector.config
|
||||||
|
config.append({
|
||||||
|
'name': 'core/countdown',
|
||||||
|
'stable': True,
|
||||||
|
'status': 'stop',
|
||||||
|
'countdown_time': 60
|
||||||
|
})
|
||||||
|
projector.config = config
|
||||||
|
projector.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
code=add_default_projector_2,
|
||||||
|
reverse_code=None,
|
||||||
|
atomic=True,
|
||||||
|
),
|
||||||
|
]
|
@ -3,8 +3,9 @@ from django.utils.translation import ugettext as _
|
|||||||
|
|
||||||
from openslides.utils.projector import ProjectorElement, ProjectorRequirement
|
from openslides.utils.projector import ProjectorElement, ProjectorRequirement
|
||||||
|
|
||||||
|
from .config import config
|
||||||
from .exceptions import ProjectorException
|
from .exceptions import ProjectorException
|
||||||
from .models import CustomSlide
|
from .models import CustomSlide, Projector
|
||||||
from .views import CustomSlideViewSet
|
from .views import CustomSlideViewSet
|
||||||
|
|
||||||
|
|
||||||
@ -47,31 +48,84 @@ class Countdown(ProjectorElement):
|
|||||||
|
|
||||||
{
|
{
|
||||||
"countdown_time": <timestamp>,
|
"countdown_time": <timestamp>,
|
||||||
"status": "go"
|
"status": "running"
|
||||||
}
|
}
|
||||||
|
|
||||||
The timestamp is a POSIX timestamp (seconds) calculated from server
|
The timestamp is a POSIX timestamp (seconds) calculated from server
|
||||||
time, server time offset and countdown duration (countdown_time = now -
|
time, server time offset and countdown duration (countdown_time = now -
|
||||||
serverTimeOffset + duration).
|
serverTimeOffset + duration).
|
||||||
|
|
||||||
To stop the countdown set the countdown time to the actual value of the
|
To stop the countdown set the countdown time to the current value of the
|
||||||
countdown (countdown_time = countdown_time - now + serverTimeOffset)
|
countdown (countdown_time = countdown_time - now + serverTimeOffset)
|
||||||
and set status to "stop".
|
and set status to "stop".
|
||||||
|
|
||||||
To reset the countdown (it is not a reset in a functional way) just
|
To reset the countdown (it is not a reset in a functional way) just
|
||||||
change the countdown_time. The status value remain 'stop'.
|
change the countdown_time. The status value remains "stop".
|
||||||
|
|
||||||
To hide a running countdown add {"hidden": true}.
|
There might be an additional value for the "default" countdown time
|
||||||
|
which is used for the internal reset method if the countdown is coupled
|
||||||
|
with the list of speakers.
|
||||||
|
|
||||||
|
To hide a countdown add {"hidden": true}.
|
||||||
"""
|
"""
|
||||||
name = 'core/countdown'
|
name = 'core/countdown'
|
||||||
|
|
||||||
def get_context(self):
|
def get_context(self):
|
||||||
if self.config_entry.get('countdown_time') is None:
|
self.validate_config(self.config_entry)
|
||||||
raise ProjectorException(_('No countdown time given.'))
|
|
||||||
if self.config_entry.get('status') is None:
|
|
||||||
raise ProjectorException(_('No status given.'))
|
|
||||||
return {'server_time': now().timestamp()}
|
return {'server_time': now().timestamp()}
|
||||||
|
|
||||||
|
@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 config_data.get('status') not in ('running', 'stop'):
|
||||||
|
raise ProjectorException(_("Invalid status. Use 'running' or 'stop'."))
|
||||||
|
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, projector_id=1, index=0):
|
||||||
|
"""
|
||||||
|
Starts, stops or resets the countdown with the given index on the
|
||||||
|
given projector.
|
||||||
|
|
||||||
|
Action must be 'start', 'stop' or 'reset'.
|
||||||
|
"""
|
||||||
|
if action not in ('start', 'stop', 'reset'):
|
||||||
|
raise ValueError("Action must be 'start', 'stop' or 'reset', not {}.".format(action))
|
||||||
|
|
||||||
|
projector_instance = Projector.objects.get(pk=projector_id)
|
||||||
|
projector_config = []
|
||||||
|
found = False
|
||||||
|
for element in projector_instance.config:
|
||||||
|
if element['name'] == cls.name:
|
||||||
|
if index == 0:
|
||||||
|
try:
|
||||||
|
cls.validate_config(element)
|
||||||
|
except ProjectorException:
|
||||||
|
# Do not proceed if the specific procjector config data is invalid.
|
||||||
|
# The variable found remains False.
|
||||||
|
break
|
||||||
|
found = True
|
||||||
|
if action == 'start' and element['status'] == 'stop':
|
||||||
|
element['status'] = 'running'
|
||||||
|
element['countdown_time'] = now().timestamp() + element['countdown_time']
|
||||||
|
elif action == 'stop' and element['status'] == 'running':
|
||||||
|
element['status'] = 'stop'
|
||||||
|
element['countdown_time'] = element['countdown_time'] - now().timestamp()
|
||||||
|
elif action == 'reset':
|
||||||
|
element['status'] = 'stop'
|
||||||
|
element['countdown_time'] = element.get('default', config['projector_default_countdown'])
|
||||||
|
else:
|
||||||
|
index += -1
|
||||||
|
projector_config.append(element)
|
||||||
|
if found:
|
||||||
|
projector_instance.config = projector_config
|
||||||
|
projector_instance.save()
|
||||||
|
|
||||||
|
|
||||||
class Message(ProjectorElement):
|
class Message(ProjectorElement):
|
||||||
"""
|
"""
|
||||||
|
@ -135,6 +135,13 @@ def setup_general_config(sender, **kwargs):
|
|||||||
group=ugettext_lazy('Projector'),
|
group=ugettext_lazy('Projector'),
|
||||||
translatable=True)
|
translatable=True)
|
||||||
|
|
||||||
|
yield ConfigVariable(
|
||||||
|
name='projector_default_countdown',
|
||||||
|
default_value=60,
|
||||||
|
label=ugettext_lazy('Default countdown'),
|
||||||
|
weight=185,
|
||||||
|
group=ugettext_lazy('Projector'))
|
||||||
|
|
||||||
|
|
||||||
config_signal = Signal(providing_args=[])
|
config_signal = Signal(providing_args=[])
|
||||||
"""Signal to get all config tabs from all apps."""
|
"""Signal to get all config tabs from all apps."""
|
||||||
|
@ -3,6 +3,8 @@ from django.core.urlresolvers import reverse
|
|||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from openslides.agenda.models import Item, Speaker
|
from openslides.agenda.models import Item, Speaker
|
||||||
|
from openslides.core.config import config
|
||||||
|
from openslides.core.models import Projector
|
||||||
from openslides.utils.test import TestCase
|
from openslides.utils.test import TestCase
|
||||||
|
|
||||||
|
|
||||||
@ -178,3 +180,21 @@ class Speak(TestCase):
|
|||||||
def test_end_speech_no_current_speaker(self):
|
def test_end_speech_no_current_speaker(self):
|
||||||
response = self.client.delete(reverse('item-speak', args=[self.item.pk]))
|
response = self.client.delete(reverse('item-speak', args=[self.item.pk]))
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
def test_begin_speech_with_countdown(self):
|
||||||
|
config['agenda_couple_countdown_and_speakers'] = True
|
||||||
|
Speaker.objects.add(self.user, self.item)
|
||||||
|
speaker = Speaker.objects.add(get_user_model().objects.get(username='admin'), self.item)
|
||||||
|
self.assertEqual(Projector.objects.get().config[2]['name'], 'core/countdown')
|
||||||
|
self.client.put(
|
||||||
|
reverse('item-speak', args=[self.item.pk]),
|
||||||
|
{'speaker': speaker.pk})
|
||||||
|
self.assertEqual(Projector.objects.get().config[2]['status'], '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.assertEqual(Projector.objects.get().config[2]['name'], 'core/countdown')
|
||||||
|
self.client.delete(reverse('item-speak', args=[self.item.pk]))
|
||||||
|
self.assertEqual(Projector.objects.get().config[2]['status'], 'stop')
|
||||||
|
Loading…
Reference in New Issue
Block a user