Ejemplo n.º 1
0
class TestHateoas(Test):

    hateoas = Hateoas()

    @with_context
    def setUp(self):
        super(TestHateoas, self).setUp()
        project = ProjectFactory.create(published=True, id=1)
        task = TaskFactory.create(id=1, project=project)
        TaskRunFactory.create(project=project, task=task)

    # Tests
    @with_context
    @patch('pybossa.api.task.TaskAPI._verify_auth')
    def test_00_link_object(self, auth):
        """Test HATEOAS object link is created"""
        user = UserFactory.create(admin=True)
        auth.return_value = True
        # For project
        res = self.app.get('/api/project/1?api_key=' + user.api_key, follow_redirects=True)
        output = json.loads(res.data)
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg

        err_msg = "There should be a Links list with the category URI"
        assert output['links'] is not None, err_msg
        assert len(output['links']) == 1, err_msg
        project_link = self.hateoas.link(rel='category', title='category',
                                     href='http://{}/api/category/1'.format(self.flask_app.config['SERVER_NAME']))
        assert project_link == output['links'][0], (project_link, output['links'][0], err_msg)

        project_link = self.hateoas.link(rel='self', title='project',
                                     href='http://{}/api/project/1'.format(self.flask_app.config['SERVER_NAME']))
        err_msg = "The object link is wrong: %s" % output['link']
        assert project_link == output['link'], err_msg

        # For task
        res = self.app.get('/api/task/1?api_key=' + user.api_key, follow_redirects=True)
        output = json.loads(res.data)
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        task_link = self.hateoas.link(rel='self', title='task',
                                      href='http://{}/api/task/1'.format(self.flask_app.config['SERVER_NAME']))
        err_msg = "The object link is wrong: %s" % output['link']
        assert task_link == output['link'], err_msg
        err_msg = "There should be one parent link: project"
        assert output.get('links') is not None, err_msg
        assert len(output.get('links')) == 1, err_msg
        err_msg = "The parent link is wrong"
        project_link = self.hateoas.link(rel='parent', title='project',
                                     href='http://{}/api/project/1'.format(self.flask_app.config['SERVER_NAME']))
        assert output.get('links')[0] == project_link, err_msg

        # For taskrun
        res = self.app.get('/api/taskrun/1?api_key=' + user.api_key, follow_redirects=True)
        output = json.loads(res.data)
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        task_link = self.hateoas.link(rel='self', title='taskrun',
                                      href='http://{}/api/taskrun/1'.format(self.flask_app.config['SERVER_NAME']))
        err_msg = "The object link is wrong: %s" % output['link']
        assert task_link == output['link'], err_msg
        err_msg = "There should be two parent links: project and task"
        assert output.get('links') is not None, err_msg
        assert len(output.get('links')) == 2, err_msg
        err_msg = "The parent project link is wrong"
        project_link = self.hateoas.link(rel='parent', title='project',
                                     href='http://{}/api/project/1'.format(self.flask_app.config['SERVER_NAME']))
        assert output.get('links')[0] == project_link, err_msg

        err_msg = "The parent task link is wrong"
        project_link = self.hateoas.link(rel='parent', title='task',
                                     href='http://{}/api/task/1'.format(self.flask_app.config['SERVER_NAME']))
        assert output.get('links')[1] == project_link, err_msg
        res = self.app.post("/api/taskrun")

        # For category
        res = self.app.get("/api/category/1", follow_redirects=True)
        output = json.loads(res.data)
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        category_link = self.hateoas.link(rel='self', title='category',
                                          href='http://{}/api/category/1'.format(self.flask_app.config['SERVER_NAME']))
        err_msg = "The object link is wrong: %s" % output['link']
        assert category_link == output['link'], err_msg
        err_msg = "There should be no other links"
        assert output.get('links') is None, err_msg
        err_msg = "The object links should are wrong"

        # For user
        # Pending define what user fields will be visible through the API
        # Issue #626. For now let's suppose link and links are not visible
        # res = self.app.get("/api/user/1?api_key=" + self.root_api_key, follow_redirects=True)
        # output = json.loads(res.data)
        # err_msg = "There should be a Link with the object URI"
        # assert output['link'] is not None, err_msg
        # user_link = self.hateoas.link(rel='self', title='user',
        #                               href='http://{}/api/user/1')
        # err_msg = "The object link ir wrong: %s" % output['link']
        # assert user_link == output['link'], err_msg
        # # when the links specification of a user will be set, modify the following
        # err_msg = "The list of links should be empty for now"
        # assert output.get('links') == None, err_msg


    @with_context
    @patch('pybossa.api.task.TaskAPI._verify_auth')
    def test_01_link_object(self, auth):
        """Test HATEOAS object link is created"""
        # For project
        user = UserFactory.create(admin=True)
        auth.return_value = True

        res = self.app.get('/api/project?all=1&api_key=' + user.api_key, follow_redirects=True)
        output = json.loads(res.data)[0]
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        project_link = self.hateoas.link(rel='self', title='project',
                                     href='http://{}/api/project/1'.format(self.flask_app.config['SERVER_NAME']))

        err_msg = "The object link is wrong: %s" % output['link']
        assert project_link == output['link'], err_msg

        err_msg = "There should be a Links list with the category URI"
        assert output['links'] is not None, err_msg
        assert len(output['links']) == 1, err_msg
        project_link = self.hateoas.link(rel='category', title='category',
                                     href='http://{}/api/category/1'.format(self.flask_app.config['SERVER_NAME']))
        assert project_link == output['links'][0], err_msg

        # For task
        res = self.app.get('/api/task?all=1&api_key=' + user.api_key, follow_redirects=True)
        output = json.loads(res.data)[0]
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        task_link = self.hateoas.link(rel='self', title='task',
                                      href='http://{}/api/task/1'.format(self.flask_app.config['SERVER_NAME']))
        err_msg = "The object link is wrong: %s" % output['link']
        assert task_link == output['link'], err_msg
        err_msg = "There should be one parent link: project"
        assert output.get('links') is not None, err_msg
        assert len(output.get('links')) == 1, err_msg
        err_msg = "The parent link is wrong"
        project_link = self.hateoas.link(rel='parent', title='project',
                                     href='http://{}/api/project/1'.format(self.flask_app.config['SERVER_NAME']))
        assert output.get('links')[0] == project_link, project_link

        # For taskrun
        res = self.app.get('/api/taskrun?all=1&api_key=' + user.api_key, follow_redirects=True)
        output = json.loads(res.data)[0]
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        task_link = self.hateoas.link(rel='self', title='taskrun',
                                      href='http://{}/api/taskrun/1'.format(self.flask_app.config['SERVER_NAME']))
        err_msg = "The object link is wrong: %s" % output['link']
        assert task_link == output['link'], err_msg
        err_msg = "There should be two parent links: project and task"
        assert output.get('links') is not None, err_msg
        assert len(output.get('links')) == 2, err_msg
        err_msg = "The parent project link is wrong"
        project_link = self.hateoas.link(rel='parent', title='project',
                                     href='http://{}/api/project/1'.format(self.flask_app.config['SERVER_NAME']))
        assert output.get('links')[0] == project_link, err_msg

        err_msg = "The parent task link is wrong"
        project_link = self.hateoas.link(rel='parent', title='task',
                                     href='http://{}/api/task/1'.format(self.flask_app.config['SERVER_NAME']))
        assert output.get('links')[1] == project_link, err_msg

        # Check that hateoas removes all link and links from item
        without_links = self.hateoas.remove_links(output)
        err_msg = "There should not be any link or links keys"
        assert without_links.get('link') is None, err_msg
        assert without_links.get('links') is None, err_msg

        # For category
        res = self.app.get("/api/category", follow_redirects=True)
        output = json.loads(res.data)[0]
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        category_link = self.hateoas.link(rel='self', title='category',
                                      href='http://{}/api/category/1'.format(self.flask_app.config['SERVER_NAME']))
        err_msg = "The object link is wrong: %s" % output['link']
        assert category_link == output['link'], err_msg
        err_msg = "There should be no other links"
        assert output.get('links') is None, err_msg
        err_msg = "The object links should are wrong"
