Refactored state access level by renaming state field to restriction.
This commit is contained in:
parent
3fd5d19daa
commit
6f24b7c169
@ -35,8 +35,9 @@ Motions:
|
|||||||
follow recommendation, manage submitters and supporters, change motion
|
follow recommendation, manage submitters and supporters, change motion
|
||||||
category, motion block and origin and manage motion polls [#3913].
|
category, motion block and origin and manage motion polls [#3913].
|
||||||
- Added new permission to create amendments [#4128].
|
- Added new permission to create amendments [#4128].
|
||||||
- Added new flag to motion state to control access for different users. Added
|
- Added new flag to motion state to control access restrictions for different
|
||||||
new permission to see motions in some internal state [#4235, #4518].
|
users. Added new permission to see motions in some internal state
|
||||||
|
[#4235, #4518, #4521].
|
||||||
- Allowed submitters to set state of new motions in complex and customized
|
- Allowed submitters to set state of new motions in complex and customized
|
||||||
workflow [#4236].
|
workflow [#4236].
|
||||||
- Added multi select action to manage submitters, tags, states and
|
- Added multi select action to manage submitters, tags, states and
|
||||||
|
@ -37,19 +37,27 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
|||||||
is_submitter = False
|
is_submitter = False
|
||||||
|
|
||||||
# Check see permission for this motion.
|
# Check see permission for this motion.
|
||||||
from .models import State
|
restriction = full["state_restriction"]
|
||||||
|
|
||||||
if await async_has_perm(user_id, "motions.can_manage"):
|
# Managers can see all motions.
|
||||||
level = State.MANAGERS_ONLY
|
permission = await async_has_perm(user_id, "motions.can_manage")
|
||||||
elif await async_has_perm(
|
# If restriction field is an empty list, everybody can see the motion.
|
||||||
user_id, "motions.can_manage_metadata"
|
permission = permission or not restriction
|
||||||
) or await async_has_perm(user_id, "motions.can_see_internal"):
|
|
||||||
level = State.EXTENDED_MANAGERS
|
if not permission:
|
||||||
elif is_submitter:
|
# Parse values of restriction field.
|
||||||
level = State.EXTENDED_MANAGERS_AND_SUBMITTER
|
for value in restriction:
|
||||||
else:
|
if value == "managers_only":
|
||||||
level = State.ALL
|
# permission remains false
|
||||||
permission = level >= full["state_access_level"]
|
break
|
||||||
|
elif value in ("motions.can_see_internal", "motions.can_manage_metadata"):
|
||||||
|
if await async_has_perm(user_id, value):
|
||||||
|
permission = True
|
||||||
|
break
|
||||||
|
elif value == "is_submitter":
|
||||||
|
if is_submitter:
|
||||||
|
permission = True
|
||||||
|
break
|
||||||
|
|
||||||
# Parse single motion.
|
# Parse single motion.
|
||||||
if permission:
|
if permission:
|
||||||
|
25
openslides/motions/migrations/0023_state_restriction_1.py
Normal file
25
openslides/motions/migrations/0023_state_restriction_1.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 2.1.7 on 2019-03-20 15:49
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import jsonfield.encoder
|
||||||
|
import jsonfield.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("motions", "0022_auto_20190320_0840")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="state",
|
||||||
|
name="restriction",
|
||||||
|
field=jsonfield.fields.JSONField(
|
||||||
|
default=list,
|
||||||
|
dump_kwargs={
|
||||||
|
"cls": jsonfield.encoder.JSONEncoder,
|
||||||
|
"separators": (",", ":"),
|
||||||
|
},
|
||||||
|
load_kwargs={},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
36
openslides/motions/migrations/0023_state_restriction_2.py
Normal file
36
openslides/motions/migrations/0023_state_restriction_2.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Generated by Django 2.1.7 on 2019-03-20 15:49
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import jsonfield.encoder
|
||||||
|
import jsonfield.fields
|
||||||
|
|
||||||
|
|
||||||
|
def copy_access_level(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Sets new restriction field of states according to access_level.
|
||||||
|
"""
|
||||||
|
# We get the model from the versioned app registry;
|
||||||
|
# if we directly import it, it will be the wrong version.
|
||||||
|
State = apps.get_model("motions", "State")
|
||||||
|
for state in State.objects.all():
|
||||||
|
if state.access_level == 3:
|
||||||
|
state.restriction = ["managers_only"]
|
||||||
|
elif state.access_level == 2:
|
||||||
|
state.restriction = [
|
||||||
|
"motions.can_see_internal",
|
||||||
|
"motions.can_manage_metadata",
|
||||||
|
]
|
||||||
|
elif state.access_level == 1:
|
||||||
|
state.restriction = [
|
||||||
|
"motions.can_see_internal",
|
||||||
|
"motions.can_manage_metadata",
|
||||||
|
"is_submitter",
|
||||||
|
]
|
||||||
|
state.save(skip_autoupdate=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("motions", "0023_state_restriction_1")]
|
||||||
|
|
||||||
|
operations = [migrations.RunPython(copy_access_level)]
|
10
openslides/motions/migrations/0023_state_restriction_3.py
Normal file
10
openslides/motions/migrations/0023_state_restriction_3.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Generated by Django 2.1.7 on 2019-03-20 15:55
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("motions", "0023_state_restriction_2")]
|
||||||
|
|
||||||
|
operations = [migrations.RemoveField(model_name="state", name="access_level")]
|
@ -985,24 +985,6 @@ class State(RESTModelMixin, models.Model):
|
|||||||
state.
|
state.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ALL = 0
|
|
||||||
EXTENDED_MANAGERS_AND_SUBMITTER = 1
|
|
||||||
EXTENDED_MANAGERS = 2
|
|
||||||
MANAGERS_ONLY = 3
|
|
||||||
|
|
||||||
ACCESS_LEVELS = (
|
|
||||||
(ALL, "All users with permission to see motions"),
|
|
||||||
(
|
|
||||||
EXTENDED_MANAGERS_AND_SUBMITTER,
|
|
||||||
"Submitters, authorized users (with permission to see internal motions), managers and users with permission to manage metadata",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
EXTENDED_MANAGERS,
|
|
||||||
"Only authorized users (with permission to see internal motions), managers and users with permission to manage metadata",
|
|
||||||
),
|
|
||||||
(MANAGERS_ONLY, "Only managers"),
|
|
||||||
)
|
|
||||||
|
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
"""A string representing the state."""
|
"""A string representing the state."""
|
||||||
|
|
||||||
@ -1024,11 +1006,22 @@ class State(RESTModelMixin, models.Model):
|
|||||||
Default value is 'primary' (blue).
|
Default value is 'primary' (blue).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_level = models.IntegerField(choices=ACCESS_LEVELS, default=0)
|
restriction = JSONField(default=list)
|
||||||
"""
|
"""
|
||||||
Defines which users may see motions in this state e. g. only managers,
|
Defines which users may see motions in this state:
|
||||||
authorized users with permission to see internal motiosn, users with permission
|
|
||||||
to manage metadata and submitters.
|
Contains a list of one or more of the following strings:
|
||||||
|
* motions.can_see_internal
|
||||||
|
* motions.can_manage_metadata
|
||||||
|
* is_submitter
|
||||||
|
* managers_only
|
||||||
|
|
||||||
|
If the list is empty, everybody with the general permission to see motions
|
||||||
|
can see this motion. If the list contains 'managers_only', only managers with
|
||||||
|
motions.can_manage permission may see this motion. In all other cases the user
|
||||||
|
shall have one of the given permissions respectivly is submitter of the motion.
|
||||||
|
|
||||||
|
Default: Empty list so everybody can see the motion.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
allow_support = models.BooleanField(default=False)
|
allow_support = models.BooleanField(default=False)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
import jsonschema
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
@ -101,7 +102,7 @@ class StateSerializer(ModelSerializer):
|
|||||||
"name",
|
"name",
|
||||||
"recommendation_label",
|
"recommendation_label",
|
||||||
"css_class",
|
"css_class",
|
||||||
"access_level",
|
"restriction",
|
||||||
"allow_support",
|
"allow_support",
|
||||||
"allow_create_poll",
|
"allow_create_poll",
|
||||||
"allow_submitter_edit",
|
"allow_submitter_edit",
|
||||||
@ -113,6 +114,30 @@ class StateSerializer(ModelSerializer):
|
|||||||
"workflow",
|
"workflow",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate_restriction(self, value):
|
||||||
|
"""
|
||||||
|
Ensures that the value is a list and only contains valid values.
|
||||||
|
"""
|
||||||
|
schema = {
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "Motion workflow state restriction field schema",
|
||||||
|
"description": "An array containing one or more explicit strings to control restriction for motions in this state.",
|
||||||
|
"type": "array",
|
||||||
|
"enum": [
|
||||||
|
"motions.can_see_internal",
|
||||||
|
"motions.can_manage_metadata",
|
||||||
|
"is_submitter",
|
||||||
|
"managers_only",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate value.
|
||||||
|
try:
|
||||||
|
jsonschema.validate(request.data, schema)
|
||||||
|
except jsonschema.ValidationError as err:
|
||||||
|
raise ValidationError({"detail": str(err)})
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class WorkflowSerializer(ModelSerializer):
|
class WorkflowSerializer(ModelSerializer):
|
||||||
"""
|
"""
|
||||||
@ -369,7 +394,7 @@ class MotionSerializer(ModelSerializer):
|
|||||||
polls = MotionPollSerializer(many=True, read_only=True)
|
polls = MotionPollSerializer(many=True, read_only=True)
|
||||||
modified_final_version = CharField(allow_blank=True, required=False)
|
modified_final_version = CharField(allow_blank=True, required=False)
|
||||||
reason = CharField(allow_blank=True, required=False)
|
reason = CharField(allow_blank=True, required=False)
|
||||||
state_access_level = SerializerMethodField()
|
state_restriction = SerializerMethodField()
|
||||||
text = CharField(allow_blank=True)
|
text = CharField(allow_blank=True)
|
||||||
title = CharField(max_length=255)
|
title = CharField(max_length=255)
|
||||||
amendment_paragraphs = AmendmentParagraphsJSONSerializerField(required=False)
|
amendment_paragraphs = AmendmentParagraphsJSONSerializerField(required=False)
|
||||||
@ -404,7 +429,7 @@ class MotionSerializer(ModelSerializer):
|
|||||||
"supporters",
|
"supporters",
|
||||||
"state",
|
"state",
|
||||||
"state_extension",
|
"state_extension",
|
||||||
"state_access_level",
|
"state_restriction",
|
||||||
"statute_paragraph",
|
"statute_paragraph",
|
||||||
"workflow_id",
|
"workflow_id",
|
||||||
"recommendation",
|
"recommendation",
|
||||||
@ -509,9 +534,9 @@ class MotionSerializer(ModelSerializer):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_state_access_level(self, motion):
|
def get_state_restriction(self, motion):
|
||||||
"""
|
"""
|
||||||
Returns the access level of this state. The default is 0 so everybody
|
Returns the restriction of this state. The default is an empty list so everybody
|
||||||
with permission to see motions can see this motion.
|
with permission to see motions can see this motion.
|
||||||
"""
|
"""
|
||||||
return motion.state.access_level
|
return motion.state.restriction
|
||||||
|
@ -477,11 +477,11 @@ class RetrieveMotion(TestCase):
|
|||||||
username=f"user_{index}", password="password"
|
username=f"user_{index}", password="password"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_guest_state_with_access_level(self):
|
def test_guest_state_with_restriction(self):
|
||||||
config["general_system_enable_anonymous"] = True
|
config["general_system_enable_anonymous"] = True
|
||||||
guest_client = APIClient()
|
guest_client = APIClient()
|
||||||
state = self.motion.state
|
state = self.motion.state
|
||||||
state.access_level = State.MANAGERS_ONLY
|
state.restriction = ["managers_only"]
|
||||||
state.save()
|
state.save()
|
||||||
# The cache has to be cleared, see:
|
# The cache has to be cleared, see:
|
||||||
# https://github.com/OpenSlides/OpenSlides/issues/3396
|
# https://github.com/OpenSlides/OpenSlides/issues/3396
|
||||||
@ -490,17 +490,16 @@ class RetrieveMotion(TestCase):
|
|||||||
response = guest_client.get(reverse("motion-detail", args=[self.motion.pk]))
|
response = guest_client.get(reverse("motion-detail", args=[self.motion.pk]))
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
def test_admin_state_with_access_level(self):
|
def test_admin_state_with_restriction(self):
|
||||||
state = self.motion.state
|
state = self.motion.state
|
||||||
state.access_level = State.MANAGERS_ONLY
|
state.restriction = ["managers_only"]
|
||||||
|
|
||||||
state.save()
|
state.save()
|
||||||
response = self.client.get(reverse("motion-detail", args=[self.motion.pk]))
|
response = self.client.get(reverse("motion-detail", args=[self.motion.pk]))
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
def test_submitter_state_with_access_level(self):
|
def test_submitter_state_with_restriction(self):
|
||||||
state = self.motion.state
|
state = self.motion.state
|
||||||
state.access_level = State.EXTENDED_MANAGERS_AND_SUBMITTER
|
state.restriction = ["is_submitter"]
|
||||||
state.save()
|
state.save()
|
||||||
user = get_user_model().objects.create_user(
|
user = get_user_model().objects.create_user(
|
||||||
username="username_ohS2opheikaSa5theijo",
|
username="username_ohS2opheikaSa5theijo",
|
||||||
|
@ -29,7 +29,7 @@ def all_data():
|
|||||||
"supporters_id": [],
|
"supporters_id": [],
|
||||||
"state_id": 1,
|
"state_id": 1,
|
||||||
"state_extension": None,
|
"state_extension": None,
|
||||||
"state_access_level": 0,
|
"state_restriction": [],
|
||||||
"statute_paragraph_id": None,
|
"statute_paragraph_id": None,
|
||||||
"workflow_id": 1,
|
"workflow_id": 1,
|
||||||
"recommendation_id": None,
|
"recommendation_id": None,
|
||||||
@ -118,7 +118,7 @@ def all_data():
|
|||||||
"supporters_id": [],
|
"supporters_id": [],
|
||||||
"state_id": 1,
|
"state_id": 1,
|
||||||
"state_extension": None,
|
"state_extension": None,
|
||||||
"state_access_level": 0,
|
"state_restriction": [],
|
||||||
"statute_paragraph_id": None,
|
"statute_paragraph_id": None,
|
||||||
"workflow_id": 1,
|
"workflow_id": 1,
|
||||||
"recommendation_id": None,
|
"recommendation_id": None,
|
||||||
@ -151,7 +151,7 @@ def all_data():
|
|||||||
"supporters_id": [],
|
"supporters_id": [],
|
||||||
"state_id": 1,
|
"state_id": 1,
|
||||||
"state_extension": None,
|
"state_extension": None,
|
||||||
"state_access_level": 0,
|
"state_restriction": [],
|
||||||
"statute_paragraph_id": 1,
|
"statute_paragraph_id": 1,
|
||||||
"workflow_id": 1,
|
"workflow_id": 1,
|
||||||
"recommendation_id": None,
|
"recommendation_id": None,
|
||||||
@ -178,7 +178,7 @@ def all_data():
|
|||||||
"name": "submitted",
|
"name": "submitted",
|
||||||
"recommendation_label": None,
|
"recommendation_label": None,
|
||||||
"css_class": "primary",
|
"css_class": "primary",
|
||||||
"access_level": 0,
|
"restriction": [],
|
||||||
"allow_support": True,
|
"allow_support": True,
|
||||||
"allow_create_poll": True,
|
"allow_create_poll": True,
|
||||||
"allow_submitter_edit": True,
|
"allow_submitter_edit": True,
|
||||||
@ -194,7 +194,7 @@ def all_data():
|
|||||||
"name": "accepted",
|
"name": "accepted",
|
||||||
"recommendation_label": "Acceptance",
|
"recommendation_label": "Acceptance",
|
||||||
"css_class": "success",
|
"css_class": "success",
|
||||||
"access_level": 0,
|
"restriction": [],
|
||||||
"allow_support": False,
|
"allow_support": False,
|
||||||
"allow_create_poll": False,
|
"allow_create_poll": False,
|
||||||
"allow_submitter_edit": False,
|
"allow_submitter_edit": False,
|
||||||
@ -210,7 +210,7 @@ def all_data():
|
|||||||
"name": "rejected",
|
"name": "rejected",
|
||||||
"recommendation_label": "Rejection",
|
"recommendation_label": "Rejection",
|
||||||
"css_class": "danger",
|
"css_class": "danger",
|
||||||
"access_level": 0,
|
"restriction": [],
|
||||||
"allow_support": False,
|
"allow_support": False,
|
||||||
"allow_create_poll": False,
|
"allow_create_poll": False,
|
||||||
"allow_submitter_edit": False,
|
"allow_submitter_edit": False,
|
||||||
@ -226,7 +226,7 @@ def all_data():
|
|||||||
"name": "not decided",
|
"name": "not decided",
|
||||||
"recommendation_label": "No decision",
|
"recommendation_label": "No decision",
|
||||||
"css_class": "default",
|
"css_class": "default",
|
||||||
"access_level": 0,
|
"restriction": [],
|
||||||
"allow_support": False,
|
"allow_support": False,
|
||||||
"allow_create_poll": False,
|
"allow_create_poll": False,
|
||||||
"allow_submitter_edit": False,
|
"allow_submitter_edit": False,
|
||||||
|
Loading…
Reference in New Issue
Block a user