-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7dc2cad
commit 7ee8521
Showing
7 changed files
with
269 additions
and
76 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,29 +1,99 @@ | ||
from django.contrib.auth.models import User | ||
from django.db import transaction | ||
from django.db.models.query_utils import Q | ||
from django.http.response import JsonResponse | ||
from rest_framework import viewsets | ||
from rest_framework.decorators import action | ||
from rest_framework.exceptions import PermissionDenied | ||
from rest_framework.permissions import IsAdminUser | ||
|
||
from isic.core.api import Conflict | ||
from isic.core.permissions import IsicObjectPermissionsFilter, get_visible_objects | ||
from isic.studies.models import Annotation, Study, StudyTask | ||
from isic.studies.serializers import AnnotationSerializer, StudySerializer, StudyTaskSerializer | ||
from isic.studies.serializers import ( | ||
AnnotationSerializer, | ||
StudySerializer, | ||
StudyTaskAssignmentSerializer, | ||
StudyTaskSerializer, | ||
) | ||
|
||
|
||
class StudyTaskViewSet(viewsets.ReadOnlyModelViewSet): | ||
serializer_class = StudyTaskSerializer | ||
queryset = StudyTask.objects.all() | ||
permission_classes = [IsAdminUser] | ||
filter_backends = [IsicObjectPermissionsFilter] | ||
|
||
swagger_schema = None | ||
|
||
|
||
class StudyViewSet(viewsets.ReadOnlyModelViewSet): | ||
serializer_class = StudySerializer | ||
queryset = Study.objects.all() | ||
permission_classes = [IsAdminUser] | ||
queryset = Study.objects.distinct() | ||
filter_backends = [IsicObjectPermissionsFilter] | ||
|
||
swagger_schema = None | ||
|
||
@action(detail=True, methods=['post'], pagination_class=None, url_path='set-tasks') | ||
def set_tasks(self, request, *args, **kwargs): | ||
study: Study = self.get_object() | ||
if not request.user.has_perm('studies.modify_study', study): | ||
raise PermissionDenied | ||
elif study.tasks.filter(annotation__isnull=False).exists(): | ||
raise Conflict('Study has answered questions, tasks cannot be overwritten.') | ||
|
||
serializer = StudyTaskAssignmentSerializer(data=request.data, many=True, max_length=100) | ||
serializer.is_valid(raise_exception=True) | ||
|
||
isic_ids = [x['isic_id'] for x in serializer.validated_data] | ||
identifier_filter = Q() | ||
for data in serializer.validated_data: | ||
identifier_filter |= Q(profile__hash_id__iexact=data['user_hash_id_or_email']) | ||
identifier_filter |= Q(email__iexact=data['user_hash_id_or_email']) | ||
|
||
requested_users = ( | ||
User.objects.select_related('profile').filter(is_active=True).filter(identifier_filter) | ||
) | ||
# create a lookup dictionary that keys users by their hash id and email | ||
requested_users_lookup = {} | ||
for user in requested_users: | ||
requested_users_lookup[user.profile.hash_id] = user | ||
requested_users_lookup[user.email] = user | ||
|
||
requested_images = study.collection.images.filter(isic_id__in=isic_ids) | ||
visible_images = get_visible_objects( | ||
request.user, 'core.view_image', requested_images | ||
).in_bulk(field_name='isic_id') | ||
|
||
summary = { | ||
'image_no_perms_or_does_not_exist': [], | ||
'user_does_not_exist': [], | ||
'succeeded': [], | ||
} | ||
|
||
with transaction.atomic(): | ||
for task_assignment in serializer.validated_data: | ||
if task_assignment['user_hash_id_or_email'] not in requested_users_lookup: | ||
summary['user_does_not_exist'].append(task_assignment['user_hash_id_or_email']) | ||
elif task_assignment['isic_id'] not in visible_images: | ||
summary['image_no_perms_or_does_not_exist'].append(task_assignment['isic_id']) | ||
else: | ||
StudyTask.objects.create( | ||
study=study, | ||
annotator=requested_users_lookup[task_assignment['user_hash_id_or_email']], | ||
image=visible_images[task_assignment['isic_id']], | ||
) | ||
summary['succeeded'].append( | ||
f'{task_assignment["isic_id"]}/{task_assignment["user_hash_id_or_email"]}' | ||
) | ||
|
||
return JsonResponse(summary) | ||
|
||
|
||
class AnnotationViewSet(viewsets.ReadOnlyModelViewSet): | ||
serializer_class = AnnotationSerializer | ||
queryset = Annotation.objects.all() | ||
permission_classes = [IsAdminUser] | ||
filter_backends = [IsicObjectPermissionsFilter] | ||
|
||
swagger_schema = None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import pytest | ||
|
||
|
||
@pytest.fixture | ||
def public_study(study_factory, user_factory): | ||
creator = user_factory() | ||
owners = [creator] + [user_factory() for _ in range(2)] | ||
return study_factory(public=True, creator=creator, owners=owners) | ||
|
||
|
||
@pytest.fixture | ||
def private_study(study_factory, user_factory): | ||
creator = user_factory() | ||
owners = [creator] + [user_factory() for _ in range(2)] | ||
return study_factory(public=False, creator=creator, owners=owners) | ||
|
||
|
||
@pytest.fixture | ||
def private_study_and_guest(private_study, user_factory): | ||
return private_study, user_factory() | ||
|
||
|
||
@pytest.fixture | ||
def private_study_and_annotator(private_study, user_factory, study_task_factory): | ||
u = user_factory() | ||
study_task_factory(annotator=u, study=private_study) | ||
return private_study, u | ||
|
||
|
||
@pytest.fixture | ||
def private_study_and_owner(private_study): | ||
return private_study, private_study.owners.first() | ||
|
||
|
||
@pytest.fixture | ||
def private_study_with_responses(study_factory, user_factory, response_factory): | ||
# create a scenario for testing that a user can only see their responses and | ||
# not another annotators. | ||
study = study_factory(public=False) | ||
u1, u2 = user_factory(), user_factory() | ||
response_factory( | ||
annotation__annotator=u1, | ||
annotation__study=study, | ||
annotation__task__annotator=u1, | ||
annotation__task__study=study, | ||
) | ||
response_factory( | ||
annotation__annotator=u2, | ||
annotation__study=study, | ||
annotation__task__annotator=u2, | ||
annotation__task__study=study, | ||
) | ||
return study, u1, u2 | ||
|
||
|
||
@pytest.fixture | ||
def study_task_with_user(study_task_factory, user_factory): | ||
u = user_factory() | ||
study_task = study_task_factory(annotator=u) | ||
return study_task |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
from unittest import TestCase | ||
|
||
import pytest | ||
|
||
from isic.studies.models import StudyTask | ||
|
||
|
||
@pytest.fixture | ||
def study_with_images(user_factory, image_factory, collection_factory, study_factory): | ||
user = user_factory() | ||
collection = collection_factory(creator=user, public=True) | ||
images = [image_factory(public=True) for _ in range(2)] | ||
collection.images.add(*images) | ||
study = study_factory(creator=user, collection=collection) | ||
return study, images | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_set_tasks(api_client, study_with_images, user_factory): | ||
users = [user_factory() for _ in range(2)] | ||
study, images = study_with_images | ||
api_client.force_login(study.creator) | ||
|
||
r = api_client.post( | ||
f'/api/v2/studies/{study.pk}/set-tasks/', | ||
[ | ||
{'isic_id': images[0].isic_id, 'user_hash_id_or_email': users[0].profile.hash_id}, | ||
{'isic_id': images[1].isic_id, 'user_hash_id_or_email': users[1].email}, | ||
# bad image | ||
{'isic_id': 'ISIC_9999999', 'user_hash_id_or_email': users[1].email}, | ||
# bad user | ||
{'isic_id': images[1].isic_id, 'user_hash_id_or_email': 'FAKEUSER'}, | ||
], | ||
) | ||
assert r.status_code == 200, r.json() | ||
assert StudyTask.objects.count() == 2 | ||
|
||
tasks_actual = list(StudyTask.objects.all().values('study', 'annotator', 'image')) | ||
tasks_expected = [ | ||
{'study': study.pk, 'annotator': users[0].pk, 'image': images[0].pk}, | ||
{'study': study.pk, 'annotator': users[1].pk, 'image': images[1].pk}, | ||
] | ||
for task_actual, task_expected in zip(tasks_actual, tasks_expected): | ||
TestCase().assertDictEqual(task_actual, task_expected) |
Oops, something went wrong.