mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-10-25 23:40:05 -05:00
[ADD] new openapi modules
This commit is contained in:
1 parent
a7916efebd
commit
bd89cf9097
54 files changed
+5094
No files matched your search
@@ -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
|
||||
@@ -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])
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user