diff --git a/openslides/__main__.py b/openslides/__main__.py index d03bd3980..e67bebf4b 100644 --- a/openslides/__main__.py +++ b/openslides/__main__.py @@ -233,7 +233,8 @@ def runserver(settings, args): start_browser(browser_url) # Now the settings is available and the function can be imported. - from openslides.utils.tornado_webserver import run_tornado + # TODO: only start tornado when it is used as wsgi server + from openslides.utils.autoupdate import run_tornado run_tornado(args.address, port) diff --git a/openslides/agenda/models.py b/openslides/agenda/models.py index 51b5098ed..1ee63e997 100644 --- a/openslides/agenda/models.py +++ b/openslides/agenda/models.py @@ -12,17 +12,17 @@ from mptt.models import MPTTModel, TreeForeignKey from openslides.config.api import config from openslides.core.models import Tag -from openslides.projector.api import (get_active_slide, reset_countdown, - start_countdown, stop_countdown, - update_projector, update_projector_overlay) +from openslides.projector.api import (reset_countdown, + start_countdown, stop_countdown) from openslides.projector.models import SlideMixin from openslides.utils.exceptions import OpenSlidesError from openslides.utils.models import AbsoluteUrlMixin +from openslides.utils.rest_api import RESTModelMixin from openslides.utils.utils import to_roman from openslides.users.models import User -class Item(SlideMixin, AbsoluteUrlMixin, MPTTModel): +class Item(RESTModelMixin, SlideMixin, AbsoluteUrlMixin, MPTTModel): """ An Agenda Item @@ -122,11 +122,6 @@ class Item(SlideMixin, AbsoluteUrlMixin, MPTTModel): class MPTTMeta: order_insertion_by = ['weight'] - def save(self, *args, **kwargs): - super(Item, self).save(*args, **kwargs) - if self.parent and self.parent.is_active_slide(): - update_projector() - def clean(self): """ Ensures that the children of orga items are only orga items. @@ -135,7 +130,7 @@ class Item(SlideMixin, AbsoluteUrlMixin, MPTTModel): raise ValidationError(_('Agenda items can not be child elements of an organizational item.')) if self.type == self.ORGANIZATIONAL_ITEM and self.get_descendants().filter(type=self.AGENDA_ITEM).exists(): raise ValidationError(_('Organizational items can not have agenda items as child elements.')) - return super(Item, self).clean() + return super().clean() def __str__(self): return self.get_title() @@ -153,14 +148,14 @@ class Item(SlideMixin, AbsoluteUrlMixin, MPTTModel): 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') + url = '%s&type=list_of_speakers' % super().get_absolute_url('projector') elif link == 'projector_summary': - url = '%s&type=summary' % super(Item, self).get_absolute_url('projector') + url = '%s&type=summary' % super().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) + url = super().get_absolute_url(link) return url def get_title(self): @@ -359,7 +354,7 @@ class SpeakerManager(models.Manager): return self.create(item=item, user=user, weight=weight + 1) -class Speaker(AbsoluteUrlMixin, models.Model): +class Speaker(RESTModelMixin, AbsoluteUrlMixin, models.Model): """ Model for the Speaker list. """ @@ -396,14 +391,6 @@ class Speaker(AbsoluteUrlMixin, models.Model): ('can_be_speaker', ugettext_noop('Can put oneself on the list of speakers')), ) - def save(self, *args, **kwargs): - super(Speaker, self).save(*args, **kwargs) - self.check_and_update_projector() - - def delete(self, *args, **kwargs): - super(Speaker, self).delete(*args, **kwargs) - self.check_and_update_projector() - def __str__(self): return str(self.user) @@ -417,17 +404,6 @@ class Speaker(AbsoluteUrlMixin, models.Model): url = super(Speaker, self).get_absolute_url(link) return url - def check_and_update_projector(self): - """ - Checks, if the agenda item, or parts of it, is on the projector. - If yes, it updates the projector. - """ - if self.item.is_active_slide(): - if get_active_slide().get('type', None) == 'list_of_speakers': - update_projector() - else: - update_projector_overlay('agenda_speaker') - def begin_speach(self): """ Let the user speak. @@ -448,10 +424,6 @@ class Speaker(AbsoluteUrlMixin, 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): """ @@ -462,7 +434,9 @@ class Speaker(AbsoluteUrlMixin, 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') + + def get_root_rest_element(self): + """ + Returns the item to this instance, which is the root rest element. + """ + return self.item diff --git a/openslides/agenda/views.py b/openslides/agenda/views.py index 7780f50ac..654cb4dd6 100644 --- a/openslides/agenda/views.py +++ b/openslides/agenda/views.py @@ -21,8 +21,7 @@ from openslides.projector.api import ( get_active_object, get_active_slide, get_projector_overlays_js, - get_overlays, - update_projector) + get_overlays) from openslides.utils import rest_api from openslides.utils.exceptions import OpenSlidesError from openslides.utils.pdf import stylesheet @@ -159,10 +158,6 @@ class Overview(TemplateView): # TODO: assure, that it is a valid tree context = self.get_context_data(**kwargs) - - if get_active_slide()['callback'] == 'agenda': - update_projector() - context = self.get_context_data(**kwargs) return self.render_to_response(context) diff --git a/openslides/assignment/models.py b/openslides/assignment/models.py index 8f5a20905..88002d4ee 100644 --- a/openslides/assignment/models.py +++ b/openslides/assignment/models.py @@ -11,15 +11,14 @@ from openslides.config.api import config from openslides.poll.models import (BaseOption, BasePoll, BaseVote, CollectDefaultVotesMixin, PublishPollMixin) -from openslides.projector.api import get_active_object, update_projector -from openslides.projector.models import RelatedModelMixin, SlideMixin +from openslides.projector.models import SlideMixin from openslides.utils.exceptions import OpenSlidesError from openslides.utils.models import AbsoluteUrlMixin from openslides.utils.utils import html_strong from openslides.users.models import User -class AssignmentCandidate(RelatedModelMixin, models.Model): +class AssignmentCandidate(models.Model): """ Many2Many table between an assignment and the candidates. """ @@ -34,12 +33,6 @@ class AssignmentCandidate(RelatedModelMixin, models.Model): def __str__(self): return str(self.person) - def get_related_model(self): - """ - Returns the assignment - """ - return self.assignment - class Assignment(SlideMixin, AbsoluteUrlMixin, models.Model): slide_callback_name = 'assignment' @@ -190,11 +183,6 @@ class Assignment(SlideMixin, AbsoluteUrlMixin, models.Model): candidate = self.assignment_candidates.get(person=person) candidate.elected = value candidate.save() - # update projector if assignment or assignmentpoll slide is active - active_object = get_active_object() - if (type(active_object) is type(self) and active_object.pk == self.pk) or \ - (type(active_object) is AssignmentPoll and active_object.assignment_id == self.pk): - update_projector() def is_elected(self, person): return person in self.elected @@ -224,16 +212,13 @@ class Assignment(SlideMixin, AbsoluteUrlMixin, models.Model): items = Item.objects.filter(content_type=ContentType.objects.get_for_model(Assignment), object_id=self.pk) for item in items: - someone_added = None for candidate in self.candidates: try: - someone_added = Speaker.objects.add(candidate, item) + Speaker.objects.add(candidate, item) except OpenSlidesError: # The Speaker is already on the list. Do nothing. # TODO: Find a smart way not to catch the error concerning AnonymousUser. pass - if someone_added is not None: - someone_added.check_and_update_projector() return poll @@ -290,7 +275,7 @@ class AssignmentOption(BaseOption): return str(self.candidate) -class AssignmentPoll(SlideMixin, RelatedModelMixin, CollectDefaultVotesMixin, +class AssignmentPoll(SlideMixin, CollectDefaultVotesMixin, PublishPollMixin, AbsoluteUrlMixin, BasePoll): slide_callback_name = 'assignmentpoll' @@ -323,9 +308,6 @@ class AssignmentPoll(SlideMixin, RelatedModelMixin, CollectDefaultVotesMixin, def get_assignment(self): return self.assignment - def get_related_model(self): - return self.assignment - def get_vote_values(self): if self.yesnoabstain: return [ugettext_noop('Yes'), ugettext_noop('No'), ugettext_noop('Abstain')] diff --git a/openslides/core/apps.py b/openslides/core/apps.py index caafdbf69..2f61497e6 100644 --- a/openslides/core/apps.py +++ b/openslides/core/apps.py @@ -11,8 +11,10 @@ class CoreAppConfig(AppConfig): from . import main_menu, widgets # noqa # Import all required stuff. + from django.db.models import signals from openslides.config.signals import config_signal from openslides.projector.api import register_slide_model + from openslides.utils.autoupdate import inform_changed_data_receiver from openslides.utils.rest_api import router from .signals import setup_general_config from .views import CustomSlideViewSet @@ -26,3 +28,8 @@ class CoreAppConfig(AppConfig): # Register viewset. router.register('core/customslide', CustomSlideViewSet) + + # Update data when any model of any installed app is saved or deleted + signals.post_save.connect(inform_changed_data_receiver, dispatch_uid='inform_changed_data_receiver') + signals.post_delete.connect(inform_changed_data_receiver, dispatch_uid='inform_changed_data_receiver') + # TODO: test if the m2m_changed signal is also needed diff --git a/openslides/core/signals.py b/openslides/core/signals.py index aae6de38d..15bb08f0e 100644 --- a/openslides/core/signals.py +++ b/openslides/core/signals.py @@ -4,7 +4,6 @@ from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_lazy, ugettext_noop from openslides.config.api import ConfigGroup, ConfigGroupedCollection, ConfigVariable -from openslides.projector.api import update_projector post_database_setup = Signal() @@ -108,8 +107,7 @@ def setup_general_config(sender, **kwargs): widget=forms.TextInput(), label=ugettext_lazy('Title'), help_text=ugettext_lazy('Also used for the default welcome slide.'), - required=False), - on_change=update_projector) + required=False)) welcome_text = ConfigVariable( name='welcome_text', diff --git a/openslides/global_settings.py b/openslides/global_settings.py index e54fe242f..7da0ea348 100644 --- a/openslides/global_settings.py +++ b/openslides/global_settings.py @@ -173,3 +173,8 @@ CKEDITOR_CONFIGS = { 'default': CKEDITOR_DEFAULT_CONFIG, 'images': CKEDITOR_IMG_CONFIG, } + + +# Use small alternative with tornado as frontend or big alternative with a +# webserver as wsgi server. +USE_TORNADO_AS_WSGI_SERVER = True diff --git a/openslides/mediafile/views.py b/openslides/mediafile/views.py index 847ec7362..1e7184d62 100644 --- a/openslides/mediafile/views.py +++ b/openslides/mediafile/views.py @@ -2,7 +2,6 @@ from django.http import HttpResponse from openslides.config.api import config from openslides.projector.api import get_active_slide -from openslides.utils.tornado_webserver import ProjectorSocketHandler from openslides.utils.views import (AjaxView, CreateView, DeleteView, RedirectView, ListView, UpdateView) @@ -119,8 +118,6 @@ class PdfNavBaseView(AjaxView): Tell connected clients to load an other pdf page. """ config['projector_active_slide'] = active_slide - ProjectorSocketHandler.send_updates( - {'calls': {'load_pdf_page': active_slide['page_num']}}) class PdfNextView(PdfNavBaseView): @@ -199,8 +196,4 @@ class PdfToggleFullscreenView(RedirectView): def get_ajax_context(self, *args, **kwargs): config['pdf_fullscreen'] = not config['pdf_fullscreen'] - active_slide = get_active_slide() - if active_slide['callback'] == 'mediafile': - ProjectorSocketHandler.send_updates( - {'calls': {'toggle_fullscreen': config['pdf_fullscreen']}}) return {'fullscreen': config['pdf_fullscreen']} diff --git a/openslides/motion/models.py b/openslides/motion/models.py index 14f4ad639..ce9645c57 100644 --- a/openslides/motion/models.py +++ b/openslides/motion/models.py @@ -9,7 +9,7 @@ from openslides.config.api import config from openslides.core.models import Tag from openslides.mediafile.models import Mediafile from openslides.poll.models import (BaseOption, BasePoll, BaseVote, CollectDefaultVotesMixin) -from openslides.projector.models import RelatedModelMixin, SlideMixin +from openslides.projector.models import SlideMixin from jsonfield import JSONField from openslides.utils.models import AbsoluteUrlMixin from openslides.users.models import User @@ -612,7 +612,7 @@ class MotionVersion(AbsoluteUrlMixin, models.Model): return self.active_version.exists() -class MotionSubmitter(RelatedModelMixin, models.Model): +class MotionSubmitter(models.Model): """Save the submitter of a Motion.""" motion = models.ForeignKey('Motion', related_name="submitter") @@ -625,9 +625,6 @@ class MotionSubmitter(RelatedModelMixin, models.Model): """Return the name of the submitter as string.""" return str(self.person) - def get_related_model(self): - return self.motion - class MotionSupporter(models.Model): """Save the submitter of a Motion.""" @@ -723,7 +720,7 @@ class MotionOption(BaseOption): """The VoteClass, to witch this Class links.""" -class MotionPoll(SlideMixin, RelatedModelMixin, CollectDefaultVotesMixin, +class MotionPoll(SlideMixin, CollectDefaultVotesMixin, AbsoluteUrlMixin, BasePoll): """The Class to saves the vote result for a motion poll.""" @@ -775,9 +772,6 @@ class MotionPoll(SlideMixin, RelatedModelMixin, CollectDefaultVotesMixin, # or call this in save() self.get_option_class()(poll=self).save() - def get_related_model(self): - return self.motion - def get_percent_base_choice(self): return config['motion_poll_100_percent_base'] diff --git a/openslides/motion/views.py b/openslides/motion/views.py index d4ee9165e..f4bef5474 100644 --- a/openslides/motion/views.py +++ b/openslides/motion/views.py @@ -10,7 +10,6 @@ from django.shortcuts import get_object_or_404 from openslides.agenda.views import CreateRelatedAgendaItemView as _CreateRelatedAgendaItemView from openslides.config.api import config from openslides.poll.views import PollFormView -from openslides.projector.api import get_active_slide, update_projector from openslides.utils.utils import html_strong, htmldiff from openslides.utils.views import (CreateView, CSVImportView, DeleteView, DetailView, ListView, PDFView, QuestionView, @@ -142,14 +141,6 @@ class MotionEditMixin(object): self.object.tags.clear() self.object.tags.add(*form.cleaned_data['tags']) - # Update the projector if the motion is on it. This can not be done in - # the model, because bulk_create does not call the save method. - active_slide = get_active_slide() - active_slide_pk = active_slide.get('pk', None) - if (active_slide['callback'] == 'motion' and - str(self.object.pk) == str(active_slide_pk)): - update_projector() - messages.success(self.request, self.get_success_message()) return HttpResponseRedirect(self.get_success_url()) diff --git a/openslides/projector/api.py b/openslides/projector/api.py index 1eb615cb4..5b98cf92b 100644 --- a/openslides/projector/api.py +++ b/openslides/projector/api.py @@ -4,7 +4,6 @@ from time import time from django.template.loader import render_to_string from openslides.config.api import config -from openslides.utils.tornado_webserver import ProjectorSocketHandler from openslides.utils.exceptions import OpenSlidesError from .signals import projector_overlays @@ -28,41 +27,6 @@ class SlideError(OpenSlidesError): pass -def update_projector(): - """ - Sends the data to the clients, who listen to the projector. - """ - # TODO: only send necessary html - ProjectorSocketHandler.send_updates({'content': get_projector_content()}) - - -def update_projector_overlay(overlay): - """ - Update one or all overlay on the projector. - - Checks if the overlay is activated and updates it in this case. - - The argument 'overlay' has to be an overlay object, the name of a - ovleray or None. If it is None, all overlays will be updated. - """ - if overlay is None: - overlays = [item for item in get_overlays().values()] - elif isinstance(overlay, str): - overlays = [get_overlays()[overlay]] - else: - overlays = [overlay] - - overlay_dict = {} - for overlay in overlays: - if overlay.is_active(): - overlay_dict[overlay.name] = { - 'html': overlay.get_projector_html(), - 'javascript': overlay.get_javascript()} - else: - overlay_dict[overlay.name] = None - ProjectorSocketHandler.send_updates({'overlays': overlay_dict}) - - def call_on_projector(calls): """ Sends data to the projector. @@ -70,10 +34,10 @@ def call_on_projector(calls): The argument call has to be a dictionary with the javascript function name as key and the argument for it as value. """ + # TODO: remove this function projector_js_cache = config['projector_js_cache'] projector_js_cache.update(calls) config['projector_js_cache'] = projector_js_cache - ProjectorSocketHandler.send_updates({'calls': calls}) def get_projector_content(slide_dict=None): @@ -181,8 +145,6 @@ def set_active_slide(callback, **kwargs): """ kwargs.update(callback=callback) config['projector_active_slide'] = kwargs - update_projector() - update_projector_overlay(None) def get_active_slide(): diff --git a/openslides/projector/models.py b/openslides/projector/models.py index f2b315a33..9aa1b2b1d 100644 --- a/openslides/projector/models.py +++ b/openslides/projector/models.py @@ -1,42 +1,8 @@ -from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse from openslides.utils.utils import int_or_none -class RelatedModelMixin(object): - """ - Mixin for motion related models, that appear on the motion slide. - """ - - def save(self, *args, **kwargs): - """ - Saves the model and updates the projector, if the motion in on it. - """ - from .api import update_projector - value = super(RelatedModelMixin, self).save(*args, **kwargs) - if self.get_related_model().is_active_slide(): - update_projector() - return value - - def delete(self, *args, **kwargs): - """ - Deletes the model and updates the projector, if the motion in on it. - """ - from .api import update_projector - value = super(RelatedModelMixin, self).delete(*args, **kwargs) - if self.get_related_model().is_active_slide(): - update_projector() - return value - - def get_related_model(self): - """ - Return the pk of the related model. - """ - raise ImproperlyConfigured( - '%s has to have a method "get_related_model_pk"' % type(self)) - - class SlideMixin(object): """ A Mixin for a Django-Model, for making the model a slide. @@ -47,31 +13,6 @@ class SlideMixin(object): Name of the callback to render the model as slide. """ - def save(self, *args, **kwargs): - """ - Updates the projector, if the object is on the projector and changed. - """ - from openslides.projector.api import update_projector - value = super(SlideMixin, self).save(*args, **kwargs) - if self.is_active_slide(): - update_projector() - return value - - def delete(self, *args, **kwargs): - """ - Updates the projector if the object is on the projector and is deleted. - """ - from openslides.projector.api import update_projector - # Checking active slide has to be done before calling super().delete() - # because super().delete() deletes the object and than we have no - # access to the former existing primary key any more. But updating - # projector has to be done after deleting the object of course. - update_required = self.is_active_slide() - value = super(SlideMixin, self).delete(*args, **kwargs) - if update_required: - update_projector() - return value - def get_absolute_url(self, link='projector'): """ Return the url to activate the slide, if link == 'projector'. diff --git a/openslides/projector/views.py b/openslides/projector/views.py index 87d6add69..8ef546f23 100644 --- a/openslides/projector/views.py +++ b/openslides/projector/views.py @@ -1,12 +1,10 @@ from openslides.config.api import config -from openslides.mediafile.models import Mediafile -from openslides.utils.tornado_webserver import ProjectorSocketHandler from openslides.utils.views import RedirectView, TemplateView from .api import (call_on_projector, get_active_slide, get_overlays, get_projector_content, get_projector_overlays_js, reset_countdown, set_active_slide, - start_countdown, stop_countdown, update_projector_overlay) + start_countdown, stop_countdown) class ProjectorView(TemplateView): @@ -53,14 +51,13 @@ class ActivateView(RedirectView): # we dont have to use set_active_slide, because is causes a content # reload. kwargs.update({'page_num': 1, 'pk': request.GET.get('pk')}) - url = Mediafile.objects.get(pk=kwargs['pk'], is_presentable=True).mediafile.url + # TODO: fix me + # url = Mediafile.objects.get(pk=kwargs['pk'], is_presentable=True).mediafile.url config['projector_active_slide'] = kwargs - ProjectorSocketHandler.send_updates( - {'calls': {'load_pdf': {'url': url, 'page_num': kwargs['page_num']}}}) + # ProjectorSocketHandler.send_updates( + # {'calls': {'load_pdf': {'url': url, 'page_num': kwargs['page_num']}}}) else: set_active_slide(kwargs['callback'], **dict(request.GET.items())) - call_on_projector({'scroll': config['projector_scroll'], - 'scale': config['projector_scale']}) class ProjectorControllView(RedirectView): @@ -121,7 +118,7 @@ class CountdownControllView(RedirectView): pass else: reset_countdown() - update_projector_overlay('projector_countdown') + # TODO: send signal to update data def get_ajax_context(self, **kwargs): return { @@ -143,7 +140,7 @@ class OverlayMessageView(RedirectView): config['projector_message'] = request.POST['message_text'] elif 'message-clean' in request.POST: config['projector_message'] = '' - update_projector_overlay('projector_message') + # TODO: update data def get_ajax_context(self, **kwargs): return { @@ -165,12 +162,12 @@ class ActivateOverlay(RedirectView): if kwargs['activate']: if not overlay.is_active(): overlay.set_active(True) - update_projector_overlay(overlay) + # Push new overlay to projector, somehow... self.active = True else: if overlay.is_active(): overlay.set_active(False) - update_projector_overlay(overlay) + # Push new overlay to projector, somehow... self.active = False def get_ajax_context(self, **kwargs): diff --git a/openslides/utils/tornado_webserver.py b/openslides/utils/autoupdate.py similarity index 68% rename from openslides/utils/tornado_webserver.py rename to openslides/utils/autoupdate.py index d8c2c52b8..d961273ea 100644 --- a/openslides/utils/tornado_webserver.py +++ b/openslides/utils/autoupdate.py @@ -18,7 +18,11 @@ from tornado.wsgi import WSGIContainer class DjangoStaticFileHandler(StaticFileHandler): - """Handels static data by using the django finders.""" + """ + Handels static data by using the django finders. + + Only needed in the "small" version with tornado as wsgi server. + """ def initialize(self): """Overwrite some attributes.""" @@ -47,25 +51,31 @@ class DjangoStaticFileHandler(StaticFileHandler): return absolute_path -class ProjectorSocketHandler(SockJSConnection): +class OpenSlidesSockJSConnection(SockJSConnection): """ - Handels the websocket for the projector. + Sockjs connections for OpenSlides. """ waiters = set() def on_open(self, info): - ProjectorSocketHandler.waiters.add(self) + OpenSlidesSockJSConnection.waiters.add(self) def on_close(self): - ProjectorSocketHandler.waiters.remove(self) + OpenSlidesSockJSConnection.waiters.remove(self) @classmethod def send_updates(cls, data): + # TODO: use a bluk send for waiter in cls.waiters: waiter.send(data) def run_tornado(addr, port): + """ + Starts the tornado webserver as wsgi server for OpenSlides. + + It runs in one thread. + """ # Don't try to read the command line args from openslides parse_command_line(args=[]) @@ -81,9 +91,9 @@ def run_tornado(addr, port): app = WSGIContainer(get_wsgi_application()) # Collect urls - projectpr_socket_js_router = SockJSRouter(ProjectorSocketHandler, '/projector/socket') from openslides.core.chatbox import ChatboxSocketHandler chatbox_socket_js_router = SockJSRouter(ChatboxSocketHandler, '/core/chatbox') + sock_js_router = SockJSRouter(OpenSlidesSockJSConnection, '/sockjs') other_urls = [ (r"%s(.*)" % settings.STATIC_URL, DjangoStaticFileHandler), (r'%s(.*)' % settings.MEDIA_URL, StaticFileHandler, {'path': settings.MEDIA_ROOT}), @@ -91,7 +101,36 @@ def run_tornado(addr, port): # Start the application debug = settings.DEBUG - tornado_app = Application(projectpr_socket_js_router.urls + chatbox_socket_js_router.urls + other_urls, debug=debug) + tornado_app = Application(sock_js_router.urls + chatbox_socket_js_router.urls + other_urls, debug=debug) server = HTTPServer(tornado_app) server.listen(port=port, address=addr) IOLoop.instance().start() + + +def inform_changed_data(*args): + """ + Informs all users about changed data. + + The arguments are Django/OpenSlides models. + """ + rest_urls = set() + for instance in args: + try: + rest_urls.add(instance.get_root_rest_url()) + except AttributeError: + # instance has no method get_root_rest_url + pass + + if settings.USE_TORNADO_AS_WSGI_SERVER: + for url in rest_urls: + OpenSlidesSockJSConnection.send_updates(url) + else: + pass + # TODO: fix me + + +def inform_changed_data_receiver(sender, instance, **kwargs): + """ + Receiver for the inform_changed_data function to use in a signal. + """ + inform_changed_data(instance) diff --git a/openslides/utils/rest_api.py b/openslides/utils/rest_api.py index 8fc21f00e..52ca3a4f9 100644 --- a/openslides/utils/rest_api.py +++ b/openslides/utils/rest_api.py @@ -1,3 +1,28 @@ +from django.core.urlresolvers import reverse from rest_framework import permissions, routers, viewsets # noqa router = routers.DefaultRouter() + + +class RESTModelMixin: + """ + Mixin for django models which are used in our rest api. + """ + + def get_root_rest_element(self): + """ + Returns the root rest instance. + + Uses self as default. + """ + return self + + def get_root_rest_url(self): + """ + Returns the detail url of the root model of this object. + """ + # Gets the default url-name in the same way as django rest framework + # does in relations.HyperlinkedModelSerializer + root_instance = self.get_root_rest_element() + rest_url = '%s-detail' % type(root_instance)._meta.object_name.lower() + return reverse(rest_url, args=[str(root_instance.pk)]) diff --git a/tests/agenda/test_list_of_speakers.py b/tests/agenda/test_list_of_speakers.py index aa8227081..54d0dcb98 100644 --- a/tests/agenda/test_list_of_speakers.py +++ b/tests/agenda/test_list_of_speakers.py @@ -72,23 +72,18 @@ class ListOfSpeakerModelTests(TestCase): self.assertIsNotNone(Speaker.objects.get(user=self.speaker1, item=self.item1).end_time) self.assertIsNotNone(speaker2_item1.begin_time) - @patch('openslides.agenda.models.update_projector_overlay') - def test_speach_coupled_with_countdown(self, mock_update_projector_overlay): + def test_speach_coupled_with_countdown(self): config['agenda_couple_countdown_and_speakers'] = True speaker1_item1 = Speaker.objects.add(self.speaker1, self.item1) self.item1.is_active_slide = MagicMock(return_value=True) speaker1_item1.begin_speach() 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): + def test_begin_speach_not_coupled_with_countdown(self): config['agenda_couple_countdown_and_speakers'] = False speaker1_item1 = Speaker.objects.add(self.speaker1, self.item1) @@ -98,7 +93,6 @@ class ListOfSpeakerModelTests(TestCase): config['countdown_state'] = 'active' speaker1_item1.end_speach() self.assertEqual(config['countdown_state'], 'active') - self.assertFalse(mock_update_projector_overlay.called) class SpeakerViewTestCase(TestCase): diff --git a/tests/projector/test_api.py b/tests/projector/test_api.py index 9373ce611..5c8a05b3f 100644 --- a/tests/projector/test_api.py +++ b/tests/projector/test_api.py @@ -5,58 +5,6 @@ from openslides.utils.test import TestCase class ApiFunctions(TestCase): - @patch('openslides.projector.api.get_projector_content') - @patch('openslides.projector.api.ProjectorSocketHandler') - def test_update_projector(self, mock_ProjectorSocketHandler, - mock_get_projector_content): - mock_get_projector_content.return_value = 'mock_string' - projector_api.update_projector() - mock_ProjectorSocketHandler.send_updates.assert_called_with( - {'content': 'mock_string'}) - - @patch('openslides.projector.api.get_overlays') - @patch('openslides.projector.api.ProjectorSocketHandler') - def test_update_projector_overlay(self, mock_ProjectorSocketHandler, - mock_get_overlays): - mock_overlay = MagicMock() - mock_overlay.name = 'mock_overlay_name' - mock_overlay.get_projector_html.return_value = 'mock_html_code' - mock_overlay.get_javascript.return_value = 'mock_javascript' - mock_get_overlays.return_value = {'mock_overlay': mock_overlay} - - # Test with active overlay - mock_overlay.is_active.return_value = False - projector_api.update_projector_overlay(None) - mock_ProjectorSocketHandler.send_updates.assert_called_with( - {'overlays': {'mock_overlay_name': None}}) - - # Test with active overlay - mock_overlay.is_active.return_value = True - projector_api.update_projector_overlay(None) - expected_data = {'overlays': {'mock_overlay_name': { - 'html': 'mock_html_code', - 'javascript': 'mock_javascript'}}} - mock_ProjectorSocketHandler.send_updates.assert_called_with(expected_data) - - # Test with overlay name as argument - projector_api.update_projector_overlay('mock_overlay') - mock_ProjectorSocketHandler.send_updates.assert_called_with(expected_data) - - # Test with overlay object as argument - projector_api.update_projector_overlay(mock_overlay) - mock_ProjectorSocketHandler.send_updates.assert_called_with(expected_data) - - @patch('openslides.projector.api.config') - @patch('openslides.projector.api.ProjectorSocketHandler') - def test_call_on_projector(self, mock_ProjectorSocketHandler, mock_config): - mock_config.__getitem__.return_value = {} - data = {'some_call': 'argument'} - projector_api.call_on_projector(data) - mock_ProjectorSocketHandler.send_updates.assert_called_with( - {'calls': data}) - mock_config.__getitem__.assert_called_with('projector_js_cache') - mock_config.__setitem__.assert_called_with('projector_js_cache', data) - @patch('openslides.projector.api.default_slide') def test_get_projector_content(self, mock_default_slide): mock_slide = MagicMock() @@ -156,17 +104,13 @@ class ApiFunctions(TestCase): mock_SlideModel.objects.get.side_effect = Exception self.assertRaises(projector_api.SlideError, used_args[1], pk=1) - @patch('openslides.projector.api.update_projector_overlay') - @patch('openslides.projector.api.update_projector') - def test_set_active_slide(self, mock_update_projector, mock_update_projector_overlay): + def test_set_active_slide(self): mock_config = {} with patch('openslides.projector.api.config', mock_config): projector_api.set_active_slide('callback_name', some='kwargs') self.assertEqual(mock_config, {'projector_active_slide': {'callback': 'callback_name', 'some': 'kwargs'}}) - mock_update_projector.assert_called_with() - mock_update_projector_overlay.assert_called_with(None) def test_get_active_slide(self): mock_config = {'projector_active_slide': 'value'} diff --git a/tests/projector/test_views.py b/tests/projector/test_views.py index 94d993484..84201e933 100644 --- a/tests/projector/test_views.py +++ b/tests/projector/test_views.py @@ -41,10 +41,9 @@ class ProjectorViewTest(TestCase): class ActivateViewTest(TestCase): rf = RequestFactory() - @patch('openslides.projector.views.call_on_projector') @patch('openslides.projector.views.config') @patch('openslides.projector.views.set_active_slide') - def test_get(self, mock_set_active_slide, mock_config, mock_call_on_projector): + def test_get(self, mock_set_active_slide, mock_config): view = views.ActivateView() view.request = self.rf.get('/?some_key=some_value') @@ -54,7 +53,6 @@ class ActivateViewTest(TestCase): **{'some_key': 'some_value'}) mock_config.get_default.assert_has_calls([]) self.assertEqual(mock_config.__setitem__.call_count, 0) - self.assertTrue(mock_call_on_projector.called) class ProjectorControllViewTest(TestCase): diff --git a/tests/utils/test_main.py b/tests/utils/test_main.py index 428c55750..5113b5216 100644 --- a/tests/utils/test_main.py +++ b/tests/utils/test_main.py @@ -7,7 +7,6 @@ from django.core.exceptions import ImproperlyConfigured from openslides.__main__ import ( add_general_arguments, django_command_line_utility, - runserver, start, syncdb) from openslides.config.api import config @@ -117,15 +116,6 @@ class TestOtherFunctions(TestCase): self.assertTrue(mock_syncdb.called) mock_runserver.assert_called_with(None, mock_args) - @patch('openslides.__main__.get_port') - @patch('openslides.utils.tornado_webserver.run_tornado') - @patch('openslides.__main__.start_browser') - def test_runserver(self, mock_start_browser, mock_run_tornado, mock_get_port): - mock_get_port.return_value = 8000 - mock_args = MagicMock() - runserver(settings=None, args=mock_args) - self.assertTrue(mock_run_tornado.called) - @patch('openslides.__main__.os.path.exists') @patch('openslides.__main__.os.makedirs') @patch('openslides.__main__.execute_from_command_line')