diff --git a/CHANGELOG b/CHANGELOG index 001b7d0db..5ba0f2c3d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,8 @@ http://openslides.org Version 1.6.2 (unreleased) ========================== [https://github.com/OpenSlides/OpenSlides/milestones/1.6.2] +Motions: +- Added possibility to hide motions from non staff users in some states. Other: - Cleaned up utils.views to increase performance when fetching single objects diff --git a/docs/de/Motion.rst b/docs/de/Motion.rst index 581ce5dac..0abce2b7d 100644 --- a/docs/de/Motion.rst +++ b/docs/de/Motion.rst @@ -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 ------------- diff --git a/openslides/motion/models.py b/openslides/motion/models.py index 7f8351c51..54a5b3e08 100644 --- a/openslides/motion/models.py +++ b/openslides/motion/models.py @@ -475,6 +475,7 @@ class Motion(SlideMixin, AbsoluteUrlMixin, models.Model): The dictonary contains the following actions. + * see * update / edit * delete * create_poll @@ -484,9 +485,14 @@ class Motion(SlideMixin, AbsoluteUrlMixin, models.Model): * reset_state """ actions = { - 'update': ((self.is_submitter(person) and - self.state.allow_submitter_edit) or - person.has_perm('motion.can_manage_motion')), + 'see': (person.has_perm('motion.can_see_motion') and + (not self.state.required_permission_to_see or + 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'), @@ -783,6 +789,16 @@ class State(models.Model): icon = models.CharField(max_length=255) """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) """If true, persons can support the motion in this state.""" diff --git a/openslides/motion/pdf.py b/openslides/motion/pdf.py index 39bcc8093..9858e2f3d 100644 --- a/openslides/motion/pdf.py +++ b/openslides/motion/pdf.py @@ -14,17 +14,16 @@ from openslides.config.api import config from openslides.participant.models import Group, User from openslides.utils.pdf import stylesheet -from .models import Category, Motion +from .models import Category # Needed to count the delegates # TODO: find another way to do this. -def motions_to_pdf(pdf): +def motions_to_pdf(pdf, motions): """ Create a PDF with all motions. """ - motions = Motion.objects.all() motions = natsorted(motions, key=attrgetter('identifier')) all_motion_cover(pdf, motions) for motion in motions: diff --git a/openslides/motion/views.py b/openslides/motion/views.py index 9a5a382bc..cbd1cbbdf 100644 --- a/openslides/motion/views.py +++ b/openslides/motion/views.py @@ -32,8 +32,30 @@ class MotionListView(ListView): """ View, to list all motions. """ - required_permission = 'motion.can_see_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() @@ -42,9 +64,14 @@ class MotionDetailView(DetailView): """ Show one motion. """ - required_permission = 'motion.can_see_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): """ Return the template context. @@ -376,10 +403,15 @@ class VersionDiffView(DetailView): """ Show diff between two versions of a motion. """ - required_permission = 'motion.can_see_motion' model = Motion 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): """ Return the template context with versions and html diff strings. @@ -678,18 +710,28 @@ create_agenda_item = CreateRelatedAgendaItemView.as_view() 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 False, the view returns a PDF with only one motion. """ - required_permission = 'motion.can_see_motion' model = Motion top_space = 0 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): if self.print_all_motions: obj = None @@ -716,7 +758,12 @@ class MotionPDFView(SingleObjectMixin, PDFView): Append PDF objects. """ 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: motion_to_pdf(pdf, self.get_object()) diff --git a/tests/motion/test_pdf.py b/tests/motion/test_pdf.py index 7becbd12c..40f485f7d 100644 --- a/tests/motion/test_pdf.py +++ b/tests/motion/test_pdf.py @@ -15,6 +15,11 @@ class MotionPDFTest(TestCase): self.admin_client = Client() 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): Motion.objects.create( title='Test Title chieM6Aing8Eegh9ePhu', @@ -24,3 +29,17 @@ class MotionPDFTest(TestCase): '
  • Element 2 rel0liiGh0bi3ree6Jei
  • ') response = self.admin_client.get('/motion/1/pdf/') 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) diff --git a/tests/motion/test_views.py b/tests/motion/test_views.py index 96e1c819b..273215c4b 100644 --- a/tests/motion/test_views.py +++ b/tests/motion/test_views.py @@ -54,6 +54,20 @@ class TestMotionListView(MotionViewTestCase): def test_get(self): 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): def test_get(self): @@ -99,6 +113,21 @@ class TestMotionDetailView(MotionViewTestCase): self.assertNotContains(self.admin_client.get('/motion/1/'), 'registered') 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): def test_get(self): @@ -110,6 +139,28 @@ class TestMotionDetailVersionView(MotionViewTestCase): 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): url = '/motion/new/'