Expose latest unlisted version in API for authors/unlisted reviewers

This commit is contained in:
Mathieu Pillard 2016-11-07 13:51:32 +01:00
Родитель 2248a2d5ab
Коммит fe0cd88b38
13 изменённых файлов: 334 добавлений и 36 удалений

Просмотреть файл

@ -125,6 +125,7 @@ This endpoint allows you to fetch a specific add-on by id, slug or guid.
:>json boolean is_source_public: Whether the add-on source is publicly viewable or not. :>json boolean is_source_public: Whether the add-on source is publicly viewable or not.
:>json string|object|null name: The add-on name (See :ref:`translated fields <api-overview-translations>`). :>json string|object|null name: The add-on name (See :ref:`translated fields <api-overview-translations>`).
:>json string last_updated: The date of the last time the add-on was updated by its developer(s). :>json string last_updated: The date of the last time the add-on was updated by its developer(s).
:>json object|null latest_unlisted_version: Object holding the latest unlisted :ref:`version <version-detail-object>` of the add-on. This field is only present if the user has unlisted reviewer permissions, or is listed as a developer of the add-on.
:>json array previews: Array holding information about the previews for the add-on. :>json array previews: Array holding information about the previews for the add-on.
:>json int previews[].id: The id for a preview. :>json int previews[].id: The id for a preview.
:>json string|object|null previews[].caption: The caption describing a preview (See :ref:`translated fields <api-overview-translations>`). :>json string|object|null previews[].caption: The caption describing a preview (See :ref:`translated fields <api-overview-translations>`).

Просмотреть файл

