Пример #1
0
 def setUp(self):
     self.get_permission = Mock
     self.patch_permission = Mock
     self.post_permission = Mock
     self.put_permission = Mock
     self.permission = ByHttpMethod({
         'get': self.get_permission,
     })
     self.set_permission_mock('get', True)
Пример #2
0
class TestByHttpMethod(TestCase):
    def setUp(self):
        self.get_permission = Mock
        self.patch_permission = Mock
        self.post_permission = Mock
        self.put_permission = Mock
        self.permission = ByHttpMethod({
            'get': self.get_permission,
        })
        self.set_permission_mock('get', True)

    def set_permission_mock(self, method, value):
        mock = self.permission.method_permissions[method]
        mock.has_permission.return_value = value

    def set_object_permission_mock(self, method, value):
        mock = self.permission.method_permissions[method]
        mock.has_object_permission.return_value = value

    def test_get(self):
        self.request = RequestFactory().get('/')
        assert self.permission.has_permission(self.request, 'myview') is True
        self.set_permission_mock('get', False)
        assert self.permission.has_permission(self.request, 'myview') is False

    def test_get_obj(self):
        obj = Mock(spec=[])
        self.request = RequestFactory().get('/')
        self.set_object_permission_mock('get', True)
        assert self.permission.has_object_permission(self.request, 'myview',
                                                     obj) is True

        self.set_object_permission_mock('get', False)
        assert self.permission.has_object_permission(self.request, 'myview',
                                                     obj) is False

    def test_missing_method(self):
        self.request = RequestFactory().post('/')
        with self.assertRaises(MethodNotAllowed):
            self.permission.has_permission(self.request, 'myview')

        obj = Mock(spec=[])
        self.request = RequestFactory().post('/')
        with self.assertRaises(MethodNotAllowed):
            self.permission.has_object_permission(self.request, 'myview', obj)

        self.request = RequestFactory().options('/')
        with self.assertRaises(MethodNotAllowed):
            self.permission.has_permission(self.request, 'myview')
Пример #3
0
class TestByHttpMethod(TestCase):
    def setUp(self):
        self.get_permission = Mock
        self.patch_permission = Mock
        self.post_permission = Mock
        self.put_permission = Mock
        self.permission = ByHttpMethod({
            'get': self.get_permission,
        })
        self.set_permission_mock('get', True)

    def set_permission_mock(self, method, value):
        mock = self.permission.method_permissions[method]
        mock.has_permission.return_value = value

    def set_object_permission_mock(self, method, value):
        mock = self.permission.method_permissions[method]
        mock.has_object_permission.return_value = value

    def test_get(self):
        self.request = RequestFactory().get('/')
        assert self.permission.has_permission(self.request, 'myview') is True
        self.set_permission_mock('get', False)
        assert self.permission.has_permission(self.request, 'myview') is False

    def test_get_obj(self):
        obj = Mock()
        self.request = RequestFactory().get('/')
        self.set_object_permission_mock('get', True)
        assert self.permission.has_object_permission(
            self.request, 'myview', obj) is True

        self.set_object_permission_mock('get', False)
        assert self.permission.has_object_permission(
            self.request, 'myview', obj) is False

    def test_missing_method(self):
        self.request = RequestFactory().post('/')
        with self.assertRaises(MethodNotAllowed):
            self.permission.has_permission(self.request, 'myview')

        obj = Mock()
        self.request = RequestFactory().post('/')
        with self.assertRaises(MethodNotAllowed):
            self.permission.has_object_permission(self.request, 'myview', obj)

        self.request = RequestFactory().options('/')
        with self.assertRaises(MethodNotAllowed):
            self.permission.has_permission(self.request, 'myview')
Пример #4
0
class SessionView(APIView):
    permission_classes = [
        ByHttpMethod({
            'options': AllowAny,  # Needed for CORS.
            'delete': IsAuthenticated,
        }),
    ]

    def options(self, request, *args, **kwargs):
        response = Response()
        response['Content-Length'] = '0'
        origin = request.META.get('HTTP_ORIGIN')
        if not origin:
            return response
        response[ACCESS_CONTROL_ALLOW_ORIGIN] = origin
        response[ACCESS_CONTROL_ALLOW_CREDENTIALS] = 'true'
        # Mimics the django-cors-headers middleware.
        response[ACCESS_CONTROL_ALLOW_HEADERS] = ', '.join(
            corsheaders_conf.CORS_ALLOW_HEADERS)
        response[ACCESS_CONTROL_ALLOW_METHODS] = ', '.join(
            corsheaders_conf.CORS_ALLOW_METHODS)
        if corsheaders_conf.CORS_PREFLIGHT_MAX_AGE:
            response[
                ACCESS_CONTROL_MAX_AGE] = corsheaders_conf.CORS_PREFLIGHT_MAX_AGE
        return response

    def delete(self, request, *args, **kwargs):
        response = Response({'ok': True})
        logout_user(request, response)
        origin = request.META.get('HTTP_ORIGIN')
        if not origin:
            return response
        response[ACCESS_CONTROL_ALLOW_ORIGIN] = origin
        response[ACCESS_CONTROL_ALLOW_CREDENTIALS] = 'true'
        return response
 def setUp(self):
     self.get_permission = Mock
     self.patch_permission = Mock
     self.post_permission = Mock
     self.put_permission = Mock
     self.permission = ByHttpMethod({
         'get': self.get_permission,
     })
     self.set_permission_mock('get', True)
Пример #6
0
class AccountViewSet(RetrieveModelMixin, UpdateModelMixin, GenericViewSet):
    permission_classes = [
        ByHttpMethod({
            'get':
            AllowAny,
            'head':
            AllowAny,
            'options':
            AllowAny,  # Needed for CORS.
            # To edit a profile it has to yours, or be an admin.
            'patch':
            AnyOf(AllowSelf, GroupPermission(amo.permissions.USERS_EDIT)),
        }),
    ]
    queryset = UserProfile.objects.all()

    def get_object(self):
        if hasattr(self, 'instance'):
            return self.instance
        identifier = self.kwargs.get('pk')
        self.lookup_field = self.get_lookup_field(identifier)
        self.kwargs[self.lookup_field] = identifier
        self.instance = super(AccountViewSet, self).get_object()
        return self.instance

    def get_lookup_field(self, identifier):
        lookup_field = 'pk'
        if identifier and not identifier.isdigit():
            # If the identifier contains anything other than a digit, it's
            # the username.
            lookup_field = 'username'
        return lookup_field

    @property
    def self_view(self):
        return (self.request.user.is_authenticated()
                and self.get_object() == self.request.user)

    def get_serializer_class(self):
        if (self.self_view or acl.action_allowed_user(
                self.request.user, amo.permissions.USERS_EDIT)):
            return UserProfileSerializer
        else:
            return PublicUserProfileSerializer

    @list_route(permission_classes=[IsAuthenticated])
    def profile(self, request, *args, **kwargs):
        self.kwargs['pk'] = unicode(self.request.user.pk)
        return self.retrieve(request, *args, **kwargs)
