class ProposalInviteViewSet(mixins.DestroyModelMixin,
                            viewsets.ReadOnlyModelViewSet):
    http_method_names = ('get', 'head', 'options', 'delete')
    schema = ObservationPortalSchema(tags=['Proposals'])
    filter_backends = (
        DjangoFilterBackend,
        filters.OrderingFilter,
    )
    filter_class = ProposalInviteFilter
    serializer_class = import_string(
        settings.SERIALIZERS['proposals']['ProposalInvite'])

    def get_queryset(self):
        if self.request.user.is_staff and self.request.user.profile.staff_view:
            return ProposalInvite.objects.all()
        else:
            proposals = self.request.user.proposal_set.filter(
                membership__role=Membership.PI)
            return ProposalInvite.objects.filter(proposal__in=proposals)

    def get_permissions(self):
        pi_only_actions = ('destroy', )
        if self.action in pi_only_actions:
            permission_classes = [IsPrincipleInvestigator]
        else:
            permission_classes = [IsAuthenticated]
        return [permission() for permission in permission_classes]

    def perform_destroy(self, instance):
        if instance.used is None:
            instance.delete()
class LastScheduledView(APIView):
    """
        Returns the datetime of the last time new observations were submitted. This endpoint is expected to be polled
        frequently (~every 5 seconds) to for a client to decide if it needs to pull down the schedule or not.

        We are only updating when observations are submitted, and not when they are cancelled, because a site should
        not really care if the only change was removing things from it's schedule.
    """
    permission_classes = (IsAdminUser,)
    schema = ObservationPortalSchema(tags=['Observations'])
    filter_backends = (DjangoFilterBackend,)
    filter_class = LastScheduledFilter

    def get(self, request):
        site = request.query_params.get('site')
        cache_key = 'observation_portal_last_schedule_time'
        if site:
            cache_key += f"_{site}"
            last_schedule_time = cache.get(cache_key, timezone.now() - timedelta(days=7))
        else:
            sites = configdb.get_site_tuples()
            keys = [cache_key + "_" + s[0] for s in sites]
            cache_dict = cache.get_many(keys)
            last_schedule_time = max(list(cache_dict.values()) + [timezone.now() - timedelta(days=7)])

        response_serializer = self.get_response_serializer({'last_schedule_time': last_schedule_time})
        return Response(response_serializer.data, status=status.HTTP_200_OK)

    def get_response_serializer(self, *args, **kwargs):
        return import_string(settings.SERIALIZERS['observations']['LastScheduled'])(*args, **kwargs)

    def get_endpoint_name(self):
        return 'getLastScheduled'
