Skip to content

Commit 873273e

Browse files
authored
Added permissions endpoint (#313)
* Added permissions endpoint * Refactor * Make serializers required * Annotators can list IdentificationTask (filtered) * Refactor MyPermissionViewSet * Added tests * Added IdentificationTaskQuerySet.browsable * Make BaseRolePermissionSerializer._get_role abstract * Refactor * Check count is 0 * Fix URL for update * Refactor browsable
1 parent cd6e75b commit 873273e

33 files changed

+2419
-52
lines changed

api/mixins.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from typing import Optional
22

3+
from rest_framework.exceptions import APIException
4+
35
from tigacrafting.models import IdentificationTask
46

57
class IdentificationTaskNestedAttribute():
@@ -10,8 +12,31 @@ def get_identification_task_obj(self) -> Optional[IdentificationTask]:
1012
if task_id := self.kwargs.get(self.get_parent_lookup_url_kwarg(), None):
1113
return IdentificationTask.objects.get(pk=task_id)
1214

13-
def get_permissions(self):
14-
return [
15-
permission(identification_task=self.get_identification_task_obj())
16-
for permission in self.permission_classes
17-
]
15+
def check_parent_permissions(self, request) -> bool:
16+
from .views import IdentificationTaskViewSet
17+
if self.action in ['list', 'retrieve']:
18+
parent_view = IdentificationTaskViewSet()
19+
parent_view.action = 'retrieve'
20+
parent_view.request = request
21+
parent_view.kwargs = self.kwargs
22+
23+
identification_task_obj = self.get_identification_task_obj()
24+
25+
try:
26+
parent_view.check_object_permissions(request, identification_task_obj)
27+
except APIException:
28+
pass
29+
else:
30+
return True
31+
return False
32+
33+
def check_permissions(self, request):
34+
if self.check_parent_permissions(request):
35+
return
36+
# If parent permissions are not checked or failed, check the current view's permissions
37+
super().check_permissions(request)
38+
39+
def check_object_permissions(self, request, obj):
40+
if self.check_parent_permissions(request):
41+
return
42+
super().check_object_permissions(request, obj)

api/permissions.py

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
from typing import Optional
2-
31
from django.contrib.auth import get_user_model
42
from django.core.exceptions import MultipleObjectsReturned
53

64
from rest_framework import permissions
75

8-
from tigacrafting.models import IdentificationTask, ExpertReportAnnotation
6+
from tigacrafting.models import IdentificationTask, ExpertReportAnnotation, UserStat
97
from tigaserver_app.models import TigaUser, Notification
108

119
from .utils import get_fk_fieldnames
@@ -137,6 +135,51 @@ def has_permission(self, request, view):
137135
return super().has_permission(request=request, view=view)
138136
return False
139137

138+
class UserRolePermission(permissions.BasePermission):
139+
ACTION_TO_PERMISSION = {
140+
'retrieve': 'view',
141+
'list': 'view',
142+
'update': 'change',
143+
'create': 'add',
144+
'destroy': 'delete'
145+
}
146+
147+
def check_permissions(self, user, action, obj_or_klass) -> bool:
148+
if isinstance(user, User):
149+
user = UserStat.objects.filter(user=user).first()
150+
if not user:
151+
return False
152+
153+
return user.has_role_permission(
154+
action=action,
155+
obj_or_klass=obj_or_klass
156+
) if action is not None else False
157+
158+
def has_permission(self, request, view):
159+
if not request.user or not request.user.is_authenticated:
160+
return False
161+
162+
if not view.action:
163+
return False
164+
165+
return self.check_permissions(
166+
user=request.user,
167+
action=self.ACTION_TO_PERMISSION.get(view.action),
168+
obj_or_klass=view.get_queryset().model
169+
)
170+
171+
def has_object_permission(self, request, view, obj):
172+
if not request.user or not request.user.is_authenticated:
173+
return False
174+
175+
if not view.action:
176+
return False
177+
178+
return self.check_permissions(
179+
user=request.user,
180+
action=self.ACTION_TO_PERMISSION.get(view.action),
181+
obj_or_klass=obj
182+
)
140183

141184
class BaseIdentificationTaskPermissions(FullDjangoModelPermissions):
142185
def _check_is_annotator(self, request, view, obj) -> bool:
@@ -160,6 +203,8 @@ def _check_is_annotator(self, request, view, obj) -> bool:
160203
return task.annotators.filter(pk=request.user.pk).exists()
161204

162205
def has_permission(self, request, view):
206+
if isinstance(request.user, TigaUser):
207+
return False
163208
if request.user and request.user.is_authenticated:
164209
if view.action == 'retrieve':
165210
return True
@@ -176,7 +221,19 @@ def has_object_permission(self, request, view, obj):
176221
return request.user.has_perms(perms)
177222

178223
class IdentificationTaskPermissions(BaseIdentificationTaskPermissions):
179-
pass
224+
def has_permission(self, request, view):
225+
if not request.user or not request.user.is_authenticated:
226+
return False
227+
228+
role_perm = False
229+
if view.action == 'list':
230+
role_perm = UserRolePermission().check_permissions(
231+
user=request.user,
232+
action='add',
233+
obj_or_klass=ExpertReportAnnotation
234+
)
235+
236+
return super().has_permission(request, view) or role_perm
180237

181238
class MyIdentificationTaskPermissions(DjangoRegularUserModelPermissions):
182239
pass
@@ -189,20 +246,16 @@ def has_permission(self, request, view):
189246
return request.user.has_perm('%(app_label)s.add_%(model_name)s' % {
190247
'app_label': ExpertReportAnnotation._meta.app_label,
191248
'model_name': ExpertReportAnnotation._meta.model_name
192-
})
249+
}) or UserRolePermission().check_permissions(
250+
user=request.user,
251+
action='add',
252+
obj_or_klass=ExpertReportAnnotation
253+
)
193254

