Esempio n. 1
0
class BaseRatingSerializer(serializers.ModelSerializer):
    addon = serializers.SerializerMethodField()
    body = serializers.CharField(allow_null=True, required=False)
    is_latest = serializers.BooleanField(read_only=True)
    previous_count = serializers.IntegerField(read_only=True)
    user = BaseUserSerializer(read_only=True)

    class Meta:
        model = Rating
        fields = ('id', 'addon', 'body', 'created', 'is_latest',
                  'previous_count', 'user')

    def __init__(self, *args, **kwargs):
        super(BaseRatingSerializer, self).__init__(*args, **kwargs)
        self.request = kwargs.get('context', {}).get('request')

    def get_addon(self, obj):
        # We only return the addon id and slug for convenience, so just return
        # them directly to avoid instantiating a full serializer. Also avoid
        # database queries if possible by re-using the addon object from the
        # view if there is one.
        addon = self.context['view'].get_addon_object() or obj.addon

        return {'id': addon.id, 'slug': addon.slug}

    def validate(self, data):
        data = super(BaseRatingSerializer, self).validate(data)
        request = self.context['request']

        data['user_responsible'] = request.user

        # There are a few fields that need to be set at creation time and never
        # modified afterwards:
        if not self.partial:
            # Because we want to avoid extra queries, addon is a
            # SerializerMethodField, which means it needs to be validated
            # manually. Fortunately the view does most of the work for us.
            data['addon'] = self.context['view'].get_addon_object()
            if data['addon'] is None:
                raise serializers.ValidationError(
                    {'addon': ugettext('This field is required.')})

            # Get the user from the request, don't allow clients to pick one
            # themselves.
            data['user'] = request.user

            # Also include the user ip adress.
            data['ip_address'] = request.META.get('REMOTE_ADDR', '')
        else:
            # When editing, you can't change the add-on.
            if self.context['request'].data.get('addon'):
                raise serializers.ValidationError({
                    'addon':
                    ugettext(u'You can\'t change the add-on of a review once'
                             u' it has been created.')
                })

        # Clean up body and automatically flag the review if an URL was in it.
        body = data.get('body', '')
        if body:
            if '<br>' in body:
                data['body'] = re.sub('<br>', '\n', body)
            # Unquote the body when searching for links, in case someone tries
            # 'example%2ecom'.
            if RatingForm.link_pattern.search(unquote(body)) is not None:
                data['flag'] = True
                data['editorreview'] = True

        return data

    def to_representation(self, instance):
        out = super(BaseRatingSerializer, self).to_representation(instance)
        if self.request and is_gate_active(self.request, 'ratings-title-shim'):
            out['title'] = None
        return out
