Permission system for MotionVersions

This commit is contained in:
Oskar Hahn 2013-02-03 13:24:29 +01:00
parent e7c65b3db1
commit ef7f6cf476
5 changed files with 141 additions and 30 deletions

View File

@ -57,10 +57,8 @@ class Motion(SlideMixin, models.Model):
""" """
prefix = "motion" prefix = "motion"
# TODO: Use this attribute for the default_version, if the permission system active_version = models.ForeignKey(
# is deactivated. Maybe it has to be renamed. 'MotionVersion', null=True, related_name="active_version")
permitted_version = models.ForeignKey(
'MotionVersion', null=True, blank=True, related_name="permitted")
state_id = models.CharField(max_length=3) state_id = models.CharField(max_length=3)
# Log (Translatable) # Log (Translatable)
identifier = models.CharField(max_length=255, null=True, blank=True, identifier = models.CharField(max_length=255, null=True, blank=True,
@ -110,6 +108,7 @@ class Motion(SlideMixin, models.Model):
else: else:
# We do not need to save the motion version # We do not need to save the motion version
return return
for attr in ['title', 'text', 'reason']: for attr in ['title', 'text', 'reason']:
_attr = '_%s' % attr _attr = '_%s' % attr
try: try:
@ -118,8 +117,17 @@ class Motion(SlideMixin, models.Model):
except AttributeError: except AttributeError:
# If the _attr was not set, use the value from last_version # If the _attr was not set, use the value from last_version
setattr(version, attr, getattr(self.last_version, attr)) setattr(version, attr, getattr(self.last_version, attr))
if version.id is None:
# TODO: auto increment the version_number in the Database
version_number = self.versions.aggregate(Max('version_number'))['version_number__max'] or 0
version.version_number = version_number + 1
version.save() version.save()
if not self.state.version_permission or self.active_version is None:
self.active_version = version
self.save()
def get_absolute_url(self, link='detail'): def get_absolute_url(self, link='detail'):
if link == 'view' or link == 'detail': if link == 'view' or link == 'detail':
return reverse('motion_detail', args=[str(self.id)]) return reverse('motion_detail', args=[str(self.id)])
@ -218,7 +226,7 @@ class Motion(SlideMixin, models.Model):
pass pass
else: else:
if type(version) is int: if type(version) is int:
version = self.versions.all()[version] version = self.versions.get(version_number=version)
elif type(version) is not MotionVersion: elif type(version) is not MotionVersion:
raise ValueError('The argument \'version\' has to be int or ' raise ValueError('The argument \'version\' has to be int or '
'MotionVersion, not %s' % type(version)) 'MotionVersion, not %s' % type(version))
@ -234,7 +242,7 @@ class Motion(SlideMixin, models.Model):
""" """
# TODO: Fix the case, that the motion has no Version # TODO: Fix the case, that the motion has no Version
try: try:
return self.versions.order_by('id').reverse()[0] return self.versions.order_by('-version_number')[0]
except IndexError: except IndexError:
return self.new_version return self.new_version
@ -360,8 +368,38 @@ class Motion(SlideMixin, models.Model):
def write_log(self, message, person=None): def write_log(self, message, person=None):
MotionLog.objects.create(motion=self, message=message, person=person) MotionLog.objects.create(motion=self, message=message, person=person)
def activate_version(self, version):
"""
Activate a version of this motion.
'version' can be a version object, or the version_number of a version.
"""
if type(version) is int:
version = self.versions.get(version_number=version)
self.active_version = version
if version.rejected:
version.rejected = False
version.save()
def reject_version(self, version):
"""
Reject a version of this motion.
'version' can be a version object, or the version_number of a version.
"""
if type(version) is int:
version = self.versions.get(version_number=version)
if version.active:
raise MotionError('The active version can not be rejected')
version.rejected = True
version.save()
class MotionVersion(models.Model): class MotionVersion(models.Model):
version_number = models.PositiveIntegerField(default=1)
title = models.CharField(max_length=255, verbose_name=ugettext_lazy("Title")) title = models.CharField(max_length=255, verbose_name=ugettext_lazy("Title"))
text = models.TextField(verbose_name=_("Text")) text = models.TextField(verbose_name=_("Text"))
reason = models.TextField(null=True, blank=True, verbose_name=ugettext_lazy("Reason")) reason = models.TextField(null=True, blank=True, verbose_name=ugettext_lazy("Reason"))
@ -371,8 +409,12 @@ class MotionVersion(models.Model):
identifier = models.CharField(max_length=255, verbose_name=ugettext_lazy("Version identifier")) identifier = models.CharField(max_length=255, verbose_name=ugettext_lazy("Version identifier"))
note = models.TextField(null=True, blank=True) note = models.TextField(null=True, blank=True)
class Meta:
unique_together = ("motion", "version_number")
def __unicode__(self): def __unicode__(self):
return "%s Version %s" % (self.motion, self.version_number) counter = self.version_number or _('new')
return "%s Version %s" % (self.motion, counter)
def get_absolute_url(self, link='detail'): def get_absolute_url(self, link='detail'):
if link == 'view' or link == 'detail': if link == 'view' or link == 'detail':
@ -380,11 +422,8 @@ class MotionVersion(models.Model):
str(self.version_number)]) str(self.version_number)])
@property @property
def version_number(self): def active(self):
if self.pk is None: return self.active_version.exists()
return 'new'
return (MotionVersion.objects.filter(motion=self.motion)
.filter(id__lte=self.pk).count())
class Category(models.Model): class Category(models.Model):
@ -418,6 +457,9 @@ class MotionLog(models.Model):
else: else:
return "%s %s by %s" % (self.time, _(self.message), self.person) return "%s %s by %s" % (self.time, _(self.message), self.person)
class MotionError(Exception):
pass
class MotionVote(BaseVote): class MotionVote(BaseVote):
option = models.ForeignKey('MotionOption') option = models.ForeignKey('MotionOption')