194255
class BaseIdentificationTaskAttributePermissions(BaseIdentificationTaskPermissions):
195-
def __init__(self, identification_task, *args, **kwargs):
196-
self.identification_task = identification_task
197-
super().__init__(*args, **kwargs)
198-
199-
def has_permission(self, request, view):
200-
if view.action == 'list' and self.identification_task:
201-
if self._check_is_annotator(request, view, obj=self.identification_task):
202-
return True
203-
return super().has_permission(request, view)
204256
pass
205257

258+
206259
class AnnotationPermissions(BaseIdentificationTaskAttributePermissions):
207260
# Always allow retrieve owned attributes
208261
def has_object_permission(self, request, view, obj):

api/serializers.py

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
from abc import abstractmethod
12
from datetime import datetime
2-
from typing import Literal, Optional
3+
from typing import Literal, Optional, Union
34
from uuid import UUID
45

56
from django.contrib.auth import get_user_model
@@ -11,6 +12,7 @@
1112
from rest_framework import serializers
1213

1314
from drf_extra_fields.geo_fields import PointField
15+
from rest_framework_dataclasses.serializers import DataclassSerializer
1416
from taggit.serializers import TaggitSerializer
1517

1618
from tigacrafting.models import (
@@ -21,6 +23,7 @@
2123
PhotoPrediction,
2224
FavoritedReports
2325
)
26+
from tigacrafting.permissions import Permissions, Role
2427
from tigaserver_app.models import (
2528
NotificationContent,
2629
Notification,
@@ -102,6 +105,88 @@ class Meta:
102105
"received_at": {"source": "server_upload_time"},
103106
}
104107

108+
class PermissionsSerializer(DataclassSerializer):
109+
class Meta:
110+
dataclass = Permissions
111+
112+
class BaseRolePermissionSerializer(serializers.Serializer):
113+
role = serializers.SerializerMethodField()
114+
permissions = serializers.SerializerMethodField()
115+
116+
@abstractmethod
117+
def _get_role(self, obj: Union[User, TigaUser]) -> Role:
118+
raise NotImplementedError
119+
120+
def get_role(self, obj: Union[User, TigaUser]) -> Role:
121+
if isinstance(obj, User):
122+
try:
123+
obj = obj.userstat
124+
except UserStat.DoesNotExist:
125+
obj = None
126+
127+
if not obj:
128+
obj = TigaUser()
129+
return self._get_role(obj=obj)
130+
131+
@extend_schema_field(PermissionsSerializer)
132+
def get_permissions(self, obj: Union[User, TigaUser]):
133+
if isinstance(obj, User):
134+
try:
135+
obj = obj.userstat
136+
except UserStat.DoesNotExist:
137+
obj = None
138+
139+
if not obj:
140+
obj = TigaUser()
141+
142+
permissions = obj.get_role_permissions(
143+
role=self.get_role(obj=obj)
144+
)
145+
return PermissionsSerializer(permissions).data
146+
147+
class UserPermissionSerializer(serializers.Serializer):
148+
class GeneralPermissionSerializer(BaseRolePermissionSerializer):
149+
is_staff = serializers.BooleanField()
150+
151+
def _get_role(self, obj: Union[User, TigaUser]) -> Role:
152+
return obj.get_role()
153+
154+
class CountryPermissionSerializer(BaseRolePermissionSerializer):
155+
def __init__(self, *args, **kwargs):
156+
self.country = kwargs.pop('country', None)
157+
super().__init__(*args, **kwargs)
158+
159+
country = serializers.SerializerMethodField()
160+
161+
def _get_role(self, obj: Union[User, TigaUser]) -> Role:
162+
return obj.get_role(country=self.country)
163+
164+
@extend_schema_field(CountrySerializer)
165+
def get_country(self, obj: Union[User, TigaUser]):
166+
return CountrySerializer(self.country).data
167+
168+
general = serializers.SerializerMethodField()
169+
countries = serializers.SerializerMethodField()
170+
171+
@extend_schema_field(GeneralPermissionSerializer)
172+
def get_general(self, obj: Union[User, TigaUser]):
173+
return self.GeneralPermissionSerializer(obj).data
174+
175+
@extend_schema_field(CountryPermissionSerializer(many=True))
176+
def get_countries(self, obj: Union[User, TigaUser]):
177+
if isinstance(obj, User):
178+
try:
179+
obj = obj.userstat
180+
except UserStat.DoesNotExist:
181+
obj = None
182+
if not obj:
183+
return self.CountryPermissionSerializer(many=True).data
184+
185+
result = []
186+
for country in obj.get_countries_with_roles():
187+
result.append(self.CountryPermissionSerializer(instance=obj, country=country).data)
188+
return result
189+
105190
class UserSerializer(serializers.ModelSerializer):
106191
class UserScoreSerializer(serializers.ModelSerializer):
107192
value = serializers.IntegerField(source="score_v2", min_value=0, read_only=True)
@@ -936,7 +1021,21 @@ def validate(self, data):
9361021
data["status"] = ExpertReportAnnotation.STATUS_PUBLIC
9371022

