Repository : http://git.fedorahosted.org/cgit/copr.git
On branch : master
commit 2f07d9f691fa82df68a78a56ff4c0634c6b3324c Author: Valentin Gologuzov vgologuz@redhat.com Date: Tue Dec 16 19:26:49 2014 +0100
[frontend] REST API [WIP]
frontend/copr-frontend.spec | 4 + frontend/coprs_frontend/coprs/__init__.py | 5 + .../coprs_frontend/coprs/logic/complex_logic.py | 27 +++ frontend/coprs_frontend/coprs/logic/coprs_logic.py | 15 +- frontend/coprs_frontend/coprs/models.py | 43 +++++ frontend/coprs_frontend/coprs/rest_api/__init__.py | 101 +++++++++++ .../coprs_frontend/coprs/rest_api/exceptions.py | 45 +++++ .../coprs/rest_api/resources}/__init__.py | 0 .../coprs/rest_api/resources/build.py | 89 ++++++++++ .../coprs/rest_api/resources/chroot.py | 54 ++++++ .../coprs/rest_api/resources/copr.py | 180 ++++++++++++++++++++ frontend/coprs_frontend/coprs/rest_api/schemas.py | 87 ++++++++++ frontend/coprs_frontend/coprs/rest_api/util.py | 54 ++++++ .../coprs/views/api_ns/api_general.py | 6 +- .../coprs/views/coprs_ns/coprs_general.py | 7 +- frontend/coprs_frontend/coprs/views/misc.py | 6 + .../test_views/test_coprs_ns/test_coprs_general.py | 2 +- frontend/requirements.txt | 3 + 18 files changed, 714 insertions(+), 14 deletions(-)
diff --git a/frontend/copr-frontend.spec b/frontend/copr-frontend.spec index e01748b..def204d 100644 --- a/frontend/copr-frontend.spec +++ b/frontend/copr-frontend.spec @@ -53,6 +53,8 @@ Requires: python-whoosh >= 2.5.3 Requires: pytz Requires: python-six Requires: python-netaddr +Requires: python-flask-restful +Requires: python-marshmallow # for tests: Requires: pytest Requires: python-flexmock @@ -85,6 +87,8 @@ BuildRequires: python-mock BuildRequires: python-decorator BuildRequires: python-markdown BuildRequires: pytz +BuildRequires: python-flask-restful +BuildRequires: python-marshmallow
%description COPR is lightweight build system. It allows you to create new project in WebUI, diff --git a/frontend/coprs_frontend/coprs/__init__.py b/frontend/coprs_frontend/coprs/__init__.py index fa02421..dd0eb05 100644 --- a/frontend/coprs_frontend/coprs/__init__.py +++ b/frontend/coprs_frontend/coprs/__init__.py @@ -70,3 +70,8 @@ app.register_blueprint(stats_receiver.stats_rcv_ns) app.register_blueprint(tmp_ns.tmp_ns)
app.add_url_rule("/", "coprs_ns.coprs_show", coprs_general.coprs_show) + +from rest_api import rest_api_bp, register_api_error_handler, URL_PREFIX +register_api_error_handler(app) +app.register_blueprint(rest_api_bp, url_prefix=URL_PREFIX) +# register_api(app, db) diff --git a/frontend/coprs_frontend/coprs/logic/complex_logic.py b/frontend/coprs_frontend/coprs/logic/complex_logic.py new file mode 100644 index 0000000..3f2bd53 --- /dev/null +++ b/frontend/coprs_frontend/coprs/logic/complex_logic.py @@ -0,0 +1,27 @@ +# coding: utf-8 + +import flask +from .builds_logic import BuildsLogic +from .coprs_logic import CoprsLogic + + +class ComplexLogic(object): + """ + Used for manipulation which affects multiply models + """ + + @classmethod + def delete_copr(cls, copr): + """ + Delete copr and all its builds. + + :param copr: + :raises ActionInProgressException: + :raises InsufficientRightsException: + """ + builds_query = BuildsLogic.get_multiple_by_copr(copr=copr) + + for build in builds_query: + BuildsLogic.delete_build(flask.g.user, build) + + CoprsLogic.delete_unsafe(flask.g.user, copr) diff --git a/frontend/coprs_frontend/coprs/logic/coprs_logic.py b/frontend/coprs_frontend/coprs/logic/coprs_logic.py index 9ea19dc..4e53e37 100644 --- a/frontend/coprs_frontend/coprs/logic/coprs_logic.py +++ b/frontend/coprs_frontend/coprs/logic/coprs_logic.py @@ -130,13 +130,13 @@ class CoprsLogic(object): return query
@classmethod - def add(cls, user, name, repos, selected_chroots, description, - instructions, check_for_duplicates=False, **kwargs): + def add(cls, user, name, repos, selected_chroots, description=None, + instructions=None, check_for_duplicates=False, **kwargs): copr = models.Copr(name=name, repos=repos, owner_id=user.id, - description=description, - instructions=instructions, + description=description or u"", + instructions=instructions or u"", created_on=int(time.time()), **kwargs)
@@ -188,7 +188,10 @@ class CoprsLogic(object): db.session.add(copr)
@classmethod - def delete(cls, user, copr, check_for_duplicates=True): + def delete_unsafe(cls, user, copr): + """ + Deletes copr without termination of ongoing builds. + """ cls.raise_if_cant_delete(user, copr) # TODO: do we want to dump the information somewhere, so that we can # search it in future? @@ -339,7 +342,9 @@ listen(models.Copr.auto_createrepo, 'set', on_auto_createrepo_change,
class CoprChrootsLogic(object): @classmethod + def mock_chroots_from_names(cls, names): + db_chroots = models.MockChroot.query.all() mock_chroots = [] for ch in db_chroots: diff --git a/frontend/coprs_frontend/coprs/models.py b/frontend/coprs_frontend/coprs/models.py index ca4d1af..15e77b7 100644 --- a/frontend/coprs_frontend/coprs/models.py +++ b/frontend/coprs_frontend/coprs/models.py @@ -1,3 +1,4 @@ +import copy import datetime import json import os @@ -170,6 +171,19 @@ class Copr(db.Model, helpers.Serializer): }
@property + def yum_repos(self): + return {} + # todo: add config option to set backen results base url + # release_tmpl = "{chroot.os_release}-{chroot.os_version}-{chroot.arch}" + # result = {} + # for chroot in repo.active_chroots: + # release = release_tmpl.format(chroot=chroot) + # url = fix_protocol_for_backend(urlparse.urljoin( + # app.config["BACKEND_RESULTS_BASE_URL"], release + '/')) + # result[release] = url + # return result + + @property def repos_list(self): """ Return repos of this copr as a list of strings @@ -244,6 +258,13 @@ class Copr(db.Model, helpers.Serializer): def full_name(self): return "{}/{}".format(self.owner.username, self.name)
+ def to_dict(self, private=False, show_builds=True, show_chroots=True): + result = {} + for key in ["id", "name", "description", "instructions"]: + result[key] = str(copy.copy(getattr(self, key))) + result["owner"] = self.owner.name + return result +
class CoprPermission(db.Model, helpers.Serializer):
@@ -355,6 +376,13 @@ class Build(db.Model, helpers.Serializer): return self.build_chroots[0].git_hash is None
@property + def repos_list(self): + if self.repos is None: + return list() + else: + return self.repos.split() + + @property def result_dir_name(self): return "{:08d}-{}".format(self.id, self.package.name)
@@ -512,6 +540,15 @@ class Build(db.Model, helpers.Serializer): else: return src_rpm_name
+ def to_dict(self, options=None): + result = super(Build, self).to_dict(options) + result["src_pkg"] = result["pkgs"] + del result["pkgs"] + del result["copr_id"] + + result["state"] = self.state + return result +
class MockChroot(db.Model, helpers.Serializer):
@@ -556,6 +593,12 @@ class MockChroot(db.Model, helpers.Serializer): """ return "{0} {1}".format(self.os_release, self.os_version)
+ @property + def serializable_attributes(self): + attr_list = super(MockChroot, self).serializable_attributes + attr_list.extend(["name", "os"]) + return attr_list +
class CoprChroot(db.Model, helpers.Serializer):
diff --git a/frontend/coprs_frontend/coprs/rest_api/__init__.py b/frontend/coprs_frontend/coprs/rest_api/__init__.py new file mode 100644 index 0000000..309ba44 --- /dev/null +++ b/frontend/coprs_frontend/coprs/rest_api/__init__.py @@ -0,0 +1,101 @@ +# coding: utf-8 +from flask import Response, url_for, Blueprint + +from flask_restful import Resource, Api +from flask_restful_swagger import swagger + +from coprs.rest_api.exceptions import ApiError +from coprs.rest_api.resources.build import BuildListR, BuildR +from coprs.rest_api.resources.chroot import ChrootListR, ChrootR +from coprs.rest_api.resources.copr import CoprListR, CoprR +from coprs.rest_api.util import bp_url_for + + +URL_PREFIX = "/api_2.0" + + +class RootR(Resource): + @swagger.operation( + notes='List main API endpoints', + nickname='get', + ) + def get(self): + return { + "links": { + "self": bp_url_for(RootR.endpoint), + "coprs": bp_url_for(CoprListR.endpoint), + "chroots": bp_url_for(ChrootListR.endpoint), + "builds": bp_url_for(BuildListR.endpoint), + } + } + + +class MyApi(Api): + # flask-restfull error handling quite buggy right now + def error_router(self, original_handler, e): + return original_handler(e) + # def handle_error(self, e): + # + # if isinstance(e, sqlalchemy.orm.exc.NoResultFound): + # return self.make_response(str(e), 404) + # + # + # super(MyApi, self).handle_error(e) + + +# def register_api(app, db): + + +rest_api_bp = Blueprint("rest_api_bp", + __name__, + # app.import_name +) + +api = MyApi( +# api = Api( + # app, + rest_api_bp, + # prefix=URL_PREFIX, + catch_all_404s=True, + +) + +################################### +# todo: maybe add later +# Wrap the Api with swagger.docs. It is a thin wrapper around the Api class that adds some swagger smarts +# api = swagger.docs( +# api, +# # apiVersion='0.1', +# # basePath=URL_PREFIX, +# # resourcePath=URL_PREFIX, +# # api_spec_url='{}/spec'.format(URL_PREFIX) +# # api_spec_url='/spec', +# # api_spec_url='{}/spec'.format(URL_PREFIX) +# api_spec_url='/spec' +# ) +################################### + +api.add_resource(RootR, "/") +api.add_resource(CoprListR, "/coprs") +api.add_resource(CoprR, "/coprs/<owner>/<project>") + +api.add_resource(ChrootListR, "/chroots") +api.add_resource(ChrootR, "/chroots/<name>") + +api.add_resource(BuildListR, "/builds") +api.add_resource(BuildR, "/builds/int:build_id") + +# app.register_blueprint(rest_api_bp, url_prefix=URL_PREFIX) + + +# TODO: try: https://github.com/sloria/flask-marshmallow +def register_api_error_handler(app): + @app.errorhandler(ApiError) + def handle_api_error(error): + response = Response( + response="{}\n".format(error.data), + status=error.code, + mimetype="text/plain", + headers=error.headers, + ) + return response diff --git a/frontend/coprs_frontend/coprs/rest_api/exceptions.py b/frontend/coprs_frontend/coprs/rest_api/exceptions.py new file mode 100644 index 0000000..0fb14da --- /dev/null +++ b/frontend/coprs_frontend/coprs/rest_api/exceptions.py @@ -0,0 +1,45 @@ +# coding: utf-8 +import six + + +class ApiError(Exception): + def __init__(self, code, data, **kwargs): + super(ApiError, self).__init__(**kwargs) + + self.code = code + self.data = data + + self.headers = kwargs.get("headers", {}) + + def __str__(self): + return str(self.data) + + if six.PY2: + def __unicode__(self): + return unicode(self.data) + + +class AuthFailed(ApiError): + def __init__(self, data, **kwargs): + + super(AuthFailed, self).__init__( + 401, data, + # headers={"Authorization": "Basic"}, + **kwargs) + + self.headers["Authorization"] = "Basic" + + +class ObjectNotFoundError(ApiError): + def __init__(self, data, **kwargs): + super(ObjectNotFoundError, self).__init__(404, data, **kwargs) + + +class ObjectAlreadyExists(ApiError): + def __init__(self, data, **kwargs): + super(ObjectAlreadyExists, self).__init__(409, data, **kwargs) + + +class MalformedRequest(ApiError): + def __init__(self, data=None, **kwargs): + super(MalformedRequest, self).__init__(400, data, **kwargs) diff --git a/builder_image/src/__init__.py b/frontend/coprs_frontend/coprs/rest_api/resources/__init__.py similarity index 100% copy from builder_image/src/__init__.py copy to frontend/coprs_frontend/coprs/rest_api/resources/__init__.py diff --git a/frontend/coprs_frontend/coprs/rest_api/resources/build.py b/frontend/coprs_frontend/coprs/rest_api/resources/build.py new file mode 100644 index 0000000..1db3bf4 --- /dev/null +++ b/frontend/coprs_frontend/coprs/rest_api/resources/build.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +import flask + +# from flask_restful_swagger import swagger + +from coprs.logic.coprs_logic import CoprsLogic +from coprs.logic.builds_logic import BuildsLogic + +from coprs.rest_api.schemas import BuildSchema + +from coprs.rest_api.util import get_one_safe, bp_url_for + +from flask_restful import Resource, reqparse + + +class BuildListR(Resource): + + def get(self): + + parser = reqparse.RequestParser() + + parser.add_argument('owner', type=str,) + parser.add_argument('project', type=str) + + parser.add_argument('limit', type=int) + parser.add_argument('offset', type=int) + + req_args = parser.parse_args() + + if "owner" and "project" in req_args: + query = BuildsLogic.get_multiple_by_name( + req_args["owner"], req_args["project"]) + else: + query = BuildsLogic.get_multiple() + + if "limit" in req_args: + limit = req_args["limit"] + else: + limit = 100 + + query = query.limit(limit) + + if "offset" in req_args: + query = query.offset(req_args["offset"]) + + builds = query.all() + return { + + "builds": [ + { + "build": BuildSchema().dump(build)[0], + "link": bp_url_for(BuildR.endpoint, build_id=build.id), + } for build in builds + ], + "links": { + "self": bp_url_for(BuildListR.endpoint, **req_args), + }, + } + + +# @swagger.model +# class BuildItem(object): +# def __init__(self, build_id): +# pass + + +class BuildR(Resource): + + def get(self, build_id): + """ + Get single build by id + """ + build = get_one_safe(BuildsLogic.get(build_id), + "Not found build with id: {}".format(build_id)) + return { + "build": BuildSchema().dump(build)[0], + "links": { + "self": bp_url_for(BuildR.endpoint, build_id=build_id), + # TODO: can't do this due to circular imports + # "parent_copr": url_for(CoprR.endpoint, + # owner=build.copr.owner.name, + # project=build.copr.name), + } + } + + + + diff --git a/frontend/coprs_frontend/coprs/rest_api/resources/chroot.py b/frontend/coprs_frontend/coprs/rest_api/resources/chroot.py new file mode 100644 index 0000000..61d6e5a --- /dev/null +++ b/frontend/coprs_frontend/coprs/rest_api/resources/chroot.py @@ -0,0 +1,54 @@ +# coding: utf-8 + +import json +import flask +from flask import url_for +from flask_restful import Resource, reqparse + +from marshmallow import Schema, fields, pprint + +from coprs.views.misc import api_login_required +from coprs.logic.coprs_logic import MockChrootsLogic, CoprChrootsLogic, CoprsLogic + +from ..util import get_one_safe, json_loads_safe, mm_deserialize, bp_url_for + + +class ChrootListR(Resource): + def get(self): + chroots = MockChrootsLogic.get_multiple(None).all() + return { + "links": { + "self": bp_url_for(ChrootListR.endpoint), + }, + "chroots": [ + { + "chroot": chroot.to_dict(), + "link": url_for(ChrootR.endpoint, name=chroot.name), + } for chroot in chroots + ] + } + + +class ChrootR(Resource): + def get(self, name): + chroot = get_one_safe(MockChrootsLogic.get_from_name(name)) + return { + "chroot": chroot.to_dict(), + "links": { + "self": bp_url_for(ChrootR.endpoint, name=chroot.name) + }, + } + + +class BuildChrootR(Resource): + def get(self, owner, project, name): + copr = get_one_safe(CoprsLogic.get(flask.g.user, owner, project), + "Copr {}/{} not found".format(owner, project)) + chroot = get_one_safe(CoprChrootsLogic.get(copr, name)) + + return { + "chroot": chroot.to_dict(), + "links": { + "self": bp_url_for(BuildChrootR.endpoint, owner=owner, project=project, name=name) + } + } diff --git a/frontend/coprs_frontend/coprs/rest_api/resources/copr.py b/frontend/coprs_frontend/coprs/rest_api/resources/copr.py new file mode 100644 index 0000000..f1f5e48 --- /dev/null +++ b/frontend/coprs_frontend/coprs/rest_api/resources/copr.py @@ -0,0 +1,180 @@ +import base64 +import datetime +import functools + +import flask +from flask_restful import Resource, reqparse +from marshmallow import pprint + +from coprs import db +from coprs.exceptions import DuplicateException +from coprs.logic.complex_logic import ComplexLogic +from coprs.logic.users_logic import UsersLogic +from coprs.logic.coprs_logic import CoprsLogic +from coprs.exceptions import ActionInProgressException, InsufficientRightsException +from .build import BuildListR +from coprs.rest_api.schemas import CoprSchema +from ..exceptions import ObjectAlreadyExists, AuthFailed +from ..util import get_one_safe, json_loads_safe, mm_deserialize, bp_url_for, render_allowed_method + + +def rest_api_auth_required(f): + @functools.wraps(f) + def decorated_function(*args, **kwargs): + token = None + apt_login = None + if "Authorization" in flask.request.headers: + base64string = flask.request.headers["Authorization"] + base64string = base64string.split()[1].strip() + userstring = base64.b64decode(base64string) + (apt_login, token) = userstring.split(":") + token_auth = False + if token and apt_login: + user = UsersLogic.get_by_api_login(apt_login).first() + if (user and user.api_token == token and + user.api_token_expiration >= datetime.date.today()): + + token_auth = True + flask.g.user = user + if not token_auth: + message = ( + "Login invalid/expired. " + "Please visit https://copr.fedoraproject.org/api " + "get or renew your API token.") + + raise AuthFailed(message) + return f(*args, **kwargs) + return decorated_function + + +class CoprListR(Resource): + + @rest_api_auth_required + def post(self): + """ + Creates new copr + """ + owner = flask.g.user + # new_copr_dict = json_loads_safe(flask.request.data, "") + + result = mm_deserialize(CoprSchema(), flask.request.data) + # todo check that chroots are available + pprint(result.data) + try: + CoprsLogic.add(user=owner, check_for_duplicates=True, **result.data) + db.session.commit() + except DuplicateException as error: + raise ObjectAlreadyExists(data=error) + + return "New copr was created", 201 + + def get(self): + """ + Get coprs collection + :return: + """ + parser = reqparse.RequestParser() + parser.add_argument('owner', dest='username', type=str) + parser.add_argument('limit', type=int) + parser.add_argument('offset', type=int) + req_args = parser.parse_args() + + kwargs = {} + for key in ["username"]: + if req_args[key]: + kwargs[key] = req_args[key] + + if "username" in kwargs: + query = CoprsLogic.get_multiple_owned_by_username(req_args["username"]) + else: + query = CoprsLogic.get_multiple(flask.g.user) + + # todo: also could be optional + query = CoprsLogic.join_builds(query) + + if req_args["offset"]: + query = query.offset(req_args["offset"]) + + # todo: add maximum allowed limit and also use as a default limit + if req_args["limit"]: + query = query.limit(req_args["limit"]) + + coprs_list = query.all() + + return { + "links": { + "self": bp_url_for(CoprListR.endpoint, **req_args) + }, + "coprs": [ + { + "copr": CoprSchema().dump(copr)[0], + "link": bp_url_for( + CoprR.endpoint, + owner=copr.owner.name, + project=copr.name + ), + } + for copr in coprs_list + ], + # TODO: show only if user provided ?help=true param + # "allowed_methods": [ + # render_allowed_method("GET", "Get list of coprs", require_auth=False, + # params=[ + # "username: filter coprs owned by user", + # "limit: show only the given number of coprs", + # "offset: skip given number of coprs", + # ]), + # render_allowed_method("POST", "Creates new copr, send dict with copr fields"), + # ] + } + + +class CoprR(Resource): + + @rest_api_auth_required + def delete(self, owner, project): + copr = get_one_safe(CoprsLogic.get(flask.g.user, owner, project), + "Copr {}/{} not found".format(owner, project)) + try: + ComplexLogic.delete_copr(copr) + except (ActionInProgressException, + InsufficientRightsException) as err: + db.session.rollback() + raise + else: + db.session.commit() + + return None, 204 + + def get(self, owner, project): + # parser = reqparse.RequestParser() + # parser.add_argument('show_builds', type=bool, default=True) + # parser.add_argument('show_chroots', type=bool, default=True) + # req_args = parser.parse_args() + + copr = get_one_safe(CoprsLogic.get(flask.g.user, owner, project), + "Copr {}/{} not found".format(owner, project)) + return { + "copr": CoprSchema().dump(copr)[0], + "links": { + "self": bp_url_for(CoprR.endpoint, + owner=owner, + project=project), + # "chroots": + "builds": bp_url_for(BuildListR.endpoint, + owner=copr.owner.name, + project=copr.name) + }, + # "allowed_methods": [ + # render_allowed_method("GET", "Get single copr", require_auth=False), + # render_allowed_method("DELETE", "Delete current copr", require_auth=True), + # ] + } + + @rest_api_auth_required + def put(self, owner, project): + """ + Modifies project by replacment of provided fields + """ + pass + diff --git a/frontend/coprs_frontend/coprs/rest_api/schemas.py b/frontend/coprs_frontend/coprs/rest_api/schemas.py new file mode 100644 index 0000000..9c243a7 --- /dev/null +++ b/frontend/coprs_frontend/coprs/rest_api/schemas.py @@ -0,0 +1,87 @@ +# coding: utf-8 +from marshmallow import Schema, fields + + +class AllowedMethodSchema(Schema): + method = fields.Str() + doc = fields.Str() + require_auth = fields.Bool() + params = fields.List(fields.Str()) + + +class MockChroot(Schema): + pass + + +class CoprSchema(Schema): + name = fields.Str(required=True) + description = fields.Str() + instructions = fields.Str() + + auto_createrepo = fields.Bool() + build_enable_net = fields.Bool() + last_modified = fields.DateTime() + + additional_repos = fields.List(fields.Str, dump_only=True, attribute="repos_list") + # yum_repos = fields.List() + + # used only for creation + chroots_to_enable = fields.List(fields.Str, load_only=True) + + _keys_to_make_object = [ + "description", + "instructions", + "auto_createrepo" + ] + + def make_object(self, data): + """ + Create kwargs for CoprsLogic.add + """ + kwargs = dict( + name=data["name"].strip(), + repos=" ".join(data.get("repos", [])), + selected_chroots=data["chroots"], + ) + for key in self._keys_to_make_object: + if key in data: + kwargs[key] = data[key] + return kwargs + + +class BuildChrootSchema(Schema): + # used only for presentation + state = fields.Str() + started_on = fields.Int() + ended_on = fields.Int() + git_hash = fields.Str() + + +class BuildSchema(Schema): + + id = fields.Int() + pkgs = fields.Str() + build_packages = fields.Str() + pkg_version = fields.Str() + + repos_list = fields.List(fields.Str()) + repos = fields.Str() # legacy + + submitted_on = fields.Int() + started_on = fields.Int() + ended_on = fields.Int() + + results = fields.Str() + memory_reqs = fields.Int() + timeout = fields.Int() + + enable_net = fields.Bool() + + source_type = fields.Int() + source_json = fields.Str() + + # chroots = fields.List(fields.Nested(BuildChrootSchema)) + + + + diff --git a/frontend/coprs_frontend/coprs/rest_api/util.py b/frontend/coprs_frontend/coprs/rest_api/util.py new file mode 100644 index 0000000..ac9b0b6 --- /dev/null +++ b/frontend/coprs_frontend/coprs/rest_api/util.py @@ -0,0 +1,54 @@ +# coding: utf-8 +import json + +import sqlalchemy.orm.exc +from .exceptions import ObjectNotFoundError, MalformedRequest +from schemas import AllowedMethodSchema +from flask import Response, url_for, Blueprint + + +class AllowedMethod(object): + def __init__(self, method, doc, require_auth=True, params=None): + self.method = method + self.doc = doc + self.require_auth = require_auth + self.params = params or [] + + +def render_allowed_method(method, doc, require_auth=True, params=None): + return AllowedMethodSchema().dump(AllowedMethod(method, doc, require_auth, params))[0] + + +def bp_url_for(endpoint, *args, **kwargs): + """ + Prepend endpoing with dot + :param endpoint: + :param args: + :param kwargs: + :return: + """ + + return url_for(".{}".format(endpoint), *args, **kwargs) + + +def get_one_safe(query, data_on_error=None): + try: + return query.one() + except sqlalchemy.orm.exc.NoResultFound: + raise ObjectNotFoundError(data_on_error) + + +def json_loads_safe(raw, data_on_error=None): + try: + return json.loads(raw) + except ValueError: + raise MalformedRequest(data_on_error or + "Failed to deserialize json string") + + +def mm_deserialize(schema, obj_dict): + result = schema.loads(obj_dict) + if result.errors: + raise MalformedRequest(data=result.errors) + # import ipdb; ipdb.set_trace() + return result diff --git a/frontend/coprs_frontend/coprs/views/api_ns/api_general.py b/frontend/coprs_frontend/coprs/views/api_ns/api_general.py index 2070ae2..b9f220b 100644 --- a/frontend/coprs_frontend/coprs/views/api_ns/api_general.py +++ b/frontend/coprs_frontend/coprs/views/api_ns/api_general.py @@ -16,6 +16,7 @@ from coprs import forms from coprs import helpers from coprs.helpers import fix_protocol_for_backend from coprs.logic.api_logic import MonitorWrapper +from coprs.logic.complex_logic import ComplexLogic
from coprs.views.misc import login_required, api_login_required
@@ -148,11 +149,8 @@ def api_copr_delete(username, coprname): httpcode = 200
if form.validate_on_submit() and copr: - builds_query = builds_logic.BuildsLogic.get_multiple_by_copr(copr=copr) try: - for build in builds_query: - builds_logic.BuildsLogic.delete_build(flask.g.user, build) - CoprsLogic.delete(flask.g.user, copr) + ComplexLogic.delete_copr(copr) except (exceptions.ActionInProgressException, exceptions.InsufficientRightsException) as err: output = {"output": "notok", "error": err} diff --git a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py index 6a69513..8a68e43 100644 --- a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py +++ b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_general.py @@ -22,6 +22,8 @@ from coprs import models from coprs.logic.stat_logic import CounterStatLogic from coprs.rmodels import TimedStatEvents
+from coprs.logic.complex_logic import ComplexLogic + from coprs.views.misc import login_required, page_not_found
from coprs.views.coprs_ns import coprs_ns @@ -507,12 +509,9 @@ def copr_delete(username, coprname): copr = coprs_logic.CoprsLogic.get(username, coprname).first()
if form.validate_on_submit() and copr: - builds_query = builds_logic.BuildsLogic.get_multiple_by_copr(copr=copr)
try: - for build in builds_query: - builds_logic.BuildsLogic.delete_build(flask.g.user, build) - coprs_logic.CoprsLogic.delete(flask.g.user, copr) + ComplexLogic.delete_copr(copr) except (exceptions.ActionInProgressException, exceptions.InsufficientRightsException) as e:
diff --git a/frontend/coprs_frontend/coprs/views/misc.py b/frontend/coprs_frontend/coprs/views/misc.py index 38d0ab5..b0a98d7 100644 --- a/frontend/coprs_frontend/coprs/views/misc.py +++ b/frontend/coprs_frontend/coprs/views/misc.py @@ -51,6 +51,11 @@ def krb_strip_realm(fullname):
@app.before_request +def set_empty_user(): + flask.g.user = None + + +@app.before_request def lookup_current_user(): flask.g.user = username = None if "openid" in flask.session: @@ -222,6 +227,7 @@ def api_login_required(f): return decorated_function
+ def login_required(role=helpers.RoleEnum("user")): def view_wrapper(f): @functools.wraps(f) diff --git a/frontend/coprs_frontend/tests/test_views/test_coprs_ns/test_coprs_general.py b/frontend/coprs_frontend/tests/test_views/test_coprs_ns/test_coprs_general.py index da3f778..bd06cd9 100644 --- a/frontend/coprs_frontend/tests/test_views/test_coprs_ns/test_coprs_general.py +++ b/frontend/coprs_frontend/tests/test_views/test_coprs_ns/test_coprs_general.py @@ -39,7 +39,7 @@ class TestMonitor(CoprsTestCase): res = self.tc.get("/coprs/{}/{}/monitor/".format(self.u1.name, copr_name)) assert res.status_code == 200
- self.db.session.add(CoprsLogic.delete(self.u1, tmp_copr)) + self.db.session.add(CoprsLogic.delete_unsafe(self.u1, tmp_copr)) self.db.session.commit()
res = self.tc.get("/coprs/{}/{}/monitor/".format(self.u1.name, copr_name)) diff --git a/frontend/requirements.txt b/frontend/requirements.txt index 82354b3..8c3653f 100644 --- a/frontend/requirements.txt +++ b/frontend/requirements.txt @@ -17,3 +17,6 @@ decorator python-dateutil netaddr alembic +flask-restful +# flask-restful-swagger # -- nice to have, but probably later +marshmallow