Ejemplo n.º 2
0
class APIBase(MethodView):
    """Class to create CRUD methods."""

    hateoas = Hateoas()

    allowed_classes_upload = ['blogpost', 'helpingmaterial', 'announcement']

    def refresh_cache(self, cls_name, oid):
        """Refresh the cache."""
        if caching.get(cls_name):
            caching.get(cls_name)['refresh'](oid)

    def valid_args(self):
        """Check if the domain object args are valid."""
        for k in request.args.keys():
            if k not in ['api_key']:
                getattr(self.__class__, k)

    def options(self, **kwargs):  # pragma: no cover
        """Return '' for Options method."""
        return ''

    @jsonpify
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def get(self, oid):
        """Get an object.

        Returns an item from the DB with the request.data JSON object or all
        the items if oid == None

        :arg self: The class of the object to be retrieved
        :arg integer oid: the ID of the object in the DB
        :returns: The JSON item/s stored in the DB

        """
        try:
            ensure_authorized_to('read', self.__class__)
            query = self._db_query(oid)
            json_response = self._create_json_response(query, oid)
            return Response(json_response, mimetype='application/json')
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='GET')

    def _create_json_response(self, query_result, oid):
        if len(query_result) == 1 and query_result[0] is None:
            raise abort(404)
        items = []
        for result in query_result:
            # This is for n_favs orderby case
            if not isinstance(result, DomainObject):
                if 'n_favs' in result.keys():
                    result = result[0]
            try:
                if (result.__class__ != self.__class__):
                    (item, headline, rank) = result
                else:
                    item = result
                    headline = None
                    rank = None
                datum = self._create_dict_from_model(item)
                if headline:
                    datum['headline'] = headline
                if rank:
                    datum['rank'] = rank
                ensure_authorized_to('read', item)
                items.append(datum)
            except (Forbidden, Unauthorized):
                # Remove last added item, as it is 401 or 403
                if len(items) > 0:
                    items.pop()
            except Exception:  # pragma: no cover
                raise
        if oid is not None:
            ensure_authorized_to('read', query_result[0])
            items = items[0]
        return json.dumps(items)

    def _create_dict_from_model(self, model):
        return self._select_attributes(self._add_hateoas_links(model))

    def _add_hateoas_links(self, item):
        obj = item.dictize()
        related = request.args.get('related')
        if related:
            if item.__class__.__name__ == 'Task':
                obj['task_runs'] = []
                obj['result'] = None
                task_runs = task_repo.filter_task_runs_by(task_id=item.id)
                results = result_repo.filter_by(task_id=item.id,
                                                last_version=True)
                for tr in task_runs:
                    obj['task_runs'].append(tr.dictize())
                for r in results:
                    obj['result'] = r.dictize()

            if item.__class__.__name__ == 'TaskRun':
                tasks = task_repo.filter_tasks_by(id=item.task_id)
                results = result_repo.filter_by(task_id=item.task_id,
                                                last_version=True)
                obj['task'] = None
                obj['result'] = None
                for t in tasks:
                    obj['task'] = t.dictize()
                for r in results:
                    obj['result'] = r.dictize()

            if item.__class__.__name__ == 'Result':
                tasks = task_repo.filter_tasks_by(id=item.task_id)
                task_runs = task_repo.filter_task_runs_by(task_id=item.task_id)
                obj['task_runs'] = []
                for t in tasks:
                    obj['task'] = t.dictize()
                for tr in task_runs:
                    obj['task_runs'].append(tr.dictize())

        links, link = self.hateoas.create_links(item)
        if links:
            obj['links'] = links
        if link:
            obj['link'] = link
        return obj

    def _db_query(self, oid):
        """Returns a list with the results of the query"""
        repo_info = repos[self.__class__.__name__]
        if oid is None:
            limit, offset, orderby = self._set_limit_and_offset()
            results = self._filter_query(repo_info, limit, offset, orderby)
        else:
            repo = repo_info['repo']
            query_func = repo_info['get']
            results = [getattr(repo, query_func)(oid)]
        return results

    def api_context(self, all_arg, **filters):
        if current_user.is_authenticated():
            filters['owner_id'] = current_user.id
        if filters.get('owner_id') and all_arg == '1':
            del filters['owner_id']
        return filters

    def _filter_query(self, repo_info, limit, offset, orderby):
        filters = {}
        for k in request.args.keys():
            if k not in [
                    'limit', 'offset', 'api_key', 'last_id', 'all',
                    'fulltextsearch', 'desc', 'orderby', 'related',
                    'participated', 'full'
            ]:
                # Raise an error if the k arg is not a column
                if self.__class__ == Task and k == 'external_uid':
                    pass
                else:
                    getattr(self.__class__, k)
                filters[k] = request.args[k]
        repo = repo_info['repo']
        filters = self.api_context(all_arg=request.args.get('all'), **filters)
        query_func = repo_info['filter']
        filters = self._custom_filter(filters)
        last_id = request.args.get('last_id')
        if request.args.get('participated'):
            filters['participated'] = get_user_id_or_ip()
        fulltextsearch = request.args.get('fulltextsearch')
        desc = request.args.get('desc') if request.args.get('desc') else False
        desc = fuzzyboolean(desc)
        if last_id:
            results = getattr(repo, query_func)(limit=limit,
                                                last_id=last_id,
                                                fulltextsearch=fulltextsearch,
                                                desc=False,
                                                orderby=orderby,
                                                **filters)
        else:
            results = getattr(repo, query_func)(limit=limit,
                                                offset=offset,
                                                fulltextsearch=fulltextsearch,
                                                desc=desc,
                                                orderby=orderby,
                                                **filters)
        return results

    def _set_limit_and_offset(self):
        try:
            limit = min(100, int(request.args.get('limit')))
        except (ValueError, TypeError):
            limit = 20
        try:
            offset = int(request.args.get('offset'))
        except (ValueError, TypeError):
            offset = 0
        try:
            orderby = request.args.get('orderby') if request.args.get(
                'orderby') else 'id'
        except (ValueError, TypeError):
            orderby = 'updated'
        return limit, offset, orderby

    @jsonpify
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def post(self):
        """Post an item to the DB with the request.data JSON object.

        :arg self: The class of the object to be inserted
        :returns: The JSON item stored in the DB

        """
        try:
            cls_name = self.__class__.__name__
            self.valid_args()
            data = self._file_upload(request)
            if data is None:
                data = json.loads(request.data)
            self._forbidden_attributes(data)
            inst = self._create_instance_from_request(data)
            repo = repos[self.__class__.__name__]['repo']
            save_func = repos[self.__class__.__name__]['save']
            getattr(repo, save_func)(inst)
            self._log_changes(None, inst)
            self.refresh_cache(cls_name, inst.id)
            return json.dumps(inst.dictize())
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='POST')

    def _create_instance_from_request(self, data):
        data = self.hateoas.remove_links(data)
        inst = self.__class__(**data)
        self._update_object(inst)
        ensure_authorized_to('create', inst)
        self._validate_instance(inst)
        return inst

    @jsonpify
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def delete(self, oid):
        """Delete a single item from the DB.

        :arg self: The class of the object to be deleted
        :arg integer oid: the ID of the object in the DB
        :returns: An HTTP status code based on the output of the action.

        More info about HTTP status codes for this action `here
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7>`_.

        """
        try:
            self.valid_args()
            self._delete_instance(oid)
            cls_name = self.__class__.__name__
            self.refresh_cache(cls_name, oid)
            return '', 204
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='DELETE')

    def _delete_instance(self, oid):
        repo = repos[self.__class__.__name__]['repo']
        query_func = repos[self.__class__.__name__]['get']
        inst = getattr(repo, query_func)(oid)
        if inst is None:
            raise NotFound
        ensure_authorized_to('delete', inst)
        self._file_delete(request, inst)
        self._log_changes(inst, None)
        delete_func = repos[self.__class__.__name__]['delete']
        getattr(repo, delete_func)(inst)
        return inst

    @jsonpify
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def put(self, oid):
        """Update a single item in the DB.

        :arg self: The class of the object to be updated
        :arg integer oid: the ID of the object in the DB
        :returns: An HTTP status code based on the output of the action.

        More info about HTTP status codes for this action `here
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6>`_.

        """
        try:
            self.valid_args()
            cls_name = self.__class__.__name__
            repo = repos[cls_name]['repo']
            query_func = repos[cls_name]['get']
            existing = getattr(repo, query_func)(oid)
            if existing is None:
                raise NotFound
            ensure_authorized_to('update', existing)
            data = self._file_upload(request)
            inst = self._update_instance(existing,
                                         repo,
                                         repos,
                                         new_upload=data)
            self.refresh_cache(cls_name, oid)
            return Response(json.dumps(inst.dictize()),
                            200,
                            mimetype='application/json')
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='PUT')

    def _update_instance(self, existing, repo, repos, new_upload=None):
        data = dict()
        if new_upload is None:
            data = json.loads(request.data)
            self._forbidden_attributes(data)
            # Remove hateoas links
            data = self.hateoas.remove_links(data)
        else:
            self._forbidden_attributes(request.form)
        # may be missing the id as we allow partial updates
        self.__class__(**data)
        old = self.__class__(**existing.dictize())
        for key in data:
            setattr(existing, key, data[key])
        if new_upload:
            existing.media_url = new_upload['media_url']
            existing.info['container'] = new_upload['info']['container']
            existing.info['file_name'] = new_upload['info']['file_name']
        self._update_attribute(existing, old)
        update_func = repos[self.__class__.__name__]['update']
        self._validate_instance(existing)
        getattr(repo, update_func)(existing)
        self._log_changes(old, existing)
        return existing

    def _update_object(self, data_dict):
        """Update object.

        Method to be overriden in inheriting classes which wish to update
        data dict.

        """
        pass

    def _update_attribute(self, new, old):
        """Update object attribute if new value is passed.
        Method to be overriden in inheriting classes which wish to update
        data dict.

        """

    def _select_attributes(self, item_data):
        """Method to be overriden in inheriting classes in case it is not
        desired that every object attribute is returned by the API.
        """
        return item_data

    def _custom_filter(self, query):
        """Method to be overriden in inheriting classes which wish to consider
        specific filtering criteria.
        """
        return query

    def _validate_instance(self, instance):
        """Method to be overriden in inheriting classes which may need to
        validate the creation (POST) or modification (PUT) of a domain object
        for reasons other than business logic ones (e.g. overlapping of a
        project name witht a URL).
        """
        pass

    def _log_changes(self, old_obj, new_obj):
        """Method to be overriden by inheriting classes for logging purposes"""
        pass

    def _forbidden_attributes(self, data):
        """Method to be overriden by inheriting classes that will not allow for
        certain fields to be used in PUT or POST requests"""
        pass

    def _file_upload(self, data):
        """Method that must be overriden by the class to allow file uploads for
        only a few classes."""
        cls_name = self.__class__.__name__.lower()
        content_type = 'multipart/form-data'
        if (content_type in request.headers.get('Content-Type')
                and cls_name in self.allowed_classes_upload):
            tmp = dict()
            for key in request.form.keys():
                tmp[key] = request.form[key]

            if isinstance(self, announcement.Announcement):
                # don't check project id for announcements
                ensure_authorized_to('create', self)
                upload_method = current_app.config.get('UPLOAD_METHOD')
                if request.files.get('file') is None:
                    raise AttributeError
                _file = request.files['file']
                container = "user_%s" % current_user.id
            else:
                ensure_authorized_to('create',
                                     self.__class__,
                                     project_id=tmp['project_id'])
                project = project_repo.get(tmp['project_id'])
                upload_method = current_app.config.get('UPLOAD_METHOD')
                if request.files.get('file') is None:
                    raise AttributeError
                _file = request.files['file']
                if current_user.admin:
                    container = "user_%s" % project.owner.id
                else:
                    container = "user_%s" % current_user.id
            uploader.upload_file(_file, container=container)
            file_url = get_avatar_url(upload_method, _file.filename, container)
            tmp['media_url'] = file_url
            if tmp.get('info') is None:
                tmp['info'] = dict()
            tmp['info']['container'] = container
            tmp['info']['file_name'] = _file.filename
            return tmp
        else:
            return None

    def _file_delete(self, request, obj):
        """Delete file object."""
        cls_name = self.__class__.__name__.lower()
        if cls_name in self.allowed_classes_upload:
            keys = obj.info.keys()
            if 'file_name' in keys and 'container' in keys:
                ensure_authorized_to('delete', obj)
                uploader.delete_file(obj.info['file_name'],
                                     obj.info['container'])