Пример #7
0
class AccountViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin,
                     GenericViewSet):
    permission_classes = [
        ByHttpMethod({
            'get':
            AllowAny,
            'head':
            AllowAny,
            'options':
            AllowAny,  # Needed for CORS.
            # To edit a profile it has to yours, or be an admin.
            'patch':
            AnyOf(AllowSelf, GroupPermission(amo.permissions.USERS_EDIT)),
            'delete':
            AnyOf(AllowSelf, GroupPermission(amo.permissions.USERS_EDIT)),
        }),
    ]

    def get_queryset(self):
        return UserProfile.objects.all()

    def get_object(self):
        if hasattr(self, 'instance'):
            return self.instance
        identifier = self.kwargs.get('pk')
        self.lookup_field = self.get_lookup_field(identifier)
        self.kwargs[self.lookup_field] = identifier
        self.instance = super(AccountViewSet, self).get_object()
        return self.instance

    def get_lookup_field(self, identifier):
        lookup_field = 'pk'
        if identifier and not identifier.isdigit():
            # If the identifier contains anything other than a digit, it's
            # the username.
            lookup_field = 'username'
        return lookup_field

    @property
    def self_view(self):
        return (self.request.user.is_authenticated()
                and self.get_object() == self.request.user)

    def get_serializer_class(self):
        if (self.self_view or acl.action_allowed_user(
                self.request.user, amo.permissions.USERS_EDIT)):
            return UserProfileSerializer
        else:
            return PublicUserProfileSerializer

    def perform_destroy(self, instance):
        if instance.is_developer:
            raise serializers.ValidationError(
                ugettext(
                    u'Developers of add-ons or themes cannot delete their '
                    u'account. You must delete all add-ons and themes linked to '
                    u'this account, or transfer them to other users.'))
        return super(AccountViewSet, self).perform_destroy(instance)

    @detail_route(methods=['delete'],
                  permission_classes=[
                      AnyOf(AllowSelf,
                            GroupPermission(amo.permissions.USERS_EDIT))
                  ])
    def picture(self, request, pk=None):
        user = self.get_object()
        user.update(picture_type='')
        log.debug(u'User (%s) deleted photo' % user)
        tasks.delete_photo.delay(user.picture_path)
        return self.retrieve(request)
Пример #8
0
class AccountViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin,
                     GenericViewSet):
    permission_classes = [
        ByHttpMethod({
            'get':
            AllowAny,
            'head':
            AllowAny,
            'options':
            AllowAny,  # Needed for CORS.
            # To edit a profile it has to yours, or be an admin.
            'patch':
            AnyOf(AllowSelf, GroupPermission(amo.permissions.USERS_EDIT)),
            'delete':
            AnyOf(AllowSelf, GroupPermission(amo.permissions.USERS_EDIT)),
        }),
    ]
    # Periods are not allowed in username, but we still have some in the
    # database so relax the lookup regexp to allow them to load their profile.
    lookup_value_regex = '[^/]+'

    def get_queryset(self):
        return UserProfile.objects.exclude(deleted=True).all()

    def get_object(self):
        if hasattr(self, 'instance'):
            return self.instance
        identifier = self.kwargs.get('pk')
        self.lookup_field = self.get_lookup_field(identifier)
        self.kwargs[self.lookup_field] = identifier
        self.instance = super(AccountViewSet, self).get_object()
        # action won't exist for other classes that are using this ViewSet.
        can_view_instance = (not getattr(self, 'action', None)
                             or self.self_view or self.admin_viewing
                             or self.instance.is_public)
        if can_view_instance:
            return self.instance
        else:
            raise Http404

    def get_lookup_field(self, identifier):
        lookup_field = 'pk'
        if identifier and not identifier.isdigit():
            # If the identifier contains anything other than a digit, it's
            # the username.
            lookup_field = 'username'
        return lookup_field

    @property
    def self_view(self):
        return (self.request.user.is_authenticated
                and self.get_object() == self.request.user)

    @property
    def admin_viewing(self):
        return acl.action_allowed_user(self.request.user,
                                       amo.permissions.USERS_EDIT)

    def get_serializer_class(self):
        if self.self_view or self.admin_viewing:
            return UserProfileSerializer
        else:
            return PublicUserProfileSerializer

    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        self.perform_destroy(instance)
        response = Response(status=HTTP_204_NO_CONTENT)
        if instance == request.user:
            logout_user(request, response)
        return response

    @action(
        detail=True,
        methods=['delete'],
        permission_classes=[
            AnyOf(AllowSelf, GroupPermission(amo.permissions.USERS_EDIT))
        ],
    )
    def picture(self, request, pk=None):
        user = self.get_object()
        user.delete_picture()
        log.info('User (%s) deleted photo' % user)
        return self.retrieve(request)
