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