Пример #1
0
class FileStatusSerializer(serializers.ModelSerializer):
    status = ReverseChoiceField(choices_dict=mkt.STATUS_CHOICES_API,
                                required=True)

    class Meta:
        model = File
        fields = ('status', )
Пример #2
0
class ExtensionVersionSerializer(ModelSerializer):
    download_url = CharField(read_only=True)
    unsigned_download_url = CharField(read_only=True)
    reviewer_mini_manifest_url = CharField(read_only=True)
    status = ReverseChoiceField(choices_dict=STATUS_FILE_CHOICES_API_v2,
                                read_only=True)

    class Meta:
        model = ExtensionVersion
        fields = [
            'id', 'created', 'download_url', 'reviewer_mini_manifest_url',
            'unsigned_download_url', 'size', 'status', 'version'
        ]
Пример #3
0
class AppStatusSerializer(serializers.ModelSerializer):
    status = ReverseChoiceField(choices_dict=amo.STATUS_CHOICES_API,
                                required=False)
    disabled_by_user = serializers.BooleanField(required=False)

    allowed_statuses = {
        # You can push to the pending queue.
        amo.STATUS_NULL: [amo.STATUS_PENDING],
        # Approved apps can be public or unlisted.
        amo.STATUS_APPROVED: [amo.STATUS_PUBLIC, amo.STATUS_UNLISTED],
        # Public apps can choose to become private (APPROVED) or unlisted.
        amo.STATUS_PUBLIC: [amo.STATUS_APPROVED, amo.STATUS_UNLISTED],
        # Unlisted apps can become private or public.
        amo.STATUS_UNLISTED: [amo.STATUS_APPROVED, amo.STATUS_PUBLIC],
    }

    class Meta:
        model = Webapp
        fields = ('status', 'disabled_by_user')

    def validate_status(self, attrs, source):
        if not self.object:
            raise serializers.ValidationError(u'Error getting app.')

        if source not in attrs:
            return attrs

        # Admins can change any status, skip validation for them.
        # It's dangerous, but with great powers comes great responsability.
        if ('request' in self.context and self.context['request'].user
                and acl.action_allowed(self.context['request'], 'Admin', '%')):
            return attrs

        # An incomplete app's status can not be changed.
        if not self.object.is_fully_complete():
            raise serializers.ValidationError(
                self.object.completion_error_msgs())

        # Only some specific changes are possible depending on the app current
        # status.
        if (self.object.status not in self.allowed_statuses or attrs[source]
                not in self.allowed_statuses[self.object.status]):
            raise serializers.ValidationError(
                'App status can not be changed to the one you specified.')

        return attrs
Пример #4
0
class ExtensionSerializer(ModelSerializer):
    description = TranslationSerializerField(read_only=True)
    device_types = ListField(CharField(), source='device_names')
    icons = SerializerMethodField('get_icons')
    latest_public_version = ExtensionVersionSerializer(
        source='latest_public_version', read_only=True)
    latest_version = ExtensionVersionSerializer(source='latest_version',
                                                read_only=True)
    mini_manifest_url = CharField(source='mini_manifest_url', read_only=True)
    name = TranslationSerializerField(read_only=True)
    status = ReverseChoiceField(choices_dict=STATUS_CHOICES_API_v2,
                                read_only=True)

    # FIXME: latest_version potentially expose private data.
    # Nothing extremely major, but maybe we care. Not a fan of moving it to
    # another endpoint since that'd mean developers and reviewers would need
    # to use that other endpoint instead of the regular one, but maybe that's
    # the way to go ? That endpoint could include all versions info, too.

    def get_icons(self, obj):
        return {size: obj.get_icon_url(size) for size in (64, 128)}

    class Meta:
        model = Extension
        fields = [
            'id',
            'author',
            'description',
            'device_types',
            'disabled',
            'icons',
            'last_updated',
            'latest_version',
            'latest_public_version',
            'mini_manifest_url',
            'name',
            'slug',
            'status',
            'uuid',
        ]