View File

@ -12,6 +12,7 @@
<p>Reason: {{ motion.reason }}</p> <p>Reason: {{ motion.reason }}</p>
<p>Submitter: {% for submitter in motion.submitter.all %}{{ submitter.person }} {% endfor %}</p> <p>Submitter: {% for submitter in motion.submitter.all %}{{ submitter.person }} {% endfor %}</p>
<p>Supporter: {% for supporter in motion.supporter.all %}{{ supporter.person }} {% endfor %}</p> <p>Supporter: {% for supporter in motion.supporter.all %}{{ supporter.person }} {% endfor %}</p>
<p>Active Version: {{ motion.active_version }}</p>
<p>State: {{ motion.state }}</p> <p>State: {{ motion.state }}</p>
<h4>possible stats:</h4> <h4>possible stats:</h4>
<ul> <ul>
@ -25,11 +26,19 @@
<h4>Versions</h4> <h4>Versions</h4>
<ol> <ol>
{% for motion_version in motion.versions.all %} {% for motion_version in motion.versions.all %}
<li>
{% if motion_version.id == motion.version.id %} {% if motion_version.id == motion.version.id %}
<li><strong><a href="{% model_url motion_version %}">{{ motion_version }}</a></strong></li> <strong><a href="{% model_url motion_version %}">{{ motion_version }}</a></strong>
{% else %} {% else %}
<li><a href="{% model_url motion_version %}">{{ motion_version }}</a></li> <a href="{% model_url motion_version %}">{{ motion_version }}</a>
{% endif %} {% endif %}
{% if motion_version.active %}
(active)
{% endif %}
{% if motion_version.rejected %}
(rejected)
{% endif %}
</li>
{% endfor %} {% endfor %}
</ol> </ol>

View File