Esempio n. 2
0
class BaseRatingSerializer(serializers.ModelSerializer):
    addon = RatingAddonSerializer(read_only=True)
    body = serializers.CharField(allow_null=True, required=False)
    is_deleted = serializers.BooleanField(read_only=True, source='deleted')
    is_developer_reply = serializers.SerializerMethodField()
    is_latest = serializers.BooleanField(read_only=True)
    previous_count = serializers.IntegerField(read_only=True)
    user = BaseUserSerializer(read_only=True)
    flags = serializers.SerializerMethodField()

    class Meta:
        model = Rating
        fields = ('id', 'addon', 'body', 'created', 'flags', 'is_deleted',
                  'is_developer_reply', 'is_latest', 'previous_count', 'user')

    def __init__(self, *args, **kwargs):
        super(BaseRatingSerializer, self).__init__(*args, **kwargs)
        self.request = kwargs.get('context', {}).get('request')

    def get_is_developer_reply(self, obj):
        return obj.reply_to_id is not None

    def validate(self, data):
        data = super(BaseRatingSerializer, self).validate(data)
        request = self.context['request']

        data['user_responsible'] = request.user

        # There are a few fields that need to be set at creation time and never
        # modified afterwards:
        if not self.partial:
            # Because we want to avoid extra queries, addon is a
            # SerializerMethodField, which means it needs to be validated
            # manually. Fortunately the view does most of the work for us.
            data['addon'] = self.context['view'].get_addon_object()
            if data['addon'] is None:
                raise serializers.ValidationError(
                    {'addon': ugettext('This field is required.')})

            # Get the user from the request, don't allow clients to pick one
            # themselves.
            data['user'] = request.user

            # Also include the user ip adress.
            data['ip_address'] = request.META.get('REMOTE_ADDR', '')
        else:
            # When editing, you can't change the add-on.
            if self.context['request'].data.get('addon'):
                raise serializers.ValidationError({
                    'addon':
                    ugettext(u'You can\'t change the add-on of a review once'
                             u' it has been created.')
                })

        # Clean up body and automatically flag the review if an URL was in it.
        body = data.get('body', '')
        if body:
            if '<br>' in body:
                data['body'] = re.sub('<br>', '\n', body)
            # Unquote the body when searching for links, in case someone tries
            # 'example%2ecom'.
            if RatingForm.link_pattern.search(unquote(body)) is not None:
                data['flag'] = True
                data['editorreview'] = True

        return data

    def get_flags(self, obj):
        if self.context['view'].should_include_flags():
            # should be maximum one RatingFlag per rating+user anyway.
            rating_flags = obj.ratingflag_set.filter(
                user=self.context['request'].user)
            return [{
                'flag': flag.flag,
                'note': flag.note or None
            } for flag in rating_flags]
        return None

    def to_representation(self, instance):
        out = super(BaseRatingSerializer, self).to_representation(instance)
        if self.request and is_gate_active(self.request, 'ratings-title-shim'):
            out['title'] = None
        if not self.context['view'].should_include_flags():
            out.pop('flags', None)
        return out
