diff --git a/openslides/assignment/models.py b/openslides/assignment/models.py index 650d517d1..5579e5ea8 100644 --- a/openslides/assignment/models.py +++ b/openslides/assignment/models.py @@ -18,22 +18,31 @@ from django.utils.datastructures import SortedDict from openslides.utils.person import PersonField from openslides.utils.utils import html_strong from openslides.config.api import config -from openslides.projector.models import SlideMixin +from openslides.projector.models import SlideMixin, RelatedModelMixin from openslides.poll.models import ( BasePoll, CountInvalid, CountVotesCast, BaseOption, PublishPollMixin, BaseVote) -class AssignmentCandidate(models.Model): +class AssignmentCandidate(RelatedModelMixin, models.Model): + """ + Many2Many table between an assignment and the candidates. + """ assignment = models.ForeignKey("Assignment") person = PersonField(db_index=True) elected = models.BooleanField(default=False) blocked = models.BooleanField(default=False) + class Meta: + unique_together = ("assignment", "person") + def __unicode__(self): return unicode(self.person) - class Meta: - unique_together = ("assignment", "person") + def get_related_model(self): + """ + Returns the assignment + """ + return self.assignment class Assignment(SlideMixin, models.Model): @@ -74,6 +83,12 @@ class Assignment(SlideMixin, models.Model): return reverse('assignment_delete', args=[str(self.id)]) return super(Assignment, self).get_absolute_url(link) + def get_slide_context(self, **context): + context.update({ + 'polls': self.poll_set.filter(published=True), + 'vote_results': self.vote_results(only_published=True)}) + return super(Assignment, self).get_slide_context(**context) + def set_status(self, status): status_dict = dict(self.STATUS) if status not in status_dict: @@ -241,15 +256,29 @@ class AssignmentOption(BaseOption): return unicode(self.candidate) -class AssignmentPoll(BasePoll, CountInvalid, CountVotesCast, PublishPollMixin): +class AssignmentPoll(RelatedModelMixin, CountInvalid, CountVotesCast, + PublishPollMixin, BasePoll): option_class = AssignmentOption assignment = models.ForeignKey(Assignment, related_name='poll_set') yesnoabstain = models.NullBooleanField() + def __unicode__(self): + return _("Ballot %d") % self.get_ballot() + + @models.permalink + def get_absolute_url(self, link='detail'): + if link == 'view' or link == 'detail' or link == 'update': + return ('assignment_poll_view', [str(self.pk)]) + if link == 'delete': + return ('assignment_poll_delete', [str(self.pk)]) + def get_assignment(self): return self.assignment + def get_related_model(self): + return self.assignment + def get_vote_values(self): if self.yesnoabstain is None: if config['assignment_poll_vote_values'] == 'votes': @@ -274,13 +303,3 @@ class AssignmentPoll(BasePoll, CountInvalid, CountVotesCast, PublishPollMixin): def get_ballot(self): return self.assignment.poll_set.filter(id__lte=self.id).count() - - @models.permalink - def get_absolute_url(self, link='detail'): - if link == 'view' or link == 'detail' or link == 'update': - return ('assignment_poll_view', [str(self.id)]) - if link == 'delete': - return ('assignment_poll_delete', [str(self.id)]) - - def __unicode__(self): - return _("Ballot %d") % self.get_ballot() diff --git a/openslides/assignment/slides.py b/openslides/assignment/slides.py index dee057a82..744fac9f6 100644 --- a/openslides/assignment/slides.py +++ b/openslides/assignment/slides.py @@ -10,29 +10,8 @@ :license: GNU GPL, see LICENSE for more details. """ -from django.template.loader import render_to_string - -from openslides.config.api import config -from openslides.projector.api import register_slide +from openslides.projector.api import register_slide_model from .models import Assignment -def assignment_slide(**kwargs): - """ - Slide for an Assignment - """ - assignment_pk = kwargs.get('pk', None) - try: - assignment = Assignment.objects.get(pk=assignment_pk) - except Assignment.DoesNotExist: - return '' - - polls = assignment.poll_set - context = { - 'polls': polls.filter(published=True), - 'vote_results': assignment.vote_results(only_published=True), - 'assignment': assignment} - - return render_to_string('assignment/slide.html', context) - -register_slide(Assignment.slide_callback_name, assignment_slide) +register_slide_model(Assignment, 'assignment/slide.html') diff --git a/openslides/assignment/templates/assignment/assignment_detail.html b/openslides/assignment/templates/assignment/assignment_detail.html index bba20fbaa..53daa97e5 100644 --- a/openslides/assignment/templates/assignment/assignment_detail.html +++ b/openslides/assignment/templates/assignment/assignment_detail.html @@ -179,7 +179,7 @@ {% endfor %} {% if assignment.candidates and perms.assignment.can_manage_assignment and assignment.status == "vot" %} - + {% trans 'New ballot' %} diff --git a/openslides/assignment/urls.py b/openslides/assignment/urls.py index fcc7dc16b..5fec7c4ce 100644 --- a/openslides/assignment/urls.py +++ b/openslides/assignment/urls.py @@ -99,6 +99,8 @@ urlpatterns = patterns('openslides.assignment.views', name='assignment_poll_delete', ), + # TODO: use seperate urls to publish and unpublish the poll + # see assignment_user_elected url(r'^poll/(?P\d+)/pub/$', SetPublishStatusView.as_view(), name='assignment_poll_publish_status', diff --git a/openslides/assignment/views.py b/openslides/assignment/views.py index eb44c59e1..822c996ac 100644 --- a/openslides/assignment/views.py +++ b/openslides/assignment/views.py @@ -246,10 +246,10 @@ class PollUpdateView(PollFormView): class SetPublishStatusView(SingleObjectMixin, RedirectView): model = AssignmentPoll permission_required = 'assignment.can_manage_assignment' - url_name = 'assignment_list' + url_name = 'assignment_detail' allow_ajax = True - def get_ajax_context(self): + def get_ajax_context(self, **kwargs): return {'published': self.object.published} def pre_redirect(self, *args, **kwargs): @@ -258,15 +258,11 @@ class SetPublishStatusView(SingleObjectMixin, RedirectView): except self.model.DoesNotExist: messages.error(self.request, _('Ballot ID %d does not exist.') % int(kwargs['poll_id'])) - return - if self.object.published: - self.object.set_published(False) else: - self.object.set_published(True) - if self.object.published: - messages.success(self.request, _("Ballot successfully published.")) - else: - messages.success(self.request, _("Ballot successfully unpublished.")) + if self.object.published: + self.object.set_published(False) + else: + self.object.set_published(True) class SetElectedView(SingleObjectMixin, RedirectView): @@ -281,14 +277,14 @@ class SetElectedView(SingleObjectMixin, RedirectView): self.elected = kwargs['elected'] self.object.set_elected(self.person, self.elected) - def get_ajax_context(self): + def get_ajax_context(self, **kwargs): if self.elected: link = reverse('assignment_user_not_elected', args=[self.object.id, self.person.person_id]) text = _('not elected') else: link = reverse('assignment_user_elected', - args=[self.self.object.id, self.person.person_id]) + args=[self.object.id, self.person.person_id]) text = _('elected') return {'elected': self.elected, 'link': link, 'text': text} diff --git a/openslides/motion/models.py b/openslides/motion/models.py index 053b43c49..bb204c1d5 100644 --- a/openslides/motion/models.py +++ b/openslides/motion/models.py @@ -23,14 +23,15 @@ from django.utils import formats from django.utils.translation import pgettext from django.utils.translation import ugettext as _, ugettext_lazy, ugettext_noop -from openslides.utils.jsonfield import JSONField -from openslides.utils.person import PersonField +from openslides.agenda.models import Item from openslides.config.api import config +from openslides.participant.models import User from openslides.poll.models import ( BaseOption, BasePoll, CountVotesCast, CountInvalid, BaseVote) -from openslides.participant.models import User -from openslides.projector.models import SlideMixin -from openslides.agenda.models import Item +from openslides.projector.models import SlideMixin, RelatedModelMixin +from openslides.projector.api import (update_projector, get_active_slide) +from openslides.utils.jsonfield import JSONField +from openslides.utils.person import PersonField from .exceptions import MotionError, WorkflowError @@ -584,7 +585,7 @@ class MotionVersion(models.Model): return self.active_version.exists() -class MotionSubmitter(models.Model): +class MotionSubmitter(RelatedModelMixin, models.Model): """Save the submitter of a Motion.""" motion = models.ForeignKey('Motion', related_name="submitter") @@ -597,6 +598,9 @@ class MotionSubmitter(models.Model): """Return the name of the submitter as string.""" return unicode(self.person) + def get_related_model(self): + return self.motion + class MotionSupporter(models.Model): """Save the submitter of a Motion.""" @@ -695,7 +699,7 @@ class MotionOption(BaseOption): """The VoteClass, to witch this Class links.""" -class MotionPoll(CountInvalid, CountVotesCast, BasePoll): +class MotionPoll(RelatedModelMixin, CountInvalid, CountVotesCast, BasePoll): """The Class to saves the poll results for a motion poll.""" motion = models.ForeignKey(Motion, related_name='polls') @@ -722,7 +726,8 @@ class MotionPoll(CountInvalid, CountVotesCast, BasePoll): return _('Vote %d') % self.poll_number def get_absolute_url(self, link='edit'): - """Return an URL for the poll. + """ + Return an URL for the poll. The keyargument 'link' can be 'edit' or 'delete'. """ @@ -744,9 +749,13 @@ class MotionPoll(CountInvalid, CountVotesCast, BasePoll): CountInvalid.append_pollform_fields(self, fields) CountVotesCast.append_pollform_fields(self, fields) + def get_related_model(self): + return self.motion + class State(models.Model): - """Defines a state for a motion. + """ + Defines a state for a motion. Every state belongs to a workflow. All states of a workflow are linked together via 'next_states'. One of these states is the first state, but this diff --git a/openslides/motion/slides.py b/openslides/motion/slides.py index 13b05c9a0..17ff5ca03 100644 --- a/openslides/motion/slides.py +++ b/openslides/motion/slides.py @@ -10,26 +10,7 @@ :license: GNU GPL, see LICENSE for more details. """ -from django.template.loader import render_to_string - -from openslides.projector.api import register_slide +from openslides.projector.api import register_slide_model from .models import Motion - -def motion_slide(**kwargs): - """ - Slide for the motion app. - """ - motion_pk = kwargs.get('pk', None) - try: - motion = Motion.objects.get(pk=motion_pk) - except Motion.DoesNotExist: - return '' - - context = { - 'motion': motion, - 'title': motion.title} - - return render_to_string('motion/slide.html', context) - -register_slide(Motion.slide_callback_name, motion_slide) +register_slide_model(Motion, 'motion/slide.html') diff --git a/openslides/motion/views.py b/openslides/motion/views.py index 260445d37..82bdf5156 100644 --- a/openslides/motion/views.py +++ b/openslides/motion/views.py @@ -11,36 +11,36 @@ :copyright: (c) 2011–2013 by the OpenSlides team, see AUTHORS. :license: GNU GPL, see LICENSE for more details. """ +from reportlab.platypus import SimpleDocTemplate from django.core.urlresolvers import reverse from django.contrib import messages from django.db import transaction from django.db.models import Model +from django.http import Http404, HttpResponseRedirect from django.utils.text import slugify from django.utils.translation import ugettext as _, ugettext_lazy, ugettext_noop -from django.http import Http404, HttpResponseRedirect - -from reportlab.platypus import SimpleDocTemplate +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.projector.projector import Widget from openslides.utils.pdf import stylesheet +from openslides.utils.template import Tab +from openslides.utils.utils import html_strong, htmldiff from openslides.utils.views import ( TemplateView, RedirectView, UpdateView, CreateView, DeleteView, PDFView, DetailView, ListView, FormView, QuestionMixin, SingleObjectMixin) -from openslides.utils.template import Tab -from openslides.utils.utils import html_strong, htmldiff -from openslides.poll.views import PollFormView -from openslides.projector.api import get_active_slide -from openslides.projector.projector import Widget -from openslides.config.api import config -from openslides.agenda.views import CreateRelatedAgendaItemView as _CreateRelatedAgendaItemView -from .models import (Motion, MotionSubmitter, MotionSupporter, MotionPoll, - MotionVersion, State, WorkflowError, Category) +from .csv_import import import_motions from .forms import (BaseMotionForm, MotionSubmitterMixin, MotionSupporterMixin, MotionDisableVersioningMixin, MotionCategoryMixin, MotionIdentifierMixin, MotionWorkflowMixin, MotionImportForm) +from .models import (Motion, MotionSubmitter, MotionSupporter, MotionPoll, + MotionVersion, State, WorkflowError, Category) from .pdf import motions_to_pdf, motion_to_pdf, motion_poll_to_pdf -from .csv_import import import_motions class MotionListView(ListView): @@ -127,6 +127,15 @@ class MotionEditMixin(object): MotionSupporter.objects.bulk_create( [MotionSupporter(motion=self.object, person=person) for person in form.cleaned_data['supporter']]) + + # 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 + unicode(self.object.pk) == unicode(active_slide_pk)): + update_projector() + messages.success(self.request, self.get_success_message()) return HttpResponseRedirect(self.get_success_url()) diff --git a/openslides/participant/models.py b/openslides/participant/models.py index 931f0f5ca..31f7f11c2 100644 --- a/openslides/participant/models.py +++ b/openslides/participant/models.py @@ -80,6 +80,11 @@ class User(SlideMixin, PersonMixin, Person, DjangoUser): return reverse('user_delete', args=[str(self.id)]) return super(User, self).get_absolute_url(link) + def get_slide_context(self, **context): + # Does not call super. In this case the context would override the name + # 'user'. + return {'shown_user': self} + @property def clean_name(self): if self.title: diff --git a/openslides/participant/slides.py b/openslides/participant/slides.py index 58ff0f91b..62a529ff1 100644 --- a/openslides/participant/slides.py +++ b/openslides/participant/slides.py @@ -10,39 +10,8 @@ :license: GNU GPL, see LICENSE for more details. """ -from django.template.loader import render_to_string - -from openslides.projector.api import register_slide +from openslides.projector.api import register_slide_model from .models import User, Group - -def user_slide(**kwargs): - """ - Slide for the user model. - """ - user_pk = kwargs.get('pk', None) - try: - user = User.objects.get(pk=user_pk) - except User.DoesNotExist: - return '' - - context = {'shown_user': user} - return render_to_string('participant/user_slide.html', context) - -register_slide(User.slide_callback_name, user_slide) - - -def group_slide(**kwargs): - """ - Slide for the group model. - """ - group_pk = kwargs.get('pk', None) - try: - group = Group.objects.get(pk=group_pk) - except Group.DoesNotExist: - return '' - - context = {'group': group} - return render_to_string('participant/group_slide.html', context) - -register_slide(Group.slide_callback_name, group_slide) +register_slide_model(User, 'participant/user_slide.html') +register_slide_model(Group, 'participant/group_slide.html') diff --git a/openslides/projector/api.py b/openslides/projector/api.py index a0937a54f..91f07b593 100644 --- a/openslides/projector/api.py +++ b/openslides/projector/api.py @@ -40,18 +40,17 @@ def update_projector(): def update_projector_overlay(overlay): """ - Update one overlay on the projector. + Update one or all overlay on the projector. Checks if the overlay is activated and updates it in this case. - 'overlay' has to be an overlay object, the name of a ovleray or None. - - If 'overlay' is None, then all overlays are updated. + 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 isinstance(overlay, basestring): - overlay = get_overlays()[overlay] if overlay is None: overlays = [overlay for overlay in get_overlays().values()] + elif isinstance(overlay, basestring): + overlays = [get_overlays()[overlay]] else: overlays = [overlay] @@ -132,6 +131,33 @@ def register_slide(name, callback): slide_callback[name] = callback +def register_slide_model(SlideModel, template): + """ + Shortcut for register_slide for a Model with the SlideMixin. + + The Argument 'SlideModel' has to be a Django-Model-Class, which is a subclass + of SlideMixin. Template has to be a string to the path of a template. + """ + + def model_slide(**kwargs): + """ + Return the html code for the model slide. + """ + slide_pk = kwargs.get('pk', None) + + try: + slide = SlideModel.objects.get(pk=slide_pk) + except SlideModel.DoesNotExist: + slide = None + context = {'slide': None} + else: + context = slide.get_slide_context() + + return render_to_string(template, context) + + register_slide(SlideModel.slide_callback_name, model_slide) + + def set_active_slide(callback, kwargs={}): """ Set the active Slide. @@ -147,7 +173,7 @@ def set_active_slide(callback, kwargs={}): def get_active_slide(): """ - Returns the dictonary, witch defindes the active slide. + Returns the dictonary, which defines the active slide. """ return config['projector_active_slide'] diff --git a/openslides/projector/models.py b/openslides/projector/models.py index 57d2a110f..eafd4b9d9 100644 --- a/openslides/projector/models.py +++ b/openslides/projector/models.py @@ -19,6 +19,39 @@ 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. @@ -34,42 +67,32 @@ class SlideMixin(object): Updates the projector, if 'self' is the active slide. """ from openslides.projector.api import update_projector - super(SlideMixin, self).save(*args, **kwargs) + value = super(SlideMixin, self).save(*args, **kwargs) if self.is_active_slide(): update_projector() + return value def delete(self, *args, **kwargs): from openslides.projector.api import update_projector - super(SlideMixin, self).delete(*args, **kwargs) + value = super(SlideMixin, self).delete(*args, **kwargs) if self.is_active_slide(): update_projector() - - def get_slide_callback_name(self): - """ - Returns the name of the slide callback, which is used to render the slide. - """ - if self.slide_callback_name is None: - raise ImproperlyConfigured( - "SlideMixin requires either a definition of 'slide_callback_name'" - " or an implementation of 'get_slide_callback_name()'") - else: - return self.slide_callback_name + return value def get_absolute_url(self, link='projector'): """ - Return the url to activate the slide, if link == 'projector' + Return the url to activate the slide, if link == 'projector'. """ - if link == 'projector': - url_name = 'projector_activate_slide' - elif link == 'projector_preview': - url_name = 'projector_preview' - if link in ('projector', 'projector_preview'): - return '%s?pk=%d' % ( + url_name = {'projector': 'projector_activate_slide', + 'projector_preview': 'projector_preview'}[link] + value = '%s?pk=%d' % ( reverse(url_name, - args=[self.get_slide_callback_name()]), + args=[self.slide_callback_name]), self.pk) - return super(SlideMixin, self).get_absolute_url(link) + else: + value = super(SlideMixin, self).get_absolute_url(link) + return value def is_active_slide(self): """ @@ -78,15 +101,24 @@ class SlideMixin(object): from openslides.projector.api import get_active_slide active_slide = get_active_slide() pk = int_or_none(active_slide.get('pk', None)) - - return (active_slide['callback'] == self.get_slide_callback_name() and + return (active_slide['callback'] == self.slide_callback_name and self.pk == pk) + def get_slide_context(self, **context): + """ + Returns the context for the template which renders the slide. + """ + slide_name = self._meta.object_name.lower() + context.update({'slide': self, + slide_name: self}) + return context + class ProjectorSlide(SlideMixin, models.Model): """ Model for Slides, only for the projector. Also called custom slides. """ + # TODO: Rename it to CustomSlide slide_callback_name = 'projector_slide' title = models.CharField(max_length=256, verbose_name=ugettext_lazy("Title")) diff --git a/openslides/projector/projector.py b/openslides/projector/projector.py index ede8e3f7e..92daea2d0 100644 --- a/openslides/projector/projector.py +++ b/openslides/projector/projector.py @@ -102,7 +102,7 @@ class Overlay(object): def set_active(self, active): """ - Publish or depublish the ovleray on the projector. + Publish or depublish the overlay on the projector. publish, if active is true, depublish, if active is false. diff --git a/openslides/projector/slides.py b/openslides/projector/slides.py index 6ecd7901e..cb20e3b0e 100644 --- a/openslides/projector/slides.py +++ b/openslides/projector/slides.py @@ -10,27 +10,9 @@ :license: GNU GPL, see LICENSE for more details. """ -from django.template.loader import render_to_string - -from openslides.config.api import config -from openslides.projector.api import register_slide +from openslides.projector.api import register_slide_model from .models import ProjectorSlide -def projector_slide(**kwargs): - """ - Return the html code for a custom slide. - """ - slide_pk = kwargs.get('pk', None) - - try: - slide = ProjectorSlide.objects.get(pk=slide_pk) - except ProjectorSlide.DoesNotExist: - slide = None - - context = {'slide': slide} - return render_to_string('projector/slide_projectorslide.html', context) - - -register_slide('projector_slide', projector_slide) +register_slide_model(ProjectorSlide, 'projector/slide_projectorslide.html') diff --git a/openslides/utils/tornado_webserver.py b/openslides/utils/tornado_webserver.py index f626ef474..a6a7219c6 100644 --- a/openslides/utils/tornado_webserver.py +++ b/openslides/utils/tornado_webserver.py @@ -39,8 +39,14 @@ class DjangoStaticFileHandler(StaticFileHandler): class ProjectorSocketHandler(WebSocketHandler): + """ + Handels the websocket for the projector. + """ waiters = set() + # The following lines can be uncommented, if there are any problems with + # websockts in iOS Safari 5.0 + ## def allow_draft76(self): ## # for iOS 5.0 Safari ## return True