Skip to content

Commit b7f04a1

Browse files
committed
Allow annotators to access their annotated identification tasks attributes
1 parent 6971fff commit b7f04a1

File tree

5 files changed

+163
-25
lines changed

5 files changed

+163
-25
lines changed

api/permissions.py

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from rest_framework import permissions
55

6-
from tigacrafting.models import ExpertReportAnnotation
6+
from tigacrafting.models import IdentificationTask, ExpertReportAnnotation
77
from tigaserver_app.models import TigaUser, Notification
88

99
from .utils import get_fk_fieldnames
@@ -29,6 +29,11 @@ def has_permission(self, request, view):
2929
request.user, User
3030
) and super().has_permission(request, view)
3131

32+
def has_object_permission(self, request, view, obj):
33+
return isinstance(
34+
request.user, User
35+
) and super().has_object_permission(request, view, obj)
36+
3237
class FullDjangoModelPermissions(DjangoRegularUserModelPermissions):
3338
perms_map = {
3439
**permissions.DjangoObjectPermissions.perms_map,
@@ -130,29 +135,47 @@ def has_permission(self, request, view):
130135
return super().has_permission(request=request, view=view)
131136
return False
132137

133-
class IdentificationTaskPermissions(FullDjangoModelPermissions):
134-
def has_permission(self, request, view):
135-
# Always require authentication
136-
if not request.user or not request.user.is_authenticated:
138+
139+
class BaseIdentificationTaskPermissions(FullDjangoModelPermissions):
140+
def _check_is_annotator(self, request, view, obj) -> bool:
141+
if isinstance(request.user, TigaUser):
137142
return False
138143

139-
if view.action == 'retrieve':
140-
return True
144+
if isinstance(obj, IdentificationTask):
145+
task = obj
146+
else:
147+
inferred_fieldnames = get_fk_fieldnames(
148+
model=obj._meta.model, related_model=IdentificationTask
149+
)
150+
if len(inferred_fieldnames) > 1:
151+
raise MultipleObjectsReturned(
152+
"Model {obj._meta.model} has {len(inferred_fieldnames)} relation to model {TigaUser}."
153+
)
154+
task_fname = inferred_fieldnames[0]
155+
if not hasattr(obj, task_fname):
156+
return False
157+
task = getattr(obj, task_fname)
158+
return task.annotators.filter(pk=request.user.pk).exists()
141159

160+
def has_permission(self, request, view):
161+
if request.user and request.user.is_authenticated:
162+
if view.action == 'retrieve':
163+
return True
142164
return super().has_permission(request, view)
143165

144166
def has_object_permission(self, request, view, obj):
145-
if isinstance(request.user, TigaUser):
167+
if not super().has_object_permission(request, view,obj):
146168
return False
147169

148-
if obj.annotators.filter(pk=request.user.pk).exists():
149-
# If it's a user that has annotated this task, allow access
150-
if view.action == 'retrieve':
151-
return True
170+
if view.action == 'retrieve' and self._check_is_annotator(request, view, obj):
171+
return True
152172

153173
perms = self.get_required_permissions(request.method, obj._meta.model)
154174
return request.user.has_perms(perms)
155175

176+
class IdentificationTaskPermissions(BaseIdentificationTaskPermissions):
177+
pass
178+
156179
class MyIdentificationTaskPermissions(DjangoRegularUserModelPermissions):
157180
pass
158181

@@ -166,25 +189,24 @@ def has_permission(self, request, view):
166189
'model_name': ExpertReportAnnotation._meta.model_name
167190
})
168191

169-
class AnnotationPermissions(FullDjangoModelPermissions):
170-
# Always allow retrieve owned annotations
171-
172-
def has_permission(self, request, view):
173-
if request.user and request.user.is_authenticated and view.action == 'retrieve':
174-
return True
175-
return super().has_permission(request=request, view=view)
192+
class BaseIdentificationTaskAttributePermissions(BaseIdentificationTaskPermissions):
193+
pass
176194

195+
class AnnotationPermissions(BaseIdentificationTaskAttributePermissions):
196+
# Always allow retrieve owned attributes
177197
def has_object_permission(self, request, view, obj):
178198
# Allow retrieve if user is the owner
179199
if view.action == 'retrieve':
180200
if obj.user == request.user:
181201
return True
182-
return super().has_permission(request=request, view=view)
183202
return super().has_object_permission(request, view, obj)
184203

185204
class MyAnnotationPermissions(DjangoRegularUserModelPermissions):
186205
pass
187206

207+
class PhotoPredictionPermissions(BaseIdentificationTaskAttributePermissions):
208+
pass
209+
188210
class TaxaPermissions(UserObjectPermissions):
189211
perms_map = permissions.DjangoModelPermissions.perms_map
190212

api/tests/integration/identification_tasks/annotations/get.tavern.yml

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ marks:
7575
- api_live_url
7676
- endpoint
7777
- annotation
78-
- annotation_from_another_user
7978
- jwt_token_user
8079