class ESAddonSerializer(BaseESSerializer, AddonSerializer):
    # Override various fields for related objects which we don't want to expose
    # data the same way than the regular serializer does (usually because we
    # some of the data is not indexed in ES).
    authors = BaseUserSerializer(many=True, source='listed_authors')
    current_version = ESCurrentVersionSerializer()
    previews = ESPreviewSerializer(many=True, source='current_previews')
    _score = serializers.SerializerMethodField()

    datetime_fields = ('created', 'last_updated', 'modified')
    translated_fields = ('name', 'description', 'developer_comments',
                         'homepage', 'summary', 'support_email', 'support_url')

    class Meta:
        model = Addon
        fields = AddonSerializer.Meta.fields + ('_score', )

    def fake_preview_object(self, obj, data, model_class=Preview):
        # This is what ESPreviewSerializer.fake_object() would do, but we do
        # it here and make that fake_object() method a no-op in order to have
        # access to the right model_class to use - VersionPreview for static
        # themes, Preview for the rest.
        preview = model_class(id=data['id'], sizes=data.get('sizes', {}))
        preview.addon = obj
        preview.version = obj.current_version
        preview_serializer = self.fields['previews'].child
        # Attach base attributes that have the same name/format in ES and in
        # the model.
        preview_serializer._attach_fields(preview, data, ('modified', ))
        # Attach translations.
        preview_serializer._attach_translations(
            preview, data, preview_serializer.translated_fields)
        return preview

    def fake_file_object(self, obj, data):
        file_ = File(id=data['id'],
                     created=self.handle_date(data['created']),
                     hash=data['hash'],
                     filename=data['filename'],
                     is_webextension=data.get('is_webextension'),
                     is_mozilla_signed_extension=data.get(
                         'is_mozilla_signed_extension'),
                     is_restart_required=data.get('is_restart_required',
                                                  False),
                     platform=data['platform'],
                     size=data['size'],
                     status=data['status'],
                     strict_compatibility=data.get('strict_compatibility',
                                                   False),
                     version=obj)
        file_.webext_permissions_list = data.get('webext_permissions_list', [])
        return file_

    def fake_version_object(self, obj, data, channel):
        if data:
            version = Version(addon=obj,
                              id=data['id'],
                              reviewed=self.handle_date(data['reviewed']),
                              version=data['version'],
                              channel=channel)
            version.all_files = [
                self.fake_file_object(version, file_data)
                for file_data in data.get('files', [])
            ]

            # In ES we store integers for the appversion info, we need to
            # convert it back to strings.
            compatible_apps = {}
            for app_id, compat_dict in data.get('compatible_apps', {}).items():
                app_name = APPS_ALL[int(app_id)]
                compatible_apps[app_name] = ApplicationsVersions(
                    min=AppVersion(version=compat_dict.get('min_human', '')),
                    max=AppVersion(version=compat_dict.get('max_human', '')))
            version._compatible_apps = compatible_apps
            version_serializer = self.fields['current_version']
            version_serializer._attach_translations(
                version, data, version_serializer.translated_fields)
            if 'license' in data:
                license_serializer = version_serializer.fields['license']
                version.license = License(id=data['license']['id'])
                license_serializer._attach_fields(version.license,
                                                  data['license'],
                                                  ('builtin', 'url'))
                # Can't use license_serializer._attach_translations() directly
                # because 'name' is a SerializerMethodField, not an
                # ESTranslatedField.
                license_serializer.db_name.attach_translations(
                    version.license, data['license'], 'name')
            else:
                version.license = None
        else:
            version = None
        return version

    def fake_object(self, data):
        """Create a fake instance of Addon and related models from ES data."""
        obj = Addon(id=data['id'], slug=data['slug'])

        # Attach base attributes that have the same name/format in ES and in
        # the model.
        self._attach_fields(
            obj, data,
            ('average_daily_users', 'bayesian_rating', 'contributions',
             'created', 'default_locale', 'guid', 'has_eula',
             'has_privacy_policy', 'hotness', 'icon_hash', 'icon_type',
             'is_experimental', 'is_recommended', 'last_updated', 'modified',
             'public_stats', 'requires_payment', 'slug', 'status', 'type',
             'view_source', 'weekly_downloads'))

        # Attach attributes that do not have the same name/format in ES.
        obj.tag_list = data.get('tags', [])
        obj.all_categories = [
            CATEGORIES_BY_ID[cat_id] for cat_id in data.get('category', [])
        ]

        # Not entirely accurate, but enough in the context of the search API.
        obj.disabled_by_user = data.get('is_disabled', False)

        # Attach translations (they require special treatment).
        self._attach_translations(obj, data, self.translated_fields)

        # Attach related models (also faking them). `current_version` is a
        # property we can't write to, so we use the underlying field which
        # begins with an underscore.
        obj._current_version = self.fake_version_object(
            obj, data.get('current_version'), amo.RELEASE_CHANNEL_LISTED)

        data_authors = data.get('listed_authors', [])
        obj.listed_authors = [
            UserProfile(id=data_author['id'],
                        display_name=data_author['name'],
                        username=data_author['username'],
                        is_public=data_author.get('is_public', False))
            for data_author in data_authors
        ]

        is_static_theme = data.get('type') == amo.ADDON_STATICTHEME
        preview_model_class = VersionPreview if is_static_theme else Preview
        obj.current_previews = [
            self.fake_preview_object(obj,
                                     preview_data,
                                     model_class=preview_model_class)
            for preview_data in data.get('previews', [])
        ]

        ratings = data.get('ratings', {})
        obj.average_rating = ratings.get('average')
        obj.total_ratings = ratings.get('count')
        obj.text_ratings_count = ratings.get('text_count')

        obj._is_featured = data.get('is_featured', False)

        return obj

    def get__score(self, obj):
        # es_meta is added by BaseESSerializer.to_representation() before DRF's
        # to_representation() is called, so it's present on all objects.
        return obj._es_meta['score']

    def to_representation(self, obj):
        data = super(ESAddonSerializer, self).to_representation(obj)
        request = self.context.get('request')
        if request and '_score' in data and not is_gate_active(
                request, 'addons-search-_score-field'):
            data.pop('_score')
        return data