Ejemplo n.º 3
0
class APIBase(MethodView):
    """
    Class to create CRUD methods for all the items: applications,
    tasks and task runs.
    """
    hateoas = Hateoas()
    error_status = {"Forbidden": 403,
                    "NotFound": 404,
                    "Unauthorized": 401,
                    "TypeError": 415,
                    "ValueError": 415,
                    "DataError": 415,
                    "AttributeError": 415,
                    "IntegrityError": 415}

    def valid_args(self):
        for k in request.args.keys():
            if k not in ['api_key']:
                getattr(self.__class__, k)

    def format_exception(self, e, action):
        """Formats the exception to a valid JSON object"""
        exception_cls = e.__class__.__name__
        if self.error_status.get(exception_cls):
            status = self.error_status.get(exception_cls)
        else:
            status = 200
        error = dict(action=action,
                     status="failed",
                     status_code=status,
                     target=self.__class__.__name__.lower(),
                     exception_cls=exception_cls,
                     exception_msg=e.message)
        return Response(json.dumps(error), status=status,
                        mimetype='application/json')

    @crossdomain(origin='*', headers=cors_headers)
    def options(self):
        return ''

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    def get(self, id):
        """
        Returns an item from the DB with the request.data JSON object or all
        the items if id == None

        :arg self: The class of the object to be retrieved
        :arg integer id: the ID of the object in the DB
        :returns: The JSON item/s stored in the DB
        """
        try:
            getattr(require, self.__class__.__name__.lower()).read()
            if id is None:
                query = db.session.query(self.__class__)
                for k in request.args.keys():
                    if k not in ['limit', 'offset', 'api_key']:
                        # Raise an error if the k arg is not a column
                        getattr(self.__class__, k)
                        query = query.filter(getattr(self.__class__, k) == request.args[k])
                try:
                    limit = min(10000, int(request.args.get('limit')))
                except (ValueError, TypeError):
                    limit = 20

                try:
                    offset = int(request.args.get('offset'))
                except (ValueError, TypeError):
                    offset = 0

                query = query.order_by(self.__class__.id)
                query = query.limit(limit)
                query = query.offset(offset)
                items = []
                for item in query.all():
                    obj = item.dictize()
                    links, link = self.hateoas.create_links(item)
                    if links:
                        obj['links'] = links
                    if link:
                        obj['link'] = link
                    items.append(obj)
                return Response(json.dumps(items), mimetype='application/json')
            else:
                item = db.session.query(self.__class__).get(id)
                if item is None:
                    abort(404)
                else:
                    obj = item.dictize()
                    links, link = self.hateoas.create_links(item)
                    if links:
                        obj['links'] = links
                    if link:
                        obj['link'] = link
                    return Response(json.dumps(obj), mimetype='application/json')
        except Exception as e:
            return self.format_exception(e, action='GET')

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    def post(self):
        """
        Adds an item to the DB with the request.data JSON object

        :arg self: The class of the object to be inserted
        :returns: The JSON item stored in the DB
        """
        try:
            self.valid_args()
            data = json.loads(request.data)
            # Clean HATEOAS args
            data = self.hateoas.remove_links(data)
            inst = self.__class__(**data)
            getattr(require, self.__class__.__name__.lower()).create(inst)
            self._update_object(inst)
            db.session.add(inst)
            db.session.commit()
            return json.dumps(inst.dictize())
        except Exception as e:
            return self.format_exception(e, action='POST')

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    def delete(self, id):
        """
        Deletes a single item from the DB

        :arg self: The class of the object to be deleted
        :arg integer id: the ID of the object in the DB
        :returns: An HTTP status code based on the output of the action.

        More info about HTTP status codes for this action `here
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7>`_.
        """
        try:
            self.valid_args()
            item = db.session.query(self.__class__).get(id)
            if item is None:
                raise NotFound
            getattr(require, self.__class__.__name__.lower()).delete(item)
            db.session.delete(item)
            db.session.commit()
            return '', 204
        except Exception as e:
            return self.format_exception(e, action='DELETE')

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    def put(self, id):
        """
        Updates a single item in the DB

        :arg self: The class of the object to be updated
        :arg integer id: the ID of the object in the DB
        :returns: An HTTP status code based on the output of the action.

        More info about HTTP status codes for this action `here
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6>`_.
        """
        try:
            self.valid_args()
            existing = db.session.query(self.__class__).get(id)
            if existing is None:
                raise NotFound
            getattr(require, self.__class__.__name__.lower()).update(existing)
            data = json.loads(request.data)
            # may be missing the id as we allow partial updates
            data['id'] = id
            # Clean HATEOAS args
            data = self.hateoas.remove_links(data)
            inst = self.__class__(**data)
            db.session.merge(inst)
            db.session.commit()
            return Response(json.dumps(inst.dictize()), 200,
                            mimetype='application/json')
        except Exception as e:
            return self.format_exception(e, 'PUT')

    def _update_object(self, data_dict):
        '''Method to be overriden in inheriting classes which wish to update
        data dict.'''
        pass
