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
|
||||
category, motion block and origin and manage motion polls [#3913].
|
||||
- Added new permission to create amendments [#4128].
|
||||
- Added new flag to motion state to control access for different users. Added
|
||||
new permission to see motions in some internal state [#4235, #4518].
|
||||
- Added new flag to motion state to control access restrictions for different
|
||||
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
|
||||
workflow [#4236].
|
||||
- Added multi select action to manage submitters, tags, states and
|
||||
|
@ -37,19 +37,27 @@ class MotionAccessPermissions(BaseAccessPermissions):
|
||||
is_submitter = False
|
||||
|
||||
# Check see permission for this motion.
|
||||
from .models import State
|
||||
restriction = full["state_restriction"]
|
||||
|
||||
if await async_has_perm(user_id, "motions.can_manage"):
|
||||
level = State.MANAGERS_ONLY
|
||||
elif await async_has_perm(
|
||||
user_id, "motions.can_manage_metadata"
|
||||
) or await async_has_perm(user_id, "motions.can_see_internal"):
|
||||
level = State.EXTENDED_MANAGERS
|
||||
elif is_submitter:
|
||||
level = State.EXTENDED_MANAGERS_AND_SUBMITTER
|
||||
else:
|
||||
level = State.ALL
|
||||
permission = level >= full["state_access_level"]
|
||||
# Managers can see all motions.
|
||||
permission = await async_has_perm(user_id, "motions.can_manage")
|
||||
# If restriction field is an empty list, everybody can see the motion.
|
||||
permission = permission or not restriction
|
||||
|
||||
if not permission:
|
||||
# Parse values of restriction field.
|
||||
for value in restriction:
|
||||
if value == "managers_only":
|
||||
# permission remains false
|
||||
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.
|
||||
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.
|
||||
"""
|
||||
|
||||
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)
|
||||
"""A string representing the state."""
|
||||
|
||||
@ -1024,11 +1006,22 @@ class State(RESTModelMixin, models.Model):
|
||||
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,
|
||||
authorized users with permission to see internal motiosn, users with permission
|
||||
to manage metadata and submitters.
|
||||
Defines which users may see motions in this state:
|
||||
|
||||
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)
|
||||
|
@ -1,5 +1,6 @@
|
||||
from typing import Dict, Optional
|
||||
|
||||
import jsonschema
|
||||
from django.db import transaction
|
||||
|
||||
from ..core.config import config
|
||||
@ -101,7 +102,7 @@ class StateSerializer(ModelSerializer):
|
||||
"name",
|
||||
"recommendation_label",
|
||||
"css_class",
|
||||
"access_level",
|
||||
"restriction",
|
||||
"allow_support",
|
||||
"allow_create_poll",
|
||||
"allow_submitter_edit",
|
||||
@ -113,6 +114,30 @@ class StateSerializer(ModelSerializer):
|
||||
"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):
|
||||
"""
|
||||
@ -369,7 +394,7 @@ class MotionSerializer(ModelSerializer):
|
||||
polls = MotionPollSerializer(many=True, read_only=True)
|
||||
modified_final_version = 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)
|
||||
title = CharField(max_length=255)
|
||||
amendment_paragraphs = AmendmentParagraphsJSONSerializerField(required=False)
|
||||
@ -404,7 +429,7 @@ class MotionSerializer(ModelSerializer):
|
||||
"supporters",
|
||||
"state",
|
||||
"state_extension",
|
||||
"state_access_level",
|
||||
"state_restriction",
|
||||
"statute_paragraph",
|
||||
"workflow_id",
|
||||
"recommendation",
|
||||
@ -509,9 +534,9 @@ class MotionSerializer(ModelSerializer):
|
||||
|
||||
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.
|
||||
"""
|
||||
return motion.state.access_level
|
||||
return motion.state.restriction
|
||||
|
@ -477,11 +477,11 @@ class RetrieveMotion(TestCase):
|
||||
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
|
||||
guest_client = APIClient()
|
||||
state = self.motion.state
|
||||
state.access_level = State.MANAGERS_ONLY
|
||||
state.restriction = ["managers_only"]
|
||||
state.save()
|
||||
# The cache has to be cleared, see:
|
||||
# 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]))
|
||||
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.access_level = State.MANAGERS_ONLY
|
||||
|
||||
state.restriction = ["managers_only"]
|
||||
state.save()
|
||||
response = self.client.get(reverse("motion-detail", args=[self.motion.pk]))
|
||||
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.access_level = State.EXTENDED_MANAGERS_AND_SUBMITTER
|
||||
state.restriction = ["is_submitter"]
|
||||
state.save()
|
||||
user = get_user_model().objects.create_user(
|
||||
username="username_ohS2opheikaSa5theijo",
|
||||
|
@ -29,7 +29,7 @@ def all_data():
|
||||
"supporters_id": [],
|
||||
"state_id": 1,
|
||||
"state_extension": None,
|
||||
"state_access_level": 0,
|
||||
"state_restriction": [],
|
||||
"statute_paragraph_id": None,
|
||||
"workflow_id": 1,
|
||||
"recommendation_id": None,
|
||||
@ -118,7 +118,7 @@ def all_data():
|
||||
"supporters_id": [],
|
||||
"state_id": 1,
|
||||
"state_extension": None,
|
||||
"state_access_level": 0,
|
||||
"state_restriction": [],
|
||||
"statute_paragraph_id": None,
|
||||
"workflow_id": 1,
|
||||
"recommendation_id": None,
|
||||
@ -151,7 +151,7 @@ def all_data():
|
||||
"supporters_id": [],
|
||||
"state_id": 1,
|
||||
"state_extension": None,
|
||||
"state_access_level": 0,
|
||||
"state_restriction": [],
|
||||
"statute_paragraph_id": 1,
|
||||
"workflow_id": 1,
|
||||
"recommendation_id": None,
|
||||
@ -178,7 +178,7 @@ def all_data():
|
||||
"name": "submitted",
|
||||
"recommendation_label": None,
|
||||
"css_class": "primary",
|
||||
"access_level": 0,
|
||||
"restriction": [],
|
||||
"allow_support": True,
|
||||
"allow_create_poll": True,
|
||||
"allow_submitter_edit": True,
|
||||
@ -194,7 +194,7 @@ def all_data():
|
||||
"name": "accepted",
|
||||
"recommendation_label": "Acceptance",
|
||||
"css_class": "success",
|
||||
"access_level": 0,
|
||||
"restriction": [],
|
||||
"allow_support": False,
|
||||
"allow_create_poll": False,
|
||||
"allow_submitter_edit": False,
|
||||
@ -210,7 +210,7 @@ def all_data():
|
||||
"name": "rejected",
|
||||
"recommendation_label": "Rejection",
|
||||
"css_class": "danger",
|
||||
"access_level": 0,
|
||||
"restriction": [],
|
||||
"allow_support": False,
|
||||
"allow_create_poll": False,
|
||||
"allow_submitter_edit": False,
|
||||
@ -226,7 +226,7 @@ def all_data():
|
||||
"name": "not decided",
|
||||
"recommendation_label": "No decision",
|
||||
"css_class": "default",
|
||||
"access_level": 0,
|
||||
"restriction": [],
|
||||
"allow_support": False,
|
||||
"allow_create_poll": False,
|
||||
"allow_submitter_edit": False,
|
||||
|
Loading…
Reference in New Issue
Block a user