Merge pull request #3128 from diox/api-expose-authors
Expose author information in add-on search and detail API
This commit is contained in:
Коммит
1c273a1dc5
|
@ -68,6 +68,10 @@ This endpoint allows you to fetch a specific add-on by id, slug or guid.
|
||||||
.. _addon-detail-object:
|
.. _addon-detail-object:
|
||||||
|
|
||||||
:>json int id: The add-on id on AMO.
|
:>json int id: The add-on id on AMO.
|
||||||
|
:>json array authors: Array holding information about the authors for this add-on.
|
||||||
|
:>json int authors[].id: The id for an author.
|
||||||
|
:>json string authors[].name: The name for an author.
|
||||||
|
:>json string authors[].url: The link to the profile page for an author.
|
||||||
:>json object compatibility: Object detailing the add-on :ref:`add-on application <addon-detail-application>` and version compatibility.
|
:>json object compatibility: Object detailing the add-on :ref:`add-on application <addon-detail-application>` and version compatibility.
|
||||||
:>json object compatibility[app_name].max: Maximum version of the corresponding app the add-on is compatible with.
|
:>json object compatibility[app_name].max: Maximum version of the corresponding app the add-on is compatible with.
|
||||||
:>json object compatibility[app_name].min: Minimum version of the corresponding app the add-on is compatible with.
|
:>json object compatibility[app_name].min: Minimum version of the corresponding app the add-on is compatible with.
|
||||||
|
|
|
@ -11,7 +11,7 @@ norecursedirs =
|
||||||
DJANGO_SETTINGS_MODULE = settings_test
|
DJANGO_SETTINGS_MODULE = settings_test
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
ignore = F999
|
ignore = F999,F405
|
||||||
exclude =
|
exclude =
|
||||||
services,
|
services,
|
||||||
src/olympia/wsgi.py,
|
src/olympia/wsgi.py,
|
||||||
|
|
|
@ -36,6 +36,11 @@ class AddonIndexer(BaseSearchIndexer):
|
||||||
'app': {'type': 'byte'},
|
'app': {'type': 'byte'},
|
||||||
'appversion': {'properties': {app.id: appver
|
'appversion': {'properties': {app.id: appver
|
||||||
for app in amo.APP_USAGE}},
|
for app in amo.APP_USAGE}},
|
||||||
|
# FIXME: See issue #3120, the 'authors' property is for
|
||||||
|
# backwards-compatibility and all code should be switched
|
||||||
|
# to use 'listed_authors.name' instead. We needed a reindex
|
||||||
|
# first though, which is why the 2 are present at the
|
||||||
|
# moment.
|
||||||
'authors': {'type': 'string'},
|
'authors': {'type': 'string'},
|
||||||
'average_daily_users': {'type': 'long'},
|
'average_daily_users': {'type': 'long'},
|
||||||
'bayesian_rating': {'type': 'double'},
|
'bayesian_rating': {'type': 'double'},
|
||||||
|
@ -74,6 +79,14 @@ class AddonIndexer(BaseSearchIndexer):
|
||||||
'is_disabled': {'type': 'boolean'},
|
'is_disabled': {'type': 'boolean'},
|
||||||
'is_listed': {'type': 'boolean'},
|
'is_listed': {'type': 'boolean'},
|
||||||
'last_updated': {'type': 'date'},
|
'last_updated': {'type': 'date'},
|
||||||
|
'listed_authors': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'id': {'type': 'long', 'index': 'no'},
|
||||||
|
'name': {'type': 'string'},
|
||||||
|
'username': {'type': 'string', 'index': 'no'},
|
||||||
|
},
|
||||||
|
},
|
||||||
'modified': {'type': 'date', 'index': 'no'},
|
'modified': {'type': 'date', 'index': 'no'},
|
||||||
# Adding word-delimiter to split on camelcase and
|
# Adding word-delimiter to split on camelcase and
|
||||||
# punctuation.
|
# punctuation.
|
||||||
|
@ -176,6 +189,11 @@ class AddonIndexer(BaseSearchIndexer):
|
||||||
'min': min_, 'min_human': min_human,
|
'min': min_, 'min_human': min_human,
|
||||||
'max': max_, 'max_human': max_human,
|
'max': max_, 'max_human': max_human,
|
||||||
}
|
}
|
||||||
|
# FIXME: See issue #3120, the 'authors' property is for
|
||||||
|
# backwards-compatibility and all code should be switched
|
||||||
|
# to use 'listed_authors.name' instead. We needed a reindex
|
||||||
|
# first though, which is why the 2 are present at the
|
||||||
|
# moment.
|
||||||
data['authors'] = [a.name for a in obj.listed_authors]
|
data['authors'] = [a.name for a in obj.listed_authors]
|
||||||
# Quadruple the boost if the add-on is public.
|
# Quadruple the boost if the add-on is public.
|
||||||
if obj.status == amo.STATUS_PUBLIC and 'boost' in data:
|
if obj.status == amo.STATUS_PUBLIC and 'boost' in data:
|
||||||
|
@ -203,6 +221,10 @@ class AddonIndexer(BaseSearchIndexer):
|
||||||
obj.current_version.supported_platforms]
|
obj.current_version.supported_platforms]
|
||||||
else:
|
else:
|
||||||
data['has_version'] = None
|
data['has_version'] = None
|
||||||
|
data['listed_authors'] = [
|
||||||
|
{'name': a.name, 'id': a.id, 'username': a.username}
|
||||||
|
for a in obj.listed_authors
|
||||||
|
]
|
||||||
|
|
||||||
# We can use all_previews because the indexing code goes through the
|
# We can use all_previews because the indexing code goes through the
|
||||||
# transformer that sets it.
|
# transformer that sets it.
|
||||||
|
|
|
@ -10,6 +10,7 @@ from olympia.api.serializers import BaseESSerializer
|
||||||
from olympia.applications.models import AppVersion
|
from olympia.applications.models import AppVersion
|
||||||
from olympia.constants.applications import APPS_ALL
|
from olympia.constants.applications import APPS_ALL
|
||||||
from olympia.files.models import File
|
from olympia.files.models import File
|
||||||
|
from olympia.users.models import UserProfile
|
||||||
from olympia.versions.models import ApplicationsVersions, Version
|
from olympia.versions.models import ApplicationsVersions, Version
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,6 +23,17 @@ class AddonFeatureCompatibilitySerializer(serializers.ModelSerializer):
|
||||||
fields = ('e10s', )
|
fields = ('e10s', )
|
||||||
|
|
||||||
|
|
||||||
|
class AddonAuthorSerializer(serializers.ModelSerializer):
|
||||||
|
url = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = UserProfile
|
||||||
|
fields = ('name', 'url')
|
||||||
|
|
||||||
|
def get_url(self, obj):
|
||||||
|
return absolutify(obj.get_url_path())
|
||||||
|
|
||||||
|
|
||||||
class FileSerializer(serializers.ModelSerializer):
|
class FileSerializer(serializers.ModelSerializer):
|
||||||
url = serializers.SerializerMethodField()
|
url = serializers.SerializerMethodField()
|
||||||
platform = ReverseChoiceField(choices=amo.PLATFORM_CHOICES_API.items())
|
platform = ReverseChoiceField(choices=amo.PLATFORM_CHOICES_API.items())
|
||||||
|
@ -99,6 +111,7 @@ class VersionSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class AddonSerializer(serializers.ModelSerializer):
|
class AddonSerializer(serializers.ModelSerializer):
|
||||||
|
authors = AddonAuthorSerializer(many=True, source='listed_authors')
|
||||||
current_version = VersionSerializer()
|
current_version = VersionSerializer()
|
||||||
description = TranslationSerializerField()
|
description = TranslationSerializerField()
|
||||||
edit_url = serializers.SerializerMethodField()
|
edit_url = serializers.SerializerMethodField()
|
||||||
|
@ -118,11 +131,12 @@ class AddonSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Addon
|
model = Addon
|
||||||
fields = ('id', 'current_version', 'default_locale', 'description',
|
fields = ('id', 'authors', 'current_version', 'default_locale',
|
||||||
'edit_url', 'guid', 'homepage', 'icon_url', 'is_listed',
|
'description', 'edit_url', 'guid', 'homepage', 'icon_url',
|
||||||
'name', 'last_updated', 'previews', 'public_stats',
|
'is_listed', 'name', 'last_updated', 'previews',
|
||||||
'review_url', 'slug', 'status', 'summary', 'support_email',
|
'public_stats', 'review_url', 'slug', 'status', 'summary',
|
||||||
'support_url', 'tags', 'theme_data', 'type', 'url')
|
'support_email', 'support_url', 'tags', 'theme_data', 'type',
|
||||||
|
'url')
|
||||||
|
|
||||||
def to_representation(self, obj):
|
def to_representation(self, obj):
|
||||||
data = super(AddonSerializer, self).to_representation(obj)
|
data = super(AddonSerializer, self).to_representation(obj)
|
||||||
|
@ -249,6 +263,14 @@ class ESAddonSerializer(BaseESSerializer, AddonSerializer):
|
||||||
|
|
||||||
obj._current_version.compatible_apps = compatible_apps
|
obj._current_version.compatible_apps = compatible_apps
|
||||||
|
|
||||||
|
data_authors = data.get('listed_authors', [])
|
||||||
|
obj.listed_authors = [
|
||||||
|
UserProfile(
|
||||||
|
id=data_author['id'], display_name=data_author['name'],
|
||||||
|
username=data_author['username'])
|
||||||
|
for data_author in data_authors
|
||||||
|
]
|
||||||
|
|
||||||
# We set obj.all_previews to the raw preview data because
|
# We set obj.all_previews to the raw preview data because
|
||||||
# ESPreviewSerializer will handle creating the fake Preview object
|
# ESPreviewSerializer will handle creating the fake Preview object
|
||||||
# for us when its to_representation() method is called.
|
# for us when its to_representation() method is called.
|
||||||
|
|
|
@ -46,8 +46,8 @@ class TestAddonIndexer(TestCase):
|
||||||
complex_fields = [
|
complex_fields = [
|
||||||
'app', 'appversion', 'authors', 'boost', 'category',
|
'app', 'appversion', 'authors', 'boost', 'category',
|
||||||
'current_version', 'description', 'has_theme_rereview',
|
'current_version', 'description', 'has_theme_rereview',
|
||||||
'has_version', 'name', 'name_sort', 'platforms', 'previews',
|
'has_version', 'listed_authors', 'name', 'name_sort', 'platforms',
|
||||||
'public_stats', 'summary', 'tags',
|
'previews', 'public_stats', 'summary', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Fields that need to be present in the mapping, but might be skipped
|
# Fields that need to be present in the mapping, but might be skipped
|
||||||
|
@ -146,6 +146,8 @@ class TestAddonIndexer(TestCase):
|
||||||
assert extracted['boost'] == self.addon.average_daily_users ** .2 * 4
|
assert extracted['boost'] == self.addon.average_daily_users ** .2 * 4
|
||||||
assert extracted['category'] == [22, 23, 24] # From fixture.
|
assert extracted['category'] == [22, 23, 24] # From fixture.
|
||||||
assert extracted['has_theme_rereview'] is None
|
assert extracted['has_theme_rereview'] is None
|
||||||
|
assert extracted['listed_authors'] == [
|
||||||
|
{'name': u'55021 التطب', 'id': 55021, 'username': '55021'}]
|
||||||
assert extracted['platforms'] == [PLATFORM_ALL.id]
|
assert extracted['platforms'] == [PLATFORM_ALL.id]
|
||||||
assert extracted['tags'] == []
|
assert extracted['tags'] == []
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,10 @@ from rest_framework.test import APIRequestFactory
|
||||||
|
|
||||||
from olympia import amo
|
from olympia import amo
|
||||||
from olympia.amo.helpers import absolutify
|
from olympia.amo.helpers import absolutify
|
||||||
from olympia.amo.tests import addon_factory, ESTestCase, TestCase
|
from olympia.amo.tests import addon_factory, ESTestCase, TestCase, user_factory
|
||||||
from olympia.amo.urlresolvers import reverse
|
from olympia.amo.urlresolvers import reverse
|
||||||
from olympia.addons.indexers import AddonIndexer
|
from olympia.addons.indexers import AddonIndexer
|
||||||
from olympia.addons.models import Addon, Persona, Preview
|
from olympia.addons.models import Addon, AddonUser, Persona, Preview
|
||||||
from olympia.addons.serializers import AddonSerializer, ESAddonSerializer
|
from olympia.addons.serializers import AddonSerializer, ESAddonSerializer
|
||||||
from olympia.addons.utils import generate_addon_guid
|
from olympia.addons.utils import generate_addon_guid
|
||||||
|
|
||||||
|
@ -39,6 +39,16 @@ class AddonSerializerOutputTestMixin(object):
|
||||||
support_url=u'https://support.example.org/support/my-addon/',
|
support_url=u'https://support.example.org/support/my-addon/',
|
||||||
tags=['some_tag', 'some_other_tag'],
|
tags=['some_tag', 'some_other_tag'],
|
||||||
)
|
)
|
||||||
|
AddonUser.objects.create(user=user_factory(username='hidden_author'),
|
||||||
|
addon=self.addon, listed=False)
|
||||||
|
second_author = user_factory(
|
||||||
|
username='second_author', display_name=u'Secönd Author')
|
||||||
|
first_author = user_factory(
|
||||||
|
username='first_author', display_name=u'First Authôr')
|
||||||
|
AddonUser.objects.create(
|
||||||
|
user=second_author, addon=self.addon, position=2)
|
||||||
|
AddonUser.objects.create(
|
||||||
|
user=first_author, addon=self.addon, position=1)
|
||||||
second_preview = Preview.objects.create(
|
second_preview = Preview.objects.create(
|
||||||
addon=self.addon, position=2,
|
addon=self.addon, position=2,
|
||||||
caption={'en-US': u'My câption', 'fr': u'Mön tîtré'})
|
caption={'en-US': u'My câption', 'fr': u'Mön tîtré'})
|
||||||
|
@ -76,6 +86,15 @@ class AddonSerializerOutputTestMixin(object):
|
||||||
assert result['current_version']['url'] == absolutify(
|
assert result['current_version']['url'] == absolutify(
|
||||||
version.get_url_path())
|
version.get_url_path())
|
||||||
|
|
||||||
|
assert result['authors']
|
||||||
|
assert len(result['authors']) == 2
|
||||||
|
assert result['authors'][0] == {
|
||||||
|
'name': first_author.name,
|
||||||
|
'url': absolutify(first_author.get_url_path())}
|
||||||
|
assert result['authors'][1] == {
|
||||||
|
'name': second_author.name,
|
||||||
|
'url': absolutify(second_author.get_url_path())}
|
||||||
|
|
||||||
assert result['edit_url'] == absolutify(self.addon.get_dev_url())
|
assert result['edit_url'] == absolutify(self.addon.get_dev_url())
|
||||||
assert result['default_locale'] == self.addon.default_locale
|
assert result['default_locale'] == self.addon.default_locale
|
||||||
assert result['description'] == {'en-US': self.addon.description}
|
assert result['description'] == {'en-US': self.addon.description}
|
||||||
|
|
Загрузка…
Ссылка в новой задаче