Ejemplo n.º 4
0
class APIBase(MethodView):
    """Class to create CRUD methods."""

    hateoas = Hateoas()

    slave_session = get_session(db, bind='slave')

    def valid_args(self):
        """Check if the domain object args are valid."""
        for k in request.args.keys():
            if k not in ['api_key']:
                getattr(self.__class__, k)

    @crossdomain(origin='*', headers=cors_headers)
    def options(self):  # pragma: no cover
        """Return '' for Options method."""
        return ''

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def get(self, id):
        """Get an object.

        Returns an item from the DB with the request.data JSON object or all
        the items if id == None

        :arg self: The class of the object to be retrieved
        :arg integer id: the ID of the object in the DB
        :returns: The JSON item/s stored in the DB

        """
        try:
            getattr(require, self.__class__.__name__.lower()).read()
            query = self._db_query(id)
            json_response = self._create_json_response(query, id)
            return Response(json_response, mimetype='application/json')
        except Exception as e:
            self.slave_session.rollback()
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='GET')
        finally:
            self.slave_session.close()

    def _create_json_response(self, query_result, id):
        if len(query_result) == 1 and query_result[0] is None:
            raise abort(404)
        items = []
        for item in query_result:
            try:
                items.append(self._create_dict_from_model(item))
                getattr(require, self.__class__.__name__.lower()).read(item)
            except (Forbidden, Unauthorized):
                # Remove last added item, as it is 401 or 403
                items.pop()
            except:  # pragma: no cover
                raise
        if id:
            getattr(require,
                    self.__class__.__name__.lower()).read(query_result[0])
            items = items[0]
        return json.dumps(items)

    def _create_dict_from_model(self, model):
        return self._select_attributes(self._add_hateoas_links(model))

    def _add_hateoas_links(self, item):
        obj = item.dictize()
        links, link = self.hateoas.create_links(item)
        if links:
            obj['links'] = links
        if link:
            obj['link'] = link
        return obj

    def _db_query(self, id):
        """ Returns a list with the results of the query"""
        query = self.slave_session.query(self.__class__)
        if id is None:
            limit, offset = self._set_limit_and_offset()
            query = self._filter_query(query, limit, offset)
        else:
            query = [query.get(id)]
        return query

    def _filter_query(self, query, limit, offset):
        for k in request.args.keys():
            if k not in ['limit', 'offset', 'api_key']:
                # Raise an error if the k arg is not a column
                getattr(self.__class__, k)
                query = query.filter(
                    getattr(self.__class__, k) == request.args[k])
        query = self._custom_filter(query)
        return self._format_query_result(query, limit, offset)

    def _format_query_result(self, query, limit, offset):
        query = query.order_by(self.__class__.id)
        query = query.limit(limit)
        query = query.offset(offset)
        return query.all()

    def _set_limit_and_offset(self):
        try:
            limit = min(100, int(request.args.get('limit')))
        except (ValueError, TypeError):
            limit = 20
        try:
            offset = int(request.args.get('offset'))
        except (ValueError, TypeError):
            offset = 0
        return limit, offset

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def post(self):
        """Post an item to the DB with the request.data JSON object.

        :arg self: The class of the object to be inserted
        :returns: The JSON item stored in the DB

        """
        try:
            self.valid_args()
            data = json.loads(request.data)
            # Clean HATEOAS args
            inst = self._create_instance_from_request(data)
            db.session.add(inst)
            db.session.commit()
            return json.dumps(inst.dictize())
        except IntegrityError:
            db.session.rollback()
            raise
        except Exception as e:
            db.session.rollback()
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='POST')

    def _create_instance_from_request(self, data):
        data = self.hateoas.remove_links(data)
        inst = self.__class__(**data)
        self._update_object(inst)
        getattr(require, self.__class__.__name__.lower()).create(inst)
        return inst

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def delete(self, id):
        """Delete a single item from the DB.

        :arg self: The class of the object to be deleted
        :arg integer id: the ID of the object in the DB
        :returns: An HTTP status code based on the output of the action.

        More info about HTTP status codes for this action `here
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7>`_.

        """
        try:
            self.valid_args()
            inst = self._delete_instance(id)
            self._refresh_cache(inst)
            return '', 204
        except Exception as e:
            db.session.rollback()
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='DELETE')

    def _delete_instance(self, id):
        inst = db.session.query(self.__class__).get(id)
        if inst is None:
            raise NotFound
        getattr(require, self.__class__.__name__.lower()).delete(inst)
        db.session.delete(inst)
        db.session.commit()
        return inst

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def put(self, id):
        """Update a single item in the DB.

        :arg self: The class of the object to be updated
        :arg integer id: the ID of the object in the DB
        :returns: An HTTP status code based on the output of the action.

        More info about HTTP status codes for this action `here
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6>`_.

        """
        try:
            self.valid_args()
            inst = self._update_instance(id)
            self._refresh_cache(inst)
            return Response(json.dumps(inst.dictize()),
                            200,
                            mimetype='application/json')
        except IntegrityError:
            db.session.rollback()
            raise
        except Exception as e:
            db.session.rollback()
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='PUT')

    def _update_instance(self, id):
        existing = db.session.query(self.__class__).get(id)
        if existing is None:
            raise NotFound
        getattr(require, self.__class__.__name__.lower()).update(existing)
        data = json.loads(request.data)
        # may be missing the id as we allow partial updates
        data['id'] = id
        # Clean HATEOAS args
        data = self.hateoas.remove_links(data)
        inst = self.__class__(**data)
        db.session.merge(inst)
        db.session.commit()
        return inst

    def _update_object(self, data_dict):
        """Update object.

        Method to be overriden in inheriting classes which wish to update
        data dict.

        """
        pass

    def _refresh_cache(self, data_dict):
        """Refresh cache.

        Method to be overriden in inheriting classes which wish to refresh
        cache for given object.

        """
        pass

    def _select_attributes(self, item_data):
        """Method to be overriden in inheriting classes in case it is not
        desired that every object attribute is returned by the API
        """
        return item_data

    def _custom_filter(self, query):
        """Method to be overriden in inheriting classes which wish to consider
        specific filtering criteria
        """
        return query
