Merge pull request #899 from ostcar/projector

Added projector.api.register_slide_model
This commit is contained in:
Oskar Hahn 2013-10-10 09:49:46 -07:00
commit a9487c22a7
15 changed files with 195 additions and 180 deletions

View File

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

View File

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

View File

@ -179,7 +179,7 @@
{% endfor %}
{% if assignment.candidates and perms.assignment.can_manage_assignment and assignment.status == "vot" %}
<th class="span1 nobr">
<a href="{% url 'assignment_poll_create' assignment.id %}" class="btn btn-mini">
<a href="{% url 'assignment_poll_create' assignment.pk %}" class="btn btn-mini">
<i class="icon-plus"></i> {% trans 'New ballot' %}
</a>
</th>

View File

@ -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<pk>\d+)/pub/$',
SetPublishStatusView.as_view(),
name='assignment_poll_publish_status',

View File

@ -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
else:
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."))
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}

View File

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

View File

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

View File

@ -11,36 +11,36 @@
:copyright: (c) 20112013 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())

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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