Example #1
0
    def _filter_mongo_query(self, request):
        """
        Build filters to pass to Mongo query.
        Acts like Django `filter_backends`

        :param request:
        :return: dict
        """
        filters = {}

        if request.method == "GET":
            filters = request.GET.dict()

        # Remove `format` from filters. No need to use it
        filters.pop('format', None)
        # Do not allow requests to retrieve more than `SUBMISSION_LIST_LIMIT`
        # submissions at one time
        limit = filters.get('limit', settings.SUBMISSION_LIST_LIMIT)
        try:
            filters['limit'] = positive_int(
                limit, strict=True, cutoff=settings.SUBMISSION_LIST_LIMIT)
        except ValueError:
            raise serializers.ValidationError(
                {'limit': _('A positive integer is required')})

        return filters
Example #2
0
 def destroy(self, request, pk, *args, **kwargs):
     deployment = self._get_deployment()
     # Coerce to int because back end only finds matches with same type
     submission_id = positive_int(pk)
     json_response = deployment.delete_submission(
         submission_id, user=request.user
     )
     return Response(**json_response)
Example #3
0
 def duplicate(self, request, pk, *args, **kwargs):
     """
     Creates a duplicate of the submission with a given `pk`
     """
     deployment = self._get_deployment()
     duplicate_response = deployment.duplicate_submission(
         requesting_user_id=request.user.id, instance_id=positive_int(pk)
     )
     return Response(duplicate_response, status=status.HTTP_201_CREATED)
Example #4
0
 def duplicate(self, request, pk, *args, **kwargs):
     """
     Creates a duplicate of the submission with a given `pk`
     """
     deployment = self._get_deployment()
     # Coerce to int because back end only finds matches with same type
     submission_id = positive_int(pk)
     duplicate_response = deployment.duplicate_submission(
         submission_id=submission_id, user=request.user
     )
     return Response(duplicate_response, status=status.HTTP_201_CREATED)
Example #5
0
 def enketo_edit(self, request, pk, *args, **kwargs):
     submission_id = positive_int(pk)
     enketo_response = self._get_enketo_link(request, submission_id, 'edit')
     if enketo_response.status_code in (
         status.HTTP_201_CREATED, status.HTTP_200_OK
     ):
         # See https://github.com/enketo/enketo-express/issues/187
         EnketoSessionAuthentication.prepare_response_with_csrf_cookie(
             request, enketo_response
         )
     return enketo_response
Example #6
0
    def retrieve(self, request, pk, *args, **kwargs):
        """
        Retrieve a submission by its primary key or its UUID.

        Warning when using the UUID. Submissions can have the same uuid because
        there is no unique constraint on this field. The first occurrence
        will be returned.
        """
        format_type = kwargs.get('format', request.GET.get('format', 'json'))
        deployment = self._get_deployment()
        params = {
            'user': request.user,
            'format_type': format_type,
            'request': request,
        }
        filters = self._filter_mongo_query(request)

        # Unfortunately, Django expects that the URL parameter is `pk`,
        # its name cannot be changed (easily).
        submission_id_or_uuid = pk
        try:
            submission_id_or_uuid = positive_int(submission_id_or_uuid)
        except ValueError:
            if not re.match(
                r'[a-z\d]{8}-([a-z\d]{4}-){3}[a-z\d]{12}', submission_id_or_uuid
            ):
                raise Http404

            try:
                query = json.loads(filters.pop('query', '{}'))
            except json.JSONDecodeError:
                raise serializers.ValidationError(
                    {'query': t('Value must be valid JSON.')}
                )
            query['_uuid'] = submission_id_or_uuid
            filters['query'] = query
        else:
            params['submission_ids'] = [submission_id_or_uuid]

        # Join all parameters to be passed to `deployment.get_submissions()`
        params.update(filters)

        # The `get_submissions()` is a generator in KobocatDeploymentBackend
        # class but a list in MockDeploymentBackend. We cast the result as a list
        # no matter what is the deployment back-end class to make it work with
        # both. Since the number of submissions is be very small, it should not
        # have a big impact on memory (i.e. list vs generator)
        submissions = list(deployment.get_submissions(**params))
        if not submissions:
            raise Http404

        submission = submissions[0]
        return Response(submission)
Example #7
0
 def retrieve(self, request, pk, *args, **kwargs):
     format_type = kwargs.get('format', request.GET.get('format', 'json'))
     deployment = self._get_deployment()
     filters = self._filter_mongo_query(request)
     try:
         submission = deployment.get_submission(positive_int(pk),
                                                request.user.id,
                                                format_type=format_type,
                                                **filters)
     except ValueError:
         raise Http404
     else:
         if not submission:
             raise Http404
     return Response(submission)