Ejemplo n.º 5
0
class TestHateoas(Test):

    hateoas = Hateoas()

    def setUp(self):
        super(TestHateoas, self).setUp()
        with self.flask_app.app_context():
            self.create()

    # Tests
    def test_00_link_object(self):
        """Test HATEOAS object link is created"""
        # For project
        res = self.app.get("/api/project/1", follow_redirects=True)
        output = json.loads(res.data)
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg

        err_msg = "There should be a Links list with the category URI"
        assert output['links'] is not None, err_msg
        assert len(output['links']) == 1, err_msg
        project_link = self.hateoas.link(
            rel='category',
            title='category',
            href='http://localhost/api/category/1')
        assert project_link == output['links'][0], err_msg

        project_link = self.hateoas.link(rel='self',
                                         title='project',
                                         href='http://localhost/api/project/1')
        err_msg = "The object link is wrong: %s" % output['link']
        assert project_link == output['link'], err_msg

        # For task
        res = self.app.get("/api/task/1", follow_redirects=True)
        output = json.loads(res.data)
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        task_link = self.hateoas.link(rel='self',
                                      title='task',
                                      href='http://localhost/api/task/1')
        err_msg = "The object link is wrong: %s" % output['link']
        assert task_link == output['link'], err_msg
        err_msg = "There should be one parent link: project"
        assert output.get('links') is not None, err_msg
        assert len(output.get('links')) == 1, err_msg
        err_msg = "The parent link is wrong"
        project_link = self.hateoas.link(rel='parent',
                                         title='project',
                                         href='http://localhost/api/project/1')
        assert output.get('links')[0] == project_link, err_msg

        # For taskrun
        res = self.app.get("/api/taskrun/1", follow_redirects=True)
        output = json.loads(res.data)
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        task_link = self.hateoas.link(rel='self',
                                      title='taskrun',
                                      href='http://localhost/api/taskrun/1')
        err_msg = "The object link is wrong: %s" % output['link']
        assert task_link == output['link'], err_msg
        err_msg = "There should be two parent links: project and task"
        assert output.get('links') is not None, err_msg
        assert len(output.get('links')) == 2, err_msg
        err_msg = "The parent project link is wrong"
        project_link = self.hateoas.link(rel='parent',
                                         title='project',
                                         href='http://localhost/api/project/1')
        assert output.get('links')[0] == project_link, err_msg

        err_msg = "The parent task link is wrong"
        project_link = self.hateoas.link(rel='parent',
                                         title='task',
                                         href='http://localhost/api/task/1')
        assert output.get('links')[1] == project_link, err_msg
        res = self.app.post("/api/taskrun")

        # For category
        res = self.app.get("/api/category/1", follow_redirects=True)
        output = json.loads(res.data)
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        category_link = self.hateoas.link(
            rel='self',
            title='category',
            href='http://localhost/api/category/1')
        err_msg = "The object link is wrong: %s" % output['link']
        assert category_link == output['link'], err_msg
        err_msg = "There should be no other links"
        assert output.get('links') is None, err_msg
        err_msg = "The object links should are wrong"

        # For user
        # Pending define what user fields will be visible through the API
        # Issue #626. For now let's suppose link and links are not visible
        # res = self.app.get("/api/user/1?api_key=" + self.root_api_key, follow_redirects=True)
        # output = json.loads(res.data)
        # err_msg = "There should be a Link with the object URI"
        # assert output['link'] is not None, err_msg
        # user_link = self.hateoas.link(rel='self', title='user',
        #                               href='http://localhost/api/user/1')
        # err_msg = "The object link ir wrong: %s" % output['link']
        # assert user_link == output['link'], err_msg
        # # when the links specification of a user will be set, modify the following
        # err_msg = "The list of links should be empty for now"
        # assert output.get('links') == None, err_msg

    def test_01_link_object(self):
        """Test HATEOAS object link is created"""
        # For project
        res = self.app.get("/api/project", follow_redirects=True)
        output = json.loads(res.data)[0]
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        project_link = self.hateoas.link(rel='self',
                                         title='project',
                                         href='http://localhost/api/project/1')

        err_msg = "The object link is wrong: %s" % output['link']
        assert project_link == output['link'], err_msg

        err_msg = "There should be a Links list with the category URI"
        assert output['links'] is not None, err_msg
        assert len(output['links']) == 1, err_msg
        project_link = self.hateoas.link(
            rel='category',
            title='category',
            href='http://localhost/api/category/1')
        assert project_link == output['links'][0], err_msg

        # For task
        res = self.app.get("/api/task", follow_redirects=True)
        output = json.loads(res.data)[0]
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        task_link = self.hateoas.link(rel='self',
                                      title='task',
                                      href='http://localhost/api/task/1')
        err_msg = "The object link is wrong: %s" % output['link']
        assert task_link == output['link'], err_msg
        err_msg = "There should be one parent link: project"
        assert output.get('links') is not None, err_msg
        assert len(output.get('links')) == 1, err_msg
        err_msg = "The parent link is wrong"
        project_link = self.hateoas.link(rel='parent',
                                         title='project',
                                         href='http://localhost/api/project/1')
        assert output.get('links')[0] == project_link, project_link

        # For taskrun
        res = self.app.get("/api/taskrun", follow_redirects=True)
        output = json.loads(res.data)[0]
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        task_link = self.hateoas.link(rel='self',
                                      title='taskrun',
                                      href='http://localhost/api/taskrun/1')
        err_msg = "The object link is wrong: %s" % output['link']
        assert task_link == output['link'], err_msg
        err_msg = "There should be two parent links: project and task"
        assert output.get('links') is not None, err_msg
        assert len(output.get('links')) == 2, err_msg
        err_msg = "The parent project link is wrong"
        project_link = self.hateoas.link(rel='parent',
                                         title='project',
                                         href='http://localhost/api/project/1')
        assert output.get('links')[0] == project_link, err_msg

        err_msg = "The parent task link is wrong"
        project_link = self.hateoas.link(rel='parent',
                                         title='task',
                                         href='http://localhost/api/task/1')
        assert output.get('links')[1] == project_link, err_msg

        # Check that hateoas removes all link and links from item
        without_links = self.hateoas.remove_links(output)
        err_msg = "There should not be any link or links keys"
        assert without_links.get('link') is None, err_msg
        assert without_links.get('links') is None, err_msg

        # For category
        res = self.app.get("/api/category", follow_redirects=True)
        output = json.loads(res.data)[0]
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        category_link = self.hateoas.link(
            rel='self',
            title='category',
            href='http://localhost/api/category/1')
        err_msg = "The object link is wrong: %s" % output['link']
        assert category_link == output['link'], err_msg
        err_msg = "There should be no other links"
        assert output.get('links') is None, err_msg
        err_msg = "The object links should are wrong"
