diff --git a/.gitignore b/.gitignore index 8f886cb..97f5dce 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,4 @@ XDG_CACHE_HOME # End of https://www.gitignore.io/api/python,visualstudiocode +.idea/ \ No newline at end of file diff --git a/controllers/controllers.py b/controllers/controllers.py index c0d837e..0a88e18 100644 --- a/controllers/controllers.py +++ b/controllers/controllers.py @@ -1,37 +1,19 @@ # -*- coding: utf-8 -*- +import base64 import json -import math import logging +import math import requests +import werkzeug -from odoo import http, _, exceptions +from odoo import http, _ from odoo.http import request - +from .exceptions import ModelException, ObjectException, QueryFormatError from .serializers import Serializer -from .exceptions import QueryFormatError - _logger = logging.getLogger(__name__) -def error_response(error, msg): - return { - "jsonrpc": "2.0", - "id": None, - "error": { - "code": 200, - "message": msg, - "data": { - "name": str(error), - "debug": "", - "message": msg, - "arguments": list(error.args), - "exception_type": type(error).__name__ - } - } - } - - class OdooAPI(http.Controller): @http.route( '/auth/', @@ -39,196 +21,139 @@ class OdooAPI(http.Controller): def authenticate(self, *args, **post): try: login = post["login"] - except KeyError: - raise exceptions.AccessDenied(message='`login` is required.') - - try: password = post["password"] - except KeyError: - raise exceptions.AccessDenied(message='`password` is required.') - - try: db = post["db"] - except KeyError: - raise exceptions.AccessDenied(message='`db` is required.') - - url_root = request.httprequest.url_root - AUTH_URL = f"{url_root}web/session/authenticate/" - - headers = {'Content-type': 'application/json'} + except KeyError as e: + msg = f"'{str(e)}' is required in params." + raise werkzeug.exceptions.BadRequest(msg) - data = { - "jsonrpc": "2.0", - "params": { - "login": login, - "password": password, - "db": db + try: + url_root = request.httprequest.url_root + auth_url = f"{url_root}web/session/authenticate/" + + data = { + "jsonrpc": "2.0", + "params": { + "login": login, + "password": password, + "db": db + } } - } - res = requests.post( - AUTH_URL, - data=json.dumps(data), - headers=headers - ) + res = requests.post( + auth_url, + data=json.dumps(data), + headers={'Content-type': 'application/json'} + ) - try: session_id = res.cookies["session_id"] user = json.loads(res.text) user["result"]["session_id"] = session_id - except Exception: - return "Invalid credentials." - return user["result"] + return user["result"] + except Exception as e: + raise werkzeug.exceptions.Unauthorized("Invalid credentials.") @http.route( '/object//', type='json', auth='user', methods=["POST"], csrf=False) def call_model_function(self, model, function, **post): - args = [] - kwargs = {} - if "args" in post: - args = post["args"] - if "kwargs" in post: - kwargs = post["kwargs"] - model = request.env[model] - result = getattr(model, function)(*args, **kwargs) - return result + try: + args = post.get("args", []) + kwargs = post.get("kwargs", {}) + model = self._get_model(model) + result = getattr(model, function)(*args, **kwargs) + return result + except (ModelException, ObjectException) as e: + raise werkzeug.exceptions.NotFound(str(e)) @http.route( '/object///', type='json', auth='user', methods=["POST"], csrf=False) def call_obj_function(self, model, rec_id, function, **post): - args = [] - kwargs = {} - if "args" in post: - args = post["args"] - if "kwargs" in post: - kwargs = post["kwargs"] - obj = request.env[model].browse(rec_id).ensure_one() - result = getattr(obj, function)(*args, **kwargs) - return result + try: + args = post.get("args", []) + kwargs = post.get("kwargs", {}) + obj = self._get_obj(model, rec_id) + result = getattr(obj, function)(*args, **kwargs) + return result + except (ModelException, ObjectException) as e: + raise werkzeug.exceptions.NotFound(str(e)) + + @http.route( + '/report/', + type='json', auth='user', methods=["POST"], csrf=False) + def call_render_qweb_pdf(self, rec_id, **post): + try: + obj = self._get_obj('ir.actions.report', rec_id) + res_ids = json.loads(post.get('res_ids')) + data = json.loads(post.get('data', '{}')) + content, _ = getattr(obj, 'render_qweb_pdf')(res_ids, data) + return base64.b64encode(content) + except (ModelException, ObjectException) as e: + raise werkzeug.exceptions.NotFound(str(e)) @http.route( '/api/', type='http', auth='user', methods=['GET'], csrf=False) def get_model_data(self, model, **params): try: - records = request.env[model].search([]) - except KeyError as e: - msg = "The model `%s` does not exist." % model - res = error_response(e, msg) - return http.Response( - json.dumps(res), - status=200, - mimetype='application/json' - ) - - if "query" in params: - query = params["query"] - else: - query = "{*}" - - if "order" in params: - orders = json.loads(params["order"]) - else: - orders = "" - - if "filter" in params: - filters = json.loads(params["filter"]) - records = request.env[model].search(filters, order=orders) - - prev_page = None - next_page = None - total_page_number = 1 - current_page = 1 - - if "page_size" in params: - page_size = int(params["page_size"]) - count = len(records) - total_page_number = math.ceil(count/page_size) - - if "page" in params: - current_page = int(params["page"]) + model = self._get_model(model) + query = params.get("query", "{*}") + filters = json.loads(params["filters"]) if "filters" in params else [] + order = params.get("order", "id") + limit = int(params.get("limit", 500)) + + records = model.search(filters, order=order, limit=limit) + + if "page_size" in params: + page_size = int(params["page_size"]) + count = len(records) + total_page_number = math.ceil(count / page_size) + + current_page = int(params.get("page", 1)) + start = page_size * (current_page - 1) + stop = current_page * page_size + records = records[start:stop] + next_page = current_page + 1 \ + if 0 < current_page + 1 <= total_page_number else None + prev_page = current_page - 1 \ + if 0 < current_page - 1 <= total_page_number else None else: - current_page = 1 # Default page Number - start = page_size*(current_page-1) - stop = current_page*page_size - records = records[start:stop] - next_page = current_page+1 \ - if 0 < current_page + 1 <= total_page_number \ - else None - prev_page = current_page-1 \ - if 0 < current_page - 1 <= total_page_number \ - else None - - if "limit" in params: - limit = int(params["limit"]) - records = records[0:limit] + prev_page = next_page = None + total_page_number = current_page = 1 - try: - serializer = Serializer(records, query, many=True) - data = serializer.data - except (SyntaxError, QueryFormatError) as e: - res = error_response(e, e.msg) - return http.Response( - json.dumps(res), - status=200, - mimetype='application/json' - ) + try: + serializer = Serializer(records, query, many=True) + data = serializer.data + except (SyntaxError, QueryFormatError) as e: + raise werkzeug.exceptions.NotFound(str(e)) - res = { - "count": len(records), - "prev": prev_page, - "current": current_page, - "next": next_page, - "total_pages": total_page_number, - "result": data - } - return http.Response( - json.dumps(res), - status=200, - mimetype='application/json' - ) + res = { + "count": len(records), + "prev": prev_page, + "current": current_page, + "next": next_page, + "total_pages": total_page_number, + "result": data + } + return self._response(res) + + except (ModelException, ObjectException) as e: + raise werkzeug.exceptions.NotFound(str(e)) @http.route( '/api//', type='http', auth='user', methods=['GET'], csrf=False) def get_model_rec(self, model, rec_id, **params): try: - records = request.env[model].search([]) - except KeyError as e: - msg = "The model `%s` does not exist." % model - res = error_response(e, msg) - return http.Response( - json.dumps(res), - status=200, - mimetype='application/json' - ) - - if "query" in params: - query = params["query"] - else: - query = "{*}" - - # TODO: Handle the error raised by `ensure_one` - record = records.browse(rec_id).ensure_one() - - try: - serializer = Serializer(record, query) - data = serializer.data + obj = self._get_obj(model, rec_id) + query = params.get("query", "{*}") + serializer = Serializer(obj, query) + return self._response(serializer.data) except (SyntaxError, QueryFormatError) as e: - res = error_response(e, e.msg) - return http.Response( - json.dumps(res), - status=200, - mimetype='application/json' - ) - - return http.Response( - json.dumps(data), - status=200, - mimetype='application/json' - ) + raise werkzeug.exceptions.BadRequest(str(e)) + except (ModelException, ObjectException) as e: + raise werkzeug.exceptions.NotFound(str(e)) @http.route( '/api//', @@ -236,15 +161,15 @@ def get_model_rec(self, model, rec_id, **params): def post_model_data(self, model, **post): try: data = post['data'] - except KeyError: + except KeyError as e: msg = "`data` parameter is not found on POST request body" - raise exceptions.ValidationError(msg) + raise werkzeug.exceptions.BadRequest(msg) try: model_to_post = request.env[model] - except KeyError: - msg = "The model `%s` does not exist." % model - raise exceptions.ValidationError(msg) + except KeyError as e: + msg = f"The model `{model}` does not exist." + raise werkzeug.exceptions.BadRequest(msg) # TODO: Handle data validation @@ -262,20 +187,19 @@ def post_model_data(self, model, **post): def put_model_record(self, model, rec_id, **post): try: data = post['data'] - except KeyError: + except KeyError as e: msg = "`data` parameter is not found on PUT request body" - raise exceptions.ValidationError(msg) + raise werkzeug.exceptions.BadRequest(msg) try: model_to_put = request.env[model] - except KeyError: - msg = "The model `%s` does not exist." % model - raise exceptions.ValidationError(msg) + except KeyError as e: + msg = f"The model `{model}` does not exist." + raise werkzeug.exceptions.BadRequest(msg) if "context" in post: # TODO: Handle error raised by `ensure_one` - rec = model_to_put.with_context(**post["context"])\ - .browse(rec_id).ensure_one() + rec = model_to_put.with_context(**post["context"]).browse(rec_id).ensure_one() else: rec = model_to_put.browse(rec_id).ensure_one() @@ -324,22 +248,21 @@ def put_model_record(self, model, rec_id, **post): def put_model_records(self, model, **post): try: data = post['data'] - except KeyError: + except KeyError as e: msg = "`data` parameter is not found on PUT request body" - raise exceptions.ValidationError(msg) + raise werkzeug.exceptions.BadRequest(msg) try: model_to_put = request.env[model] - except KeyError: - msg = "The model `%s` does not exist." % model - raise exceptions.ValidationError(msg) + except KeyError as e: + msg = f"The model `{model}` does not exist." + raise werkzeug.exceptions.BadRequest(msg) # TODO: Handle errors on filter filters = post["filter"] if "context" in post: - recs = model_to_put.with_context(**post["context"])\ - .search(filters) + recs = model_to_put.with_context(**post["context"]).search(filters) else: recs = model_to_put.search(filters) @@ -389,17 +312,12 @@ def put_model_records(self, model, **post): @http.route( '/api///', type='http', auth="user", methods=['DELETE'], csrf=False) - def delete_model_record(self, model, rec_id, **post): + def delete_model_record(self, model, rec_id, **post): try: model_to_del_rec = request.env[model] except KeyError as e: - msg = "The model `%s` does not exist." % model - res = error_response(e, msg) - return http.Response( - json.dumps(res), - status=200, - mimetype='application/json' - ) + msg = f"The model `{model}` does not exist." + raise werkzeug.exceptions.NotFound(msg) # TODO: Handle error raised by `ensure_one` rec = model_to_del_rec.browse(rec_id).ensure_one() @@ -409,38 +327,23 @@ def delete_model_record(self, model, rec_id, **post): res = { "result": is_deleted } - return http.Response( - json.dumps(res), - status=200, - mimetype='application/json' - ) + return self.response(res) except Exception as e: - res = error_response(e, str(e)) - return http.Response( - json.dumps(res), - status=200, - mimetype='application/json' - ) + return self._error_response(e, str(e), 500) # This is for bulk deletion @http.route( '/api//', type='http', auth="user", methods=['DELETE'], csrf=False) def delete_model_records(self, model, **post): - filters = json.loads(post["filter"]) - try: model_to_del_rec = request.env[model] except KeyError as e: - msg = "The model `%s` does not exist." % model - res = error_response(e, msg) - return http.Response( - json.dumps(res), - status=200, - mimetype='application/json' - ) + msg = f"The model `{model}` does not exist." + raise werkzeug.exceptions.NotFound(str(e)) # TODO: Handle error raised by `filters` + filters = json.loads(post["filter"]) recs = model_to_del_rec.search(filters) try: @@ -448,39 +351,49 @@ def delete_model_records(self, model, **post): res = { "result": is_deleted } - return http.Response( - json.dumps(res), - status=200, - mimetype='application/json' - ) + return self.response(res) except Exception as e: - res = error_response(e, str(e)) - return http.Response( - json.dumps(res), - status=200, - mimetype='application/json' - ) + return self._error_response(e, str(e), status=500) @http.route( '/api///', type='http', auth="user", methods=['GET'], csrf=False) - def get_binary_record(self, model, rec_id, field, **post): + def get_binary_record(self, model, rec_id, field, **post): try: request.env[model] except KeyError as e: - msg = "The model `%s` does not exist." % model - res = error_response(e, msg) - return http.Response( - json.dumps(res), - status=200, - mimetype='application/json' - ) + msg = f"The model `{model}` does not exist." + raise werkzeug.exceptions.NotFound(msg) - rec = request.env[model].browse(rec_id).ensure_one() - if rec.exists(): - src = getattr(rec, field).decode("utf-8") - else: - src = False + try: + rec = request.env[model].browse(rec_id).ensure_one() + src = getattr(rec, field).decode("utf-8") if rec.exists() else False + return http.Response(src) + except Exception as e: + raise werkzeug.exceptions.InternalServerError(str(e)) + + @staticmethod + def _get_model(model): + try: + return request.env[model] + except KeyError as e: + msg = f"The model '{model}' does not exist." + raise ModelException(msg) + + def _get_obj(self, model, id): + try: + return self._get_model(model).browse(id).ensure_one() + except AttributeError as e: + msg = f"The object '{id}' of '{model}' does not exist." + raise ObjectException(msg, id) + except ValueError as e: + msg = f"The object '{id}' of '{model}' is not single." + raise ObjectException(msg, id) + + @staticmethod + def _response(res: dict, status=200): return http.Response( - src + json.dumps(res), + status=status, + mimetype='application/json' ) diff --git a/controllers/exceptions.py b/controllers/exceptions.py index d810742..3b5a553 100644 --- a/controllers/exceptions.py +++ b/controllers/exceptions.py @@ -1,2 +1,10 @@ +class ModelException(Exception): + """Undefined model""" + + +class ObjectException(Exception): + """Undefined object""" + + class QueryFormatError(Exception): - """Invalid Query Format.""" \ No newline at end of file + """Invalid Query Format.""" diff --git a/controllers/serializers.py b/controllers/serializers.py index 508ec0b..9ca0c12 100644 --- a/controllers/serializers.py +++ b/controllers/serializers.py @@ -42,7 +42,10 @@ def build_flat_field(cls, rec, field_name): if field_name not in all_fields: msg = "'%s' field is not found" % field_name raise LookupError(msg) - field_type = rec.fields_get(field_name).get(field_name).get('type') + field = rec.fields_get(field_name).get(field_name) + if not field: + return {field_name: None} + field_type = field.get('type') if field_type in ['one2many', 'many2many']: return { field_name: [record.id for record in rec[field_name]] @@ -76,8 +79,8 @@ def build_nested_field(cls, rec, field_name, nested_parsed_query): if field_type in ['one2many', 'many2many']: return { field_name: [ - cls.serialize(record, nested_parsed_query) - for record + cls.serialize(record, nested_parsed_query) + for record in rec[field_name] ] } @@ -93,7 +96,7 @@ def build_nested_field(cls, rec, field_name, nested_parsed_query): @classmethod def serialize(cls, rec, parsed_query): data = {} - + # NOTE: self.parsed_restql_query["include"] not being empty # is not a guarantee that the exclude operator(-) has not been # used because the same self.parsed_restql_query["include"] @@ -106,17 +109,17 @@ def serialize(cls, rec, parsed_query): continue for nested_field, nested_parsed_query in field.items(): built_nested_field = cls.build_nested_field( - rec, - nested_field, + rec, + nested_field, nested_parsed_query ) data.update(built_nested_field) - - flat_fields= set(all_fields).symmetric_difference(set(parsed_query['exclude'])) + + flat_fields = set(all_fields).symmetric_difference(set(parsed_query['exclude'])) for field in flat_fields: flat_field = cls.build_flat_field(rec, field) data.update(flat_field) - + elif parsed_query["include"]: # Here we are sure that self.parsed_restql_query["exclude"] # is empty which means the exclude operator(-) is not used, @@ -126,7 +129,7 @@ def serialize(cls, rec, parsed_query): if "*" in parsed_query['include']: # Include all fields parsed_query['include'] = filter( - lambda item: item != "*", + lambda item: item != "*", parsed_query['include'] ) fields = chain(parsed_query['include'], all_fields) @@ -136,8 +139,8 @@ def serialize(cls, rec, parsed_query): if isinstance(field, dict): for nested_field, nested_parsed_query in field.items(): built_nested_field = cls.build_nested_field( - rec, - nested_field, + rec, + nested_field, nested_parsed_query ) data.update(built_nested_field) @@ -148,4 +151,4 @@ def serialize(cls, rec, parsed_query): # The query is empty i.e query={} # return nothing return {} - return data \ No newline at end of file + return data