Пример #9
0
class RatingViewSet(AddonChildMixin, ModelViewSet):
    serializer_class = RatingSerializer
    permission_classes = [
        ByHttpMethod({
            'get': AllowAny,
            'head': AllowAny,
            'options': AllowAny,  # Needed for CORS.

            # Deletion requires a specific permission check.
            'delete': CanDeleteRatingPermission,

            # To post a rating you just need to be authenticated.
            'post': IsAuthenticated,

            # To edit a rating you need to be the author or be an admin.
            'patch': AnyOf(AllowOwner, GroupPermission(
                amo.permissions.ADDONS_EDIT)),

            # Implementing PUT would be a little incoherent as we don't want to
            # allow users to change `version` but require it at creation time.
            # So only PATCH is allowed for editing.
        }),
    ]
    reply_permission_classes = [AnyOf(
        GroupPermission(amo.permissions.ADDONS_EDIT),
        AllowRelatedObjectPermissions('addon', [AllowAddonAuthor]),
    )]
    reply_serializer_class = RatingSerializerReply
    throttle_classes = (RatingThrottle,)

    def set_addon_object_from_rating(self, rating):
        """Set addon object on the instance from a rating object."""
        # At this point it's likely we didn't have an addon in the request, so
        # if we went through get_addon_object() before it's going to be set
        # to None already. We delete the addon_object property cache and set
        # addon_pk in kwargs to force get_addon_object() to reset
        # self.addon_object.
        del self.addon_object
        self.kwargs['addon_pk'] = str(rating.addon.pk)
        return self.get_addon_object()

    def get_addon_object(self):
        """Return addon object associated with the request, or None if not
        relevant.

        Will also fire permission checks on the addon object when it's loaded.
        """
        if hasattr(self, 'addon_object'):
            return self.addon_object

        if 'addon_pk' not in self.kwargs:
            self.kwargs['addon_pk'] = (
                self.request.data.get('addon') or
                self.request.GET.get('addon'))
        if not self.kwargs['addon_pk']:
            # If we don't have an addon object, set it as None on the instance
            # and return immediately, that's fine.
            self.addon_object = None
            return
        else:
            # AddonViewSet.get_lookup_field() expects a string.
            self.kwargs['addon_pk'] = force_text(self.kwargs['addon_pk'])
        # When loading the add-on, pass a specific permission class - the
        # default from AddonViewSet is too restrictive, we are not modifying
        # the add-on itself so we don't need all the permission checks it does.
        return super(RatingViewSet, self).get_addon_object(
            permission_classes=[AllowIfPublic])

    def check_permissions(self, request):
        """Perform permission checks.

        The regular DRF permissions checks are made, but also, before that, if
        an addon was requested, verify that it exists, is public and listed,
        through AllowIfPublic permission, that get_addon_object() uses."""
        self.get_addon_object()

        # Proceed with the regular permission checks.
        return super(RatingViewSet, self).check_permissions(request)

    def get_serializer(self, *args, **kwargs):
        if self.action in ('partial_update', 'update'):
            instance = args[0]
            if instance.reply_to is not None:
                self.rating_object = instance.reply_to
                self.serializer_class = self.reply_serializer_class
        return super(RatingViewSet, self).get_serializer(*args, **kwargs)

    def filter_queryset(self, qs):
        if self.action == 'list':
            addon_identifier = self.request.GET.get('addon')
            user_identifier = self.request.GET.get('user')
            version_identifier = self.request.GET.get('version')
            if addon_identifier:
                qs = qs.filter(addon=self.get_addon_object())
            if user_identifier:
                try:
                    user_identifier = int(user_identifier)
                except ValueError:
                    raise ParseError('user parameter should be an integer.')
                qs = qs.filter(user=user_identifier)
            if version_identifier:
                try:
                    version_identifier = int(version_identifier)
                except ValueError:
                    raise ParseError('version parameter should be an integer.')
                qs = qs.filter(version=version_identifier)
            elif addon_identifier:
                # When filtering on addon but not on version, only return the
                # latest rating posted by each user.
                qs = qs.filter(is_latest=True)
            if not addon_identifier and not user_identifier:
                # Don't allow listing ratings without filtering by add-on or
                # user.
                raise ParseError('Need an addon or user parameter')
            if user_identifier and addon_identifier and version_identifier:
                # When user, addon and version identifiers are set, we are
                # effectively only looking for one or zero objects. Fake
                # pagination in that case, avoiding all count() calls and
                # therefore related cache-machine invalidation issues. Needed
                # because the frontend wants to call this before and after
                # having posted a new rating, and needs accurate results.
                self.pagination_class = OneOrZeroPageNumberPagination
        return super(RatingViewSet, self).filter_queryset(qs)

    def get_paginated_response(self, data):
        response = super(RatingViewSet, self).get_paginated_response(data)
        if 'show_grouped_ratings' in self.request.GET:
            try:
                show_grouped_ratings = (
                    serializers.BooleanField().to_internal_value(
                        self.request.GET['show_grouped_ratings']))
            except serializers.ValidationError:
                raise ParseError(
                    'show_grouped_ratings parameter should be a boolean')
            if show_grouped_ratings and self.get_addon_object():
                response.data['grouped_ratings'] = dict(GroupedRating.get(
                    self.addon_object.id))
        return response

    def get_queryset(self):
        requested = self.request.GET.get('filter', '').split(',')
        has_addons_edit = acl.action_allowed(self.request,
                                             amo.permissions.ADDONS_EDIT)

        # Add this as a property of the view, because we need to pass down the
        # information to the serializer to show/hide delete replies.
        if not hasattr(self, 'should_access_deleted_ratings'):
            self.should_access_deleted_ratings = (
                ('with_deleted' in requested or self.action != 'list') and
                self.request.user.is_authenticated() and
                has_addons_edit)

        should_access_only_top_level_ratings = (
            self.action == 'list' and self.get_addon_object())

        if self.should_access_deleted_ratings:
            # For admins or add-on authors replying. When listing, we include
            # deleted ratings but still filter out out replies, because they'll
            # be in the serializer anyway. For other actions, we simply remove
            # any filtering, allowing them to access any rating out of the box
            # with no extra parameter needed.
            if self.action == 'list':
                queryset = Rating.unfiltered.filter(reply_to__isnull=True)
            else:
                queryset = Rating.unfiltered.all()
        elif should_access_only_top_level_ratings:
            # When listing add-on ratings, exclude replies, they'll be
            # included during serialization as children of the relevant
            # ratings instead.
            queryset = Rating.without_replies.all()
        else:
            queryset = Rating.objects.all()

        # Filter out empty ratings if specified.
        # Should the users own empty ratings be filtered back in?
        if 'with_yours' in requested and self.request.user.is_authenticated():
            user_filter = Q(user=self.request.user.pk)
        else:
            user_filter = Q()
        # Apply the filter(s)
        if 'without_empty_body' in requested:
            queryset = queryset.filter(~Q(body=None) | user_filter)

        # The serializer needs reply, version and user. We don't need much
        # for version and user, so we can make joins with select_related(),
        # but for replies additional queries will be made for translations
        # anyway so we're better off using prefetch_related() to make a
        # separate query to fetch them all.
        queryset = queryset.select_related('version', 'user')
        replies_qs = Rating.unfiltered.select_related('user')
        return queryset.prefetch_related(
            Prefetch('reply', queryset=replies_qs))

    @detail_route(
        methods=['post'], permission_classes=reply_permission_classes,
        serializer_class=reply_serializer_class,
        throttle_classes=[RatingReplyThrottle])
    def reply(self, *args, **kwargs):
        # A reply is just like a regular post, except that we set the reply
        # FK to the current rating object and only allow add-on authors/admins.
        # Call get_object() to trigger 404 if it does not exist.
        self.rating_object = self.get_object()
        self.set_addon_object_from_rating(self.rating_object)
        if Rating.unfiltered.filter(reply_to=self.rating_object).exists():
            # A reply already exists, just edit it.
            # We set should_access_deleted_ratings so that it works even if
            # the reply has been deleted.
            self.kwargs['pk'] = kwargs['pk'] = self.rating_object.reply.pk
            self.should_access_deleted_ratings = True
            return self.partial_update(*args, **kwargs)
        return self.create(*args, **kwargs)

    @detail_route(methods=['post'], throttle_classes=[])
    def flag(self, request, *args, **kwargs):
        # We load the add-on object from the rating to trigger permission
        # checks.
        self.rating_object = self.get_object()
        self.set_addon_object_from_rating(self.rating_object)

        # Re-use flag view since it's already returning json. We just need to
        # pass it the addon slug (passing it the PK would result in a redirect)
        # and make sure request.POST is set with whatever data was sent to the
        # DRF view.
        request._request.POST = request.data
        request = request._request
        response = flag(request, self.addon_object.slug, kwargs.get('pk'))
        if response.status_code == 200:
            # 202 is a little better than 200: we're accepting the request, but
            # make no promises to act on it :)
            response.status_code = 202
        return response

    def perform_destroy(self, instance):
        instance.delete(user_responsible=self.request.user)
