[ADD] new openapi modules

This commit is contained in:
erp committed 2021-01-03 22:07:15 -08:00
1 parent a7916efebd
commit bd89cf9097
54 files changed
+5094

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
from . import openapi_log
from . import openapi_namespace
from . import ir_model
from . import openapi_access
from . import res_users
from . import ir_exports
+33
View File
@@ -0,0 +1,33 @@
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo import _, api, exceptions, models
class IrExports(models.Model):
_inherit = "ir.exports"
@api.constrains("resource", "export_fields")
def _check_fields(self):
# this exports record used in openapi.access
if not self.env["openapi.access"].search_count(
["|", ("read_one_id", "=", self.id), ("read_many_id", "=", self.id)]
):
return True
fields = self.export_fields.mapped("name")
for field in fields:
field_count = fields.count(field)
if field_count > 1:
self.export_fields.search(
[("name", "=", field)], limit=field_count - 1
).unlink()
fields.sort()
for i in range(len(fields) - 1):
if fields[i + 1].startswith(fields[i]) and "/" in fields[i + 1].replace(
fields[i], ""
):
raise exceptions.ValidationError(
_('You must delete the "%s" field or "%s" field')
% (fields[i], fields[i + 1])
)
+18
View File
@@ -0,0 +1,18 @@
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo import fields, models
class IrModel(models.Model):
_inherit = "ir.model"
api_access_ids = fields.One2many("openapi.access", "model_id", "Access via API")
api_accesses_count = fields.Integer(
compute="_compute_related_accesses_count",
string="Related openapi accesses count",
store=False,
)
def _compute_related_accesses_count(self):
for record in self:
record.api_accesses_count = len(record.api_access_ids)
+531
View File
@@ -0,0 +1,531 @@
# Copyright 2018-2019 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2018 Rafis Bikbov <https://it-projects.info/team/bikbov>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import collections
import inspect
import json
import types
import urllib.parse as urlparse
from inspect import getmro, isclass
from odoo import _, api, exceptions, fields, models
from odoo.addons.base_api.lib.pinguin import transform_strfields_to_dict
from ..controllers import pinguin
PARAM_ID = {
"name": "id",
"in": "path",
"description": "Record ID",
"required": True,
"type": "integer",
"format": "int64",
}
class Access(models.Model):
_name = "openapi.access"
_description = "Access via API "
active = fields.Boolean("Active", default=True)
namespace_id = fields.Many2one("openapi.namespace", "Integration", required=True)
model_id = fields.Many2one("ir.model", "Model", required=True, ondelete='cascade')
model = fields.Char("Model Name", related="model_id.model")
api_create = fields.Boolean("Create via API", default=False)
api_read = fields.Boolean("Read via API", default=False)
api_update = fields.Boolean("Update via API", default=False)
api_delete = fields.Boolean("Delete via API", default=False)
# Options for Public methods:
# * all forbidden
# * all allowed
# * some are allowed
api_public_methods = fields.Boolean("Call Public methods via API", default=False,)
public_methods = fields.Text(
"Restric Public methods",
help="Allowed public methods besides basic ones.\n"
"Public methods are ones that don't start with underscore).\n"
"Format: one method per line.\n"
"When empty -- all public methods are allowed",
)
# Options for Private methods
# * all forbidden
# * some are allowed
private_methods = fields.Text(
"Allow Private methods",
help="Allowed private methods. "
"Private methods are ones that start with underscore. "
"Format: one method per line. "
"When empty -- private methods are not allowed",
)
read_one_id = fields.Many2one(
"ir.exports",
"Read One Fields",
help="Fields to return on reading one record, on creating a record",
domain="[('resource', '=', model)]",
)
read_many_id = fields.Many2one(
"ir.exports",
"Read Many Fields",
help="Fields to return on reading via non one-record endpoint",
domain="[('resource', '=', model)]",
)
create_context_ids = fields.Many2many(
"openapi.access.create.context",
string="Creation Context Presets",
help="Can be used to pass default values or custom context",
domain="[('model_id', '=', model_id)]",
)
_sql_constraints = [
(
"namespace_model_uniq",
"unique (namespace_id, model_id)",
"There is already a record for this Model",
)
]
@api.model
def _get_method_list(self):
return {
m[0] for m in getmembers(self.env[self.model], predicate=inspect.ismethod)
}
@api.constrains("public_methods")
def _check_public_methods(self):
for access in self:
if not access.public_methods:
continue
for line in access.public_methods.split("\n"):
if not line:
continue
if line.startswith("_"):
raise exceptions.ValidationError(
_(
'Private method (starting with "_" listed in public methods whitelist)'
)
)
if line not in self._get_method_list():
raise exceptions.ValidationError(
_("Method %r is not part of the model's method list:\n %r")
% (line, self._get_method_list())
)
@api.constrains("private_methods")
def _check_private_methods(self):
for access in self:
if not access.private_methods:
continue
for line in access.private_methods.split("\n"):
if not line:
continue
if not line.startswith("_"):
raise exceptions.ValidationError(
_(
'Public method (not starting with "_" listed in private methods whitelist'
)
)
if line not in self._get_method_list():
raise exceptions.ValidationError(
_("Method %r is not part of the model's method list:\n %r")
% (line, self._get_method_list())
)
@api.constrains("api_create", "api_read", "api_update", "api_delete")
def _check_methods(self):
for record in self:
methods = [
record.api_create,
record.api_read,
record.api_update,
record.api_delete,
record.api_public_methods,
]
methods += (record.public_methods or "").split("\n")
methods += (record.private_methods or "").split("\n")
if all(not m for m in methods):
raise exceptions.ValidationError(
_('You must select at least one API method for "%s" model.')
% record.model
)
def name_get(self):
return [
(record.id, "{}/{}".format(record.namespace_id.name, record.model))
for record in self
]
def get_OAS_paths_part(self):
model_name = self.model
read_many_path = "/%s" % model_name
read_one_path = "%s/{id}" % read_many_path
patch_one_path = read_one_path + "/call/{method_name}"
patch_model_path = read_many_path + "/call/{method_name}"
patch_many_path = read_many_path + "/call/{method_name}/{ids}"
read_many_definition_ref = "#/definitions/%s" % pinguin.get_definition_name(
self.model, "", "read_many"
)
read_one_definition_ref = "#/definitions/%s" % pinguin.get_definition_name(
self.model, "", "read_one"
)
patch_definition_ref = "#/definitions/%s" % pinguin.get_definition_name(
self.model, "", "patch"
)
capitalized_model_name = "".join(
[s.capitalize() for s in model_name.split(".")]
)
paths_object = collections.OrderedDict(
[
(read_many_path, {}),
(read_one_path, {}),
(patch_model_path, {}),
(patch_many_path, {}),
(patch_one_path, {}),
]
)
if self.api_create:
paths_object[read_many_path]["post"] = {
"summary": "Add a new %s object to the store" % model_name,
"description": "",
"operationId": "add%s" % capitalized_model_name,
"consumes": ["application/json"],
"parameters": [
{
"in": "body",
"name": "body",
"description": "%s object that needs to be added to the store"
% model_name,
"required": True,
"schema": {
"$ref": "#/definitions/%s"
% pinguin.get_definition_name(self.model)
},
}
],
"responses": {
"201": {
"description": "successful create",
"schema": {"$ref": "#/definitions/%s-read_one" % model_name},
},
},
}
if self.api_read:
paths_object[read_many_path]["get"] = {
"summary": "Get all %s objects" % model_name,
"description": "Returns all %s objects" % model_name,
"operationId": "getAll%s" % capitalized_model_name,
"produces": ["application/json"],
"responses": {
"200": {
"description": "A list of %s." % model_name,
"schema": {
"type": "array",
"items": {"$ref": read_many_definition_ref},
},
}
},
}
paths_object[read_one_path]["get"] = {
"summary": "Get %s by ID" % model_name,
"description": "Returns a single %s" % model_name,
"operationId": "get%sById" % capitalized_model_name,
"produces": ["application/json"],
"parameters": [PARAM_ID],
"responses": {
"200": {
"description": "successful operation",
"schema": {"$ref": read_one_definition_ref},
},
"404": {"description": "%s not found" % model_name},
},
}
if self.api_update:
paths_object[read_one_path]["put"] = {
"summary": "Update %s by ID" % model_name,
"description": "",
"operationId": "update%sById" % capitalized_model_name,
"parameters": [
PARAM_ID,
{
"in": "body",
"name": "body",
"description": "Updated %s object" % model_name,
"required": True,
"schema": {
"$ref": "#/definitions/%s"
% pinguin.get_definition_name(self.model)
},
},
],
"responses": {
"204": {"description": "successful update"},
"404": {"description": "%s not found" % model_name},
},
}
if self.api_delete:
paths_object[read_one_path]["delete"] = {
"summary": "Delete %s by ID" % model_name,
"description": "",
"operationId": "delete%s" % capitalized_model_name,
"produces": ["application/json"],
"parameters": [PARAM_ID],
"responses": {
"204": {"description": "successful delete"},
"404": {"description": "%s not found" % model_name},
},
}
if self.api_public_methods or self.public_methods or self.private_methods:
allowed_methods = []
if self.api_public_methods:
allowed_methods += [
m for m in self._get_method_list() if not m.startswith("_")
]
elif self.public_methods:
allowed_methods += [m for m in self.public_methods.split("\n") if m]
if self.private_methods:
allowed_methods += [m for m in self.private_methods.split("\n") if m]
allowed_methods = list(set(allowed_methods))
PARAM_METHOD_NAME = {
"name": "method_name",
"in": "path",
"description": "Method Name",
"required": True,
"type": "string",
"enum": allowed_methods,
}
PARAM_BODY = {
"in": "body",
"name": "body",
"description": "Parameters for calling the method on a recordset",
"schema": {"$ref": patch_definition_ref},
}
RESPONSES = {
"200": {"description": "successful patch"},
"403": {
"description": "Requested model method is not allowed",
"schema": {"$ref": "#/definitions/ErrorResponse"},
},
}
paths_object[patch_one_path]["patch"] = {
"summary": "Patch %s by single ID" % model_name,
"description": "Call model method for single record.",
"operationId": "callMethodFor%sSingleRecord" % capitalized_model_name,
"consumes": ["application/json"],
"produces": ["application/json"],
"parameters": [PARAM_ID, PARAM_METHOD_NAME, PARAM_BODY],
"responses": RESPONSES,
}
paths_object[patch_model_path]["patch"] = {
"summary": "Patch %s" % model_name,
"description": "Call model method on model",
"operationId": "callMethodFor%sModel" % capitalized_model_name,
"consumes": ["application/json"],
"produces": ["application/json"],
"parameters": [PARAM_METHOD_NAME, PARAM_BODY],
"responses": RESPONSES,
}
paths_object[patch_many_path]["patch"] = {
"summary": "Patch %s by some IDs" % model_name,
"description": "Call model method for recordset.",
"operationId": "callMethodFor%sRecordset" % capitalized_model_name,
"consumes": ["application/json"],
"produces": ["application/json"],
"parameters": [
{
"name": "ids",
"in": "path",
"description": "Comma-separated Record IDS",
"required": True,
"type": "string",
},
PARAM_METHOD_NAME,
PARAM_BODY,
],
"responses": RESPONSES,
}
paths_object = {k: v for k, v in paths_object.items() if v}
for _path_item_key, path_item_value in paths_object.items():
for path_method in path_item_value.values():
# add tag
path_method.update({"tags": [model_name]})
# add global responses
path_method["responses"].update(
{
400: {"$ref": "#/responses/400"},
401: {"$ref": "#/responses/401"},
500: {"$ref": "#/responses/500"},
}
)
return paths_object
def get_OAS_definitions_part(self):
related_model = self.env[self.model]
export_fields_read_one = transform_strfields_to_dict(
self.read_one_id.export_fields.mapped("name") or ("id",)
)
export_fields_read_many = transform_strfields_to_dict(
self.read_many_id.export_fields.mapped("name") or ("id",)
)
definitions = {}
definitions.update(
pinguin.get_OAS_definitions_part(
related_model, export_fields_read_one, definition_postfix="read_one"
)
)
definitions.update(
pinguin.get_OAS_definitions_part(
related_model, export_fields_read_many, definition_postfix="read_many"
)
)
if self.api_create or self.api_update:
all_fields = transform_strfields_to_dict(related_model.fields_get_keys())
definitions.update(
pinguin.get_OAS_definitions_part(related_model, all_fields)
)
if self.api_public_methods or self.private_methods:
definitions.update(
{
pinguin.get_definition_name(self.model, "", "patch"): {
"type": "object",
"example": {
"args": [],
"kwargs": {
"body": "Message is posted via API by calling message_post method",
"subject": "Test API",
},
"context": {},
},
}
}
)
return definitions
def get_OAS_part(self):
self = self.sudo()
return {
"definitions": self.get_OAS_definitions_part(),
"paths": self.get_OAS_paths_part(),
"tag": {
"name": "%s" % self.model,
"description": "Everything about %s" % self.model,
},
}
class AccessCreateContext(models.Model):
_name = "openapi.access.create.context"
_description = "Context on creating via API "
name = fields.Char("Name", required=True)
description = fields.Char("Description")
model_id = fields.Many2one("ir.model", "Model", required=True, ondelete='cascade')
context = fields.Text("Context", required=True)
_sql_constraints = [
(
"context_model_name_uniq",
"unique (name, model_id)",
"There is already a context with the same name for this Model",
)
]
@api.model
def _fix_name(self, vals):
if "name" in vals:
vals["name"] = urlparse.quote_plus(vals["name"].lower())
return vals
@api.model
def create(self, vals):
vals = self._fix_name(vals)
return super(AccessCreateContext, self).create(vals)
def write(self, vals):
vals = self._fix_name(vals)
return super(AccessCreateContext, self).write(vals)
@api.constrains("context")
def _check_context(self):
Model = self.env[self.model_id.model]
fields = Model.fields_get()
for record in self:
try:
data = json.loads(record.context)
except ValueError:
raise exceptions.ValidationError(_("Context must be jsonable."))
for k, _v in data.items():
if k.startswith("default_") and k[8:] not in fields:
raise exceptions.ValidationError(
_('The model "%s" has no such field: "%s".') % (Model, k[8:])
)
def getmembers(obj, predicate=None):
# This is copy-pasted method from inspect lib with updates marked as NEW
"""Return all members of an object as (name, value) pairs sorted by name.
Optionally, only return members that satisfy a given predicate."""
if isclass(obj):
mro = (obj,) + getmro(obj)
else:
mro = ()
results = []
processed = set()
names = dir(obj)
# :dd any DynamicClassAttributes to the list of names if object is a class;
# this may result in duplicate entries if, for example, a virtual
# attribute with the same name as a DynamicClassAttribute exists
try:
for base in obj.__bases__:
for k, v in base.__dict__.items():
if isinstance(v, types.DynamicClassAttribute):
names.append(k)
except AttributeError:
pass
for key in names:
if key == "_cache":
# NEW
# trying to read this key will return error in odoo 11.0+
# AssertionError: Unexpected RecordCache(res.partner())
continue
# First try to get the value via getattr. Some descriptors don't
# like calling their __get__ (see bug #1785), so fall back to
# looking in the __dict__.
try:
value = getattr(obj, key)
# handle the duplicate key
if key in processed:
raise AttributeError
except AttributeError:
for base in mro:
if key in base.__dict__:
value = base.__dict__[key]
break
else:
# could be a (currently) missing slot member, or a buggy
# __dir__; discard and move on
continue
if not predicate or predicate(value):
results.append((key, value))
processed.add(key)
results.sort(key=lambda pair: pair[0])
return results
+16
View File
@@ -0,0 +1,16 @@
# Copyright 2018 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from odoo import fields, models
class Log(models.Model):
_name = "openapi.log"
_order = "id desc"
_description = "OpenAPI logs"
namespace_id = fields.Many2one("openapi.namespace", "Integration")
request = fields.Char("Request")
request_data = fields.Text("Request Data")
response_data = fields.Text("Response Data")
# create_uid -- auto field
# create_date -- auto field
+260
View File
@@ -0,0 +1,260 @@
# Copyright 2018-2019 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2018 Rafis Bikbov <https://it-projects.info/team/bikbov>
# Copyright 2019 Yan Chirino <https://xoe.solutions/>
# Copyright 2020 Anvar Kildebekov <https://it-projects.info/team/fedoranvar>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import collections
import urllib.parse as urlparse
import uuid
from odoo import api, fields, models
from odoo.addons.base_api.lib import pinguin
class Namespace(models.Model):
_name = "openapi.namespace"
_description = "Integration"
active = fields.Boolean("Active", default=True)
name = fields.Char(
"Name",
required=True,
help="""Integration name, e.g. ebay, amazon, magento, etc.
The name is used in api endpoint""",
)
description = fields.Char("Description")
log_ids = fields.One2many("openapi.log", "namespace_id", string="Logs")
log_count = fields.Integer("Log count", compute="_compute_log_count")
log_request = fields.Selection(
[("disabled", "Disabled"), ("info", "Short"), ("debug", "Full")],
"Log Requests",
default="disabled",
)
log_response = fields.Selection(
[("disabled", "Disabled"), ("error", "Errors only"), ("debug", "Full")],
"Log Responses",
default="error",
)
last_log_date = fields.Datetime(compute="_compute_last_used", string="Latest usage")
access_ids = fields.One2many(
"openapi.access",
"namespace_id",
string="Accesses",
context={"active_test": False},
)
user_ids = fields.Many2many(
"res.users", string="Allowed Users", default=lambda self: self.env.user
)
token = fields.Char(
"Identification token",
default=lambda self: str(uuid.uuid4()),
readonly=True,
required=True,
copy=False,
help="Token passed by a query string parameter to access the specification.",
)
spec_url = fields.Char("Specification Link", compute="_compute_spec_url")
_sql_constraints = [
(
"name_uniq",
"unique (name)",
"A namespace already exists with this name. Namespace's name must be unique!",
)
]
def name_get(self):
return [
(
record.id,
"/api/v1/%s%s"
% (
record.name,
" (%s)" % record.description if record.description else "",
),
)
for record in self
]
@api.model
def _fix_name(self, vals):
if "name" in vals:
vals["name"] = urlparse.quote_plus(vals["name"].lower())
return vals
@api.model
def create(self, vals):
vals = self._fix_name(vals)
return super(Namespace, self).create(vals)
def write(self, vals):
vals = self._fix_name(vals)
return super(Namespace, self).write(vals)
def get_OAS(self):
current_host = self.env["ir.config_parameter"].sudo().get_param("web.base.url")
parsed_current_host = urlparse.urlparse(current_host)
report_parameters = [
{
"name": "report_external_id",
"in": "path",
"description": "Report xml id or report name",
"required": True,
"type": "string",
},
{
"name": "docids",
"in": "path",
"description": "One identifier or several identifiers separated by commas",
"required": True,
"type": "string",
},
]
spec = collections.OrderedDict(
[
("swagger", "2.0"),
("info", {"title": self.name, "version": self.write_date}),
("host", parsed_current_host.netloc),
("basePath", "/api/v1/%s" % self.name),
("schemes", [parsed_current_host.scheme]),
(
"consumes",
["multipart/form-data", "application/x-www-form-urlencoded"],
),
("produces", ["application/json"]),
(
"paths",
{
"/report/pdf/{report_external_id}/{docids}": {
"get": {
"summary": "Get PDF report file for %s namespace"
% self.name,
"description": "Returns PDF report file for %s namespace"
% self.name,
"operationId": "getPdfReportFileFor%sNamespace"
% self.name.capitalize(),
"produces": ["application/pdf"],
"responses": {
"200": {
"description": "A PDF report file for %s namespace."
% self.name,
"schema": {"type": "file"},
}
},
"parameters": report_parameters,
"tags": ["report"],
}
},
"/report/html/{report_external_id}/{docids}": {
"get": {
"summary": "Get HTML report file for %s namespace"
% self.name,
"description": "Returns HTML report file for %s namespace"
% self.name,
"operationId": "getHtmlReportFileFor%sNamespace"
% self.name.capitalize(),
"produces": ["application/pdf"],
"responses": {
"200": {
"description": "A HTML report file for %s namespace."
% self.name,
"schema": {"type": "file"},
}
},
"parameters": report_parameters,
"tags": ["report"],
}
},
},
),
(
"definitions",
{
"ErrorResponse": {
"type": "object",
"required": ["error", "error_descrip"],
"properties": {
"error": {"type": "string"},
"error_descrip": {"type": "string"},
},
},
},
),
(
"responses",
{
"400": {
"description": "Invalid Data",
"schema": {"$ref": "#/definitions/ErrorResponse"},
},
"401": {
"description": "Authentication information is missing or invalid",
"schema": {"$ref": "#/definitions/ErrorResponse"},
},
"500": {
"description": "Server Error",
"schema": {"$ref": "#/definitions/ErrorResponse"},
},
},
),
("securityDefinitions", {"basicAuth": {"type": "basic"}}),
("security", [{"basicAuth": []}]),
("tags", []),
]
)
for openapi_access in self.access_ids.filtered("active"):
OAS_part_for_model = openapi_access.get_OAS_part()
spec["tags"].append(OAS_part_for_model["tag"])
del OAS_part_for_model["tag"]
pinguin.update(spec, OAS_part_for_model)
return spec
@api.depends("name", "token")
def _compute_spec_url(self):
for record in self:
record.spec_url = "/api/v1/{}/swagger.json?token={}&db={}".format(
record.name, record.token, self._cr.dbname,
)
def reset_token(self):
for record in self:
token = str(uuid.uuid4())
while self.search([("token", "=", token)]).exists():
token = str(uuid.uuid4())
record.write({"token": token})
def action_show_logs(self):
return {
"name": "Logs",
"view_mode": "tree,form",
"res_model": "openapi.log",
"type": "ir.actions.act_window",
"domain": [["namespace_id", "=", self.id]],
}
def _compute_last_used(self):
for s in self:
s.last_log_date = (
s.env["openapi.log"]
.search(
[("namespace_id", "=", s.id), ("create_date", "!=", False)],
limit=1,
order="id desc",
)
.create_date
)
def _compute_log_count(self):
self._cr.execute(
"SELECT COUNT(*) FROM openapi_log WHERE namespace_id=(%s);", [str(self.id)]
)
self.log_count = self._cr.dictfetchone()["count"]
+33
View File
@@ -0,0 +1,33 @@
# Copyright 2018-2019 Ivan Yelizariev <https://it-projects.info/team/yelizariev>
# Copyright 2018 Rafis Bikbov <https://it-projects.info/team/bikbov>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
import uuid
from odoo import api, fields, models
class ResUsers(models.Model):
_inherit = "res.users"
namespace_ids = fields.Many2many("openapi.namespace", string="Allowed Integrations")
openapi_token = fields.Char(
"OpenAPI Token",
default=lambda self: self._get_unique_openapi_token(),
required=True,
copy=False,
help="Authentication token for access to API (/api).",
)
def reset_openapi_token(self):
for record in self:
record.write({"openapi_token": self._get_unique_openapi_token()})
def _get_unique_openapi_token(self):
openapi_token = str(uuid.uuid4())
while self.search_count([("openapi_token", "=", openapi_token)]):
openapi_token = str(uuid.uuid4())
return openapi_token
@api.model
def reset_all_openapi_tokens(self):
self.search([]).reset_openapi_token()