<commit_before># -*- coding: utf-8 -*-
"""API Serializers."""
from collections import OrderedDict
from copy import deepcopy

from django.db.models import CharField
from django.contrib.auth.models import User
from rest_framework.serializers import (
    CurrentUserDefault, DateTimeField, IntegerField,
    ModelSerializer, SerializerMethodField, ValidationError)
from sortedm2m.fields import SortedManyToManyField

from . import fields
from .drf_fields import (
    CurrentHistoryField, HistoricalObjectField, HistoryField,
    MPTTRelationField, OptionalCharField, OptionalIntegerField,
    PrimaryKeyRelatedField, TranslatedTextField)
from .history import Changeset
from .models import (
    Browser, Feature, Maturity, Section, Specification, Support, Version)
from .validators import VersionAndStatusValidator


#
# "Regular" Serializers
#
class WriteRestrictedMixin(object):

    def get_fields(self):
        """Add read_only flag for write-restricted fields."""
        fields = super(WriteRestrictedMixin, self).get_fields()

        # Some fields are read-only based on view action
        view = self.context.get('view', None)
        if view and view.action in ('list', 'create'):
            set_to_readonly = 'update_only'
        elif view and view.action in ('update', 'partial_update'):
            set_to_readonly = 'create_only'
        else:
            set_to_readonly = None

        # Set fields to read-only based on view action
        if set_to_readonly:
            fields_extra = getattr(self.Meta, 'fields_extra', {})
            for field_name, field in fields.items():
                field_extra = fields_extra.get(field_name, {})
                writable = field_extra.get('writable', True)
                if writable == set_to_readonly:
                    assert not field.read_only, (
                        ('%s was requested to be set read-only for %s,'
                         ' but is already read-only by default.')
                        % (field_name, view and view.action or 'unknown'))
                    field.read_only = True

        return fields


class FieldMapMixin(object):
    """Automatically handle fields used by this project."""

    serializer_field_mapping = ModelSerializer.serializer_field_mapping
    serializer_field_mapping[fields.TranslatedField] = TranslatedTextField
    serializer_field_mapping[CharField] = OptionalCharField
    serializer_field_mapping[SortedManyToManyField] = PrimaryKeyRelatedField
    serializer_related_field = PrimaryKeyRelatedField

    def build_standard_field(self, field_name, model_field):
        field_class, field_kwargs = super(
            FieldMapMixin, self).build_standard_field(
                field_name, model_field)
        if isinstance(model_field, fields.TranslatedField):
            if not (model_field.blank or model_field.null):
                field_kwargs['required'] = True
            if model_field.allow_canonical:
                field_kwargs['allow_canonical'] = True
        return field_class, field_kwargs


class FieldsExtraMixin(object):
    """Add fields_extra property to serializer."""

    @classmethod
    def get_fields_extra(cls):
        return getattr(cls.Meta, 'fields_extra', {})


class HistoricalModelSerializer(
        WriteRestrictedMixin, FieldMapMixin, FieldsExtraMixin,
        ModelSerializer):
    """Model serializer with history manager."""

    omit_historical_fields = (
        'id', 'history_id', 'history_date', 'history_user', 'history_type',
        'history_changeset')

    def build_property_field(self, field_name, model_class):
        """Handle history field.

        The history field is a list of PKs for all the history records.
        """
        assert field_name == 'history', (
            'Expected field name to be "history", got "%s"'
            % field_name)
        field_kwargs = {'many': True, 'read_only': True}
        return HistoryField, field_kwargs

    def build_unknown_field(self, field_name, model_class):
        """Handle history_current field.

        history_current returns the PK of the most recent history record.
        It is treated as read-only unless it is an update view.
        """
        assert field_name == 'history_current', (
            'Expected field name to be "history_current", got "%s"'
            % field_name)

        return CurrentHistoryField, {}

    def to_internal_value(self, data):
        """If history_current in data, load historical data into instance."""
        if data and 'history_current' in data:
            history_id = int(data['history_current'])
            current_history = self.instance.history.all()[0]
            if current_history.history_id != history_id:
                try:
                    historical = self.instance.history.get(
                        history_id=history_id)
                except self.instance.history.model.DoesNotExist:
                    err = 'Invalid history ID for this object'
                    raise ValidationError({'history_current': [err]})
                else:
                    for field in historical._meta.fields:
                        if field.attname in self.omit_historical_fields:
                            continue
                        attname = field.attname
                        hist_value = getattr(historical, attname)
                        data_name = attname
                        if data_name.endswith('_id'):
                            data_name = data_name[:-len('_id')]
                        data[data_name] = hist_value
        return super(HistoricalModelSerializer, self).to_internal_value(data)