Пример #10
0
class ReviewViewSet(AddonChildMixin, ModelViewSet):
    serializer_class = ReviewSerializer
    permission_classes = [
        ByHttpMethod({
            'get': AllowAny,
            'head': AllowAny,
            'options': AllowAny,  # Needed for CORS.

            # Deletion requires a specific permission check.
            'delete': CanDeleteReviewPermission,

            # To post a review you just need to be authenticated.
            'post': IsAuthenticated,

            # To edit a review you need to be the author or be an admin.
            'patch': AnyOf(AllowOwner, GroupPermission('Addons', 'Edit')),

            # Implementing PUT would be a little incoherent as we don't want to
            # allow users to change `version` but require it at creation time.
            # So only PATCH is allowed for editing.
        }),
    ]
    reply_permission_classes = [AnyOf(
        GroupPermission('Addons', 'Edit'),
        AllowRelatedObjectPermissions('addon', [AllowAddonAuthor]),
    )]
    reply_serializer_class = ReviewSerializerReply

    queryset = Review.objects.all()

    def get_addon_object(self):
        # When loading the add-on, pass a specific permission class - the
        # default from AddonViewSet is too restrictive, we are not modifying
        # the add-on itself so we don't need all the permission checks it does.
        return super(ReviewViewSet, self).get_addon_object(
            permission_classes=[AllowIfReviewedAndListed])

    def check_permissions(self, request):
        if 'addon_pk' in self.kwargs:
            # In addition to the regular permission checks that are made, we
            # need to verify that the add-on exists, is public and listed. Just
            # loading the addon should be enough to do that, since
            # AddonChildMixin implementation calls AddonViewSet.get_object().
            self.get_addon_object()

        # Proceed with the regular permission checks.
        return super(ReviewViewSet, self).check_permissions(request)

    def get_serializer(self, *args, **kwargs):
        if self.action in ('partial_update', 'update'):
            instance = args[0]
            if instance.reply_to is not None:
                self.review_object = instance.reply_to
                self.serializer_class = self.reply_serializer_class
        return super(ReviewViewSet, self).get_serializer(*args, **kwargs)

    def filter_queryset(self, qs):
        if self.action == 'list':
            if 'addon_pk' in self.kwargs:
                qs = qs.filter(is_latest=True, addon=self.get_addon_object())
            elif 'account_pk' in self.kwargs:
                qs = qs.filter(user=self.kwargs.get('account_pk'))
            else:
                # Don't allow listing reviews without filtering by add-on or
                # user.
                raise ParseError('Need an addon or user identifier')
        return qs

    def get_paginated_response(self, data):
        response = super(ReviewViewSet, self).get_paginated_response(data)
        show_grouped_ratings = self.request.GET.get('show_grouped_ratings')
        if 'addon_pk' in self.kwargs and show_grouped_ratings:
            response.data['grouped_ratings'] = dict(GroupedRating.get(
                self.addon_object.id))
        return response

    def get_queryset(self):
        requested = self.request.GET.get('filter')

        # Add this as a property of the view, because we need to pass down the
        # information to the serializer to show/hide delete replies.
        self.should_access_deleted_reviews = (
            (requested == 'with_deleted' or self.action != 'list') and
            self.request.user.is_authenticated() and
            acl.action_allowed(self.request, 'Addons', 'Edit'))

        should_access_only_replies = (
            self.action == 'list' and self.kwargs.get('addon_pk'))

        if self.should_access_deleted_reviews:
            # For admins. When listing, we include deleted reviews but still
            # filter out out replies, because they'll be in the serializer
            # anyway. For other actions, we simply remove any filtering,
            # allowing them to access any review out of the box with no
            # extra parameter needed.
            if self.action == 'list':
                self.queryset = Review.unfiltered.filter(reply_to__isnull=True)
            else:
                self.queryset = Review.unfiltered.all()
        elif should_access_only_replies:
            # When listing add-on reviews, exclude replies, they'll be
            # included during serialization as children of the relevant
            # reviews instead.
            self.queryset = Review.without_replies.all()

        qs = super(ReviewViewSet, self).get_queryset()
        if self.action in ('list', 'retrieve'):
            # Also avoid loading addon since we don't need it, we already
            # loaded it for permission checks through the pk specified in the
            # URL. Don't do it for write operations to avoid a bug in django
            # 1.8 and signals (https://github.com/django/django/pull/7274)
            qs = qs.defer('addon')
        # The serializer needs user, reply and version, so use
        # prefetch_related() to avoid extra queries (avoid select_related() as
        # we need crazy joins already).
        return qs.prefetch_related('reply', 'user', 'version')

    @detail_route(
        methods=['post'], permission_classes=reply_permission_classes,
        serializer_class=reply_serializer_class)
    def reply(self, *args, **kwargs):
        # A reply is just like a regular post, except that we set the reply
        # FK to the current review object and only allow add-on authors/admins.
        # Call get_object() to trigger 404 if it does not exist.
        self.review_object = self.get_object()
        return self.create(*args, **kwargs)

    @detail_route(methods=['post'])
    def flag(self, request, *args, **kwargs):
        # Re-use flag view since it's already returning json. We just need to
        # pass it the addon slug (passing it the PK would result in a redirect)
        # and make sure request.POST is set with whatever data was sent to the
        # DRF view.
        addon = self.get_addon_object()
        request._request.POST = request.data
        request = request._request
        response = flag(request, addon.slug, kwargs.get('pk'))
        if response.status_code == 200:
            response.status_code = 202
        return response
