tabbycat/api/serializers.py
from collections import OrderedDict
from collections.abc import Mapping
from functools import partialmethod
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import IntegrityError
from django.db.models import QuerySet
from django.utils import timezone
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from rest_framework.fields import get_error_detail, SkipField
from rest_framework.settings import api_settings
from adjallocation.models import DebateAdjudicator, PreformedPanel
from adjfeedback.models import AdjudicatorBaseScoreHistory, AdjudicatorFeedback, AdjudicatorFeedbackQuestion
from breakqual.models import BreakCategory, BreakingTeam
from draw.models import Debate, DebateTeam
from motions.models import DebateTeamMotionPreference, Motion, RoundMotion
from participants.emoji import pick_unused_emoji
from participants.models import Adjudicator, Institution, Region, Speaker, SpeakerCategory, Team
from participants.utils import populate_code_names
from privateurls.utils import populate_url_keys
from results.mixins import TabroomSubmissionFieldsMixin
from results.models import BallotSubmission, SpeakerScore, TeamScore
from results.result import DebateResult, ResultError
from standings.speakers import SpeakerStandingsGenerator
from standings.teams import TeamStandingsGenerator
from tournaments.models import Round, Tournament
from venues.models import Venue, VenueCategory, VenueConstraint
from . import fields
from .utils import is_staff
def _validate_field(self, field, value):
if value is None:
return None
qs = self.Meta.model.objects.filter(
tournament=self.context['tournament'], **{field: value}).exclude(id=getattr(self.instance, 'id', None))
if qs.exists():
raise serializers.ValidationError("Object with same value exists in the tournament")
return value
class RootSerializer(serializers.Serializer):
class RootLinksSerializer(serializers.Serializer):
v1 = serializers.HyperlinkedIdentityField(view_name='api-v1-root')
_links = RootLinksSerializer(source='*', read_only=True)
timezone = serializers.CharField(allow_blank=False, read_only=True)
version = serializers.CharField()
class V1RootSerializer(serializers.Serializer):
class V1LinksSerializer(serializers.Serializer):
tournaments = serializers.HyperlinkedIdentityField(view_name='api-tournament-list')
institutions = serializers.HyperlinkedIdentityField(view_name='api-global-institution-list')
users = serializers.HyperlinkedIdentityField(view_name='api-users-list')
_links = V1LinksSerializer(source='*', read_only=True)
class CheckinSerializer(serializers.Serializer):
object = serializers.HyperlinkedIdentityField(view_name='api-root')
barcode = serializers.CharField()
checked = serializers.BooleanField()
timestamp = serializers.DateTimeField()
class AvailabilitiesSerializer(serializers.ListSerializer):
child = fields.ParticipantAvailabilityForeignKeyField(view_name='api-availability-list')
class VenueConstraintSerializer(serializers.ModelSerializer):
category = fields.TournamentHyperlinkedRelatedField(view_name='api-venuecategory-detail', queryset=VenueCategory.objects.all())
class Meta:
model = VenueConstraint
fields = ('category', 'priority')
class TournamentSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='api-tournament-detail',
lookup_field='slug', lookup_url_kwarg='tournament_slug')
current_rounds = fields.TournamentHyperlinkedRelatedField(
view_name='api-round-detail', read_only=True, many=True,
lookup_field='seq', lookup_url_kwarg='round_seq',
)
class TournamentLinksSerializer(serializers.Serializer):
rounds = serializers.HyperlinkedIdentityField(
view_name='api-round-list',
lookup_field='slug', lookup_url_kwarg='tournament_slug')
break_categories = serializers.HyperlinkedIdentityField(
view_name='api-breakcategory-list',
lookup_field='slug', lookup_url_kwarg='tournament_slug')
speaker_categories = serializers.HyperlinkedIdentityField(
view_name='api-speakercategory-list',
lookup_field='slug', lookup_url_kwarg='tournament_slug')
institutions = serializers.HyperlinkedIdentityField(
view_name='api-institution-list',
lookup_field='slug', lookup_url_kwarg='tournament_slug')
teams = serializers.HyperlinkedIdentityField(
view_name='api-team-list',
lookup_field='slug', lookup_url_kwarg='tournament_slug')
adjudicators = serializers.HyperlinkedIdentityField(
view_name='api-adjudicator-list',
lookup_field='slug', lookup_url_kwarg='tournament_slug')
speakers = serializers.HyperlinkedIdentityField(
view_name='api-speaker-list',
lookup_field='slug', lookup_url_kwarg='tournament_slug')
venues = serializers.HyperlinkedIdentityField(
view_name='api-venue-list',
lookup_field='slug', lookup_url_kwarg='tournament_slug')
venue_categories = serializers.HyperlinkedIdentityField(
view_name='api-venuecategory-list',
lookup_field='slug', lookup_url_kwarg='tournament_slug')
motions = serializers.HyperlinkedIdentityField(
view_name='api-motion-list',
lookup_field='slug', lookup_url_kwarg='tournament_slug')
feedback = serializers.HyperlinkedIdentityField(
view_name='api-feedback-list',
lookup_field='slug', lookup_url_kwarg='tournament_slug')
feedback_questions = serializers.HyperlinkedIdentityField(
view_name='api-feedbackquestion-list',
lookup_field='slug', lookup_url_kwarg='tournament_slug')
preferences = serializers.HyperlinkedIdentityField(
view_name='tournamentpreferencemodel-list',
lookup_field='slug', lookup_url_kwarg='tournament_slug')
_links = TournamentLinksSerializer(source='*', read_only=True)
class Meta:
model = Tournament
fields = '__all__'
class RoundSerializer(serializers.ModelSerializer):
class RoundMotionSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(source='motion.pk', read_only=True)
url = fields.TournamentHyperlinkedRelatedField(required=False,
view_name='api-motion-detail', queryset=Motion.objects.all(), source='motion')
text = serializers.CharField(source='motion.text', max_length=500, required=False)
reference = serializers.CharField(source='motion.reference', max_length=100, required=False)
info_slide = serializers.CharField(source='motion.info_slide', required=False)
info_slide_plain = serializers.CharField(source='motion.info_slide_plain', read_only=True)
seq = serializers.IntegerField(read_only=True)
class Meta:
model = RoundMotion
exclude = ('round', 'motion')
def create(self, validated_data):
motion_data = validated_data.pop('motion')
if isinstance(motion_data, Motion): # If passed in a URL - Becomes an object
validated_data['motion'] = motion_data
else:
validated_data['motion'] = Motion(text=motion_data['text'], reference=motion_data['reference'], info_slide=motion_data.get('info_slide', ''), tournament=self.context['tournament'])
validated_data['motion'].save()
return super().create(validated_data)
class RoundLinksSerializer(serializers.Serializer):
pairing = fields.TournamentHyperlinkedIdentityField(
view_name='api-pairing-list',
lookup_field='seq', lookup_url_kwarg='round_seq')
url = fields.TournamentHyperlinkedIdentityField(
view_name='api-round-detail',
lookup_field='seq', lookup_url_kwarg='round_seq')
break_category = fields.TournamentHyperlinkedRelatedField(
view_name='api-breakcategory-detail',
queryset=BreakCategory.objects.all(),
allow_null=True, required=False)
motions = RoundMotionSerializer(many=True, source='roundmotion_set', required=False)
_links = RoundLinksSerializer(source='*', read_only=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not is_staff(kwargs.get('context')):
self.fields.pop('feedback_weight')
# Can't show in a ListSerializer
if isinstance(self.instance, QuerySet) or not self.instance.motions_released:
self.fields.pop('motions')
class Meta:
model = Round
exclude = ('tournament',)
validate_seq = partialmethod(_validate_field, 'seq')
def validate(self, data):
bc = data.get('break_category', getattr(self.instance, 'break_category', None))
stage = data.get('stage', getattr(self.instance, 'stage', Round.Stage.PRELIMINARY))
if (bc is None) == (stage == Round.Stage.ELIMINATION):
# break category is None _XNOR_ stage is elimination
raise serializers.ValidationError("Rounds are elimination iff they have a break category.")
return super().validate(data)
def create(self, validated_data):
motions_data = validated_data.pop('roundmotion_set', [])
round = super().create(validated_data)
if len(motions_data) > 0:
for i, motion in enumerate(motions_data, start=1):
motion['seq'] = i
motions = self.RoundMotionSerializer(many=True, context=self.context)
motions._validated_data = motions_data # Data was already validated
motions.save(round=round)
return round
def update(self, instance, validated_data):
motions_data = validated_data.pop('roundmotion_set', [])
for i, roundmotion in enumerate(motions_data, start=1):
roundmotion['seq'] = i
motion = roundmotion['motion'].get('pk')
if motion is None:
motion = Motion(
text=roundmotion['motion']['text'],
reference=roundmotion['motion']['reference'],
info_slide=roundmotion['motion'].get('info_slide', ''),
tournament=instance.tournament,
)
else:
motion.text = roundmotion['motion']['text']
motion.reference = roundmotion['motion']['reference']
motion.info_slide = roundmotion['motion'].get('info_slide', '')
motion.save()
RoundMotion.objects.update_or_create(round=instance, motion=motion, defaults={'seq': roundmotion['seq']})
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance
"""Remove once DRF supports the serializer's structure"""
def set_value(self, dictionary, keys, value):
"""
Similar to Python's built in `dictionary[key] = value`,
but takes a list of nested keys instead of a single key.
set_value({'a': 1}, [], {'b': 2}) -> {'a': 1, 'b': 2}
set_value({'a': 1}, ['x'], 2) -> {'a': 1, 'x': 2}
set_value({'a': 1}, ['x', 'y'], 2) -> {'a': 1, 'x': {'y': 2}}
"""
if not keys:
dictionary.update(value)
return
for key in keys[:-1]:
if key not in dictionary:
dictionary[key] = {}
elif type(dictionary[key]) is not dict:
dictionary[key] = {'': dictionary[key]}
dictionary = dictionary[key]
if keys[-1] in dictionary and type(dictionary[keys[-1]]) is dict:
dictionary[keys[-1]][''] = value
else:
dictionary[keys[-1]] = value
def to_internal_value(self, data):
"""
Dict of native values <- Dict of primitive datatypes.
Copied from DRF while waiting for #8001/#7671 as the format is nested
differently from the stock "set_value"
"""
if not isinstance(data, Mapping):
message = self.error_messages['invalid'].format(datatype=type(data).__name__)
raise serializers.ValidationError({
api_settings.NON_FIELD_ERRORS_KEY: [message],
}, code='invalid')
ret = OrderedDict()
errors = OrderedDict()
fields = self._writable_fields
for field in fields:
validate_method = getattr(self, 'validate_' + field.field_name, None)
primitive_value = field.get_value(data)
try:
validated_value = field.run_validation(primitive_value)
if validate_method is not None:
validated_value = validate_method(validated_value)
except serializers.ValidationError as exc:
errors[field.field_name] = exc.detail
except DjangoValidationError as exc:
errors[field.field_name] = get_error_detail(exc)
except SkipField:
pass
else:
self.set_value(ret, field.source_attrs, validated_value)
if errors:
raise serializers.ValidationError(errors)
return ret
class MotionSerializer(serializers.ModelSerializer):
class RoundsSerializer(serializers.ModelSerializer):
# Should these be filtered if unreleased?
round = fields.TournamentHyperlinkedRelatedField(view_name='api-round-detail',
lookup_field='seq', lookup_url_kwarg='round_seq',
queryset=Round.objects.all())
class Meta:
model = RoundMotion
fields = ('round', 'seq')
url = fields.TournamentHyperlinkedIdentityField(view_name='api-motion-detail')
rounds = RoundsSerializer(many=True, source='roundmotion_set')
info_slide_plain = serializers.CharField(read_only=True)
class Meta:
model = Motion
exclude = ('tournament',)
def create(self, validated_data):
rounds_data = validated_data.pop('roundmotion_set')
motion = super().create(validated_data)
if len(rounds_data) > 0:
rounds = self.RoundsSerializer(many=True, context=self.context)
rounds._validated_data = rounds_data # Data was already validated
rounds.save(motion=motion)
return motion
class BreakCategorySerializer(serializers.ModelSerializer):
class BreakCategoryLinksSerializer(serializers.Serializer):
eligibility = fields.TournamentHyperlinkedIdentityField(
view_name='api-breakcategory-eligibility')
breaking_teams = fields.TournamentHyperlinkedIdentityField(
view_name='api-breakcategory-break')
url = fields.TournamentHyperlinkedIdentityField(
view_name='api-breakcategory-detail')
_links = BreakCategoryLinksSerializer(source='*', read_only=True)
class Meta:
model = BreakCategory
exclude = ('tournament', 'breaking_teams')
validate_slug = partialmethod(_validate_field, 'slug')
validate_seq = partialmethod(_validate_field, 'seq')
class SpeakerCategorySerializer(serializers.ModelSerializer):
class SpeakerCategoryLinksSerializer(serializers.Serializer):
eligibility = fields.TournamentHyperlinkedIdentityField(
view_name='api-speakercategory-eligibility', lookup_field='pk')
url = fields.TournamentHyperlinkedIdentityField(
view_name='api-speakercategory-detail', lookup_field='pk')
_links = SpeakerCategoryLinksSerializer(source='*', read_only=True)
class Meta:
model = SpeakerCategory
exclude = ('tournament',)
validate_slug = partialmethod(_validate_field, 'slug')
validate_seq = partialmethod(_validate_field, 'seq')
class BaseEligibilitySerializer(serializers.ModelSerializer):
class Meta:
read_only_fields = ('slug',)
def update(self, instance, validated_data):
participants = validated_data.get(self.Meta.participants_field, [])
if self.partial:
# Add teams to category, don't remove any
getattr(self.instance, self.Meta.participants_field).add(*participants)
else:
getattr(self.instance, self.Meta.participants_field).set(participants)
return self.instance
class BreakEligibilitySerializer(BaseEligibilitySerializer):
team_set = fields.TournamentHyperlinkedRelatedField(
many=True,
queryset=Team.objects.all(),
view_name='api-team-detail',
)
class Meta(BaseEligibilitySerializer.Meta):
model = BreakCategory
participants_field = 'team_set'
fields = ('slug', participants_field)
class SpeakerEligibilitySerializer(BaseEligibilitySerializer):
speaker_set = fields.TournamentHyperlinkedRelatedField(
many=True,
queryset=Speaker.objects.all(),
view_name='api-speaker-detail',
tournament_field='team__tournament',
)
class Meta(BaseEligibilitySerializer.Meta):
model = SpeakerCategory
participants_field = 'speaker_set'
fields = ('slug', participants_field)
class BreakingTeamSerializer(serializers.ModelSerializer):
team = fields.TournamentHyperlinkedRelatedField(view_name='api-team-detail', queryset=Team.objects.all())
class Meta:
model = BreakingTeam
exclude = ('id', 'break_category')
def validate_team(self, value):
qs = BreakingTeam.objects.filter(
break_category=self.context['break_category'], team=value).exclude(id=getattr(self.instance, 'id', None))
if qs.exists():
raise serializers.ValidationError("Object with same value already exists")
return value
class PartialBreakingTeamSerializer(BreakingTeamSerializer):
class Meta:
model = BreakingTeam
fields = ('team', 'remark')
def validate_team(self, value):
try:
return self.context['break_category'].breakingteam_set.get(team=value)
except BreakingTeam.DoesNotExist:
raise serializers.ValidationError('Team is not included in break')
def save(self, **kwargs):
bt = self.validated_data['team']
bt.remark = self.validated_data.get('remark', '')
bt.save()
return bt
class SpeakerSerializer(serializers.ModelSerializer):
class SpeakerLinksSerializer(serializers.Serializer):
checkin = fields.TournamentHyperlinkedIdentityField(tournament_field='team__tournament', view_name='api-speaker-checkin')
url = fields.TournamentHyperlinkedIdentityField(tournament_field='team__tournament', view_name='api-speaker-detail')
name = fields.AnonymisingParticipantNameField()
team = fields.TournamentHyperlinkedRelatedField(view_name='api-team-detail', queryset=Team.objects.all())
categories = fields.TournamentHyperlinkedRelatedField(
many=True,
view_name='api-speakercategory-detail',
queryset=SpeakerCategory.objects.all(),
)
_links = SpeakerLinksSerializer(source='*', read_only=True)
barcode = serializers.CharField(source='checkin_identifier.barcode', read_only=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not is_staff(kwargs.get('context')):
self.fields.pop('gender')
self.fields.pop('email')
self.fields.pop('phone')
self.fields.pop('pronoun')
self.fields.pop('url_key')
self.fields.pop('barcode')
if kwargs['context']['tournament'].pref('participant_code_names') == 'everywhere':
self.fields.pop('name')
class Meta:
model = Speaker
fields = '__all__'
def create(self, validated_data):
url_key = validated_data.pop('url_key', None)
if url_key is not None and len(url_key) != 0: # Let an empty string be null for the uniqueness constraint
validated_data['url_key'] = url_key
speaker = super().create(validated_data)
if url_key is None:
populate_url_keys([speaker])
if validated_data.get('code_name') is None:
populate_code_names([speaker])
return speaker
class AdjudicatorSerializer(serializers.ModelSerializer):
class AdjudicatorLinksSerializer(serializers.Serializer):
checkin = fields.TournamentHyperlinkedIdentityField(view_name='api-adjudicator-checkin')
url = fields.TournamentHyperlinkedIdentityField(view_name='api-adjudicator-detail')
name = fields.AnonymisingParticipantNameField()
institution = serializers.HyperlinkedRelatedField(
allow_null=True,
view_name='api-global-institution-detail',
queryset=Institution.objects.all(),
)
institution_conflicts = serializers.HyperlinkedRelatedField(
many=True,
view_name='api-global-institution-detail',
queryset=Institution.objects.all(),
)
team_conflicts = fields.TournamentHyperlinkedRelatedField(
many=True,
view_name='api-team-detail',
queryset=Team.objects.all(),
)
adjudicator_conflicts = fields.TournamentHyperlinkedRelatedField(
many=True,
view_name='api-adjudicator-detail',
queryset=Adjudicator.objects.all(),
)
venue_constraints = VenueConstraintSerializer(many=True, required=False)
_links = AdjudicatorLinksSerializer(source='*', read_only=True)
barcode = serializers.CharField(source='checkin_identifier.barcode', read_only=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Remove private fields in the public endpoint if needed
if not is_staff(kwargs.get('context')):
self.fields.pop('institution_conflicts')
self.fields.pop('team_conflicts')
self.fields.pop('adjudicator_conflicts')
self.fields.pop('venue_constraints')
t = kwargs['context']['tournament']
if not t.pref('show_adjudicator_institutions'):
self.fields.pop('institution')
if not t.pref('public_breaking_adjs'):
self.fields.pop('breaking')
if t.pref('participant_code_names') == 'everywhere':
self.fields.pop('name')
self.fields.pop('base_score')
self.fields.pop('trainee')
self.fields.pop('gender')
self.fields.pop('email')
self.fields.pop('phone')
self.fields.pop('pronoun')
self.fields.pop('url_key')
self.fields.pop('barcode')
class Meta:
model = Adjudicator
exclude = ('tournament',)
def create(self, validated_data):
venue_constraints = validated_data.pop('venue_constraints', [])
url_key = validated_data.pop('url_key', None)
if url_key is not None and len(url_key) != 0: # Let an empty string be null for the uniqueness constraint
validated_data['url_key'] = url_key
adj = super().create(validated_data)
if len(venue_constraints) > 0:
vc = VenueConstraintSerializer(many=True, context=self.context)
vc._validated_data = venue_constraints # Data was already validated
vc.save(adjudicator=adj)
if url_key is None: # If explicitly null (and not just an empty string)
populate_url_keys([adj])
if validated_data.get('code_name') is None:
populate_code_names([adj])
if adj.institution is not None:
adj.adjudicatorinstitutionconflict_set.get_or_create(institution=adj.institution)
return adj
def update(self, instance, validated_data):
venue_constraints = validated_data.pop('venue_constraints', [])
if len(venue_constraints) > 0:
vc = VenueConstraintSerializer(many=True, context=self.context)
vc._validated_data = venue_constraints # Data was already validated
vc.save(adjudicator=instance)
if 'base_score' in validated_data and validated_data['base_score'] != instance.base_score:
AdjudicatorBaseScoreHistory.objects.create(
adjudicator=instance, round=self.context['tournament'].current_round, score=validated_data['base_score'])
if self.partial:
# Avoid removing conflicts if merely PATCHing
for field in ['institution_conflicts', 'adjudicator_conflicts', 'team_conflicts']:
validated_data[field] = list(getattr(instance, field).all()) + validated_data.get(field, [])
return super().update(instance, validated_data)
class TeamSerializer(serializers.ModelSerializer):
class TeamSpeakerSerializer(SpeakerSerializer):
team = None
class Meta:
model = Speaker
exclude = ('team',)
url = fields.TournamentHyperlinkedIdentityField(view_name='api-team-detail')
institution = serializers.HyperlinkedRelatedField(
allow_null=True,
view_name='api-global-institution-detail',
queryset=Institution.objects.all(),
required=False,
)
break_categories = fields.TournamentHyperlinkedRelatedField(
many=True,
view_name='api-breakcategory-detail',
queryset=BreakCategory.objects.all(),
required=False,
)
institution_conflicts = serializers.HyperlinkedRelatedField(
many=True,
view_name='api-global-institution-detail',
queryset=Institution.objects.all(),
required=False,
)
venue_constraints = VenueConstraintSerializer(many=True, required=False)
class Meta:
model = Team
exclude = ('tournament', 'type')
def __init__(self, *args, **kwargs):
self.fields['speakers'] = self.TeamSpeakerSerializer(*args, many=True, required=False, **kwargs)
super().__init__(*args, **kwargs)
# Remove private fields in the public endpoint if needed
if not is_staff(kwargs.get('context')):
self.fields.pop('institution_conflicts')
self.fields.pop('venue_constraints')
t = kwargs['context']['tournament']
if t.pref('team_code_names') in ('admin-tooltips-code', 'admin-tooltips-real', 'everywhere'):
self.fields.pop('institution')
self.fields.pop('use_institution_prefix')
self.fields.pop('reference')
self.fields.pop('short_reference')
self.fields.pop('short_name')
self.fields.pop('long_name')
elif not t.pref('show_team_institutions'):
self.fields.pop('institution')
self.fields.pop('use_institution_prefix')
if not t.pref('public_break_categories'):
self.fields.pop('break_categories')
validate_emoji = partialmethod(_validate_field, 'emoji')
def validate(self, data):
if data.get('institution') is None and data.get('use_institution_prefix', False):
raise serializers.ValidationError("Cannot include institution prefix without institution.")
uniqueness_qs = Team.objects.filter(
tournament=self.context['tournament'],
reference=data.get('reference'),
institution=data.get('institution'),
).exclude(id=getattr(self.instance, 'id', None))
if uniqueness_qs.exists() and not self.partial:
raise serializers.ValidationError("Team with same reference and institution exists in the tournament")
return super().validate(data)
def create(self, validated_data):
"""Four things must be done, excluding saving the Team object:
1. Create the short_reference based on 'reference',
2. Create emoji/code name if not stated,
3. Create the speakers.
4. Add institution conflict"""
if validated_data.get('short_reference') is None:
validated_data['short_reference'] = validated_data.get('reference', '')[:34]
speakers_data = validated_data.pop('speakers', [])
break_categories = validated_data.pop('break_categories', [])
venue_constraints = validated_data.pop('venue_constraints', [])
emoji, code_name = pick_unused_emoji(validated_data['tournament'].id)
if 'emoji' not in validated_data or validated_data.get('emoji') is None:
validated_data['emoji'] = emoji
if 'code_name' not in validated_data or validated_data.get('code_name') is None:
validated_data['code_name'] = code_name
if validated_data['emoji'] == '':
validated_data['emoji'] = None # Must convert to null to avoid uniqueness errors
team = super().create(validated_data)
# Add general break categories
team.break_categories.set(list(BreakCategory.objects.filter(
tournament=team.tournament, is_general=True,
).exclude(pk__in=[bc.pk for bc in break_categories])) + break_categories)
# The data is passed to the sub-serializer so that it handles categories
if len(speakers_data) > 0:
speakers = SpeakerSerializer(many=True, context=self.context)
speakers._validated_data = speakers_data # Data was already validated
speakers.save(team=team)
if len(venue_constraints) > 0:
vc = VenueConstraintSerializer(many=True, context=self.context)
vc._validated_data = venue_constraints # Data was already validated
vc.save(team=team)
if team.institution is not None:
team.teaminstitutionconflict_set.get_or_create(institution=team.institution)
return team
def update(self, instance, validated_data):
speakers_data = validated_data.pop('speakers', [])
venue_constraints = validated_data.pop('venue_constraints', [])
if len(speakers_data) > 0:
speakers = SpeakerSerializer(many=True, context=self.context)
speakers._validated_data = speakers_data # Data was already validated
speakers.save(team=instance)
if len(venue_constraints) > 0:
vc = VenueConstraintSerializer(many=True, context=self.context)
vc._validated_data = venue_constraints # Data was already validated
vc.save(institution=instance)
if self.partial:
# Avoid removing conflicts if merely PATCHing
validated_data['institution_conflicts'] = list(instance.institution_conflicts.all()) + validated_data.get('institution_conflicts', [])
return super().update(instance, validated_data)
class InstitutionSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='api-global-institution-detail')
region = fields.CreatableSlugRelatedField(slug_field='name', queryset=Region.objects.all(), required=False)
venue_constraints = VenueConstraintSerializer(many=True, required=False)
class Meta:
model = Institution
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not is_staff(kwargs.get('context')):
self.fields.pop('venue_constraints')
def create(self, validated_data):
venue_constraints = validated_data.pop('venue_constraints', [])
institution = super().create(validated_data)
if len(venue_constraints) > 0:
vc = VenueConstraintSerializer(many=True, context=self.context)
vc._validated_data = venue_constraints # Data was already validated
vc.save(institution=institution)
return institution
def update(self, instance, validated_data):
venue_constraints = validated_data.pop('venue_constraints', [])
if len(venue_constraints) > 0:
vc = VenueConstraintSerializer(many=True, context=self.context)
vc._validated_data = venue_constraints # Data was already validated
vc.save(institution=instance)
return super().update(instance, validated_data)
class PerTournamentInstitutionSerializer(InstitutionSerializer):
teams = fields.TournamentHyperlinkedRelatedField(
source='team_set',
many=True,
view_name='api-team-detail',
required=False,
)
adjudicators = fields.TournamentHyperlinkedRelatedField(
source='adjudicator_set',
many=True,
view_name='api-adjudicator-detail',
required=False,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not is_staff(kwargs.get('context')):
self.fields.pop('teams')
self.fields.pop('adjudicators')
class VenueSerializer(serializers.ModelSerializer):
class VenueLinksSerializer(serializers.Serializer):
checkin = fields.TournamentHyperlinkedIdentityField(view_name='api-venue-checkin')
url = fields.TournamentHyperlinkedIdentityField(view_name='api-venue-detail')
categories = fields.TournamentHyperlinkedRelatedField(
source='venuecategory_set', many=True,
view_name='api-venuecategory-detail',
queryset=VenueCategory.objects.all(),
)
display_name = serializers.ReadOnlyField()
external_url = serializers.URLField(source='url', required=False, allow_blank=True)
_links = VenueLinksSerializer(source='*', read_only=True)
class Meta:
model = Venue
exclude = ('tournament',)
class VenueCategorySerializer(serializers.ModelSerializer):
url = fields.TournamentHyperlinkedIdentityField(view_name='api-venuecategory-detail')
venues = fields.TournamentHyperlinkedRelatedField(
many=True,
view_name='api-venue-detail',
queryset=Venue.objects.all(),
)
class Meta:
model = VenueCategory
exclude = ('tournament',)
def get_metrics_field_type(generator):
return {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'metric': {'type': 'string', 'enum': list(generator.metric_annotator_classes.keys())},
'value': {'type': 'number'},
},
},
}
class BaseStandingsSerializer(serializers.Serializer):
rank = serializers.SerializerMethodField()
tied = serializers.SerializerMethodField()
metrics = serializers.SerializerMethodField()
def get_rank(self, obj) -> int:
return obj.rankings['rank'][0]
def get_tied(self, obj) -> bool:
return obj.rankings['rank'][1]
def get_metrics(self, obj) -> list:
return [{'metric': s, 'value': v} for s, v in obj.metrics.items()]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class TeamStandingsSerializer(BaseStandingsSerializer):
team = fields.TournamentHyperlinkedRelatedField(view_name='api-team-detail', queryset=Team.objects.all())
@extend_schema_field(get_metrics_field_type(TeamStandingsGenerator))
def get_metrics(self, obj) -> list:
return super().get_metrics(obj)
class SpeakerStandingsSerializer(BaseStandingsSerializer):
speaker = fields.AnonymisingHyperlinkedTournamentRelatedField(view_name='api-speaker-detail', anonymous_source='anonymous')
@extend_schema_field(get_metrics_field_type(SpeakerStandingsGenerator))
def get_metrics(self, obj) -> list:
return super().get_metrics(obj)
class DebateAdjudicatorSerializer(serializers.Serializer):
adjudicators = Adjudicator.objects.all()
chair = fields.TournamentHyperlinkedRelatedField(view_name='api-adjudicator-detail', queryset=adjudicators)
panellists = fields.TournamentHyperlinkedRelatedField(many=True, view_name='api-adjudicator-detail', queryset=adjudicators)
trainees = fields.TournamentHyperlinkedRelatedField(many=True, view_name='api-adjudicator-detail', queryset=adjudicators)
def save(self, **kwargs):
aa = kwargs['debate'].adjudicators
aa.chair = self.validated_data.get('chair')
aa.panellists = self.validated_data.get('panellists')
aa.trainees = self.validated_data.get('trainees')
aa.save()
return aa
class RoundPairingSerializer(serializers.ModelSerializer):
class DebateTeamSerializer(serializers.ModelSerializer):
team = fields.TournamentHyperlinkedRelatedField(view_name='api-team-detail', queryset=Team.objects.all())
class Meta:
model = DebateTeam
fields = ('team', 'side')
url = fields.RoundHyperlinkedIdentityField(view_name='api-pairing-detail', lookup_url_kwarg='debate_pk')
venue = fields.TournamentHyperlinkedRelatedField(view_name='api-venue-detail', queryset=Venue.objects.all(),
required=False, allow_null=True)
teams = DebateTeamSerializer(many=True, source='debateteam_set')
adjudicators = DebateAdjudicatorSerializer(required=False, allow_null=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not is_staff(kwargs.get('context')):
self.fields.pop('bracket')
self.fields.pop('room_rank')
self.fields.pop('importance')
self.fields.pop('result_status')
class Meta:
model = Debate
exclude = ('round', 'flags')
def create(self, validated_data):
teams_data = validated_data.pop('debateteam_set')
adjs_data = validated_data.pop('adjudicators', None)
validated_data['round'] = self.context['round']
debate = super().create(validated_data)
teams = self.DebateTeamSerializer(many=True)
teams._validated_data = teams_data # Data was already validated
teams.save(debate=debate)
if adjs_data is not None:
adjudicators = DebateAdjudicatorSerializer()
adjudicators._validated_data = adjs_data
adjudicators.save(debate=debate)
return debate
def update(self, instance, validated_data):
for team in validated_data.pop('debateteam_set', []):
try:
DebateTeam.objects.update_or_create(debate=instance, side=team.get('side'), defaults={
'team': team.get('team'),
})
except (IntegrityError, TypeError) as e:
raise serializers.ValidationError(e)
if 'adjudicators' in validated_data and validated_data['adjudicators'] is not None:
adjudicators = DebateAdjudicatorSerializer()
adjudicators._validated_data = validated_data.pop('adjudicators')
adjudicators.save(debate=instance)
return super().update(instance, validated_data)
class FeedbackQuestionSerializer(serializers.ModelSerializer):
url = fields.TournamentHyperlinkedIdentityField(view_name='api-feedbackquestion-detail')
class Meta:
model = AdjudicatorFeedbackQuestion
exclude = ('tournament',)
validate_reference = partialmethod(_validate_field, 'reference')
validate_seq = partialmethod(_validate_field, 'seq')
class FeedbackSerializer(TabroomSubmissionFieldsMixin, serializers.ModelSerializer):
class SubmitterSourceField(fields.BaseSourceField):
field_source_name = 'source'
models = {
'api-adjudicator-detail': (Adjudicator, 'source_adjudicator'),
'api-team-detail': (Team, 'source_team'),
}
def get_url_options(self, value, format):
for view_name, (model, field) in self.models.items():
if getattr(value, field) is not None:
return self.get_url(
getattr(getattr(value, field), model.__name__.lower()),
view_name, self.context['request'], format)
class DebateHyperlinkedRelatedField(fields.RoundHyperlinkedRelatedField):
def lookup_kwargs(self):
return {self.tournament_field: self.context['tournament']}
class FeedbackAnswerSerializer(serializers.Serializer):
question = fields.TournamentHyperlinkedRelatedField(
view_name='api-feedbackquestion-detail',
queryset=AdjudicatorFeedbackQuestion.objects.all(),
)
answer = fields.AnyField()
def validate(self, data):
# Convert answer to correct type
model = AdjudicatorFeedbackQuestion.ANSWER_TYPE_CLASSES[data['question'].answer_type]
if type(data['answer']) != model.ANSWER_TYPE:
raise serializers.ValidationError({'answer': 'The answer must be of type %s' % model.ANSWER_TYPE.__name__})
data['answer'] = model.ANSWER_TYPE(data['answer'])
option_error = serializers.ValidationError({'answer': 'Answer must be in set of options'})
if len(data['question'].choices) > 0:
if model.ANSWER_TYPE is list and len(set(data['answer']) - set(data['question'].choices)) > 0:
raise option_error
if data['answer'] not in data['question'].choices:
raise option_error
if (data['question'].min_value is not None and data['answer'] < data['question'].min_value) or (data['question'].max_value is not None and data['answer'] > data['question'].max_value):
raise option_error
return super().validate(data)
url = fields.AdjudicatorFeedbackIdentityField(view_name='api-feedback-detail')
adjudicator = fields.TournamentHyperlinkedRelatedField(view_name='api-adjudicator-detail', queryset=Adjudicator.objects.all())
source = SubmitterSourceField(source='*')
participant_submitter = fields.ParticipantSourceField(allow_null=True)
debate = DebateHyperlinkedRelatedField(view_name='api-pairing-detail', queryset=Debate.objects.all(), lookup_url_kwarg='debate_pk')
answers = FeedbackAnswerSerializer(many=True, source='get_answers', required=False)
class Meta:
model = AdjudicatorFeedback
exclude = ('source_adjudicator', 'source_team')
read_only_fields = ('timestamp', 'version',
'submitter_type', 'participant_submitter', 'submitter',
'confirmer', 'confirm_timestamp', 'ip_address', 'private_url')
def validate(self, data):
source = data.pop('source')
debate = data.pop('debate')
source_type = 'from_team' if isinstance(source, Team) else 'from_adj'
required_questions = self.context['tournament'].adjudicatorfeedbackquestion_set.filter(required=True, **{source_type: True})
answers = data.get('get_answers', [])
if len(set(required_questions) - set(a['question'] for a in answers)) > 0:
raise serializers.ValidationError("Answer to required question is missing")
# Test answers for correct source
for answer in answers:
if not getattr(answer['question'], source_type, False):
raise serializers.ValidationError("Question is not permitted from source.")
# Test participants in debate
if not data['adjudicator'].debateadjudicator_set.filter(debate=debate).exists():
raise serializers.ValidationError("Target is not in debate")
# Also move the source field into participant_specific fields
if isinstance(source, Team):
try:
data['source_team'] = source.debateteam_set.get(debate=debate)
except DebateTeam.DoesNotExist:
raise serializers.ValidationError("Source is not in debate")
elif isinstance(source, Adjudicator):
try:
data['source_adjudicator'] = source.debateadjudicator_set.get(debate=debate)
except DebateAdjudicator.DoesNotExist:
raise serializers.ValidationError("Source is not in debate")
return super().validate(data)
def get_request(self):
return self.context['request']
def create(self, validated_data):
answers = validated_data.pop('get_answers')
validated_data.update(self.get_submitter_fields())
if validated_data.get('confirmed', False):
validated_data['confirmer'] = self.context['request'].user
validated_data['confirm_timestamp'] = timezone.now()
feedback = super().create(validated_data)
# Create answers
for answer in answers:
question = answer['question']
model = AdjudicatorFeedbackQuestion.ANSWER_TYPE_CLASSES[question.answer_type]
obj = model(question=question, feedback=feedback, answer=answer['answer'])
try:
obj.save()
except TypeError as e:
raise serializers.ValidationError(e)
return feedback
def update(self, instance, validated_data):
if validated_data.get('confirmed', False) and not instance.confirmed:
validated_data['confirmer'] = self.context['request'].user
validated_data['confirm_timestamp'] = timezone.now()
instance.confirmed = validated_data['confirmed']
instance.ignored = validated_data['ignored']
return super().update(instance, validated_data)
class BallotSerializer(TabroomSubmissionFieldsMixin, serializers.ModelSerializer):
class ResultSerializer(serializers.Serializer):
class SheetSerializer(serializers.Serializer):
class TeamResultSerializer(serializers.Serializer):
side = serializers.ChoiceField(choices=DebateTeam.Side.choices)
points = serializers.IntegerField(required=False)
win = serializers.BooleanField(required=False)
score = serializers.FloatField(required=False, allow_null=True)
team = fields.TournamentHyperlinkedRelatedField(
view_name='api-team-detail',
queryset=Team.objects.all(),
)
class SpeechSerializer(serializers.Serializer):
ghost = serializers.BooleanField(required=False, help_text=SpeakerScore._meta.get_field('ghost').help_text)
score = serializers.FloatField()
rank = serializers.IntegerField(required=False)
speaker = fields.TournamentHyperlinkedRelatedField(
view_name='api-speaker-detail',
queryset=Speaker.objects.all(),
tournament_field='team__tournament',
)
def save(self, **kwargs):
"""Requires `result`, `side`, `seq`, and `adjudicator` as extra"""
result = kwargs['result']
speaker_args = [kwargs['side'], kwargs['seq']]
result.set_speaker(*speaker_args, self.validated_data['speaker'])
if self.validated_data.get('ghost', False):
result.set_ghost(*speaker_args)
if kwargs.get('adjudicator') is not None:
speaker_args.insert(0, kwargs['adjudicator'])
result.set_score(*speaker_args, self.validated_data['score'])
if kwargs.get('rank') is not None:
result.set_speaker_rank(*speaker_args, self.validated_data['rank'])
return result
speeches = SpeechSerializer(many=True, required=False)
def validate(self, data):
# Make sure the score is the sum of the speech scores
score = data.get('score', None)
speeches = data.get('speeches', [])
if len(speeches) == 0:
if score is not None:
raise serializers.ValidationError("Speeches are required to assign scores.")
elif score is not None and score != sum(speech['score'] for speech in speeches):
raise serializers.ValidationError("Score must be the sum of speech scores.")
# Speakers must be in correct team
team = data.get('team', None)
speakers_team = set(s['speaker'].team_id for s in speeches)
if team is None or len(speakers_team) > 1 or (len(speakers_team) == 1 and team.id not in speakers_team):
raise serializers.ValidationError("Speakers must be in their team.")
return data
def save(self, **kwargs):
result = kwargs['result']
if result.get_scoresheet_class().uses_declared_winners and self.validated_data.get('win', False):
args = [self.validated_data['side']]
if kwargs.get('adjudicator') is not None and not result.ballotsub.single_adj:
args.insert(0, kwargs.get('adjudicator'))
result.add_winner(*args)
speech_serializer = self.SpeechSerializer(context=self.context)
for i, speech in enumerate(self.validated_data.get('speeches', []), 1):
speech_serializer._validated_data = speech
speech_serializer.save(
result=result,
side=self.validated_data['side'],
seq=i,
adjudicator=kwargs.get('adjudicator'),
)
return result
teams = TeamResultSerializer(many=True)
adjudicator = fields.TournamentHyperlinkedRelatedField(
view_name='api-adjudicator-detail',
queryset=Adjudicator.objects.all(),
required=False, allow_null=True,
)
def validate_adjudicator(self, value):
# Make sure adj is in debate
if not self.context.get('debate').debateadjudicator_set.filter(adjudicator=value).exists():
raise serializers.ValidationError('Adjudicator must be in debate')
return value
def validate_teams(self, value):
# Teams in their proper positions - this also checks having proper and consistent sides
debate = self.context.get('debate')
if len(value) != debate.debateteam_set.count():
raise serializers.ValidationError('Incorrect number of teams')
for team in value:
if debate.get_team(team['side']) != team['team']:
raise serializers.ValidationError('Inconsistent team')
return value
def save(self, **kwargs):
team_serializer = self.TeamResultSerializer(context=self.context)
for team in self.validated_data.get('teams', []):
team_serializer._validated_data = team
team_serializer.save(result=kwargs['result'], adjudicator=self.validated_data.get('adjudicator'))
return kwargs['result']
sheets = SheetSerializer(many=True, required=True)
def validate(self, data):
# Make sure the speaker order is the same between adjs
# There must be at least one sheet
if len(data.get('sheets', [])) == 0:
raise serializers.ValidationError('Must have at least one sheet')
speaker_order = [s['speaker'] for t in data['sheets'][0]['teams'] for s in t.get('speeches', [])]
for sheet in data['sheets']:
speeches = [s['speaker'] for t in sheet.get('teams', []) for s in t.get('speeches', [])]
for i, speech in enumerate(speeches):
if speech != speaker_order[i]:
raise serializers.ValidationError('Inconsistant speaker order')
return data
def create(self, validated_data):
result = DebateResult(validated_data['ballot'], tournament=self.context.get('tournament'))
sheets = self.SheetSerializer(context=self.context)
for sheet in validated_data['sheets']:
sheets._validated_data = sheet
sheets.save(result=result)
try:
result.save()
except ResultError as e:
raise serializers.ValidationError(str(e))
return result
class VetoSerializer(serializers.ModelSerializer):
team = fields.TournamentHyperlinkedRelatedField(
source='debate_team.team', view_name='api-team-detail', queryset=Team.objects.all())
motion = fields.TournamentHyperlinkedRelatedField(view_name='api-motion-detail', queryset=Motion.objects.all())
class Meta:
model = DebateTeamMotionPreference
exclude = ('id', 'ballot_submission', 'preference', 'debate_team')
def create(self, validated_data):
team = validated_data.pop('debate_team').pop('team')
try:
validated_data['debate_team'] = DebateTeam.objects.get(debate=self.context['debate'], team=team)
except (DebateTeam.DoesNotExist, DebateTeam.MultipleObjectsReturned):
raise serializers.ValidationError('Team is not in debate')
return super().create(validated_data)
result = ResultSerializer(source='result.get_result_info')
motion = fields.TournamentHyperlinkedRelatedField(view_name='api-motion-detail', required=False, queryset=Motion.objects.all())
url = fields.DebateHyperlinkedIdentityField(view_name='api-ballot-detail')
participant_submitter = fields.ParticipantSourceField(allow_null=True, required=False)
vetos = VetoSerializer(many=True, source='debateteammotionpreference_set', required=False, allow_null=True)
class Meta:
model = BallotSubmission
exclude = ('debate',)
read_only_fields = ('timestamp', 'version',
'submitter_type', 'submitter', 'participant_submitter',
'confirmer', 'confirm_timestamp', 'ip_address', 'private_url')
def get_request(self):
return self.context['request']
def create(self, validated_data):
result_data = validated_data.pop('result').pop('get_result_info')
veto_data = validated_data.pop('debateteammotionpreference_set', None)
validated_data.update(self.get_submitter_fields())
if validated_data.get('confirmed', False):
validated_data['confirmer'] = self.context['request'].user
validated_data['confirm_timestamp'] = timezone.now()
stage = 'elim' if self.context['round'].stage == Round.Stage.ELIMINATION else 'prelim'
if self.context['tournament'].pref('ballots_per_debate_' + stage) == 'per-adj':
debateadj_count = self.context['debate'].debateadjudicator_set.exclude(type=DebateAdjudicator.TYPE_TRAINEE).count()
if debateadj_count > 1:
if len(result_data['sheets']) == 1:
validated_data['participant_submitter'] = result_data['sheets'][0]['adjudicator']
validated_data['single_adj'] = True
elif validated_data.get('single_adj', False):
raise serializers.ValidationError({'single_adj': 'Single-adjudicator ballots can only have one scoresheet'})
elif len(result_data['sheets']) != debateadj_count:
raise serializers.ValidationError({
'result': 'Voting ballots must either have one scoresheet or ballots from all voting adjudicators',
})
elif len(result_data['sheets']) > 1:
raise serializers.ValidationError({'result': 'Consensus ballots can only have one scoresheet'})
ballot = super().create(validated_data)
result = self.ResultSerializer(context=self.context)
result._validated_data = result_data
result._errors = []
result.save(ballot=ballot)
if veto_data:
vetos = self.VetoSerializer(context=self.context, many=True)
vetos._validated_data = veto_data
vetos._errors = []
vetos.save(ballot_submission=ballot, preference=3)
return ballot
def update(self, instance, validated_data):
if validated_data['confirmed'] and not instance.confirmed:
validated_data['confirmer'] = self.context['request'].user
validated_data['confirm_timestamp'] = timezone.now()
instance.confirmed = validated_data['confirmed']
instance.discarded = validated_data['discarded']
instance.save()
return instance
class UpdateBallotSerializer(serializers.ModelSerializer):
"""Unused, just for OpenAPI with BallotSerializer.update()"""
class Meta:
model = BallotSubmission
fields = ('confirmed', 'discarded')
class PreformedPanelSerializer(serializers.ModelSerializer):
url = fields.RoundHyperlinkedIdentityField(view_name='api-preformedpanel-detail', lookup_url_kwarg='debate_pk')
adjudicators = DebateAdjudicatorSerializer(required=False, allow_null=True)
class Meta:
model = PreformedPanel
exclude = ('round',)
def create(self, validated_data):
adjs_data = validated_data.pop('adjudicators', None)
validated_data['round'] = self.context['round']
debate = super().create(validated_data)
if adjs_data is not None:
adjudicators = DebateAdjudicatorSerializer()
adjudicators._validated_data = adjs_data
adjudicators.save(debate=debate)
return debate
def update(self, instance, validated_data):
if validated_data.get('adjudicators', None) is not None:
adjudicators = DebateAdjudicatorSerializer()
adjudicators._validated_data = validated_data.pop('adjudicators')
adjudicators.save(debate=instance)
return super().update(instance, validated_data)
class SpeakerRoundScoresSerializer(serializers.ModelSerializer):
class RoundScoresSerializer(serializers.ModelSerializer):
class RoundSpeechSerializer(serializers.ModelSerializer):
class Meta:
model = SpeakerScore
fields = ('score', 'position', 'ghost')
round = fields.TournamentHyperlinkedRelatedField(view_name='api-round-detail', source='debate.round',
lookup_field='seq', lookup_url_kwarg='round_seq',
queryset=Round.objects.all())
speeches = RoundSpeechSerializer(many=True, source="scores")
class Meta:
model = DebateTeam
fields = ('round', 'speeches')
speaker = fields.TournamentHyperlinkedIdentityField(tournament_field='team__tournament', view_name='api-speaker-detail')
rounds = RoundScoresSerializer(many=True, source="debateteams")
class Meta:
model = Speaker
fields = ('speaker', 'rounds')
class TeamRoundScoresSerializer(serializers.ModelSerializer):
class ScoreSerializer(serializers.ModelSerializer):
round = fields.TournamentHyperlinkedRelatedField(view_name='api-round-detail', source='debate.round',
lookup_field='seq', lookup_url_kwarg='round_seq',
queryset=Round.objects.all())
points = serializers.IntegerField(source='ballot.points')
score = serializers.FloatField(source='ballot.score')
has_ghost = serializers.BooleanField(source='ballot.has_ghost')
class Meta:
model = TeamScore
fields = ('round', 'points', 'score', 'has_ghost')
team = fields.TournamentHyperlinkedIdentityField(view_name='api-team-detail')
rounds = ScoreSerializer(many=True, source="debateteam_set")
class Meta:
model = Team
fields = ('team', 'rounds')
class UserSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='api-users-detail')
class Meta:
model = get_user_model()
fields = ('url', 'id', 'username', 'password', 'email', 'is_staff', 'is_superuser', 'is_active')
def create(self, validated_data):
user = self.Meta.model(**validated_data)
user.set_password(validated_data['password'])
user.save()
return user