From f8048da76c179504e241da778f4198755d415917 Mon Sep 17 00:00:00 2001 From: Oskar Hahn Date: Fri, 22 Nov 2013 18:18:06 +0100 Subject: [PATCH] Fixed countdown and projector update issues * agenda_item.get_absolute_url('projector') returns the activate-url of the related slide. * agenda_item.is_activate() returns True, if the related item is active * Fixed set_active_slide to accept kwargs * Reset countdown when saving a new duration time * Update countdown overlay when begin_speak and end_speak is called * Fixed blinking countdown Fixes: #1078, #1076, #1075 --- openslides/agenda/models.py | 44 +++++++++++++++++++++------ openslides/projector/api.py | 3 +- openslides/projector/signals.py | 21 ++++++------- openslides/projector/views.py | 8 ++--- tests/agenda/models.py | 15 +++++++-- tests/agenda/test_list_of_speakers.py | 42 +++++++++++++++---------- tests/agenda/tests.py | 22 +++++++++++++- tests/projector/test_api.py | 2 +- tests/projector/test_signals.py | 21 +++++++++++++ tests/projector/test_views.py | 17 +++++++++++ 10 files changed, 146 insertions(+), 49 deletions(-) create mode 100644 tests/projector/test_signals.py diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py index 1e59cf963..020770c9b 100644 --- a/openslides/agenda/models.py +++ b/openslides/agenda/models.py @@ -128,16 +128,21 @@ class Item(SlideMixin, MPTTModel): The link can be detail, update or delete. """ if link == 'detail' or link == 'view': - return reverse('item_view', args=[str(self.id)]) - if link == 'update' or link == 'edit': - return reverse('item_edit', args=[str(self.id)]) - if link == 'delete': - return reverse('item_delete', args=[str(self.id)]) - if link == 'projector_list_of_speakers': - return '%s&type=list_of_speakers' % super(Item, self).get_absolute_url('projector') - if link == 'projector_summary': - return '%s&type=summary' % super(Item, self).get_absolute_url('projector') - return super(Item, self).get_absolute_url(link) + url = reverse('item_view', args=[str(self.id)]) + elif link == 'update' or link == 'edit': + url = reverse('item_edit', args=[str(self.id)]) + elif link == 'delete': + url = reverse('item_delete', args=[str(self.id)]) + elif link == 'projector_list_of_speakers': + url = '%s&type=list_of_speakers' % super(Item, self).get_absolute_url('projector') + elif link == 'projector_summary': + url = '%s&type=summary' % super(Item, self).get_absolute_url('projector') + elif (link in ('projector', 'projector_preview') and + self.content_object and isinstance(self.content_object, SlideMixin)): + url = self.content_object.get_absolute_url(link) + else: + url = super(Item, self).get_absolute_url(link) + return url def get_title(self): """ @@ -268,6 +273,17 @@ class Item(SlideMixin, MPTTModel): # The list of speakers is empty. return None + def is_active_slide(self): + """ + Returns True if the slide is True. If the slide is a related item, + Returns True if the related object is active. + """ + if self.content_object and isinstance(self.content_object, SlideMixin): + value = self.content_object.is_active_slide() + else: + value = super(Item, self).is_active_slide() + return value + class SpeakerManager(models.Manager): def add(self, person, item): @@ -373,6 +389,10 @@ class Speaker(models.Model): if config['agenda_couple_countdown_and_speakers']: reset_countdown() start_countdown() + if self.item.is_active_slide(): + # TODO: only update the overlay if the overlay is active and + # slide type is None. + update_projector_overlay('projector_countdown') def end_speach(self): """ @@ -383,3 +403,7 @@ class Speaker(models.Model): # stop countdown if config['agenda_couple_countdown_and_speakers']: stop_countdown() + if self.item.is_active_slide(): + # TODO: only update the overlay if the overlay is active and + # slide type is None. + update_projector_overlay('projector_countdown') diff --git a/openslides/projector/api.py b/openslides/projector/api.py index f091815cc..46a9887e7 100644 --- a/openslides/projector/api.py +++ b/openslides/projector/api.py @@ -172,14 +172,13 @@ def register_slide_model(SlideModel, template): register_slide(SlideModel.slide_callback_name, model_slide) -def set_active_slide(callback, kwargs=None): +def set_active_slide(callback, **kwargs): """ Set the active Slide. callback: The name of the slide callback. kwargs: Keyword arguments for the slide callback. """ - kwargs = kwargs or {} kwargs.update(callback=callback) config['projector_active_slide'] = kwargs update_projector() diff --git a/openslides/projector/signals.py b/openslides/projector/signals.py index ea90e5c9f..34b8155b4 100644 --- a/openslides/projector/signals.py +++ b/openslides/projector/signals.py @@ -5,6 +5,7 @@ from django.contrib.staticfiles.templatetags.staticfiles import static from django.core.context_processors import csrf from django.dispatch import receiver, Signal from django.template.loader import render_to_string +from django.utils.datastructures import SortedDict from openslides.config.api import config, ConfigPage, ConfigVariable from openslides.config.signals import config_signal @@ -99,18 +100,14 @@ def countdown(sender, **kwargs): """ Returns JavaScript for the projector """ - start = int(config['countdown_start_stamp']) - duration = int(config['countdown_time']) - pause = int(config['countdown_pause_stamp']) - state = config['countdown_state'] - - return { - 'load_file': static('javascript/countdown.js'), - 'call': 'update_countdown();', - 'projector_countdown_start': start, - 'projector_countdown_duration': duration, - 'projector_countdown_pause': pause, - 'projector_countdown_state': state} + value = SortedDict() + value['load_file'] = static('javascript/countdown.js') + value['projector_countdown_start'] = int(config['countdown_start_stamp']) + value['projector_countdown_duration'] = int(config['countdown_time']) + value['projector_countdown_pause'] = int(config['countdown_pause_stamp']) + value['projector_countdown_state'] = config['countdown_state'] + value['call'] = 'update_countdown();' + return value def get_projector_html(): """ diff --git a/openslides/projector/views.py b/openslides/projector/views.py index 1f969ea54..88e0967d2 100644 --- a/openslides/projector/views.py +++ b/openslides/projector/views.py @@ -85,7 +85,7 @@ class ActivateView(RedirectView): ProjectorSocketHandler.send_updates( {'calls': {'load_pdf': {'url': url, 'page_num': kwargs['page_num']}}}) else: - set_active_slide(kwargs['callback'], kwargs=dict(request.GET.items())) + set_active_slide(kwargs['callback'], **dict(request.GET.items())) config['projector_scroll'] = config.get_default('projector_scroll') config['projector_scale'] = config.get_default('projector_scale') call_on_projector({'scroll': config['projector_scroll'], @@ -186,10 +186,10 @@ class CountdownControllView(RedirectView): try: config['countdown_time'] = \ int(self.request.GET['countdown_time']) - except ValueError: - pass - except AttributeError: + except (ValueError, AttributeError): pass + else: + reset_countdown() update_projector_overlay('projector_countdown') def get_ajax_context(self, **kwargs): diff --git a/tests/agenda/models.py b/tests/agenda/models.py index c14b81117..bb576ed47 100644 --- a/tests/agenda/models.py +++ b/tests/agenda/models.py @@ -1,7 +1,12 @@ +# -*- coding: utf-8 -*- + from django.db import models +from openslides.projector.models import SlideMixin -class RelatedItem(models.Model): + +class RelatedItem(SlideMixin, models.Model): + slide_callback_name = 'test_related_item' name = models.CharField(max_length='255') class Meta: @@ -13,8 +18,12 @@ class RelatedItem(models.Model): def get_agenda_title_supplement(self): return 'test item' - def get_absolute_url(self, *args, **kwargs): - return '/absolute-url-here/' + def get_absolute_url(self, link=None): + if link is None: + value = '/absolute-url-here/' + else: + value = super(RelatedItem, self).get_absolute_url(link) + return value class BadRelatedItem(models.Model): diff --git a/tests/agenda/test_list_of_speakers.py b/tests/agenda/test_list_of_speakers.py index 428318123..f74bbb662 100644 --- a/tests/agenda/test_list_of_speakers.py +++ b/tests/agenda/test_list_of_speakers.py @@ -2,6 +2,7 @@ from django.contrib.auth.models import Permission from django.test.client import Client +from mock import patch, MagicMock from openslides.agenda.models import Item, Speaker from openslides.config.api import config @@ -69,24 +70,33 @@ class ListOfSpeakerModelTests(TestCase): self.assertIsNotNone(Speaker.objects.get(person=self.speaker1, item=self.item1).end_time) self.assertIsNotNone(speaker2_item1.begin_time) - def test_speach_coupled_with_countdown(self): + @patch('openslides.agenda.models.update_projector_overlay') + def test_speach_coupled_with_countdown(self, mock_update_projector_overlay): config['agenda_couple_countdown_and_speakers'] = True - self.assertTrue(config['countdown_state'] == 'inactive') speaker1_item1 = Speaker.objects.add(self.speaker1, self.item1) - speaker1_item1.begin_speach() - self.assertTrue(config['countdown_state'] == 'active') - speaker1_item1.end_speach() - self.assertTrue(config['countdown_state'] == 'paused') + self.item1.is_active_slide = MagicMock(return_value=True) - def test_begin_speach_not_coupled_with_countdown(self): - config['agenda_couple_countdown_and_speakers'] = False - self.assertTrue(config['countdown_state'] == 'inactive') - speaker1_item1 = Speaker.objects.add(self.speaker1, self.item1) speaker1_item1.begin_speach() - self.assertTrue(config['countdown_state'] == 'inactive') + self.assertEqual(config['countdown_state'], 'active') + mock_update_projector_overlay.assert_called_with('projector_countdown') + + mock_update_projector_overlay.reset_mock() + speaker1_item1.end_speach() + self.assertEqual(config['countdown_state'], 'paused') + mock_update_projector_overlay.assert_called_with('projector_countdown') + + @patch('openslides.agenda.models.update_projector_overlay') + def test_begin_speach_not_coupled_with_countdown(self, mock_update_projector_overlay): + config['agenda_couple_countdown_and_speakers'] = False + speaker1_item1 = Speaker.objects.add(self.speaker1, self.item1) + + speaker1_item1.begin_speach() + self.assertEqual(config['countdown_state'], 'inactive') + config['countdown_state'] = 'active' speaker1_item1.end_speach() - self.assertTrue(config['countdown_state'] == 'active') + self.assertEqual(config['countdown_state'], 'active') + self.assertFalse(mock_update_projector_overlay.called) class SpeakerViewTestCase(TestCase): @@ -233,7 +243,7 @@ class GlobalListOfSpeakersLinks(SpeakerViewTestCase): self.assertRedirects(response, '/projector/dashboard/') self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.') - set_active_slide('agenda', {'pk': 1}) + set_active_slide('agenda', pk=1) response = self.speaker1_client.get('/agenda/list_of_speakers/') self.assertRedirects(response, '/agenda/1/') @@ -242,7 +252,7 @@ class GlobalListOfSpeakersLinks(SpeakerViewTestCase): self.assertRedirects(response, '/projector/dashboard/') self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.') - set_active_slide('agenda', {'pk': 1}) + set_active_slide('agenda', pk=1) response = self.speaker1_client.get('/agenda/list_of_speakers/add/') self.assertRedirects(response, '/agenda/1/') self.assertEqual(Speaker.objects.get(item__pk='1').person, self.speaker1) @@ -258,7 +268,7 @@ class GlobalListOfSpeakersLinks(SpeakerViewTestCase): self.assertRedirects(response, '/projector/dashboard/') self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.') - set_active_slide('agenda', {'pk': 1}) + set_active_slide('agenda', pk=1) response = self.admin_client.get('/agenda/list_of_speakers/next/') self.assertRedirects(response, '/projector/dashboard/') self.assertMessage(response, 'The list of speakers is empty.') @@ -274,7 +284,7 @@ class GlobalListOfSpeakersLinks(SpeakerViewTestCase): self.assertRedirects(response, '/projector/dashboard/') self.assertMessage(response, 'There is no list of speakers for the current slide. Please choose the agenda item manually from the agenda.') - set_active_slide('agenda', {'pk': 1}) + set_active_slide('agenda', pk=1) response = self.admin_client.get('/agenda/list_of_speakers/end_speach/') self.assertRedirects(response, '/projector/dashboard/') self.assertMessage(response, 'There is no one speaking at the moment.') diff --git a/tests/agenda/tests.py b/tests/agenda/tests.py index 77fe1ce33..821c2bbe9 100644 --- a/tests/agenda/tests.py +++ b/tests/agenda/tests.py @@ -8,6 +8,7 @@ from mock import patch from openslides.agenda.models import Item from openslides.agenda.slides import agenda_slide from openslides.participant.models import User +from openslides.projector.api import set_active_slide from openslides.utils.test import TestCase from .models import BadRelatedItem, RelatedItem @@ -77,7 +78,26 @@ class ItemTest(TestCase): def test_deleted_related_item(self): self.related.delete() self.assertFalse(RelatedItem.objects.all().exists()) - self.assertEqual(Item.objects.get(pk=self.item5.pk).title, '< Item for deleted slide (ekdfjen458gj1siek45nv) >') + self.assertEqual(Item.objects.get(pk=self.item5.pk).title, + '< Item for deleted slide (ekdfjen458gj1siek45nv) >') + + def test_related_item_get_absolute_url(self): + """ + Tests that the get_absolute_url method with the link 'projector' + and 'projector_preview' returns the absolute_url for the related + item. + """ + self.assertEqual(self.item5.get_absolute_url('projector'), + '/projector/activate/test_related_item/?pk=1') + self.assertEqual(self.item5.get_absolute_url('projector_preview'), + '/projector/preview/test_related_item/?pk=1') + + def test_activate_related_item(self): + """ + The agenda item has to be active, if its related item is. + """ + set_active_slide('test_related_item', pk=1) + self.assertTrue(self.item5.is_active_slide) def test_bad_related_item(self): bad = BadRelatedItem.objects.create(name='dhfne94irkgl2047fzvb') diff --git a/tests/projector/test_api.py b/tests/projector/test_api.py index 253ee9501..8b0b25bb5 100644 --- a/tests/projector/test_api.py +++ b/tests/projector/test_api.py @@ -170,7 +170,7 @@ class ApiFunctions(TestCase): def test_set_active_slide(self, mock_update_projector, mock_update_projector_overlay): mock_config = {} with patch('openslides.projector.api.config', mock_config): - projector_api.set_active_slide('callback_name', {'some': 'kwargs'}) + projector_api.set_active_slide('callback_name', some='kwargs') self.assertEqual(mock_config, {'projector_active_slide': {'callback': 'callback_name', 'some': 'kwargs'}}) diff --git a/tests/projector/test_signals.py b/tests/projector/test_signals.py new file mode 100644 index 000000000..b78edd189 --- /dev/null +++ b/tests/projector/test_signals.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +from openslides.projector.signals import countdown +from openslides.utils.test import TestCase + + +class CountdownTest(TestCase): + def test_order_of_get_projector_js(self): + """ + Tests that the order of the js values is in the right order. Especially + the value 'call' has to come at the end. + """ + overlay = countdown('fake sender') + test_value = overlay.get_javascript() + + self.assertIsInstance(test_value, dict) + self.assertEqual( + test_value.keys(), + ['load_file', 'projector_countdown_start', + 'projector_countdown_duration', 'projector_countdown_pause', + 'projector_countdown_state', 'call']) diff --git a/tests/projector/test_views.py b/tests/projector/test_views.py index ea2673b46..6366ca9b9 100644 --- a/tests/projector/test_views.py +++ b/tests/projector/test_views.py @@ -3,6 +3,7 @@ from django.test.client import Client, RequestFactory from mock import call, MagicMock, patch +from openslides.config.api import config from openslides.projector.models import ProjectorSlide from openslides.projector import views from openslides.utils.test import TestCase @@ -199,3 +200,19 @@ class CustomSlidesTest(TestCase): response = self.admin_client.post(url, {'yes': 'true'}) self.assertRedirects(response, '/projector/dashboard/') self.assertFalse(ProjectorSlide.objects.exists()) + + +class CountdownControllView(TestCase): + def setUp(self): + self.admin_client = Client() + self.admin_client.login(username='admin', password='admin') + + @patch('openslides.projector.views.reset_countdown') + def test_set_default(self, mock_reset_countdown): + """ + Test, that the url /countdown/set-default/ sets the time for the countdown + and reset the countdown. + """ + self.admin_client.get('/projector/countdown/set-default/', {'countdown_time': 42}) + self.assertEqual(config['countdown_time'], 42) + mock_reset_countdown.assert_called_with()