Repository: doccano/doccano

Issue:
Allow user to change another users' annotation
How to reproduce the behaviour
---------
<!-- Before submitting an issue, make sure to check the docs and closed issues and FAQ to see if any of the solutions work for you. https://github.com/chakki-works/doccano/wiki/Frequently-Asked-Questions -->
I have added the permission to allow users change the annotations.
But I cannot find where to allow users to change other people's annotations.
This is especially important because I've already had some annotations. I need multiple people to correct these annotations.

<!--
Include a code example or the steps that led to the problem. Please try to be as specific as possible. -->

Your Environment
---------
<!-- Include details of your environment. -->

*   Operating System: linux
*   Python Version Used: python 3.6
*   When you install doccano: Oct 2019
*   How did you install doccano (Heroku button etc):



Discussion:
Did you try Share annotations across all users"?

![image](https://user-images.githubusercontent.com/544269/72195532-1c02d180-3456-11ea-8b2f-763b810f1406.png)

@icoxfog417 yes, i've set this option when creating the project.
#519 ?
@Slyne Thank you for reporting, please help me to clarify your problem.

* You checked "Share annotations across all users" then ...
  * You can't "watch" any annotations by other users?
  * You can't "edit" any annotations by other users?
  * You can't "control" the role of other annotators for editing/watching?

Yes, I checked "share annotations across all users" and also set collaborative annotation in admin page.
I can watch annotations by other users.
I can't edit any annotations by other users.
I'm not sure where to control the role.  I give all the permissions to all users on the admin page but it still doesn't work.
@Slyne Thank you for the clarification. We arrange the relation between the feature to share the annotation and the feature to control the authority to it (read/write).

|         | read                                     | write       |
|---------|------------------------------------------|-------------|
| private | (default)                                  | (default)     |
| share   | "Share annotations across all users" now | **missing** |
I think this should be fixed with my implementation of #703 if it gets approved. After that annotation approvers can edit/remove other annotations that were made by annotators.
I guess annotator should be able to edit other annotators tags only if document was not approved. 
To make my mind clear:

- If `share annotation` is not checked:
  - all the roles can't view other's annotations.
- If `share annotation` is checked:
  - all the roles can view other's annotations.
  - `project_admin` and `annotation_approver` can edit/delete other's annotations.
  - `annotator` cannot edit/delete other's annotations.

Ideal situation:
- If `share annotation` is not checked, all the roles can view/edit/delete only their own annotations.
- If `share annotation` is checked, all the roles can view/edit/delete other's annotations.
- (I don't think `annotation approver` is allowed to annotate. we need to design roles and their permissions more clearly.)

Solution:
- Make `permission_classes` in `AnnotationDetail` dependent on `share annotation` option.
- If `share annotation` is not checked, `permission_classes` has `[IsAuthenticated & IsProjectAdmin & IsOwnAnnotation]`.
- If `share annotation` is checked, `permission_classes` has `[IsAuthenticated & IsInProjectOrAdmin]`.

The way:
- overwrite `get_permissions` method.

```
class AnnotationDetail(generics.RetrieveUpdateDestroyAPIView):
    lookup_url_kwarg = 'annotation_id'
    # permission_classes = [IsAuthenticated & IsInProjectOrAdmin]
    swagger_schema = None

    def get_permissions(self):
        project = get_object_or_404(Project, pk=self.kwargs['project_id'])
        if project.collaborative_annotation:
            self.permission_classes = [IsAuthenticated & IsInProjectOrAdmin]
        else:
            self.permission_classes = [IsAuthenticated & IsInProjectOrAdmin & IsOwnAnnotation]
        return super().get_permissions()
```

Resolving patch:
diff --git a/app/api/views.py b/app/api/views.py
--- a/app/api/views.py
+++ b/app/api/views.py
@@ -235,9 +235,16 @@ def check_single_class_classification(project_id, doc_id, user):
 
 class AnnotationDetail(generics.RetrieveUpdateDestroyAPIView):
     lookup_url_kwarg = 'annotation_id'
-    permission_classes = [IsAuthenticated & (((IsAnnotator & IsOwnAnnotation) | IsAnnotationApprover)  | IsProjectAdmin)]
     swagger_schema = None
 
+    def get_permissions(self):
+        project = get_object_or_404(Project, pk=self.kwargs['project_id'])
+        if project.collaborative_annotation:
+            self.permission_classes = [IsAuthenticated & IsInProjectOrAdmin]
+        else:
+            self.permission_classes = [IsAuthenticated & IsInProjectOrAdmin & IsOwnAnnotation]
+        return super().get_permissions()
+
     def get_serializer_class(self):
         project = get_object_or_404(Project, pk=self.kwargs['project_id'])
         self.serializer_class = project.get_annotation_serializer()


Test patch:
diff --git a/app/api/tests/test_api.py b/app/api/tests/test_api.py
--- a/app/api/tests/test_api.py
+++ b/app/api/tests/test_api.py
@@ -880,9 +880,11 @@ def setUpTestData(cls):
         another_entity = mommy.make('SequenceAnnotation',
                                     document=main_project_doc, user=another_project_member)
 
-        sub_project = mommy.make('SequenceLabelingProject', users=[non_project_member])
-        sub_project_doc = mommy.make('Document', project=sub_project)
-        mommy.make('SequenceAnnotation', document=sub_project_doc)
+        shared_project = mommy.make('SequenceLabelingProject',
+                                    collaborative_annotation=True,
+                                    users=[project_member, another_project_member])
+        shared_project_doc = mommy.make('Document', project=shared_project)
+        shared_entity = mommy.make('SequenceAnnotation', document=shared_project_doc, user=another_project_member)
 
         cls.url = reverse(viewname='annotation_detail', args=[main_project.id,
                                                               main_project_doc.id,
@@ -890,9 +892,12 @@ def setUpTestData(cls):
         cls.another_url = reverse(viewname='annotation_detail', args=[main_project.id,
                                                                       main_project_doc.id,
                                                                       another_entity.id])
+        cls.shared_url = reverse(viewname='annotation_detail', args=[shared_project.id,
+                                                                     shared_project_doc.id,
+                                                                     shared_entity.id])
         cls.post_data = {'start_offset': 0, 'end_offset': 10}
-        assign_user_to_role(project_member=project_member, project=main_project,
-                            role_name=settings.ROLE_ANNOTATOR)
+        assign_user_to_role(project_member=project_member, project=main_project, role_name=settings.ROLE_ANNOTATOR)
+        assign_user_to_role(project_member=project_member, project=shared_project, role_name=settings.ROLE_ANNOTATOR)
 
     def test_returns_annotation_to_project_member(self):
         self.client.login(username=self.project_member_name,
@@ -954,6 +959,18 @@ def test_disallows_project_member_to_delete_annotation_of_another_member(self):
         response = self.client.delete(self.another_url, format='json', data=self.post_data)
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
+    def test_allow_member_to_update_others_annotation_in_shared_project(self):
+        self.client.login(username=self.project_member_name,
+                          password=self.project_member_pass)
+        response = self.client.patch(self.shared_url, format='json', data=self.post_data)
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+    def test_allow_member_to_delete_others_annotation_in_shared_project(self):
+        self.client.login(username=self.project_member_name,
+                          password=self.project_member_pass)
+        response = self.client.delete(self.shared_url, format='json')
+        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+
     @classmethod
     def doCleanups(cls):
         remove_all_role_mappings()