class BrowserSerializer(HistoricalModelSerializer):
    """Browser Serializer."""

    def update(self, instance, validated_data):
        versions = validated_data.pop('versions', None)
        instance = super(BrowserSerializer, self).update(
            instance, validated_data)

        if versions:
            v_pks = [v.pk for v in versions]
            current_order = instance.get_version_order()
            if v_pks != current_order:
                instance.set_version_order(v_pks)
        return instance

    class Meta:
        model = Browser
        fields = (
            'id', 'slug', 'name', 'note', 'versions', 'history_current',
            'history')
        fields_extra = {
            'id': {
                'link': 'self',
                'resource': 'browsers',
            },
            'slug': {
                'writable': 'create_only',
            },
            'versions': {
                'link': 'from_many',
                'writable': 'update_only',
            },
            'history_current': {
                'archive': 'history_id',
                'link': 'from_one',
                'resource': 'historical_browsers',
                'writable': 'update_only',
            },
            'history': {
                'archive': 'omit',
                'link': 'from_many',
                'resource': 'historical_browsers',
            },
        }


class FeatureSerializer(HistoricalModelSerializer):
    """Feature Serializer."""

    children = MPTTRelationField(
        many=True, queryset=Feature.objects.all(), required=False)

    def update(self, instance, validated_data):
        children = validated_data.pop('children', None)
        if children:
            current_order = list(instance.get_children())
            if current_order == children:
                children = None
        instance = super(FeatureSerializer, self).update(
            instance, validated_data)

        if children:
            instance.set_children_order(children)
        return instance

    def validate_children(self, value):
        if self.instance:
            current_children = list(self.instance.get_children())
            current_set = set([child.pk for child in current_children])
            new_set = set([child.pk for child in value])
            if current_set - new_set:
                raise ValidationError(
                    'All child features must be included in children.')
            if new_set - current_set:
                raise ValidationError(
                    'Set child.parent to add a child feature.')
        else:
            if value != []:  # pragma: no cover
                # Because children is in update_only_fields, never happens
                raise ValidationError(
                    'Can not set children when creating a feature.')
        return value

    class Meta:
        model = Feature
        fields = (
            'id', 'slug', 'mdn_uri', 'experimental', 'standardized',
            'stable', 'obsolete', 'name', 'parent', 'children',
            'sections', 'supports', 'history_current', 'history')
        read_only_fields = ('supports',)
        fields_extra = {
            'id': {
                'link': 'self',
                'resource': 'features',
            },
            'parent': {
                'link': 'to_one',
                'resource': 'features',
            },
            'children': {
                'link': 'from_many',
                'resource': 'features',
                'writable': 'update_only'
            },
            'sections': {
                'link': 'from_many',
            },
            'supports': {
                'archive': 'omit',
                'link': 'from_many',
            },
            'history_current': {
                'archive': 'history_id',
                'link': 'from_one',
                'resource': 'historical_features',
                'writable': 'update_only',
            },
            'history': {
                'archive': 'omit',
                'link': 'from_many',
                'resource': 'historical_features',
            },
        }


class MaturitySerializer(HistoricalModelSerializer):
    """Specification Maturity Serializer."""

    class Meta:
        model = Maturity
        fields = (
            'id', 'slug', 'name', 'specifications',
            'history_current', 'history')
        read_only_fields = ('specifications',)
        fields_extra = {
            'id': {
                'link': 'self',
                'resource': 'maturities',
                'singular': 'maturity',
            },
            'specifications': {
                'archive': 'omit',
                'link': 'from_many',
            },
            'history_current': {
                'archive': 'history_id',
                'link': 'from_one',
                'resource': 'historical_maturities',
                'writable': 'update_only',
            },
            'history': {
                'archive': 'omit',
                'link': 'from_many',
                'resource': 'historical_maturities',
            },
        }


