Permission system for MotionVersions
This commit is contained in:
parent
e7c65b3db1
commit
ef7f6cf476
@ -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')
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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
|
||||||
|
@ -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)'))]
|
||||||
|
Loading…
Reference in New Issue
Block a user