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
This commit is contained in:
Oskar Hahn 2013-11-22 18:18:06 +01:00
parent 3be0507f1b
commit f8048da76c
10 changed files with 146 additions and 49 deletions

View File

@ -128,16 +128,21 @@ class Item(SlideMixin, MPTTModel):
The link can be detail, update or delete. The link can be detail, update or delete.
""" """
if link == 'detail' or link == 'view': if link == 'detail' or link == 'view':
return reverse('item_view', args=[str(self.id)]) url = reverse('item_view', args=[str(self.id)])
if link == 'update' or link == 'edit': elif link == 'update' or link == 'edit':
return reverse('item_edit', args=[str(self.id)]) url = reverse('item_edit', args=[str(self.id)])
if link == 'delete': elif link == 'delete':
return reverse('item_delete', args=[str(self.id)]) url = reverse('item_delete', args=[str(self.id)])
if link == 'projector_list_of_speakers': elif link == 'projector_list_of_speakers':
return '%s&type=list_of_speakers' % super(Item, self).get_absolute_url('projector') url = '%s&type=list_of_speakers' % super(Item, self).get_absolute_url('projector')
if link == 'projector_summary': elif link == 'projector_summary':
return '%s&type=summary' % super(Item, self).get_absolute_url('projector') url = '%s&type=summary' % super(Item, self).get_absolute_url('projector')
return super(Item, self).get_absolute_url(link) 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): def get_title(self):
""" """
@ -268,6 +273,17 @@ class Item(SlideMixin, MPTTModel):
# The list of speakers is empty. # The list of speakers is empty.
return None 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): class SpeakerManager(models.Manager):
def add(self, person, item): def add(self, person, item):
@ -373,6 +389,10 @@ class Speaker(models.Model):
if config['agenda_couple_countdown_and_speakers']: if config['agenda_couple_countdown_and_speakers']:
reset_countdown() reset_countdown()
start_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): def end_speach(self):
""" """
@ -383,3 +403,7 @@ class Speaker(models.Model):
# stop countdown # stop countdown
if config['agenda_couple_countdown_and_speakers']: if config['agenda_couple_countdown_and_speakers']:
stop_countdown() 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')

View File

@ -172,14 +172,13 @@ def register_slide_model(SlideModel, template):
register_slide(SlideModel.slide_callback_name, model_slide) 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. Set the active Slide.
callback: The name of the slide callback. callback: The name of the slide callback.
kwargs: Keyword arguments for the slide callback. kwargs: Keyword arguments for the slide callback.
""" """
kwargs = kwargs or {}
kwargs.update(callback=callback) kwargs.update(callback=callback)
config['projector_active_slide'] = kwargs config['projector_active_slide'] = kwargs
update_projector() update_projector()

View File

@ -5,6 +5,7 @@ from django.contrib.staticfiles.templatetags.staticfiles import static
from django.core.context_processors import csrf from django.core.context_processors import csrf
from django.dispatch import receiver, Signal from django.dispatch import receiver, Signal
from django.template.loader import render_to_string 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.api import config, ConfigPage, ConfigVariable
from openslides.config.signals import config_signal from openslides.config.signals import config_signal
@ -99,18 +100,14 @@ def countdown(sender, **kwargs):
""" """
Returns JavaScript for the projector Returns JavaScript for the projector
""" """
start = int(config['countdown_start_stamp']) value = SortedDict()
duration = int(config['countdown_time']) value['load_file'] = static('javascript/countdown.js')
pause = int(config['countdown_pause_stamp']) value['projector_countdown_start'] = int(config['countdown_start_stamp'])
state = config['countdown_state'] value['projector_countdown_duration'] = int(config['countdown_time'])
value['projector_countdown_pause'] = int(config['countdown_pause_stamp'])
return { value['projector_countdown_state'] = config['countdown_state']
'load_file': static('javascript/countdown.js'), value['call'] = 'update_countdown();'
'call': 'update_countdown();', return value
'projector_countdown_start': start,
'projector_countdown_duration': duration,
'projector_countdown_pause': pause,
'projector_countdown_state': state}
def get_projector_html(): def get_projector_html():
""" """

View File