Esempio n. 4
0
class ESAddonSerializer(BaseESSerializer, AddonSerializer):
    # Override various fields for related objects which we don't want to expose
    # data the same way than the regular serializer does (usually because we
    # some of the data is not indexed in ES).
    authors = BaseUserSerializer(many=True, source='listed_authors')
    current_version = SimpleESVersionSerializer()
    previews = ESPreviewSerializer(many=True, source='current_previews')

    datetime_fields = ('created', 'last_updated', 'modified')
    translated_fields = ('name', 'description', 'developer_comments',
                         'homepage', 'summary', 'support_email', 'support_url')

    def fake_preview_object(self, obj, data, model_class=Preview):
        # This is what ESPreviewSerializer.fake_object() would do, but we do
        # it here and make that fake_object() method a no-op in order to have
        # access to the right model_class to use - VersionPreview for static
        # themes, Preview for the rest.
        preview = model_class(id=data['id'], sizes=data.get('sizes', {}))
        preview.addon = obj
        preview.version = obj.current_version
        preview_serializer = self.fields['previews'].child
        # Attach base attributes that have the same name/format in ES and in
        # the model.
        preview_serializer._attach_fields(preview, data, ('modified', ))
        # Attach translations.
        preview_serializer._attach_translations(
            preview, data, preview_serializer.translated_fields)
        return preview

    def fake_file_object(self, obj, data):
        file_ = File(id=data['id'],
                     created=self.handle_date(data['created']),
                     hash=data['hash'],
                     filename=data['filename'],
                     is_webextension=data.get('is_webextension'),
                     is_mozilla_signed_extension=data.get(
                         'is_mozilla_signed_extension'),
                     is_restart_required=data.get('is_restart_required',
                                                  False),
                     platform=data['platform'],
                     size=data['size'],
                     status=data['status'],
                     strict_compatibility=data.get('strict_compatibility',
                                                   False),
                     version=obj)
        file_.webext_permissions_list = data.get('webext_permissions_list', [])
        return file_

    def fake_version_object(self, obj, data, channel):
        if data:
            version = Version(addon=obj,
                              id=data['id'],
                              reviewed=self.handle_date(data['reviewed']),
                              version=data['version'],
                              channel=channel)
            version.all_files = [
                self.fake_file_object(version, file_data)
                for file_data in data.get('files', [])
            ]

            # In ES we store integers for the appversion info, we need to
            # convert it back to strings.
            compatible_apps = {}
            for app_id, compat_dict in data.get('compatible_apps', {}).items():
                app_name = APPS_ALL[int(app_id)]
                compatible_apps[app_name] = ApplicationsVersions(
                    min=AppVersion(version=compat_dict.get('min_human', '')),
                    max=AppVersion(version=compat_dict.get('max_human', '')))
            version._compatible_apps = compatible_apps
        else:
            version = None
        return version

    def fake_object(self, data):
        """Create a fake instance of Addon and related models from ES data."""
        obj = Addon(id=data['id'], slug=data['slug'])

        # Attach base attributes that have the same name/format in ES and in
        # the model.
        self._attach_fields(
            obj, data,
            ('average_daily_users', 'bayesian_rating', 'contributions',
             'created', 'default_locale', 'guid', 'has_eula',
             'has_privacy_policy', 'hotness', 'icon_hash', 'icon_type',
             'is_experimental', 'last_updated', 'modified', 'public_stats',
             'requires_payment', 'slug', 'status', 'type', 'view_source',
             'weekly_downloads'))

        # Attach attributes that do not have the same name/format in ES.
        obj.tag_list = data.get('tags', [])
        obj.all_categories = [
            CATEGORIES_BY_ID[cat_id] for cat_id in data.get('category', [])
        ]

        # Not entirely accurate, but enough in the context of the search API.
        obj.disabled_by_user = data.get('is_disabled', False)

        # Attach translations (they require special treatment).
        self._attach_translations(obj, data, self.translated_fields)

        # Attach related models (also faking them). `current_version` is a
        # property we can't write to, so we use the underlying field which
        # begins with an underscore. `latest_unlisted_version` is writeable
        # cached_property so we can directly write to them.
        obj._current_version = self.fake_version_object(
            obj, data.get('current_version'), amo.RELEASE_CHANNEL_LISTED)
        obj.latest_unlisted_version = self.fake_version_object(
            obj, data.get('latest_unlisted_version'),
            amo.RELEASE_CHANNEL_UNLISTED)

        data_authors = data.get('listed_authors', [])
        obj.listed_authors = [
            UserProfile(id=data_author['id'],
                        display_name=data_author['name'],
                        username=data_author['username'],
                        is_public=data_author.get('is_public', False))
            for data_author in data_authors
        ]

        is_static_theme = data.get('type') == amo.ADDON_STATICTHEME
        preview_model_class = VersionPreview if is_static_theme else Preview
        obj.current_previews = [
            self.fake_preview_object(obj,
                                     preview_data,
                                     model_class=preview_model_class)
            for preview_data in data.get('previews', [])
        ]

        ratings = data.get('ratings', {})
        obj.average_rating = ratings.get('average')
        obj.total_ratings = ratings.get('count')
        obj.text_ratings_count = ratings.get('text_count')

        obj._is_featured = data.get('is_featured', False)

        if data['type'] == amo.ADDON_PERSONA:
            persona_data = data.get('persona')
            if persona_data:
                obj.persona = Persona(
                    addon=obj,
                    accentcolor=persona_data['accentcolor'],
                    display_username=persona_data['author'],
                    header=persona_data['header'],
                    footer=persona_data['footer'],
                    # "New" Persona do not have a persona_id, it's a relic from
                    # old ones.
                    persona_id=0 if persona_data['is_new'] else 42,
                    textcolor=persona_data['textcolor'],
                    popularity=data.get('average_daily_users'),
                )
            else:
                # Sadly, https://code.djangoproject.com/ticket/14368 prevents
                # us from setting obj.persona = None. This is fixed in
                # Django 1.9, but in the meantime, work around it by creating
                # a Persona instance with a custom attribute indicating that
                # it should not be used.
                obj.persona = Persona()
                obj.persona._broken = True

        return obj