class SectionSerializer(HistoricalModelSerializer):
    """Specification Section Serializer."""

    class Meta:
        model = Section
        fields = (
            'id', 'number', 'name', 'subpath', 'note', 'specification',
            'features', 'history_current', 'history')
        extra_kwargs = {
            'features': {
                'default': []
            }
        }
        fields_extra = {
            'id': {
                'link': 'self',
                'resource': 'sections',
            },
            'specification': {
                'link': 'to_one',
                'resource': 'specifications',
            },
            'features': {
                'archive': 'omit',
                'link': 'from_many',
            },
            'history_current': {
                'archive': 'history_id',
                'link': 'from_one',
                'resource': 'historical_sections',
                'writable': 'update_only',
            },
            'history': {
                'archive': 'omit',
                'link': 'from_many',
                'resource': 'historical_sections',
            },
        }


class SpecificationSerializer(HistoricalModelSerializer):
    """Specification Serializer."""

    def update(self, instance, validated_data):
        sections = validated_data.pop('sections', None)
        instance = super(SpecificationSerializer, self).update(
            instance, validated_data)

        if sections:
            s_pks = [s.pk for s in sections]
            current_order = instance.get_section_order()
            if s_pks != current_order:
                instance.set_section_order(s_pks)

        return instance

    class Meta:
        model = Specification
        fields = (
            'id', 'slug', 'mdn_key', 'name', 'uri', 'maturity', 'sections',
            'history_current', 'history')
        extra_kwargs = {
            'sections': {
                'default': []
            }
        }
        fields_extra = {
            'id': {
                'link': 'self',
                'resource': 'specifications',
            },
            'maturity': {
                'link': 'to_one',
                'resource': 'maturities',
            },
            'sections': {
                'link': 'from_many',
            },
            'history_current': {
                'archive': 'history_id',
                'link': 'from_one',
                'resource': 'historical_specifications',
                'writable': 'update_only',
            },
            'history': {
                'archive': 'omit',
                'link': 'from_many',
                'resource': 'historical_specifications',
            },
        }


class SupportSerializer(HistoricalModelSerializer):
    """Support Serializer."""

    class Meta:
        model = Support
        fields = (
            'id', 'support', 'prefix',
            'prefix_mandatory', 'alternate_name', 'alternate_mandatory',
            'requires_config', 'default_config', 'protected', 'note',
            'version', 'feature', 'history_current', 'history')
        fields_extra = {
            'id': {
                'link': 'self',
                'resource': 'supports',
            },
            'version': {
                'link': 'to_one',
                'resource': 'versions',
            },
            'feature': {
                'link': 'to_one',
                'resource': 'features',
            },
            'history_current': {
                'archive': 'history_id',
                'link': 'from_one',
                'resource': 'historical_supports',
                'writable': 'update_only',
            },
            'history': {
                'archive': 'omit',
                'link': 'from_many',
                'resource': 'historical_supports',
            },
        }


class VersionSerializer(HistoricalModelSerializer):
    """Browser Version Serializer."""

    order = IntegerField(read_only=True, source='_order')

    class Meta:
        model = Version
        fields = (
            'id', 'version', 'release_day', 'retirement_day',
            'status', 'release_notes_uri', 'note', 'order', 'browser',
            'supports', 'history_current', 'history')
        extra_kwargs = {
            'version': {
                'allow_blank': False
            }
        }
        read_only_fields = ('supports',)
        validators = [VersionAndStatusValidator()]
        fields_extra = {
            'id': {
                'link': 'self',
                'resource': 'versions',
            },
            'version': {
                'writable': 'create_only',
            },
            'browser': {
                'link': 'to_one',
                'resource': 'browsers',
            },
            'supports': {
                'archive': 'omit',
                'link': 'from_many',
            },
            'history_current': {
                'archive': 'history_id',
                'link': 'from_one',
                'resource': 'historical_versions',
                'writable': 'update_only',
            },
            'history': {
                'archive': 'omit',
                'link': 'from_many',
                'resource': 'historical_versions',
            },
        }