Пример #5
0
class AppStatusSerializer(serializers.ModelSerializer):
    status = ReverseChoiceField(choices_dict=amo.STATUS_CHOICES_API,
                                required=False)
    disabled_by_user = serializers.BooleanField(required=False)

    allowed_statuses = {
        # You can push to the pending queue.
        amo.STATUS_NULL:
        amo.STATUS_PENDING,
        # You can push to public if you've been reviewed.
        amo.STATUS_PUBLIC_WAITING:
        amo.STATUS_PUBLIC,
    }

    class Meta:
        model = Webapp
        fields = ('status', 'disabled_by_user')

    def validate_status(self, attrs, source):
        if not self.object:
            raise serializers.ValidationError(u'Error getting app.')

        if not source in attrs:
            return attrs

        # An incomplete app's status can not be changed.
        if not self.object.is_fully_complete():
            raise serializers.ValidationError(
                self.object.completion_error_msgs())

        # Only some specific changes are possible depending on the app current
        # status.
        if (self.object.status not in self.allowed_statuses
                or attrs[source] != self.allowed_statuses[self.object.status]):
            raise serializers.ValidationError(
                'App status can not be changed to the one you specified.')

        return attrs
Пример #6
0
class AppSerializer(serializers.ModelSerializer):
    app_type = serializers.ChoiceField(
        choices=amo.ADDON_WEBAPP_TYPES_LOOKUP.items(), read_only=True)
    author = serializers.CharField(source='developer_name', read_only=True)
    banner_message = TranslationSerializerField(read_only=True,
        source='geodata.banner_message')
    banner_regions = serializers.Field(source='geodata.banner_regions_slugs')
    categories = serializers.SlugRelatedField(source='categories',
        many=True, slug_field='slug', required=True,
        queryset=Category.objects.filter(type=amo.ADDON_WEBAPP))
    content_ratings = serializers.SerializerMethodField('get_content_ratings')
    created = serializers.DateField(read_only=True)
    current_version = serializers.CharField(
        source='current_version.version',
        read_only=True)
    default_locale = serializers.CharField(read_only=True)
    device_types = SemiSerializerMethodField('get_device_types')
    description = TranslationSerializerField(required=False)
    homepage = TranslationSerializerField(required=False)
    icons = serializers.SerializerMethodField('get_icons')
    id = serializers.IntegerField(source='pk', required=False)
    is_packaged = serializers.BooleanField(read_only=True)
    manifest_url = serializers.CharField(source='get_manifest_url',
                                         read_only=True)
    name = TranslationSerializerField(required=False)
    payment_account = serializers.SerializerMethodField('get_payment_account')
    payment_required = serializers.SerializerMethodField(
        'get_payment_required')
    premium_type = ReverseChoiceField(
        choices_dict=amo.ADDON_PREMIUM_API, required=False)
    previews = PreviewSerializer(many=True, required=False,
                                 source='all_previews')
    price = SemiSerializerMethodField('get_price')
    price_locale = serializers.SerializerMethodField('get_price_locale')
    privacy_policy = LargeTextField(view_name='app-privacy-policy-detail',
                                    required=False)
    public_stats = serializers.BooleanField(read_only=True)
    ratings = serializers.SerializerMethodField('get_ratings_aggregates')
    regions = RegionSerializer(read_only=True, source='get_regions')
    release_notes = TranslationSerializerField(read_only=True,
        source='current_version.releasenotes')
    resource_uri = serializers.HyperlinkedIdentityField(view_name='app-detail')
    slug = serializers.CharField(source='app_slug', required=False)
    status = serializers.IntegerField(read_only=True)
    support_email = TranslationSerializerField(required=False)

    support_url = TranslationSerializerField(required=False)
    supported_locales = serializers.SerializerMethodField(
        'get_supported_locales')
    tags = serializers.SerializerMethodField('get_tags')
    upsell = serializers.SerializerMethodField('get_upsell')
    upsold = serializers.HyperlinkedRelatedField(
        view_name='app-detail', source='upsold.free',
        required=False, queryset=Webapp.objects.all())
    user = serializers.SerializerMethodField('get_user_info')
    versions = serializers.SerializerMethodField('get_versions')
    weekly_downloads = serializers.SerializerMethodField(
        'get_weekly_downloads')

    class Meta:
        model = Webapp
        fields = [
            'app_type', 'author', 'banner_message', 'banner_regions',
            'categories', 'content_ratings', 'created', 'current_version',
            'default_locale', 'description', 'device_types', 'homepage',
            'icons', 'id', 'is_packaged', 'manifest_url', 'name',
            'payment_account', 'payment_required', 'premium_type', 'previews',
            'price', 'price_locale', 'privacy_policy', 'public_stats',
            'release_notes', 'ratings', 'regions', 'resource_uri', 'slug',
            'status', 'support_email', 'support_url', 'supported_locales',
            'tags', 'upsell', 'upsold', 'user', 'versions', 'weekly_downloads']

    def _get_region_id(self):
        request = self.context.get('request')
        REGION = getattr(request, 'REGION', None)
        return REGION.id if REGION else None

    def _get_region_slug(self):
        request = self.context.get('request')
        REGION = getattr(request, 'REGION', None)
        return REGION.slug if REGION else None

    def get_content_ratings(self, app):
        body = mkt.regions.REGION_TO_RATINGS_BODY().get(
            self._get_region_slug(), 'generic')
        return {
            'body': body,
            'rating': app.get_content_ratings_by_body().get(body, None),
            'descriptors': app.get_descriptors_dehydrated().get(body, []),
            'interactives': app.get_interactives_dehydrated(),
        }

    def get_icons(self, app):
        return dict([(icon_size, app.get_icon_url(icon_size))
                     for icon_size in (16, 48, 64, 128)])

    def get_payment_account(self, app):

        # Avoid a query for payment_account if the app is not premium.
        if not app.is_premium():
            return None

        try:
            # This is a soon to be deprecated API property that only
            # returns the Bango account for historic compatibility.
            app_acct = app.payment_account(PROVIDER_BANGO)
            return reverse('payment-account-detail',
                           args=[app_acct.payment_account.pk])
        except app.PayAccountDoesNotExist:
            return None

    def get_payment_required(self, app):
        if app.has_premium():
            tier = app.get_tier()
            return bool(tier and tier.price)
        return False

    def get_price(self, app):
        if app.has_premium():
            region = self._get_region_id()
            if region in app.get_price_region_ids():
                return app.get_price(region=region)
        return None

    def get_price_locale(self, app):
        if app.has_premium():
            region = self._get_region_id()
            if region in app.get_price_region_ids():
                return app.get_price_locale(region=region)
        return None

    def get_ratings_aggregates(self, app):
        return {'average': app.average_rating,
                'count': app.total_reviews}

    def get_supported_locales(self, app):
        locs = getattr(app.current_version, 'supported_locales', '')
        if locs:
            return locs.split(',') if isinstance(locs, basestring) else locs
        else:
            return []

    def get_tags(self, app):
        return [t.tag_text for t in app.tags.all()]

    def get_upsell(self, app):
        upsell = False
        if app.upsell:
            upsell = app.upsell.premium
        # Only return the upsell app if it's public and we are not in an
        # excluded region.
        if (upsell and upsell.is_public() and self._get_region_id()
                not in upsell.get_excluded_region_ids()):
            return {
                'id': upsell.id,
                'app_slug': upsell.app_slug,
                'icon_url': upsell.get_icon_url(128),
                'name': unicode(upsell.name),
                'resource_uri': reverse('app-detail', kwargs={'pk': upsell.pk})
            }
        else:
            return False

    def get_user_info(self, app):
        user = getattr(self.context.get('request'), 'amo_user', None)
        if user:
            return {
                'developed': app.addonuser_set.filter(
                    user=user, role=amo.AUTHOR_ROLE_OWNER).exists(),
                'installed': app.has_installed(user),
                'purchased': app.pk in user.purchase_ids(),
            }

    def get_versions(self, app):
        # Disable transforms, we only need two fields: version and pk.
        # Unfortunately, cache-machine gets in the way so we can't use .only()
        # (.no_transforms() is ignored, defeating the purpose), and we can't use
        # .values() / .values_list() because those aren't cached :(
        return dict((v.version, reverse('version-detail', kwargs={'pk': v.pk}))
                    for v in app.versions.all().no_transforms())

    def get_weekly_downloads(self, app):
        if app.public_stats:
            return app.weekly_downloads

    def validate_categories(self, attrs, source):
        if not attrs.get('categories'):
            raise serializers.ValidationError('This field is required.')
        set_categories = set(attrs[source])
        total = len(set_categories)
        max_cat = amo.MAX_CATEGORIES

        if total > max_cat:
            # L10n: {0} is the number of categories.
            raise serializers.ValidationError(ngettext(
                'You can have only {0} category.',
                'You can have only {0} categories.',
                max_cat).format(max_cat))

        return attrs

    def get_device_types(self, app):
        with no_translation():
            return [n.api_name for n in app.device_types]

    def save_device_types(self, obj, new_types):
        new_types = [amo.DEVICE_LOOKUP[d].id for d in new_types]
        old_types = [x.id for x in obj.device_types]

        added_devices = set(new_types) - set(old_types)
        removed_devices = set(old_types) - set(new_types)

        for d in added_devices:
            obj.addondevicetype_set.create(device_type=d)
        for d in removed_devices:
            obj.addondevicetype_set.filter(device_type=d).delete()

        # Send app to re-review queue if public and new devices are added.
        if added_devices and obj.status in amo.WEBAPPS_APPROVED_STATUSES:
            mark_for_rereview(obj, added_devices, removed_devices)

    def save_categories(self, obj, categories):
        before = set(obj.categories.values_list('id', flat=True))
        # Add new categories.
        to_add = set(c.id for c in categories) - before
        for c in to_add:
            AddonCategory.objects.create(addon=obj, category_id=c)

        # Remove old categories.
        to_remove = before - set(categories)
        for c in to_remove:
            obj.addoncategory_set.filter(category=c).delete()

    def save_upsold(self, obj, upsold):
        current_upsell = obj.upsold
        if upsold and upsold != obj.upsold.free:
            if not current_upsell:
                log.debug('[1@%s] Creating app upsell' % obj.pk)
                current_upsell = AddonUpsell(premium=obj)
            current_upsell.free = upsold
            current_upsell.save()

        elif current_upsell:
            # We're deleting the upsell.
            log.debug('[1@%s] Deleting the app upsell' % obj.pk)
            current_upsell.delete()

    def save_price(self, obj, price):
        premium = obj.premium
        if not premium:
            premium = AddonPremium()
            premium.addon = obj
        premium.price = Price.objects.active().get(price=price)
        premium.save()

    def validate_device_types(self, attrs, source):
        if attrs.get('device_types') is None:
            raise serializers.ValidationError('This field is required.')
        for v in attrs['device_types']:
            if v not in amo.DEVICE_LOOKUP.keys():
                raise serializers.ValidationError(
                    str(v) + ' is not one of the available choices.')
        return attrs

    def validate_price(self, attrs, source):
        if attrs.get('premium_type', None) not in (amo.ADDON_FREE,
                                                   amo.ADDON_FREE_INAPP):
            valid_prices = Price.objects.exclude(
                price='0.00').values_list('price', flat=True)
            price = attrs.get('price')
            if not (price and Decimal(price) in valid_prices):
                raise serializers.ValidationError(
                    'Premium app specified without a valid price. Price can be'
                    ' one of %s.' % (', '.join('"%s"' % str(p)
                                               for p in valid_prices),))
        return attrs

    def restore_object(self, attrs, instance=None):
        # restore_object creates or updates a model instance, during
        # input validation.
        extras = []
        # Upsell bits are handled here because we need to remove it
        # from the attrs dict before deserializing.
        upsold = attrs.pop('upsold.free', None)
        if upsold is not None:
            extras.append((self.save_upsold, upsold))
        price = attrs.pop('price', None)
        if price is not None:
            extras.append((self.save_price, price))
        device_types = attrs['device_types']
        if device_types:
            extras.append((self.save_device_types, device_types))
            del attrs['device_types']
        instance = super(AppSerializer, self).restore_object(
            attrs, instance=instance)
        for f, v in extras:
            f(instance, v)
        return instance

    def save_object(self, obj, **kwargs):
        # this only gets called if validation succeeds.
        m2m = getattr(obj, '_m2m_data', {})
        cats = m2m.pop('categories', None)
        super(AppSerializer, self).save_object(obj, **kwargs)
        # Categories are handled here because we can't look up
        # existing ones until the initial save is done.
        self.save_categories(obj, cats)