Esempio n. 5
0
class CollectionSerializer(serializers.ModelSerializer):
    name = TranslationSerializerField()
    description = TranslationSerializerField(allow_blank=True, required=False)
    url = serializers.SerializerMethodField()
    author = BaseUserSerializer(default=serializers.CurrentUserDefault())
    public = serializers.BooleanField(source='listed', default=True)
    uuid = serializers.UUIDField(format='hex', required=False)

    class Meta:
        model = Collection
        fields = (
            'id',
            'uuid',
            'url',
            'addon_count',
            'author',
            'description',
            'modified',
            'name',
            'slug',
            'public',
            'default_locale',
        )
        writeable_fields = ('description', 'name', 'slug', 'public',
                            'default_locale')
        read_only_fields = tuple(set(fields) - set(writeable_fields))
        validators = [
            UniqueTogetherValidator(
                queryset=Collection.objects.all(),
                message=_('This custom URL is already in use by another one '
                          'of your collections.'),
                fields=('slug', 'author'),
            ),
        ]

    def get_url(self, obj):
        return absolutify(obj.get_url_path())

    def validate_name(self, value):
        # if we have a localised dict of values validate them all.
        if isinstance(value, dict):
            return {
                locale: self.validate_name(sub_value)
                for locale, sub_value in value.items()
            }
        if DeniedName.blocked(value):
            raise serializers.ValidationError(
                gettext('This name cannot be used.'))
        return value

    def validate_description(self, value):
        if has_links(clean_nl(str(value))):
            # There's some links, we don't want them.
            raise serializers.ValidationError(gettext('No links are allowed.'))
        return value

    def validate_slug(self, value):
        slug_validator(
            value,
            message=gettext('The custom URL must consist of letters, '
                            'numbers, underscores or hyphens.'),
        )
        if DeniedName.blocked(value):
            raise serializers.ValidationError(
                gettext('This custom URL cannot be used.'))

        return value
