eradicate tastypie (bug 910624)
This commit is contained in:
Родитель
055ccfea5d
Коммит
da66cbecaf
|
@ -28,7 +28,6 @@ from nose.exc import SkipTest
|
|||
from nose.tools import eq_, nottest, ok_
|
||||
from pyquery import PyQuery as pq
|
||||
from redisutils import mock_redis, reset_redis
|
||||
from tastypie.exceptions import ImmediateHttpResponse
|
||||
from test_utils import RequestFactory
|
||||
from waffle import cache_sample, cache_switch
|
||||
from waffle.models import Flag, Sample, Switch
|
||||
|
|
|
@ -678,7 +678,6 @@ def fudge_headers(response, stats):
|
|||
patch_cache_control(response, max_age=seven_days)
|
||||
|
||||
|
||||
# TODO: Move to utils and implement tastypie serializer.
|
||||
class UnicodeCSVDictWriter(csv.DictWriter):
|
||||
"""A DictWriter that writes a unicode stream."""
|
||||
|
||||
|
|
|
@ -122,8 +122,3 @@ def log_configure():
|
|||
logger['propagate'] = False
|
||||
|
||||
dictconfig.dictConfig(cfg)
|
||||
|
||||
# logging.getLogger() accesses a singleton, this just binds
|
||||
# in the SentryHandler to error level messages
|
||||
tastypie = logging.getLogger('django.request.tastypie')
|
||||
tastypie.addHandler(SentryHandler())
|
||||
|
|
|
@ -8,13 +8,12 @@ from nose.tools import eq_
|
|||
|
||||
from abuse.models import AbuseReport
|
||||
from mkt.api.tests.test_oauth import RestOAuth
|
||||
from mkt.api.tests.test_throttle import ThrottleTests
|
||||
from mkt.site.fixtures import fixture
|
||||
from mkt.webapps.models import Webapp
|
||||
from users.models import UserProfile
|
||||
|
||||
|
||||
class BaseTestAbuseResource(ThrottleTests):
|
||||
class BaseTestAbuseResource(object):
|
||||
"""
|
||||
Setup for AbuseResource tests that require inheritance from TestCase.
|
||||
"""
|
||||
|
|
|
@ -4,11 +4,10 @@ import json
|
|||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django import http
|
||||
|
||||
import commonware.log
|
||||
from rest_framework.authentication import BaseAuthentication
|
||||
from tastypie import http
|
||||
from tastypie.authentication import Authentication
|
||||
import waffle
|
||||
|
||||
from access.middleware import ACLMiddleware
|
||||
|
@ -26,13 +25,7 @@ class OAuthError(RuntimeError):
|
|||
self.message = message
|
||||
|
||||
|
||||
errors = {
|
||||
'headers': 'Error with OAuth headers',
|
||||
'roles': 'Cannot be a user with roles.',
|
||||
}
|
||||
|
||||
|
||||
class OAuthAuthentication(Authentication):
|
||||
class RestOAuthAuthentication(BaseAuthentication):
|
||||
"""
|
||||
This is based on https://github.com/amrox/django-tastypie-two-legged-oauth
|
||||
with permission.
|
||||
|
@ -41,10 +34,6 @@ class OAuthAuthentication(Authentication):
|
|||
def __init__(self, realm='API'):
|
||||
self.realm = realm
|
||||
|
||||
def _error(self, reason):
|
||||
return http.HttpUnauthorized(content=json.dumps({'reason':
|
||||
errors[reason]}))
|
||||
|
||||
def is_authenticated(self, request, **kwargs):
|
||||
if not settings.SITE_URL:
|
||||
raise ValueError('SITE_URL is not specified')
|
||||
|
@ -54,7 +43,7 @@ class OAuthAuthentication(Authentication):
|
|||
'oauth_token' not in request.META['QUERY_STRING']):
|
||||
self.user = AnonymousUser()
|
||||
log.error('No header')
|
||||
return self._error('headers')
|
||||
return None
|
||||
|
||||
auth_header = {'Authorization': auth_header_value}
|
||||
method = getattr(request, 'signed_method', request.method)
|
||||
|
@ -74,7 +63,7 @@ class OAuthAuthentication(Authentication):
|
|||
if not valid:
|
||||
log.error(u'Cannot find APIAccess token with that key: %s'
|
||||
% oauth.attempted_key)
|
||||
return self._error('headers')
|
||||
return None
|
||||
uid = Token.objects.filter(
|
||||
token_type=ACCESS_TOKEN,
|
||||
key=oauth_request.resource_owner_key).values_list(
|
||||
|
@ -96,7 +85,7 @@ class OAuthAuthentication(Authentication):
|
|||
if not valid:
|
||||
log.error(u'Cannot find APIAccess token with that key: %s'
|
||||
% oauth.attempted_key)
|
||||
return self._error('headers')
|
||||
return None
|
||||
uid = Access.objects.filter(
|
||||
key=oauth_request.client_key).values_list(
|
||||
'user_id', flat=True)[0]
|
||||
|
@ -124,32 +113,28 @@ class OAuthAuthentication(Authentication):
|
|||
if roles and roles.intersection(denied_groups):
|
||||
log.info(u'Attempt to use API with denied role, user: %s'
|
||||
% request.amo_user.pk)
|
||||
return self._error('roles')
|
||||
return None
|
||||
|
||||
log.info('Successful OAuth with user: %s' % request.user)
|
||||
return True
|
||||
|
||||
|
||||
class OptionalOAuthAuthentication(OAuthAuthentication):
|
||||
"""
|
||||
Like OAuthAuthentication, but doesn't require there to be
|
||||
authentication headers. If no headers are provided, just continue
|
||||
as an anonymous user.
|
||||
"""
|
||||
|
||||
def is_authenticated(self, request, **kw):
|
||||
auth_header_value = request.META.get('HTTP_AUTHORIZATION', None)
|
||||
if (not auth_header_value and
|
||||
'oauth_token' not in request.META['QUERY_STRING']):
|
||||
request.user = AnonymousUser()
|
||||
log.info('Successful OptionalOAuth as anonymous user')
|
||||
return True
|
||||
|
||||
return (super(OptionalOAuthAuthentication, self)
|
||||
.is_authenticated(request, **kw))
|
||||
def authenticate(self, request):
|
||||
# The DRF Request object wraps the actual WSGIRequest. Its
|
||||
# 'method' attribute is a property. when using
|
||||
# X-HTTP-Method-Override, request.method will return the
|
||||
# overriding method instead of POST. OAuth signatures include
|
||||
# the actual HTTP method, so we pass the unwrapped WSGIRequest
|
||||
# for authentication.
|
||||
result = self.is_authenticated(request._request)
|
||||
user = getattr(request._request, 'user', None)
|
||||
if user:
|
||||
request._user = user
|
||||
if (not result or not request.user):
|
||||
return None
|
||||
return (request.user, None)
|
||||
|
||||
|
||||
class SharedSecretAuthentication(Authentication):
|
||||
class RestSharedSecretAuthentication(BaseAuthentication):
|
||||
|
||||
def is_authenticated(self, request, **kwargs):
|
||||
header = request.META.get('HTTP_AUTHORIZATION', '').split(None, 1)
|
||||
|
@ -195,31 +180,6 @@ class SharedSecretAuthentication(Authentication):
|
|||
log.info('Bad shared-secret auth data: %s (%s)', auth, e)
|
||||
return False
|
||||
|
||||
|
||||
class RestOAuthAuthentication(BaseAuthentication, OAuthAuthentication):
|
||||
"""OAuthAuthentication suitable for DRF, wraps around tastypie ones."""
|
||||
|
||||
def authenticate(self, request):
|
||||
# The DRF Request object wraps the actual WSGIRequest. Its
|
||||
# 'method' attribute is a property. when using
|
||||
# X-HTTP-Method-Override, request.method will return the
|
||||
# overriding method instead of POST. OAuth signatures include
|
||||
# the actual HTTP method, so we pass the unwrapped WSGIRequest
|
||||
# for authentication.
|
||||
result = self.is_authenticated(request._request)
|
||||
user = getattr(request._request, 'user', None)
|
||||
if user:
|
||||
request._user = user
|
||||
if (not result or isinstance(result, http.HttpUnauthorized)
|
||||
or not request.user):
|
||||
return None
|
||||
return (request.user, None)
|
||||
|
||||
|
||||
class RestSharedSecretAuthentication(BaseAuthentication,
|
||||
SharedSecretAuthentication):
|
||||
"""SharedSecretAuthentication suitable for DRF."""
|
||||
|
||||
def authenticate(self, request):
|
||||
result = self.is_authenticated(request._request)
|
||||
user = getattr(request._request, 'user', None)
|
||||
|
|
|
@ -3,7 +3,6 @@ from collections import defaultdict
|
|||
import commonware.log
|
||||
|
||||
from rest_framework.permissions import BasePermission, SAFE_METHODS
|
||||
from tastypie.authorization import Authorization, ReadOnlyAuthorization
|
||||
from waffle import flag_is_active, switch_is_active
|
||||
|
||||
from access import acl
|
||||
|
@ -11,92 +10,6 @@ from access import acl
|
|||
log = commonware.log.getLogger('z.api')
|
||||
|
||||
|
||||
class OwnerAuthorization(Authorization):
|
||||
|
||||
def is_authorized(self, request, object=None):
|
||||
# There is no object being passed, so we'll assume it's ok
|
||||
if not object:
|
||||
return True
|
||||
# There is no request user or no user on the object.
|
||||
if not request.amo_user:
|
||||
return False
|
||||
|
||||
return self.check_owner(request, object)
|
||||
|
||||
def check_owner(self, request, object):
|
||||
if not object.user:
|
||||
return False
|
||||
# If the user on the object and the amo_user match, we are golden.
|
||||
return object.user.pk == request.amo_user.pk
|
||||
|
||||
|
||||
class AppOwnerAuthorization(OwnerAuthorization):
|
||||
|
||||
def check_owner(self, request, object):
|
||||
# If the user on the object and the amo_user match, we are golden.
|
||||
try:
|
||||
if object.authors.filter(user__id=request.amo_user.pk):
|
||||
return True
|
||||
|
||||
# Appropriately handles AnonymousUsers when `amo_user` is None.
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
# Reviewers can see non-public apps.
|
||||
if request.method == 'GET':
|
||||
if acl.action_allowed(request, 'Apps', 'Review'):
|
||||
return True
|
||||
|
||||
|
||||
class AnonymousReadOnlyAuthorization(ReadOnlyAuthorization):
|
||||
"""
|
||||
Allows read-only access for anonymous users and optional auth for
|
||||
authenticated users.
|
||||
|
||||
If the user is anonymous, only ``GET`` requests are allowed.
|
||||
|
||||
If the user is authenticated, a custom authorization object can be used.
|
||||
If no object is set, authenticated users always have access.
|
||||
|
||||
Keyword Arguments
|
||||
|
||||
**authorizer=None**
|
||||
If set, this authorization object will be used to check the action for
|
||||
authenticated users.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
self.authorizer = kw.pop('authorizer', None)
|
||||
super(AnonymousReadOnlyAuthorization, self).__init__(*args, **kw)
|
||||
|
||||
def is_authorized(self, request, object=None):
|
||||
if request.user.is_anonymous():
|
||||
sup = super(AnonymousReadOnlyAuthorization, self)
|
||||
res = sup.is_authorized(request, object)
|
||||
log.info('ReadOnlyAuthorization returned: %s' % res)
|
||||
return res
|
||||
if self.authorizer:
|
||||
res = self.authorizer.is_authorized(request, object)
|
||||
log.info('Authorizer %s returned: %s' %
|
||||
(self.authorizer.__class__.__name__, res))
|
||||
return res
|
||||
return True
|
||||
|
||||
|
||||
class PermissionAuthorization(Authorization):
|
||||
|
||||
def __init__(self, app, action, *args, **kw):
|
||||
self.app, self.action = app, action
|
||||
|
||||
def is_authorized(self, request, object=None):
|
||||
if acl.action_allowed(request, self.app, self.action):
|
||||
return True
|
||||
log.info('Permission authorization failed')
|
||||
return False
|
||||
|
||||
has_permission = is_authorized
|
||||
|
||||
|
||||
class AnyOf(BasePermission):
|
||||
"""
|
||||
Takes multiple permission objects and succeeds if any single one does.
|
||||
|
|
421
mkt/api/base.py
421
mkt/api/base.py
|
@ -1,16 +1,8 @@
|
|||
import functools
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
from collections import defaultdict
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf.urls.defaults import url
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.db.models.sql import EmptyResultSet
|
||||
from django.http import HttpResponseNotFound
|
||||
|
||||
import commonware.log
|
||||
from rest_framework.decorators import api_view
|
||||
|
@ -19,23 +11,8 @@ from rest_framework.mixins import ListModelMixin
|
|||
from rest_framework.routers import Route, SimpleRouter
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from tastypie import fields, http
|
||||
from tastypie.bundle import Bundle
|
||||
from tastypie.exceptions import (ImmediateHttpResponse, NotFound,
|
||||
UnsupportedFormat)
|
||||
from tastypie.fields import ToOneField
|
||||
from tastypie.http import HttpConflict
|
||||
from tastypie.resources import ModelResource, Resource
|
||||
|
||||
from access import acl
|
||||
from translations.fields import PurifiedField, TranslatedField
|
||||
|
||||
from .exceptions import AlreadyPurchased, DeserializationError
|
||||
from .http import HttpTooManyRequests
|
||||
from .serializers import Serializer
|
||||
|
||||
log = commonware.log.getLogger('z.api')
|
||||
tasty_log = logging.getLogger('django.request.tastypie')
|
||||
|
||||
|
||||
def list_url(name, **kw):
|
||||
|
@ -47,40 +24,6 @@ def get_url(name, pk, **kw):
|
|||
kw.update({'resource_name': name, 'pk': pk})
|
||||
return ('api_dispatch_detail', kw)
|
||||
|
||||
|
||||
def http_error(errorclass, reason, extra_data=None):
|
||||
response = errorclass()
|
||||
data = {'reason': reason}
|
||||
if extra_data:
|
||||
data.update(extra_data)
|
||||
response.content = json.dumps(data)
|
||||
return ImmediateHttpResponse(response)
|
||||
|
||||
|
||||
def handle_500(resource, request, exception):
|
||||
response_class = http.HttpApplicationError
|
||||
if isinstance(exception, (NotFound, ObjectDoesNotExist)):
|
||||
response_class = HttpResponseNotFound
|
||||
|
||||
# Print some nice 500 errors back to the clients if not in debug mode.
|
||||
exc_info = sys.exc_info()
|
||||
tasty_log.error('%s: %s %s\n%s' % (request.path,
|
||||
exception.__class__.__name__,
|
||||
exception,
|
||||
traceback.format_tb(exc_info[2])),
|
||||
extra={'status_code': 500, 'request': request},
|
||||
exc_info=exc_info)
|
||||
data = {
|
||||
'error_message': str(exception),
|
||||
'error_code': getattr(exception, 'id',
|
||||
exception.__class__.__name__),
|
||||
'error_data': getattr(exception, 'data', {})
|
||||
}
|
||||
serialized = resource.serialize(request, data, 'application/json')
|
||||
return response_class(content=serialized,
|
||||
content_type='application/json; charset=utf-8')
|
||||
|
||||
|
||||
def _collect_form_errors(forms):
|
||||
errors = {}
|
||||
if not isinstance(forms, list):
|
||||
|
@ -101,327 +44,6 @@ def form_errors(forms):
|
|||
errors = _collect_form_errors(forms)
|
||||
raise ParseError(errors)
|
||||
|
||||
|
||||
class Marketplace(object):
|
||||
"""
|
||||
A mixin with some general Marketplace stuff.
|
||||
"""
|
||||
|
||||
class Meta(object):
|
||||
serializer = Serializer()
|
||||
|
||||
def _handle_500(self, request, exception):
|
||||
return handle_500(self, request, exception)
|
||||
|
||||
def dispatch(self, request_type, request, **kwargs):
|
||||
# OAuth authentication uses the method in the signature. So we need
|
||||
# to store the original method used to sign the request.
|
||||
request.signed_method = request.method
|
||||
if 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
|
||||
request.method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
|
||||
|
||||
log.info('Request: %s' % request.META.get('PATH_INFO'))
|
||||
ct = request.META.get('CONTENT_TYPE')
|
||||
try:
|
||||
return (super(Marketplace, self)
|
||||
.dispatch(request_type, request, **kwargs))
|
||||
|
||||
except DeserializationError:
|
||||
if ct:
|
||||
error = "Unable to deserialize request body as '%s'" % ct
|
||||
else:
|
||||
error = 'Content-Type header required'
|
||||
raise self.non_form_errors((('__all__', error),),)
|
||||
|
||||
except UnsupportedFormat:
|
||||
msgs = []
|
||||
if ct not in self._meta.serializer.supported_formats:
|
||||
msgs.append(('__all__',
|
||||
"Unsupported Content-Type header '%s'" % ct))
|
||||
|
||||
accept = request.META.get('HTTP_ACCEPT')
|
||||
if accept and accept != 'application/json':
|
||||
msgs.append(('__all__',
|
||||
"Unsupported Accept header '%s'" % accept))
|
||||
|
||||
raise self.non_form_errors(msgs)
|
||||
|
||||
except PermissionDenied:
|
||||
# Reraise PermissionDenied as 403, otherwise you get 500.
|
||||
raise http_error(http.HttpForbidden, 'Permission denied.')
|
||||
|
||||
except AlreadyPurchased:
|
||||
raise http_error(HttpConflict, 'Already purchased app.')
|
||||
|
||||
def non_form_errors(self, error_list):
|
||||
"""
|
||||
Raises passed field errors as an immediate HttpBadRequest response.
|
||||
Similar to Marketplace.form_errors, except that it allows you to raise
|
||||
form field errors outside of form validation.
|
||||
|
||||
Accepts a list of two-tuples, consisting of a field name and error
|
||||
message.
|
||||
|
||||
Example usage:
|
||||
|
||||
errors = []
|
||||
|
||||
if 'app' in bundle.data:
|
||||
errors.append(('app', 'Cannot update the app of a rating.'))
|
||||
|
||||
if 'user' in bundle.data:
|
||||
errors.append(('user', 'Cannot update the author of a rating.'))
|
||||
|
||||
if errors:
|
||||
raise self.non_form_errors(errors)
|
||||
"""
|
||||
errors = defaultdict(list)
|
||||
for e in error_list:
|
||||
errors[e[0]].append(e[1])
|
||||
response = http.HttpBadRequest(json.dumps({'error_message': errors}),
|
||||
content_type='application/json')
|
||||
return ImmediateHttpResponse(response=response)
|
||||
|
||||
def form_errors(self, forms):
|
||||
errors = _collect_form_errors(forms)
|
||||
response = http.HttpBadRequest(json.dumps({'error_message': errors}),
|
||||
content_type='application/json')
|
||||
return ImmediateHttpResponse(response=response)
|
||||
|
||||
def _auths(self):
|
||||
auths = self._meta.authentication
|
||||
if not isinstance(auths, (list, tuple)):
|
||||
auths = [self._meta.authentication]
|
||||
return auths
|
||||
|
||||
def is_authenticated(self, request):
|
||||
"""
|
||||
An override of the tastypie Authentication to accept an iterator
|
||||
of Authentication methods. If so it will go through in order, when one
|
||||
passes, it will use that.
|
||||
|
||||
Any authentication method can still return a HttpResponse to break out
|
||||
of the loop if they desire.
|
||||
"""
|
||||
for auth in self._auths():
|
||||
log.info('Trying authentication with %s' % auth.__class__.__name__)
|
||||
auth_result = auth.is_authenticated(request)
|
||||
|
||||
if isinstance(auth_result, http.HttpResponse):
|
||||
raise ImmediateHttpResponse(response=auth_result)
|
||||
|
||||
if auth_result:
|
||||
log.info('Logged in using %s' % auth.__class__.__name__)
|
||||
return
|
||||
|
||||
raise http_error(http.HttpUnauthorized, 'Authentication required.')
|
||||
|
||||
def get_throttle_identifiers(self, request):
|
||||
return set(a.get_identifier(request) for a in self._auths())
|
||||
|
||||
def throttle_check(self, request):
|
||||
"""
|
||||
Handles checking if the user should be throttled.
|
||||
|
||||
Mostly a hook, this uses class assigned to ``throttle`` from
|
||||
``Resource._meta``.
|
||||
"""
|
||||
# Never throttle users with Apps:APIUnthrottled or "safe" requests.
|
||||
if (not settings.API_THROTTLE or
|
||||
request.method in ('GET', 'HEAD', 'OPTIONS') or
|
||||
acl.action_allowed(request, 'Apps', 'APIUnthrottled')):
|
||||
return
|
||||
|
||||
identifiers = self.get_throttle_identifiers(request)
|
||||
|
||||
# Check to see if they should be throttled.
|
||||
if any(self._meta.throttle.should_be_throttled(identifier)
|
||||
for identifier in identifiers):
|
||||
# Throttle limit exceeded.
|
||||
raise http_error(HttpTooManyRequests,
|
||||
'Throttle limit exceeded.')
|
||||
|
||||
def log_throttled_access(self, request):
|
||||
"""
|
||||
Handles the recording of the user's access for throttling purposes.
|
||||
|
||||
Mostly a hook, this uses class assigned to ``throttle`` from
|
||||
``Resource._meta``.
|
||||
"""
|
||||
request_method = request.method.lower()
|
||||
identifiers = self.get_throttle_identifiers(request)
|
||||
for identifier in identifiers:
|
||||
self._meta.throttle.accessed(identifier,
|
||||
url=request.get_full_path(),
|
||||
request_method=request_method)
|
||||
|
||||
def cached_obj_get_list(self, request=None, **kwargs):
|
||||
"""Do not interfere with cache machine caching."""
|
||||
return self.obj_get_list(request=request, **kwargs)
|
||||
|
||||
def cached_obj_get(self, request=None, **kwargs):
|
||||
"""Do not interfere with cache machine caching."""
|
||||
return self.obj_get(request, **kwargs)
|
||||
|
||||
def is_valid(self, bundle, request=None):
|
||||
"""A simple wrapper to return form errors in the format we want."""
|
||||
errors = self._meta.validation.is_valid(bundle, request)
|
||||
if errors:
|
||||
raise self.form_errors(errors)
|
||||
|
||||
def dehydrate_objects(self, objects, request=None):
|
||||
"""
|
||||
Dehydrates each object using the full_dehydrate and then
|
||||
returns the data for each object. This is useful for compound
|
||||
results that return sub objects data. If you need request in the
|
||||
dehydration, pass that through (eg: accessing region)
|
||||
"""
|
||||
return [self.full_dehydrate(Bundle(obj=o, request=request)).data
|
||||
for o in objects]
|
||||
|
||||
|
||||
class MarketplaceResource(Marketplace, Resource):
|
||||
"""
|
||||
Use this if you would like to expose something that is *not* a Django
|
||||
model as an API.
|
||||
"""
|
||||
|
||||
def get_resource_uri(self, *args, **kw):
|
||||
return ''
|
||||
|
||||
|
||||
class MarketplaceModelResource(Marketplace, ModelResource):
|
||||
"""Use this if you would like to expose a Django model as an API."""
|
||||
|
||||
def get_resource_uri(self, bundle_or_obj):
|
||||
# Fix until my pull request gets pulled into tastypie.
|
||||
# https://github.com/toastdriven/django-tastypie/pull/490
|
||||
kwargs = {
|
||||
'resource_name': self._meta.resource_name,
|
||||
}
|
||||
|
||||
if isinstance(bundle_or_obj, Bundle):
|
||||
kwargs['pk'] = bundle_or_obj.obj.pk
|
||||
else:
|
||||
kwargs['pk'] = bundle_or_obj.pk
|
||||
|
||||
if self._meta.api_name is not None:
|
||||
kwargs['api_name'] = self._meta.api_name
|
||||
|
||||
return self._build_reverse_url("api_dispatch_detail", kwargs=kwargs)
|
||||
|
||||
@classmethod
|
||||
def should_skip_field(cls, field):
|
||||
# We don't want to skip translated fields.
|
||||
if isinstance(field, (PurifiedField, TranslatedField)):
|
||||
return False
|
||||
|
||||
return True if getattr(field, 'rel') else False
|
||||
|
||||
def get_object_or_404(self, cls, **filters):
|
||||
"""
|
||||
A wrapper around our more familiar get_object_or_404, for when we need
|
||||
to get access to an object that isn't covered by get_obj.
|
||||
"""
|
||||
if not filters:
|
||||
raise http_error(http.HttpNotFound, 'Not found.')
|
||||
try:
|
||||
return cls.objects.get(**filters)
|
||||
except (cls.DoesNotExist, cls.MultipleObjectsReturned):
|
||||
raise http_error(http.HttpNotFound, 'Not found.')
|
||||
|
||||
def get_by_resource_or_404(self, request, **kwargs):
|
||||
"""
|
||||
A wrapper around the obj_get to just get the object.
|
||||
"""
|
||||
try:
|
||||
obj = self.obj_get(request, **kwargs)
|
||||
except ObjectDoesNotExist:
|
||||
raise http_error(http.HttpNotFound, 'Not found.')
|
||||
return obj
|
||||
|
||||
def base_urls(self):
|
||||
"""
|
||||
If `slug_lookup` is specified on the Meta of a resource, add
|
||||
in an extra resource that allows lookup by that slug field. This
|
||||
assumes that the slug won't be all numbers. If the slug is numeric, it
|
||||
will hit the pk URL pattern and chaos will ensue.
|
||||
"""
|
||||
if not getattr(self._meta, 'slug_lookup', None):
|
||||
return super(MarketplaceModelResource, self).base_urls()
|
||||
|
||||
return super(MarketplaceModelResource, self).base_urls()[:3] + [
|
||||
url(r'^(?P<resource_name>%s)/(?P<pk>\d+)/$' %
|
||||
self._meta.resource_name,
|
||||
self.wrap_view('dispatch_detail'),
|
||||
name='api_dispatch_detail'),
|
||||
url(r"^(?P<resource_name>%s)/(?P<%s>[^/<>\"']+)/$" %
|
||||
(self._meta.resource_name, self._meta.slug_lookup),
|
||||
self.wrap_view('dispatch_detail'),
|
||||
name='api_dispatch_detail')
|
||||
]
|
||||
|
||||
|
||||
class GenericObject(dict):
|
||||
"""
|
||||
tastypie-friendly subclass of dict that allows direct attribute assignment
|
||||
of dict items. Best used as `object_class` when not using a `ModelResource`
|
||||
subclass.
|
||||
"""
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self.__getitem__(name)
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
self.__setitem__(name, value)
|
||||
|
||||
|
||||
class CORSResource(object):
|
||||
"""
|
||||
A mixin to provide CORS support to your API.
|
||||
"""
|
||||
|
||||
def method_check(self, request, allowed=None):
|
||||
"""
|
||||
This is the first entry point from dispatch and a place to check CORS.
|
||||
|
||||
It will set a value on the request for the middleware to pick up on
|
||||
the response and add in the headers, so that any immediate http
|
||||
responses (which are usually errors) get the headers.
|
||||
|
||||
Optionally, you can specify the methods that will be specifying the
|
||||
`cors_allowed` attribute on the resource meta. Otherwise, it will use
|
||||
the combination of allowed_methods specified on the resource.
|
||||
"""
|
||||
request.CORS = getattr(self._meta, 'cors_allowed', None) or allowed
|
||||
return super(CORSResource, self).method_check(request, allowed=allowed)
|
||||
|
||||
|
||||
class PotatoCaptchaResource(object):
|
||||
"""
|
||||
A mixin adding the fields required by PotatoCaptcha to the resource.
|
||||
"""
|
||||
tuber = fields.CharField(attribute='tuber')
|
||||
sprout = fields.CharField(attribute='sprout')
|
||||
|
||||
def remove_potato(self, bundle):
|
||||
for field in ['tuber', 'sprout']:
|
||||
if field in bundle.data:
|
||||
del bundle.data[field]
|
||||
return bundle
|
||||
|
||||
def alter_detail_data_to_serialize(self, request, data):
|
||||
"""
|
||||
Remove `sprout` from bundle data before returning serialized object to
|
||||
the consumer.
|
||||
"""
|
||||
sup = super(PotatoCaptchaResource, self)
|
||||
bundle = sup.alter_detail_data_to_serialize(request, data)
|
||||
return self.remove_potato(bundle)
|
||||
|
||||
|
||||
def check_potatocaptcha(data):
|
||||
if data.get('tuber', False):
|
||||
return Response(json.dumps({'tuber': 'Invalid value'}), 400)
|
||||
|
@ -429,31 +51,6 @@ def check_potatocaptcha(data):
|
|||
return Response(json.dumps({'sprout': 'Invalid value'}), 400)
|
||||
|
||||
|
||||
class CompatToOneField(ToOneField):
|
||||
"""
|
||||
Tastypie field to relate a resource to a django-rest-framework view.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.url_name = kwargs.pop('url_name', None)
|
||||
self.extra_fields = kwargs.pop('extra_fields', None)
|
||||
return super(CompatToOneField, self).__init__(*args, **kwargs)
|
||||
|
||||
def dehydrate_related(self, bundle, related_resource):
|
||||
uri = reverse(self.url_name, kwargs={'pk': bundle.obj.pk})
|
||||
if self.full:
|
||||
raise NotImplementedError
|
||||
elif self.extra_fields:
|
||||
result = {'resource_uri': uri}
|
||||
for field in self.extra_fields:
|
||||
result[field] = getattr(bundle.obj, field)
|
||||
return result
|
||||
else:
|
||||
return uri
|
||||
|
||||
def get_related_resource(self, related_instance):
|
||||
return
|
||||
|
||||
|
||||
class AppRouter(SimpleRouter):
|
||||
routes = [
|
||||
# List route.
|
||||
|
@ -610,21 +207,3 @@ class SilentListModelMixin(ListModelMixin):
|
|||
if res.status_code == 404:
|
||||
return Response([])
|
||||
return res
|
||||
|
||||
|
||||
class AppViewSet(GenericViewSet):
|
||||
|
||||
def initialize_request(self, request, *args, **kwargs):
|
||||
"""
|
||||
Pass the value in the URL through to the form defined on the
|
||||
ViewSet, which will populate the app property with the app object.
|
||||
|
||||
You must define a form which will take an app object.
|
||||
"""
|
||||
request = (super(AppViewSet, self)
|
||||
.initialize_request(request, *args, **kwargs))
|
||||
self.app = None
|
||||
form = self.form({'app': kwargs.get('pk')})
|
||||
if form.is_valid():
|
||||
self.app = form.cleaned_data['app']
|
||||
return request
|
||||
|
|
|
@ -5,12 +5,6 @@ from rest_framework import status
|
|||
from rest_framework.exceptions import APIException
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import exception_handler
|
||||
from tastypie.exceptions import TastypieError
|
||||
|
||||
|
||||
class DeserializationError(TastypieError):
|
||||
def __init__(self, original=None):
|
||||
self.original = original
|
||||
|
||||
|
||||
class AlreadyPurchased(Exception):
|
||||
|
|
|
@ -5,7 +5,6 @@ import StringIO
|
|||
from django import forms
|
||||
|
||||
import happyforms
|
||||
from tastypie.validation import CleanedDataFormValidation
|
||||
from tower import ugettext_lazy as _lazy
|
||||
|
||||
import amo
|
||||
|
@ -49,25 +48,6 @@ class SluggableModelChoiceField(forms.ModelChoiceField):
|
|||
return super(SluggableModelChoiceField, self).to_python(value)
|
||||
|
||||
|
||||
class RequestFormValidation(CleanedDataFormValidation):
|
||||
"""
|
||||
A sub class of CleanedDataFormValidation that passes request through to
|
||||
the form.
|
||||
"""
|
||||
def is_valid(self, bundle, request=None):
|
||||
data = bundle.data
|
||||
if data is None:
|
||||
data = {}
|
||||
|
||||
form = self.form_class(data, request=request)
|
||||
|
||||
if form.is_valid():
|
||||
bundle.data = form.cleaned_data
|
||||
return {}
|
||||
|
||||
return form.errors
|
||||
|
||||
|
||||
def parse(file_, require_name=False, require_type=None):
|
||||
try:
|
||||
if not set(['data', 'type']).issubset(set(file_.keys())):
|
||||
|
|
|
@ -4,37 +4,12 @@ from django.utils.simplejson import JSONDecodeError
|
|||
import commonware.log
|
||||
from rest_framework import serializers
|
||||
from rest_framework.reverse import reverse
|
||||
from tastypie.serializers import Serializer
|
||||
from tastypie.exceptions import UnsupportedFormat
|
||||
from tower import ugettext as _
|
||||
|
||||
from mkt.api.exceptions import DeserializationError
|
||||
|
||||
|
||||
log = commonware.log.getLogger('z.mkt.api.forms')
|
||||
|
||||
|
||||
class Serializer(Serializer):
|
||||
|
||||
formats = ['json', 'urlencode']
|
||||
content_types = {
|
||||
'json': 'application/json',
|
||||
'urlencode': 'application/x-www-form-urlencoded',
|
||||
}
|
||||
|
||||
def from_urlencode(self, data):
|
||||
return QueryDict(data).dict()
|
||||
|
||||
def to_urlencode(self, data, options=None):
|
||||
raise UnsupportedFormat
|
||||
|
||||
def deserialize(self, content, format='application/json'):
|
||||
try:
|
||||
return super(Serializer, self).deserialize(content, format)
|
||||
except JSONDecodeError, exc:
|
||||
raise DeserializationError(original=exc)
|
||||
|
||||
|
||||
class PotatoCaptchaSerializer(serializers.Serializer):
|
||||
"""
|
||||
Serializer class to inherit from to get PotatoCaptcha (tm) protection for
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from datetime import datetime
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
|
@ -8,88 +7,25 @@ from mock import Mock, patch
|
|||
from multidb import this_thread_is_pinned
|
||||
from nose.tools import eq_, ok_
|
||||
from rest_framework.request import Request
|
||||
from tastypie.exceptions import ImmediateHttpResponse
|
||||
|
||||
from access.models import Group, GroupUser
|
||||
from addons.models import AddonUser
|
||||
from amo.helpers import absolutify
|
||||
from amo.tests import app_factory, TestCase
|
||||
from amo.tests import TestCase
|
||||
from amo.urlresolvers import reverse
|
||||
from test_utils import RequestFactory
|
||||
from users.models import UserProfile
|
||||
|
||||
from mkt.api import authentication, authorization
|
||||
from mkt.api.authentication import errors
|
||||
from mkt.api.base import MarketplaceResource
|
||||
from mkt.api import authentication
|
||||
from mkt.api.models import Access, generate
|
||||
from mkt.api.tests.test_oauth import OAuthClient
|
||||
from mkt.site.fixtures import fixture
|
||||
|
||||
|
||||
class OwnerAuthorization(TestCase):
|
||||
fixtures = fixture('user_2519')
|
||||
|
||||
def setUp(self):
|
||||
self.profile = UserProfile.objects.get(pk=2519)
|
||||
self.user = self.profile.user
|
||||
|
||||
def request(self, user=None):
|
||||
return Mock(amo_user=user, groups=user.groups.all() if user else None)
|
||||
|
||||
def obj(self, user=None):
|
||||
return Mock(user=user)
|
||||
|
||||
|
||||
class TestOwnerAuthorization(OwnerAuthorization):
|
||||
|
||||
def setUp(self):
|
||||
super(TestOwnerAuthorization, self).setUp()
|
||||
self.auth = authorization.OwnerAuthorization()
|
||||
|
||||
def test_user(self):
|
||||
ok_(self.auth.check_owner(self.request(self.profile),
|
||||
self.obj(self.user)))
|
||||
|
||||
def test_not_user(self):
|
||||
ok_(not self.auth.check_owner(self.request(self.profile),
|
||||
self.obj()))
|
||||
|
||||
def test_diff_user(self):
|
||||
user = Mock()
|
||||
user.pk = self.user.pk + 1
|
||||
ok_(not self.auth.check_owner(self.request(self.profile),
|
||||
self.obj(user)))
|
||||
|
||||
def test_no_object(self):
|
||||
ok_(self.auth.is_authorized(None))
|
||||
|
||||
def test_no_user(self):
|
||||
ok_(not self.auth.is_authorized(self.request(None), True))
|
||||
|
||||
|
||||
class TestAppOwnerAuthorization(OwnerAuthorization):
|
||||
|
||||
def setUp(self):
|
||||
super(TestAppOwnerAuthorization, self).setUp()
|
||||
self.auth = authorization.AppOwnerAuthorization()
|
||||
self.app = app_factory()
|
||||
|
||||
def test_owner(self):
|
||||
AddonUser.objects.create(addon=self.app, user=self.profile)
|
||||
ok_(self.auth.check_owner(self.request(self.profile),
|
||||
self.app))
|
||||
|
||||
def test_not_owner(self):
|
||||
ok_(not self.auth.check_owner(self.request(self.profile),
|
||||
self.app))
|
||||
|
||||
|
||||
class TestOAuthAuthentication(TestCase):
|
||||
fixtures = fixture('user_2519', 'group_admin', 'group_editor')
|
||||
|
||||
def setUp(self):
|
||||
self.api_name = 'foo'
|
||||
self.auth = authentication.OAuthAuthentication()
|
||||
self.profile = UserProfile.objects.get(pk=2519)
|
||||
self.profile.update(read_dev_agreement=datetime.today())
|
||||
self.access = Access.objects.create(key='test_oauth_key',
|
||||
|
@ -103,37 +39,11 @@ class TestOAuthAuthentication(TestCase):
|
|||
HTTP_HOST='testserver',
|
||||
HTTP_AUTHORIZATION=client.sign('GET', url)[1]['Authorization'])
|
||||
|
||||
def test_accepted(self):
|
||||
req = Request(self.call())
|
||||
ok_(self.auth.is_authenticated(req))
|
||||
if req.method in ['DELETE', 'PATCH', 'POST', 'PUT']:
|
||||
ok_(this_thread_is_pinned())
|
||||
else:
|
||||
ok_(not this_thread_is_pinned())
|
||||
|
||||
def test_request_token_fake(self):
|
||||
c = Mock()
|
||||
c.key = self.access.key
|
||||
c.secret = 'mom'
|
||||
res = self.auth.is_authenticated(self.call(client=OAuthClient(c)))
|
||||
eq_(res.status_code, 401)
|
||||
eq_(json.loads(res.content)['reason'], errors['headers'])
|
||||
|
||||
def add_group_user(self, user, *names):
|
||||
for name in names:
|
||||
group = Group.objects.get(name=name)
|
||||
GroupUser.objects.create(user=self.profile, group=group)
|
||||
|
||||
def test_request_admin(self):
|
||||
self.add_group_user(self.profile, 'Admins')
|
||||
res = self.auth.is_authenticated(self.call())
|
||||
eq_(res.status_code, 401)
|
||||
eq_(json.loads(res.content)['reason'], errors['roles'])
|
||||
|
||||
def test_request_has_role(self):
|
||||
self.add_group_user(self.profile, 'App Reviewers')
|
||||
ok_(self.auth.is_authenticated(self.call()))
|
||||
|
||||
|
||||
class TestRestOAuthAuthentication(TestOAuthAuthentication):
|
||||
|
||||
|
@ -182,7 +92,7 @@ class TestSharedSecretAuthentication(TestCase):
|
|||
fixtures = fixture('user_2519')
|
||||
|
||||
def setUp(self):
|
||||
self.auth = authentication.SharedSecretAuthentication()
|
||||
self.auth = authentication.RestSharedSecretAuthentication()
|
||||
self.profile = UserProfile.objects.get(pk=2519)
|
||||
self.profile.update(email=self.profile.user.email)
|
||||
|
||||
|
@ -235,79 +145,6 @@ class TestSharedSecretAuthentication(TestCase):
|
|||
assert not self.auth.is_authenticated(req)
|
||||
|
||||
|
||||
class TestOptionalOAuthAuthentication(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.auth = authentication.OptionalOAuthAuthentication()
|
||||
|
||||
def test_none(self):
|
||||
req = RequestFactory().get('/')
|
||||
ok_(self.auth.is_authenticated(req))
|
||||
|
||||
def test_something(self):
|
||||
# Malformed auth info is rejected.
|
||||
req = RequestFactory().get('/', HTTP_AUTHORIZATION='No!')
|
||||
ok_(not self.auth.is_authenticated(req))
|
||||
|
||||
|
||||
class MultipleTestResource(MarketplaceResource):
|
||||
pass
|
||||
|
||||
|
||||
@patch.object(settings, 'SECRET_KEY', 'gubbish')
|
||||
class TestMultipleAuthentication(TestCase):
|
||||
fixtures = fixture('user_2519')
|
||||
|
||||
def setUp(self):
|
||||
self.resource = MultipleTestResource()
|
||||
self.profile = UserProfile.objects.get(pk=2519)
|
||||
self.profile.update(email=self.profile.user.email)
|
||||
|
||||
def test_single(self):
|
||||
req = RequestFactory().get(
|
||||
'/',
|
||||
HTTP_AUTHORIZATION='mkt-shared-secret '
|
||||
'cfinke@m.com,56b6f1a3dd735d962c56'
|
||||
'ce7d8f46e02ec1d4748d2c00c407d75f0969d08bb'
|
||||
'9c68c31b3371aa8130317815c89e5072e31bb94b4'
|
||||
'121c5c165f3515838d4d6c60c4,165d631d3c3045'
|
||||
'458b4516242dad7ae')
|
||||
self.resource._meta.authentication = (
|
||||
authentication.SharedSecretAuthentication(),)
|
||||
eq_(self.resource.is_authenticated(req), None)
|
||||
eq_(self.profile.user.pk, req.amo_user.pk)
|
||||
|
||||
def test_multiple_passes(self):
|
||||
req = RequestFactory().get('/')
|
||||
req.user = AnonymousUser()
|
||||
self.resource._meta.authentication = (
|
||||
authentication.SharedSecretAuthentication(),
|
||||
# Optional auth passes because there are not auth headers.
|
||||
authentication.OptionalOAuthAuthentication())
|
||||
|
||||
eq_(self.resource.is_authenticated(req), None)
|
||||
|
||||
def test_multiple_fails(self):
|
||||
client = OAuthClient(Mock(key='test_oauth_key',
|
||||
secret='test_oauth_secret'))
|
||||
req = RequestFactory().get(
|
||||
'/',
|
||||
HTTP_HOST='testserver',
|
||||
HTTP_AUTHORIZATION=client.sign(
|
||||
'GET', 'http://foo/')[1]['Authorization'])
|
||||
req.user = AnonymousUser()
|
||||
next_auth = Mock()
|
||||
self.resource._meta.authentication = (
|
||||
# OAuth fails because there are bogus auth headers.
|
||||
authentication.OAuthAuthentication(),
|
||||
next_auth)
|
||||
|
||||
with self.assertRaises(ImmediateHttpResponse):
|
||||
eq_(self.resource.is_authenticated(req), None)
|
||||
# This never even got called.
|
||||
ok_(not next_auth.is_authenticated.called)
|
||||
|
||||
|
||||
@patch.object(settings, 'SECRET_KEY', 'gubbish')
|
||||
class TestMultipleAuthenticationDRF(TestCase):
|
||||
fixtures = fixture('user_2519')
|
||||
|
|
|
@ -5,73 +5,16 @@ from mock import Mock
|
|||
from nose.tools import eq_, ok_
|
||||
from test_utils import RequestFactory
|
||||
|
||||
from amo.tests import app_factory, TestCase
|
||||
from amo.tests import TestCase
|
||||
from users.models import UserProfile
|
||||
|
||||
from mkt.api.authorization import (AllowAppOwner, AllowNone, AllowOwner,
|
||||
AllowRelatedAppOwner, AllowSelf,
|
||||
AnonymousReadOnlyAuthorization, AnyOf,
|
||||
ByHttpMethod, flag, GroupPermission,
|
||||
PermissionAuthorization, switch)
|
||||
AnyOf,
|
||||
ByHttpMethod, flag, GroupPermission, switch)
|
||||
from mkt.site.fixtures import fixture
|
||||
from mkt.webapps.models import Webapp
|
||||
|
||||
from .test_authentication import OwnerAuthorization
|
||||
|
||||
|
||||
class TestAnonymousReadOnlyAuthorization(TestCase):
|
||||
fixtures = fixture('user_2519')
|
||||
|
||||
def setUp(self):
|
||||
self.get = RequestFactory().get('/')
|
||||
self.post = RequestFactory().post('/')
|
||||
self.auth = AnonymousReadOnlyAuthorization()
|
||||
self.anon = AnonymousUser()
|
||||
self.user = User.objects.get(pk=2519)
|
||||
|
||||
def test_get_anonymous(self):
|
||||
self.get.user = self.anon
|
||||
eq_(self.auth.is_authorized(self.get), True)
|
||||
|
||||
def test_get_authenticated(self):
|
||||
self.get.user = self.user
|
||||
eq_(self.auth.is_authorized(self.get), True)
|
||||
|
||||
def test_post_anonymous(self):
|
||||
self.post.user = self.anon
|
||||
eq_(self.auth.is_authorized(self.post), False)
|
||||
|
||||
def test_post_authenticated(self):
|
||||
self.post.user = self.user
|
||||
eq_(self.auth.is_authorized(self.post), True)
|
||||
|
||||
def test_with_authorizer(self):
|
||||
|
||||
class LockedOut:
|
||||
def is_authorized(self, request, object=None):
|
||||
return False
|
||||
|
||||
self.auth = AnonymousReadOnlyAuthorization(
|
||||
authorizer=LockedOut())
|
||||
self.post.user = self.user
|
||||
eq_(self.auth.is_authorized(self.post), False)
|
||||
|
||||
|
||||
class TestPermissionAuthorization(OwnerAuthorization):
|
||||
|
||||
def setUp(self):
|
||||
super(TestPermissionAuthorization, self).setUp()
|
||||
self.auth = PermissionAuthorization('Drinkers', 'Beer')
|
||||
self.app = app_factory()
|
||||
|
||||
def test_has_role(self):
|
||||
self.grant_permission(self.profile, 'Drinkers:Beer')
|
||||
ok_(self.auth.is_authorized(self.request(self.profile), self.app))
|
||||
|
||||
def test_not_has_role(self):
|
||||
self.grant_permission(self.profile, 'Drinkers:Scotch')
|
||||
ok_(not self.auth.is_authorized(self.request(self.profile), self.app))
|
||||
|
||||
|
||||
class TestWaffle(TestCase):
|
||||
|
||||
|
|
|
@ -1,43 +1,22 @@
|
|||
import json
|
||||
import urllib
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
from mock import patch
|
||||
from nose.tools import eq_, ok_
|
||||
from nose.tools import eq_
|
||||
|
||||
from rest_framework.decorators import (authentication_classes,
|
||||
permission_classes)
|
||||
from rest_framework.response import Response
|
||||
|
||||
from tastypie import http
|
||||
from tastypie.authentication import Authentication
|
||||
from tastypie.authorization import Authorization
|
||||
from tastypie.exceptions import ImmediateHttpResponse
|
||||
from tastypie.throttle import BaseThrottle
|
||||
from test_utils import RequestFactory
|
||||
|
||||
from access.middleware import ACLMiddleware
|
||||
from amo.tests import TestCase
|
||||
from mkt.api.base import (AppViewSet, cors_api_view, CORSResource, handle_500,
|
||||
MarketplaceResource)
|
||||
from mkt.api.http import HttpTooManyRequests
|
||||
from mkt.api.serializers import Serializer
|
||||
from mkt.receipts.tests.test_views import RawRequestFactory
|
||||
from mkt.site.fixtures import fixture
|
||||
from amo.urlresolvers import reverse
|
||||
|
||||
|
||||
class SampleResource(MarketplaceResource):
|
||||
|
||||
class Meta(object):
|
||||
authorization = Authorization()
|
||||
serializer = Serializer()
|
||||
object_class = dict
|
||||
|
||||
def get_resource_uri(self, bundle):
|
||||
return ''
|
||||
from mkt.api.base import cors_api_view
|
||||
from mkt.api.tests.test_oauth import RestOAuth
|
||||
from mkt.webapps.api import AppViewSet
|
||||
|
||||
|
||||
class URLRequestFactory(RequestFactory):
|
||||
|
@ -46,191 +25,37 @@ class URLRequestFactory(RequestFactory):
|
|||
return urllib.urlencode(data)
|
||||
|
||||
|
||||
class TestLogging(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.resource = SampleResource()
|
||||
self.request = URLRequestFactory().get('/')
|
||||
self.exception_cls = type('SampleException', (Exception,), {})
|
||||
|
||||
@patch('mkt.api.base.tasty_log.error')
|
||||
def test_logging(self, mock_error_log):
|
||||
msg = 'oops'
|
||||
handle_500(self.resource, self.request, self.exception_cls(msg))
|
||||
eq_(mock_error_log.call_count, 1)
|
||||
ok_(self.exception_cls.__name__ in mock_error_log.call_args[0][0])
|
||||
ok_(msg in mock_error_log.call_args[0][0])
|
||||
ok_('exc_info' in mock_error_log.call_args[1])
|
||||
|
||||
|
||||
class TestEncoding(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.resource = SampleResource()
|
||||
self.request = URLRequestFactory().post('/')
|
||||
class TestEncoding(RestOAuth):
|
||||
|
||||
def test_blah_encoded(self):
|
||||
"""
|
||||
Regression test of bug #858403: ensure that a 400 (and not 500) is
|
||||
Regression test of bug #858403: ensure that a 415 (and not 500) is
|
||||
raised when an unsupported Content-Type header is passed to an API
|
||||
endpoint.
|
||||
"""
|
||||
self.request.META['CONTENT_TYPE'] = 'application/blah'
|
||||
with self.assertImmediate(http.HttpBadRequest):
|
||||
self.resource.dispatch('list', self.request)
|
||||
|
||||
def test_no_contenttype(self):
|
||||
del self.request.META['CONTENT_TYPE']
|
||||
with self.assertImmediate(http.HttpBadRequest):
|
||||
self.resource.dispatch('list', self.request)
|
||||
r = self.client.post(reverse('app-list'),
|
||||
CONTENT_TYPE='application/blah',
|
||||
data='cvan was here')
|
||||
eq_(r.status_code, 415)
|
||||
|
||||
def test_bad_json(self):
|
||||
request = RawRequestFactory().post('/', "not ' json ' 5")
|
||||
with self.assertImmediate(http.HttpBadRequest):
|
||||
self.resource.dispatch('list', request)
|
||||
r = self.client.post(reverse('app-list'),
|
||||
CONTENT_TYPE='application/json',
|
||||
data="not ' json ' 5")
|
||||
eq_(r.status_code, 400)
|
||||
|
||||
def test_not_json(self):
|
||||
self.request.META['HTTP_ACCEPT'] = 'application/blah'
|
||||
with self.assertImmediate(http.HttpBadRequest):
|
||||
self.resource.dispatch('list', self.request)
|
||||
r = self.client.get(reverse('app-list'),
|
||||
HTTP_ACCEPT='application/blah')
|
||||
eq_(r.status_code, 406)
|
||||
|
||||
def test_errors(self):
|
||||
self.request.META['HTTP_ACCEPT'] = 'application/blah'
|
||||
self.request.META['CONTENT_TYPE'] = 'application/blah'
|
||||
try:
|
||||
self.resource.dispatch('list', self.request)
|
||||
except ImmediateHttpResponse, error:
|
||||
pass
|
||||
|
||||
res = json.loads(error.response.content)['error_message']['__all__']
|
||||
eq_([u"Unsupported Content-Type header 'application/blah'",
|
||||
u"Unsupported Accept header 'application/blah'"], res)
|
||||
|
||||
@patch.object(SampleResource, 'obj_create')
|
||||
def test_form_encoded(self, obj_create):
|
||||
request = URLRequestFactory().post('/', data={'foo': 'bar'},
|
||||
content_type='application/x-www-form-urlencoded')
|
||||
self.resource.dispatch('list', request)
|
||||
eq_(obj_create.call_args[0][0].data, {'foo': 'bar'})
|
||||
|
||||
@patch.object(SampleResource, 'obj_create')
|
||||
def test_permission(self, obj_create):
|
||||
request = RequestFactory().post('/', data={},
|
||||
content_type='application/json')
|
||||
obj_create.side_effect = PermissionDenied
|
||||
with self.assertImmediate(http.HttpForbidden):
|
||||
self.resource.dispatch('list', request)
|
||||
|
||||
|
||||
class FakeAuthentication(Authentication):
|
||||
def get_identifier(self, request):
|
||||
return 'fake'
|
||||
|
||||
|
||||
class ThrottleResource(MarketplaceResource):
|
||||
|
||||
class Meta(object):
|
||||
authorization = Authorization()
|
||||
throttle = BaseThrottle()
|
||||
|
||||
|
||||
class TestThrottling(TestCase):
|
||||
fixtures = fixture('user_2519')
|
||||
|
||||
def setUp(self):
|
||||
super(TestThrottling, self).setUp()
|
||||
self.resource = ThrottleResource()
|
||||
self.request = RequestFactory().post('/')
|
||||
self.user = User.objects.get(pk=2519)
|
||||
self.request.user = self.user
|
||||
self.throttle = self.resource._meta.throttle
|
||||
self.request.META['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
|
||||
self.mocked_sbt = patch.object(self.throttle, 'should_be_throttled')
|
||||
|
||||
def no_throttle_expected(self, request=None):
|
||||
if request is None:
|
||||
request = self.request
|
||||
try:
|
||||
self.resource.throttle_check(request)
|
||||
except ImmediateHttpResponse, e:
|
||||
if isinstance(e.response, HttpTooManyRequests):
|
||||
self.fail('Unexpected 429')
|
||||
raise e
|
||||
|
||||
def throttle_expected(self):
|
||||
with self.assertImmediate(HttpTooManyRequests):
|
||||
self.resource.throttle_check(self.request)
|
||||
|
||||
def test_get_throttle_identifiers_multiple_auth(self):
|
||||
self.resource._meta.authentication = [FakeAuthentication(),
|
||||
FakeAuthentication()]
|
||||
identifiers = list(self.resource.get_throttle_identifiers(self.request))
|
||||
eq_(identifiers, ['fake'])
|
||||
|
||||
def test_should_throttle(self):
|
||||
with self.mocked_sbt as sbt:
|
||||
sbt.return_value = True
|
||||
self.throttle_expected()
|
||||
eq_(self.throttle.should_be_throttled.call_count, 1)
|
||||
|
||||
def test_shouldnt_throttle(self):
|
||||
with self.mocked_sbt as sbt:
|
||||
sbt.return_value = False
|
||||
self.no_throttle_expected()
|
||||
eq_(self.throttle.should_be_throttled.call_count, 1)
|
||||
|
||||
def test_GET_shouldnt_throttle(self):
|
||||
with self.mocked_sbt as sbt:
|
||||
sbt.return_value = True
|
||||
self.no_throttle_expected(RequestFactory().get('/'))
|
||||
eq_(self.throttle.should_be_throttled.call_count, 0)
|
||||
|
||||
def test_unthrottled_user(self):
|
||||
self.grant_permission(self.user.get_profile(), 'Apps:APIUnthrottled')
|
||||
ACLMiddleware().process_request(self.request)
|
||||
with self.mocked_sbt as sbt:
|
||||
sbt.return_value = True
|
||||
self.no_throttle_expected()
|
||||
eq_(self.throttle.should_be_throttled.call_count, 0)
|
||||
|
||||
def test_throttled_user_setting_enabled(self):
|
||||
with self.settings(API_THROTTLE=True):
|
||||
ACLMiddleware().process_request(self.request)
|
||||
with self.mocked_sbt as sbt:
|
||||
sbt.return_value = True
|
||||
self.throttle_expected()
|
||||
eq_(self.throttle.should_be_throttled.call_count, 1)
|
||||
|
||||
def test_throttled_user_setting_disabled(self):
|
||||
with self.settings(API_THROTTLE=False):
|
||||
ACLMiddleware().process_request(self.request)
|
||||
with self.mocked_sbt as sbt:
|
||||
sbt.return_value = True
|
||||
self.no_throttle_expected()
|
||||
eq_(self.throttle.should_be_throttled.call_count, 0)
|
||||
|
||||
|
||||
class FilteredCORS(CORSResource, MarketplaceResource):
|
||||
|
||||
class Meta(object):
|
||||
cors_allowed = ['get', 'put']
|
||||
|
||||
|
||||
class UnfilteredCORS(CORSResource, MarketplaceResource):
|
||||
pass
|
||||
|
||||
|
||||
class TestCORSResource(TestCase):
|
||||
|
||||
def test_filtered(self):
|
||||
request = RequestFactory().get('/')
|
||||
FilteredCORS().method_check(request, allowed=['get'])
|
||||
eq_(request.CORS, ['get', 'put'])
|
||||
|
||||
def test_unfiltered(self):
|
||||
request = RequestFactory().get('/')
|
||||
UnfilteredCORS().method_check(request, allowed=['get'])
|
||||
eq_(request.CORS, ['get'])
|
||||
@patch.object(AppViewSet, 'create')
|
||||
def test_form_encoded(self, create_mock):
|
||||
create_mock.return_value = Response()
|
||||
self.client.post(reverse('app-list'),
|
||||
data='foo=bar',
|
||||
content_type='application/x-www-form-urlencoded')
|
||||
eq_(create_mock.call_args[0][0].DATA['foo'], 'bar')
|
||||
|
||||
|
||||
class TestCORSWrapper(TestCase):
|
||||
|
@ -247,20 +72,3 @@ class TestCORSWrapper(TestCase):
|
|||
|
||||
class Form(forms.Form):
|
||||
app = forms.ChoiceField(choices=(('valid', 'valid'),))
|
||||
|
||||
|
||||
class TestAppViewSet(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.request = RequestFactory().get('/')
|
||||
self.viewset = AppViewSet()
|
||||
self.viewset.action_map = {}
|
||||
self.viewset.form = Form
|
||||
|
||||
def test_ok(self):
|
||||
self.viewset.initialize_request(self.request, pk='valid')
|
||||
ok_(self.viewset.app)
|
||||
|
||||
def test_not_ok(self):
|
||||
self.viewset.initialize_request(self.request, pk='invalid')
|
||||
eq_(self.viewset.app, None)
|
||||
|
|
|
@ -19,7 +19,6 @@ from amo.helpers import absolutify, urlparams
|
|||
from amo.urlresolvers import reverse
|
||||
|
||||
from mkt.api import authentication
|
||||
from mkt.api.base import CORSResource, MarketplaceResource
|
||||
from mkt.api.models import Access, Token, generate, REQUEST_TOKEN, ACCESS_TOKEN
|
||||
from mkt.api.tests import BaseAPI
|
||||
from mkt.site.fixtures import fixture
|
||||
|
@ -169,23 +168,6 @@ class RestOAuth(BaseOAuth):
|
|||
self.anon = RestOAuthClient(None)
|
||||
|
||||
|
||||
class Resource(CORSResource, MarketplaceResource):
|
||||
|
||||
class Meta:
|
||||
list_allowed_method = ['get']
|
||||
|
||||
|
||||
class TestCORS(BaseOAuth):
|
||||
|
||||
def setUp(self):
|
||||
self.resource = Resource()
|
||||
|
||||
def test_cors(self):
|
||||
req = RequestFactory().get('/')
|
||||
self.resource.method_check(req, allowed=['get'])
|
||||
eq_(req.CORS, ['get'])
|
||||
|
||||
|
||||
class Test3LeggedOAuthFlow(TestCase):
|
||||
fixtures = fixture('user_2519', 'user_999')
|
||||
|
||||
|
@ -214,7 +196,7 @@ class Test3LeggedOAuthFlow(TestCase):
|
|||
url, auth_header = self._oauth_request_info(
|
||||
url, client_key=self.access.key, client_secret=self.access.secret,
|
||||
resource_owner_key=t.key, resource_owner_secret=t.secret)
|
||||
auth = authentication.OAuthAuthentication()
|
||||
auth = authentication.RestOAuthAuthentication()
|
||||
req = RequestFactory().get(
|
||||
url, HTTP_HOST='testserver',
|
||||
HTTP_AUTHORIZATION=auth_header)
|
||||
|
@ -228,11 +210,11 @@ class Test3LeggedOAuthFlow(TestCase):
|
|||
url, client_key=self.access.key,
|
||||
client_secret=self.access.secret, resource_owner_key=generate(),
|
||||
resource_owner_secret=generate())
|
||||
auth = authentication.OAuthAuthentication()
|
||||
auth = authentication.RestOAuthAuthentication()
|
||||
req = RequestFactory().get(
|
||||
url, HTTP_HOST='testserver',
|
||||
HTTP_AUTHORIZATION=auth_header)
|
||||
eq_(auth.is_authenticated(req).status_code, 401)
|
||||
assert not auth.is_authenticated(req)
|
||||
|
||||
def test_get_authorize_page(self):
|
||||
t = Token.generate_new(REQUEST_TOKEN, self.access)
|
||||
|
|
|
@ -9,54 +9,15 @@ from django.utils.http import urlencode
|
|||
|
||||
import mock
|
||||
from nose.tools import eq_, ok_
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.serializers import Serializer, ValidationError
|
||||
from simplejson import JSONDecodeError
|
||||
from tastypie.exceptions import UnsupportedFormat
|
||||
from test_utils import RequestFactory
|
||||
|
||||
from mkt.api.exceptions import DeserializationError
|
||||
from mkt.api.serializers import (PotatoCaptchaSerializer, Serializer,
|
||||
URLSerializerMixin)
|
||||
from mkt.api.serializers import PotatoCaptchaSerializer, URLSerializerMixin
|
||||
from mkt.site.fixtures import fixture
|
||||
from mkt.site.tests.test_forms import PotatoCaptchaTestCase
|
||||
|
||||
|
||||
class TestSerializer(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.s = Serializer()
|
||||
|
||||
def test_json(self):
|
||||
eq_(self.s.deserialize(json.dumps({'foo': 'bar'}),
|
||||
'application/json'),
|
||||
{'foo': 'bar'})
|
||||
|
||||
def test_decimal(self):
|
||||
eq_(self.s.serialize({'foo': Decimal('5.00')}),
|
||||
json.dumps({'foo': '5.00'}))
|
||||
|
||||
def test_url(self):
|
||||
eq_(self.s.deserialize(urlencode({'foo': 'bar'}),
|
||||
'application/x-www-form-urlencoded'),
|
||||
{'foo': 'bar'})
|
||||
|
||||
eq_(self.s.deserialize(urlencode({'foo': u'baré'}),
|
||||
'application/x-www-form-urlencoded'),
|
||||
{'foo': u'baré'})
|
||||
|
||||
def test_from_url(self):
|
||||
with self.assertRaises(UnsupportedFormat):
|
||||
self.s.to_urlencode({})
|
||||
|
||||
def test_deserialization_error(self):
|
||||
try:
|
||||
self.s.deserialize('')
|
||||
except DeserializationError, e:
|
||||
self.assertIsInstance(e.original, JSONDecodeError)
|
||||
else:
|
||||
self.fail('DeserializationError not raised')
|
||||
|
||||
|
||||
class TestPotatoCaptchaSerializer(PotatoCaptchaTestCase):
|
||||
fixtures = fixture('user_999')
|
||||
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
from django.test.client import RequestFactory
|
||||
|
||||
from mock import patch
|
||||
from tastypie.exceptions import ImmediateHttpResponse
|
||||
|
||||
from mkt.api.base import HttpTooManyRequests, MarketplaceResource
|
||||
from mkt.api.tests.test_oauth import BaseOAuth
|
||||
|
||||
|
||||
class ThrottleTests(object):
|
||||
"""
|
||||
Mixin to add tests that ensure API endpoints are being appropriately
|
||||
throttled.
|
||||
|
||||
Note: subclasses will need to define the resource being tested.
|
||||
"""
|
||||
resource = None
|
||||
request = RequestFactory().post('/')
|
||||
|
||||
def test_should_throttle(self):
|
||||
if not self.resource:
|
||||
return
|
||||
|
||||
with patch.object(self.resource._meta, 'throttle') as throttle:
|
||||
throttle.should_be_throttled.return_value = True
|
||||
with self.assertImmediate(HttpTooManyRequests):
|
||||
self.resource.throttle_check(self.request)
|
||||
|
||||
def test_shouldnt_throttle(self):
|
||||
with patch.object(self, 'resource') as resource:
|
||||
resource._meta.throttle.should_be_throttled.return_value = False
|
||||
try:
|
||||
self.resource.throttle_check(self.request)
|
||||
except ImmediateHttpResponse:
|
||||
self.fail('Unthrottled request raises ImmediateHttpResponse')
|
||||
|
||||
|
||||
class TestThrottle(ThrottleTests, BaseOAuth):
|
||||
resource = MarketplaceResource()
|
|
@ -4,6 +4,7 @@ from django.conf import settings
|
|||
from django.db import models
|
||||
|
||||
import amo.models
|
||||
import mkt.carriers
|
||||
import mkt.regions
|
||||
from addons.models import Addon, Category, clean_slug
|
||||
from amo.decorators import use_master
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from nose.tools import eq_, ok_
|
||||
from rest_framework import serializers
|
||||
from tastypie.bundle import Bundle
|
||||
from test_utils import RequestFactory
|
||||
|
||||
import amo
|
||||
|
|
|
@ -23,7 +23,7 @@ from mkt.api.authorization import (AllowAppOwner, GroupPermission,
|
|||
switch)
|
||||
from mkt.api.authentication import (RestOAuthAuthentication,
|
||||
RestSharedSecretAuthentication)
|
||||
from mkt.api.base import AppViewSet, MarketplaceView
|
||||
from mkt.api.base import MarketplaceView
|
||||
from mkt.constants.payments import PAYMENT_STATUSES
|
||||
from mkt.developers.forms_payments import (BangoPaymentAccountForm,
|
||||
PaymentCheckForm)
|
||||
|
@ -37,6 +37,23 @@ from lib.pay_server import get_client
|
|||
|
||||
log = commonware.log.getLogger('z.api.payments')
|
||||
|
||||
class PaymentAppViewSet(GenericViewSet):
|
||||
|
||||
def initialize_request(self, request, *args, **kwargs):
|
||||
"""
|
||||
Pass the value in the URL through to the form defined on the
|
||||
ViewSet, which will populate the app property with the app object.
|
||||
|
||||
You must define a form which will take an app object.
|
||||
"""
|
||||
request = (super(PaymentAppViewSet, self)
|
||||
.initialize_request(request, *args, **kwargs))
|
||||
self.app = None
|
||||
form = self.form({'app': kwargs.get('pk')})
|
||||
if form.is_valid():
|
||||
self.app = form.cleaned_data['app']
|
||||
return request
|
||||
|
||||
|
||||
class PaymentAccountSerializer(Serializer):
|
||||
"""
|
||||
|
@ -249,7 +266,7 @@ class AddonPaymentAccountViewSet(CreateModelMixin, RetrieveModelMixin,
|
|||
obj.save()
|
||||
|
||||
|
||||
class PaymentCheckViewSet(AppViewSet):
|
||||
class PaymentCheckViewSet(PaymentAppViewSet):
|
||||
permission_classes = (AllowAppOwner,)
|
||||
form = PaymentCheckForm
|
||||
|
||||
|
@ -277,7 +294,7 @@ class PaymentCheckViewSet(AppViewSet):
|
|||
return Response(filtered, status=200)
|
||||
|
||||
|
||||
class PaymentDebugViewSet(AppViewSet):
|
||||
class PaymentDebugViewSet(PaymentAppViewSet):
|
||||
permission_classes = [GroupPermission('Transaction', 'Debug')]
|
||||
form = PaymentCheckForm
|
||||
|
||||
|
@ -293,3 +310,8 @@ class PaymentDebugViewSet(AppViewSet):
|
|||
'bango': res['bango'],
|
||||
}
|
||||
return Response(filtered, status=200)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -2,17 +2,22 @@ import json
|
|||
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from django import forms
|
||||
|
||||
from curling.lib import HttpClientError, HttpServerError
|
||||
from mock import Mock, patch
|
||||
from nose.tools import eq_, ok_
|
||||
|
||||
from test_utils import RequestFactory
|
||||
|
||||
import amo
|
||||
from addons.models import AddonUpsell, AddonUser
|
||||
from amo.tests import app_factory, TestCase
|
||||
from market.models import AddonPremium, Price
|
||||
|
||||
from mkt.api.tests.test_oauth import RestOAuth
|
||||
from mkt.developers.api_payments import AddonPaymentAccountSerializer
|
||||
from mkt.developers.api_payments import (AddonPaymentAccountSerializer,
|
||||
PaymentAppViewSet)
|
||||
from mkt.developers.models import (AddonPaymentAccount, PaymentAccount,
|
||||
SolitudeSeller)
|
||||
from mkt.developers.tests.test_providers import Patcher
|
||||
|
@ -465,3 +470,28 @@ class TestPaymentDebug(AccountCase, RestOAuth):
|
|||
res = self.client.get(self.list_url)
|
||||
eq_(res.status_code, 200)
|
||||
eq_(res.json['bango']['environment'], 'dev')
|
||||
|
||||
class Form(forms.Form):
|
||||
app = forms.ChoiceField(choices=(('valid', 'valid'),))
|
||||
|
||||
class TestPaymentAppViewSet(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.request = RequestFactory().get('/')
|
||||
self.viewset = PaymentAppViewSet()
|
||||
self.viewset.action_map = {}
|
||||
self.viewset.form = Form
|
||||
|
||||
def test_ok(self):
|
||||
self.viewset.initialize_request(self.request, pk='valid')
|
||||
ok_(self.viewset.app)
|
||||
|
||||
def test_not_ok(self):
|
||||
self.viewset.initialize_request(self.request, pk='invalid')
|
||||
eq_(self.viewset.app, None)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
from mkt.api.base import MarketplaceResource
|
||||
|
||||
|
||||
class RegionResource(MarketplaceResource):
|
||||
|
||||
class Meta(MarketplaceResource.Meta):
|
||||
allowed_methods = []
|
||||
fields = ('name', 'slug', 'mcc', 'adolescent')
|
||||
resource_name = 'region'
|
||||
include_resource_uri = False
|
||||
|
||||
def full_dehydrate(self, bundle):
|
||||
bundle.data = {}
|
||||
for field in self._meta.fields:
|
||||
bundle.data[field] = getattr(bundle.obj, field)
|
||||
return bundle
|
|
@ -1,23 +0,0 @@
|
|||
from nose.tools import eq_
|
||||
|
||||
from tastypie.bundle import Bundle
|
||||
|
||||
import amo
|
||||
import amo.tests
|
||||
|
||||
from mkt.constants.regions import REGIONS_DICT as regions
|
||||
from mkt.regions.api import RegionResource
|
||||
|
||||
|
||||
class TestRegionResource(amo.tests.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.app = amo.tests.app_factory()
|
||||
self.resource = RegionResource()
|
||||
self.bundle = Bundle(obj=regions['us'], request=None)
|
||||
|
||||
def test_full_dehydrate(self):
|
||||
res = self.resource.full_dehydrate(self.bundle)
|
||||
eq_(res.obj, regions['us'])
|
||||
for field in self.resource._meta.fields:
|
||||
eq_(res.data[field], getattr(res.obj, field))
|
|
@ -201,7 +201,7 @@ class TestApi(RestOAuth, ESTestCase):
|
|||
req.amo_user = user
|
||||
return True
|
||||
|
||||
with patch('mkt.api.authentication.SharedSecretAuthentication'
|
||||
with patch('mkt.api.authentication.RestSharedSecretAuthentication'
|
||||
'.is_authenticated', fakeauth):
|
||||
with self.settings(SITE_URL=''):
|
||||
self.create()
|
||||
|
@ -215,7 +215,7 @@ class TestApi(RestOAuth, ESTestCase):
|
|||
req.user = AnonymousUser()
|
||||
return True
|
||||
|
||||
with patch('mkt.api.authentication.SharedSecretAuthentication'
|
||||
with patch('mkt.api.authentication.RestSharedSecretAuthentication'
|
||||
'.is_authenticated', fakeauth):
|
||||
with self.settings(SITE_URL=''):
|
||||
self.create()
|
||||
|
|
|
@ -36,10 +36,6 @@ log = logging.getLogger('z.mkt.site')
|
|||
# {% csrf_token %}.
|
||||
@requires_csrf_token
|
||||
def handler403(request):
|
||||
# NOTE: The mkt.api uses Tastypie which has its own mechanism for
|
||||
# triggering 403s. If we ever end up calling PermissionDenied there, we'll
|
||||
# need something here similar to the 404s and 500s.
|
||||
#
|
||||
# TODO: Bug 793241 for different 403 templates at different URL paths.
|
||||
return jingo.render(request, 'site/403.html', status=403)
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ from users.models import UserProfile
|
|||
|
||||
import mkt
|
||||
from mkt.regions import REGIONS_CHOICES_ID_DICT
|
||||
from mkt.regions.api import RegionResource
|
||||
|
||||
log = commonware.log.getLogger('z.webapps')
|
||||
|
||||
|
@ -129,11 +128,15 @@ def es_app_to_dict(obj, region=None, profile=None, request=None):
|
|||
|
||||
if not data['public_stats']:
|
||||
data['weekly_downloads'] = None
|
||||
|
||||
data['regions'] = RegionResource().dehydrate_objects(
|
||||
map(REGIONS_CHOICES_ID_DICT.get,
|
||||
app.get_region_ids(worldwide=True,
|
||||
excluded=obj.region_exclusions)))
|
||||
def serialize_region(o):
|
||||
d = {}
|
||||
for field in ('name', 'slug', 'mcc', 'adolescent'):
|
||||
d[field] = getattr(o, field, None)
|
||||
return d
|
||||
data['regions'] = [serialize_region(REGIONS_CHOICES_ID_DICT.get(k))
|
||||
for k in app.get_region_ids(
|
||||
worldwide=True,
|
||||
excluded=obj.region_exclusions)]
|
||||
|
||||
if src.get('premium_type') in amo.ADDON_PREMIUMS:
|
||||
acct = list(AddonPaymentAccount.objects.filter(addon=app))
|
||||
|
|
|
@ -39,7 +39,6 @@ djangorestframework==2.3.9
|
|||
#django-session-csrf==0.6
|
||||
django-statsd-mozilla==0.3.8.6
|
||||
django-storages==1.1.8
|
||||
django-tastypie==0.9.11
|
||||
django-waffle==0.9.1
|
||||
easy-thumbnails==1.3
|
||||
elasticutils==0.8.1
|
||||
|
|
Загрузка…
Ссылка в новой задаче