Пример #11
0
class RatingViewSet(AddonChildMixin, ModelViewSet):
    serializer_class = RatingSerializer
    permission_classes = [
        ByHttpMethod({
            'get':
            AllowAny,
            'head':
            AllowAny,
            'options':
            AllowAny,  # Needed for CORS.
            # Deletion requires a specific permission check.
            'delete':
            CanDeleteRatingPermission,
            # To post a rating you just need to be authenticated.
            'post':
            IsAuthenticated,
            # To edit a rating you need to be the author or be an admin.
            'patch':
            AnyOf(AllowOwner, GroupPermission(amo.permissions.ADDONS_EDIT)),
            # Implementing PUT would be a little incoherent as we don't want to
            # allow users to change `version` but require it at creation time.
            # So only PATCH is allowed for editing.
        }),
    ]
    reply_permission_classes = [
        AnyOf(
            GroupPermission(amo.permissions.ADDONS_EDIT),
            AllowRelatedObjectPermissions('addon', [AllowAddonAuthor]),
        )
    ]
    reply_serializer_class = RatingSerializerReply
    flag_permission_classes = [AllowNotOwner]
    throttle_classes = (
        RatingUserThrottle,
        RatingIPThrottle,
        RatingEditDeleteIPThrottle,
        RatingEditDeleteUserThrottle,
    )

    def set_addon_object_from_rating(self, rating):
        """Set addon object on the instance from a rating object."""
        # At this point it's likely we didn't have an addon in the request, so
        # if we went through get_addon_object() before it's going to be set
        # to None already. We delete the addon_object property cache and set
        # addon_pk in kwargs to force get_addon_object() to reset
        # self.addon_object.
        del self.addon_object
        self.kwargs['addon_pk'] = str(rating.addon.pk)
        return self.get_addon_object()

    def get_addon_object(self):
        """Return addon object associated with the request, or None if not
        relevant.

        Will also fire permission checks on the addon object when it's loaded.
        """
        if hasattr(self, 'addon_object'):
            return self.addon_object

        if 'addon_pk' not in self.kwargs:
            self.kwargs['addon_pk'] = self.request.data.get(
                'addon') or self.request.GET.get('addon')
        if not self.kwargs['addon_pk']:
            # If we don't have an addon object, set it as None on the instance
            # and return immediately, that's fine.
            self.addon_object = None
            return
        else:
            # AddonViewSet.get_lookup_field() expects a string.
            self.kwargs['addon_pk'] = force_str(self.kwargs['addon_pk'])
        # When loading the add-on, pass a specific permission class - the
        # default from AddonViewSet is too restrictive, we are not modifying
        # the add-on itself so we don't need all the permission checks it does.
        return super().get_addon_object(permission_classes=[AllowIfPublic])

    def should_include_flags(self):
        if not hasattr(self, '_should_include_flags'):
            request = self.request
            self._should_include_flags = (
                'show_flags_for' in request.GET
                and not is_gate_active(request, 'del-ratings-flags'))
            if self._should_include_flags:
                # Check the parameter was sent correctly
                try:
                    show_flags_for = serializers.IntegerField(
                    ).to_internal_value(request.GET['show_flags_for'])
                    if show_flags_for != request.user.pk:
                        raise serializers.ValidationError
                except serializers.ValidationError:
                    raise ParseError(
                        'show_flags_for parameter value should be equal to '
                        'the user id of the authenticated user')
        return self._should_include_flags

    def check_permissions(self, request):
        """Perform permission checks.

        The regular DRF permissions checks are made, but also, before that, if
        an addon was requested, verify that it exists, is public and listed,
        through AllowIfPublic permission, that get_addon_object() uses."""
        self.get_addon_object()

        # Proceed with the regular permission checks.
        return super().check_permissions(request)

    def get_serializer(self, *args, **kwargs):
        if self.action in ('partial_update', 'update'):
            instance = args[0]
            if instance.reply_to is not None:
                self.rating_object = instance.reply_to
                self.serializer_class = self.reply_serializer_class
        return super().get_serializer(*args, **kwargs)

    def filter_queryset(self, qs):
        if self.action == 'list':
            addon_identifier = self.request.GET.get('addon')
            user_identifier = self.request.GET.get('user')
            version_identifier = self.request.GET.get('version')
            score_filter = (self.request.GET.get('score') if is_gate_active(
                self.request, 'ratings-score-filter') else None)
            exclude_ratings = self.request.GET.get('exclude_ratings')
            if addon_identifier:
                qs = qs.filter(addon=self.get_addon_object())
            if user_identifier:
                try:
                    user_identifier = int(user_identifier)
                except ValueError:
                    raise ParseError('user parameter should be an integer.')
                qs = qs.filter(user=user_identifier)
            if version_identifier:
                try:
                    version_identifier = int(version_identifier)
                except ValueError:
                    raise ParseError('version parameter should be an integer.')
                qs = qs.filter(version=version_identifier)
            elif addon_identifier:
                # When filtering on addon but not on version, only return the
                # latest rating posted by each user.
                qs = qs.filter(is_latest=True)
            if not addon_identifier and not user_identifier:
                # Don't allow listing ratings without filtering by add-on or
                # user.
                raise ParseError('Need an addon or user parameter')
            if user_identifier and addon_identifier and version_identifier:
                # When user, addon and version identifiers are set, we are
                # effectively only looking for one or zero objects. Fake
                # pagination in that case, avoiding all count() calls and
                # therefore related cache-machine invalidation issues. Needed
                # because the frontend wants to call this before and after
                # having posted a new rating, and needs accurate results.
                self.pagination_class = OneOrZeroPageNumberPagination
            if score_filter:
                try:
                    scores = [int(score) for score in score_filter.split(',')]
                except ValueError:
                    raise ParseError(
                        'score parameter should be an integer or a list of '
                        'integers (separated by a comma).')
                qs = qs.filter(rating__in=scores)
            if exclude_ratings:
                try:
                    exclude_ratings = [
                        int(rating) for rating in exclude_ratings.split(',')
                    ]
                except ValueError:
                    raise ParseError('exclude_ratings parameter should be an '
                                     'integer or a list of integers '
                                     '(separated by a comma).')
                qs = qs.exclude(pk__in=exclude_ratings)
        return super().filter_queryset(qs)

    def get_paginated_response(self, data):
        request = self.request
        extra_data = {}
        if grouped_rating := get_grouped_ratings(request,
                                                 self.get_addon_object()):
            extra_data['grouped_ratings'] = grouped_rating
        if 'show_permissions_for' in request.GET and is_gate_active(
                self.request, 'ratings-can_reply'):
            if 'addon' not in request.GET:
                raise ParseError(
                    'show_permissions_for parameter is only valid if the '
                    'addon parameter is also present')
            try:
                show_permissions_for = serializers.IntegerField(
                ).to_internal_value(request.GET['show_permissions_for'])
                if show_permissions_for != request.user.pk:
                    raise serializers.ValidationError
            except serializers.ValidationError:
                raise ParseError(
                    'show_permissions_for parameter value should be equal to '
                    'the user id of the authenticated user')
            extra_data[
                'can_reply'] = self.check_can_reply_permission_for_ratings_list(
                )
        # Call this here so the validation checks on the `show_flags_for` are
        # carried out even when there are no results to serialize.
        self.should_include_flags()
        response = super().get_paginated_response(data)
        if extra_data:
            response.data.update(extra_data)
        return response