Пример #7
0
class BaseAppSerializer(serializers.ModelSerializer):
    # REST Framework 3.x doesn't allow meta.fields to omit fields declared in
    # the class body, but it does allow omitting ones in superclasses. All the
    # serializers are subsets of the full field collection, hence this
    # superclass.
    app_type = serializers.ChoiceField(
        choices=mkt.ADDON_WEBAPP_TYPES_LOOKUP.items(), read_only=True)
    author = serializers.CharField(source='developer_name', read_only=True)
    categories = serializers.ListField(child=serializers.ChoiceField(
        choices=CATEGORY_CHOICES, read_only=False),
                                       read_only=False,
                                       required=True)
    content_ratings = serializers.SerializerMethodField()
    created = serializers.DateTimeField(read_only=True, format=None)
    current_version = serializers.CharField(source='current_version.version',
                                            read_only=True)
    default_locale = serializers.CharField(read_only=True)
    device_types = SemiSerializerMethodField()
    description = TranslationSerializerField(required=False)
    homepage = TranslationSerializerField(required=False)
    feature_compatibility = serializers.SerializerMethodField()
    file_size = serializers.IntegerField(read_only=True)
    icons = serializers.SerializerMethodField()
    id = serializers.IntegerField(source='pk', required=False)
    is_disabled = serializers.BooleanField(read_only=True)
    is_homescreen = serializers.SerializerMethodField()
    is_offline = serializers.BooleanField(read_only=True)
    is_packaged = serializers.BooleanField(read_only=True)
    last_updated = serializers.DateTimeField(read_only=True, format=None)
    manifest_url = serializers.CharField(source='get_manifest_url',
                                         read_only=True)
    modified = serializers.DateTimeField(read_only=True, format=None)
    name = TranslationSerializerField(required=False)
    package_path = serializers.CharField(source='get_package_path',
                                         read_only=True)
    payment_account = serializers.SerializerMethodField()
    payment_required = serializers.SerializerMethodField()
    premium_type = ReverseChoiceField(choices_dict=mkt.ADDON_PREMIUM_API,
                                      required=False)
    previews = PreviewSerializer(many=True,
                                 required=False,
                                 source='all_previews')
    price = SemiSerializerMethodField(source='*', required=False)
    price_locale = serializers.SerializerMethodField()
    privacy_policy = LargeTextField(view_name='app-privacy-policy-detail',
                                    queryset=Webapp.objects,
                                    required=False)
    promo_imgs = serializers.SerializerMethodField()
    public_stats = serializers.BooleanField(read_only=True)
    ratings = serializers.SerializerMethodField('get_ratings_aggregates')
    regions = RegionSerializer(read_only=True, source='get_regions', many=True)
    release_notes = TranslationSerializerField(
        read_only=True, source='current_version.releasenotes')
    resource_uri = serializers.HyperlinkedIdentityField(view_name='app-detail')
    slug = serializers.CharField(source='app_slug', required=False)
    status = serializers.IntegerField(read_only=True)
    support_email = TranslationSerializerField(required=False)
    support_url = TranslationSerializerField(required=False)
    supported_locales = serializers.SerializerMethodField()
    tags = serializers.SerializerMethodField()
    upsell = serializers.SerializerMethodField()
    upsold = serializers.HyperlinkedRelatedField(view_name='app-detail',
                                                 source='upsold.free',
                                                 required=False,
                                                 queryset=Webapp.objects.all())
    user = serializers.SerializerMethodField('get_user_info')
    versions = serializers.SerializerMethodField()
