Merge pull request #1377 from normanjaeckel/WorkflowAndPermissions
Hide motions from non staff users in early workflow state.
This commit is contained in:
commit
a93e5aa785
@ -7,6 +7,8 @@ http://openslides.org
|
|||||||
Version 1.6.2 (unreleased)
|
Version 1.6.2 (unreleased)
|
||||||
==========================
|
==========================
|
||||||
[https://github.com/OpenSlides/OpenSlides/milestones/1.6.2]
|
[https://github.com/OpenSlides/OpenSlides/milestones/1.6.2]
|
||||||
|
Motions:
|
||||||
|
- Added possibility to hide motions from non staff users in some states.
|
||||||
|
|
||||||
Other:
|
Other:
|
||||||
- Cleaned up utils.views to increase performance when fetching single objects
|
- Cleaned up utils.views to increase performance when fetching single objects
|
||||||
|
@ -216,6 +216,21 @@ Komplexer Arbeitsablauf
|
|||||||
+---------------------+-----------------+---------------+------------+---------------+------------------------+---------------+
|
+---------------------+-----------------+---------------+------------+---------------+------------------------+---------------+
|
||||||
|
|
||||||
|
|
||||||
|
Experteneinstellung
|
||||||
|
'''''''''''''''''''
|
||||||
|
|
||||||
|
Es kann eingestellt werden, dass ein Antrag in einem bestimmten Status nur
|
||||||
|
für bestimmte Benutzer sichtbar ist. Die Anpassung kann vor dem ersten
|
||||||
|
Anlegen der Datenbank in der Datei ``openslides/motion/signals.py`` in der
|
||||||
|
Funktion ``create_builtin_workflows()`` erfolgen: Dazu beim gewünschten
|
||||||
|
Status beispielsweise die Zeile
|
||||||
|
``required_permission_to_see='motion.can_manage_motion',`` einfügen. Die
|
||||||
|
Anpassung kann aber auch bei bestehender Datenbank erfolgen: Dazu mit einem
|
||||||
|
entsprechenden Tool direkt auf die Datenbank zugreifen und beim gewünschten
|
||||||
|
Status das Feld ``required_permission_to_see`` beispielsweise mit der
|
||||||
|
Zeichenkette ``motion.can_manage_motion`` überschreiben.
|
||||||
|
|
||||||
|
|
||||||
Versionierung
|
Versionierung
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
@ -475,6 +475,7 @@ class Motion(SlideMixin, AbsoluteUrlMixin, models.Model):
|
|||||||
|
|
||||||
The dictonary contains the following actions.
|
The dictonary contains the following actions.
|
||||||
|
|
||||||
|
* see
|
||||||
* update / edit
|
* update / edit
|
||||||
* delete
|
* delete
|
||||||
* create_poll
|
* create_poll
|
||||||
@ -484,9 +485,14 @@ class Motion(SlideMixin, AbsoluteUrlMixin, models.Model):
|
|||||||
* reset_state
|
* reset_state
|
||||||
"""
|
"""
|
||||||
actions = {
|
actions = {
|
||||||
'update': ((self.is_submitter(person) and
|
'see': (person.has_perm('motion.can_see_motion') and
|
||||||
self.state.allow_submitter_edit) or
|
(not self.state.required_permission_to_see or
|
||||||
person.has_perm('motion.can_manage_motion')),
|
person.has_perm(self.state.required_permission_to_see) or
|
||||||
|
self.is_submitter(person))),
|
||||||
|
|
||||||
|
'update': (person.has_perm('motion.can_manage_motion') or
|
||||||
|
(self.is_submitter(person) and
|
||||||
|
self.state.allow_submitter_edit)),
|
||||||
|
|
||||||
'delete': person.has_perm('motion.can_manage_motion'),
|
'delete': person.has_perm('motion.can_manage_motion'),
|
||||||
|
|
||||||
@ -783,6 +789,16 @@ class State(models.Model):
|
|||||||
icon = models.CharField(max_length=255)
|
icon = models.CharField(max_length=255)
|
||||||
"""A string representing the url to the icon-image."""
|
"""A string representing the url to the icon-image."""
|
||||||
|
|
||||||
|
required_permission_to_see = models.CharField(max_length=255, blank=True)
|
||||||
|
"""
|
||||||
|
A permission string. If not empty, the user has to have this permission to
|
||||||
|
see a motion in this state.
|
||||||
|
|
||||||
|
To use this feature change the database entry of a state object and add
|
||||||
|
your favourite permission string. You can do this e. g. by editing the
|
||||||
|
definitions in create_builtin_workflows() in openslides/motion/signals.py.
|
||||||
|
"""
|
||||||
|
|
||||||
allow_support = models.BooleanField(default=False)
|
allow_support = models.BooleanField(default=False)
|
||||||
"""If true, persons can support the motion in this state."""
|
"""If true, persons can support the motion in this state."""
|
||||||
|
|
||||||
|
@ -14,17 +14,16 @@ from openslides.config.api import config
|
|||||||
from openslides.participant.models import Group, User
|
from openslides.participant.models import Group, User
|
||||||
from openslides.utils.pdf import stylesheet
|
from openslides.utils.pdf import stylesheet
|
||||||
|
|
||||||
from .models import Category, Motion
|
from .models import Category
|
||||||
|
|
||||||
# Needed to count the delegates
|
# Needed to count the delegates
|
||||||
# TODO: find another way to do this.
|
# TODO: find another way to do this.
|
||||||
|
|
||||||
|
|
||||||
def motions_to_pdf(pdf):
|
def motions_to_pdf(pdf, motions):
|
||||||
"""
|
"""
|
||||||
Create a PDF with all motions.
|
Create a PDF with all motions.
|
||||||
"""
|
"""
|
||||||
motions = Motion.objects.all()
|
|
||||||
motions = natsorted(motions, key=attrgetter('identifier'))
|
motions = natsorted(motions, key=attrgetter('identifier'))
|
||||||
all_motion_cover(pdf, motions)
|
all_motion_cover(pdf, motions)
|
||||||
for motion in motions:
|
for motion in motions:
|
||||||
|
@ -32,8 +32,30 @@ class MotionListView(ListView):
|
|||||||
"""
|
"""
|
||||||
View, to list all motions.
|
View, to list all motions.
|
||||||
"""
|
"""
|
||||||
required_permission = 'motion.can_see_motion'
|
|
||||||
model = Motion
|
model = Motion
|
||||||
|
required_permission = 'motion.can_see_motion'
|
||||||
|
# The template name must be set explicitly because the overridden method
|
||||||
|
# get_queryset() does not return a QuerySet any more so that Django can't
|
||||||
|
# generate the template name from the name of the model.
|
||||||
|
template_name = 'motion/motion_list.html'
|
||||||
|
# The attribute context_object_name must be set explicitly because the
|
||||||
|
# overridden method get_queryset() does not return a QuerySet any more so
|
||||||
|
# that Django can't generate the context_object_name from the name of the
|
||||||
|
# model.
|
||||||
|
context_object_name = 'motion_list'
|
||||||
|
|
||||||
|
def get_queryset(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns not a QuerySet but a filtered list of motions. Excludes motions
|
||||||
|
that the user is not allowed to see.
|
||||||
|
"""
|
||||||
|
queryset = super(MotionListView, self).get_queryset(*args, **kwargs)
|
||||||
|
motions = []
|
||||||
|
for motion in queryset:
|
||||||
|
if (not motion.state.required_permission_to_see or
|
||||||
|
self.request.user.has_perm(motion.state.required_permission_to_see)):
|
||||||
|
motions.append(motion)
|
||||||
|
return motions
|
||||||
|
|
||||||
motion_list = MotionListView.as_view()
|
motion_list = MotionListView.as_view()
|
||||||
|
|
||||||
@ -42,9 +64,14 @@ class MotionDetailView(DetailView):
|
|||||||
"""
|
"""
|
||||||
Show one motion.
|
Show one motion.
|
||||||
"""
|
"""
|
||||||
required_permission = 'motion.can_see_motion'
|
|
||||||
model = Motion
|
model = Motion
|
||||||
|
|
||||||
|
def check_permission(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Check if the request.user has the permission to see the motion.
|
||||||
|
"""
|
||||||
|
return self.get_object().get_allowed_actions(request.user)['see']
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Return the template context.
|
Return the template context.
|
||||||
@ -376,10 +403,15 @@ class VersionDiffView(DetailView):
|
|||||||
"""
|
"""
|
||||||
Show diff between two versions of a motion.
|
Show diff between two versions of a motion.
|
||||||
"""
|
"""
|
||||||
required_permission = 'motion.can_see_motion'
|
|
||||||
model = Motion
|
model = Motion
|
||||||
template_name = 'motion/motion_diff.html'
|
template_name = 'motion/motion_diff.html'
|
||||||
|
|
||||||
|
def check_permission(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Check if the request.user has the permission to see the motion.
|
||||||
|
"""
|
||||||
|
return self.get_object().get_allowed_actions(request.user)['see']
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Return the template context with versions and html diff strings.
|
Return the template context with versions and html diff strings.
|
||||||
@ -678,18 +710,28 @@ create_agenda_item = CreateRelatedAgendaItemView.as_view()
|
|||||||
|
|
||||||
class MotionPDFView(SingleObjectMixin, PDFView):
|
class MotionPDFView(SingleObjectMixin, PDFView):
|
||||||
"""
|
"""
|
||||||
Create the PDF for one, or all motions.
|
Create the PDF for one or for all motions.
|
||||||
|
|
||||||
If self.print_all_motions is True, the view returns a PDF with all motions.
|
If self.print_all_motions is True, the view returns a PDF with all motions.
|
||||||
|
|
||||||
If self.print_all_motions is False, the view returns a PDF with only one
|
If self.print_all_motions is False, the view returns a PDF with only one
|
||||||
motion.
|
motion.
|
||||||
"""
|
"""
|
||||||
required_permission = 'motion.can_see_motion'
|
|
||||||
model = Motion
|
model = Motion
|
||||||
top_space = 0
|
top_space = 0
|
||||||
print_all_motions = False
|
print_all_motions = False
|
||||||
|
|
||||||
|
def check_permission(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Checks if the requesting user has the permission to see the motion as
|
||||||
|
PDF.
|
||||||
|
"""
|
||||||
|
if self.print_all_motions:
|
||||||
|
is_allowed = request.user.has_perm('motion.can_see_motion')
|
||||||
|
else:
|
||||||
|
is_allowed = self.get_object().get_allowed_actions(request.user)['see']
|
||||||
|
return is_allowed
|
||||||
|
|
||||||
def get_object(self, *args, **kwargs):
|
def get_object(self, *args, **kwargs):
|
||||||
if self.print_all_motions:
|
if self.print_all_motions:
|
||||||
obj = None
|
obj = None
|
||||||
@ -716,7 +758,12 @@ class MotionPDFView(SingleObjectMixin, PDFView):
|
|||||||
Append PDF objects.
|
Append PDF objects.
|
||||||
"""
|
"""
|
||||||
if self.print_all_motions:
|
if self.print_all_motions:
|
||||||
motions_to_pdf(pdf)
|
motions = []
|
||||||
|
for motion in Motion.objects.all():
|
||||||
|
if (not motion.state.required_permission_to_see or
|
||||||
|
self.request.user.has_perm(motion.state.required_permission_to_see)):
|
||||||
|
motions.append(motion)
|
||||||
|
motions_to_pdf(pdf, motions)
|
||||||
else:
|
else:
|
||||||
motion_to_pdf(pdf, self.get_object())
|
motion_to_pdf(pdf, self.get_object())
|
||||||
|
|
||||||
|
@ -15,6 +15,11 @@ class MotionPDFTest(TestCase):
|
|||||||
self.admin_client = Client()
|
self.admin_client = Client()
|
||||||
self.admin_client.login(username='admin', password='admin')
|
self.admin_client.login(username='admin', password='admin')
|
||||||
|
|
||||||
|
# Registered
|
||||||
|
self.registered = User.objects.create_user('registered', 'registered@user.user', 'registered')
|
||||||
|
self.registered_client = Client()
|
||||||
|
self.registered_client.login(username='registered', password='registered')
|
||||||
|
|
||||||
def test_render_nested_list(self):
|
def test_render_nested_list(self):
|
||||||
Motion.objects.create(
|
Motion.objects.create(
|
||||||
title='Test Title chieM6Aing8Eegh9ePhu',
|
title='Test Title chieM6Aing8Eegh9ePhu',
|
||||||
@ -24,3 +29,17 @@ class MotionPDFTest(TestCase):
|
|||||||
'<li>Element 2 rel0liiGh0bi3ree6Jei</li></ul>')
|
'<li>Element 2 rel0liiGh0bi3ree6Jei</li></ul>')
|
||||||
response = self.admin_client.get('/motion/1/pdf/')
|
response = self.admin_client.get('/motion/1/pdf/')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_get_without_required_permission_from_state(self):
|
||||||
|
motion = Motion.objects.create(title='motion_title_zthguis8qqespgknme52')
|
||||||
|
motion.state.required_permission_to_see = 'motion.can_manage_motion'
|
||||||
|
motion.state.save()
|
||||||
|
response = self.registered_client.get('/motion/1/pdf/')
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_get_with_filtered_motion_list(self):
|
||||||
|
motion = Motion.objects.create(title='motion_title_qwgvzf6487guni0oikcc')
|
||||||
|
motion.state.required_permission_to_see = 'motion.can_manage_motion'
|
||||||
|
motion.state.save()
|
||||||
|
response = self.registered_client.get('/motion/pdf/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
@ -54,6 +54,20 @@ class TestMotionListView(MotionViewTestCase):
|
|||||||
def test_get(self):
|
def test_get(self):
|
||||||
self.check_url('/motion/', self.admin_client, 200)
|
self.check_url('/motion/', self.admin_client, 200)
|
||||||
|
|
||||||
|
def test_get_with_motion(self):
|
||||||
|
self.motion1.title = 'motion1_iozaixeeDuMah8sheGhe'
|
||||||
|
self.motion1.save()
|
||||||
|
response = self.admin_client.get('/motion/')
|
||||||
|
self.assertContains(response, 'motion1_iozaixeeDuMah8sheGhe')
|
||||||
|
|
||||||
|
def test_get_with_filtered_motion_list(self):
|
||||||
|
self.motion1.state.required_permission_to_see = 'motion.can_manage_motion'
|
||||||
|
self.motion1.state.save()
|
||||||
|
self.motion1.title = 'motion1_djfplquczyxasvvgdnmbr'
|
||||||
|
self.motion1.save()
|
||||||
|
response = self.registered_client.get('/motion/')
|
||||||
|
self.assertNotContains(response, 'motion1_djfplquczyxasvvgdnmbr')
|
||||||
|
|
||||||
|
|
||||||
class TestMotionDetailView(MotionViewTestCase):
|
class TestMotionDetailView(MotionViewTestCase):
|
||||||
def test_get(self):
|
def test_get(self):
|
||||||
@ -99,6 +113,21 @@ class TestMotionDetailView(MotionViewTestCase):
|
|||||||
self.assertNotContains(self.admin_client.get('/motion/1/'), 'registered')
|
self.assertNotContains(self.admin_client.get('/motion/1/'), 'registered')
|
||||||
self.assertContains(self.admin_client.get('/motion/1/'), 'empty')
|
self.assertContains(self.admin_client.get('/motion/1/'), 'empty')
|
||||||
|
|
||||||
|
def test_get_without_required_permission_from_state(self):
|
||||||
|
self.motion1.state.required_permission_to_see = 'motion.can_manage_motion'
|
||||||
|
self.motion1.state.save()
|
||||||
|
self.check_url('/motion/1/', self.admin_client, 200)
|
||||||
|
self.check_url('/motion/1/', self.registered_client, 403)
|
||||||
|
self.motion1.set_state(state=State.objects.get(name='permitted'))
|
||||||
|
self.motion1.save()
|
||||||
|
self.check_url('/motion/1/', self.registered_client, 200)
|
||||||
|
|
||||||
|
def test_get_without_required_permission_from_state_but_by_submitter(self):
|
||||||
|
self.motion1.state.required_permission_to_see = 'motion.can_manage_motion'
|
||||||
|
self.motion1.state.save()
|
||||||
|
self.motion1.add_submitter(self.registered)
|
||||||
|
self.check_url('/motion/1/', self.registered_client, 200)
|
||||||
|
|
||||||
|
|
||||||
class TestMotionDetailVersionView(MotionViewTestCase):
|
class TestMotionDetailVersionView(MotionViewTestCase):
|
||||||
def test_get(self):
|
def test_get(self):
|
||||||
@ -110,6 +139,28 @@ class TestMotionDetailVersionView(MotionViewTestCase):
|
|||||||
self.check_url('/motion/1/version/500/', self.admin_client, 404)
|
self.check_url('/motion/1/version/500/', self.admin_client, 404)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMotionVersionDiffView(MotionViewTestCase):
|
||||||
|
def test_get_without_required_permission_from_state(self):
|
||||||
|
self.motion1.reason = 'reason1_bnmkjiutufjbnvcde334'
|
||||||
|
self.motion1.save()
|
||||||
|
self.motion1.title = 'motion1_bnvhfzqsgxcyvasfr57t'
|
||||||
|
self.motion1.save(use_version=self.motion1.get_new_version())
|
||||||
|
|
||||||
|
response = self.registered_client.get(
|
||||||
|
'/motion/1/diff/',
|
||||||
|
{'rev1': '1', 'rev2': '2'})
|
||||||
|
self.assertNotContains(response, 'At least one version number is not valid.')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
self.motion1.state.required_permission_to_see = 'motion.can_manage_motion'
|
||||||
|
self.motion1.state.save()
|
||||||
|
|
||||||
|
response = self.registered_client.get(
|
||||||
|
'/motion/1/diff/',
|
||||||
|
{'rev1': '1', 'rev2': '2'})
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
|
||||||
class TestMotionCreateView(MotionViewTestCase):
|
class TestMotionCreateView(MotionViewTestCase):
|
||||||
url = '/motion/new/'
|
url = '/motion/new/'
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user