Пример #12
0
class RatingViewSet(AddonChildMixin, ModelViewSet):
    serializer_class = RatingSerializer
    permission_classes = [
        ByHttpMethod({
            'get':
            AllowAny,
            'head':
            AllowAny,
            'options':
            AllowAny,  # Needed for CORS.
            # Deletion requires a specific permission check.
            'delete':
            CanDeleteRatingPermission,
            # To post a rating you just need to be authenticated.
            'post':
            IsAuthenticated,
            # To edit a rating you need to be the author or be an admin.
            'patch':
            AnyOf(AllowOwner, GroupPermission(amo.permissions.ADDONS_EDIT)),
            # Implementing PUT would be a little incoherent as we don't want to
            # allow users to change `version` but require it at creation time.
            # So only PATCH is allowed for editing.
        }),
    ]
    reply_permission_classes = [
        AnyOf(
            GroupPermission(amo.permissions.ADDONS_EDIT),
            AllowRelatedObjectPermissions('addon', [AllowAddonAuthor]),
        )
    ]
    reply_serializer_class = RatingSerializerReply
    flag_permission_classes = [AllowNotOwner]
    throttle_classes = (RatingThrottle, )

    def set_addon_object_from_rating(self, rating):
        """Set addon object on the instance from a rating object."""
        # At this point it's likely we didn't have an addon in the request, so
        # if we went through get_addon_object() before it's going to be set
        # to None already. We delete the addon_object property cache and set
        # addon_pk in kwargs to force get_addon_object() to reset
        # self.addon_object.
        del self.addon_object
        self.kwargs['addon_pk'] = str(rating.addon.pk)
        return self.get_addon_object()

    def get_addon_object(self):
        """Return addon object associated with the request, or None if not
        relevant.

        Will also fire permission checks on the addon object when it's loaded.
        """
        if hasattr(self, 'addon_object'):
            return self.addon_object

        if 'addon_pk' not in self.kwargs:
            self.kwargs['addon_pk'] = self.request.data.get(
                'addon') or self.request.GET.get('addon')
        if not self.kwargs['addon_pk']:
            # If we don't have an addon object, set it as None on the instance
            # and return immediately, that's fine.
            self.addon_object = None
            return
        else:
            # AddonViewSet.get_lookup_field() expects a string.
            self.kwargs['addon_pk'] = force_str(self.kwargs['addon_pk'])
        # When loading the add-on, pass a specific permission class - the
        # default from AddonViewSet is too restrictive, we are not modifying
        # the add-on itself so we don't need all the permission checks it does.
        return super(RatingViewSet,
                     self).get_addon_object(permission_classes=[AllowIfPublic])

    def should_include_flags(self):
        if not hasattr(self, '_should_include_flags'):
            request = self.request
            self._should_include_flags = (
                'show_flags_for' in request.GET
                and not is_gate_active(request, 'del-ratings-flags'))
            if self._should_include_flags:
                # Check the parameter was sent correctly
                try:
                    show_flags_for = serializers.IntegerField(
                    ).to_internal_value(request.GET['show_flags_for'])
                    if show_flags_for != request.user.pk:
                        raise serializers.ValidationError
                except serializers.ValidationError:
                    raise ParseError(
                        'show_flags_for parameter value should be equal to '
                        'the user id of the authenticated user')
        return self._should_include_flags

    def check_permissions(self, request):
        """Perform permission checks.

        The regular DRF permissions checks are made, but also, before that, if
        an addon was requested, verify that it exists, is public and listed,
        through AllowIfPublic permission, that get_addon_object() uses."""
        self.get_addon_object()

        # Proceed with the regular permission checks.
        return super(RatingViewSet, self).check_permissions(request)

    def get_serializer(self, *args, **kwargs):
        if self.action in ('partial_update', 'update'):
            instance = args[0]
            if instance.reply_to is not None:
                self.rating_object = instance.reply_to
                self.serializer_class = self.reply_serializer_class
        return super(RatingViewSet, self).get_serializer(*args, **kwargs)

    def filter_queryset(self, qs):
        if self.action == 'list':
            addon_identifier = self.request.GET.get('addon')
            user_identifier = self.request.GET.get('user')
            version_identifier = self.request.GET.get('version')
            score_filter = (self.request.GET.get('score') if is_gate_active(
                self.request, 'ratings-score-filter') else None)
            exclude_ratings = self.request.GET.get('exclude_ratings')
            if addon_identifier:
                qs = qs.filter(addon=self.get_addon_object())
            if user_identifier:
                try:
                    user_identifier = int(user_identifier)
                except ValueError:
                    raise ParseError('user parameter should be an integer.')
                qs = qs.filter(user=user_identifier)
            if version_identifier:
                try:
                    version_identifier = int(version_identifier)
                except ValueError:
                    raise ParseError('version parameter should be an integer.')
                qs = qs.filter(version=version_identifier)
            elif addon_identifier:
                # When filtering on addon but not on version, only return the
                # latest rating posted by each user.
                qs = qs.filter(is_latest=True)
            if not addon_identifier and not user_identifier:
                # Don't allow listing ratings without filtering by add-on or
                # user.
                raise ParseError('Need an addon or user parameter')
            if user_identifier and addon_identifier and version_identifier:
                # When user, addon and version identifiers are set, we are
                # effectively only looking for one or zero objects. Fake
                # pagination in that case, avoiding all count() calls and
                # therefore related cache-machine invalidation issues. Needed
                # because the frontend wants to call this before and after
                # having posted a new rating, and needs accurate results.
                self.pagination_class = OneOrZeroPageNumberPagination
            if score_filter:
                try:
                    scores = [int(score) for score in score_filter.split(',')]
                except ValueError:
                    raise ParseError(
                        'score parameter should be an integer or a list of '
                        'integers (separated by a comma).')
                qs = qs.filter(rating__in=scores)
            if exclude_ratings:
                try:
                    exclude_ratings = [
                        int(rating) for rating in exclude_ratings.split(',')
                    ]
                except ValueError:
                    raise ParseError('exclude_ratings parameter should be an '
                                     'integer or a list of integers '
                                     '(separated by a comma).')
                qs = qs.exclude(pk__in=exclude_ratings)
        return super(RatingViewSet, self).filter_queryset(qs)

    def get_paginated_response(self, data):
        request = self.request
        extra_data = {}
        if 'show_grouped_ratings' in request.GET:
            try:
                show_grouped_ratings = serializers.BooleanField(
                ).to_internal_value(request.GET['show_grouped_ratings'])
            except serializers.ValidationError:
                raise ParseError(
                    'show_grouped_ratings parameter should be a boolean')
            if show_grouped_ratings and self.get_addon_object():
                extra_data['grouped_ratings'] = dict(
                    GroupedRating.get(self.addon_object.id))
        if 'show_permissions_for' in request.GET and is_gate_active(
                self.request, 'ratings-can_reply'):
            if 'addon' not in request.GET:
                raise ParseError(
                    'show_permissions_for parameter is only valid if the '
                    'addon parameter is also present')
            try:
                show_permissions_for = serializers.IntegerField(
                ).to_internal_value(request.GET['show_permissions_for'])
                if show_permissions_for != request.user.pk:
                    raise serializers.ValidationError
            except serializers.ValidationError:
                raise ParseError(
                    'show_permissions_for parameter value should be equal to '
                    'the user id of the authenticated user')
            extra_data[
                'can_reply'] = self.check_can_reply_permission_for_ratings_list(
                )
        # Call this here so the validation checks on the `show_flags_for` are
        # carried out even when there are no results to serialize.
        self.should_include_flags()
        response = super(RatingViewSet, self).get_paginated_response(data)
        if extra_data:
            response.data.update(extra_data)
        return response

    def check_can_reply_permission_for_ratings_list(self):
        """Check whether or not the current request contains an user that can
        reply to ratings we're about to return.

        Used to populate the `can_reply` property in ratings list, when an
        addon is passed."""
        # Clone the current viewset, but change permission_classes.
        viewset = self.__class__(**self.__dict__)
        viewset.permission_classes = self.reply_permission_classes

        # Create a fake rating with the addon object attached, to be passed to
        # check_object_permissions().
        dummy_rating = Rating(addon=self.get_addon_object())

        try:
            viewset.check_permissions(self.request)
            viewset.check_object_permissions(self.request, dummy_rating)
            return True
        except (PermissionDenied, NotAuthenticated):
            return False

    def get_queryset(self):
        requested = self.request.GET.get('filter', '').split(',')
        has_addons_edit = acl.action_allowed(self.request,
                                             amo.permissions.ADDONS_EDIT)

        # Add this as a property of the view, because we need to pass down the
        # information to the serializer to show/hide delete replies.
        if not hasattr(self, 'should_access_deleted_ratings'):
            self.should_access_deleted_ratings = (
                ('with_deleted' in requested or self.action != 'list')
                and self.request.user.is_authenticated and has_addons_edit)

        should_access_only_top_level_ratings = (self.action == 'list'
                                                and self.get_addon_object())

        if self.should_access_deleted_ratings:
            # For admins or add-on authors replying. When listing, we include
            # deleted ratings but still filter out out replies, because they'll
            # be in the serializer anyway. For other actions, we simply remove
            # any filtering, allowing them to access any rating out of the box
            # with no extra parameter needed.
            if self.action == 'list':
                queryset = Rating.unfiltered.filter(reply_to__isnull=True)
            else:
                queryset = Rating.unfiltered.all()
        elif should_access_only_top_level_ratings:
            # When listing add-on ratings, exclude replies, they'll be
            # included during serialization as children of the relevant
            # ratings instead.
            queryset = Rating.without_replies.all()
        else:
            queryset = Rating.objects.all()

        # Filter out empty ratings if specified.
        # Should the users own empty ratings be filtered back in?
        if 'with_yours' in requested and self.request.user.is_authenticated:
            user_filter = Q(user=self.request.user.pk)
        else:
            user_filter = Q()
        # Apply the filter(s)
        if 'without_empty_body' in requested:
            queryset = queryset.filter(~Q(body=None) | user_filter)

        # The serializer needs reply, version and user. We don't need much
        # for version and user, so we can make joins with select_related(),
        # but for replies additional queries will be made for translations
        # anyway so we're better off using prefetch_related() to make a
        # separate query to fetch them all.
        queryset = queryset.select_related('version', 'user')
        replies_qs = Rating.unfiltered.select_related('user')
        return queryset.prefetch_related(Prefetch('reply',
                                                  queryset=replies_qs))

    @action(
        detail=True,
        methods=['post'],
        permission_classes=reply_permission_classes,
        serializer_class=reply_serializer_class,
        throttle_classes=[RatingReplyThrottle],
    )
    def reply(self, *args, **kwargs):
        # A reply is just like a regular post, except that we set the reply
        # FK to the current rating object and only allow add-on authors/admins.
        # Call get_object() to trigger 404 if it does not exist.
        self.rating_object = self.get_object()
        self.set_addon_object_from_rating(self.rating_object)
        if Rating.unfiltered.filter(reply_to=self.rating_object).exists():
            # A reply already exists, just edit it.
            # We set should_access_deleted_ratings so that it works even if
            # the reply has been deleted.
            self.kwargs['pk'] = kwargs['pk'] = self.rating_object.reply.pk
            self.should_access_deleted_ratings = True
            return self.partial_update(*args, **kwargs)
        return self.create(*args, **kwargs)

    @action(
        detail=True,
        methods=['post'],
        permission_classes=flag_permission_classes,
        throttle_classes=[],
    )
    def flag(self, request, *args, **kwargs):
        # We load the add-on object from the rating to trigger permission
        # checks.
        self.rating_object = self.get_object()
        self.set_addon_object_from_rating(self.rating_object)

        try:
            flag_instance = RatingFlag.objects.get(rating=self.rating_object,
                                                   user=self.request.user)
        except RatingFlag.DoesNotExist:
            flag_instance = None
        if flag_instance is None:
            serializer = RatingFlagSerializer(
                data=request.data, context=self.get_serializer_context())
        else:
            serializer = RatingFlagSerializer(
                flag_instance,
                data=request.data,
                partial=False,
                context=self.get_serializer_context(),
            )
        serializer.is_valid(raise_exception=True)
        serializer.save()
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data,
                        status=HTTP_202_ACCEPTED,
                        headers=headers)

    def perform_destroy(self, instance):
        instance.delete(user_responsible=self.request.user)
