mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-18 05:52:07 +02:00
add rpc addon and fix 19.0 compatibility issues
- add missing rpc addon (auto_install, required by server_wide_modules)
- add __init__.py -> init.py symlink for odoo package importability
- re-export image_process from odoo.tools (needed by web_editor)
- add backward-compatible slug/unslug functions in http_routing
🤖 assisted by claude
This commit is contained in:
parent
2d3ee4855a
commit
3037cab43e
11 changed files with 629 additions and 0 deletions
1
odoo-bringout-oca-ocb-base/odoo/__init__.py
Symbolic link
1
odoo-bringout-oca-ocb-base/odoo/__init__.py
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
init.py
|
||||||
|
|
@ -18,3 +18,4 @@ from .translate import _, html_translate, xml_translate, LazyTranslate
|
||||||
from .xml_utils import cleanup_xml_node, load_xsd_files_from_url, validate_xml_from_attachment
|
from .xml_utils import cleanup_xml_node, load_xsd_files_from_url, validate_xml_from_attachment
|
||||||
from .convert import convert_csv_import, convert_file, convert_sql_import, convert_xml_import
|
from .convert import convert_csv_import, convert_file, convert_sql_import, convert_xml_import
|
||||||
from .set_expression import SetDefinitions
|
from .set_expression import SetDefinitions
|
||||||
|
from .image import image_process, image_data_uri, base64_to_image, image_to_base64
|
||||||
|
|
|
||||||
|
|
@ -628,3 +628,14 @@ class IrHttp(models.AbstractModel):
|
||||||
except werkzeug.exceptions.NotFound:
|
except werkzeug.exceptions.NotFound:
|
||||||
new_url = path
|
new_url = path
|
||||||
return new_url or path, endpoint and endpoint[0]
|
return new_url or path, endpoint and endpoint[0]
|
||||||
|
|
||||||
|
|
||||||
|
# Backward-compatible module-level functions that delegate to IrHttp class methods.
|
||||||
|
# In Odoo 19.0 these were moved to IrHttp._slug/_unslug, but many addons
|
||||||
|
# still import them as standalone functions.
|
||||||
|
def slug(value):
|
||||||
|
return IrHttp._slug(value)
|
||||||
|
|
||||||
|
|
||||||
|
def unslug(value):
|
||||||
|
return IrHttp._unslug(value)
|
||||||
|
|
|
||||||
1
odoo-bringout-oca-ocb-rpc/rpc/__init__.py
Normal file
1
odoo-bringout-oca-ocb-rpc/rpc/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import controllers
|
||||||
15
odoo-bringout-oca-ocb-rpc/rpc/__manifest__.py
Normal file
15
odoo-bringout-oca-ocb-rpc/rpc/__manifest__.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
'name': 'RPC endpoints',
|
||||||
|
'description': """\
|
||||||
|
Standard Odoo RPC endpoints to models
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
This module provides the /xmlrpc and /jsonrpc endpoints used to
|
||||||
|
programmatically access models.
|
||||||
|
""",
|
||||||
|
'depends': ["base"],
|
||||||
|
'category': 'Extra Tools',
|
||||||
|
'auto_install': True,
|
||||||
|
'author': 'Odoo S.A.',
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
29
odoo-bringout-oca-ocb-rpc/rpc/controllers/__init__.py
Normal file
29
odoo-bringout-oca-ocb-rpc/rpc/controllers/__init__.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import odoo.release
|
||||||
|
from odoo.http import request, route
|
||||||
|
|
||||||
|
from . import json2
|
||||||
|
|
||||||
|
RPC_DEPRECATION_NOTICE = """\
|
||||||
|
The /xmlrpc, /xmlrpc/2 and /jsonrpc endpoints are deprecated in Odoo 19 \
|
||||||
|
and scheduled for removal in Odoo 20. Please report the problem to the \
|
||||||
|
client making the request.
|
||||||
|
Mute this logger: --log-handler %s:ERROR
|
||||||
|
https://www.odoo.com/documentation/latest/developer/reference/external_api.html#migrating-from-xml-rpc-json-rpc"""
|
||||||
|
|
||||||
|
|
||||||
|
def _check_request():
|
||||||
|
if request.db:
|
||||||
|
request.env.cr.close()
|
||||||
|
|
||||||
|
|
||||||
|
from .jsonrpc import JSONRPC # noqa: E402
|
||||||
|
from .xmlrpc import XMLRPC # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class RPC(XMLRPC, JSONRPC):
|
||||||
|
@route(['/web/version', '/json/version'], type='http', auth='none', readonly=True)
|
||||||
|
def version(self):
|
||||||
|
return request.make_json_response({
|
||||||
|
'version_info': odoo.release.version_info,
|
||||||
|
'version': odoo.release.version,
|
||||||
|
})
|
||||||
90
odoo-bringout-oca-ocb-rpc/rpc/controllers/json2.py
Normal file
90
odoo-bringout-oca-ocb-rpc/rpc/controllers/json2.py
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import logging
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from werkzeug.exceptions import (
|
||||||
|
NotFound,
|
||||||
|
UnprocessableEntity,
|
||||||
|
)
|
||||||
|
|
||||||
|
from odoo import http
|
||||||
|
from odoo.http import request
|
||||||
|
from odoo.models import BaseModel
|
||||||
|
from odoo.service.model import get_public_method
|
||||||
|
from odoo.tools import frozendict
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class WebJson2Controller(http.Controller):
|
||||||
|
def _web_json_2_rpc_readonly(self, rule, args):
|
||||||
|
try:
|
||||||
|
model_name = args['__model__']
|
||||||
|
method_name = args['__method__']
|
||||||
|
Model = request.registry[model_name]
|
||||||
|
except KeyError:
|
||||||
|
# no need of a read/write cursor to send a 404 http error
|
||||||
|
return True
|
||||||
|
for cls in Model.mro():
|
||||||
|
method = getattr(cls, method_name, None)
|
||||||
|
if method is not None and hasattr(method, '_readonly'):
|
||||||
|
return method._readonly
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Take over /json/<path:subpath>
|
||||||
|
@http.route(
|
||||||
|
['/json/2', '/json/2/<path:subpath>'],
|
||||||
|
auth='public',
|
||||||
|
type='json2',
|
||||||
|
readonly=True,
|
||||||
|
methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
||||||
|
)
|
||||||
|
def web_json_2_404(self, subpath=None):
|
||||||
|
e = "Did you mean POST /json/2/<model>/<method>?"
|
||||||
|
raise request.not_found(e)
|
||||||
|
|
||||||
|
@http.route(
|
||||||
|
'/json/2/<__model__>/<__method__>',
|
||||||
|
methods=['POST'],
|
||||||
|
auth='bearer',
|
||||||
|
type='json2',
|
||||||
|
readonly=_web_json_2_rpc_readonly,
|
||||||
|
save_session=False,
|
||||||
|
)
|
||||||
|
def web_json_2_rpc(
|
||||||
|
self,
|
||||||
|
__model__: str,
|
||||||
|
__method__: str,
|
||||||
|
ids: Sequence[int] = (),
|
||||||
|
context: Mapping[str, Any] = frozendict(),
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
Model = request.env[__model__].with_context(context)
|
||||||
|
except KeyError as exc:
|
||||||
|
e = f"the model {__model__!r} does not exist"
|
||||||
|
raise NotFound(e) from exc
|
||||||
|
|
||||||
|
try:
|
||||||
|
func = get_public_method(Model, __method__)
|
||||||
|
except AttributeError as exc:
|
||||||
|
raise NotFound(exc.args[0]) from exc
|
||||||
|
if hasattr(func, '_api_model') and ids:
|
||||||
|
e = f"cannot call {__model__}.{__method__} with ids"
|
||||||
|
raise UnprocessableEntity(e)
|
||||||
|
|
||||||
|
records = Model.browse(ids)
|
||||||
|
signature = inspect.signature(func)
|
||||||
|
try:
|
||||||
|
signature.bind(records, **kwargs)
|
||||||
|
except TypeError as exc:
|
||||||
|
raise UnprocessableEntity(exc.args[0])
|
||||||
|
|
||||||
|
result = func(records, **kwargs)
|
||||||
|
if isinstance(result, BaseModel):
|
||||||
|
result = result.ids
|
||||||
|
|
||||||
|
return result
|
||||||
16
odoo-bringout-oca-ocb-rpc/rpc/controllers/jsonrpc.py
Normal file
16
odoo-bringout-oca-ocb-rpc/rpc/controllers/jsonrpc.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from odoo.http import Controller, dispatch_rpc, route
|
||||||
|
|
||||||
|
from . import RPC_DEPRECATION_NOTICE, _check_request
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class JSONRPC(Controller):
|
||||||
|
@route('/jsonrpc', type='jsonrpc', auth="none", save_session=False)
|
||||||
|
def jsonrpc(self, service, method, args):
|
||||||
|
""" Method used by client APIs to contact OpenERP. """
|
||||||
|
logger.warning(RPC_DEPRECATION_NOTICE, __name__)
|
||||||
|
_check_request()
|
||||||
|
return dispatch_rpc(service, method, args)
|
||||||
169
odoo-bringout-oca-ocb-rpc/rpc/controllers/xmlrpc.py
Normal file
169
odoo-bringout-oca-ocb-rpc/rpc/controllers/xmlrpc.py
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
import xmlrpc.client
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
from markupsafe import Markup
|
||||||
|
|
||||||
|
import odoo.exceptions
|
||||||
|
from odoo.fields import Command, Date, Datetime
|
||||||
|
from odoo.http import Controller, Response, dispatch_rpc, request, route
|
||||||
|
from odoo.tools import lazy
|
||||||
|
from odoo.tools.misc import frozendict
|
||||||
|
|
||||||
|
from . import RPC_DEPRECATION_NOTICE, _check_request
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# XML-RPC fault codes. Some care must be taken when changing these: the
|
||||||
|
# constants are also defined client-side and must remain in sync.
|
||||||
|
# User code must use the exceptions defined in ``odoo.exceptions`` (not
|
||||||
|
# create directly ``xmlrpc.client.Fault`` objects).
|
||||||
|
RPC_FAULT_CODE_CLIENT_ERROR = 1 # indistinguishable from app. error.
|
||||||
|
RPC_FAULT_CODE_APPLICATION_ERROR = 1
|
||||||
|
RPC_FAULT_CODE_WARNING = 2
|
||||||
|
RPC_FAULT_CODE_ACCESS_DENIED = 3
|
||||||
|
RPC_FAULT_CODE_ACCESS_ERROR = 4
|
||||||
|
|
||||||
|
# 0 to 31, excluding tab, newline, and carriage return
|
||||||
|
CONTROL_CHARACTERS = dict.fromkeys(set(range(32)) - {9, 10, 13})
|
||||||
|
|
||||||
|
|
||||||
|
def xmlrpc_handle_exception_int(e):
|
||||||
|
if isinstance(e, odoo.exceptions.RedirectWarning):
|
||||||
|
fault = xmlrpc.client.Fault(RPC_FAULT_CODE_WARNING, str(e))
|
||||||
|
elif isinstance(e, odoo.exceptions.AccessError):
|
||||||
|
fault = xmlrpc.client.Fault(RPC_FAULT_CODE_ACCESS_ERROR, str(e))
|
||||||
|
elif isinstance(e, odoo.exceptions.AccessDenied):
|
||||||
|
fault = xmlrpc.client.Fault(RPC_FAULT_CODE_ACCESS_DENIED, str(e))
|
||||||
|
elif isinstance(e, odoo.exceptions.UserError):
|
||||||
|
fault = xmlrpc.client.Fault(RPC_FAULT_CODE_WARNING, str(e))
|
||||||
|
else:
|
||||||
|
info = sys.exc_info()
|
||||||
|
formatted_info = "".join(traceback.format_exception(*info))
|
||||||
|
fault = xmlrpc.client.Fault(RPC_FAULT_CODE_APPLICATION_ERROR, formatted_info)
|
||||||
|
|
||||||
|
return dumps(fault)
|
||||||
|
|
||||||
|
|
||||||
|
def xmlrpc_handle_exception_string(e):
|
||||||
|
if isinstance(e, odoo.exceptions.RedirectWarning):
|
||||||
|
fault = xmlrpc.client.Fault(f'warning -- Warning\n\n{e}', '')
|
||||||
|
elif isinstance(e, odoo.exceptions.MissingError):
|
||||||
|
fault = xmlrpc.client.Fault(f'warning -- MissingError\n\n{e}', '')
|
||||||
|
elif isinstance(e, odoo.exceptions.AccessError):
|
||||||
|
fault = xmlrpc.client.Fault(f'warning -- AccessError\n\n{e}', '')
|
||||||
|
elif isinstance(e, odoo.exceptions.AccessDenied):
|
||||||
|
fault = xmlrpc.client.Fault('AccessDenied', str(e))
|
||||||
|
elif isinstance(e, odoo.exceptions.UserError):
|
||||||
|
fault = xmlrpc.client.Fault(f'warning -- UserError\n\n{e}', '')
|
||||||
|
# InternalError
|
||||||
|
else:
|
||||||
|
info = sys.exc_info()
|
||||||
|
formatted_info = "".join(traceback.format_exception(*info))
|
||||||
|
fault = xmlrpc.client.Fault(odoo.tools.exception_to_unicode(e), formatted_info)
|
||||||
|
|
||||||
|
return dumps(fault)
|
||||||
|
|
||||||
|
|
||||||
|
class OdooMarshaller(xmlrpc.client.Marshaller):
|
||||||
|
dispatch = dict(xmlrpc.client.Marshaller.dispatch)
|
||||||
|
|
||||||
|
def dump_frozen_dict(self, value, write):
|
||||||
|
value = dict(value)
|
||||||
|
self.dump_struct(value, write)
|
||||||
|
|
||||||
|
# By default, in xmlrpc, bytes are converted to xmlrpc.client.Binary object.
|
||||||
|
# Historically, odoo is sending binary as base64 string.
|
||||||
|
# In python 3, base64.b64{de,en}code() methods now works on bytes.
|
||||||
|
def dump_bytes(self, value, write):
|
||||||
|
self.dump_unicode(value.decode(), write)
|
||||||
|
|
||||||
|
def dump_datetime(self, value, write):
|
||||||
|
# override to marshall as a string for backwards compatibility
|
||||||
|
value = Datetime.to_string(value)
|
||||||
|
self.dump_unicode(value, write)
|
||||||
|
|
||||||
|
# convert date objects to strings in iso8061 format.
|
||||||
|
def dump_date(self, value, write):
|
||||||
|
value = Date.to_string(value)
|
||||||
|
self.dump_unicode(value, write)
|
||||||
|
|
||||||
|
def dump_lazy(self, value, write):
|
||||||
|
v = value._value
|
||||||
|
return self.dispatch[type(v)](self, v, write)
|
||||||
|
|
||||||
|
def dump_unicode(self, value, write):
|
||||||
|
# XML 1.0 disallows control characters, remove them otherwise they break clients
|
||||||
|
return super().dump_unicode(value.translate(CONTROL_CHARACTERS), write)
|
||||||
|
|
||||||
|
dispatch[frozendict] = dump_frozen_dict
|
||||||
|
dispatch[bytes] = dump_bytes
|
||||||
|
dispatch[datetime] = dump_datetime
|
||||||
|
dispatch[date] = dump_date
|
||||||
|
dispatch[lazy] = dump_lazy
|
||||||
|
dispatch[str] = dump_unicode
|
||||||
|
dispatch[Command] = dispatch[int]
|
||||||
|
dispatch[defaultdict] = dispatch[dict]
|
||||||
|
dispatch[Markup] = lambda self, value, write: self.dispatch[str](self, str(value), write)
|
||||||
|
|
||||||
|
|
||||||
|
def dumps(params: list | tuple | xmlrpc.client.Fault) -> str:
|
||||||
|
response = OdooMarshaller(allow_none=False).dumps(params)
|
||||||
|
return f"""\
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<methodResponse>
|
||||||
|
{response}
|
||||||
|
</methodResponse>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# RPC Controller
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
|
||||||
|
class XMLRPC(Controller):
|
||||||
|
"""Handle RPC connections."""
|
||||||
|
|
||||||
|
def _xmlrpc(self, service):
|
||||||
|
"""Common method to handle an XML-RPC request."""
|
||||||
|
data = request.httprequest.get_data()
|
||||||
|
params, method = xmlrpc.client.loads(data, use_datetime=True)
|
||||||
|
result = dispatch_rpc(service, method, params)
|
||||||
|
return dumps((result,))
|
||||||
|
|
||||||
|
@route("/xmlrpc/<service>", auth="none", methods=["POST"], csrf=False, save_session=False)
|
||||||
|
def xmlrpc_1(self, service):
|
||||||
|
"""XML-RPC service that returns faultCode as strings.
|
||||||
|
|
||||||
|
This entrypoint is historical and non-compliant, but kept for
|
||||||
|
backwards-compatibility.
|
||||||
|
"""
|
||||||
|
logger.warning(RPC_DEPRECATION_NOTICE, __name__)
|
||||||
|
_check_request()
|
||||||
|
try:
|
||||||
|
response = self._xmlrpc(service)
|
||||||
|
except Exception as error:
|
||||||
|
error.error_response = Response(
|
||||||
|
response=xmlrpc_handle_exception_string(error),
|
||||||
|
mimetype='text/xml',
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
return Response(response=response, mimetype='text/xml')
|
||||||
|
|
||||||
|
@route("/xmlrpc/2/<service>", auth="none", methods=["POST"], csrf=False, save_session=False)
|
||||||
|
def xmlrpc_2(self, service):
|
||||||
|
"""XML-RPC service that returns faultCode as int."""
|
||||||
|
logger.warning(RPC_DEPRECATION_NOTICE, __name__)
|
||||||
|
_check_request()
|
||||||
|
try:
|
||||||
|
response = self._xmlrpc(service)
|
||||||
|
except Exception as error:
|
||||||
|
error.error_response = Response(
|
||||||
|
response=xmlrpc_handle_exception_int(error),
|
||||||
|
mimetype='text/xml',
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
return Response(response=response, mimetype='text/xml')
|
||||||
1
odoo-bringout-oca-ocb-rpc/rpc/tests/__init__.py
Normal file
1
odoo-bringout-oca-ocb-rpc/rpc/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import test_xmlrpc
|
||||||
295
odoo-bringout-oca-ocb-rpc/rpc/tests/test_xmlrpc.py
Normal file
295
odoo-bringout-oca-ocb-rpc/rpc/tests/test_xmlrpc.py
Normal file
|
|
@ -0,0 +1,295 @@
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
import collections
|
||||||
|
import datetime
|
||||||
|
import time
|
||||||
|
|
||||||
|
import odoo
|
||||||
|
import odoo.tools
|
||||||
|
from odoo.exceptions import AccessDenied, AccessError
|
||||||
|
from odoo.http import _request_stack
|
||||||
|
from odoo.service import common as auth
|
||||||
|
from odoo.service import model
|
||||||
|
from odoo.tests import common
|
||||||
|
from odoo.tools import DotDict, mute_logger
|
||||||
|
|
||||||
|
from odoo.addons.base.tests.common import SavepointCaseWithUserDemo
|
||||||
|
|
||||||
|
|
||||||
|
class TestExternalAPI(SavepointCaseWithUserDemo):
|
||||||
|
|
||||||
|
def test_call_kw(self):
|
||||||
|
"""kwargs is not modified by the execution of the call"""
|
||||||
|
partner = self.env['res.partner'].create({'name': 'MyPartner1'})
|
||||||
|
args = (partner.ids, ['name'])
|
||||||
|
kwargs = {'context': {'test': True}}
|
||||||
|
model.call_kw(self.env['res.partner'], 'read', args, kwargs)
|
||||||
|
self.assertEqual(kwargs, {'context': {'test': True}})
|
||||||
|
|
||||||
|
|
||||||
|
@common.tagged('post_install', '-at_install')
|
||||||
|
class TestXMLRPC(common.HttpCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestXMLRPC, self).setUp()
|
||||||
|
self.admin_uid = self.env.ref('base.user_admin').id
|
||||||
|
|
||||||
|
ml_xml = mute_logger('odoo.addons.rpc.controllers.xmlrpc')
|
||||||
|
ml_xml.__enter__() # noqa: PLC2801
|
||||||
|
self.addCleanup(ml_xml.__exit__)
|
||||||
|
|
||||||
|
ml_json = mute_logger('odoo.addons.rpc.controllers.jsonrpc')
|
||||||
|
ml_json.__enter__() # noqa: PLC2801
|
||||||
|
self.addCleanup(ml_json.__exit__)
|
||||||
|
|
||||||
|
def xmlrpc(self, model, method, *args, **kwargs):
|
||||||
|
return self.xmlrpc_object.execute_kw(
|
||||||
|
common.get_db_name(), self.admin_uid, 'admin',
|
||||||
|
model, method, args, kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_01_xmlrpc_login(self):
|
||||||
|
""" Try to login on the common service. """
|
||||||
|
db_name = common.get_db_name()
|
||||||
|
uid = self.xmlrpc_common.login(db_name, 'admin', 'admin')
|
||||||
|
self.assertEqual(uid, self.admin_uid)
|
||||||
|
|
||||||
|
def test_xmlrpc_ir_model_search(self):
|
||||||
|
""" Try a search on the object service. """
|
||||||
|
o = self.xmlrpc_object
|
||||||
|
db_name = common.get_db_name()
|
||||||
|
ids = o.execute(db_name, self.admin_uid, 'admin', 'ir.model', 'search', [])
|
||||||
|
self.assertIsInstance(ids, list)
|
||||||
|
ids = o.execute(db_name, self.admin_uid, 'admin', 'ir.model', 'search', [], {})
|
||||||
|
self.assertIsInstance(ids, list)
|
||||||
|
|
||||||
|
def test_xmlrpc_datetime(self):
|
||||||
|
""" Test that native datetime can be sent over xmlrpc
|
||||||
|
"""
|
||||||
|
m = self.env.ref('base.model_res_device_log')
|
||||||
|
self.env['ir.model.access'].create({
|
||||||
|
'name': "w/e",
|
||||||
|
'model_id': m.id,
|
||||||
|
'perm_read': True,
|
||||||
|
'perm_create': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
ids = self.xmlrpc(
|
||||||
|
'res.device.log', 'create',
|
||||||
|
{'session_identifier': "abc", 'first_activity': now, 'revoked': False}
|
||||||
|
)
|
||||||
|
[r] = self.xmlrpc(
|
||||||
|
'res.device.log', 'read',
|
||||||
|
ids, ['first_activity'],
|
||||||
|
)
|
||||||
|
self.assertEqual(r['first_activity'], now.isoformat(" ", "seconds"))
|
||||||
|
|
||||||
|
def test_xmlrpc_read_group(self):
|
||||||
|
self.xmlrpc_object.execute(
|
||||||
|
common.get_db_name(), self.admin_uid, 'admin',
|
||||||
|
'res.partner', 'formatted_read_group', [], ['parent_id'], ['color:sum'],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_xmlrpc_name_search(self):
|
||||||
|
self.xmlrpc_object.execute(
|
||||||
|
common.get_db_name(), self.admin_uid, 'admin',
|
||||||
|
'res.partner', 'name_search', "admin"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_xmlrpc_html_field(self):
|
||||||
|
sig = '<p>bork bork bork <span style="font-weight: bork">bork</span><br></p>'
|
||||||
|
r = self.env['res.users'].create({
|
||||||
|
'name': 'bob',
|
||||||
|
'login': 'bob',
|
||||||
|
'signature': sig
|
||||||
|
})
|
||||||
|
self.assertEqual(str(r.signature), sig)
|
||||||
|
[x] = self.xmlrpc('res.users', 'read', r.id, ['signature'])
|
||||||
|
self.assertEqual(x['signature'], sig)
|
||||||
|
|
||||||
|
def test_xmlrpc_frozendict_marshalling(self):
|
||||||
|
""" Test that the marshalling of a frozendict object works properly over XMLRPC """
|
||||||
|
self.env.ref('base.user_admin').tz = "Europe/Brussels"
|
||||||
|
ctx = self.xmlrpc_object.execute(
|
||||||
|
common.get_db_name(), self.admin_uid, 'admin',
|
||||||
|
'res.users', 'context_get',
|
||||||
|
)
|
||||||
|
self.assertEqual(ctx['lang'], 'en_US')
|
||||||
|
self.assertEqual(ctx['tz'], 'Europe/Brussels')
|
||||||
|
|
||||||
|
def test_xmlrpc_defaultdict_marshalling(self):
|
||||||
|
"""
|
||||||
|
Test that the marshalling of a collections.defaultdict object
|
||||||
|
works properly over XMLRPC
|
||||||
|
"""
|
||||||
|
self.patch(self.registry['res.users'], 'context_get',
|
||||||
|
odoo.api.model(lambda *_: collections.defaultdict(int)))
|
||||||
|
self.assertEqual(self.xmlrpc('res.users', 'context_get'), {})
|
||||||
|
|
||||||
|
def test_xmlrpc_remove_control_characters(self):
|
||||||
|
record = self.env['res.users'].create({
|
||||||
|
'name': 'bob with a control character: \x03',
|
||||||
|
'login': 'bob',
|
||||||
|
})
|
||||||
|
self.assertEqual(record.name, 'bob with a control character: \x03')
|
||||||
|
[record_data] = self.xmlrpc('res.users', 'read', record.id, ['name'])
|
||||||
|
self.assertEqual(record_data['name'], 'bob with a control character: ')
|
||||||
|
|
||||||
|
def test_jsonrpc_read_group(self):
|
||||||
|
self._json_call(
|
||||||
|
common.get_db_name(), self.admin_uid, 'admin',
|
||||||
|
'res.partner', 'formatted_read_group', [], ['parent_id'], ['color:sum'],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_jsonrpc_name_search(self):
|
||||||
|
# well that's some sexy sexy call right there
|
||||||
|
self._json_call(
|
||||||
|
common.get_db_name(),
|
||||||
|
self.admin_uid, 'admin',
|
||||||
|
'res.partner', 'name_search', 'admin'
|
||||||
|
)
|
||||||
|
|
||||||
|
def _json_call(self, *args):
|
||||||
|
self.url_open(f"{self.base_url()}/jsonrpc", json={
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'id': None,
|
||||||
|
'method': 'call',
|
||||||
|
'params': {
|
||||||
|
'service': 'object',
|
||||||
|
'method': 'execute',
|
||||||
|
'args': args
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_xmlrpc_attachment_raw(self):
|
||||||
|
ids = self.env['ir.attachment'].create({'name': 'n', 'raw': b'\x01\x09'}).ids
|
||||||
|
[att] = self.xmlrpc_object.execute(
|
||||||
|
common.get_db_name(), self.admin_uid, 'admin',
|
||||||
|
'ir.attachment', 'read', ids, ['raw'])
|
||||||
|
self.assertEqual(att['raw'], '\t',
|
||||||
|
"on read, binary data should be decoded as a string and stripped from control character")
|
||||||
|
|
||||||
|
# really just for the test cursor
|
||||||
|
@common.tagged('post_install', '-at_install')
|
||||||
|
class TestAPIKeys(common.HttpCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls._user = cls.env['res.users'].create({
|
||||||
|
'name': "Bylan",
|
||||||
|
'login': 'byl',
|
||||||
|
'password': 'ananananan',
|
||||||
|
'tz': 'Australia/Eucla',
|
||||||
|
})
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
def get_json_data():
|
||||||
|
raise ValueError("There is no json here")
|
||||||
|
# needs a fake request in order to call methods protected with check_identity
|
||||||
|
self.http_request_key = self.canonical_tag
|
||||||
|
fake_req = DotDict({
|
||||||
|
# various things go and access request items
|
||||||
|
'httprequest': DotDict({
|
||||||
|
'environ': {'REMOTE_ADDR': 'localhost'},
|
||||||
|
'cookies': {common.TEST_CURSOR_COOKIE_NAME: self.canonical_tag},
|
||||||
|
'args': {},
|
||||||
|
}),
|
||||||
|
'cookies': {common.TEST_CURSOR_COOKIE_NAME: self.canonical_tag},
|
||||||
|
# bypass check_identity flow
|
||||||
|
'session': {'identity-check-last': time.time()},
|
||||||
|
'geoip': {},
|
||||||
|
'get_json_data': get_json_data,
|
||||||
|
})
|
||||||
|
_request_stack.push(fake_req)
|
||||||
|
self.addCleanup(_request_stack.pop)
|
||||||
|
|
||||||
|
def test_trivial(self):
|
||||||
|
uid = auth.dispatch('authenticate', [self.env.cr.dbname, 'byl', 'ananananan', {}])
|
||||||
|
self.assertEqual(uid, self._user.id)
|
||||||
|
|
||||||
|
ctx = model.dispatch('execute_kw', [
|
||||||
|
self.env.cr.dbname, uid, 'ananananan',
|
||||||
|
'res.users', 'context_get', []
|
||||||
|
])
|
||||||
|
self.assertEqual(ctx['tz'], 'Australia/Eucla')
|
||||||
|
|
||||||
|
def test_wrongpw(self):
|
||||||
|
# User.authenticate raises but RPC.authenticate returns False
|
||||||
|
uid = auth.dispatch('authenticate', [self.env.cr.dbname, 'byl', 'aws', {}])
|
||||||
|
self.assertFalse(uid)
|
||||||
|
with self.assertRaises(AccessDenied):
|
||||||
|
model.dispatch('execute_kw', [
|
||||||
|
self.env.cr.dbname, self._user.id, 'aws',
|
||||||
|
'res.users', 'context_get', []
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_key(self):
|
||||||
|
env = self.env(user=self._user)
|
||||||
|
r = env['res.users.apikeys.description'].create({
|
||||||
|
'name': 'a',
|
||||||
|
}).make_key()
|
||||||
|
k = r['context']['default_key']
|
||||||
|
|
||||||
|
uid = auth.dispatch('authenticate', [self.env.cr.dbname, 'byl', 'ananananan', {}])
|
||||||
|
self.assertEqual(uid, self._user.id)
|
||||||
|
|
||||||
|
uid = auth.dispatch('authenticate', [self.env.cr.dbname, 'byl', k, {}])
|
||||||
|
self.assertEqual(uid, self._user.id)
|
||||||
|
|
||||||
|
ctx = model.dispatch('execute_kw', [
|
||||||
|
self.env.cr.dbname, uid, k,
|
||||||
|
'res.users', 'context_get', []
|
||||||
|
])
|
||||||
|
self.assertEqual(ctx['tz'], 'Australia/Eucla')
|
||||||
|
|
||||||
|
api_key = model.call_kw(
|
||||||
|
model=self.env['res.users.apikeys.description'],
|
||||||
|
name='create',
|
||||||
|
args=[{'name': 'Name of the key'}],
|
||||||
|
kwargs={}
|
||||||
|
)
|
||||||
|
self.assertTrue(isinstance(api_key, int))
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
env = self.env(user=self._user)
|
||||||
|
env['res.users.apikeys.description'].create({'name': 'b',}).make_key()
|
||||||
|
env['res.users.apikeys.description'].create({'name': 'b',}).make_key()
|
||||||
|
env['res.users.apikeys.description'].create({'name': 'b',}).make_key()
|
||||||
|
k0, k1, k2 = env['res.users.apikeys'].search([])
|
||||||
|
|
||||||
|
# user can remove their own keys
|
||||||
|
k0.remove()
|
||||||
|
self.assertFalse(k0.exists())
|
||||||
|
|
||||||
|
# admin can remove user keys
|
||||||
|
k1.with_user(self.env.ref('base.user_admin')).remove ()
|
||||||
|
self.assertFalse(k1.exists())
|
||||||
|
|
||||||
|
# other user can't remove user keys
|
||||||
|
u = self.env['res.users'].create({
|
||||||
|
'name': 'a',
|
||||||
|
'login': 'a',
|
||||||
|
'group_ids': self.env.ref('base.group_user').ids,
|
||||||
|
})
|
||||||
|
with self.assertRaises(AccessError):
|
||||||
|
k2.with_user(u).remove()
|
||||||
|
|
||||||
|
def test_disabled(self):
|
||||||
|
env = self.env(user=self._user)
|
||||||
|
k = env['res.users.apikeys.description'].create({'name': 'b',}).make_key()['context']['default_key']
|
||||||
|
|
||||||
|
self._user.active = False
|
||||||
|
|
||||||
|
with self.assertRaises(AccessDenied):
|
||||||
|
model.dispatch('execute_kw', [
|
||||||
|
self.env.cr.dbname, self._user.id, 'ananananan',
|
||||||
|
'res.users', 'context_get', []
|
||||||
|
])
|
||||||
|
|
||||||
|
with self.assertRaises(AccessDenied):
|
||||||
|
model.dispatch('execute_kw', [
|
||||||
|
self.env.cr.dbname, self._user.id, k,
|
||||||
|
'res.users', 'context_get', []
|
||||||
|
])
|
||||||
Loading…
Add table
Add a link
Reference in a new issue