addons api (pending) authors create and confirm endpoint (#19191)
* addons api (pending) authors create and confirm endpoint * update docs; add to tests
This commit is contained in:
Родитель
1e0ed88eb4
Коммит
e4fe20f0ff
|
@ -732,100 +732,6 @@ This endpoint allows the metadata for an existing preview for a non-theme add-on
|
|||
.. http:delete:: /api/v5/addons/addon/(int:addon_id|string:addon_slug|string:addon_guid)/previews/(int:id)/
|
||||
|
||||
|
||||
-----------
|
||||
Author List
|
||||
-----------
|
||||
|
||||
.. _addon-author-list:
|
||||
|
||||
This endpoint returns a list of all the authors of an add-on. No pagination is supported.
|
||||
|
||||
.. note::
|
||||
This API requires :doc:`authentication <auth>`, and for the user to be an author of the add-on.
|
||||
|
||||
|
||||
.. http:get:: /api/v5/addons/addon/(int:id|string:slug|string:guid)/authors/
|
||||
|
||||
|
||||
-------------
|
||||
Author Detail
|
||||
-------------
|
||||
|
||||
.. _addon-author-detail:
|
||||
|
||||
This endpoint allows the properties of an add-on author to be retrieved, given a user (account) id.
|
||||
|
||||
.. note::
|
||||
This API requires :doc:`authentication <auth>`, and for the user to be an author of the add-on.
|
||||
|
||||
|
||||
.. http:get:: /api/v5/addons/addon/(int:id|string:slug|string:guid)/authors/(int:user_id)/
|
||||
|
||||
.. _addon-author-detail-object:
|
||||
|
||||
:>json int user_id: The user id for an author.
|
||||
:>json string name: The name for an author.
|
||||
:>json string email: The email address for an author.
|
||||
:>json string role: The :ref:`role <addon-author-detail-role>` the author holds on this add-on.
|
||||
:>json boolean listed: If ``true`` the user will be listed as an add-on author publicly on the add-on detail page. (If ``false`` the user is not exposed as an author.)
|
||||
:>json int position: The position the author should be returned in the list of authors of the add-on :ref:`detail <addon-detail-object>`. Order is ascending so lower positions are placed earlier.
|
||||
|
||||
|
||||
.. _addon-author-detail-role:
|
||||
|
||||
Possible values for the ``role`` field:
|
||||
|
||||
============== ==============================================================
|
||||
Value Description
|
||||
============== ==============================================================
|
||||
developer A developer of the add-on. Developers can change all add-on
|
||||
metadata and create, delete, and edit versions of the add-on.
|
||||
owner An owner of the add-on. Owners have all the abilities of a
|
||||
developer, plus they can add, remove, and edit add-on authors,
|
||||
and delete the add-on.
|
||||
============== ==============================================================
|
||||
|
||||
-----------
|
||||
Author Edit
|
||||
-----------
|
||||
|
||||
.. _addon-author-edit:
|
||||
|
||||
This endpoint allows the properties of an add-on author to be edited.
|
||||
|
||||
.. note::
|
||||
This API requires :doc:`authentication <auth>`, and for the user to be an owner of the add-on.
|
||||
|
||||
.. warning::
|
||||
If you change your own author role to developer from owner you will lose permission to make any further author changes.
|
||||
|
||||
.. http:patch:: /api/v5/addons/addon/(int:addon_id|string:addon_slug|string:addon_guid)/authors/(int:user_id)/
|
||||
|
||||
.. _addon-author-edit-request:
|
||||
|
||||
:<json string role: The :ref:`role <addon-author-detail-role>` the author holds on this add-on. Add-ons must have at least one owner author.
|
||||
:<json boolean listed: If ``true`` the user will be listed as an add-on author publicly on the add-on detail page. (If ``false`` the user is not exposed as an author.) Add-ons must have at least one listed author.
|
||||
:<json int position: The position the author should be returned in the list of authors of the add-on :ref:`detail <addon-detail-object>`. Order is ascending so lower positions are placed earlier.
|
||||
|
||||
|
||||
-------------
|
||||
Author Delete
|
||||
-------------
|
||||
|
||||
.. _addon-author-delete:
|
||||
|
||||
This endpoint allows an add-on author to be removed from an add-on.
|
||||
Add-ons must have at least one owner, and at least one listed author.
|
||||
|
||||
.. note::
|
||||
This API requires :doc:`authentication <auth>`, and for the user to be an owner of the add-on.
|
||||
|
||||
.. warning::
|
||||
If you delete yourself as an add-on author you will lose all access to the add-on.
|
||||
|
||||
.. http:delete:: /api/v5/addons/addon/(int:addon_id|string:addon_slug|string:addon_guid)/authors/(int:user_id)/
|
||||
|
||||
|
||||
-------------
|
||||
Upload Create
|
||||
-------------
|
||||
|
|
|
@ -0,0 +1,225 @@
|
|||
==============
|
||||
Add-on Authors
|
||||
==============
|
||||
|
||||
.. note::
|
||||
|
||||
These APIs are not frozen and can change at any time without warning.
|
||||
See :ref:`the API versions available<api-versions-list>` for alternatives
|
||||
if you need stability - note the following APIs are only available on v5+ though.
|
||||
|
||||
-----------
|
||||
Author List
|
||||
-----------
|
||||
|
||||
.. _addon-author-list:
|
||||
|
||||
This endpoint returns a list of all the authors of an add-on. No pagination is supported.
|
||||
New authors are created as pending authors, which become authors once the user has confirmed.
|
||||
|
||||
.. note::
|
||||
This API requires :doc:`authentication <auth>`, and for the user to be an author of the add-on.
|
||||
|
||||
|
||||
.. http:get:: /api/v5/addons/addon/(int:id|string:slug|string:guid)/authors/
|
||||
|
||||
|
||||
-------------
|
||||
Author Detail
|
||||
-------------
|
||||
|
||||
.. _addon-author-detail:
|
||||
|
||||
This endpoint allows the properties of an add-on author to be retrieved, given a user (account) id.
|
||||
|
||||
.. note::
|
||||
This API requires :doc:`authentication <auth>`, and for the user to be an author of the add-on.
|
||||
|
||||
|
||||
.. http:get:: /api/v5/addons/addon/(int:id|string:slug|string:guid)/authors/(int:user_id)/
|
||||
|
||||
.. _addon-author-detail-object:
|
||||
|
||||
:>json int user_id: The user id for an author.
|
||||
:>json string name: The name for an author.
|
||||
:>json string email: The email address for an author.
|
||||
:>json string role: The :ref:`role <addon-author-detail-role>` the author holds on this add-on.
|
||||
:>json boolean listed: If ``true`` the user will be listed as an add-on author publicly on the add-on detail page. (If ``false`` the user is not exposed as an author.)
|
||||
:>json int position: The position the author should be returned in the list of authors of the add-on :ref:`detail <addon-detail-object>`. Order is ascending so lower positions are placed earlier.
|
||||
|
||||
|
||||
.. _addon-author-detail-role:
|
||||
|
||||
Possible values for the ``role`` field:
|
||||
|
||||
============== ==============================================================
|
||||
Value Description
|
||||
============== ==============================================================
|
||||
developer A developer of the add-on. Developers can change all add-on
|
||||
metadata and create, delete, and edit versions of the add-on.
|
||||
owner An owner of the add-on. Owners have all the abilities of a
|
||||
developer, plus they can add, remove, and edit add-on authors,
|
||||
and delete the add-on.
|
||||
============== ==============================================================
|
||||
|
||||
-----------
|
||||
Author Edit
|
||||
-----------
|
||||
|
||||
.. _addon-author-edit:
|
||||
|
||||
This endpoint allows the properties of an add-on author to be edited.
|
||||
|
||||
.. note::
|
||||
This API requires :doc:`authentication <auth>`, and for the user to be an owner of the add-on.
|
||||
|
||||
.. warning::
|
||||
If you change your own author role to developer from owner you will lose permission to make any further author changes.
|
||||
|
||||
.. http:patch:: /api/v5/addons/addon/(int:addon_id|string:addon_slug|string:addon_guid)/authors/(int:user_id)/
|
||||
|
||||
.. _addon-author-edit-request:
|
||||
|
||||
:<json string role: The :ref:`role <addon-author-detail-role>` the author holds on this add-on. Add-ons must have at least one owner author.
|
||||
:<json boolean listed: If ``true`` the user will be listed as an add-on author publicly on the add-on detail page. (If ``false`` the user is not exposed as an author.) Add-ons must have at least one listed author.
|
||||
:<json int position: The position the author should be returned in the list of authors of the add-on :ref:`detail <addon-detail-object>`. Order is ascending so lower positions are placed earlier.
|
||||
|
||||
|
||||
-------------
|
||||
Author Delete
|
||||
-------------
|
||||
|
||||
.. _addon-author-delete:
|
||||
|
||||
This endpoint allows an add-on author to be removed from an add-on.
|
||||
Add-ons must have at least one owner, and at least one listed author.
|
||||
|
||||
.. note::
|
||||
This API requires :doc:`authentication <auth>`, and for the user to be an owner of the add-on.
|
||||
|
||||
.. warning::
|
||||
If you delete yourself as an add-on author you will lose all access to the add-on.
|
||||
|
||||
.. http:delete:: /api/v5/addons/addon/(int:addon_id|string:addon_slug|string:addon_guid)/authors/(int:user_id)/
|
||||
|
||||
|
||||
---------------------
|
||||
Pending Author Create
|
||||
---------------------
|
||||
|
||||
.. _addon-pending-author-create:
|
||||
|
||||
This endpoint allows an owner to invite a user to become an author of an add-on - the user will be sent an email notifying them of the invitation.
|
||||
A pending author is created for the add-on, and once they confirm the invitation, they will be an author of that add-on.
|
||||
Authors will be given the position at the end of the list of authors when confirmed.
|
||||
|
||||
.. note::
|
||||
This API requires :doc:`authentication <auth>`, and for the user to be an owner of the add-on.
|
||||
|
||||
.. http:post:: /api/v5/addons/addon/(int:addon_id|string:addon_slug|string:addon_guid)/pending-authors/
|
||||
|
||||
.. _addon-pending-author-create-request:
|
||||
|
||||
:<json int user_id: The user to invited to become an author of this add-on.
|
||||
:<json string role: The :ref:`role <addon-author-detail-role>` the author will hold on this add-on.
|
||||
:<json boolean listed: If ``true`` the user will be listed as an add-on author publicly on the add-on detail page once confirmed. (If ``false`` the user will not be exposed as an author.)
|
||||
|
||||
|
||||
----------------------
|
||||
Pending Author Confirm
|
||||
----------------------
|
||||
|
||||
.. _addon-pending-author-confirm:
|
||||
|
||||
This endpoint allows a user to confirm they want to be an author of an add-on.
|
||||
Authors will be given the position at the end of the list of authors when confirmed.
|
||||
|
||||
.. note::
|
||||
This API requires :doc:`authentication <auth>`, and for the user to be invited to be an author (to be a pending author).
|
||||
|
||||
.. http:post:: /api/v5/addons/addon/(int:addon_id|string:addon_slug|string:addon_guid)/pending-authors/confirm
|
||||
|
||||
|
||||
----------------------
|
||||
Pending Author Decline
|
||||
----------------------
|
||||
|
||||
.. _addon-pending-author-decline:
|
||||
|
||||
This endpoint allows a user to decline the invitation to be an author of an add-on.
|
||||
|
||||
.. note::
|
||||
This API requires :doc:`authentication <auth>`, and for the user to be invited to be an author (to be a pending author).
|
||||
|
||||
.. http:post:: /api/v5/addons/addon/(int:addon_id|string:addon_slug|string:addon_guid)/pending-authors/decline
|
||||
|
||||
|
||||
-------------------
|
||||
Pending Author List
|
||||
-------------------
|
||||
|
||||
.. _addon-pending-author-list:
|
||||
|
||||
This endpoint returns a list of all the pending authors of an add-on. No pagination is supported.
|
||||
|
||||
.. note::
|
||||
This API requires :doc:`authentication <auth>`, and for the user to be an author of the add-on.
|
||||
|
||||
|
||||
.. http:get:: /api/v5/addons/addon/(int:id|string:slug|string:guid)/pending-authors/
|
||||
|
||||
|
||||
---------------------
|
||||
Pending Author Detail
|
||||
---------------------
|
||||
|
||||
.. _addon-pending-author-detail:
|
||||
|
||||
This endpoint allows the properties of a pending add-on author to be retrieved, given a user (account) id.
|
||||
|
||||
.. note::
|
||||
This API requires :doc:`authentication <auth>`, and for the user to be an author of the add-on.
|
||||
|
||||
|
||||
.. http:get:: /api/v5/addons/addon/(int:id|string:slug|string:guid)/pending-authors/(int:user_id)/
|
||||
|
||||
.. _addon-pending-author-detail-object:
|
||||
|
||||
:>json int user_id: The user id for a pending author.
|
||||
:>json string name: The name for a pending author.
|
||||
:>json string email: The email address for a pending author.
|
||||
:>json string role: The :ref:`role <addon-author-detail-role>` the author will hold on this add-on.
|
||||
:>json boolean listed: If ``true`` the user will be listed as an add-on author publicly on the add-on detail page once confirmed. (If ``false`` the user will not be exposed as an author.)
|
||||
|
||||
|
||||
-------------------
|
||||
Pending Author Edit
|
||||
-------------------
|
||||
|
||||
.. _addon-pending-author-edit:
|
||||
|
||||
This endpoint allows the properties of a pending add-on author to be edited.
|
||||
|
||||
.. note::
|
||||
This API requires :doc:`authentication <auth>`, and for the user to be an owner of the add-on.
|
||||
|
||||
.. http:patch:: /api/v5/addons/addon/(int:addon_id|string:addon_slug|string:addon_guid)/pending-authors/(int:user_id)/
|
||||
|
||||
.. _addon-pending-author-edit-request:
|
||||
|
||||
:<json string role: The :ref:`role <addon-author-detail-role>` the author will hold on this add-on.
|
||||
:<json boolean listed: If ``true`` the user will be listed as an add-on author publicly on the add-on detail page once confirmed. (If ``false`` the user will not be exposed as an author.)
|
||||
|
||||
|
||||
---------------------
|
||||
Pending Author Delete
|
||||
---------------------
|
||||
|
||||
.. _addon-pending-author-delete:
|
||||
|
||||
This endpoint allows a pending add-on author to be deleted, so the user is no longer able to confirm to become an author of the add-on.
|
||||
|
||||
.. note::
|
||||
This API requires :doc:`authentication <auth>`, and for the user to be an owner of the add-on.
|
||||
|
||||
.. http:delete:: /api/v5/addons/addon/(int:addon_id|string:addon_slug|string:addon_guid)/pending-authors/(int:user_id)/
|
|
@ -36,6 +36,7 @@ using the API.
|
|||
accounts
|
||||
activity
|
||||
addons
|
||||
authors
|
||||
applications
|
||||
blocklist
|
||||
categories
|
||||
|
|
|
@ -441,6 +441,7 @@ These are `v5` specific changes - `v4` changes apply also.
|
|||
* 2022-04-28: added the ability to delete add-ons via addon detail api endpoint. https://github.com/mozilla/addons-server/issues/19072
|
||||
* 2022-05-05: added the ability to list and edit add-on authors. https://github.com/mozilla/addons-server/issues/18231
|
||||
* 2022-05-05: added the ability to delete add-on authors. https://github.com/mozilla/addons-server/issues/19163
|
||||
* 2022-05-12: added the ability to add new add-on authors, as pending authors. https://github.com/mozilla/addons-server/issues/19164
|
||||
|
||||
.. _`#11380`: https://github.com/mozilla/addons-server/issues/11380/
|
||||
.. _`#11379`: https://github.com/mozilla/addons-server/issues/11379/
|
||||
|
|
|
@ -9,6 +9,7 @@ from olympia.tags.views import TagListView
|
|||
|
||||
from .views import (
|
||||
AddonAuthorViewSet,
|
||||
AddonPendingAuthorViewSet,
|
||||
AddonAutoCompleteSearchView,
|
||||
AddonFeaturedView,
|
||||
AddonRecommendationView,
|
||||
|
@ -31,6 +32,9 @@ sub_addons = NestedSimpleRouter(addons, r'addon', lookup='addon')
|
|||
sub_addons.register('versions', AddonVersionViewSet, basename='addon-version')
|
||||
sub_addons.register('previews', AddonPreviewViewSet, basename='addon-preview')
|
||||
sub_addons.register('authors', AddonAuthorViewSet, basename='addon-author')
|
||||
sub_addons.register(
|
||||
'pending-authors', AddonPendingAuthorViewSet, basename='addon-pending-author'
|
||||
)
|
||||
sub_versions = NestedSimpleRouter(sub_addons, r'versions', lookup='version')
|
||||
sub_versions.register(
|
||||
r'reviewnotes', VersionReviewNotesViewSet, basename='version-reviewnotes'
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
import re
|
||||
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext
|
||||
|
||||
|
@ -43,7 +44,7 @@ from olympia.promoted.models import PromotedAddon
|
|||
from olympia.search.filters import AddonAppVersionQueryParam
|
||||
from olympia.ratings.utils import get_grouped_ratings
|
||||
from olympia.tags.models import Tag
|
||||
from olympia.users.models import UserProfile
|
||||
from olympia.users.models import EmailUserRestriction, RESTRICTION_TYPES, UserProfile
|
||||
from olympia.versions.models import (
|
||||
ApplicationsVersions,
|
||||
License,
|
||||
|
@ -66,6 +67,7 @@ from .models import (
|
|||
AddonApprovalsCounter,
|
||||
AddonReviewerFlags,
|
||||
AddonUser,
|
||||
AddonUserPendingConfirmation,
|
||||
DeniedSlug,
|
||||
Preview,
|
||||
ReplacementAddon,
|
||||
|
@ -681,25 +683,23 @@ class AddonDeveloperSerializer(BaseUserSerializer):
|
|||
|
||||
|
||||
class AddonAuthorSerializer(serializers.ModelSerializer):
|
||||
user_id = serializers.IntegerField(source='user.id', read_only=True)
|
||||
name = serializers.CharField(source='user.name', read_only=True)
|
||||
email = serializers.CharField(source='user.email', read_only=True)
|
||||
role = ReverseChoiceField(choices=list(amo.AUTHOR_CHOICES_API.items()))
|
||||
role = ReverseChoiceField(
|
||||
choices=list(amo.AUTHOR_CHOICES_API.items()), default=amo.AUTHOR_ROLE_OWNER
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = AddonUser
|
||||
fields = ('user_id', 'name', 'email', 'listed', 'position', 'role')
|
||||
|
||||
writeable_fields = (
|
||||
'listed',
|
||||
'position',
|
||||
'role',
|
||||
)
|
||||
writeable_fields = ('listed', 'position', 'role')
|
||||
read_only_fields = tuple(set(fields) - set(writeable_fields))
|
||||
|
||||
def validate_role(self, value):
|
||||
if (
|
||||
value != amo.AUTHOR_ROLE_OWNER
|
||||
self.instance
|
||||
and value != amo.AUTHOR_ROLE_OWNER
|
||||
and not AddonUser.objects.filter(
|
||||
addon_id=self.instance.addon_id, role=amo.AUTHOR_ROLE_OWNER
|
||||
)
|
||||
|
@ -713,7 +713,8 @@ class AddonAuthorSerializer(serializers.ModelSerializer):
|
|||
|
||||
def validate_listed(self, value):
|
||||
if (
|
||||
value is not True
|
||||
self.instance
|
||||
and value is not True
|
||||
and not AddonUser.objects.filter(
|
||||
addon_id=self.instance.addon_id, listed=True
|
||||
)
|
||||
|
@ -726,6 +727,49 @@ class AddonAuthorSerializer(serializers.ModelSerializer):
|
|||
return value
|
||||
|
||||
|
||||
class AddonPendingAuthorSerializer(AddonAuthorSerializer):
|
||||
user_id = serializers.IntegerField()
|
||||
addon = serializers.HiddenField(default=ThisAddonDefault())
|
||||
|
||||
class Meta:
|
||||
model = AddonUserPendingConfirmation
|
||||
fields = ('addon', 'user_id', 'name', 'email', 'listed', 'role')
|
||||
|
||||
writeable_fields = ('addon', 'user_id', 'listed', 'role')
|
||||
read_only_fields = tuple(set(fields) - set(writeable_fields))
|
||||
|
||||
def validate_user_id(self, value):
|
||||
try:
|
||||
user = UserProfile.objects.get(id=value)
|
||||
except UserProfile.DoesNotExist:
|
||||
raise exceptions.ValidationError(gettext('Account not found.'))
|
||||
|
||||
if not EmailUserRestriction.allow_email(
|
||||
user.email, restriction_type=RESTRICTION_TYPES.SUBMISSION
|
||||
):
|
||||
raise exceptions.ValidationError(EmailUserRestriction.error_message)
|
||||
|
||||
if self.context['view'].get_addon_object().authors.filter(pk=user.pk).exists():
|
||||
raise exceptions.ValidationError(
|
||||
gettext('An author can only be present once.')
|
||||
)
|
||||
|
||||
try:
|
||||
if user.display_name is None:
|
||||
raise DjangoValidationError('') # raise so we can catch below.
|
||||
for validator in user._meta.get_field('display_name').validators:
|
||||
validator(user.display_name)
|
||||
except DjangoValidationError:
|
||||
raise exceptions.ValidationError(
|
||||
gettext(
|
||||
'The account needs a display name before it can be added as an '
|
||||
'author.'
|
||||
)
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class PromotedAddonSerializer(serializers.ModelSerializer):
|
||||
GROUP_CHOICES = [(group.id, group.api_name) for group in PROMOTED_GROUPS]
|
||||
apps = serializers.SerializerMethodField()
|
||||
|
|
|
@ -57,7 +57,7 @@ from olympia.files.utils import parse_addon, parse_xpi
|
|||
from olympia.files.tests.test_models import UploadMixin
|
||||
from olympia.ratings.models import Rating
|
||||
from olympia.tags.models import Tag
|
||||
from olympia.users.models import UserProfile
|
||||
from olympia.users.models import EmailUserRestriction, UserProfile
|
||||
from olympia.versions.models import (
|
||||
ApplicationsVersions,
|
||||
AppVersion,
|
||||
|
@ -72,12 +72,14 @@ from ..models import (
|
|||
AddonRegionalRestrictions,
|
||||
AddonReviewerFlags,
|
||||
AddonUser,
|
||||
AddonUserPendingConfirmation,
|
||||
DeniedSlug,
|
||||
ReplacementAddon,
|
||||
Preview,
|
||||
)
|
||||
from ..serializers import (
|
||||
AddonAuthorSerializer,
|
||||
AddonPendingAuthorSerializer,
|
||||
AddonSerializer,
|
||||
AddonSerializerWithUnlistedData,
|
||||
DeveloperVersionSerializer,
|
||||
|
@ -5897,3 +5899,190 @@ class TestAddonAuthorViewSet(TestCase):
|
|||
new_author.update(listed=True)
|
||||
response = self.client.delete(self.detail_url)
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
class TestAddonPendingAuthorViewSet(TestCase):
|
||||
client_class = APITestClientSessionID
|
||||
|
||||
def setUp(self):
|
||||
self.user = user_factory(read_dev_agreement=self.days_ago(0))
|
||||
self.addon = addon_factory(users=(self.user,))
|
||||
self.pending_author = AddonUserPendingConfirmation.objects.create(
|
||||
addon=self.addon, user=user_factory(), role=amo.AUTHOR_ROLE_OWNER
|
||||
)
|
||||
self.detail_url = reverse_ns(
|
||||
'addon-pending-author-detail',
|
||||
kwargs={'addon_pk': self.addon.pk, 'user_id': self.pending_author.user.id},
|
||||
api_version='v5',
|
||||
)
|
||||
self.list_url = reverse_ns(
|
||||
'addon-pending-author-list',
|
||||
kwargs={'addon_pk': self.addon.pk},
|
||||
api_version='v5',
|
||||
)
|
||||
|
||||
def test_list(self):
|
||||
dev_author = AddonUserPendingConfirmation.objects.create(
|
||||
addon=self.addon, user=user_factory(), role=amo.AUTHOR_ROLE_DEV
|
||||
)
|
||||
hidden_author = AddonUserPendingConfirmation.objects.create(
|
||||
addon=self.addon, user=user_factory(), listed=False
|
||||
)
|
||||
|
||||
assert self.client.get(self.list_url).status_code == 401
|
||||
|
||||
self.client.login_api(user_factory())
|
||||
assert self.client.get(self.list_url).status_code == 403
|
||||
|
||||
self.client.login_api(self.pending_author.user)
|
||||
assert self.client.get(self.list_url).status_code == 403
|
||||
|
||||
self.client.login_api(self.user)
|
||||
response = self.client.get(self.list_url)
|
||||
assert response.status_code == 200, response.content
|
||||
assert len(response.data) == 3, response.data
|
||||
assert response.data[0] == AddonPendingAuthorSerializer().to_representation(
|
||||
instance=self.pending_author
|
||||
)
|
||||
assert response.data[1] == AddonPendingAuthorSerializer().to_representation(
|
||||
instance=dev_author
|
||||
)
|
||||
assert response.data[2] == AddonPendingAuthorSerializer().to_representation(
|
||||
instance=hidden_author
|
||||
)
|
||||
|
||||
def test_detail(self):
|
||||
assert self.client.get(self.detail_url).status_code == 401
|
||||
|
||||
self.client.login_api(user_factory())
|
||||
assert self.client.get(self.detail_url).status_code == 403
|
||||
|
||||
self.client.login_api(self.pending_author.user)
|
||||
assert self.client.get(self.detail_url).status_code == 403
|
||||
|
||||
self.client.login_api(self.user)
|
||||
response = self.client.get(self.detail_url)
|
||||
assert response.status_code == 200, response.content
|
||||
assert response.data == AddonPendingAuthorSerializer().to_representation(
|
||||
instance=self.pending_author
|
||||
)
|
||||
|
||||
def test_delete(self):
|
||||
assert self.client.delete(self.detail_url).status_code == 401
|
||||
|
||||
self.client.login_api(user_factory())
|
||||
assert self.client.delete(self.detail_url).status_code == 403
|
||||
|
||||
self.client.login_api(self.pending_author.user)
|
||||
assert self.client.get(self.detail_url).status_code == 403
|
||||
|
||||
self.client.login_api(self.user)
|
||||
response = self.client.delete(self.detail_url)
|
||||
assert response.status_code == 204
|
||||
assert not AddonUserPendingConfirmation.objects.exists()
|
||||
|
||||
def test_create(self):
|
||||
# This will be user that will be created
|
||||
user = user_factory(display_name='new_guy')
|
||||
data = {'user_id': user.id, 'role': 'developer'}
|
||||
assert self.client.post(self.list_url, data=data).status_code == 401
|
||||
|
||||
self.client.login_api(user_factory())
|
||||
assert self.client.post(self.list_url, data=data).status_code == 403
|
||||
|
||||
self.client.login_api(self.user)
|
||||
response = self.client.post(self.list_url, data=data)
|
||||
assert response.status_code == 201, response.content
|
||||
assert AddonUserPendingConfirmation.objects.filter(
|
||||
user=user, addon=self.addon, role=amo.AUTHOR_ROLE_DEV
|
||||
).exists()
|
||||
|
||||
@override_settings(API_THROTTLING=False)
|
||||
def test_create_validation(self):
|
||||
self.client.login_api(self.user)
|
||||
|
||||
# user doesn't exist
|
||||
response = self.client.post(self.list_url, data={'user_id': 12345})
|
||||
assert response.status_code == 400, response.content
|
||||
assert response.data == {'user_id': ['Account not found.']}
|
||||
|
||||
# not allowed
|
||||
user = user_factory(email='foo@baa.com')
|
||||
EmailUserRestriction.objects.create(email_pattern='*@baa.com')
|
||||
response = self.client.post(self.list_url, data={'user_id': user.id})
|
||||
assert response.status_code == 400, response.content
|
||||
assert response.data == {
|
||||
'user_id': [
|
||||
'The email address used for your account is not allowed for add-on '
|
||||
'submission.'
|
||||
]
|
||||
}
|
||||
|
||||
# can't add a user that is already an author
|
||||
user.update(email='foo@mozilla.com')
|
||||
dupe_addonuser = AddonUser.objects.create(addon=self.addon, user=user)
|
||||
response = self.client.post(self.list_url, data={'user_id': user.id})
|
||||
assert response.status_code == 400, response.content
|
||||
assert response.data == {'user_id': ['An author can only be present once.']}
|
||||
|
||||
# account needs a display name
|
||||
dupe_addonuser.delete()
|
||||
user.update(display_name=None)
|
||||
response = self.client.post(self.list_url, data={'user_id': user.id})
|
||||
assert response.status_code == 400, response.content
|
||||
assert response.data == {
|
||||
'user_id': [
|
||||
'The account needs a display name before it can be added as an '
|
||||
'author.'
|
||||
]
|
||||
}
|
||||
|
||||
def test_confirm(self):
|
||||
AddonUser.objects.create(addon=self.addon, user=user_factory(), position=3)
|
||||
confirm_url = reverse_ns(
|
||||
'addon-pending-author-confirm',
|
||||
kwargs={'addon_pk': self.addon.pk},
|
||||
api_version='v5',
|
||||
)
|
||||
|
||||
assert self.client.post(confirm_url).status_code == 401
|
||||
|
||||
self.client.login_api(user_factory()) # random user can't confirm, only invited
|
||||
assert self.client.post(confirm_url).status_code == 403
|
||||
|
||||
pending_user = self.pending_author.user
|
||||
assert pending_user not in self.addon.reload().authors.all()
|
||||
self.client.login_api(pending_user)
|
||||
response = self.client.post(confirm_url)
|
||||
assert response.status_code == 200
|
||||
|
||||
assert pending_user in self.addon.reload().authors.all()
|
||||
assert not AddonUserPendingConfirmation.objects.filter(
|
||||
id=self.pending_author.id
|
||||
).exists()
|
||||
addonuser = AddonUser.objects.get(addon=self.addon, user=pending_user)
|
||||
assert addonuser.position == 4 # should be + 1 after the existing authors
|
||||
assert addonuser.role == self.pending_author.role
|
||||
assert addonuser.listed == self.pending_author.listed
|
||||
|
||||
def test_decline(self):
|
||||
decline_url = reverse_ns(
|
||||
'addon-pending-author-decline',
|
||||
kwargs={'addon_pk': self.addon.pk},
|
||||
api_version='v5',
|
||||
)
|
||||
|
||||
assert self.client.post(decline_url).status_code == 401
|
||||
|
||||
self.client.login_api(user_factory()) # random user can't decline, only invited
|
||||
assert self.client.post(decline_url).status_code == 403
|
||||
|
||||
pending_user = self.pending_author.user
|
||||
self.client.login_api(pending_user)
|
||||
response = self.client.post(decline_url)
|
||||
assert response.status_code == 200
|
||||
|
||||
assert not AddonUserPendingConfirmation.objects.filter(
|
||||
id=self.pending_author.id
|
||||
).exists()
|
||||
assert pending_user not in self.addon.reload().authors.all()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from collections import OrderedDict
|
||||
|
||||
from django import http
|
||||
from django.db.models import Prefetch
|
||||
from django.db.models import Max, Prefetch
|
||||
from django.db.transaction import non_atomic_requests
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.cache import patch_cache_control
|
||||
|
@ -19,6 +19,7 @@ from rest_framework.mixins import (
|
|||
RetrieveModelMixin,
|
||||
UpdateModelMixin,
|
||||
)
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
|
@ -71,7 +72,7 @@ from olympia.versions.models import Version
|
|||
|
||||
from .decorators import addon_view_factory
|
||||
from .indexers import AddonIndexer
|
||||
from .models import Addon, ReplacementAddon
|
||||
from .models import Addon, AddonUser, AddonUserPendingConfirmation, ReplacementAddon
|
||||
from .serializers import (
|
||||
AddonEulaPolicySerializer,
|
||||
AddonAuthorSerializer,
|
||||
|
@ -83,6 +84,7 @@ from .serializers import (
|
|||
ESAddonSerializer,
|
||||
LanguageToolsSerializer,
|
||||
ReplacementAddonSerializer,
|
||||
AddonPendingAuthorSerializer,
|
||||
PreviewSerializer,
|
||||
StaticCategorySerializer,
|
||||
VersionSerializer,
|
||||
|
@ -376,7 +378,6 @@ class AddonChildMixin:
|
|||
used."""
|
||||
if hasattr(self, 'addon_object'):
|
||||
return self.addon_object
|
||||
|
||||
if permission_classes is None:
|
||||
permission_classes = (
|
||||
AddonViewSet.write_permission_classes
|
||||
|
@ -699,7 +700,7 @@ class AddonAuthorViewSet(
|
|||
# for list and get - see check_permissions and check_object_permissions. Permissions
|
||||
# are also always checked against the parent add-on in get_addon_object() using
|
||||
# AddonViewSet's permissions.
|
||||
permission_classes = []
|
||||
permission_classes = [APIGatePermission('addon-submission-api')]
|
||||
authentication_classes = [
|
||||
JWTKeyAuthentication,
|
||||
SessionIDAuthentication,
|
||||
|
@ -718,7 +719,6 @@ class AddonAuthorViewSet(
|
|||
# We're overriding the permission_classes to restrict access to add-on authors.
|
||||
addon = self.get_addon_object(
|
||||
permission_classes=[
|
||||
APIGatePermission('addon-submission-api'),
|
||||
AnyOf(
|
||||
AllowAddonAuthor,
|
||||
AllowListedViewerOrReviewer,
|
||||
|
@ -726,7 +726,9 @@ class AddonAuthorViewSet(
|
|||
),
|
||||
]
|
||||
)
|
||||
return super().check_object_permissions(request, addon)
|
||||
return super().check_permissions(request) and super().check_object_permissions(
|
||||
request, addon
|
||||
)
|
||||
|
||||
def check_object_permissions(self, request, obj):
|
||||
# check_permissions will have been executed by this point, enforcing that add-on
|
||||
|
@ -743,12 +745,60 @@ class AddonAuthorViewSet(
|
|||
return self.get_addon_object().addonuser_set.all().order_by('position')
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
serializer = self.get_serializer(instance)
|
||||
serializer.validate_role(value=None)
|
||||
serializer.validate_listed(value=None)
|
||||
if isinstance(instance, AddonUser):
|
||||
serializer = self.get_serializer(instance)
|
||||
serializer.validate_role(value=None)
|
||||
serializer.validate_listed(value=None)
|
||||
return super().perform_destroy(instance)
|
||||
|
||||
|
||||
class AddonPendingAuthorViewSet(CreateModelMixin, AddonAuthorViewSet):
|
||||
serializer_class = AddonPendingAuthorSerializer
|
||||
permission_classes = [APIGatePermission('addon-submission-api'), IsAuthenticated]
|
||||
|
||||
def check_permissions(self, request):
|
||||
# pending_confirm needs to be confirmable without being an addon author already
|
||||
if self.action in ('confirm', 'decline'):
|
||||
# .get_addon_object is cached so we just need to call without restricted
|
||||
# permission_classes first and it'll be used for subsequent calls too.
|
||||
self.get_addon_object(permission_classes=())
|
||||
|
||||
return super().check_permissions(request)
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_addon_object().addonuserpendingconfirmation_set.all()
|
||||
|
||||
def get_object_for_request(self, request):
|
||||
try:
|
||||
return self.get_queryset().get(user=request.user)
|
||||
except AddonUserPendingConfirmation.DoesNotExist:
|
||||
raise exceptions.PermissionDenied()
|
||||
|
||||
@action(detail=False, methods=('post',))
|
||||
def confirm(self, request, *args, **kwargs):
|
||||
pending = self.get_object_for_request(request)
|
||||
|
||||
max_position = super().get_queryset().aggregate(max=Max('position'))['max'] or 0
|
||||
AddonUser.unfiltered.update_or_create(
|
||||
addon=pending.addon,
|
||||
user=pending.user,
|
||||
defaults={
|
||||
'role': pending.role,
|
||||
'listed': pending.listed,
|
||||
'position': max_position + 1,
|
||||
},
|
||||
)
|
||||
# The invitation is now obsolete.
|
||||
pending.delete()
|
||||
|
||||
return Response()
|
||||
|
||||
@action(detail=False, methods=('post',))
|
||||
def decline(self, request, *args, **kwargs):
|
||||
self.get_object_for_request(request).delete()
|
||||
return Response()
|
||||
|
||||
|
||||
class AddonSearchView(ListAPIView):
|
||||
authentication_classes = []
|
||||
filter_backends = [
|
||||
|
|
|
@ -811,8 +811,7 @@ class EmailUserRestriction(RestrictionAbstractBaseModel, NormalizeEmailMixin):
|
|||
)
|
||||
|
||||
error_message = _(
|
||||
'The email address used for your account is not '
|
||||
'allowed for add-on submission.'
|
||||
'The email address used for your account is not allowed for add-on submission.'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
|
Загрузка…
Ссылка в новой задаче