9381023
data['validation_complete_executive'] = data.pop("is_decisive")
939-
1024+
user_role = data['user']
1025+
if isinstance(user_role, User):
1026+
try:
1027+
user_role = user_role.userstat
1028+
except UserStat.DoesNotExist:
1029+
user_role = None
1030+
can_set_is_decisive = False
1031+
if user_role:
1032+
can_set_is_decisive = user_role.has_role_permission_by_model(
1033+
action='mark_as_decisive',
1034+
model=ExpertReportAnnotation,
1035+
country=data['report'].country
1036+
)
1037+
if not can_set_is_decisive:
1038+
data['validation_complete_executive'] = False
9401039
return data
9411040

9421041
def create(self, validated_data):

api/tests/conftest.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from rest_framework.authtoken.models import Token
88

9+
from django.contrib.auth.models import Group
910
from django.core.files.uploadedfile import SimpleUploadedFile
1011
from django.core.management import call_command
1112
from django.contrib.auth import get_user_model
@@ -18,7 +19,7 @@
1819
from api.tests.utils import grant_permission_to_user
1920
from api.tests.clients import AppAPIClient
2021

21-
from tigacrafting.models import IdentificationTask, Taxon
22+
from tigacrafting.models import IdentificationTask, Taxon, UserStat
2223
from tigaserver_app.models import EuropeCountry, Report, Photo
2324

2425
from .factories import create_mobile_user, create_regular_user
@@ -250,4 +251,47 @@ def taxon_root():
250251
rank=Taxon.TaxonomicRank.CLASS,
251252
name="Insecta",
252253
common_name=""
253-
)
254+
)
255+
256+
@pytest.fixture
257+
def group_expert():
258+
group, _ = Group.objects.get_or_create(name="expert")
259+
return group
260+
261+
@pytest.fixture
262+
def group_superexpert():
263+
group, _ = Group.objects.get_or_create(name="superexpert")
264+
return group
265+
266+
@pytest.fixture
267+
def user_with_role_annotator(user, group_expert):
268+
user.groups.add(group_expert)
269+
return user
270+
271+
@pytest.fixture
272+
def user_with_role_annotator_in_country(user_with_role_annotator, es_country):
273+
user_stat = UserStat.objects.get(user=user_with_role_annotator)
274+
user_stat.native_of = es_country
275+
user_stat.save()
276+
277+
return user_with_role_annotator
278+
279+
@pytest.fixture
280+
def user_with_role_supervisor_in_country(user, group_expert, es_country):
281+
user.groups.add(group_expert)
282+
user_stat = UserStat.objects.get(user=user)
283+
user_stat.national_supervisor_of = es_country
284+
user_stat.save()
285+
286+
return user
287+
288+
@pytest.fixture
289+
def user_with_role_reviewer(user, group_superexpert):
290+
user.groups.add(group_superexpert)
291+
return user
292+
293+
@pytest.fixture
294+
def user_with_role_admin(user):
295+
user.is_superuser = True
296+
user.save()
297+
return user

0 commit comments

Comments
 (0)