#
# Change control object serializers
#

class ChangesetSerializer(FieldsExtraMixin, ModelSerializer):
    """Changeset Serializer."""

    target_resource_type = OptionalCharField(required=False)
    target_resource_id = OptionalIntegerField(required=False)

    class Meta:
        model = Changeset
        fields = (
            'id', 'created', 'modified', 'closed', 'target_resource_type',
            'target_resource_id', 'user',
            'historical_browsers', 'historical_features',
            'historical_maturities', 'historical_sections',
            'historical_specifications', 'historical_supports',
            'historical_versions')
        read_only_fields = (
            'id', 'created', 'modified',
            'historical_browsers', 'historical_features',
            'historical_maturities', 'historical_sections',
            'historical_specifications', 'historical_supports',
            'historical_versions')
        extra_kwargs = {
            'user': {
                'default': CurrentUserDefault()
            }
        }
        fields_extra = {
            'id': {
                'link': 'self',
                'resource': 'changesets',
            },
            'user': {
                'link': 'to_one',
                'resource': 'users',
                'writable': 'update_only',
            },
            'target_resource_type': {
                'writable': 'update_only',
            },
            'target_resource_id': {
                'writable': 'update_only',
            },
            'historical_browsers': {
                'link': 'from_many',
            },
            'historical_features': {
                'link': 'from_many',
            },
            'historical_maturities': {
                'link': 'from_many',
            },
            'historical_specifications': {
                'link': 'from_many',
            },
            'historical_sections': {
                'link': 'from_many',
            },
            'historical_supports': {
                'link': 'from_many',
            },
            'historical_versions': {
                'link': 'from_many',
            },
        }


class UserSerializer(FieldsExtraMixin, ModelSerializer):
    """User Serializer."""

    created = DateTimeField(source='date_joined', read_only=True)
    agreement = SerializerMethodField()
    permissions = SerializerMethodField()

    def get_agreement(self, obj):
        """Return the version of the contribution terms the user agreed to.

        Placeholder for when we have a license agreement.
        """
        return 0

    def get_permissions(self, obj):
        """Return names of django.contrib.auth Groups."""
        try:
            # Cached objects (or those that have run through
            #  cache.user_v1_serializer) have this property
            return obj.group_names
        except AttributeError:
            return sorted(obj.groups.values_list('name', flat=True))

    class Meta:
        model = User
        fields = (
            'id', 'username', 'created', 'agreement', 'permissions',
            'changesets')
        read_only_fields = ('username', 'changesets')
        fields_extra = {
            'id': {
                'link': 'self',
                'resource': 'users',
            },
            'changesets': {
                'link': 'from_many',
            },
        }


#
# Historical object serializers
#


class ArchiveMixin(object):
    def get_fields(self):
        """Modify fields when loading or preparing an archive."""
        fields = super(ArchiveMixin, self).get_fields()
        fields_extra = getattr(self.Meta, 'fields_extra', {})
        for field_name, field in fields.items():
            field_extra = fields_extra.get(field_name, {})
            archive = field_extra.get('archive')
            link = field_extra.get('link')
            if archive == 'omit':
                # Does not appear in archived representation
                del fields[field_name]
            elif link in ('from_one', 'from_many'):
                # Defer loading until HistoricalObjectSerializer.get_archive
                del fields[field_name]
            elif link == 'to_one':
                # Use the name_id field
                field.source = field_name + '_id'
        return fields


class HistoricalObjectSerializer(ModelSerializer):
    """Common serializer attributes for Historical models."""

    id = IntegerField(source='history_id')
    date = DateTimeField(source='history_date')
    event = SerializerMethodField()
    changeset = PrimaryKeyRelatedField(
        source='history_changeset', read_only=True)
    object_id = HistoricalObjectField()
    archived_representation = SerializerMethodField()

    EVENT_CHOICES = {
        '+': 'created',
        '~': 'changed',
        '-': 'deleted',
    }
<commit_msg>bug 1238587 - Don't mutate dict during iteration<commit_after># -*- coding: utf-8 -*-
"""API Serializers."""
from collections import OrderedDict
from copy import deepcopy