Пример #8
0
class AppSerializer(serializers.ModelSerializer):

    app_type = serializers.ChoiceField(
        choices=amo.ADDON_WEBAPP_TYPES_LOOKUP.items(), read_only=True)
    author = serializers.CharField(source='developer_name', read_only=True)
    categories = serializers.SlugRelatedField(
        many=True,
        slug_field='slug',
        required=True,
        queryset=Category.objects.filter(type=amo.ADDON_WEBAPP))
    content_ratings = serializers.SerializerMethodField('get_content_ratings')
    created = serializers.DateField(read_only=True)
    current_version = serializers.CharField(source='current_version.version',
                                            read_only=True)
    default_locale = serializers.CharField(read_only=True)
    device_types = SemiSerializerMethodField('get_device_types')
    description = TranslationSerializerField(required=False)
    homepage = TranslationSerializerField(required=False)
    icons = serializers.SerializerMethodField('get_icons')
    id = serializers.IntegerField(source='pk', required=False)
    is_packaged = serializers.BooleanField(read_only=True)
    manifest_url = serializers.CharField(source='get_manifest_url',
                                         read_only=True)
    name = TranslationSerializerField(required=False)
    payment_account = serializers.HyperlinkedRelatedField(
        view_name='payment-account-detail',
        source='app_payment_account',
        required=False)
    payment_required = serializers.SerializerMethodField(
        'get_payment_required')
    premium_type = ReverseChoiceField(choices_dict=amo.ADDON_PREMIUM_API,
                                      required=False)
    previews = PreviewSerializer(many=True, required=False)
    price = SemiSerializerMethodField('get_price')
    price_locale = serializers.SerializerMethodField('get_price_locale')
    privacy_policy = LargeTextField(view_name='app-privacy-policy-detail',
                                    required=False)
    public_stats = serializers.BooleanField(read_only=True)
    ratings = serializers.SerializerMethodField('get_ratings_aggregates')
    regions = RegionSerializer(read_only=True, source='get_regions')
    resource_uri = serializers.HyperlinkedIdentityField(view_name='app-detail')
    slug = serializers.CharField(source='app_slug', required=False)
    status = serializers.IntegerField(read_only=True)
    summary = TranslationSerializerField(required=False)
    support_email = TranslationSerializerField(required=False)

    support_url = TranslationSerializerField(required=False)
    supported_locales = serializers.SerializerMethodField(
        'get_supported_locales')
    tags = serializers.SerializerMethodField('get_tags')
    upsell = serializers.SerializerMethodField('get_upsell')
    upsold = serializers.HyperlinkedRelatedField(view_name='app-detail',
                                                 source='upsold.free',
                                                 required=False,
                                                 queryset=Webapp.objects.all())
    user = serializers.SerializerMethodField('get_user_info')
    versions = serializers.SerializerMethodField('get_versions')
    weekly_downloads = serializers.SerializerMethodField(
        'get_weekly_downloads')

    class Meta:
        model = Webapp
        exclude = [
            '_latest_version', 'ts_slowness', '_backup_version',
            'last_updated', 'nomination_message', '_current_version',
            'make_public', 'charity', 'modified'
        ]

    def get_content_ratings(self, app):
        return {
            'ratings': app.get_content_ratings_by_region() or None,
            'descriptors': app.get_descriptors() or None,
            'interactive_elements': app.get_interactives() or None,
        }

    def get_icons(self, app):
        return dict([(icon_size, app.get_icon_url(icon_size))
                     for icon_size in (16, 48, 64, 128)])

    def get_payment_required(self, app):
        if app.premium:
            tier = app.get_tier()
            return bool(tier and tier.price)
        return False

    def get_price(self, app):
        region = self.context.get('region')
        if (region in app.get_price_region_ids()
                or payments_enabled(self.context['request'])):
            return app.get_price(region=region)

    def get_price_locale(self, app):
        region = self.context.get('region')
        if (region in app.get_price_region_ids()
                or payments_enabled(self.context['request'])):
            return app.get_price_locale(region=region)

    def get_ratings_aggregates(self, app):
        return {'average': app.average_rating, 'count': app.total_reviews}

    def get_supported_locales(self, app):
        locs = getattr(app.current_version, 'supported_locales', '')
        if locs:
            return locs.split(',')
        else:
            return []

    def get_tags(self, app):
        return [t.tag_text for t in app.tags.all()]

    def get_upsell(self, app):
        if (app.upsell and self.context.get('region')
                in app.upsell.premium.get_price_region_ids()):
            upsell = app.upsell.premium
            return {
                'id': upsell.id,
                'app_slug': upsell.app_slug,
                'icon_url': upsell.get_icon_url(128),
                'name': unicode(upsell.name),
                'resource_uri': reverse('app-detail', kwargs={'pk': upsell.pk})
            }
        else:
            return False

    def get_user_info(self, app):
        user = self.context.get('profile')
        if user:
            return {
                'developed':
                app.addonuser_set.filter(user=user,
                                         role=amo.AUTHOR_ROLE_OWNER).exists(),
                'installed':
                app.has_installed(user),
                'purchased':
                app.pk in user.purchase_ids(),
            }

    def get_versions(self, app):
        return dict(
            (v.version, reverse_version(v)) for v in app.versions.all())

    def get_weekly_downloads(self, app):
        if app.public_stats:
            return app.weekly_downloads

    def validate_categories(self, attrs, source):
        if not attrs.get('categories'):
            raise serializers.ValidationError('This field is required.')
        set_categories = set(attrs[source])
        total = len(set_categories)
        max_cat = amo.MAX_CATEGORIES

        if total > max_cat:
            # L10n: {0} is the number of categories.
            raise serializers.ValidationError(
                ngettext('You can have only {0} category.',
                         'You can have only {0} categories.',
                         max_cat).format(max_cat))
        return attrs

    def get_device_types(self, app):
        with no_translation():
            return [n.api_name for n in app.device_types]

    def save_device_types(self, obj, new_types):
        new_types = [amo.DEVICE_LOOKUP[d].id for d in new_types]
        old_types = [x.id for x in obj.device_types]

        added_devices = set(new_types) - set(old_types)
        removed_devices = set(old_types) - set(new_types)

        for d in added_devices:
            obj.addondevicetype_set.create(device_type=d)
        for d in removed_devices:
            obj.addondevicetype_set.filter(device_type=d).delete()

        # Send app to re-review queue if public and new devices are added.
        if added_devices and obj.status in amo.WEBAPPS_APPROVED_STATUSES:
            mark_for_rereview(obj, added_devices, removed_devices)

    def save_categories(self, obj, categories):
        before = set(obj.categories.values_list('id', flat=True))
        # Add new categories.
        to_add = set(c.id for c in categories) - before
        for c in to_add:
            AddonCategory.objects.create(addon=obj, category_id=c)

        # Remove old categories.
        to_remove = before - set(categories)
        for c in to_remove:
            obj.addoncategory_set.filter(category=c).delete()

        # Disallow games in Brazil without a rating.
        games = Webapp.category('games')
        if not games:
            return

        for region in ALL_REGIONS_WITH_CONTENT_RATINGS:
            if (self.product.listed_in(region)
                    and not self.product.content_ratings_in(region)):

                if games.id in to_add:
                    aer, created = AddonExcludedRegion.objects.get_or_create(
                        addon=self.product, region=region.id)
                    if created:
                        log.info(u'[Webapp:%s] Game excluded from new region '
                                 u'(%s).' % (self.product, region.slug))

                elif games.id in to_remove:
                    self.product.addonexcludedregion.filter(
                        region=region.id).delete()
                    log.info(u'[Webapp:%s] Game no longer excluded from region'
                             u' (%s).' % (self.product, region.slug))

    def save_upsold(self, obj, upsold):
        current_upsell = obj.upsold
        if upsold and upsold != obj.upsold.free:
            if not current_upsell:
                log.debug('[1@%s] Creating app upsell' % obj.pk)
                current_upsell = AddonUpsell(premium=obj)
            current_upsell.free = upsold
            current_upsell.save()

        elif current_upsell:
            # We're deleting the upsell.
            log.debug('[1@%s] Deleting the app upsell' % obj.pk)
            current_upsell.delete()

    def save_price(self, obj, price):
        premium = obj.premium
        if not premium:
            premium = AddonPremium()
            premium.addon = obj
        premium.price = Price.objects.active().get(price=price)
        premium.save()

    def validate_device_types(self, attrs, source):
        if attrs.get('device_types') is None:
            raise serializers.ValidationError('This field is required.')
        for v in attrs['device_types']:
            if v not in amo.DEVICE_LOOKUP.keys():
                raise serializers.ValidationError(
                    str(v) + ' is not one of the available choices.')
        return attrs

    def validate_price(self, attrs, source):
        if attrs.get('premium_type',
                     None) not in (amo.ADDON_FREE, amo.ADDON_FREE_INAPP):
            valid_prices = Price.objects.exclude(price='0.00').values_list(
                'price', flat=True)
            price = attrs.get('price')
            if not (price and Decimal(price) in valid_prices):
                raise serializers.ValidationError(
                    'Premium app specified without a valid price. Price can be'
                    ' one of %s.' % (', '.join('"%s"' % str(p)
                                               for p in valid_prices), ))
        return attrs

    def restore_object(self, attrs, instance=None):
        # restore_object creates or updates a model instance, during
        # input validation.
        extras = []
        # Upsell bits are handled here because we need to remove it
        # from the attrs dict before deserializing.
        upsold = attrs.pop('upsold.free', None)
        if upsold is not None:
            extras.append((self.save_upsold, upsold))
        price = attrs.pop('price', None)
        if price is not None:
            extras.append((self.save_price, price))
        device_types = attrs['device_types']
        if device_types:
            extras.append((self.save_device_types, device_types))
            del attrs['device_types']
        if attrs.get('app_payment_account') is None:
            attrs.pop('app_payment_account')
        instance = super(AppSerializer, self).restore_object(attrs,
                                                             instance=instance)
        for f, v in extras:
            f(instance, v)
        return instance

    def save_object(self, obj, **kwargs):
        # this only gets called if validation succeeds.
        m2m = getattr(obj, '_m2m_data', {})
        cats = m2m.pop('categories', None)
        super(AppSerializer, self).save_object(obj, **kwargs)
        # Categories are handled here because we can't look up
        # existing ones until the initial save is done.
        self.save_categories(obj, cats)