Ejemplo n.º 6
0
class APIBase(MethodView):
    """Class to create CRUD methods."""

    hateoas = Hateoas()

    def valid_args(self):
        """Check if the domain object args are valid."""
        for k in request.args.keys():
            if k not in ['api_key']:
                getattr(self.__class__, k)

    @crossdomain(origin='*', headers=cors_headers)
    def options(self):  # pragma: no cover
        """Return '' for Options method."""
        return ''

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def get(self, oid):
        """Get an object.

        Returns an item from the DB with the request.data JSON object or all
        the items if oid == None

        :arg self: The class of the object to be retrieved
        :arg integer oid: the ID of the object in the DB
        :returns: The JSON item/s stored in the DB

        """
        try:
            ensure_authorized_to('read', self.__class__)
            query = self._db_query(oid)
            json_response = self._create_json_response(query, oid)
            return Response(json_response, mimetype='application/json')
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='GET')

    def _create_json_response(self, query_result, oid):
        if len(query_result) == 1 and query_result[0] is None:
            raise abort(404)
        items = []
        for item in query_result:
            try:
                items.append(self._create_dict_from_model(item))
                ensure_authorized_to('read', item)
            except (Forbidden, Unauthorized):
                # Remove last added item, as it is 401 or 403
                items.pop()
            except Exception:  # pragma: no cover
                raise
        if oid is not None:
            ensure_authorized_to('read', query_result[0])
            items = items[0]
        return json.dumps(items)

    def _create_dict_from_model(self, model):
        return self._select_attributes(self._add_hateoas_links(model))

    def _add_hateoas_links(self, item):
        obj = item.dictize()
        links, link = self.hateoas.create_links(item)
        if links:
            obj['links'] = links
        if link:
            obj['link'] = link
        return obj

    def _db_query(self, oid):
        """Returns a list with the results of the query"""
        repo_info = repos[self.__class__.__name__]
        if oid is None:
            limit, offset = self._set_limit_and_offset()
            results = self._filter_query(repo_info, limit, offset)
        else:
            repo = repo_info['repo']
            query_func = repo_info['get']
            results = [getattr(repo, query_func)(oid)]
        return results

    def api_context(self, all_arg, **filters):
        if current_user.is_authenticated():
            filters['owner_id'] = current_user.id
        if filters.get('owner_id') and all_arg == '1':
            del filters['owner_id']
        return filters

    def _filter_query(self, repo_info, limit, offset):
        filters = {}
        for k in request.args.keys():
            if k not in [
                    'limit', 'offset', 'api_key', 'last_id', 'all',
                    'fulltextsearch'
            ]:
                # Raise an error if the k arg is not a column
                getattr(self.__class__, k)
                filters[k] = request.args[k]
        repo = repo_info['repo']
        filters = self.api_context(all_arg=request.args.get('all'), **filters)
        query_func = repo_info['filter']
        filters = self._custom_filter(filters)
        last_id = request.args.get('last_id')
        fulltextsearch = request.args.get('fulltextsearch')
        if last_id:
            results = getattr(repo, query_func)(limit=limit,
                                                last_id=last_id,
                                                fulltextsearch=fulltextsearch,
                                                **filters)
        else:
            results = getattr(repo, query_func)(limit=limit,
                                                offset=offset,
                                                fulltextsearch=fulltextsearch,
                                                **filters)
        return results

    def _set_limit_and_offset(self):
        try:
            limit = min(100, int(request.args.get('limit')))
        except (ValueError, TypeError):
            limit = 20
        try:
            offset = int(request.args.get('offset'))
        except (ValueError, TypeError):
            offset = 0
        return limit, offset

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def post(self):
        """Post an item to the DB with the request.data JSON object.

        :arg self: The class of the object to be inserted
        :returns: The JSON item stored in the DB

        """
        try:
            self.valid_args()
            data = json.loads(request.data)
            self._forbidden_attributes(data)
            inst = self._create_instance_from_request(data)
            repo = repos[self.__class__.__name__]['repo']
            save_func = repos[self.__class__.__name__]['save']
            getattr(repo, save_func)(inst)
            self._log_changes(None, inst)
            return json.dumps(inst.dictize())
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='POST')

    def _create_instance_from_request(self, data):
        data = self.hateoas.remove_links(data)
        inst = self.__class__(**data)
        self._update_object(inst)
        ensure_authorized_to('create', inst)
        self._validate_instance(inst)
        return inst

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def delete(self, oid):
        """Delete a single item from the DB.

        :arg self: The class of the object to be deleted
        :arg integer oid: the ID of the object in the DB
        :returns: An HTTP status code based on the output of the action.

        More info about HTTP status codes for this action `here
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7>`_.

        """
        try:
            self.valid_args()
            self._delete_instance(oid)
            return '', 204
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='DELETE')

    def _delete_instance(self, oid):
        repo = repos[self.__class__.__name__]['repo']
        query_func = repos[self.__class__.__name__]['get']
        inst = getattr(repo, query_func)(oid)
        if inst is None:
            raise NotFound
        ensure_authorized_to('delete', inst)
        self._log_changes(inst, None)
        delete_func = repos[self.__class__.__name__]['delete']
        getattr(repo, delete_func)(inst)
        return inst

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=ratelimits.get('LIMIT'), per=ratelimits.get('PER'))
    def put(self, oid):
        """Update a single item in the DB.

        :arg self: The class of the object to be updated
        :arg integer oid: the ID of the object in the DB
        :returns: An HTTP status code based on the output of the action.

        More info about HTTP status codes for this action `here
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6>`_.

        """
        try:
            self.valid_args()
            inst = self._update_instance(oid)
            return Response(json.dumps(inst.dictize()),
                            200,
                            mimetype='application/json')
        except Exception as e:
            return error.format_exception(
                e, target=self.__class__.__name__.lower(), action='PUT')

    def _update_instance(self, oid):
        repo = repos[self.__class__.__name__]['repo']
        query_func = repos[self.__class__.__name__]['get']
        existing = getattr(repo, query_func)(oid)
        if existing is None:
            raise NotFound
        ensure_authorized_to('update', existing)
        data = json.loads(request.data)
        self._forbidden_attributes(data)
        # Remove hateoas links
        data = self.hateoas.remove_links(data)
        # may be missing the id as we allow partial updates
        data['id'] = oid
        self.__class__(**data)
        old = self.__class__(**existing.dictize())
        for key in data:
            setattr(existing, key, data[key])
        self._update_attribute(existing, old)
        update_func = repos[self.__class__.__name__]['update']
        self._validate_instance(existing)
        getattr(repo, update_func)(existing)
        self._log_changes(old, existing)
        return existing

    def _update_object(self, data_dict):
        """Update object.

        Method to be overriden in inheriting classes which wish to update
        data dict.

        """
        pass

    def _update_attribute(self, new, old):
        """Update object attribute if new value is passed.
        Method to be overriden in inheriting classes which wish to update
        data dict.

        """

    def _select_attributes(self, item_data):
        """Method to be overriden in inheriting classes in case it is not
        desired that every object attribute is returned by the API.
        """
        return item_data

    def _custom_filter(self, query):
        """Method to be overriden in inheriting classes which wish to consider
        specific filtering criteria.
        """
        return query

    def _validate_instance(self, instance):
        """Method to be overriden in inheriting classes which may need to
        validate the creation (POST) or modification (PUT) of a domain object
        for reasons other than business logic ones (e.g. overlapping of a
        project name witht a URL).
        """
        pass

    def _log_changes(self, old_obj, new_obj):
        """Method to be overriden by inheriting classes for logging purposes"""
        pass

    def _forbidden_attributes(self, data):
        """Method to be overriden by inheriting classes that will not allow for
        certain fields to be used in PUT or POST requests"""
        pass