from django.db.models import CharField
from django.contrib.auth.models import User
from rest_framework.serializers import (
    CurrentUserDefault, DateTimeField, IntegerField,
    ModelSerializer, SerializerMethodField, ValidationError)
from sortedm2m.fields import SortedManyToManyField

from . import fields
from .drf_fields import (
    CurrentHistoryField, HistoricalObjectField, HistoryField,
    MPTTRelationField, OptionalCharField, OptionalIntegerField,
    PrimaryKeyRelatedField, TranslatedTextField)
from .history import Changeset
from .models import (
    Browser, Feature, Maturity, Section, Specification, Support, Version)
from .validators import VersionAndStatusValidator


#
# "Regular" Serializers
#
class WriteRestrictedMixin(object):

    def get_fields(self):
        """Add read_only flag for write-restricted fields."""
        fields = super(WriteRestrictedMixin, self).get_fields()

        # Some fields are read-only based on view action
        view = self.context.get('view', None)
        if view and view.action in ('list', 'create'):
            set_to_readonly = 'update_only'
        elif view and view.action in ('update', 'partial_update'):
            set_to_readonly = 'create_only'
        else:
            set_to_readonly = None

        # Set fields to read-only based on view action
        if set_to_readonly:
            fields_extra = getattr(self.Meta, 'fields_extra', {})
            for field_name, field in fields.items():
                field_extra = fields_extra.get(field_name, {})
                writable = field_extra.get('writable', True)
                if writable == set_to_readonly:
                    assert not field.read_only, (
                        ('%s was requested to be set read-only for %s,'
                         ' but is already read-only by default.')
                        % (field_name, view and view.action or 'unknown'))
                    field.read_only = True

        return fields


class FieldMapMixin(object):
    """Automatically handle fields used by this project."""

    serializer_field_mapping = ModelSerializer.serializer_field_mapping
    serializer_field_mapping[fields.TranslatedField] = TranslatedTextField
    serializer_field_mapping[CharField] = OptionalCharField
    serializer_field_mapping[SortedManyToManyField] = PrimaryKeyRelatedField
    serializer_related_field = PrimaryKeyRelatedField

    def build_standard_field(self, field_name, model_field):
        field_class, field_kwargs = super(
            FieldMapMixin, self).build_standard_field(
                field_name, model_field)
        if isinstance(model_field, fields.TranslatedField):
            if not (model_field.blank or model_field.null):
                field_kwargs['required'] = True
            if model_field.allow_canonical:
                field_kwargs['allow_canonical'] = True
        return field_class, field_kwargs


class FieldsExtraMixin(object):
    """Add fields_extra property to serializer."""

    @classmethod
    def get_fields_extra(cls):
        return getattr(cls.Meta, 'fields_extra', {})


class HistoricalModelSerializer(
        WriteRestrictedMixin, FieldMapMixin, FieldsExtraMixin,
        ModelSerializer):
    """Model serializer with history manager."""

    omit_historical_fields = (
        'id', 'history_id', 'history_date', 'history_user', 'history_type',
        'history_changeset')

    def build_property_field(self, field_name, model_class):
        """Handle history field.

        The history field is a list of PKs for all the history records.
        """
        assert field_name == 'history', (
            'Expected field name to be "history", got "%s"'
            % field_name)
        field_kwargs = {'many': True, 'read_only': True}
        return HistoryField, field_kwargs

    def build_unknown_field(self, field_name, model_class):
        """Handle history_current field.

        history_current returns the PK of the most recent history record.
        It is treated as read-only unless it is an update view.
        """
        assert field_name == 'history_current', (
            'Expected field name to be "history_current", got "%s"'
            % field_name)

        return CurrentHistoryField, {}

    def to_internal_value(self, data):
        """If history_current in data, load historical data into instance."""
        if data and 'history_current' in data:
            history_id = int(data['history_current'])
            current_history = self.instance.history.all()[0]
            if current_history.history_id != history_id:
                try:
                    historical = self.instance.history.get(
                        history_id=history_id)
                except self.instance.history.model.DoesNotExist:
                    err = 'Invalid history ID for this object'
                    raise ValidationError({'history_current': [err]})
                else:
                    for field in historical._meta.fields:
                        if field.attname in self.omit_historical_fields:
                            continue
                        attname = field.attname
                        hist_value = getattr(historical, attname)
                        data_name = attname
                        if data_name.endswith('_id'):
                            data_name = data_name[:-len('_id')]
                        data[data_name] = hist_value
        return super(HistoricalModelSerializer, self).to_internal_value(data)


