Merge pull request #4037 from normanjaeckel/MultiSubmitters
Added multi select for motion submitters, tags and recommendations.
This commit is contained in:
commit
9e007437ec
@ -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
|
||||||
|
@ -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)),
|
||||||
|
})
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user