Ejemplo n.º 7
0
class APIBase(MethodView):

    """Class to create CRUD methods."""

    hateoas = Hateoas()

    def valid_args(self):
        """Check if the domain object args are valid."""
        for k in request.args.keys():
            if k not in ['api_key']:
                getattr(self.__class__, k)

    @crossdomain(origin='*', headers=cors_headers)
    def options(self):  # pragma: no cover
        """Return '' for Options method."""
        return ''

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=300, per=15 * 60)
    def get(self, id):
        """Get an object.

        Returns an item from the DB with the request.data JSON object or all
        the items if id == None

        :arg self: The class of the object to be retrieved
        :arg integer id: the ID of the object in the DB
        :returns: The JSON item/s stored in the DB

        """
        try:
            self._get()
            getattr(require, self.__class__.__name__.lower()).read()
            if id is None:
                query = db.session.query(self.__class__)
                for k in request.args.keys():
                    if k not in ['limit', 'offset', 'api_key']:
                        # Raise an error if the k arg is not a column
                        getattr(self.__class__, k)
                        query = query.filter(
                            getattr(self.__class__, k) == request.args[k])
                try:
                    limit = min(10000, int(request.args.get('limit')))
                except (ValueError, TypeError):
                    limit = 20

                try:
                    offset = int(request.args.get('offset'))
                except (ValueError, TypeError):
                    offset = 0

                query = query.order_by(self.__class__.id)
                query = query.limit(limit)
                query = query.offset(offset)
                items = []
                for item in query.all():
                    obj = item.dictize()
                    links, link = self.hateoas.create_links(item)
                    if links:
                        obj['links'] = links
                    if link:
                        obj['link'] = link
                    items.append(obj)
                return Response(json.dumps(items), mimetype='application/json')
            else:
                item = db.session.query(self.__class__).get(id)
                if item is None:
                    raise abort(404)
                else:
                    getattr(require,
                            self.__class__.__name__.lower()).read(item)
                    obj = item.dictize()
                    links, link = self.hateoas.create_links(item)
                    if links:
                        obj['links'] = links
                    if link:
                        obj['link'] = link
                    return Response(json.dumps(obj),
                                    mimetype='application/json')
        except Exception as e:
            return error.format_exception(
                e,
                target=self.__class__.__name__.lower(),
                action='GET')

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=300, per=15 * 60)
    def post(self):
        """Post an item to the DB with the request.data JSON object.

        :arg self: The class of the object to be inserted
        :returns: The JSON item stored in the DB

        """
        try:
            self._post()
            self.valid_args()
            data = json.loads(request.data)
            # Clean HATEOAS args
            data = self.hateoas.remove_links(data)
            inst = self.__class__(**data)
            getattr(require, self.__class__.__name__.lower()).create(inst)
            self._update_object(inst)
            db.session.add(inst)
            db.session.commit()
            return json.dumps(inst.dictize())
        except Exception as e:
            return error.format_exception(
                e,
                target=self.__class__.__name__.lower(),
                action='POST')

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=300, per=15 * 60)
    def delete(self, id):
        """Delete a single item from the DB.

        :arg self: The class of the object to be deleted
        :arg integer id: the ID of the object in the DB
        :returns: An HTTP status code based on the output of the action.

        More info about HTTP status codes for this action `here
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.7>`_.

        """
        try:
            self._delete()
            self.valid_args()
            inst = db.session.query(self.__class__).get(id)
            if inst is None:
                raise NotFound
            getattr(require, self.__class__.__name__.lower()).delete(inst)
            db.session.delete(inst)
            db.session.commit()
            self._refresh_cache(inst)
            return '', 204
        except Exception as e:
            return error.format_exception(
                e,
                target=self.__class__.__name__.lower(),
                action='DELETE')

    @jsonpify
    @crossdomain(origin='*', headers=cors_headers)
    @ratelimit(limit=300, per=15 * 60)
    def put(self, id):
        """Update a single item in the DB.

        :arg self: The class of the object to be updated
        :arg integer id: the ID of the object in the DB
        :returns: An HTTP status code based on the output of the action.

        More info about HTTP status codes for this action `here
        <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6>`_.

        """
        try:
            self._put()
            self.valid_args()
            existing = db.session.query(self.__class__).get(id)
            if existing is None:
                raise NotFound
            getattr(require, self.__class__.__name__.lower()).update(existing)
            data = json.loads(request.data)
            # may be missing the id as we allow partial updates
            data['id'] = id
            # Clean HATEOAS args
            data = self.hateoas.remove_links(data)
            inst = self.__class__(**data)
            db.session.merge(inst)
            db.session.commit()
            self._refresh_cache(inst)
            return Response(json.dumps(inst.dictize()), 200,
                            mimetype='application/json')
        except Exception as e:
            return error.format_exception(
                e,
                target=self.__class__.__name__.lower(),
                action='PUT')

    def _get(self):
        """GET method to override."""
        pass

    def _post(self):
        """POST method to override."""
        pass

    def _put(self):
        """PUT method to override."""
        pass

    def _delete(self):
        """DELETE method to override."""
        pass

    def _update_object(self, data_dict):
        """Update object.

        Method to be overriden in inheriting classes which wish to update
        data dict.

        """
        pass

    def _refresh_cache(self, data_dict):
        """Refresh cache.

        Method to be overriden in inheriting classes which wish to refresh
        cache for given object.

        """
        pass