Example #8
0
    def create(self, request, *args, **kwargs):
        """
        It's only used to trigger hook services of the Asset (so far).

        :param request:
        :return:
        """
        try:
            instance_id = positive_int(request.data.get('instance_id'),
                                       strict=True)
        except ValueError:
            raise serializers.ValidationError(
                {'instance_id': _('A positive integer is required.')})

        # Check if instance really belongs to Asset.
        try:
            instance = self.asset.deployment.get_submission(
                instance_id, request.user.id)
        except ValueError:
            raise Http404

        instance_id_fieldname = self.asset.deployment.INSTANCE_ID_FIELDNAME
        if not (instance
                and int(instance.get(instance_id_fieldname)) == instance_id):
            raise Http404

        if HookUtils.call_services(self.asset, instance_id):
            # Follow Open Rosa responses by default
            response_status_code = status.HTTP_202_ACCEPTED
            response = {
                "detail":
                _("We got and saved your data, but may not have "
                  "fully processed it. You should not try to resubmit.")
            }
        else:
            # call_services() refused to launch any task because this
            # instance already has a `HookLog`
            response_status_code = status.HTTP_409_CONFLICT
            response = {
                "detail":
                _("Your data for instance {} has been already "
                  "submitted.".format(instance_id))
            }

        return Response(response, status=response_status_code)
Example #9
0
    def validation_status(self, request, pk, *args, **kwargs):
        deployment = self._get_deployment()
        # Coerce to int because back end only finds matches with same type
        submission_id = positive_int(pk)
        if request.method == 'GET':
            json_response = deployment.get_validation_status(
                submission_id=submission_id,
                user=request.user,
            )
        else:
            json_response = deployment.set_validation_status(
                submission_id=submission_id,
                user=request.user,
                data=request.data,
                method=request.method,
            )

        return Response(**json_response)
Example #10
0
    def validate_submission_list_params(
        self,
        user: '******',
        format_type: str = SUBMISSION_FORMAT_TYPE_JSON,
        validate_count: bool = False,
        partial_perm=PERM_VIEW_SUBMISSIONS,
        **mongo_query_params
    ) -> dict:
        """
        Validates parameters (`mongo_query_params`) to be passed to MongoDB.
        parameters can be:
            - start
            - limit
            - sort
            - fields
            - query
            - submission_ids
        If `validate_count` is True,`start`, `limit`, `fields` and `sort` are
        ignored.
        If `user` has partial permissions, conditions are
        applied to the query to narrow down results to what they are allowed
        to see. Partial permissions are validated with 'view_submissions' by
        default. To check with another permission, pass a different permission
        to `partial_perm`.
        """

        if 'count' in mongo_query_params:
            raise serializers.ValidationError(
                {
                    'count': t(
                        'This param is not implemented. Use `count` property '
                        'of the response instead.'
                    )
                }
            )

        if validate_count is False and format_type == SUBMISSION_FORMAT_TYPE_XML:
            if 'sort' in mongo_query_params:
                # FIXME. Use Mongo to sort data and ask PostgreSQL to follow the order
                # See. https://stackoverflow.com/a/867578
                raise serializers.ValidationError({
                    'sort': t('This param is not supported in `XML` format')
                })

            if 'fields' in mongo_query_params:
                raise serializers.ValidationError({
                    'fields': t('This is not supported in `XML` format')
                })

        start = mongo_query_params.get('start', 0)
        limit = mongo_query_params.get('limit')
        sort = mongo_query_params.get('sort', {})
        fields = mongo_query_params.get('fields', [])
        query = mongo_query_params.get('query', {})
        submission_ids = mongo_query_params.get('submission_ids', [])
        skip_count = mongo_query_params.get('skip_count', False)

        # I've copied these `ValidationError` messages verbatim from DRF where
        # possible.TODO: Should this validation be in (or called directly by)
        # the view code? Does DRF have a validator for GET params?

        if isinstance(query, str):
            try:
                query = json.loads(query, object_hook=json_util.object_hook)
            except ValueError:
                raise serializers.ValidationError(
                    {'query': t('Value must be valid JSON.')}
                )

        if not isinstance(submission_ids, list):

            raise serializers.ValidationError(
                {'submission_ids': t('Value must be a list.')}
            )

        # This error should not be returned as `ValidationError` to user.
        # We want to return a 500.
        try:
            permission_filters = self.asset.get_filters_for_partial_perm(
                user.pk, perm=partial_perm)
        except ValueError:
            raise ValueError('Invalid `user_id` param')

        if validate_count:
            return {
                'query': query,
                'submission_ids': submission_ids,
                'permission_filters': permission_filters
            }

        if isinstance(sort, str):
            try:
                sort = json.loads(sort, object_hook=json_util.object_hook)
            except ValueError:
                raise serializers.ValidationError(
                    {'sort': t('Value must be valid JSON.')}
                )

        try:
            start = positive_int(start)
        except ValueError:
            raise serializers.ValidationError(
                {'start': t('A positive integer is required.')}
            )
        try:
            if limit is not None:
                limit = positive_int(limit, strict=True)
        except ValueError:
            raise serializers.ValidationError(
                {'limit': t('A positive integer is required.')}
            )

        if isinstance(fields, str):
            try:
                fields = json.loads(fields, object_hook=json_util.object_hook)
            except ValueError:
                raise serializers.ValidationError(
                    {'fields': t('Value must be valid JSON.')}
                )

        params = {
            'query': query,
            'start': start,
            'fields': fields,
            'sort': sort,
            'submission_ids': submission_ids,
            'permission_filters': permission_filters,
            'skip_count': skip_count,
        }

        if limit:
            params['limit'] = limit

        return params