@ -78,6 +78,7 @@ class AddonIndexer(BaseSearchIndexer):
'is_experimental': {'type': 'boolean'}, 'is_experimental': {'type': 'boolean'},
'is_listed': {'type': 'boolean'}, 'is_listed': {'type': 'boolean'},
'last_updated': {'type': 'date'}, 'last_updated': {'type': 'date'},
'latest_unlisted_version': version_mapping,
'listed_authors': { 'listed_authors': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
@ -160,7 +161,7 @@ class AddonIndexer(BaseSearchIndexer):
} for file_ in version_obj.all_files], } for file_ in version_obj.all_files],
'reviewed': version_obj.reviewed, 'reviewed': version_obj.reviewed,
'version': version_obj.version, 'version': version_obj.version,
} } if version_obj else None
@classmethod @classmethod
def extract_compatibility_info(cls, version_obj): def extract_compatibility_info(cls, version_obj):
@ -234,11 +235,8 @@ class AddonIndexer(BaseSearchIndexer):
obj.current_version.supported_platforms] obj.current_version.supported_platforms]
else: else:
data['has_version'] = None data['has_version'] = None
if obj.current_beta_version: data['current_beta_version'] = cls.extract_version(
data['current_beta_version'] = cls.extract_version( obj, obj.current_beta_version)
obj, obj.current_beta_version)
else:
data['current_beta_version'] = None
data['listed_authors'] = [ data['listed_authors'] = [
{'name': a.name, 'id': a.id, 'username': a.username} {'name': a.name, 'id': a.id, 'username': a.username}
for a in obj.listed_authors for a in obj.listed_authors
@ -247,6 +245,9 @@ class AddonIndexer(BaseSearchIndexer):
data['has_eula'] = bool(obj.eula) data['has_eula'] = bool(obj.eula)
data['has_privacy_policy'] = bool(obj.privacy_policy) data['has_privacy_policy'] = bool(obj.privacy_policy)
data['latest_unlisted_version'] = cls.extract_version(
obj, obj.latest_unlisted_version)
# 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.
data['previews'] = [{'id': preview.id, 'modified': preview.modified} data['previews'] = [{'id': preview.id, 'modified': preview.modified}

Просмотреть файл

@ -954,6 +954,12 @@ class Addon(OnChangeMixin, ModelBase):
pass pass
return None return None
@amo.cached_property(writable=True)
def latest_unlisted_version(self):
"""Shortcut property for Addon.find_latest_version(
channel=RELEASE_CHANNEL_UNLISTED)."""
return self.find_latest_version(channel=amo.RELEASE_CHANNEL_UNLISTED)
@amo.cached_property @amo.cached_property
def binary(self): def binary(self):
"""Returns if the current version has binary files.""" """Returns if the current version has binary files."""

Просмотреть файл

@ -179,8 +179,8 @@ class AddonSerializer(serializers.ModelSerializer):
'is_experimental', 'is_experimental',
'is_listed', 'is_listed',
'is_source_public', 'is_source_public',
'name',
'last_updated', 'last_updated',
'name',
'previews', 'previews',
'public_stats', 'public_stats',
'ratings', 'ratings',
@ -275,9 +275,15 @@ class AddonSerializer(serializers.ModelSerializer):
return False return False
class ESAddonSerializer(BaseESSerializer, AddonSerializer): class AddonSerializerWithUnlistedData(AddonSerializer):
previews = ESPreviewSerializer(many=True, source='all_previews') latest_unlisted_version = SimpleVersionSerializer()
class Meta:
model = Addon
fields = AddonSerializer.Meta.fields + ('latest_unlisted_version',)
class ESBaseAddonSerializer(BaseESSerializer):
datetime_fields = ('created', 'last_updated', 'modified') datetime_fields = ('created', 'last_updated', 'modified')
translated_fields = ('name', 'description', 'homepage', 'summary', translated_fields = ('name', 'description', 'homepage', 'summary',
'support_email', 'support_url') 'support_email', 'support_url')
@ -351,12 +357,15 @@ class ESAddonSerializer(BaseESSerializer, AddonSerializer):
# Attach related models (also faking them). `current_version` is a # Attach related models (also faking them). `current_version` is a
# property we can't write to, so we use the underlying field which # property we can't write to, so we use the underlying field which
# begins with an underscore. `current_beta_version` is a # begins with an underscore. `current_beta_version` and
# cached_property so we can directly write to it. # `latest_unlisted_version` are writeable cached_property so we can
# directly write to them.
obj.current_beta_version = self.fake_version_object( obj.current_beta_version = self.fake_version_object(
obj, data.get('current_beta_version')) obj, data.get('current_beta_version'))
obj._current_version = self.fake_version_object( obj._current_version = self.fake_version_object(
obj, data.get('current_version')) obj, data.get('current_version'))
obj.latest_unlisted_version = self.fake_version_object(
obj, data.get('latest_unlisted_version'))
data_authors = data.get('listed_authors', []) data_authors = data.get('listed_authors', [])
obj.listed_authors = [ obj.listed_authors = [
@ -398,6 +407,15 @@ class ESAddonSerializer(BaseESSerializer, AddonSerializer):
return obj return obj
class ESAddonSerializer(ESBaseAddonSerializer, AddonSerializer):
previews = ESPreviewSerializer(many=True, source='all_previews')
class ESAddonSerializerWithUnlistedData(
ESBaseAddonSerializer, AddonSerializerWithUnlistedData):
previews = ESPreviewSerializer(many=True, source='all_previews')
class StaticCategorySerializer(serializers.Serializer): class StaticCategorySerializer(serializers.Serializer):
"""Serializes a `StaticCategory` as found in constants.categories""" """Serializes a `StaticCategory` as found in constants.categories"""
id = serializers.IntegerField() id = serializers.IntegerField()

Просмотреть файл

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from datetime import timedelta
from itertools import chain from itertools import chain
from olympia import amo from olympia import amo
@ -47,9 +48,9 @@ class TestAddonIndexer(TestCase):
complex_fields = [ complex_fields = [
'app', 'boost', 'category', 'current_beta_version', 'app', 'boost', 'category', 'current_beta_version',
'current_version', 'description', 'has_eula', 'has_privacy_policy', 'current_version', 'description', 'has_eula', 'has_privacy_policy',
'has_theme_rereview', 'has_version', 'listed_authors', 'name', 'has_theme_rereview', 'has_version', 'latest_unlisted_version',
'name_sort', 'platforms', 'previews', 'public_stats', 'ratings', 'listed_authors', 'name', 'name_sort', 'platforms', 'previews',
'summary', 'tags', 'public_stats', 'ratings', '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
@ -142,6 +143,7 @@ class TestAddonIndexer(TestCase):
assert extracted['current_beta_version'] is None assert extracted['current_beta_version'] is None
assert extracted['current_version'] assert extracted['current_version']
assert extracted['has_theme_rereview'] is None assert extracted['has_theme_rereview'] is None
assert extracted['latest_unlisted_version'] is None
assert extracted['listed_authors'] == [ assert extracted['listed_authors'] == [
{'name': u'55021 التطب', 'id': 55021, 'username': '55021'}] {'name': u'55021 التطب', 'id': 55021, 'username': '55021'}]
assert extracted['platforms'] == [PLATFORM_ALL.id] assert extracted['platforms'] == [PLATFORM_ALL.id]
@ -165,10 +167,16 @@ class TestAddonIndexer(TestCase):
assert extracted['has_privacy_policy'] is False assert extracted['has_privacy_policy'] is False
def test_extract_version_and_files(self): def test_extract_version_and_files(self):
current_beta_version = version_factory(
addon=self.addon, file_kw={'status': amo.STATUS_BETA})
version = self.addon.current_version version = self.addon.current_version
file_factory(version=version, platform=PLATFORM_MAC.id) file_factory(version=version, platform=PLATFORM_MAC.id)
current_beta_version = version_factory(
addon=self.addon, file_kw={'status': amo.STATUS_BETA})
unlisted_version = version_factory(
addon=self.addon, channel=amo.RELEASE_CHANNEL_UNLISTED)
# FIXME: remove this next line once current_version is modified to only
# return listed versions.
unlisted_version.update(
created=version.created - timedelta(days=42))
extracted = self._extract() extracted = self._extract()
assert extracted['current_version'] assert extracted['current_version']
@ -218,6 +226,29 @@ class TestAddonIndexer(TestCase):
assert extracted_file['size'] == file_.size assert extracted_file['size'] == file_.size
assert extracted_file['status'] == file_.status assert extracted_file['status'] == file_.status
version = unlisted_version
assert extracted['latest_unlisted_version']
assert extracted['latest_unlisted_version']['id'] == version.pk
assert extracted['latest_unlisted_version']['compatible_apps'] == {
FIREFOX.id: {
'min': 4009900200100L,
'max': 5009900200100L,
'max_human': '5.0.99',
'min_human': '4.0.99',
}
}
assert (
extracted['latest_unlisted_version']['version'] == version.version)
for idx, file_ in enumerate(version.all_files):
extracted_file = extracted['latest_unlisted_version']['files'][idx]
assert extracted_file['id'] == file_.pk
assert extracted_file['created'] == file_.created
assert extracted_file['filename'] == file_.filename
assert extracted_file['hash'] == file_.hash
assert extracted_file['platform'] == file_.platform
assert extracted_file['size'] == file_.size
assert extracted_file['status'] == file_.status
def test_extract_translations(self): def test_extract_translations(self):
translations_name = { translations_name = {
'en-US': u'Name in ënglish', 'en-US': u'Name in ënglish',

Просмотреть файл

@ -425,6 +425,32 @@ class TestAddonModels(TestCase):
a = Addon.objects.get(pk=3723) a = Addon.objects.get(pk=3723)
assert a.current_version is None assert a.current_version is None
def test_latest_unlisted_version(self):
addon = Addon.objects.get(pk=3615)
an_unlisted_version = version_factory(
addon=addon, version='3.0', channel=amo.RELEASE_CHANNEL_UNLISTED)
an_unlisted_version.update(created=self.days_ago(2))
a_newer_unlisted_version = version_factory(
addon=addon, version='4.0', channel=amo.RELEASE_CHANNEL_UNLISTED)
a_newer_unlisted_version.update(created=self.days_ago(1))
version_factory(
addon=addon, version='5.0', channel=amo.RELEASE_CHANNEL_UNLISTED,
file_kw={'status': amo.STATUS_DISABLED})
assert addon.latest_unlisted_version == a_newer_unlisted_version
# Make sure the property is cached.
an_even_newer_unlisted_version = version_factory(
addon=addon, version='6.0', channel=amo.RELEASE_CHANNEL_UNLISTED)
assert addon.latest_unlisted_version == a_newer_unlisted_version
# Make sure it can be deleted to reset it.
del addon.latest_unlisted_version
assert addon.latest_unlisted_version == an_even_newer_unlisted_version
# Make sure it's writeable.
addon.latest_unlisted_version = an_unlisted_version
assert addon.latest_unlisted_version == an_unlisted_version
def test_find_latest_version(self): def test_find_latest_version(self):
""" """
Tests that we get the latest version of an addon. Tests that we get the latest version of an addon.

Просмотреть файл

@ -12,7 +12,8 @@ from olympia.addons.indexers import AddonIndexer
from olympia.addons.models import ( from olympia.addons.models import (
Addon, AddonCategory, AddonUser, Category, Persona, Preview) Addon, AddonCategory, AddonUser, Category, Persona, Preview)
from olympia.addons.serializers import ( from olympia.addons.serializers import (
AddonSerializer, ESAddonSerializer, VersionSerializer) AddonSerializer, AddonSerializerWithUnlistedData, ESAddonSerializer,
ESAddonSerializerWithUnlistedData, VersionSerializer)
from olympia.addons.utils import generate_addon_guid from olympia.addons.utils import generate_addon_guid
from olympia.constants.categories import CATEGORIES from olympia.constants.categories import CATEGORIES
from olympia.versions.models import ApplicationsVersions, AppVersion, License from olympia.versions.models import ApplicationsVersions, AppVersion, License
@ -127,6 +128,9 @@ class AddonSerializerOutputTestMixin(object):
assert result['current_beta_version'] is None assert result['current_beta_version'] is None
# In this serializer latest_unlisted_version is omitted.
assert 'latest_unlisted_version' not in result
assert result['current_version'] assert result['current_version']
self._test_version( self._test_version(
self.addon.current_version, result['current_version']) self.addon.current_version, result['current_version'])
@ -154,9 +158,9 @@ class AddonSerializerOutputTestMixin(object):
assert result['is_experimental'] == self.addon.is_experimental is False assert result['is_experimental'] == self.addon.is_experimental is False
assert result['is_listed'] == self.addon.is_listed assert result['is_listed'] == self.addon.is_listed
assert result['is_source_public'] == self.addon.view_source assert result['is_source_public'] == self.addon.view_source
assert result['name'] == {'en-US': self.addon.name}
assert result['last_updated'] == ( assert result['last_updated'] == (
self.addon.last_updated.isoformat() + 'Z') self.addon.last_updated.isoformat() + 'Z')
assert result['name'] == {'en-US': self.addon.name}
assert result['previews'] assert result['previews']
assert len(result['previews']) == 2 assert len(result['previews']) == 2
@ -200,6 +204,34 @@ class AddonSerializerOutputTestMixin(object):
return result return result
def test_latest_unlisted_version(self):
self.addon = addon_factory()
version_factory(
addon=self.addon, channel=amo.RELEASE_CHANNEL_UNLISTED,
version='1.1')
assert self.addon.latest_unlisted_version
result = self.serialize()
# In this serializer latest_unlisted_version is omitted even if there
# is one, because it's limited to users with specific rights.
assert 'latest_unlisted_version' not in result
def test_latest_unlisted_version_with_rights(self):
self.serializer_class = self.serializer_class_with_unlisted_data
self.addon = addon_factory()
version_factory(
addon=self.addon, channel=amo.RELEASE_CHANNEL_UNLISTED,
version='1.1')
assert self.addon.latest_unlisted_version
result = self.serialize()
# In this serializer latest_unlisted_version is present.
assert result['latest_unlisted_version']
self._test_version(
self.addon.latest_unlisted_version,
result['latest_unlisted_version'])
def test_current_beta_version(self): def test_current_beta_version(self):
self.addon = addon_factory() self.addon = addon_factory()
@ -360,14 +392,21 @@ class AddonSerializerOutputTestMixin(object):
class TestAddonSerializerOutput(AddonSerializerOutputTestMixin, TestCase): class TestAddonSerializerOutput(AddonSerializerOutputTestMixin, TestCase):
serializer_class = AddonSerializer
serializer_class_with_unlisted_data = AddonSerializerWithUnlistedData
def serialize(self): def serialize(self):
self.serializer = self.serializer_class(
context={'request': self.request})
# Manually reload the add-on first to clear any cached properties. # Manually reload the add-on first to clear any cached properties.
self.addon = Addon.unfiltered.get(pk=self.addon.pk) self.addon = Addon.unfiltered.get(pk=self.addon.pk)
serializer = AddonSerializer(context={'request': self.request}) return self.serializer.to_representation(self.addon)
return serializer.to_representation(self.addon)
class TestESAddonSerializerOutput(AddonSerializerOutputTestMixin, ESTestCase): class TestESAddonSerializerOutput(AddonSerializerOutputTestMixin, ESTestCase):
serializer_class = ESAddonSerializer
serializer_class_with_unlisted_data = ESAddonSerializerWithUnlistedData
def tearDown(self): def tearDown(self):
super(TestESAddonSerializerOutput, self).tearDown() super(TestESAddonSerializerOutput, self).tearDown()
self.empty_index('default') self.empty_index('default')
@ -382,11 +421,13 @@ class TestESAddonSerializerOutput(AddonSerializerOutputTestMixin, ESTestCase):
return qs.filter('term', id=self.addon.pk).execute()[0] return qs.filter('term', id=self.addon.pk).execute()[0]
def serialize(self): def serialize(self):
self.serializer = self.serializer_class(
context={'request': self.request})
obj = self.search() obj = self.search()
with self.assertNumQueries(0): with self.assertNumQueries(0):
serializer = ESAddonSerializer(context={'request': self.request}) result = self.serializer.to_representation(obj)
result = serializer.to_representation(obj)
return result return result

Просмотреть файл

@ -1750,10 +1750,53 @@ class TestAddonViewSetDetail(AddonAndVersionViewSetDetailMixin, TestCase):
assert result['slug'] == 'my-addon' assert result['slug'] == 'my-addon'
assert result['last_updated'] == ( assert result['last_updated'] == (
self.addon.last_updated.isoformat() + 'Z') self.addon.last_updated.isoformat() + 'Z')
return result
def _set_tested_url(self, param): def _set_tested_url(self, param):
self.url = reverse('addon-detail', kwargs={'pk': param}) self.url = reverse('addon-detail', kwargs={'pk': param})
def test_hide_latest_unlisted_version_anonymous(self):
unlisted_version = version_factory(
addon=self.addon, channel=amo.RELEASE_CHANNEL_UNLISTED)
unlisted_version.update(created=self.days_ago(1))
result = self._test_url()
assert 'latest_unlisted_version' not in result
def test_hide_latest_unlisted_version_simple_reviewer(self):
user = UserProfile.objects.create(username='reviewer')
self.grant_permission(user, 'Addons:Review')
self.client.login_api(user)
unlisted_version = version_factory(
addon=self.addon, channel=amo.RELEASE_CHANNEL_UNLISTED)
unlisted_version.update(created=self.days_ago(1))
result = self._test_url()
assert 'latest_unlisted_version' not in result
def test_show_latest_unlisted_version_author(self):
user = UserProfile.objects.create(username='author')
AddonUser.objects.create(user=user, addon=self.addon)
self.client.login_api(user)
unlisted_version = version_factory(
addon=self.addon, channel=amo.RELEASE_CHANNEL_UNLISTED)
unlisted_version.update(created=self.days_ago(1))
result = self._test_url()
assert result['latest_unlisted_version']
assert result['latest_unlisted_version']['id'] == unlisted_version.pk
def test_show_latest_unlisted_version_unlisted_reviewer(self):
user = UserProfile.objects.create(username='author')
self.grant_permission(user, 'Addons:ReviewUnlisted')
self.client.login_api(user)
unlisted_version = version_factory(
addon=self.addon, channel=amo.RELEASE_CHANNEL_UNLISTED)
unlisted_version.update(created=self.days_ago(1))
result = self._test_url()
assert result['latest_unlisted_version']
assert result['latest_unlisted_version']['id'] == unlisted_version.pk
class TestVersionViewSetDetail(AddonAndVersionViewSetDetailMixin, TestCase): class TestVersionViewSetDetail(AddonAndVersionViewSetDetailMixin, TestCase):
client_class = APITestClient client_class = APITestClient
@ -2094,11 +2137,17 @@ class TestAddonSearchView(ESTestCase):
assert result['slug'] == 'my-addon' assert result['slug'] == 'my-addon'
assert result['last_updated'] == addon.last_updated.isoformat() + 'Z' assert result['last_updated'] == addon.last_updated.isoformat() + 'Z'
# latest_unlisted_version should never be exposed in public search.
assert 'latest_unlisted_version' not in result
result = data['results'][1] result = data['results'][1]
assert result['id'] == addon2.pk assert result['id'] == addon2.pk
assert result['name'] == {'en-US': u'My second Addôn'} assert result['name'] == {'en-US': u'My second Addôn'}
assert result['slug'] == 'my-second-addon' assert result['slug'] == 'my-second-addon'
# latest_unlisted_version should never be exposed in public search.
assert 'latest_unlisted_version' not in result
def test_empty(self): def test_empty(self):
data = self.perform_search(self.url) data = self.perform_search(self.url)
assert data['count'] == 0 assert data['count'] == 0

Просмотреть файл

@ -33,6 +33,7 @@ from rest_framework.viewsets import GenericViewSet
from session_csrf import anonymous_csrf_exempt from session_csrf import anonymous_csrf_exempt
from olympia import amo from olympia import amo
from olympia.access import acl
from olympia.amo import messages from olympia.amo import messages
from olympia.amo.decorators import post_required from olympia.amo.decorators import post_required
from olympia.amo.forms import AbuseForm from olympia.amo.forms import AbuseForm
@ -65,8 +66,8 @@ from .indexers import AddonIndexer
from .models import Addon, Persona, FrozenAddon from .models import Addon, Persona, FrozenAddon
from .serializers import ( from .serializers import (
AddonEulaPolicySerializer, AddonFeatureCompatibilitySerializer, AddonEulaPolicySerializer, AddonFeatureCompatibilitySerializer,
AddonSerializer, ESAddonSerializer, VersionSerializer, AddonSerializer, AddonSerializerWithUnlistedData, ESAddonSerializer,
StaticCategorySerializer) VersionSerializer, StaticCategorySerializer)
from .utils import get_creatured_ids, get_featured_ids from .utils import get_creatured_ids, get_featured_ids
@ -615,13 +616,14 @@ class AddonViewSet(RetrieveModelMixin, GenericViewSet):
AllowReviewer, AllowReviewerUnlisted), AllowReviewer, AllowReviewerUnlisted),
] ]
serializer_class = AddonSerializer serializer_class = AddonSerializer
serializer_class_with_unlisted_data = AddonSerializerWithUnlistedData
addon_id_pattern = re.compile( addon_id_pattern = re.compile(
# Match {uuid} or something@host.tld ("something" being optional) # Match {uuid} or something@host.tld ("something" being optional)
# guids. Copied from mozilla-central XPIProvider.jsm. # guids. Copied from mozilla-central XPIProvider.jsm.
r'^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}' r'^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}'
r'|[a-z0-9-\._]*\@[a-z0-9-\._]+)$', re.IGNORECASE) r'|[a-z0-9-\._]*\@[a-z0-9-\._]+)$', re.IGNORECASE)
# Permission classes disallow access to non-public/unlisted add-ons unless # Permission classes disallow access to non-public/unlisted add-ons unless
# logged in as a reviewer/addon owner/admin, so the unfiltered queryset # logged in as a reviewer/addon owner/admin, so the with_unlisted queryset
# is fine here. # is fine here.
queryset = Addon.with_unlisted.all() queryset = Addon.with_unlisted.all()
lookup_value_regex = '[^/]+' # Allow '.' for email-like guids. lookup_value_regex = '[^/]+' # Allow '.' for email-like guids.
@ -634,6 +636,17 @@ class AddonViewSet(RetrieveModelMixin, GenericViewSet):
return Addon.unfiltered.all() return Addon.unfiltered.all()
return super(AddonViewSet, self).get_queryset() return super(AddonViewSet, self).get_queryset()
def get_serializer_class(self):
# Override serializer to use serializer_class_with_unlisted_data if
# we are allowed to access unlisted data.
obj = getattr(self, 'instance')
request = self.request
if (acl.check_unlisted_addons_reviewer(request) or
(obj and request.user.is_authenticated() and
obj.authors.filter(pk=request.user.pk).exists())):
return self.serializer_class_with_unlisted_data
return self.serializer_class
def get_lookup_field(self, identifier): def get_lookup_field(self, identifier):
lookup_field = 'pk' lookup_field = 'pk'
if identifier and not identifier.isdigit(): if identifier and not identifier.isdigit():
@ -650,7 +663,8 @@ class AddonViewSet(RetrieveModelMixin, GenericViewSet):
identifier = self.kwargs.get('pk') identifier = self.kwargs.get('pk')
self.lookup_field = self.get_lookup_field(identifier) self.lookup_field = self.get_lookup_field(identifier)
self.kwargs[self.lookup_field] = identifier self.kwargs[self.lookup_field] = identifier
return super(AddonViewSet, self).get_object() self.instance = super(AddonViewSet, self).get_object()
return self.instance
@detail_route() @detail_route()
def feature_compatibility(self, request, pk=None): def feature_compatibility(self, request, pk=None):

Просмотреть файл

@ -672,6 +672,9 @@ def addon_factory(
addon = Addon.objects.create(type=type_, **kwargs) addon = Addon.objects.create(type=type_, **kwargs)
# Save 2. # Save 2.
if 'channel' not in version_kw and 'is_listed' in kw:
version_kw['channel'] = (amo.RELEASE_CHANNEL_LISTED if kw['is_listed']
else amo.RELEASE_CHANNEL_UNLISTED)
version = version_factory(file_kw, addon=addon, **version_kw) version = version_factory(file_kw, addon=addon, **version_kw)
addon.update_version() addon.update_version()
addon.status = status addon.status = status

Просмотреть файл

@ -5,8 +5,9 @@ from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import override_settings from django.test import override_settings
from olympia import amo
from olympia.amo.tests import version_factory
from olympia.accounts.tests.test_views import BaseAuthenticationView from olympia.accounts.tests.test_views import BaseAuthenticationView
from olympia.addons.tests.test_views import AddonAndVersionViewSetDetailMixin
from olympia.addons.utils import generate_addon_guid from olympia.addons.utils import generate_addon_guid
from olympia.amo.tests import ( from olympia.amo.tests import (
addon_factory, APITestClient, ESTestCase, TestCase) addon_factory, APITestClient, ESTestCase, TestCase)
@ -93,12 +94,16 @@ class TestInternalAddonSearchView(ESTestCase):
assert result['name'] == {'en-US': u'My Addôn'} assert result['name'] == {'en-US': u'My Addôn'}
assert result['slug'] == 'my-addon' assert result['slug'] == 'my-addon'
assert result['last_updated'] == addon.last_updated.isoformat() + 'Z' assert result['last_updated'] == addon.last_updated.isoformat() + 'Z'
assert result['latest_unlisted_version']
assert (result['latest_unlisted_version']['id'] ==
addon.latest_unlisted_version.pk)
result = data['results'][1] result = data['results'][1]
assert result['id'] == addon2.pk assert result['id'] == addon2.pk
assert result['name'] == {'en-US': u'My second Addôn'} assert result['name'] == {'en-US': u'My second Addôn'}
assert result['slug'] is None # Because it was deleted. assert result['slug'] is None # Because it was deleted.
assert result['status'] == 'deleted' assert result['status'] == 'deleted'
assert result['latest_unlisted_version'] is None
def test_empty(self): def test_empty(self):
data = self.perform_search_with_senior_editor(self.url) data = self.perform_search_with_senior_editor(self.url)
@ -168,14 +173,17 @@ class TestInternalAddonSearchView(ESTestCase):
assert result['name'] == {'en-US': u'By second Addôn'} assert result['name'] == {'en-US': u'By second Addôn'}
class TestAddonViewSetDetail(AddonAndVersionViewSetDetailMixin, TestCase): class TestInternalAddonViewSetDetail(TestCase):
client_class = APITestClient client_class = APITestClient
def setUp(self): def setUp(self):
super(TestAddonViewSetDetail, self).setUp() super(TestInternalAddonViewSetDetail, self).setUp()
self.addon = addon_factory( self.addon = addon_factory(
guid=generate_addon_guid(), name=u'My Addôn', slug='my-addon') guid=generate_addon_guid(), name=u'My Addôn', slug='my-addon')
self._set_tested_url(self.addon.pk) self._set_tested_url(self.addon.pk)
user = UserProfile.objects.create(username='reviewer-admin-tools')
self.grant_permission(user, 'ReviewerAdminTools:View')
self.client.login_api(user)
def _test_url(self): def _test_url(self):
response = self.client.get(self.url) response = self.client.get(self.url)
@ -186,10 +194,93 @@ class TestAddonViewSetDetail(AddonAndVersionViewSetDetailMixin, TestCase):
assert result['slug'] == 'my-addon' assert result['slug'] == 'my-addon'
assert result['last_updated'] == ( assert result['last_updated'] == (
self.addon.last_updated.isoformat() + 'Z') self.addon.last_updated.isoformat() + 'Z')
assert 'latest_unlisted_version' in result
return result
def _set_tested_url(self, param): def _set_tested_url(self, param):
self.url = reverse('internal-addon-detail', kwargs={'pk': param}) self.url = reverse('internal-addon-detail', kwargs={'pk': param})
def test_get_by_id(self):
self._test_url()
def test_get_by_slug(self):
self._set_tested_url(self.addon.slug)
self._test_url()
def test_get_by_guid(self):
self._set_tested_url(self.addon.guid)
self._test_url()
def test_get_by_guid_uppercase(self):
self._set_tested_url(self.addon.guid.upper())
self._test_url()
def test_get_by_guid_email_format(self):
self.addon.update(guid='my-addon@example.tld')
self._set_tested_url(self.addon.guid)
self._test_url()
def test_get_by_guid_email_short_format(self):
self.addon.update(guid='@example.tld')
self._set_tested_url(self.addon.guid)
self._test_url()
def test_get_by_guid_email_really_short_format(self):
self.addon.update(guid='@example')
self._set_tested_url(self.addon.guid)
self._test_url()
def test_get_anonymous(self):
self.client.logout_api()
response = self.client.get(self.url)
assert response.status_code == 401
def test_get_no_rights(self):
self.client.logout_api()
user = UserProfile.objects.create(username='simpleuser')
self.client.login_api(user)
response = self.client.get(self.url)
assert response.status_code == 403
def test_get_no_rights_even_if_reviewer(self):
self.client.logout_api()
user = UserProfile.objects.create(username='reviewer')
self.grant_permission(user, 'Addons:Review')
self.client.login_api(user)
response = self.client.get(self.url)
assert response.status_code == 403
def test_get_no_rights_even_if_author(self):
self.client.logout_api()
user = UserProfile.objects.create(username='author')
self.addon.addonuser_set.create(user=user, addon=self.addon)
self.client.login_api(user)
response = self.client.get(self.url)
assert response.status_code == 403
def test_get_not_listed(self):
self.addon.update(is_listed=False)
response = self.client.get(self.url)
assert response.status_code == 200
def test_get_deleted(self):
self.addon.delete()
response = self.client.get(self.url)
assert response.status_code == 200
def test_get_addon_not_found(self):
self._set_tested_url(self.addon.pk + 42)
response = self.client.get(self.url)
assert response.status_code == 404
def test_show_latest_unlisted_version_unlisted(self):
unlisted_version = version_factory(
addon=self.addon, channel=amo.RELEASE_CHANNEL_UNLISTED)
unlisted_version.update(created=self.days_ago(1))
result = self._test_url()
assert result['latest_unlisted_version']
assert result['latest_unlisted_version']['id'] == unlisted_version.pk
class TestLoginStartView(TestCase): class TestLoginStartView(TestCase):

Просмотреть файл

@ -2,12 +2,11 @@ from django.conf.urls import include, patterns, url
from rest_framework.routers import SimpleRouter from rest_framework.routers import SimpleRouter
from olympia.addons.views import AddonViewSet
from . import views from . import views
addons = SimpleRouter() addons = SimpleRouter()
addons.register(r'addon', AddonViewSet, base_name='internal-addon') addons.register(r'addon', views.InternalAddonViewSet,
base_name='internal-addon')
urlpatterns = patterns( urlpatterns = patterns(

Просмотреть файл

@ -3,7 +3,10 @@ import logging
from django.conf import settings from django.conf import settings
from olympia.accounts.views import LoginBaseView, LoginStartBaseView from olympia.accounts.views import LoginBaseView, LoginStartBaseView
from olympia.addons.views import AddonSearchView from olympia.addons.models import Addon
from olympia.addons.views import AddonViewSet, AddonSearchView
from olympia.addons.serializers import (
AddonSerializerWithUnlistedData, ESAddonSerializerWithUnlistedData)
from olympia.api.authentication import JSONWebTokenAuthentication from olympia.api.authentication import JSONWebTokenAuthentication
from olympia.api.permissions import AnyOf, GroupPermission from olympia.api.permissions import AnyOf, GroupPermission
from olympia.search.filters import ( from olympia.search.filters import (
@ -16,9 +19,10 @@ class InternalAddonSearchView(AddonSearchView):
# AddonSearchView disables auth classes so we need to add it back. # AddonSearchView disables auth classes so we need to add it back.
authentication_classes = [JSONWebTokenAuthentication] authentication_classes = [JSONWebTokenAuthentication]
# Similar to AddonSearchView but without the PublicContentFilter and with # Similar to AddonSearchView but without the ReviewedContentFilter (
# InternalSearchParameterFilter instead of SearchParameterFilter to allow # allowing unlisted, deleted, unreviewed addons to show up) and with
# searching by status. # InternalSearchParameterFilter instead of SearchParameterFilter (allowing
# to search by status).
filter_backends = [ filter_backends = [
SearchQueryFilter, InternalSearchParameterFilter, SortingFilter SearchQueryFilter, InternalSearchParameterFilter, SortingFilter
] ]
@ -26,6 +30,20 @@ class InternalAddonSearchView(AddonSearchView):
# Restricted to specific permissions. # Restricted to specific permissions.
permission_classes = [AnyOf(GroupPermission('AdminTools', 'View'), permission_classes = [AnyOf(GroupPermission('AdminTools', 'View'),
GroupPermission('ReviewerAdminTools', 'View'))] GroupPermission('ReviewerAdminTools', 'View'))]
# Can display unlisted data.
serializer_class = ESAddonSerializerWithUnlistedData
class InternalAddonViewSet(AddonViewSet):
# Restricted to specific permissions.
permission_classes = [AnyOf(GroupPermission('AdminTools', 'View'),
GroupPermission('ReviewerAdminTools', 'View'))]
# Internal tools allow access to everything, including deleted add-ons.
queryset = Addon.unfiltered.all()
# Can display unlisted data.
serializer_class = AddonSerializerWithUnlistedData
class LoginStartView(LoginStartBaseView): class LoginStartView(LoginStartBaseView):