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
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
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
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
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