diff --git a/docs/api/api.md b/docs/api/api.md index 21b962eb..e9b1b347 100644 --- a/docs/api/api.md +++ b/docs/api/api.md @@ -683,7 +683,7 @@ Documents](#sharing-documents) | ------ | ------- | --------------- | ----------------------------------------------------------------- | | access | String | Default: `view` | The [access level](#sharing-documents) for this collaborator | | email | Email | Create Only | Email address of user to add as a collaborator to this project | -| user | Integer | Read Only | The ID for the [user](#user) who is collaborating on this project | +| user | Integer | Read and Create | The ID for the [user](#user) who is collaborating on this project | [Expandable fields](#expandable-fields): user @@ -691,8 +691,8 @@ Documents](#sharing-documents) - `GET /api/projects//users/` - List collaborators on the project - `POST /api/projects//users/` - Add a collaborator to the project - — you must know the email address of a user with a DocumentCloud - account in order to add them as a collaborator on your project + — you must know either the ID or the email address of a user with a + DocumentCloud account in order to add them as a collaborator on your project - `GET /api/projects//users//` - Get a collaborator in the project - `PUT /api/projects//users//` - Update collaborator in the project - `PATCH /api/projects//users//` - Partial update collaborator in the project diff --git a/documentcloud/projects/serializers.py b/documentcloud/projects/serializers.py index 009dcb4c..2c0aaca2 100644 --- a/documentcloud/projects/serializers.py +++ b/documentcloud/projects/serializers.py @@ -189,13 +189,25 @@ def validate(self, attrs): class CollaborationSerializer(FlexFieldsModelSerializer): + user = serializers.PrimaryKeyRelatedField( + label=_("User"), + queryset=User.objects.all(), + required=False, + help_text=_( + "The ID of the user you wish to add as a collaborator. " + "Either this or `email` is required on creation." + ), + ) email = serializers.SlugRelatedField( label=_("Email"), - source="user", slug_field="email", write_only=True, + required=False, queryset=User.objects.all(), - help_text=_("The email address of the user you wish to add as a collaborator"), + help_text=_( + "The email address of the user you wish to add as a collaborator. " + "Either this or `user` is required on creation." + ), error_messages={ **serializers.SlugRelatedField.default_error_messages, "does_not_exist": _( @@ -217,7 +229,6 @@ class CollaborationSerializer(FlexFieldsModelSerializer): class Meta: model = Collaboration fields = ["user", "email", "access"] - extra_kwargs = {"user": {"read_only": True}} expandable_fields = {"user": ("documentcloud.users.UserSerializer", {})} def validate_access(self, value): @@ -239,20 +250,38 @@ def validate_access(self, value): return value - def validate_email(self, value): + def validate(self, attrs): + """The user may be specified by either ID or email""" + email_user = attrs.pop("email", None) + user = attrs.get("user") + if user and email_user and user != email_user: + raise serializers.ValidationError( + "`user` and `email` must refer to the same user" + ) + user = user or email_user + + if user is None: + if not self.instance: + raise serializers.ValidationError( + {"user": "You must specify either `user` or `email`"} + ) + return attrs + view = self.context.get("view") project = Project.objects.get(pk=view.kwargs["project_pk"]) - # on updates check that email does not change - if self.instance and self.instance.user != value: - raise serializers.ValidationError("You may not update `email`") + # on updates check that user does not change + if self.instance and self.instance.user != user: + raise serializers.ValidationError({"user": "You may not update `user`"}) # check for duplicates on creation (instance is none implies creation) - if ( - not self.instance - and project.collaborators.filter(email=value.email).exists() - ): + if not self.instance and project.collaborators.filter(pk=user.pk).exists(): raise serializers.ValidationError( - f"You may not add user {value.email} as a collaborator more than once" + { + "user": f"You may not add user {user.name} as a collaborator " + "more than once" + } ) - return value + + attrs["user"] = user + return attrs diff --git a/documentcloud/projects/tests/test_views.py b/documentcloud/projects/tests/test_views.py index b80533fd..0314e920 100644 --- a/documentcloud/projects/tests/test_views.py +++ b/documentcloud/projects/tests/test_views.py @@ -673,8 +673,15 @@ def test_list_queries(self, client, expand): assert num_queries == len(connection.queries) assert len(response.json()["results"]) == size + small_size + 1 - def test_create(self, client, project, user): - """Add a user to a project""" + def test_create_user(self, client, project, user): + """Add a user to a project by id""" + client.force_authenticate(user=project.user) + response = client.post(f"/api/projects/{project.pk}/users/", {"user": user.pk}) + assert response.status_code == status.HTTP_201_CREATED + assert project.collaborators.filter(pk=user.pk).exists() + + def test_create_email(self, client, project, user): + """Add a user to a project by email""" client.force_authenticate(user=project.user) response = client.post( f"/api/projects/{project.pk}/users/", {"email": user.email} @@ -682,6 +689,28 @@ def test_create(self, client, project, user): assert response.status_code == status.HTTP_201_CREATED assert project.collaborators.filter(pk=user.pk).exists() + def test_create_bad_user(self, client, project): + """Trying adding a user ID that does not exist""" + client.force_authenticate(user=project.user) + response = client.post(f"/api/projects/{project.pk}/users/", {"user": 0}) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_create_bad_mismatch(self, client, project, user): + """User ID and email must match if both are given""" + other_user = UserFactory() + client.force_authenticate(user=project.user) + response = client.post( + f"/api/projects/{project.pk}/users/", + {"user": user.pk, "email": other_user.email}, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_create_bad_missing(self, client, project): + """Must specify a user ID or email""" + client.force_authenticate(user=project.user) + response = client.post(f"/api/projects/{project.pk}/users/", {"access": "view"}) + assert response.status_code == status.HTTP_400_BAD_REQUEST + def test_create_bad_collaborator(self, client, project, user): """You may not add a collaborator to a project you are not a collaborator on""" client.force_authenticate(user=user)