Merge pull request #1377 from normanjaeckel/WorkflowAndPermissions

Hide motions from non staff users in early workflow state.
This commit is contained in:
Norman Jäckel 2015-01-02 21:03:12 +01:00
commit a93e5aa785
7 changed files with 161 additions and 12 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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