From 719b5ffeddaf1a354499029f835b95cdb21c2450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Norman=20J=C3=A4ckel?= Date: Sat, 5 Sep 2015 14:58:10 +0200 Subject: [PATCH] Bundle countdown with list of speakers. Fixed #1541. --- openslides/agenda/models.py | 13 ++-- openslides/core/migrations/0002_countdown.py | 35 ++++++++++ openslides/core/projector.py | 72 +++++++++++++++++--- openslides/core/signals.py | 7 ++ tests/integration/agenda/test_viewsets.py | 20 ++++++ 5 files changed, 129 insertions(+), 18 deletions(-) create mode 100644 openslides/core/migrations/0002_countdown.py diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py index 39fbf62cc..e407f9193 100644 --- a/openslides/agenda/models.py +++ b/openslides/agenda/models.py @@ -11,6 +11,7 @@ from django.utils.translation import ugettext_lazy, ugettext_noop from openslides.core.config import config from openslides.core.models import Tag +from openslides.core.projector import Countdown from openslides.users.models import User from openslides.utils.exceptions import OpenSlidesError from openslides.utils.models import RESTModelMixin @@ -409,12 +410,9 @@ class Speaker(RESTModelMixin, models.Model): self.weight = None self.begin_time = datetime.now() self.save() - # start countdown if config['agenda_couple_countdown_and_speakers']: - # TODO: Fix me with the new countdown api - # reset_countdown() - # start_countdown() - pass + Countdown.control(action='reset') + Countdown.control(action='start') def end_speech(self): """ @@ -422,11 +420,8 @@ class Speaker(RESTModelMixin, models.Model): """ self.end_time = datetime.now() self.save() - # stop countdown if config['agenda_couple_countdown_and_speakers']: - # TODO: Fix me with the new countdown api - # stop_countdown() - pass + Countdown.control(action='stop') def get_root_rest_element(self): """ diff --git a/openslides/core/migrations/0002_countdown.py b/openslides/core/migrations/0002_countdown.py new file mode 100644 index 000000000..7ae5cc588 --- /dev/null +++ b/openslides/core/migrations/0002_countdown.py @@ -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, + ), + ] diff --git a/openslides/core/projector.py b/openslides/core/projector.py index 0083d19ce..62454d4b9 100644 --- a/openslides/core/projector.py +++ b/openslides/core/projector.py @@ -3,8 +3,9 @@ from django.utils.translation import ugettext as _ from openslides.utils.projector import ProjectorElement, ProjectorRequirement +from .config import config from .exceptions import ProjectorException -from .models import CustomSlide +from .models import CustomSlide, Projector from .views import CustomSlideViewSet @@ -47,31 +48,84 @@ class Countdown(ProjectorElement): { "countdown_time": , - "status": "go" + "status": "running" } The timestamp is a POSIX timestamp (seconds) calculated from server time, server time offset and countdown duration (countdown_time = now - 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) and set status to "stop". 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' def get_context(self): - if self.config_entry.get('countdown_time') is None: - raise ProjectorException(_('No countdown time given.')) - if self.config_entry.get('status') is None: - raise ProjectorException(_('No status given.')) + self.validate_config(self.config_entry) 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): """ diff --git a/openslides/core/signals.py b/openslides/core/signals.py index c8641de41..e57dfa6df 100644 --- a/openslides/core/signals.py +++ b/openslides/core/signals.py @@ -135,6 +135,13 @@ def setup_general_config(sender, **kwargs): group=ugettext_lazy('Projector'), 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=[]) """Signal to get all config tabs from all apps.""" diff --git a/tests/integration/agenda/test_viewsets.py b/tests/integration/agenda/test_viewsets.py index 06efa50b7..8fcc6434e 100644 --- a/tests/integration/agenda/test_viewsets.py +++ b/tests/integration/agenda/test_viewsets.py @@ -3,6 +3,8 @@ from django.core.urlresolvers import reverse from rest_framework.test import APIClient 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 @@ -178,3 +180,21 @@ class Speak(TestCase): def test_end_speech_no_current_speaker(self): response = self.client.delete(reverse('item-speak', args=[self.item.pk])) 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')