OpenSlides/openslides/motions/serializers.py
FinnStutzenstein 1a17862d6b New item type internal.
The old hidden type was used as internal, so everything is changed to
not be shown if the item is internal. hidden is "new", and actually
behaves as hidden now.
2018-08-16 15:28:30 +02:00

524 lines
17 KiB
Python

from typing import Dict # noqa
from django.db import transaction
from django.utils.translation import ugettext as _
from ..poll.serializers import default_votes_validator
from ..utils.rest_api import (
CharField,
DictField,
Field,
IntegerField,
ModelSerializer,
PrimaryKeyRelatedField,
SerializerMethodField,
ValidationError,
)
from ..utils.validate import validate_html
from .models import (
Category,
Motion,
MotionBlock,
MotionChangeRecommendation,
MotionLog,
MotionPoll,
MotionVersion,
State,
Submitter,
Workflow,
)
def validate_workflow_field(value):
"""
Validator to ensure that the workflow with the given id exists.
"""
if not Workflow.objects.filter(pk=value).exists():
raise ValidationError({'detail': _('Workflow %(pk)d does not exist.') % {'pk': value}})
class CategorySerializer(ModelSerializer):
"""
Serializer for motion.models.Category objects.
"""
class Meta:
model = Category
fields = ('id', 'name', 'prefix',)
class MotionBlockSerializer(ModelSerializer):
"""
Serializer for motion.models.Category objects.
"""
agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=3)
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
class Meta:
model = MotionBlock
fields = ('id', 'title', 'agenda_item_id', 'agenda_type', 'agenda_parent_id',)
def create(self, validated_data):
"""
Customized create method. Set information about related agenda item
into agenda_item_update_information container.
"""
agenda_type = validated_data.pop('agenda_type', None)
agenda_parent_id = validated_data.pop('agenda_parent_id', None)
motion_block = MotionBlock(**validated_data)
motion_block.agenda_item_update_information['type'] = agenda_type
motion_block.agenda_item_update_information['parent_id'] = agenda_parent_id
motion_block.save()
return motion_block
class StateSerializer(ModelSerializer):
"""
Serializer for motion.models.State objects.
"""
class Meta:
model = State
fields = (
'id',
'name',
'action_word',
'recommendation_label',
'css_class',
'required_permission_to_see',
'allow_support',
'allow_create_poll',
'allow_submitter_edit',
'versioning',
'leave_old_version_active',
'dont_set_identifier',
'show_state_extension_field',
'show_recommendation_extension_field',
'next_states',
'workflow')
class WorkflowSerializer(ModelSerializer):
"""
Serializer for motion.models.Workflow objects.
"""
states = StateSerializer(many=True, read_only=True)
# The first_state is checked in the update() method
first_state = PrimaryKeyRelatedField(queryset=State.objects.all(), required=False)
class Meta:
model = Workflow
fields = ('id', 'name', 'states', 'first_state',)
@transaction.atomic
def create(self, validated_data):
"""
Customized create method. Creating a new workflow does always create a
new state which is used as first state.
"""
workflow = super().create(validated_data)
first_state = State.objects.create(
name='new',
action_word='new',
workflow=workflow,
allow_create_poll=True,
allow_support=True,
allow_submitter_edit=True
)
workflow.first_state = first_state
workflow.save()
return workflow
@transaction.atomic
def update(self, workflow, validated_data):
"""
Check, if the first state is in the right workflow.
"""
first_state = validated_data.get('first_state')
if first_state is not None:
if workflow.pk != first_state.workflow.pk:
raise ValidationError({'detail': 'You cannot select a state which is not in the workflow as the first state.'})
return super().update(workflow, validated_data)
class MotionCommentsJSONSerializerField(Field):
"""
Serializer for motions's comments JSONField.
"""
def to_representation(self, obj):
"""
Returns the value of the field.
"""
return obj
def to_internal_value(self, data):
"""
Checks that data is a list of strings.
"""
if type(data) is not dict:
raise ValidationError({'detail': 'Data must be a dict.'})
for id, comment in data.items():
try:
id = int(id)
except ValueError:
raise ValidationError({'detail': 'Id must be an int.'})
if type(comment) is not str:
raise ValidationError({'detail': 'Comment must be a string.'})
return data
class AmendmentParagraphsJSONSerializerField(Field):
"""
Serializer for motions's amendment_paragraphs JSONField.
"""
def to_representation(self, obj):
"""
Returns the value of the field.
"""
return obj
def to_internal_value(self, data):
"""
Checks that data is a list of strings.
"""
if type(data) is not list:
raise ValidationError({'detail': 'Data must be a list.'})
for paragraph in data:
if type(paragraph) is not str and paragraph is not None:
raise ValidationError({'detail': 'Paragraph must be either a string or null/None.'})
return data
class MotionLogSerializer(ModelSerializer):
"""
Serializer for motion.models.MotionLog objects.
"""
message = SerializerMethodField()
class Meta:
model = MotionLog
fields = ('message_list', 'person', 'time', 'message',)
def get_message(self, obj):
"""
Concats the message parts to one string. Useful for smart template code.
"""
return str(obj)
class MotionPollSerializer(ModelSerializer):
"""
Serializer for motion.models.MotionPoll objects.
"""
yes = SerializerMethodField()
no = SerializerMethodField()
abstain = SerializerMethodField()
votes = DictField(
child=IntegerField(min_value=-2, allow_null=True),
write_only=True)
has_votes = SerializerMethodField()
class Meta:
model = MotionPoll
fields = (
'id',
'motion',
'yes',
'no',
'abstain',
'votesvalid',
'votesinvalid',
'votescast',
'votes',
'has_votes')
validators = (default_votes_validator,)
def __init__(self, *args, **kwargs):
# The following dictionary is just a cache for several votes.
self._votes_dicts = {} # type: Dict[int, Dict[int, int]]
return super().__init__(*args, **kwargs)
def get_yes(self, obj):
try:
result = self.get_votes_dict(obj)['Yes']
except KeyError:
result = None
return result
def get_no(self, obj):
try:
result = self.get_votes_dict(obj)['No']
except KeyError:
result = None
return result
def get_abstain(self, obj):
try:
result = self.get_votes_dict(obj)['Abstain']
except KeyError:
result = None
return result
def get_votes_dict(self, obj):
try:
votes_dict = self._votes_dicts[obj.pk]
except KeyError:
votes_dict = self._votes_dicts[obj.pk] = {}
for vote in obj.get_votes():
votes_dict[vote.value] = vote.weight
return votes_dict
def get_has_votes(self, obj):
"""
Returns True if this poll has some votes.
"""
return obj.has_votes()
@transaction.atomic
def update(self, instance, validated_data):
"""
Customized update method for polls. To update votes use the write
only field 'votes'.
Example data:
"votes": {"Yes": 10, "No": 4, "Abstain": -2}
"""
# Update votes.
votes = validated_data.get('votes')
if votes:
if len(votes) != len(instance.get_vote_values()):
raise ValidationError({
'detail': _('You have to submit data for %d vote values.') % len(instance.get_vote_values())})
for vote_value, vote_weight in votes.items():
if vote_value not in instance.get_vote_values():
raise ValidationError({
'detail': _('Vote value %s is invalid.') % vote_value})
instance.set_vote_objects_with_values(instance.get_options().get(), votes, skip_autoupdate=True)
# Update remaining writeable fields.
instance.votesvalid = validated_data.get('votesvalid', instance.votesvalid)
instance.votesinvalid = validated_data.get('votesinvalid', instance.votesinvalid)
instance.votescast = validated_data.get('votescast', instance.votescast)
instance.save()
return instance
class MotionVersionSerializer(ModelSerializer):
amendment_paragraphs = AmendmentParagraphsJSONSerializerField(required=False)
"""
Serializer for motion.models.MotionVersion objects.
"""
class Meta:
model = MotionVersion
fields = (
'id',
'version_number',
'creation_time',
'title',
'text',
'amendment_paragraphs',
'modified_final_version',
'reason',)
class MotionChangeRecommendationSerializer(ModelSerializer):
"""
Serializer for motion.models.MotionChangeRecommendation objects.
"""
class Meta:
model = MotionChangeRecommendation
fields = (
'id',
'motion_version',
'rejected',
'type',
'other_description',
'line_from',
'line_to',
'text',
'creation_time',)
def is_title_cr(self, data):
return int(data['line_from']) == 0 and int(data['line_to']) == 0
def validate(self, data):
# Change recommendations for titles are stored as plain-text, thus they don't need to be html-escaped
if 'text' in data and not self.is_title_cr(data):
data['text'] = validate_html(data['text'])
return data
class SubmitterSerializer(ModelSerializer):
"""
Serializer for motion.models.Submitter objects.
"""
class Meta:
model = Submitter
fields = (
'id',
'user',
'motion',
'weight',
)
class MotionSerializer(ModelSerializer):
"""
Serializer for motion.models.Motion objects.
"""
active_version = PrimaryKeyRelatedField(read_only=True)
comments = MotionCommentsJSONSerializerField(required=False)
log_messages = MotionLogSerializer(many=True, read_only=True)
polls = MotionPollSerializer(many=True, read_only=True)
modified_final_version = CharField(allow_blank=True, required=False, write_only=True)
reason = CharField(allow_blank=True, required=False, write_only=True)
state_required_permission_to_see = SerializerMethodField()
text = CharField(write_only=True, allow_blank=True)
title = CharField(max_length=255, write_only=True)
amendment_paragraphs = AmendmentParagraphsJSONSerializerField(required=False, write_only=True)
versions = MotionVersionSerializer(many=True, read_only=True)
workflow_id = IntegerField(
min_value=1,
required=False,
validators=[validate_workflow_field],
write_only=True)
agenda_type = IntegerField(write_only=True, required=False, min_value=1, max_value=3)
agenda_parent_id = IntegerField(write_only=True, required=False, min_value=1)
submitters = SubmitterSerializer(many=True, read_only=True)
class Meta:
model = Motion
fields = (
'id',
'identifier',
'title',
'text',
'amendment_paragraphs',
'modified_final_version',
'reason',
'versions',
'active_version',
'parent',
'category',
'motion_block',
'origin',
'submitters',
'supporters',
'comments',
'state',
'state_required_permission_to_see',
'workflow_id',
'recommendation',
'tags',
'attachments',
'polls',
'agenda_item_id',
'agenda_type',
'agenda_parent_id',
'log_messages',)
read_only_fields = ('state', 'recommendation',) # Some other fields are also read_only. See definitions above.
def validate(self, data):
if 'text'in data:
data['text'] = validate_html(data['text'])
if 'modified_final_version' in data:
data['modified_final_version'] = validate_html(data['modified_final_version'])
if 'reason' in data:
data['reason'] = validate_html(data['reason'])
validated_comments = dict()
for id, comment in data.get('comments', {}).items():
validated_comments[id] = validate_html(comment)
data['comments'] = validated_comments
if 'amendment_paragraphs' in data:
data['amendment_paragraphs'] = list(map(lambda entry: validate_html(entry) if type(entry) is str else None,
data['amendment_paragraphs']))
data['text'] = ''
else:
if 'text' in data and len(data['text']) == 0:
raise ValidationError({
'detail': _('This field may not be blank.')
})
return data
@transaction.atomic
def create(self, validated_data):
"""
Customized method to create a new motion from some data.
Set also information about related agenda item into
agenda_item_update_information container.
"""
motion = Motion()
motion.title = validated_data['title']
motion.text = validated_data['text']
motion.amendment_paragraphs = validated_data.get('amendment_paragraphs')
motion.modified_final_version = validated_data.get('modified_final_version', '')
motion.reason = validated_data.get('reason', '')
motion.identifier = validated_data.get('identifier')
motion.category = validated_data.get('category')
motion.motion_block = validated_data.get('motion_block')
motion.origin = validated_data.get('origin', '')
motion.comments = validated_data.get('comments')
motion.parent = validated_data.get('parent')
motion.reset_state(validated_data.get('workflow_id'))
motion.agenda_item_update_information['type'] = validated_data.get('agenda_type')
motion.agenda_item_update_information['parent_id'] = validated_data.get('agenda_parent_id')
motion.save()
motion.supporters.add(*validated_data.get('supporters', []))
motion.attachments.add(*validated_data.get('attachments', []))
motion.tags.add(*validated_data.get('tags', []))
return motion
@transaction.atomic
def update(self, motion, validated_data):
"""
Customized method to update a motion.
"""
# Identifier, category, motion_block, origin and comments.
for key in ('identifier', 'category', 'motion_block', 'origin', 'comments'):
if key in validated_data.keys():
setattr(motion, key, validated_data[key])
# Workflow.
workflow_id = validated_data.get('workflow_id')
if workflow_id is not None and workflow_id != motion.workflow:
motion.reset_state(workflow_id)
# Decide if a new version is saved to the database.
if (motion.state.versioning and
not validated_data.get('disable_versioning', False)): # TODO
version = motion.get_new_version()
else:
version = motion.get_last_version()
# Title, text, reason, ...
for key in ('title', 'text', 'amendment_paragraphs', 'modified_final_version', 'reason'):
if key in validated_data.keys():
setattr(version, key, validated_data[key])
motion.save(use_version=version)
# Submitters, supporters, attachments and tags
for key in ('submitters', 'supporters', 'attachments', 'tags'):
if key in validated_data.keys():
attr = getattr(motion, key)
attr.clear()
attr.add(*validated_data[key])
return motion
def get_state_required_permission_to_see(self, motion):
"""
Returns the permission (as string) that is required for non
managers that are not submitters to see this motion in this state.
Hint: Most states have and empty string here so this restriction is
disabled.
"""
return motion.state.required_permission_to_see