OpenSlides/openslides/motions/serializers.py

513 lines
16 KiB
Python
Raw Normal View History

from typing import Dict, Optional
2017-08-24 12:26:55 +02:00
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,
DecimalField,
DictField,
Field,
IntegerField,
ModelSerializer,
PrimaryKeyRelatedField,
2015-10-21 21:13:45 +02:00
SerializerMethodField,
ValidationError,
)
from ..utils.validate import validate_html
from .models import (
Category,
Motion,
MotionBlock,
2016-09-10 18:49:38 +02:00
MotionChangeRecommendation,
MotionLog,
MotionPoll,
MotionVersion,
State,
2018-06-12 14:17:02 +02:00
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)
class Meta:
model = Workflow
fields = ('id', 'name', 'states', 'first_state',)
read_only_fields = ('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
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.
"""
2015-10-21 21:13:45 +02:00
yes = SerializerMethodField()
no = SerializerMethodField()
abstain = SerializerMethodField()
votes = DictField(
child=DecimalField(max_digits=15, decimal_places=6, min_value=-2, allow_null=True),
write_only=True)
has_votes = SerializerMethodField()
class Meta:
model = MotionPoll
fields = (
'id',
2015-10-13 21:23:21 +02:00
'motion',
2015-10-21 21:13:45 +02:00
'yes',
'no',
'abstain',
'votesvalid',
'votesinvalid',
'votescast',
'votes',
'has_votes')
validators = (default_votes_validator,)
2015-10-21 21:13:45 +02:00
def __init__(self, *args, **kwargs):
# The following dictionary is just a cache for several votes.
2018-08-22 22:00:08 +02:00
self._votes_dicts: Dict[int, Dict[int, int]] = {}
return super().__init__(*args, **kwargs)
2015-10-21 21:13:45 +02:00
def get_yes(self, obj):
try:
result: Optional[str] = str(self.get_votes_dict(obj)['Yes'])
except KeyError:
result = None
return result
2015-10-21 21:13:45 +02:00
def get_no(self, obj):
try:
result: Optional[str] = str(self.get_votes_dict(obj)['No'])
except KeyError:
result = None
return result
2015-10-21 21:13:45 +02:00
def get_abstain(self, obj):
try:
result: Optional[str] = str(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',)
2016-09-10 18:49:38 +02:00
class MotionChangeRecommendationSerializer(ModelSerializer):
"""
Serializer for motion.models.MotionChangeRecommendation objects.
"""
class Meta:
model = MotionChangeRecommendation
fields = (
'id',
'motion_version',
'rejected',
'type',
2017-11-17 12:10:46 +01:00
'other_description',
2016-09-10 18:49:38 +02:00
'line_from',
'line_to',
'text',
'creation_time',)
2018-03-07 16:36:30 +01:00
def is_title_cr(self, data):
return int(data['line_from']) == 0 and int(data['line_to']) == 0
2017-01-20 11:34:05 +01:00
def validate(self, data):
2018-03-07 16:36:30 +01:00
# 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'])
2017-01-20 11:34:05 +01:00
return data
2016-09-10 18:49:38 +02:00
2018-06-12 14:17:02 +02:00
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)
2018-06-12 14:17:02 +02:00
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',
2016-07-13 14:45:40 +02:00
'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.
2017-01-20 11:34:05 +01:00
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)
2017-01-20 11:34:05 +01:00
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.')
})
2017-01-20 11:34:05 +01:00
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')
2016-07-13 14:45:40 +02:00
motion.origin = validated_data.get('origin', '')
motion.comments = validated_data.get('comments')
2016-08-19 21:03:14 +02:00
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