Esempio n. 6
0
class DraftCommentSerializer(serializers.ModelSerializer):
    user = SplitField(
        serializers.PrimaryKeyRelatedField(queryset=UserProfile.objects.all()),
        BaseUserSerializer())
    version = SplitField(
        serializers.PrimaryKeyRelatedField(
            queryset=Version.unfiltered.all()),
        AddonBrowseVersionSerializer())
    canned_response = SplitField(
        serializers.PrimaryKeyRelatedField(
            queryset=CannedResponse.objects.all(),
            required=False),
        CannedResponseSerializer(),
        allow_null=True,
        required=False)

    class Meta:
        model = DraftComment
        fields = (
            'id', 'filename', 'lineno', 'comment',
            'version', 'user', 'canned_response'
        )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Set the instance for `AddonBrowseVersionSerializer` which requires
        # on `instance` being set correctly.
        self.fields['version'].output.instance = self.context['version']

    def get_or_default(self, key, data, default=''):
        """Return the value of ``key`` in ``data``

        If that key is not present then return the value of ``key`` from
        ``self.instance`, otherwise return the ``default``.

        This method is a helper to simplify validation for partial updates.
        """
        retval = data.get(key)

        if retval is None and self.instance is not None:
            retval = getattr(self.instance, key)

        return retval or default

    def validate(self, data):
        canned_response = self.get_or_default('canned_response', data)
        comment = self.get_or_default('comment', data)

        if comment and canned_response:
            raise serializers.ValidationError(
                {'comment': ugettext(
                    'You can\'t submit a comment if `canned_response` is '
                    'defined.')})

        if not canned_response and not comment:
            raise serializers.ValidationError(
                {'comment': ugettext(
                    'You can\'t submit an empty comment.')})

        lineno = self.get_or_default('lineno', data)
        filename = self.get_or_default('filename', data)

        if lineno and not filename:
            raise serializers.ValidationError(
                {'comment': ugettext(
                    'You can\'t submit a line number without associating '
                    'it to a filename.')})
        return data
class DraftCommentSerializer(serializers.ModelSerializer):
    user = SplitField(
        serializers.PrimaryKeyRelatedField(queryset=UserProfile.objects.all()),
        BaseUserSerializer(),
    )
    version_id = serializers.PrimaryKeyRelatedField(
        queryset=Version.unfiltered.all(), source='version')
    canned_response = SplitField(
        serializers.PrimaryKeyRelatedField(
            queryset=CannedResponse.objects.all(), required=False),
        CannedResponseSerializer(),
        allow_null=True,
        required=False,
    )

    class Meta:
        model = DraftComment
        fields = (
            'id',
            'filename',
            'lineno',
            'comment',
            'version_id',
            'user',
            'canned_response',
        )

    def get_or_default(self, key, data, default=''):
        """Return the value of ``key`` in ``data``

        If that key is not present then return the value of ``key`` from
        ``self.instance`, otherwise return the ``default``.

        This method is a helper to simplify validation for partial updates.
        """
        retval = data.get(key)

        if retval is None and self.instance is not None:
            retval = getattr(self.instance, key)

        return retval or default

    def validate(self, data):
        canned_response = self.get_or_default('canned_response', data)
        comment = self.get_or_default('comment', data)

        if comment and canned_response:
            raise serializers.ValidationError({
                'comment':
                ugettext("You can't submit a comment if `canned_response` is "
                         'defined.')
            })

        if not canned_response and not comment:
            raise serializers.ValidationError(
                {'comment': ugettext("You can't submit an empty comment.")})

        lineno = self.get_or_default('lineno', data)
        filename = self.get_or_default('filename', data)

        if lineno and not filename:
            raise serializers.ValidationError({
                'comment':
                ugettext("You can't submit a line number without associating "
                         'it to a filename.')
            })
        return data