8180
stages:
@@ -87,7 +86,48 @@ stages:
8786
Authorization: "Bearer {jwt_token_user:s}"
8887
response:
8988
status_code: 200
90-
- name: User without perm view can not retrieve if not owned annotation
89+
90+
---
91+
92+
test_name: Annotation can be retrieved by users without permissions if from a identification task they have annotate.
93+
94+
includes:
95+
- !include schema.yml
96+
97+
marks:
98+
- usefixtures:
99+
- api_live_url
100+
- endpoint
101+
- annotation
102+
- annotation_from_another_user
103+
- jwt_token_user
104+
105+
stages:
106+
- name: User without perm view can retrieve if annotation from the same task
107+
request:
108+
url: "{api_live_url}/{endpoint}/{annotation_from_another_user.pk}/"
109+
method: "GET"
110+
headers:
111+
Authorization: "Bearer {jwt_token_user:s}"
112+
response:
113+
status_code: 200
114+
115+
---
116+
117+
test_name: Annotation can not be retrieved by users if not owned and they have not annotated in the same identification task.
118+
119+
includes:
120+
- !include schema.yml
121+
122+
marks:
123+
- usefixtures:
124+
- api_live_url
125+
- endpoint
126+
- annotation_from_another_user
127+
- jwt_token_user
128+
129+
stages:
130+
- name: User without perm view can not retrieve if not owned annotation and not annotator from the identification task
91131
request:
92132
url: "{api_live_url}/{endpoint}/{annotation_from_another_user.pk}/"
93133
method: "GET"

api/tests/integration/identification_tasks/predictions/get.tavern.yml

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22

3-
test_name: Predictions can be read only by authenticated users.
3+
test_name: Predictions can be read by users not authenticated or without permissions.
44

55
includes:
66
- !include schema.yml
@@ -28,12 +28,62 @@ stages:
2828
method: "GET"
2929
response:
3030
status_code: 401
31-
- name: User without perm view can retrieve
31+
- name: User without perm view can not retrieve
3232
request:
3333
url: "{api_live_url}/{endpoint}/{photo_prediction.photo.uuid}/"
3434
method: "GET"
3535
headers:
3636
Authorization: "Bearer {jwt_token_user:s}"
37+
response:
38+
status_code: 403
39+
40+
---
41+
42+
test_name: Predictions can be read only by authenticated users with permissions.
43+
44+
includes:
45+
- !include schema.yml
46+
47+
marks:
48+
- usefixtures:
49+
- api_live_url
50+
- endpoint
51+
- photo_prediction
52+
- jwt_token_user_can_view
53+
54+
stages:
55+
- name: User without perm view can not retrieve
56+
request:
57+
url: "{api_live_url}/{endpoint}/{photo_prediction.photo.uuid}/"
58+
method: "GET"
59+
headers:
60+
Authorization: "Bearer {jwt_token_user_can_view:s}"
3761
response:
3862
status_code: 200
3963
json: !force_format_include "{response_data_validation}"
64+
65+
---
66+
67+
test_name: Photo prediction can be retrieved by users without permissions if from a identification task they have annotate.
68+
69+
includes:
70+
- !include schema.yml
71+
72+
marks:
73+
- usefixtures:
74+
- api_live_url
75+
- endpoint
76+
- annotation
77+
- photo_prediction
78+
- jwt_token_user
79+
80+
stages:
81+
- name: User without perm view can retrieve if annotation from the same task
82+
request:
83+
url: "{api_live_url}/{endpoint}/{photo_prediction.photo.uuid}/"
84+
method: "GET"
85+
headers:
86+
Authorization: "Bearer {jwt_token_user:s}"
87+
response:
88+
status_code: 200
89+
json: !force_format_include "{response_data_validation}"

api/tests/integration/identification_tasks/predictions/list.tavern.yml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,36 @@ stages:
2626
method: "GET"
2727
response:
2828
status_code: 401
29-
- name: User without perm view can retrieve
29+
- name: User without perm view can not retrieve
3030
request:
3131
url: "{api_live_url}/{endpoint}/"
3232
method: "GET"
3333
headers:
3434
Authorization: "Bearer {jwt_token_user:s}"
35+
response:
36+
status_code: 403
37+
38+
---
39+
40+
test_name: Predictions can be list only by authenticated users with permissions.
41+
42+
includes:
43+
- !include schema.yml
44+
45+
marks:
46+
- usefixtures:
47+
- api_live_url
48+
- endpoint
49+
- photo_prediction
50+
- jwt_token_user_can_view
51+
52+
stages:
53+
- name: User without perm view can not retrieve
54+
request:
55+
url: "{api_live_url}/{endpoint}/"
56+
method: "GET"
57+
headers:
58+
Authorization: "Bearer {jwt_token_user_can_view:s}"
3559
response:
3660
status_code: 200
3761
json: !force_format_include "{response_list_data_validation}"

api/views.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
IdentificationTaskAssignmentPermissions,
9494
AnnotationPermissions,
9595
MyAnnotationPermissions,
96+
PhotoPredictionPermissions,
9697
TaxaPermissions,
9798
CountriesPermissions
9899
)
@@ -566,6 +567,7 @@ def assign_next(self, request):
566567
)
567568
class PhotoPredictionViewSet(NestedViewSetMixin, CreateModelMixin, RetrieveModelMixin, ListModelMixin, UpdateModelMixin, DestroyModelMixin, GenericNoMobileViewSet):
568569
queryset = PhotoPrediction.objects.all()
570+
permission_classes = (PhotoPredictionPermissions, )
569571

570572
parent_lookup_kwargs = {
571573
'observation_uuid': 'identification_task__pk'

0 commit comments

Comments
 (0)