class BrowserSerializer(HistoricalModelSerializer):
    """Browser Serializer."""

    def update(self, instance, validated_data):
        versions = validated_data.pop('versions', None)
        instance = super(BrowserSerializer, self).update(
            instance, validated_data)

        if versions:
            v_pks = [v.pk for v in versions]
            current_order = instance.get_version_order()
            if v_pks != current_order:
                instance.set_version_order(v_pks)
        return instance

    class Meta:
        model = Browser
        fields = (
            'id', 'slug', 'name', 'note', 'versions', 'history_current',
            'history')
        fields_extra = {
            'id': {
                'link': 'self',
                'resource': 'browsers',
            },
            'slug': {
                'writable': 'create_only',
            },
            'versions': {
                'link': 'from_many',
                'writable': 'update_only',
            },
            'history_current': {
                'archive': 'history_id',
                'link': 'from_one',
                'resource': 'historical_browsers',
                'writable': 'update_only',
            },
            'history': {
                'archive': 'omit',
                'link': 'from_many',
                'resource': 'historical_browsers',
            },
        }


class FeatureSerializer(HistoricalModelSerializer):
    """Feature Serializer."""

    children = MPTTRelationField(
        many=True, queryset=Feature.objects.all(), required=False)

    def update(self, instance, validated_data):
        children = validated_data.pop('children', None)
        if children:
            current_order = list(instance.get_children())
            if current_order == children:
                children = None
        instance = super(FeatureSerializer, self).update(
            instance, validated_data)

        if children:
            instance.set_children_order(children)
        return instance

    def validate_children(self, value):
        if self.instance:
            current_children = list(self.instance.get_children())
            current_set = set([child.pk for child in current_children])
            new_set = set([child.pk for child in value])
            if current_set - new_set:
                raise ValidationError(
                    'All child features must be included in children.')
            if new_set - current_set:
                raise ValidationError(
                    'Set child.parent to add a child feature.')
        else:
            if value != []:  # pragma: no cover
                # Because children is in update_only_fields, never happens
                raise ValidationError(
                    'Can not set children when creating a feature.')
        return value

    class Meta:
        model = Feature
        fields = (
            'id', 'slug', 'mdn_uri', 'experimental', 'standardized',
            'stable', 'obsolete', 'name', 'parent', 'children',
            'sections', 'supports', 'history_current', 'history')
        read_only_fields = ('supports',)
        fields_extra = {
            'id': {
                'link': 'self',
                'resource': 'features',
            },
            'parent': {
                'link': 'to_one',
                'resource': 'features',
            },
            'children': {
                'link': 'from_many',
                'resource': 'features',
                'writable': 'update_only'
            },
            'sections': {
                'link': 'from_many',
            },
            'supports': {
                'archive': 'omit',
                'link': 'from_many',
            },
            'history_current': {
                'archive': 'history_id',
                'link': 'from_one',
                'resource': 'historical_features',
                'writable': 'update_only',
            },
            'history': {
                'archive': 'omit',
                'link': 'from_many',
                'resource': 'historical_features',
            },
        }


class MaturitySerializer(HistoricalModelSerializer):
    """Specification Maturity Serializer."""

    class Meta:
        model = Maturity
        fields = (
            'id', 'slug', 'name', 'specifications',
            'history_current', 'history')
        read_only_fields = ('specifications',)
        fields_extra = {
            'id': {
                'link': 'self',
                'resource': 'maturities',
                'singular': 'maturity',
            },
            'specifications': {
                'archive': 'omit',
                'link': 'from_many',
            },
            'history_current': {
                'archive': 'history_id',
                'link': 'from_one',
                'resource': 'historical_maturities',
                'writable': 'update_only',
            },
            'history': {
                'archive': 'omit',
                'link': 'from_many',
                'resource': 'historical_maturities',
            },
        }