Пример #13
0
class AccountViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin,
                     GenericViewSet):
    permission_classes = [
        ByHttpMethod({
            'get':
            AllowAny,
            'head':
            AllowAny,
            'options':
            AllowAny,  # Needed for CORS.
            # To edit a profile it has to yours, or be an admin.
            'patch':
            AnyOf(AllowSelf, GroupPermission(amo.permissions.USERS_EDIT)),
            'delete':
            AnyOf(AllowSelf, GroupPermission(amo.permissions.USERS_EDIT)),
        }),
    ]

    def get_queryset(self):
        return UserProfile.objects.exclude(deleted=True).all()

    def get_object(self):
        if hasattr(self, 'instance'):
            return self.instance
        identifier = self.kwargs.get('pk')
        self.lookup_field = self.get_lookup_field(identifier)
        self.kwargs[self.lookup_field] = identifier
        self.instance = super(AccountViewSet, self).get_object()
        # action won't exist for other classes that are using this ViewSet.
        can_view_instance = (not getattr(self, 'action', None)
                             or self.self_view or self.admin_viewing
                             or self.instance.is_public)
        if can_view_instance:
            return self.instance
        else:
            raise Http404

    def get_lookup_field(self, identifier):
        lookup_field = 'pk'
        if identifier and not identifier.isdigit():
            # If the identifier contains anything other than a digit, it's
            # the username.
            lookup_field = 'username'
        return lookup_field

    @property
    def self_view(self):
        return (self.request.user.is_authenticated()
                and self.get_object() == self.request.user)

    @property
    def admin_viewing(self):
        return acl.action_allowed_user(self.request.user,
                                       amo.permissions.USERS_EDIT)

    def get_serializer_class(self):
        if self.self_view or self.admin_viewing:
            return UserProfileSerializer
        else:
            return PublicUserProfileSerializer

    def perform_destroy(self, instance):
        if instance.is_developer:
            raise serializers.ValidationError(
                ugettext(
                    u'Developers of add-ons or themes cannot delete their '
                    u'account. You must delete all add-ons and themes linked to '
                    u'this account, or transfer them to other users.'))
        return super(AccountViewSet, self).perform_destroy(instance)

    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        self.perform_destroy(instance)
        response = Response(status=HTTP_204_NO_CONTENT)
        if instance == request.user:
            logout_user(request, response)
        return response

    @detail_route(methods=['delete'],
                  permission_classes=[
                      AnyOf(AllowSelf,
                            GroupPermission(amo.permissions.USERS_EDIT))
                  ])
    def picture(self, request, pk=None):
        user = self.get_object()
        user.delete_picture()
        log.debug(u'User (%s) deleted photo' % user)
        return self.retrieve(request)
