from django.utils.translation import gettext_lazy as _ # from rest_framework import viewsets from rest_framework import status from rest_framework.decorators import action, api_view from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.generics import get_object_or_404 from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from drf_yasg.utils import swagger_auto_schema from .models import Study, StudyRole, StudyRoleNames from .serializers import ContributorSerializer, ContributorUpdateSerializer, InviteSerializer, StudyRoleSerializer, StudySerializer, StudyDetailSerializer from apps.researcher.models import Researcher from apps.university.serializers import StudyFieldSerializer class Studies(ModelViewSet): """ API endpoint for creating/reading/updating/deleting studies. """ queryset = Study.objects.all().order_by('name') serializer_class = StudyDetailSerializer http_method_names = ['get', 'post', 'put', 'delete'] serializer_class_by_action = { 'create': StudySerializer, 'update': StudySerializer, } def get_serializer_class(self): if hasattr(self, 'serializer_class_by_action'): return self.serializer_class_by_action.get(self.action, self.serializer_class) return super().get_serializer_class() def get_queryset(self): if getattr(self, 'swagger_fake_view', False): return self.queryset return Study.objects.filter(contributors__in=[self.request.user.researcher]).order_by('name') def _valid_study_field_check(self, serializer): # Check if the study field is correct based on the researcher that is creating this study if serializer.validated_data.get('field').faculty != self.request.user.researcher.faculty: raise ValidationError(_('Study field %(study_field_name)s is not valid for faculty %(faculty_name)s') % {'study_field_name': serializer.validated_data['field'], 'faculty_name': self.request.user.researcher.faculty}) def _valid_contributors_check(self, serializer): contributors = serializer.validated_data.get('contributors', []) if len(contributors) == 0: # There are zero contributors, so add the current logged in user as the first one with admin rights contributors.append({'researcher': self.request.user.researcher, 'role': StudyRoleNames.ADMIN, 'active': True}) else: # Check if the current logged in user is already part of this study creator_in_contributors = any([contributor['researcher'] == self.request.user.researcher for contributor in contributors]) if not creator_in_contributors: # Add the current logged in user as an admin to the study contributors.append({'researcher': self.request.user.researcher, 'role': StudyRoleNames.ADMIN, 'active': True}) # Get a list of the researchers working on the same faculty valid_researchers = list(Researcher.objects.filter(faculty=self.request.user.researcher.faculty).values_list('pk', flat=True)) # Filter the list based on the logged in researcher his faculty contributors = [contributor for contributor in contributors if contributor['researcher'].id in valid_researchers] return contributors def perform_create(self, serializer): # Check if there are already extra contributors added contributors = self._valid_contributors_check(serializer) # Check if the study field is correct based on the researcher that is creating this study self._valid_study_field_check(serializer) serializer.save(owner=self.request.user.researcher, contributors=contributors) def perform_update(self, serializer): # Check if there are already extra contributors added # TODO: Is this needed? As with an update, we cannot change the contributors... # self._valid_contributors_check(serializer) # Check if the study field is correct based on the researcher that is creating this study self._valid_study_field_check(serializer) serializer.save() @swagger_auto_schema(request_body=StudySerializer(many=False), responses={200: StudyDetailSerializer(many=False)}) def create(self, request, *args, **kwargs): study_response = super().create(request, *args, **kwargs) # Get the created study and reload it for different serializer study = get_object_or_404(Study, pk=study_response.data['id'], contributors__in=[self.request.user.researcher]) return Response(StudyDetailSerializer(study).data, status=study_response.status_code) @swagger_auto_schema(request_body=StudySerializer(many=False), responses={200: StudyDetailSerializer(many=False)}) def update(self, request, *args, **kwargs): study_response = super().update(request, *args, **kwargs) # Get the created study and reload it for different serializer study = get_object_or_404(Study, pk=study_response.data['id'], contributors__in=[self.request.user.researcher]) return Response(StudyDetailSerializer(study).data, status=study_response.status_code) # TODO: Figure out how to add the pagination to the schema output... @swagger_auto_schema(responses={200: StudyRoleSerializer(many=True)}) @action(detail=False, methods=['get']) def roles(self, request): """Get all the availabe researcher roles for a study. Args: request ([type]): The incoming web request Returns: StudyRoleSerializer: A list of zero or more study roles """ study_roles = [] for role in StudyRoleNames.choices: study_roles.append({'id': role[0], 'name': role[1]}) page = self.paginate_queryset(study_roles) if page is not None: serializer = StudyRoleSerializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = StudyRoleSerializer(study_roles, many=True) return Response(serializer.data) # TODO: Figure out how to add the pagination to the schema output... @swagger_auto_schema(responses={200: StudyFieldSerializer(many=True)}) @action(detail=False, methods=['get']) def fields(self, request): """Get the logged in researcher his study fields based on the faculty where he belongs to. In other words, this is the list of study fields where the logged in user can do research on. Args: request ([type]): The incoming web request Returns: StudyFieldSerializer: A list with zero or more study fields for the logged in researcher. """ study_fields = request.user.researcher.faculty.studyfield_set.all().order_by('name') page = self.paginate_queryset(study_fields) if page is not None: serializer = StudyFieldSerializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = StudyFieldSerializer(study_fields, many=True) return Response(serializer.data) @swagger_auto_schema(responses={200: StudyDetailSerializer(many=True)}) @action(detail=False, methods=['get']) def invited(self, request): """Show a list of studies where the researcher is invited to. Args: request ([type]): The incoming web request Returns: StudyFieldSerializer: A list with zero or more study fields for the logged in researcher which he is invited to. """ studies = Study.objects.filter(contributors__in=[request.user.researcher], studyrole__active=False, studyrole__invited_at__isnull=False) request.user.researcher.faculty.studyfield_set.all().order_by('name') page = self.paginate_queryset(studies) if page is not None: serializer = StudyDetailSerializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = StudyDetailSerializer(studies, many=True) return Response(serializer.data) class Contributors(ModelViewSet): http_method_names = ['get', 'put', 'post', 'delete'] queryset = StudyRole.objects.all() serializer_class = ContributorSerializer @swagger_auto_schema(responses={200: ContributorSerializer(many=True)}) def list(self, request, *args, **kwargs): """ Get all the contributors that are assigned to a study of which you are also a member of. """ study = get_object_or_404(Study, pk=kwargs['study_id'], contributors__in=[self.request.user.researcher]) contributors = StudyRole.objects.filter(study=study).order_by('researcher__last_name') # Add manual pagination. page = self.paginate_queryset(contributors) if page is not None: serializer = ContributorSerializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = ContributorSerializer(contributors, many=True) return Response(serializer.data) @swagger_auto_schema(responses={200: ContributorSerializer(many=False)}) def get(self, request, *args, **kwargs): """ Get a single contributors that is assigned to a study for a study of which you are also a member of. """ contributor = get_object_or_404(StudyRole, pk=kwargs['contributor_id'], study=kwargs['study_id'], study__contributors__in=[self.request.user.researcher]) serializer = ContributorSerializer(contributor, many=False) return Response(serializer.data) @swagger_auto_schema(request_body=ContributorUpdateSerializer(many=False), responses={200: ContributorSerializer(many=False)}) def update(self, request, *args, **kwargs): """Update an existing contributor role. This can only be done by study admins. Raises: Http404 : When the combination contributor and study does not exists, or you are not a member of this study PermissionDenied: If you are not an admin of this study. ValidationError: If you change the owner rights or when you delete all admins Returns: ContributorSerializer: Serialized data of the new contributor role """ # Check if combination of study and contributor does exists and the logged in user is part of the study contributor = get_object_or_404(StudyRole, pk=kwargs['contributor_id'], study=kwargs['study_id'], study__contributors__in=[self.request.user.researcher]) # Check if logged in user is an admin for this study if not (contributor.study.owner == request.user.researcher or contributor.study.is_an_admin(request.user.researcher)): raise PermissionDenied(_('You are not allowed to update this contributor.')) # We cannot change the study rights for an owner if contributor.researcher == contributor.study.owner: raise ValidationError(_('The rights of an owner of the study cannot be changed or deleted.')) # Load serializer serializer = self.get_serializer(contributor, data=request.data, partial=False) # Check if data is valid serializer.is_valid(raise_exception=True) if contributor.role != serializer.validated_data['role'] and contributor.role == StudyRoleNames.ADMIN: # Make sure at least 1 admin is left if contributor.study.contributors.filter(studyrole__role=StudyRoleNames.ADMIN).exclude(studyrole__researcher=contributor.researcher).count() == 0: raise ValidationError(_('At least 1 administrator needs to remain for this study.')) # Update the role contributor.role = serializer.validated_data['role'] # Save to database contributor.save() serializer = ContributorSerializer(contributor, many=False) return Response(serializer.data) def delete(self, request, *args, **kwargs): # Check if combination of study and contributor does exists and the logged in user is part of the study contributor = get_object_or_404(StudyRole, pk=kwargs['contributor_id'], study=kwargs['study_id'], study__contributors__in=[self.request.user.researcher]) # Check if logged in user is an admin for this study if not (contributor.study.owner == request.user.researcher or contributor.study.is_an_admin(request.user.researcher)): raise PermissionDenied(_('You are not allowed to delete this contributor')) # We cannot change the study rights for an owner if contributor.researcher == contributor.study.owner: raise ValidationError(_('The rights of an owner of the study cannot be removed')) if contributor.role == StudyRoleNames.ADMIN: # Make sure at least 1 admin is left if contributor.study.contributors.filter(studyrole__role=StudyRoleNames.ADMIN).exclude(studyrole__researcher=contributor.researcher).count() == 0: raise ValidationError(_('At least 1 administrator needs to remain for this study')) # Send an email to the researcher that is removed from the study contributor.send_remove_notification_email(sender=self.request.user.researcher) self.perform_destroy(contributor) return Response(status=status.HTTP_204_NO_CONTENT) @swagger_auto_schema(methods=['post'], request_body=InviteSerializer(many=False)) @api_view(['POST']) def process_study_invite(request, *args, **kwargs): # TODO: How is allowed to make an invitation? Owner or any Admin? For now, an owner and any admin can send invitations # Check if we are a contributor of the project and does project exists study = get_object_or_404(Study, pk=kwargs['study_id'], contributors__in=[request.user.researcher], studyrole__active=True) # Check if logged in user is an admin for this study if not (study.owner == request.user.researcher or study.is_an_admin(request.user.researcher)): raise PermissionDenied(_('You are not allowed to invite a new researcher')) invite = InviteSerializer(data=request.data) invite.is_valid(raise_exception=True) contributor = invite.save(study=study) contributor.sent_invitation_email(sender=request.user.researcher) return Response({'message': _('Invitation to %(researcher_name)s for study %(study_name)s is sent.') % {'researcher_name': contributor.researcher.display_name, 'study_name': study.name}}) @swagger_auto_schema(method='get', auto_schema=None) @api_view(['GET']) def validate_study_invite(request, *args, **kwargs): # Check if we are a contributor of the project and does project exists. And make sure the user is not yet activated study = get_object_or_404(Study, pk=kwargs['pk'], contributors__in=[request.user.researcher], studyrole__active=False) # Check if we are invited for this study invitation = get_object_or_404(StudyRole, study=study, researcher=request.user.researcher, active=False) if not invitation.check_invitation_link(request.user.researcher, kwargs['jwt_token']): raise ValidationError(_('Could not validate the invitation to study %(study_name)s') % {'study_name': study.name}) invitation.active = True invitation.save() return Response({'message': _('Invitation for %(researcher_name)s for study %(study_name)s is accepted.') % {'researcher_name': invitation.researcher.display_name, 'study_name': study.name}})