class SectionSerializer(HistoricalModelSerializer):
    """Specification Section Serializer."""

    class Meta:
        model = Section
        fields = (
            'id', 'number', 'name', 'subpath', 'note', 'specification',
            'features', 'history_current', 'history')
        extra_kwargs = {
            'features': {
                'default': []
            }
        }
        fields_extra = {
            'id': {
                'link': 'self',
                'resource': 'sections',
            },
            'specification': {
                'link': 'to_one',
                'resource': 'specifications',
            },
            'features': {
                'archive': 'omit',
                'link': 'from_many',
            },
            'history_current': {
                'archive': 'history_id',
                'link': 'from_one',
                'resource': 'historical_sections',
                'writable': 'update_only',
            },
            'history': {
                'archive': 'omit',
                'link': 'from_many',
                'resource': 'historical_sections',
            },
        }


class SpecificationSerializer(HistoricalModelSerializer):
    """Specification Serializer."""

    def update(self, instance, validated_data):
        sections = validated_data.pop('sections', None)
        instance = super(SpecificationSerializer, self).update(
            instance, validated_data)

        if sections:
            s_pks = [s.pk for s in sections]
            current_order = instance.get_section_order()
            if s_pks != current_order:
                instance.set_section_order(s_pks)

        return instance

    class Meta:
        model = Specification
        fields = (
            'id', 'slug', 'mdn_key', 'name', 'uri', 'maturity', 'sections',
            'history_current', 'history')
        extra_kwargs = {
            'sections': {
                'default': []
            }
        }
        fields_extra = {
            'id': {
                'link': 'self',
                'resource': 'specifications',
            },
            'maturity': {
                'link': 'to_one',
                'resource': 'maturities',
            },
            'sections': {
                'link': 'from_many',
            },
            'history_current': {
                'archive': 'history_id',
                'link': 'from_one',
                'resource': 'historical_specifications',
                'writable': 'update_only',
            },
            'history': {
                'archive': 'omit',
                'link': 'from_many',
                'resource': 'historical_specifications',
            },
        }


class SupportSerializer(HistoricalModelSerializer):
    """Support Serializer."""

    class Meta:
        model = Support
        fields = (
            'id', 'support', 'prefix',
            'prefix_mandatory', 'alternate_name', 'alternate_mandatory',
            'requires_config', 'default_config', 'protected', 'note',
            'version', 'feature', 'history_current', 'history')
        fields_extra = {
            'id': {
                'link': 'self',
                'resource': 'supports',
            },
            'version': {
                'link': 'to_one',
                'resource': 'versions',
            },
            'feature': {
                'link': 'to_one',
                'resource': 'features',
            },
            'history_current': {
                'archive': 'history_id',
                'link': 'from_one',
                'resource': 'historical_supports',
                'writable': 'update_only',
            },
            'history': {
                'archive': 'omit',
                'link': 'from_many',
                'resource': 'historical_supports',
            },
        }


class VersionSerializer(HistoricalModelSerializer):
    """Browser Version Serializer."""

    order = IntegerField(read_only=True, source='_order')

    class Meta:
        model = Version
        fields = (
            'id', 'version', 'release_day', 'retirement_day',
            'status', 'release_notes_uri', 'note', 'order', 'browser',
            'supports', 'history_current', 'history')
        extra_kwargs = {
            'version': {
                'allow_blank': False
            }
        }
        read_only_fields = ('supports',)
        validators = [VersionAndStatusValidator()]
        fields_extra = {
            'id': {
                'link': 'self',
                'resource': 'versions',
            },
            'version': {
                'writable': 'create_only',
            },
            'browser': {
                'link': 'to_one',
                'resource': 'browsers',
            },
            'supports': {
                'archive': 'omit',
                'link': 'from_many',
            },
            'history_current': {
                'archive': 'history_id',
                'link': 'from_one',
                'resource': 'historical_versions',
                'writable': 'update_only',
            },
            'history': {
                'archive': 'omit',
                'link': 'from_many',
                'resource': 'historical_versions',
            },
        }


#
# Change control object serializers
#