Пример #14
0
class ReviewViewSet(AddonChildMixin, ModelViewSet):
    serializer_class = ReviewSerializer
    permission_classes = [
        ByHttpMethod({
            'get':
            AllowAny,
            'head':
            AllowAny,
            'options':
            AllowAny,  # Needed for CORS.

            # Deletion requires a specific permission check.
            'delete':
            CanDeleteReviewPermission,

            # To post a review you just need to be authenticated.
            'post':
            IsAuthenticated,

            # To edit a review you need to be the author or be an admin.
            'patch':
            AnyOf(AllowOwner, GroupPermission('Addons', 'Edit')),

            # Implementing PUT would be a little incoherent as we don't want to
            # allow users to change `version` but require it at creation time.
            # So only PATCH is allowed for editing.
        }),
    ]
    reply_permission_classes = [
        AnyOf(
            GroupPermission('Addons', 'Edit'),
            AllowRelatedObjectPermissions('addon', [AllowAddonAuthor]),
        )
    ]
    reply_serializer_class = ReviewSerializerReply

    queryset = Review.objects.all()

    def get_addon_object(self):
        # When loading the add-on, pass a specific permission class - the
        # default from AddonViewSet is too restrictive, we are not modifying
        # the add-on itself so we don't need all the permission checks it does.
        return super(ReviewViewSet, self).get_addon_object(
            permission_classes=[AllowIfReviewedAndListed])

    def check_permissions(self, request):
        if 'addon_pk' in self.kwargs:
            # In addition to the regular permission checks that are made, we
            # need to verify that the add-on exists, is public and listed. Just
            # loading the addon should be enough to do that, since
            # AddonChildMixin implementation calls AddonViewSet.get_object().
            self.get_addon_object()

        # Proceed with the regular permission checks.
        return super(ReviewViewSet, self).check_permissions(request)

    def filter_queryset(self, qs):
        if self.action == 'list':
            if 'addon_pk' in self.kwargs:
                qs = qs.filter(addon=self.get_addon_object())
            elif 'account_pk' in self.kwargs:
                qs = qs.filter(user=self.kwargs.get('account_pk'))
            else:
                # Don't allow listing reviews without filtering by add-on or
                # user.
                raise ParseError('Need an addon or user identifier')
        return qs

    def get_queryset(self):
        if self.action == 'list':
            if self.kwargs.get('addon_pk'):
                # When listing add-on reviews, exclude replies, they'll be
                # included during serialization as children of the relevant
                # reviews instead.
                self.queryset = Review.without_replies.all()

        qs = super(ReviewViewSet, self).get_queryset()
        # The serializer needs user, reply and version, so use
        # prefetch_related() to avoid extra queries (avoid select_related() as
        # we need crazy joins already). Also avoid loading addon since we don't
        # need it, we already loaded it for permission checks through the pk
        # specified in the URL.
        return qs.defer('addon').prefetch_related('reply', 'user', 'version')

    @detail_route(methods=['post'],
                  permission_classes=reply_permission_classes,
                  serializer_class=reply_serializer_class)
    def reply(self, *args, **kwargs):
        # A reply is just like a regular post, except that we set the reply
        # FK to the current review object and only allow add-on authors/admins.
        # Call get_object() to trigger 404 if it does not exist.
        self.review_object = self.get_object()
        return self.create(*args, **kwargs)