Ejemplo n.º 8
0
class TestHateoas(web_helper.Helper):
    url = "/app/%s/tasks/export" % Fixtures.app_short_name

    hateoas = Hateoas()

    def setUp(self):
        super(TestHateoas, self).setUp()
        Fixtures.create()

    # Tests

    def test_00_link_object(self):
        """Test HATEOAS object link is created"""
        # For app
        res = self.app.get("/api/app/1", follow_redirects=True)
        output = json.loads(res.data)
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        app_link = self.hateoas.link(rel='self',
                                     title='app',
                                     href='http://localhost/api/app/1')

        err_msg = "The object link is wrong: %s" % output['link']
        assert app_link == output['link'], err_msg
        err_msg = "There should not be links, this is the parent object"
        assert output.get('links') is None, err_msg

        # For task
        res = self.app.get("/api/task/1", follow_redirects=True)
        output = json.loads(res.data)
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        task_link = self.hateoas.link(rel='self',
                                      title='task',
                                      href='http://localhost/api/task/1')
        err_msg = "The object link is wrong: %s" % output['link']
        assert task_link == output['link'], err_msg
        err_msg = "There should be one parent link: app"
        assert output.get('links') is not None, err_msg
        assert len(output.get('links')) == 1, err_msg
        err_msg = "The parent link is wrong"
        app_link = self.hateoas.link(rel='parent',
                                     title='app',
                                     href='http://localhost/api/app/1')
        assert output.get('links')[0] == app_link, err_msg

        # For taskrun
        res = self.app.get("/api/taskrun/1", follow_redirects=True)
        output = json.loads(res.data)
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        task_link = self.hateoas.link(rel='self',
                                      title='taskrun',
                                      href='http://localhost/api/taskrun/1')
        err_msg = "The object link is wrong: %s" % output['link']
        assert task_link == output['link'], err_msg
        err_msg = "There should be two parent links: app and task"
        assert output.get('links') is not None, err_msg
        assert len(output.get('links')) == 2, err_msg
        err_msg = "The parent app link is wrong"
        app_link = self.hateoas.link(rel='parent',
                                     title='app',
                                     href='http://localhost/api/app/1')
        assert output.get('links')[0] == app_link, err_msg

        err_msg = "The parent task link is wrong"
        app_link = self.hateoas.link(rel='parent',
                                     title='task',
                                     href='http://localhost/api/task/1')
        assert output.get('links')[1] == app_link, err_msg

    def test_01_link_object(self):
        """Test HATEOAS object link is created"""
        # For app
        res = self.app.get("/api/app", follow_redirects=True)
        output = json.loads(res.data)[0]
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        app_link = self.hateoas.link(rel='self',
                                     title='app',
                                     href='http://localhost/api/app/1')

        err_msg = "The object link is wrong: %s" % output['link']
        assert app_link == output['link'], err_msg
        err_msg = "There should not be links, this is the parent object"
        assert output.get('links') is None, err_msg

        # For task
        res = self.app.get("/api/task", follow_redirects=True)
        output = json.loads(res.data)[0]
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        task_link = self.hateoas.link(rel='self',
                                      title='task',
                                      href='http://localhost/api/task/1')
        err_msg = "The object link is wrong: %s" % output['link']
        assert task_link == output['link'], err_msg
        err_msg = "There should be one parent link: app"
        assert output.get('links') is not None, err_msg
        assert len(output.get('links')) == 1, err_msg
        err_msg = "The parent link is wrong"
        app_link = self.hateoas.link(rel='parent',
                                     title='app',
                                     href='http://localhost/api/app/1')
        assert output.get('links')[0] == app_link, err_msg

        # For taskrun
        res = self.app.get("/api/taskrun", follow_redirects=True)
        output = json.loads(res.data)[0]
        err_msg = "There should be a Link with the object URI"
        assert output['link'] is not None, err_msg
        task_link = self.hateoas.link(rel='self',
                                      title='taskrun',
                                      href='http://localhost/api/taskrun/1')
        err_msg = "The object link is wrong: %s" % output['link']
        assert task_link == output['link'], err_msg
        err_msg = "There should be two parent links: app and task"
        assert output.get('links') is not None, err_msg
        assert len(output.get('links')) == 2, err_msg
        err_msg = "The parent app link is wrong"
        app_link = self.hateoas.link(rel='parent',
                                     title='app',
                                     href='http://localhost/api/app/1')
        assert output.get('links')[0] == app_link, err_msg

        err_msg = "The parent task link is wrong"
        app_link = self.hateoas.link(rel='parent',
                                     title='task',
                                     href='http://localhost/api/task/1')
        assert output.get('links')[1] == app_link, err_msg