class ChangesetSerializer(FieldsExtraMixin, ModelSerializer):
    """Changeset Serializer."""

    target_resource_type = OptionalCharField(required=False)
    target_resource_id = OptionalIntegerField(required=False)

    class Meta:
        model = Changeset
        fields = (
            'id', 'created', 'modified', 'closed', 'target_resource_type',
            'target_resource_id', 'user',
            'historical_browsers', 'historical_features',
            'historical_maturities', 'historical_sections',
            'historical_specifications', 'historical_supports',
            'historical_versions')
        read_only_fields = (
            'id', 'created', 'modified',
            'historical_browsers', 'historical_features',
            'historical_maturities', 'historical_sections',
            'historical_specifications', 'historical_supports',
            'historical_versions')
        extra_kwargs = {
            'user': {
                'default': CurrentUserDefault()
            }
        }
        fields_extra = {
            'id': {
                'link': 'self',
                'resource': 'changesets',
            },
            'user': {
                'link': 'to_one',
                'resource': 'users',
                'writable': 'update_only',
            },
            'target_resource_type': {
                'writable': 'update_only',
            },
            'target_resource_id': {
                'writable': 'update_only',
            },
            'historical_browsers': {
                'link': 'from_many',
            },
            'historical_features': {
                'link': 'from_many',
            },
            'historical_maturities': {
                'link': 'from_many',
            },
            'historical_specifications': {
                'link': 'from_many',
            },
            'historical_sections': {
                'link': 'from_many',
            },
            'historical_supports': {
                'link': 'from_many',
            },
            'historical_versions': {
                'link': 'from_many',
            },
        }


class UserSerializer(FieldsExtraMixin, ModelSerializer):
    """User Serializer."""

    created = DateTimeField(source='date_joined', read_only=True)
    agreement = SerializerMethodField()
    permissions = SerializerMethodField()

    def get_agreement(self, obj):
        """Return the version of the contribution terms the user agreed to.

        Placeholder for when we have a license agreement.
        """
        return 0

    def get_permissions(self, obj):
        """Return names of django.contrib.auth Groups."""
        try:
            # Cached objects (or those that have run through
            #  cache.user_v1_serializer) have this property
            return obj.group_names
        except AttributeError:
            return sorted(obj.groups.values_list('name', flat=True))

    class Meta:
        model = User
        fields = (
            'id', 'username', 'created', 'agreement', 'permissions',
            'changesets')
        read_only_fields = ('username', 'changesets')
        fields_extra = {
            'id': {
                'link': 'self',
                'resource': 'users',
            },
            'changesets': {
                'link': 'from_many',
            },
        }


#
# Historical object serializers
#


class ArchiveMixin(object):
    def get_fields(self):
        """Modify fields when loading or preparing an archive."""
        fields = super(ArchiveMixin, self).get_fields()
        fields_extra = getattr(self.Meta, 'fields_extra', {})
        to_delete = []
        append_id = []
        for field_name, field in fields.items():
            field_extra = fields_extra.get(field_name, {})
            archive = field_extra.get('archive')
            link = field_extra.get('link')
            if archive == 'omit':
                # Does not appear in archived representation
                to_delete.append(field_name)
            elif link in ('from_one', 'from_many'):
                # Defer loading until HistoricalObjectSerializer.get_archive
                to_delete.append(field_name)
            elif link == 'to_one':
                # Use the name_id field
                append_id.append(field_name)
        for field_name in to_delete:
            del fields[field_name]
        for field_name in append_id:
            fields[field_name].source = field_name + '_id'
        return fields


class HistoricalObjectSerializer(ModelSerializer):
    """Common serializer attributes for Historical models."""

    id = IntegerField(source='history_id')
    date = DateTimeField(source='history_date')
    event = SerializerMethodField()
    changeset = PrimaryKeyRelatedField(
        source='history_changeset', read_only=True)
    object_id = HistoricalObjectField()
    archived_representation = SerializerMethodField()

    EVENT_CHOICES = {
        '+': 'created',
        '~': 'changed',
        '-': 'deleted',
    }

{'fraction_non_alphanumeric': 0.11259551041716102, 'fraction_numerical': 0.0004033980352142755, 'mean_word_length': 1.7099864960452704, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5374366', 'n_tokens_mistral': 11169, 'n_tokens_neox': 10544, 'n_words': 2425}