Example #11
0
    def validate_submission_list_params(self,
                                        requesting_user_id,
                                        format_type=INSTANCE_FORMAT_TYPE_JSON,
                                        validate_count=False,
                                        **kwargs):
        """
        Ensure types of query and each param.

        Args:
            requesting_user_id (int)
            format_type (str): INSTANCE_FORMAT_TYPE_JSON|INSTANCE_FORMAT_TYPE_XML
            validate_count (bool): If `True`, ignores `start`, `limit`, `fields` & `sort`
            kwargs (dict): Can contain
                - start
                - limit
                - sort
                - fields
                - query
                - instance_ids


        Returns:
            dict
        """

        if 'count' in kwargs:
            raise serializers.ValidationError({
                'count': _('This param is not implemented. Use `count` property '
                           'of the response instead.')
            })

        if validate_count is False and format_type == INSTANCE_FORMAT_TYPE_XML:
            if 'sort' in kwargs:
                # FIXME. Use Mongo to sort data and ask PostgreSQL to follow the order.
                # See. https://stackoverflow.com/a/867578
                raise serializers.ValidationError({
                    'sort': _('This param is not supported in `XML` format')
                })

            if 'fields' in kwargs:
                raise serializers.ValidationError({
                    'fields': _('This is not supported in `XML` format')
                })

        start = kwargs.get('start', 0)
        limit = kwargs.get('limit')
        sort = kwargs.get('sort', {})
        fields = kwargs.get('fields', [])
        query = kwargs.get('query', {})
        instance_ids = kwargs.get('instance_ids', [])

        # I've copied these `ValidationError` messages verbatim from DRF where
        # possible. TODO: Should this validation be in (or called directly by)
        # the view code? Does DRF have a validator for GET params?

        if isinstance(query, str):
            try:
                query = json.loads(query, object_hook=json_util.object_hook)
            except ValueError:
                raise serializers.ValidationError(
                    {'query': _('Value must be valid JSON.')}
                )

        if not isinstance(instance_ids, list):
            raise serializers.ValidationError(
                {'instance_ids': _('Value must be a list.')}
            )

        # This error should not be returned as `ValidationError` to user.
        # We want to return a 500.
        try:
            permission_filters = self.asset.get_filters_for_partial_perm(
                requesting_user_id)
        except ValueError:
            raise ValueError(_('Invalid `requesting_user_id` param'))

        if validate_count:
            return {
                'query': query,
                'instance_ids': instance_ids,
                'permission_filters': permission_filters
            }

        if isinstance(sort, str):
            try:
                sort = json.loads(sort, object_hook=json_util.object_hook)
            except ValueError:
                raise serializers.ValidationError(
                    {'sort': _('Value must be valid JSON.')}
                )

        try:
            start = positive_int(start)
        except ValueError:
            raise serializers.ValidationError(
                {'start': _('A positive integer is required.')}
            )
        try:
            if limit is not None:
                limit = positive_int(limit, strict=True)
        except ValueError:
            raise serializers.ValidationError(
                {'limit': _('A positive integer is required.')}
            )

        if isinstance(fields, str):
            try:
                fields = json.loads(fields, object_hook=json_util.object_hook)
            except ValueError:
                raise serializers.ValidationError(
                    {'fields': _('Value must be valid JSON.')}
                )

        params = {
            'query': query,
            'start': start,
            'fields': fields,
            'sort': sort,
            'instance_ids': instance_ids,
            'permission_filters': permission_filters
        }

        if limit:
            params['limit'] = limit

        return params
Example #12
0
 def enketo_view(self, request, pk, *args, **kwargs):
     submission_id = positive_int(pk)
     return self._get_enketo_link(request, submission_id, 'view')