@ -38,11 +38,21 @@ urlpatterns = patterns('openslides.motion.views',
name='motion_delete', name='motion_delete',
), ),
url(r'^(?P<pk>\d+)/version/(?P<version_id>[1-9]\d*)/$', url(r'^(?P<pk>\d+)/version/(?P<version_number>\d+)/$',
'motion_detail', 'motion_detail',
name='motion_version_detail', name='motion_version_detail',
), ),
url(r'^(?P<pk>\d+)/version/(?P<version_number>\d+)/permit/$',
'version_permit',
name='motion_version_permit',
),
url(r'^(?P<pk>\d+)/version/(?P<version_number>\d+)/reject/$',
'version_reject',
name='motion_version_reject',
),
url(r'^(?P<pk>\d+)/support/$', url(r'^(?P<pk>\d+)/support/$',
'motion_support', 'motion_support',
name='motion_support', name='motion_support',

View File

@ -30,7 +30,7 @@ from openslides.projector.api import get_active_slide
from openslides.projector.projector import Widget, SLIDE from openslides.projector.projector import Widget, SLIDE
from openslides.config.models import config from openslides.config.models import config
from openslides.agenda.models import Item from openslides.agenda.models import Item
from .models import Motion, MotionSubmitter, MotionSupporter, MotionPoll from .models import Motion, MotionSubmitter, MotionSupporter, MotionPoll, MotionVersion
from .forms import (BaseMotionForm, MotionSubmitterMixin, MotionSupporterMixin, from .forms import (BaseMotionForm, MotionSubmitterMixin, MotionSupporterMixin,
MotionCreateNewVersionMixin, ConfigForm) MotionCreateNewVersionMixin, ConfigForm)
from .workflow import WorkflowError from .workflow import WorkflowError
@ -46,7 +46,19 @@ class MotionListView(ListView):
motion_list = MotionListView.as_view() motion_list = MotionListView.as_view()
class MotionDetailView(DetailView): class GetVersionMixin(object):
def get_object(self):
object = super(GetVersionMixin, self).get_object()
version_number = self.kwargs.get('version_number', None)
if version_number is not None:
try:
object.version = int(version_number)
except MotionVersion.DoesNotExist:
raise Http404('Version %s not found' % version_number)
return object
class MotionDetailView(GetVersionMixin, DetailView):
""" """
Show the details of one motion. Show the details of one motion.
""" """
@ -54,16 +66,6 @@ class MotionDetailView(DetailView):
model = Motion model = Motion
template_name = 'motion/motion_detail.html' template_name = 'motion/motion_detail.html'
def get_object(self):
object = super(MotionDetailView, self).get_object()
version_id = self.kwargs.get('version_id', None)
if version_id is not None:
try:
object.version = int(version_id) - 1
except IndexError:
raise Http404
return object
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(MotionDetailView, self).get_context_data(**kwargs) context = super(MotionDetailView, self).get_context_data(**kwargs)
context['allowed_actions'] = self.object.get_allowed_actions(self.request.user) context['allowed_actions'] = self.object.get_allowed_actions(self.request.user)
@ -157,6 +159,50 @@ class MotionDeleteView(DeleteView):
motion_delete = MotionDeleteView.as_view() motion_delete = MotionDeleteView.as_view()
class VersionPermitView(GetVersionMixin, SingleObjectMixin, QuestionMixin, RedirectView):
model = Motion
question_url_name = 'motion_version_detail'
success_url_name = 'motion_version_detail'
def get(self, *args, **kwargs):
self.object = self.get_object()
return super(VersionPermitView, self).get(*args, **kwargs)
def get_url_name_args(self):
return [self.object.pk, self.object.version.version_number]
def get_question(self):
return _('Are you sure you want permit Version %s?') % self.object.version.version_number
def case_yes(self):
self.object.activate_version(self.object.version)
self.object.save()
version_permit = VersionPermitView.as_view()
class VersionRejectView(GetVersionMixin, SingleObjectMixin, QuestionMixin, RedirectView):
model = Motion
question_url_name = 'motion_version_detail'
success_url_name = 'motion_version_detail'
def get(self, *args, **kwargs):
self.object = self.get_object()
return super(VersionRejectView, self).get(*args, **kwargs)
def get_url_name_args(self):
return [self.object.pk, self.object.version.version_number]
def get_question(self):
return _('Are you sure you want reject Version %s?') % self.object.version.version_number
def case_yes(self):
self.object.reject_version(self.object.version)
self.object.save()
version_reject = VersionRejectView.as_view()
class SupportView(SingleObjectMixin, QuestionMixin, RedirectView): class SupportView(SingleObjectMixin, QuestionMixin, RedirectView):
""" """
Classed based view to support or unsupport a motion. Use Classed based view to support or unsupport a motion. Use

View File

@ -10,13 +10,14 @@ _workflow = None
class State(object): class State(object):
def __init__(self, id, name, next_states=[], create_poll=False, support=False, def __init__(self, id, name, next_states=[], create_poll=False, support=False,
edit_as_submitter=False): edit_as_submitter=False, version_permission=True):
self.id = id self.id = id
self.name = name self.name = name
self.next_states = next_states self.next_states = next_states
self.create_poll = create_poll self.create_poll = create_poll
self.support = support self.support = support
self.edit_as_submitter = edit_as_submitter self.edit_as_submitter = edit_as_submitter
self.version_permission = version_permission
def __unicode__(self): def __unicode__(self):
return self.name return self.name
@ -75,7 +76,10 @@ def populate_workflow(state, workflow):
DUMMY_STATE = State('dummy', ugettext('Unknwon state')) DUMMY_STATE = State('dummy', ugettext('Unknwon state'))
default_workflow = State('pub', ugettext('Published'), support=True, edit_as_submitter=True, next_states=[ default_workflow = State('pub', ugettext('Published'), support=True,
edit_as_submitter=True, version_permission=False)
default_workflow.next_states=[
State('per', ugettext('Permitted'), create_poll=True, edit_as_submitter=True, next_states=[ State('per', ugettext('Permitted'), create_poll=True, edit_as_submitter=True, next_states=[
State('acc', ugettext('Accepted')), State('acc', ugettext('Accepted')),
State('rej', ugettext('Rejected')), State('rej', ugettext('Rejected')),
@ -84,4 +88,4 @@ default_workflow = State('pub', ugettext('Published'), support=True, edit_as_sub
State('noc', ugettext('Not Concerned')), State('noc', ugettext('Not Concerned')),
State('com', ugettext('Commited a bill')), State('com', ugettext('Commited a bill')),
State('rev', ugettext('Needs Review'))]), State('rev', ugettext('Needs Review'))]),
State('nop', ugettext('Rejected (not authorized)'))]) State('nop', ugettext('Rejected (not authorized)'))]