@ -85,7 +85,7 @@ class ActivateView(RedirectView):
ProjectorSocketHandler.send_updates( ProjectorSocketHandler.send_updates(
{'calls': {'load_pdf': {'url': url, 'page_num': kwargs['page_num']}}}) {'calls': {'load_pdf': {'url': url, 'page_num': kwargs['page_num']}}})
else: 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_scroll'] = config.get_default('projector_scroll')
config['projector_scale'] = config.get_default('projector_scale') config['projector_scale'] = config.get_default('projector_scale')
call_on_projector({'scroll': config['projector_scroll'], call_on_projector({'scroll': config['projector_scroll'],
@ -186,10 +186,10 @@ class CountdownControllView(RedirectView):
try: try:
config['countdown_time'] = \ config['countdown_time'] = \
int(self.request.GET['countdown_time']) int(self.request.GET['countdown_time'])
except ValueError: except (ValueError, AttributeError):
pass
except AttributeError:
pass pass
else:
reset_countdown()
update_projector_overlay('projector_countdown') update_projector_overlay('projector_countdown')
def get_ajax_context(self, **kwargs): def get_ajax_context(self, **kwargs):

View File

@ -1,7 +1,12 @@
# -*- coding: utf-8 -*-
from django.db import models 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') name = models.CharField(max_length='255')
class Meta: class Meta:
@ -13,8 +18,12 @@ class RelatedItem(models.Model):
def get_agenda_title_supplement(self): def get_agenda_title_supplement(self):
return 'test item' return 'test item'
def get_absolute_url(self, *args, **kwargs): def get_absolute_url(self, link=None):
return '/absolute-url-here/' if link is None:
value = '/absolute-url-here/'
else:
value = super(RelatedItem, self).get_absolute_url(link)
return value
class BadRelatedItem(models.Model): class BadRelatedItem(models.Model):

View File

@ -2,6 +2,7 @@
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.test.client import Client from django.test.client import Client
from mock import patch, MagicMock
from openslides.agenda.models import Item, Speaker from openslides.agenda.models import Item, Speaker
from openslides.config.api import config 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(Speaker.objects.get(person=self.speaker1, item=self.item1).end_time)
self.assertIsNotNone(speaker2_item1.begin_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 config['agenda_couple_countdown_and_speakers'] = True
self.assertTrue(config['countdown_state'] == 'inactive')
speaker1_item1 = Speaker.objects.add(self.speaker1, self.item1) speaker1_item1 = Speaker.objects.add(self.speaker1, self.item1)
speaker1_item1.begin_speach() self.item1.is_active_slide = MagicMock(return_value=True)
self.assertTrue(config['countdown_state'] == 'active')
speaker1_item1.end_speach()
self.assertTrue(config['countdown_state'] == 'paused')
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() 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' config['countdown_state'] = 'active'
speaker1_item1.end_speach() 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): class SpeakerViewTestCase(TestCase):
@ -233,7 +243,7 @@ class GlobalListOfSpeakersLinks(SpeakerViewTestCase):
self.assertRedirects(response, '/projector/dashboard/') 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.') 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/') response = self.speaker1_client.get('/agenda/list_of_speakers/')
self.assertRedirects(response, '/agenda/1/') self.assertRedirects(response, '/agenda/1/')
@ -242,7 +252,7 @@ class GlobalListOfSpeakersLinks(SpeakerViewTestCase):
self.assertRedirects(response, '/projector/dashboard/') 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.') 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/') response = self.speaker1_client.get('/agenda/list_of_speakers/add/')
self.assertRedirects(response, '/agenda/1/') self.assertRedirects(response, '/agenda/1/')
self.assertEqual(Speaker.objects.get(item__pk='1').person, self.speaker1) self.assertEqual(Speaker.objects.get(item__pk='1').person, self.speaker1)
@ -258,7 +268,7 @@ class GlobalListOfSpeakersLinks(SpeakerViewTestCase):
self.assertRedirects(response, '/projector/dashboard/') 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.') 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/') response = self.admin_client.get('/agenda/list_of_speakers/next/')
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/projector/dashboard/')
self.assertMessage(response, 'The list of speakers is empty.') self.assertMessage(response, 'The list of speakers is empty.')
@ -274,7 +284,7 @@ class GlobalListOfSpeakersLinks(SpeakerViewTestCase):
self.assertRedirects(response, '/projector/dashboard/') 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.') 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/') response = self.admin_client.get('/agenda/list_of_speakers/end_speach/')
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/projector/dashboard/')
self.assertMessage(response, 'There is no one speaking at the moment.') self.assertMessage(response, 'There is no one speaking at the moment.')

View File

@ -8,6 +8,7 @@ from mock import patch
from openslides.agenda.models import Item from openslides.agenda.models import Item
from openslides.agenda.slides import agenda_slide from openslides.agenda.slides import agenda_slide
from openslides.participant.models import User from openslides.participant.models import User
from openslides.projector.api import set_active_slide
from openslides.utils.test import TestCase from openslides.utils.test import TestCase
from .models import BadRelatedItem, RelatedItem from .models import BadRelatedItem, RelatedItem
@ -77,7 +78,26 @@ class ItemTest(TestCase):
def test_deleted_related_item(self): def test_deleted_related_item(self):
self.related.delete() self.related.delete()
self.assertFalse(RelatedItem.objects.all().exists()) 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): def test_bad_related_item(self):
bad = BadRelatedItem.objects.create(name='dhfne94irkgl2047fzvb') bad = BadRelatedItem.objects.create(name='dhfne94irkgl2047fzvb')

View File

@ -170,7 +170,7 @@ class ApiFunctions(TestCase):
def test_set_active_slide(self, mock_update_projector, mock_update_projector_overlay): def test_set_active_slide(self, mock_update_projector, mock_update_projector_overlay):
mock_config = {} mock_config = {}
with patch('openslides.projector.api.config', 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, self.assertEqual(mock_config,
{'projector_active_slide': {'callback': 'callback_name', {'projector_active_slide': {'callback': 'callback_name',
'some': 'kwargs'}}) 'some': 'kwargs'}})

View File

@ -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'])

View File

@ -3,6 +3,7 @@
from django.test.client import Client, RequestFactory from django.test.client import Client, RequestFactory
from mock import call, MagicMock, patch from mock import call, MagicMock, patch
from openslides.config.api import config
from openslides.projector.models import ProjectorSlide from openslides.projector.models import ProjectorSlide
from openslides.projector import views from openslides.projector import views
from openslides.utils.test import TestCase from openslides.utils.test import TestCase
@ -199,3 +200,19 @@ class CustomSlidesTest(TestCase):
response = self.admin_client.post(url, {'yes': 'true'}) response = self.admin_client.post(url, {'yes': 'true'})
self.assertRedirects(response, '/projector/dashboard/') self.assertRedirects(response, '/projector/dashboard/')
self.assertFalse(ProjectorSlide.objects.exists()) 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()