Merge pull request #4037 from normanjaeckel/MultiSubmitters

Added multi select for motion submitters, tags and recommendations.
This commit is contained in:
Norman Jäckel 2018-11-29 20:16:49 +01:00 committed by GitHub
commit 9e007437ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 323 additions and 10 deletions

View File

@ -21,12 +21,16 @@ Core:
- Switch from Yarn back to npm [#3964]. - Switch from Yarn back to npm [#3964].
- Added password reset link (password reset via email) [#3914]. - Added password reset link (password reset via email) [#3914].
Agenda:
- Added viewpoint to assign multiple items to a new parent item [#4037].
Motions: Motions:
- Option to customly sort motions [#3894]. - Option to customly sort motions [#3894].
- Added support for adding a statute [#3894]. - Added support for adding a statute [#3894].
- Added new permission to manage metadata, i. e. set motion state, set and - Added new permission to manage metadata, i. e. set motion state, set and
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 multi select action to manage submitters, tags and recommendations [#4037].
User: User:
- Added new admin group which grants all permissions. Users of existing group - Added new admin group which grants all permissions. Users of existing group

View File

@ -1,3 +1,4 @@
import jsonschema
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import transaction from django.db import transaction
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -27,8 +28,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
""" """
API endpoint for agenda items. API endpoint for agenda items.
There are the following views: metadata, list, retrieve, create, There are some views, see check_view_permissions.
partial_update, update, destroy, manage_speaker, speak and tree.
""" """
access_permissions = ItemAccessPermissions() access_permissions = ItemAccessPermissions()
queryset = Item.objects.all() queryset = Item.objects.all()
@ -43,7 +43,7 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
result = has_perm(self.request.user, 'agenda.can_see') result = has_perm(self.request.user, 'agenda.can_see')
# For manage_speaker and tree requests the rest of the check is # For manage_speaker and tree requests the rest of the check is
# done in the specific method. See below. # done in the specific method. See below.
elif self.action in ('partial_update', 'update', 'sort'): elif self.action in ('partial_update', 'update', 'sort', 'assign'):
result = (has_perm(self.request.user, 'agenda.can_see') and result = (has_perm(self.request.user, 'agenda.can_see') and
has_perm(self.request.user, 'agenda.can_see_internal_items') and has_perm(self.request.user, 'agenda.can_see_internal_items') and
has_perm(self.request.user, 'agenda.can_manage')) has_perm(self.request.user, 'agenda.can_manage'))
@ -339,3 +339,82 @@ class ItemViewSet(ListModelMixin, RetrieveModelMixin, UpdateModelMixin, GenericV
inform_changed_data(items) inform_changed_data(items)
return Response({'detail': _('The agenda has been sorted.')}) return Response({'detail': _('The agenda has been sorted.')})
@list_route(methods=['post'])
@transaction.atomic
def assign(self, request):
"""
Assign multiple agenda items to a new parent item.
Send POST {... see schema ...} to assign the new parent.
This aslo checks the parent field to prevent hierarchical loops.
"""
schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Agenda items assign new parent schema",
"description": "An object containing an array of agenda item ids and the new parent id the items should be assigned to.",
"type": "object",
"propterties": {
"items": {
"description": "An array of agenda item ids where the items should be assigned to the new parent id.",
"type": "array",
"items": {
"type": "integer",
},
"minItems": 1,
"uniqueItems": True,
},
"parent_id": {
"description": "The agenda item id of the new parent item.",
"type": "integer",
},
},
"required": ["items", "parent_id"],
}
# Validate request data.
try:
jsonschema.validate(request.data, schema)
except jsonschema.ValidationError as err:
raise ValidationError({'detail': str(err)})
# Check parent item
try:
parent = Item.objects.get(pk=request.data['parent_id'])
except Item.DoesNotExist:
raise ValidationError({'detail': 'Parent item {} does not exist'.format(request.data['parent_id'])})
# Collect ancestors
ancestors = [parent.pk]
grandparent = parent.parent
while grandparent is not None:
ancestors.append(grandparent.pk)
grandparent = grandparent.parent
# First validate all items before changeing them.
items = []
for item_id in request.data['items']:
# Prevent hierarchical loops.
if item_id in ancestors:
raise ValidationError({'detail': 'Assigning item {} to one of its children is not possible.'.format(item_id)})
# Check every item
try:
items.append(Item.objects.get(pk=item_id))
except Item.DoesNotExist:
raise ValidationError({'detail': 'Item {} does not exist'.format(item_id)})
# OK, assign new parents.
for item in items:
# Assign new parent.
item.parent = parent
item.save(skip_autoupdate=True)
# Now inform all clients.
inform_changed_data(items)
# Send response.
return Response({
'detail': _('{number} items successfully assigned.').format(number=len(items)),
})

View File

@ -1,6 +1,7 @@
import re import re
from typing import List from typing import List
import jsonschema
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
@ -11,6 +12,7 @@ from django.utils.translation import ugettext as _, ugettext_noop
from rest_framework import status from rest_framework import status
from ..core.config import config from ..core.config import config
from ..core.models import Tag
from ..utils.auth import has_perm, in_some_groups from ..utils.auth import has_perm, in_some_groups
from ..utils.autoupdate import inform_changed_data from ..utils.autoupdate import inform_changed_data
from ..utils.exceptions import OpenSlidesError from ..utils.exceptions import OpenSlidesError
@ -58,9 +60,7 @@ class MotionViewSet(ModelViewSet):
""" """
API endpoint for motions. API endpoint for motions.
There are the following views: metadata, list, retrieve, create, There are a lot of views. See check_view_permissions().
partial_update, update, destroy, support, set_state and
create_poll.
""" """
access_permissions = MotionAccessPermissions() access_permissions = MotionAccessPermissions()
queryset = Motion.objects.all() queryset = Motion.objects.all()
@ -80,9 +80,10 @@ class MotionViewSet(ModelViewSet):
has_perm(self.request.user, 'motions.can_create') and has_perm(self.request.user, 'motions.can_create') and
(not config['motions_stop_submitting'] or (not config['motions_stop_submitting'] or
has_perm(self.request.user, 'motions.can_manage'))) has_perm(self.request.user, 'motions.can_manage')))
elif self.action in ('set_state', 'set_recommendation', elif self.action in ('set_state', 'set_recommendation', 'manage_multiple_recommendation',
'follow_recommendation', 'manage_submitters', 'follow_recommendation', 'manage_submitters',
'sort_submitters', 'create_poll'): 'sort_submitters', 'manage_multiple_submitters',
'manage_multiple_tags', 'create_poll'):
result = (has_perm(self.request.user, 'motions.can_see') and result = (has_perm(self.request.user, 'motions.can_see') and
has_perm(self.request.user, 'motions.can_manage_metadata')) has_perm(self.request.user, 'motions.can_manage_metadata'))
elif self.action in ('sort', 'manage_comments'): elif self.action in ('sort', 'manage_comments'):
@ -477,6 +478,85 @@ class MotionViewSet(ModelViewSet):
# Initiate response. # Initiate response.
return Response({'detail': _('Submitters successfully sorted.')}) return Response({'detail': _('Submitters successfully sorted.')})
@list_route(methods=['post'])
@transaction.atomic
def manage_multiple_submitters(self, request):
"""
Set or reset submitters of multiple motions.
Send POST {"motions": [... see schema ...]} to changed the submitters.
"""
motions = request.data.get('motions')
schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Motion manage multiple submitters schema",
"description": "An array of motion ids with the respective user ids that should be set as submitter.",
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"description": "The id of the motion.",
"type": "integer",
},
"submitters": {
"description": "An array of user ids the should become submitters. Use an empty array to clear submitter field.",
"type": "array",
"items": {
"type": "integer",
},
"uniqueItems": True,
},
},
"required": ["id", "submitters"],
},
"uniqueItems": True,
}
# Validate request data.
try:
jsonschema.validate(motions, schema)
except jsonschema.ValidationError as err:
raise ValidationError({'detail': str(err)})
motion_result = []
new_submitters = []
for item in motions:
# Get motion.
try:
motion = Motion.objects.get(pk=item['id'])
except Motion.DoesNotExist:
raise ValidationError({'detail': 'Motion {} does not exist'.format(item['id'])})
# Remove all submitters.
Submitter.objects.filter(motion=motion).delete()
# Set new submitters.
for submitter_id in item['submitters']:
try:
submitter = get_user_model().objects.get(pk=submitter_id)
except get_user_model().DoesNotExist:
raise ValidationError({'detail': 'Submitter {} does not exist'.format(submitter_id)})
Submitter.objects.add(submitter, motion)
new_submitters.append(submitter)
# Finish motion.
motion_result.append(motion)
# Now inform all clients.
inform_changed_data(motion_result)
# Also send all new submitters via autoupdate because users without
# permission to see users may not have them but can get it now.
# TODO: Skip history.
inform_changed_data(new_submitters)
# Send response.
return Response({
'detail': _('{number} motions successfully updated.').format(number=len(motion_result)),
})
@detail_route(methods=['post', 'delete']) @detail_route(methods=['post', 'delete'])
def support(self, request, pk=None): def support(self, request, pk=None):
""" """
@ -583,16 +663,99 @@ class MotionViewSet(ModelViewSet):
motion.recommendation = None motion.recommendation = None
# Save motion. # Save motion.
motion.save(update_fields=['recommendation']) motion.save(update_fields=['recommendation'], skip_autoupdate=True)
label = motion.recommendation.recommendation_label if motion.recommendation else 'None' label = motion.recommendation.recommendation_label if motion.recommendation else 'None'
message = _('The recommendation of the motion was set to %s.') % label message = _('The recommendation of the motion was set to %s.') % label
# Write the log message and initiate response. # Write the log message and initiate response.
motion.write_log( motion.write_log(
message_list=[ugettext_noop('Recommendation set to'), ' ', label], message_list=[ugettext_noop('Recommendation set to'), ' ', label],
person=request.user) person=request.user,
skip_autoupdate=True)
inform_changed_data(motion)
return Response({'detail': message}) return Response({'detail': message})
@list_route(methods=['post'])
@transaction.atomic
def manage_multiple_recommendation(self, request):
"""
Set or reset recommendations of multiple motions.
Send POST {"motions": [... see schema ...]} to changed the recommendations.
"""
motions = request.data.get('motions')
schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Motion manage multiple recommendations schema",
"description": "An array of motion ids with the respective state ids that should be set as recommendation.",
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"description": "The id of the motion.",
"type": "integer",
},
"recommendation": {
"description": "The state id the should become recommendation. Use 0 to clear recommendation field.",
"type": "integer",
},
},
"required": ["id", "recommendation"],
},
"uniqueItems": True,
}
# Validate request data.
try:
jsonschema.validate(motions, schema)
except jsonschema.ValidationError as err:
raise ValidationError({'detail': str(err)})
motion_result = []
for item in motions:
# Get motion.
try:
motion = Motion.objects.get(pk=item['id'])
except Motion.DoesNotExist:
raise ValidationError({'detail': 'Motion {} does not exist'.format(item['id'])})
# Set or reset recommendation.
recommendation_state_id = item['recommendation']
if recommendation_state_id == 0:
# Reset recommendation.
motion.recommendation = None
else:
# Check data and set recommendation.
recommendable_states = State.objects.filter(workflow=motion.workflow_id, recommendation_label__isnull=False)
if recommendation_state_id not in [item.id for item in recommendable_states]:
raise ValidationError(
{'detail': _('You can not set the recommendation to {recommendation_state_id}.').format(
recommendation_state_id=recommendation_state_id)})
motion.set_recommendation(recommendation_state_id)
# Save motion.
motion.save(update_fields=['recommendation'], skip_autoupdate=True)
label = motion.recommendation.recommendation_label if motion.recommendation else 'None'
# Write the log message.
motion.write_log(
message_list=[ugettext_noop('Recommendation set to'), ' ', label],
person=request.user,
skip_autoupdate=True)
# Finish motion.
motion_result.append(motion)
# Now inform all clients.
inform_changed_data(motion_result)
# Send response.
return Response({
'detail': _('{number} motions successfully updated.').format(number=len(motion_result)),
})
@detail_route(methods=['post']) @detail_route(methods=['post'])
def follow_recommendation(self, request, pk=None): def follow_recommendation(self, request, pk=None):
motion = self.get_object() motion = self.get_object()
@ -640,6 +803,73 @@ class MotionViewSet(ModelViewSet):
'detail': _('Vote created successfully.'), 'detail': _('Vote created successfully.'),
'createdPollId': poll.pk}) 'createdPollId': poll.pk})
@list_route(methods=['post'])
@transaction.atomic
def manage_multiple_tags(self, request):
"""
Set or reset tags of multiple motions.
Send POST {"motions": [... see schema ...]} to changed the tags.
"""
motions = request.data.get('motions')
schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Motion manage multiple tags schema",
"description": "An array of motion ids with the respective tags ids that should be set as tag.",
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"description": "The id of the motion.",
"type": "integer",
},
"tags": {
"description": "An array of tag ids the should become tags. Use an empty array to clear tag field.",
"type": "array",
"items": {
"type": "integer",
},
"uniqueItems": True,
},
},
"required": ["id", "tags"],
},
"uniqueItems": True,
}
# Validate request data.
try:
jsonschema.validate(motions, schema)
except jsonschema.ValidationError as err:
raise ValidationError({'detail': str(err)})
motion_result = []
for item in motions:
# Get motion.
try:
motion = Motion.objects.get(pk=item['id'])
except Motion.DoesNotExist:
raise ValidationError({'detail': 'Motion {} does not exist'.format(item['id'])})
# Set new tags
for tag_id in item['tags']:
if not Tag.objects.filter(pk=tag_id).exists():
raise ValidationError({'detail': 'Tag {} does not exist'.format(tag_id)})
motion.tags.set(item['tags'])
# Finish motion.
motion_result.append(motion)
# Now inform all clients.
inform_changed_data(motion_result)
# Send response.
return Response({
'detail': _('{number} motions successfully updated.').format(number=len(motion_result)),
})
class MotionPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet): class MotionPollViewSet(UpdateModelMixin, DestroyModelMixin, GenericViewSet):
""" """