Example #3
0
class AccountRemovalRequestApiView(APIView):
    """View to request account removal."""
    permission_classes = [IsAuthenticated]
    schema = ObservationPortalSchema(tags=['Accounts'])

    def post(self, request):
        request_serializer = self.get_request_serializer(data=request.data)
        if request_serializer.is_valid():
            message = 'User {0} would like their account removed.\nReason:\n {1}'.format(
                request.user.email, request_serializer.validated_data['reason']
            )
            send_mail.send(
                'Account removal request submitted', message, settings.ORGANIZATION_EMAIL, [settings.ORGANIZATION_SUPPORT_EMAIL]
            )
            response_serializer = self.get_response_serializer({'message': 'Account removal request successfully submitted.'})
            return Response(response_serializer.data, status=status.HTTP_200_OK)
        else:
            return Response(request_serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def get_request_serializer(self, *args, **kwargs):
        return import_string(settings.SERIALIZERS['accounts']['AccountRemovalRequest'])(*args, **kwargs)

    def get_response_serializer(self, *args, **kwargs):
        return import_string(settings.SERIALIZERS['accounts']['AccountRemovalResponse'])(*args, **kwargs)

    def get_endpoint_name(self):
        return 'requestAccountRemoval'
class AirmassView(APIView):
    """Gets the airmasses for the request at available sites
    """
    permission_classes = (AllowAny, )
    schema = ObservationPortalSchema(tags=['Requests'])

    def post(self, request):
        request_serializer = self.get_request_serializer(data=request.data)
        if request_serializer.is_valid():
            airmass_data = get_airmasses_for_request_at_sites(
                request_serializer.validated_data,
                is_staff=request.user.is_staff)
            return Response(airmass_data)
        else:
            return Response(request_serializer.errors)

    def get_request_serializer(self, *args, **kwargs):
        return import_string(settings.SERIALIZERS['requestgroups']['Request'])(
            *args, **kwargs)

    def get_example_response(self):
        return Response(EXAMPLE_RESPONSES['requestgroups'].get('airmass'),
                        status=status.HTTP_200_OK)

    def get_endpoint_name(self):
        return 'getAirmass'
class ObservationPortalLastChangedView(APIView):
    """Returns the datetime of the last status of requests change or new requests addition
    """
    permission_classes = (IsAdminUser, )
    schema = ObservationPortalSchema(tags=['RequestGroups'],
                                     is_list_view=False)
    filter_class = LastChangedFilter
    filter_backends = (DjangoFilterBackend, )

    def get(self, request):
        telescope_classes = request.GET.getlist('telescope_class', ['all'])
        most_recent_change_time = timezone.now() - timedelta(days=7)
        for telescope_class in telescope_classes:
            most_recent_change_time = max(
                most_recent_change_time,
                cache.get(
                    f"observation_portal_last_change_time_{telescope_class}",
                    timezone.now() - timedelta(days=7)))

        response_serializer = self.get_response_serializer(
            data={'last_change_time': most_recent_change_time})
        if response_serializer.is_valid():
            return Response(response_serializer.validated_data)
        else:
            raise ValidationError(response_serializer.errors)

    def get_response_serializer(self, *args, **kwargs):
        return import_string(
            settings.SERIALIZERS['requestgroups']['LastChanged'])(*args,
                                                                  **kwargs)

    def get_endpoint_name(self):
        return 'getLastChangedTime'
Example #6
0
class ProfileApiView(RetrieveUpdateAPIView):
    serializer_class = import_string(settings.SERIALIZERS['accounts']['User'])
    schema = ObservationPortalSchema(tags=['Accounts'])
    permission_classes = [IsAuthenticated]

    #TODO: Docstrings on get_object are not plumbed into the description for the API endpoint - override this.
    def get_object(self):
        """Once authenticated, retrieve profile data"""
        qs = User.objects.filter(pk=self.request.user.pk).prefetch_related(
            'profile', 'proposal_set', 'proposal_set__timeallocation_set', 'proposalnotification_set'
        )
        return qs.first()
Example #7
0
class CallViewSet(viewsets.ReadOnlyModelViewSet):
    permission_classes = (IsAuthenticated,)
    schema = ObservationPortalSchema(tags=['Science Applications'])
    filter_class = CallFilter
    serializer_class = import_string(settings.SERIALIZERS['sciapplications']['Call'])
    filter_backends = (
        filters.OrderingFilter,
        DjangoFilterBackend
    )

    def get_queryset(self):
        if self.request.user.profile.is_scicollab_admin:
            return Call.objects.all()
        else:
            return Call.objects.all().exclude(proposal_type=Call.COLLAB_PROPOSAL)
class DraftRequestGroupViewSet(viewsets.ModelViewSet):
    schema = ObservationPortalSchema(tags=['RequestGroups'])
    serializer_class = import_string(
        settings.SERIALIZERS['requestgroups']['DraftRequestGroup'])
    ordering = ('-modified', )

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

    def get_queryset(self):
        if self.request.user.is_staff and self.request.user.profile.staff_view:
            return DraftRequestGroup.objects.all()
        elif self.request.user.is_authenticated:
            return DraftRequestGroup.objects.filter(
                proposal__in=self.request.user.proposal_set.all())
        else:
            return DraftRequestGroup.objects.none()
Example #9
0
class RevokeApiTokenApiView(APIView):
    """View to revoke an API token."""
    permission_classes = [IsAuthenticated]
    schema = ObservationPortalSchema(tags=['Accounts'], empty_request=True)

    def post(self, request):
        """A simple POST request (empty request body) with user authentication information in the HTTP header will revoke a user's API Token."""
        request.user.auth_token.delete()
        Token.objects.create(user=request.user)
        serializer = self.get_response_serializer({'message': 'API token revoked.'})
        return Response(serializer.data, status=status.HTTP_200_OK)

    def get_response_serializer(self, *args, **kwargs):
        return import_string(settings.SERIALIZERS['accounts']['RevokeToken'])(*args, **kwargs)

    def get_endpoint_name(self):
        return 'revokeApiToken'
class ContentionView(APIView):
    """Retrieve the contention for a given instrument type binned by RA hour. For every RA hour, the time currently requested
    on this instrument type for the next 24 hours is returned.
    """
    permission_classes = (AllowAny, )
    schema = ObservationPortalSchema(tags=['Utility'])

    def get(self, request, instrument_type):
        if request.user.is_staff:
            contention = Contention(instrument_type, anonymous=False)
        else:
            contention = Contention(instrument_type)
        return Response(contention.data())

    def get_example_response(self):
        return Response(data=EXAMPLE_RESPONSES['requestgroups']['contention'],
                        status=status.HTTP_200_OK)

    def get_endpoint_name(self):
        return 'getContention'
Example #11
0
class ScienceApplicationViewSet(viewsets.ModelViewSet):
    permission_classes = (IsAuthenticated, )
    schema = ObservationPortalSchema(tags=['Science Applications'])
    filter_class = ScienceApplicationFilter
    serializer_class = import_string(settings.SERIALIZERS['sciapplications']['ScienceApplication'])
    http_method_names = ('get', 'head', 'options', 'post', 'put', 'delete')
    filter_backends = (
        filters.OrderingFilter,
        DjangoFilterBackend
    )
    ordering_fields = (
        ('call__semester', 'semester'),
        ('tac_rank', 'tac_rank')
    )

    def get_queryset(self):
        if self.request.user.is_staff and self.request.user.profile.staff_view:
            qs = ScienceApplication.objects.all()
        else:
            qs = ScienceApplication.objects.filter(submitter=self.request.user)

        # Only DRAFT applications are allowed to be updated
        if self.action == 'update':
            qs = qs.filter(status=ScienceApplication.DRAFT)

        return qs.prefetch_related(
            'call', 'call__semester', 'submitter', 'submitter__profile', 'submitter__sciencecollaborationallocation',
            'timerequest_set', 'timerequest_set__instrument_types', 'coinvestigator_set',
        )

    def perform_destroy(self, instance):
        if instance.status == ScienceApplication.DRAFT:
            instance.delete()

    def perform_update(self, serializer):
        instance = serializer.save()
        send_email_if_ddt_submission(instance)

    def perform_create(self, serializer):
        instance = serializer.save()
        send_email_if_ddt_submission(instance)
class ConfigurationViewSet(viewsets.GenericViewSet):
    permission_classes = (IsAuthenticated, )
    schema = ObservationPortalSchema(tags=['RequestGroups'])
    serializer_class = import_string(
        settings.SERIALIZERS['requestgroups']['Dither'])

    @action(detail=False, methods=['post'])
    def dither(self, request):
        # Check that the dither parameters specified are valid
        request_serializer = self.get_request_serializer(data=request.data)
        if not request_serializer.is_valid():
            return Response(request_serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)

        # Expand the instrument_configs within the configuration based on the dither pattern specified
        configuration_dict = expand_dither_pattern(
            request_serializer.validated_data)

        return Response(configuration_dict)

    def get_request_serializer(self, *args, **kwargs):
        request_serializers = {
            'dither':
            import_string(settings.SERIALIZERS['requestgroups']['Dither'])
        }

        return request_serializers.get(self.action)(*args, **kwargs)

    def get_example_response(self):
        example_data = {
            'dither':
            Response(data=EXAMPLE_RESPONSES['requestgroups']['dither'],
                     status=status.HTTP_200_OK)
        }

        return example_data.get(self.action)

    def get_endpoint_name(self):
        endpoint_names = {'dither': 'expandDitherPattern'}

        return endpoint_names.get(self.action)
Example #13
0
class AcceptTermsApiView(APIView):
    permission_classes = [IsAuthenticated]
    schema=ObservationPortalSchema(tags=['Accounts'], empty_request=True)

    def post(self, request):
        """A simple POST request (empty request body) with user authentication information in the HTTP header will accept the terms of use for the Observation Portal."""
        try:
            profile = request.user.profile
        except Profile.DoesNotExist:
            profile = Profile.objects.create(user=request.user, institution='', title='')

        profile.terms_accepted = timezone.now()
        profile.save()
        serializer = self.get_response_serializer({'message': 'Terms accepted'})
        return Response(serializer.data, status=status.HTTP_200_OK)

    def get_response_serializer(self, *args, **kwargs):
        return import_string(settings.SERIALIZERS['accounts']['AcceptTerms'])(*args, **kwargs)

    def get_endpoint_name(self):
        return 'acceptTerms'
class PressureView(APIView):
    """Retrieves the pressure for a given site and instrument for the next 24 hours, binned into 15-minute intervals. The pressure
    for an observation is defined as its length divided by the total length of time during which it is visible.
    """
    permission_classes = (AllowAny, )
    schema = ObservationPortalSchema(tags=['Utility'], is_list_view=False)

    def get(self, request):
        instrument_type = request.GET.get('instrument')
        site = request.GET.get('site')
        if request.user.is_staff:
            pressure = Pressure(instrument_type, site, anonymous=False)
        else:
            pressure = Pressure(instrument_type, site)
        return Response(pressure.data())

    def get_example_response(self):
        return Response(data=EXAMPLE_RESPONSES['requestgroups']['pressure'])

    def get_query_parameters(self):
        return QUERY_PARAMETERS['requestgroups']['pressure']

    def get_endpoint_name(self):
        return 'getPressure'
class SemesterViewSet(viewsets.ReadOnlyModelViewSet):
    permission_classes = (AllowAny, )
    schema = ObservationPortalSchema(tags=['Proposals'])
    serializer_class = import_string(
        settings.SERIALIZERS['proposals']['Semester'])
    filter_backends = (
        DjangoFilterBackend,
        filters.OrderingFilter,
    )
    filter_class = SemesterFilter
    ordering = ('-start', )
    queryset = Semester.objects.all()

    @action(detail=True, methods=['get'])
    def proposals(self, request, pk=None):
        """ Get proposals in a given semester.
        """
        semester = self.get_object()
        proposals = semester.proposals.filter(
            active=True, non_science=False).prefetch_related(
                'sca', 'membership_set', 'membership_set__user',
                'membership_set__user__profile', 'semester_set',
                'timeallocation_set').distinct().order_by('sca__name')
        results = []
        for proposal in proposals:
            results.append({
                'id':
                proposal.id,
                'title':
                proposal.title,
                'abstract':
                proposal.abstract,
                'allocation':
                proposal.allocation(semester=semester),
                'pis': [{
                    'first_name': mem.user.first_name,
                    'last_name': mem.user.last_name,
                    'institution': mem.user.profile.institution
                } for mem in proposal.membership_set.all()
                        if mem.role == Membership.PI],
                'sca_id':
                proposal.sca.id,
                'sca_name':
                proposal.sca.name,
                'semesters':
                proposal.semester_set.distinct().values_list('id', flat=True)
            })
        return Response(results)

    @action(detail=True, methods=['get'], permission_classes=(IsAdminUser, ))
    def timeallocations(self, request, pk=None):
        """ Get TimeAllocations for a given semester.
        """
        timeallocations = self.get_object(
        ).timeallocation_set.prefetch_related(
            'proposal', 'proposal__membership_set',
            'proposal__membership_set__user').distinct()
        results = []
        for timeallocation in timeallocations:
            memberships = timeallocation.proposal.membership_set
            timeallocation_dict = timeallocation.as_dict(
                exclude=['proposal', 'semester'])
            timeallocation_dict['proposal'] = {
                'notes':
                timeallocation.proposal.notes,
                'id':
                timeallocation.proposal.id,
                'tac_priority':
                timeallocation.proposal.tac_priority,
                'num_users':
                memberships.count(),
                'pis': [{
                    'first_name': mem.user.first_name,
                    'last_name': mem.user.last_name
                } for mem in memberships.all() if mem.role == Membership.PI]
            }
            results.append(timeallocation_dict)
        return Response(results)

    def get_example_response(self):
        example_responses = {
            'proposals':
            Response(data=EXAMPLE_RESPONSES['semesters']['proposals'],
                     status=status.HTTP_200_OK),
            'timeallocations':
            Response(data=EXAMPLE_RESPONSES['semesters']['timeallocations'],
                     status=status.HTTP_200_OK)
        }

        return example_responses.get(self.action)
class ProposalViewSet(DetailAsDictMixin, ListAsDictMixin,
                      viewsets.ReadOnlyModelViewSet):
    permission_classes = (IsAuthenticated, )
    schema = ObservationPortalSchema(tags=['Proposals'])
    serializer_class = import_string(
        settings.SERIALIZERS['proposals']['Proposal'])
    filter_class = ProposalFilter
    filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
    ordering = ('-id', )

    def get_queryset(self):
        if self.request.user.is_staff and self.request.user.profile.staff_view:
            return Proposal.objects.all().prefetch_related(
                'sca', 'membership_set', 'membership_set__user',
                'timeallocation_set')
        else:
            return self.request.user.proposal_set.all().prefetch_related(
                'sca', 'membership_set', 'membership_set__user',
                'timeallocation_set')

    @action(detail=True, methods=['post'])
    def notification(self, request, pk=None):
        """ Subscribe to notifications
        """
        proposal = self.get_object()
        request_serializer = self.get_request_serializer(data=request.data)
        if request_serializer.is_valid():
            if request_serializer.validated_data['enabled']:
                ProposalNotification.objects.get_or_create(user=request.user,
                                                           proposal=proposal)
            else:
                ProposalNotification.objects.filter(
                    user=request.user, proposal=proposal).delete()

            response_serializer = self.get_response_serializer(
                {'message': 'Preferences saved'})
            return Response(response_serializer.data,
                            status=status.HTTP_200_OK)
        else:
            return Response({'errors': request_serializer.errors},
                            status=status.HTTP_400_BAD_REQUEST)

    @action(detail=True,
            methods=['post'],
            permission_classes=(IsPrincipleInvestigator, ))
    def invite(self, request, pk=None):
        """ Invite a Co-Investigator to the proposal. Must be a Principle Investigator.
        """
        proposal = self.get_object()
        request_serializer = self.get_request_serializer(data=request.data,
                                                         context={
                                                             'user':
                                                             self.request.user,
                                                             'proposal':
                                                             proposal
                                                         })
        if request_serializer.is_valid():
            proposal.add_users(request_serializer.validated_data['emails'],
                               Membership.CI)

            response_serializer = self.get_response_serializer(
                {'message': _('Co Investigator(s) invited')})
            return Response(response_serializer.data,
                            status=status.HTTP_200_OK)
        else:
            return Response(request_serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)

    @action(detail=True,
            methods=['post'],
            permission_classes=(IsPrincipleInvestigator, ))
    def globallimit(self, request, pk=None):
        """ Set global time limit for Co-Investigators. Must be a Principle Investigator
        """
        proposal = self.get_object()
        request_serializer = self.get_request_serializer(data=request.data)
        if request_serializer.is_valid():
            time_limit_hours = request_serializer.validated_data[
                'time_limit_hours']
            proposal.membership_set.filter(role=Membership.CI).update(
                time_limit=time_limit_hours * 3600)

            response_serializer = self.get_response_serializer({
                'message':
                f'All CI time limits set to {time_limit_hours} hours'
            })
            return Response(response_serializer.data,
                            status=status.HTTP_200_OK)
        else:
            return Response({'errors': request_serializer.errors},
                            status=status.HTTP_400_BAD_REQUEST)

    @action(detail=False, methods=['get'])
    def tags(self, request, pk=None):
        """ Get tags for a proposal
        """
        proposal_tags = get_queryset_field_values(self.get_queryset(), 'tags')
        return Response(list(proposal_tags))

    def get_request_serializer(self, *args, **kwargs):
        serializers = {
            'notification':
            import_string(
                settings.SERIALIZERS['proposals']['ProposalNotification']),
            'invite':
            import_string(settings.SERIALIZERS['proposals']['ProposalInvite']),
            'globallimit':
            import_string(settings.SERIALIZERS['proposals']['TimeLimit'])
        }
        return serializers.get(self.action, self.serializer_class)(*args,
                                                                   **kwargs)

    def get_response_serializer(self, *args, **kwargs):
        serializers = {
            'notification':
            import_string(settings.SERIALIZERS['proposals']
                          ['ProposalNotificationResponse']),
            'invite':
            import_string(
                settings.SERIALIZERS['proposals']['ProposalInviteResponse']),
            'globallimit':
            import_string(
                settings.SERIALIZERS['proposals']['TimeLimitResponse'])
        }

        return serializers.get(self.action, self.serializer_class)(*args,
                                                                   **kwargs)

    def get_example_response(self):
        example_data = {
            'tags':
            Response(data=EXAMPLE_RESPONSES['proposals']['tags'],
                     status=status.HTTP_200_OK)
        }

        return example_data.get(self.action)

    def get_endpoint_name(self):
        endpoint_names = {
            'notification': 'createProposalNotification',
            'invite': 'createProposalInvite',
            'globallimit': 'setTimeLimit',
            'tags': 'getProposalTags'
        }

        return endpoint_names.get(self.action)
class RequestGroupViewSet(ListAsDictMixin, viewsets.ModelViewSet):
    permission_classes = (IsAuthenticatedOrReadOnly, )
    http_method_names = ['get', 'post', 'head', 'options']
    schema = ObservationPortalSchema(tags=['RequestGroups'])
    serializer_class = import_string(
        settings.SERIALIZERS['requestgroups']['RequestGroup'])
    filter_class = RequestGroupFilter
    filter_backends = (filters.OrderingFilter, DjangoFilterBackend)
    ordering = ('-id', )

    def get_throttles(self):
        actions_to_throttle = ['cancel', 'validate', 'create']
        if self.action in actions_to_throttle:
            self.throttle_scope = 'requestgroups.' + self.action
        return super().get_throttles()

    def get_queryset(self):
        if self.request.user.is_authenticated:
            if self.request.user.profile.staff_view and self.request.user.is_staff:
                qs = RequestGroup.objects.all()
            else:
                qs = RequestGroup.objects.filter(
                    proposal__in=self.request.user.proposal_set.all())
                if self.request.user.profile.view_authored_requests_only:
                    qs = qs.filter(submitter=self.request.user)
        else:
            qs = RequestGroup.objects.filter(
                proposal__in=Proposal.objects.filter(public=True))
        return qs.prefetch_related(
            'requests', 'requests__windows', 'requests__configurations',
            'requests__location',
            'requests__configurations__instrument_configs',
            'requests__configurations__target',
            'requests__configurations__acquisition_config', 'submitter',
            'proposal', 'requests__configurations__guiding_config',
            'requests__configurations__constraints',
            'requests__configurations__instrument_configs__rois').distinct()

    def perform_create(self, serializer):
        serializer.save(submitter=self.request.user)

    @action(detail=False, methods=['get'], permission_classes=(IsAdminUser, ))
    def schedulable_requests(self, request):
        """
            Gets the set of schedulable User requests for the scheduler.
            Needs a start and end time specified as the range of time to get requests in. Usually this is the entire
            semester for a scheduling run.
        """
        current_semester = Semester.current_semesters().first()
        start = parse(
            request.query_params.get('start', str(
                current_semester.start))).replace(tzinfo=timezone.utc)
        end = parse(request.query_params.get('end', str(
            current_semester.end))).replace(tzinfo=timezone.utc)
        telescope_classes = request.query_params.getlist('telescope_class')
        # Schedulable requests are not in a terminal state, are part of an active proposal,
        # and have a window within this semester
        instrument_config_query = InstrumentConfig.objects.prefetch_related(
            'rois')
        configuration_query = Configuration.objects.select_related(
            'constraints', 'target', 'acquisition_config',
            'guiding_config').prefetch_related(
                Prefetch('instrument_configs',
                         queryset=instrument_config_query))
        request_query = Request.objects.select_related(
            'location').prefetch_related(
                'windows',
                Prefetch('configurations', queryset=configuration_query))
        queryset = RequestGroup.objects.exclude(
            state__in=TERMINAL_REQUEST_STATES).exclude(
                observation_type=RequestGroup.DIRECT).filter(
                    requests__windows__start__lte=end,
                    requests__windows__start__gte=start,
                    proposal__active=True).prefetch_related(
                        Prefetch('requests', queryset=request_query),
                        Prefetch('proposal',
                                 queryset=Proposal.objects.only('id').all()),
                        Prefetch('submitter',
                                 queryset=User.objects.only(
                                     'username',
                                     'is_staff').all())).distinct()
        if telescope_classes:
            queryset = queryset.filter(
                requests__location__telescope_class__in=telescope_classes)

        # queryset now contains all the schedulable URs and their associated requests and data
        # Check that each request time available in its proposal still
        request_group_data = []
        tas = {}
        for request_group in queryset.all():
            total_duration_dict = request_group.total_duration
            for tak, duration in total_duration_dict.items():
                if (tak, request_group.proposal.id) in tas:
                    time_allocation = tas[(tak, request_group.proposal.id)]
                else:
                    time_allocation = TimeAllocation.objects.get(
                        semester=tak.semester,
                        instrument_types__contains=[tak.instrument_type],
                        proposal=request_group.proposal.id,
                    )
                    tas[(tak, request_group.proposal.id)] = time_allocation
                if request_group.observation_type == RequestGroup.NORMAL:
                    time_left = time_allocation.std_allocation - time_allocation.std_time_used
                elif request_group.observation_type == RequestGroup.RAPID_RESPONSE:
                    time_left = time_allocation.rr_allocation - time_allocation.rr_time_used
                elif request_group.observation_type == RequestGroup.TIME_CRITICAL:
                    time_left = time_allocation.tc_allocation - time_allocation.tc_time_used
                else:
                    logger.critical(
                        'request_group {} observation_type {} is not allowed'.
                        format(request_group.id,
                               request_group.observation_type))
                    continue
                if time_left * settings.PROPOSAL_TIME_OVERUSE_ALLOWANCE >= (
                        duration / 3600.0):
                    request_group_dict = request_group.as_dict()
                    request_group_dict[
                        'is_staff'] = request_group.submitter.is_staff
                    request_group_data.append(request_group_dict)
                    break
                else:
                    logger.warning(
                        'not enough time left {0} in proposal {1} for ur {2} of duration {3}, skipping'
                        .format(time_left, request_group.proposal.id,
                                request_group.id, (duration / 3600.0)))
        return Response(request_group_data)

    @action(detail=True, methods=['post'])
    def cancel(self, request, pk=None):
        """ Cancel a RequestGroup
        """
        request_group = self.get_object()
        try:
            request_group.state = 'CANCELED'
            request_group.save()
        except InvalidStateChange as exc:
            return Response({'errors': [str(exc)]},
                            status=status.HTTP_400_BAD_REQUEST)
        return Response(
            import_string(settings.SERIALIZERS['requestgroups']
                          ['RequestGroup'])(request_group).data)

    @action(detail=False, methods=['post'])
    def validate(self, request):
        """ Validate a RequestGrouo
        """
        serializer = import_string(
            settings.SERIALIZERS['requestgroups']['RequestGroup'])(
                data=request.data, context={
                    'request': request
                })
        req_durations = {}
        if serializer.is_valid():
            req_durations = get_request_duration_dict(
                serializer.validated_data['requests'], request.user.is_staff)
            errors = {}
        else:
            errors = serializer.errors

        return Response({'request_durations': req_durations, 'errors': errors})

    @action(detail=False, methods=['post'])
    def max_allowable_ipp(self, request):
        """ Get the maximum allowable IPP for a RequestGroup
        """
        # change requested ipp to 1 because we want it to always pass the serializers ipp check
        request.data['ipp_value'] = 1.0
        serializer = import_string(
            settings.SERIALIZERS['requestgroups']['RequestGroup'])(
                data=request.data, context={
                    'request': request
                })
        if serializer.is_valid():
            ipp_dict = get_max_ipp_for_requestgroup(serializer.validated_data)
            return Response(ipp_dict)
        else:
            return Response({'errors': serializer.errors})

    @action(detail=False, methods=['post'])
    def cadence(self, request):
        """ Given a well-formed RequestGroup containing a single Request, return a new RequestGroup containing many requests
        generated by the cadence function.
        """
        request_serializer = self.get_request_serializer(
            data=request.data, context={'request': request})
        expanded_requests = []
        if request_serializer.is_valid():
            cadence_request = request_serializer.validated_data.get(
                'requests')[0]
            if isinstance(cadence_request,
                          dict) and cadence_request.get('cadence'):
                cadence_request_serializer = import_string(
                    settings.SERIALIZERS['requestgroups']['CadenceRequest'])(
                        data=cadence_request)
                if cadence_request_serializer.is_valid():
                    expanded_requests.extend(
                        expand_cadence_request(
                            cadence_request_serializer.validated_data,
                            request.user.is_staff))
                else:
                    return Response(cadence_request_serializer.errors,
                                    status=status.HTTP_400_BAD_REQUEST)

            # if we couldn't find any valid cadence requests, return that as an error
            if not expanded_requests:
                return Response(
                    {
                        'errors':
                        'No visible requests within cadence window parameters'
                    },
                    status=status.HTTP_400_BAD_REQUEST)

            # now replace the originally sent requests with the cadence requests and send it back
            ret_data = request.data.copy()
            ret_data['requests'] = expanded_requests

            if len(ret_data['requests']) > 1:
                ret_data['operator'] = 'MANY'
            response_serializer = self.get_response_serializer(
                data=ret_data, context={'request': request})
            if not response_serializer.is_valid():
                return Response(response_serializer.errors,
                                status=status.HTTP_400_BAD_REQUEST)
            return Response(ret_data, status=status.HTTP_200_OK)
        else:
            return (Response(request_serializer.errors,
                             status=status.HTTP_400_BAD_REQUEST))

    def get_example_response(self):
        example_data = {
            'max_allowable_ipp':
            Response(
                data=EXAMPLE_RESPONSES['requestgroups']['max_allowable_ipp'],
                status=status.HTTP_200_OK),
            'schedulable_requests':
            Response(data=EXAMPLE_RESPONSES['requestgroups']
                     ['schedulable_requests'],
                     status=status.HTTP_200_OK)
        }

        return example_data.get(self.action)

    def get_endpoint_name(self):
        endpoint_names = {
            'max_allowable_ipp': 'getMaxAllowableIPP',
            'cadence': 'generateCadence',
            'schedulable_requests': 'listSchedulableRequests'
        }

        return endpoint_names.get(self.action)

    def get_request_serializer(self, *args, **kwargs):
        serializers = {
            'cadence':
            import_string(
                settings.SERIALIZERS['requestgroups']['CadenceRequestGroup'])
        }

        return serializers.get(self.action, self.serializer_class)(*args,
                                                                   **kwargs)

    def get_response_serializer(self, *args, **kwargs):
        serializers = {
            'cadence':
            import_string(
                settings.SERIALIZERS['requestgroups']['RequestGroup'])
        }

        return serializers.get(self.action, self.serializer_class)(*args,
                                                                   **kwargs)
class RequestViewSet(ListAsDictMixin, viewsets.ReadOnlyModelViewSet):
    permission_classes = (IsAuthenticatedOrReadOnly, )
    schema = ObservationPortalSchema(tags=['Requests'])
    serializer_class = import_string(
        settings.SERIALIZERS['requestgroups']['Request'])
    filter_class = RequestFilter
    filter_backends = (filters.OrderingFilter, DjangoFilterBackend)
    ordering = ('-id', )
    ordering_fields = ('id', 'state')
    undocumented_actions = ['telescope_states']

    def get_queryset(self):
        if self.request.user.is_authenticated:
            if self.request.user.profile.staff_view and self.request.user.is_staff:
                qs = Request.objects.all()
            else:
                qs = Request.objects.filter(request_group__proposal__in=self.
                                            request.user.proposal_set.all())
                if self.request.user.profile.view_authored_requests_only:
                    qs = qs.filter(request_group__submitter=self.request.user)
        else:
            qs = Request.objects.filter(
                request_group__proposal__in=Proposal.objects.filter(
                    public=True))
        return qs.prefetch_related(
            'windows', 'configurations', 'location',
            'configurations__instrument_configs', 'configurations__target',
            'configurations__acquisition_config',
            'configurations__guiding_config', 'configurations__constraints',
            'configurations__instrument_configs__rois').distinct()

    @action(detail=True)
    def airmass(self, request, pk=None):
        return Response(
            get_airmasses_for_request_at_sites(self.get_object().as_dict(),
                                               is_staff=request.user.is_staff))

    @action(detail=True)
    def telescope_states(self, request, pk=None):
        telescope_states = get_telescope_states_for_request(
            self.get_object().as_dict(), is_staff=request.user.is_staff)
        str_telescope_states = {str(k): v for k, v in telescope_states.items()}
        return Response(str_telescope_states)

    @action(detail=True)
    def observations(self, request, pk=None):
        observations = self.get_object().observation_set.order_by('id').all()
        if request.GET.get('exclude_canceled'):
            return Response([
                o.as_dict(no_request=True) for o in observations
                if o.state != 'CANCELED'
            ])
        return Response([o.as_dict(no_request=True) for o in observations])

    @action(detail=False, methods=['post'])
    def mosaic(self, request):
        # Check that the mosaic parameters specified are valid
        mosaic_serializer = import_string(
            settings.SERIALIZERS['requestgroups']['Mosaic'])(data=request.data)
        if not mosaic_serializer.is_valid():
            return Response(mosaic_serializer.errors,
                            status=status.HTTP_400_BAD_REQUEST)

        # Expand the configurations within the request based on the mosaic pattern specified
        request_dict = expand_mosaic_pattern(mosaic_serializer.validated_data)
        return Response(request_dict)

    def get_request_serializer(self, *args, **kwargs):
        serializers = {
            'mosaic':
            import_string(settings.SERIALIZERS['requestgroups']['Mosaic'])
        }

        return serializers.get(self.action, self.serializer_class)(*args,
                                                                   **kwargs)

    def get_example_response(self):
        example_data = {
            'airmass':
            Response(data=EXAMPLE_RESPONSES['requests']['airmass'],
                     status=status.HTTP_200_OK),
            'observations':
            Response(data=EXAMPLE_RESPONSES['requests']['observations'],
                     status=status.HTTP_200_OK)
        }

        return example_data.get(self.action)

    def get_query_parameters(self):
        query_parameters = {
            'observations': QUERY_PARAMETERS['requests']['observations']
        }

        return query_parameters.get(self.action)

    def get_endpoint_name(self):
        endpoint_names = {
            'mosaic': 'expandMosaic',
            'observations': 'observationsForRequest',
            'airmass': 'airmassForRequest'
        }

        return endpoint_names.get(self.action)
class MembershipViewSet(ListAsDictMixin, DetailAsDictMixin,
                        mixins.DestroyModelMixin,
                        viewsets.ReadOnlyModelViewSet):
    http_method_names = ('get', 'head', 'options', 'post', 'delete')
    schema = ObservationPortalSchema(tags=['Proposals'])
    filter_backends = (
        DjangoFilterBackend,
        filters.OrderingFilter,
    )
    filter_class = MembershipFilter
    serializer_class = import_string(
        settings.SERIALIZERS['proposals']['Membership'])

    def get_queryset(self):
        if self.request.user.is_staff and self.request.user.profile.staff_view:
            return Membership.objects.all()
        else:
            users_memberships = self.request.user.membership_set.all()
            pi_memberships_of_users_proposals = Membership.objects.filter(
                proposal__in=self.request.user.proposal_set.all(),
                role=Membership.PI)
            memberships_where_user_is_pi = Membership.objects.filter(
                proposal__in=self.request.user.proposal_set.filter(
                    membership__role=Membership.PI))
            all_memberships = users_memberships | memberships_where_user_is_pi | pi_memberships_of_users_proposals
            return all_memberships.distinct()

    def get_permissions(self):
        pi_only_actions = ('destroy', 'limit')
        if self.action in pi_only_actions:
            permission_classes = [IsPrincipleInvestigator]
        else:
            permission_classes = [IsAuthenticated]
        return [permission() for permission in permission_classes]

    @action(detail=True, methods=['post'])
    def limit(self, request, pk=None):
        """ Set time limit for a member of a proposal.
        """
        membership = self.get_object()
        request_serializer = self.get_request_serializer(
            data=request.data, context={'membership': membership})
        if request_serializer.is_valid():
            time_limit_hours = request_serializer.validated_data[
                'time_limit_hours']
            membership.time_limit = time_limit_hours * 3600
            membership.save()
            message = (
                f'Time limit for {membership.user.first_name} {membership.user.last_name} set '
                f'to {time_limit_hours} hours')

            response_serializer = self.get_response_serializer(
                {'message': message})
            return Response(response_serializer.data,
                            status=status.HTTP_200_OK)
        else:
            return Response({'errors': request_serializer.errors},
                            status=status.HTTP_400_BAD_REQUEST)

    def perform_destroy(self, instance):
        if instance.role == Membership.CI:
            instance.delete()

    def get_request_serializer(self, *args, **kwargs):
        serializers = {
            'limit':
            import_string(settings.SERIALIZERS['proposals']['TimeLimit'])
        }

        return serializers.get(self.action, self.serializer_class)(*args,
                                                                   **kwargs)

    def get_response_serializer(self, *args, **kwargs):
        serializers = {
            'limit':
            import_string(
                settings.SERIALIZERS['proposals']['TimeLimitResponse'])
        }

        return serializers.get(self.action, self.serializer_class)(*args,
                                                                   **kwargs)
class InstrumentsInformationView(APIView):
    """ Gets information about current instruments from the ConfigDB.
    """
    permission_classes = (AllowAny, )
    schema = ObservationPortalSchema(tags=['Utility'])
    filter_backends = (DjangoFilterBackend, )
    filter_class = InstrumentsInformationFilter

    def get(self, request):
        info = {}
        # Staff users by default should see all instruments, but can request only schedulable instruments.
        # Non-staff users are only allowed access to schedulable instruments.
        if request.user.is_staff:
            only_schedulable = request.query_params.get(
                'only_schedulable', False)
        else:
            only_schedulable = True

        requested_instrument_type = request.query_params.get(
            'instrument_type', '')
        location = {
            'site': request.query_params.get('site', ''),
            'enclosure': request.query_params.get('enclosure', ''),
            'telescope_class': request.query_params.get('telescope_class', ''),
            'telescope': request.query_params.get('telescope', ''),
        }
        for instrument_type in configdb.get_instrument_type_codes(
                location=location, only_schedulable=only_schedulable):
            if not requested_instrument_type or requested_instrument_type.lower(
            ) == instrument_type.lower():
                ccd_size = configdb.get_ccd_size(instrument_type)
                info[instrument_type] = {
                    'type':
                    configdb.get_instrument_type_category(instrument_type),
                    'class':
                    configdb.get_instrument_type_telescope_class(
                        instrument_type),
                    'name':
                    configdb.get_instrument_type_full_name(instrument_type),
                    'optical_elements':
                    configdb.get_optical_elements(instrument_type),
                    'modes':
                    configdb.get_modes_by_type(instrument_type),
                    'default_acceptability_threshold':
                    configdb.get_default_acceptability_threshold(
                        instrument_type),
                    'configuration_types':
                    configdb.get_configuration_types(instrument_type),
                    'default_configuration_type':
                    configdb.get_default_configuration_type(instrument_type),
                    'camera_type': {
                        'science_field_of_view':
                        configdb.get_diagonal_ccd_fov(instrument_type,
                                                      autoguider=False),
                        'autoguider_field_of_view':
                        configdb.get_diagonal_ccd_fov(instrument_type,
                                                      autoguider=True),
                        'pixel_scale':
                        configdb.get_pixel_scale(instrument_type),
                        'pixels_x':
                        ccd_size['x'],
                        'pixels_y':
                        ccd_size['y'],
                        'orientation':
                        configdb.get_average_ccd_orientation(instrument_type)
                    }
                }
        return Response(info)

    def get_example_response(self):
        return Response(EXAMPLE_RESPONSES['requestgroups'].get('instruments'),
                        status=status.HTTP_200_OK)

    def get_endpoint_name(self):
        return 'getInstruments'