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.
"""
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')

View File

@ -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()

View File

@ -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():
"""

View File

@ -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):

View File

@ -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):

View File

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

View File

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

View File

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

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 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()