mirror of
https://github.com/OCA/server-backend.git
synced 2025-02-18 09:52:42 +02:00
[ADD] base_external_system: Implement interface/adapter (#993)
* [ADD] base_external_system: Implement interface/adapter for external systems * base_external_system: Fix OS model, add inherits, add validate * base_external_system: Usability and private key pass * base_external_system: Use contextmanager in adapter client * base_external_system: Move contextmanager to interface * base_external_system: Include contextmanager on adapter and system * base_external_system: Unify client * Use password widget for password field * Add tests & security * Fix lint * Add plaintext note
This commit is contained in:
committed by
Ronald Portier
parent
1d2cad45f9
commit
0c879cf0cf
96
base_external_system/README.rst
Executable file
96
base_external_system/README.rst
Executable file
@@ -0,0 +1,96 @@
|
|||||||
|
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.svg
|
||||||
|
:target: http://www.gnu.org/licenses/lgpl.html
|
||||||
|
:alt: License: LGPL-3
|
||||||
|
|
||||||
|
======================
|
||||||
|
Base - External System
|
||||||
|
======================
|
||||||
|
|
||||||
|
This module provides an interface/adapter mechanism for the definition of remote
|
||||||
|
systems.
|
||||||
|
|
||||||
|
Note that this module stores everything in plain text. In the interest of security,
|
||||||
|
it is recommended you use another module (such as `keychain` or `red_october` to
|
||||||
|
encrypt things like the password and private key). This is not done here in order
|
||||||
|
to not force a specific security method.
|
||||||
|
|
||||||
|
Implementation
|
||||||
|
==============
|
||||||
|
|
||||||
|
The credentials for systems are stored in the ``external.system`` model, and are to
|
||||||
|
be configured by the user. This model is the unified interface for the underlying
|
||||||
|
adapters.
|
||||||
|
|
||||||
|
Using the Interface
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Given an ``external.system`` singleton called ``external_system``, you would do the
|
||||||
|
following to get the underlying system client:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
with external_system.client() as client:
|
||||||
|
client.do_something()
|
||||||
|
|
||||||
|
The client will be destroyed once the context has completed. Destruction takes place
|
||||||
|
in the adapter's ``external_destroy_client`` method.
|
||||||
|
|
||||||
|
The only unified aspect of this interface is the client connection itself. Other more
|
||||||
|
opinionated interface/adapter mechanisms can be implemented in other modules, such as
|
||||||
|
the file system interface in `OCA/server-tools/external_file_location
|
||||||
|
<https://github.com/OCA/server-tools/tree/9.0/external_file_location>`_.
|
||||||
|
|
||||||
|
Creating an Adapter
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Modules looking to add an external system adapter should inherit the
|
||||||
|
``external.system.adapter`` model and override the following methods:
|
||||||
|
|
||||||
|
* ``external_get_client``: Returns a usable client for the system
|
||||||
|
* ``external_destroy_client``: Destroy the connection, if applicable. Does not need
|
||||||
|
to be defined if the connection destroys itself.
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
Configure external systems in Settings => Technical => External Systems
|
||||||
|
|
||||||
|
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
||||||
|
:alt: Try me on Runbot
|
||||||
|
:target: https://runbot.odoo-community.org/runbot/149/10.0
|
||||||
|
|
||||||
|
Bug Tracker
|
||||||
|
===========
|
||||||
|
|
||||||
|
Bugs are tracked on `GitHub Issues
|
||||||
|
<https://github.com/OCA/server-tools/issues>`_. In case of trouble, please
|
||||||
|
check there if your issue has already been reported. If you spotted it first,
|
||||||
|
help us smash it by providing detailed and welcomed feedback.
|
||||||
|
|
||||||
|
Credits
|
||||||
|
=======
|
||||||
|
|
||||||
|
Images
|
||||||
|
------
|
||||||
|
|
||||||
|
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
|
||||||
|
|
||||||
|
Contributors
|
||||||
|
------------
|
||||||
|
|
||||||
|
* Dave Lasley <dave@laslabs.com>
|
||||||
|
|
||||||
|
Maintainer
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. image:: https://odoo-community.org/logo.png
|
||||||
|
:alt: Odoo Community Association
|
||||||
|
:target: https://odoo-community.org
|
||||||
|
|
||||||
|
This module is maintained by the OCA.
|
||||||
|
|
||||||
|
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||||
|
mission is to support the collaborative development of Odoo features and
|
||||||
|
promote its widespread use.
|
||||||
|
|
||||||
|
To contribute to this module, please visit https://odoo-community.org.
|
||||||
5
base_external_system/__init__.py
Normal file
5
base_external_system/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2017 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
|
||||||
|
from . import models
|
||||||
23
base_external_system/__manifest__.py
Normal file
23
base_external_system/__manifest__.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2017 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Base External System",
|
||||||
|
"summary": "Data models allowing for connection to external systems.",
|
||||||
|
"version": "10.0.1.0.0",
|
||||||
|
"category": "Base",
|
||||||
|
"website": "https://laslabs.com/",
|
||||||
|
"author": "LasLabs, Odoo Community Association (OCA)",
|
||||||
|
"license": "LGPL-3",
|
||||||
|
"application": False,
|
||||||
|
"installable": True,
|
||||||
|
'depends': [
|
||||||
|
'base',
|
||||||
|
],
|
||||||
|
'data': [
|
||||||
|
'demo/external_system_os_demo.xml',
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/external_system_view.xml',
|
||||||
|
],
|
||||||
|
}
|
||||||
17
base_external_system/demo/external_system_os_demo.xml
Executable file
17
base_external_system/demo/external_system_os_demo.xml
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Copyright 2017 LasLabs Inc.
|
||||||
|
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||||
|
-->
|
||||||
|
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="external_system_os" model="external.system.os">
|
||||||
|
<field name="name">Example OS Connection</field>
|
||||||
|
<field name="system_type">external.system.os</field>
|
||||||
|
<field name="remote_path">/tmp</field>
|
||||||
|
<field name="company_ids" eval="[(5, 0), (4, ref('base.main_company'))]" />
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
4
base_external_system/models/__init__.py
Normal file
4
base_external_system/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import external_system
|
||||||
|
from . import external_system_adapter
|
||||||
|
from . import external_system_os
|
||||||
125
base_external_system/models/external_system.py
Normal file
125
base_external_system/models/external_system.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2017 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class ExternalSystem(models.Model):
|
||||||
|
|
||||||
|
_name = 'external.system'
|
||||||
|
_description = 'External System'
|
||||||
|
|
||||||
|
name = fields.Char(
|
||||||
|
required=True,
|
||||||
|
help='This is the canonical (humanized) name for the system.',
|
||||||
|
)
|
||||||
|
host = fields.Char(
|
||||||
|
help='This is the domain or IP address that the system can be reached '
|
||||||
|
'at.',
|
||||||
|
)
|
||||||
|
port = fields.Integer(
|
||||||
|
help='This is the port number that the system is listening on.',
|
||||||
|
)
|
||||||
|
username = fields.Char(
|
||||||
|
help='This is the username that is used for authenticating to this '
|
||||||
|
'system, if applicable.',
|
||||||
|
)
|
||||||
|
password = fields.Char(
|
||||||
|
help='This is the password that is used for authenticating to this '
|
||||||
|
'system, if applicable.',
|
||||||
|
)
|
||||||
|
private_key = fields.Text(
|
||||||
|
help='This is the private key that is used for authenticating to '
|
||||||
|
'this system, if applicable.',
|
||||||
|
)
|
||||||
|
private_key_password = fields.Text(
|
||||||
|
help='This is the password to unlock the private key that was '
|
||||||
|
'provided for this sytem.',
|
||||||
|
)
|
||||||
|
fingerprint = fields.Text(
|
||||||
|
help='This is the fingerprint that is advertised by this system in '
|
||||||
|
'order to validate its identity.',
|
||||||
|
)
|
||||||
|
ignore_fingerprint = fields.Boolean(
|
||||||
|
default=True,
|
||||||
|
help='Set this to `True` in order to ignore an invalid/unknown '
|
||||||
|
'fingerprint from the system.',
|
||||||
|
)
|
||||||
|
remote_path = fields.Char(
|
||||||
|
help='Restrict to this directory path on the remote, if applicable.',
|
||||||
|
)
|
||||||
|
company_ids = fields.Many2many(
|
||||||
|
string='Companies',
|
||||||
|
comodel_name='res.company',
|
||||||
|
required=True,
|
||||||
|
default=lambda s: [(6, 0, s.env.user.company_id.ids)],
|
||||||
|
help='Access to this system is restricted to these companies.',
|
||||||
|
)
|
||||||
|
system_type = fields.Selection(
|
||||||
|
selection='_get_system_types',
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
interface = fields.Reference(
|
||||||
|
selection='_get_system_types',
|
||||||
|
readonly=True,
|
||||||
|
help='This is the interface that this system represents. It is '
|
||||||
|
'created automatically upon creation of the external system.',
|
||||||
|
)
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('name_uniq', 'UNIQUE(name)', 'Connection name must be unique.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_system_types(self):
|
||||||
|
"""Return the adapter interface models that are installed."""
|
||||||
|
adapter = self.env['external.system.adapter']
|
||||||
|
return [
|
||||||
|
(m, self.env[m]._description) for m in adapter._inherit_children
|
||||||
|
]
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
@api.constrains('fingerprint', 'ignore_fingerprint')
|
||||||
|
def check_fingerprint_ignore_fingerprint(self):
|
||||||
|
"""Do not allow a blank fingerprint if not set to ignore."""
|
||||||
|
for record in self:
|
||||||
|
if not record.ignore_fingerprint and not record.fingerprint:
|
||||||
|
raise ValidationError(_(
|
||||||
|
'Fingerprint cannot be empty when Ignore Fingerprint is '
|
||||||
|
'not checked.',
|
||||||
|
))
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
@contextmanager
|
||||||
|
def client(self):
|
||||||
|
"""Client object usable as a context manager to include destruction.
|
||||||
|
|
||||||
|
Yields the result from ``external_get_client``, then calls
|
||||||
|
``external_destroy_client`` to cleanup the client.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
mixed: An object representing the client connection to the remote
|
||||||
|
system.
|
||||||
|
"""
|
||||||
|
with self.interface.client() as client:
|
||||||
|
yield client
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
"""Create the interface for the record and assign to ``interface``."""
|
||||||
|
record = super(ExternalSystem, self).create(vals)
|
||||||
|
interface = self.env[vals['system_type']].create({
|
||||||
|
'system_id': record.id,
|
||||||
|
})
|
||||||
|
record.interface = interface
|
||||||
|
return record
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def action_test_connection(self):
|
||||||
|
"""Test the connection to the external system."""
|
||||||
|
self.ensure_one()
|
||||||
|
self.interface.external_test_connection()
|
||||||
71
base_external_system/models/external_system_adapter.py
Normal file
71
base_external_system/models/external_system_adapter.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2017 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
class ExternalSystemAdapter(models.AbstractModel):
|
||||||
|
"""This is the model that should be inherited for new external systems.
|
||||||
|
|
||||||
|
Methods provided are prefixed with ``external_`` in order to keep from
|
||||||
|
"""
|
||||||
|
|
||||||
|
_name = 'external.system.adapter'
|
||||||
|
_description = 'External System Adapter'
|
||||||
|
_inherits = {'external.system': 'system_id'}
|
||||||
|
|
||||||
|
system_id = fields.Many2one(
|
||||||
|
string='System',
|
||||||
|
comodel_name='external.system',
|
||||||
|
required=True,
|
||||||
|
ondelete='cascade',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
@contextmanager
|
||||||
|
def client(self):
|
||||||
|
"""Client object usable as a context manager to include destruction.
|
||||||
|
|
||||||
|
Yields the result from ``external_get_client``, then calls
|
||||||
|
``external_destroy_client`` to cleanup the client.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
mixed: An object representing the client connection to the remote
|
||||||
|
system.
|
||||||
|
"""
|
||||||
|
client = self.external_get_client()
|
||||||
|
try:
|
||||||
|
yield client
|
||||||
|
finally:
|
||||||
|
self.external_destroy_client(client)
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def external_get_client(self):
|
||||||
|
"""Return a usable client representing the remote system."""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def external_destroy_client(self, client):
|
||||||
|
"""Perform any logic necessary to destroy the client connection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client (mixed): The client that was returned by
|
||||||
|
``external_get_client``.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def external_test_connection(self):
|
||||||
|
"""Adapters should override this method, then call super if valid.
|
||||||
|
|
||||||
|
If the connection is invalid, adapters should raise an instance of
|
||||||
|
``odoo.ValidationError``.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
odoo.exceptions.UserError: In the event of a good connection.
|
||||||
|
"""
|
||||||
|
raise UserError(_('The connection was a success.'))
|
||||||
43
base_external_system/models/external_system_os.py
Normal file
43
base_external_system/models/external_system_os.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2017 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
|
||||||
|
class ExternalSystemOs(models.Model):
|
||||||
|
"""This is an Interface implementing the OS module.
|
||||||
|
|
||||||
|
For the most part, this is just a sample of how to implement an external
|
||||||
|
system interface. This is still a fully usable implementation, however.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_name = 'external.system.os'
|
||||||
|
_inherit = 'external.system.adapter'
|
||||||
|
_description = 'External System OS'
|
||||||
|
|
||||||
|
previous_dir = None
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def external_get_client(self):
|
||||||
|
"""Return a usable client representing the remote system."""
|
||||||
|
super(ExternalSystemOs, self).external_get_client()
|
||||||
|
if self.system_id.remote_path:
|
||||||
|
self.previous_dir = os.getcwd()
|
||||||
|
os.chdir(self.system_id.remote_path)
|
||||||
|
return os
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def external_destroy_client(self, client):
|
||||||
|
"""Perform any logic necessary to destroy the client connection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client (mixed): The client that was returned by
|
||||||
|
``external_get_client``.
|
||||||
|
"""
|
||||||
|
super(ExternalSystemOs, self).external_destroy_client(client)
|
||||||
|
if self.previous_dir:
|
||||||
|
os.chdir(self.previous_dir)
|
||||||
|
self.previous_dir = None
|
||||||
3
base_external_system/security/ir.model.access.csv
Normal file
3
base_external_system/security/ir.model.access.csv
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_external_system_os_admin,access_external_system_os_admin,model_external_system_os,base.group_system,1,1,1,1
|
||||||
|
access_external_system_admin,access_external_system_admin,model_external_system,base.group_system,1,1,1,1
|
||||||
|
BIN
base_external_system/static/description/icon.png
Normal file
BIN
base_external_system/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
5
base_external_system/tests/__init__.py
Normal file
5
base_external_system/tests/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import test_external_system
|
||||||
|
from . import test_external_system_adapter
|
||||||
|
from . import test_external_system_os
|
||||||
22
base_external_system/tests/common.py
Normal file
22
base_external_system/tests/common.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2017 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from mock import MagicMock
|
||||||
|
|
||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
|
|
||||||
|
class Common(TransactionCase):
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def _mock_method(self, method_name, method_obj=None):
|
||||||
|
if method_obj is None:
|
||||||
|
method_obj = self.record
|
||||||
|
magic = MagicMock()
|
||||||
|
method_obj._patch_method(method_name, magic)
|
||||||
|
try:
|
||||||
|
yield magic
|
||||||
|
finally:
|
||||||
|
method_obj._revert_method(method_name)
|
||||||
54
base_external_system/tests/test_external_system.py
Normal file
54
base_external_system/tests/test_external_system.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2017 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||||
|
|
||||||
|
from odoo.exceptions import UserError, ValidationError
|
||||||
|
|
||||||
|
from .common import Common
|
||||||
|
|
||||||
|
|
||||||
|
class TestExternalSystem(Common):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestExternalSystem, self).setUp()
|
||||||
|
self.record = self.env.ref('base_external_system.external_system_os')
|
||||||
|
|
||||||
|
def test_get_system_types(self):
|
||||||
|
"""It should return at least the test record's interface."""
|
||||||
|
self.assertIn(
|
||||||
|
(self.record._name, self.record._description),
|
||||||
|
self.env['external.system']._get_system_types(),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_check_fingerprint_blank(self):
|
||||||
|
"""It should not allow blank fingerprints when checking enabled."""
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
self.record.write({
|
||||||
|
'ignore_fingerprint': False,
|
||||||
|
'fingerprint': False,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_check_fingerprint_allowed(self):
|
||||||
|
"""It should not raise a validation error if there is a fingerprint."""
|
||||||
|
self.record.write({
|
||||||
|
'ignore_fingerprint': False,
|
||||||
|
'fingerprint': 'Data',
|
||||||
|
})
|
||||||
|
self.assertTrue(True)
|
||||||
|
|
||||||
|
def test_client(self):
|
||||||
|
"""It should yield the open interface client."""
|
||||||
|
with self._mock_method('client', self.record) as magic:
|
||||||
|
with self.record.system_id.client() as client:
|
||||||
|
self.assertEqual(client, magic().__enter__())
|
||||||
|
|
||||||
|
def test_create_creates_and_assigns_interface(self):
|
||||||
|
"""It should create and assign the interface on record create."""
|
||||||
|
self.assertEqual(
|
||||||
|
self.record.interface._name, 'external.system.os',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_action_test_connection(self):
|
||||||
|
"""It should proxy to the interface connection tester."""
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
self.record.system_id.action_test_connection()
|
||||||
45
base_external_system/tests/test_external_system_adapter.py
Normal file
45
base_external_system/tests/test_external_system_adapter.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2017 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||||
|
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
from .common import Common
|
||||||
|
|
||||||
|
|
||||||
|
class TestExternalSystemAdapter(Common):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestExternalSystemAdapter, self).setUp()
|
||||||
|
self.system = self.env.ref('base_external_system.external_system_os')
|
||||||
|
self.record = self.env['external.system.adapter'].new({
|
||||||
|
'system_id': self.system.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_client_yields_client(self):
|
||||||
|
"""It should yield the client."""
|
||||||
|
with self._mock_method('external_get_client') as magic:
|
||||||
|
with self.record.client() as client:
|
||||||
|
self.assertEqual(client, magic())
|
||||||
|
|
||||||
|
def test_client_destroys_client(self):
|
||||||
|
"""It should destroy the client after use."""
|
||||||
|
with self._mock_method('external_destroy_client') as magic:
|
||||||
|
with self.record.client() as client:
|
||||||
|
self.assertFalse(magic.call_count)
|
||||||
|
magic.assert_called_once_with(client)
|
||||||
|
|
||||||
|
def test_external_get_client_ensure_one(self):
|
||||||
|
"""It should assert singletons."""
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.env['external.system.adapter'].external_get_client()
|
||||||
|
|
||||||
|
def test_external_destroy_client_ensure_one(self):
|
||||||
|
"""It should assert singletons."""
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self.env['external.system.adapter'].external_destroy_client(None)
|
||||||
|
|
||||||
|
def test_external_test_connection(self):
|
||||||
|
"""It should raise a UserError."""
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
self.record.external_test_connection()
|
||||||
40
base_external_system/tests/test_external_system_os.py
Normal file
40
base_external_system/tests/test_external_system_os.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2017 LasLabs Inc.
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from .common import Common
|
||||||
|
|
||||||
|
|
||||||
|
class TestExternalSystemOs(Common):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
"""Remember the working dir, just in case."""
|
||||||
|
super(TestExternalSystemOs, cls).setUpClass()
|
||||||
|
cls.working_dir = os.getcwd()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
"""Set the working dir back to origin, just in case."""
|
||||||
|
super(TestExternalSystemOs, cls).tearDownClass()
|
||||||
|
os.chdir(cls.working_dir)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestExternalSystemOs, self).setUp()
|
||||||
|
self.record = self.env.ref('base_external_system.external_system_os')
|
||||||
|
|
||||||
|
def test_external_get_client_returns_os(self):
|
||||||
|
"""It should return the Pyhton OS module."""
|
||||||
|
self.assertEqual(self.record.external_get_client(), os)
|
||||||
|
|
||||||
|
def test_external_get_client_changes_directories(self):
|
||||||
|
"""It should change to the proper directory."""
|
||||||
|
self.record.external_get_client()
|
||||||
|
self.assertEqual(os.getcwd(), self.record.remote_path)
|
||||||
|
|
||||||
|
def test_external_destroy_client_changes_directory(self):
|
||||||
|
"""It should change back to the previous working directory."""
|
||||||
|
self.record.external_destroy_client(None)
|
||||||
|
self.assertEqual(os.getcwd(), self.working_dir)
|
||||||
108
base_external_system/views/external_system_view.xml
Executable file
108
base_external_system/views/external_system_view.xml
Executable file
@@ -0,0 +1,108 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Copyright 2017 LasLabs Inc.
|
||||||
|
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||||
|
-->
|
||||||
|
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="external_system_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">external.system.view.form</field>
|
||||||
|
<field name="model">external.system</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="External System">
|
||||||
|
<header>
|
||||||
|
<button name="action_test_connection"
|
||||||
|
type="object"
|
||||||
|
string="Test Connection" />
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group name="data">
|
||||||
|
<group name="group_server_data">
|
||||||
|
<field name="name" />
|
||||||
|
<field name="company_ids" widget="many2many_tags" />
|
||||||
|
<field name="remote_path" />
|
||||||
|
<field name="ignore_fingerprint" />
|
||||||
|
</group>
|
||||||
|
<group name="group_connection_data">
|
||||||
|
<field name="host" />
|
||||||
|
<field name="port" />
|
||||||
|
<field name="username" />
|
||||||
|
<field name="password" widget="password" />
|
||||||
|
<field name="system_type" />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Keys">
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="private_key" />
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="fingerprint" />
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
<footer />
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="external_system_view_tree" model="ir.ui.view">
|
||||||
|
<field name="name">external.system.view.tree</field>
|
||||||
|
<field name="model">external.system</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="External Systems">
|
||||||
|
<field name="name" />
|
||||||
|
<field name="host" />
|
||||||
|
<field name="port" />
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="external_system_view_search" model="ir.ui.view">
|
||||||
|
<field name="name">external.system.view.search</field>
|
||||||
|
<field name="model">external.system</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="External Systems">
|
||||||
|
|
||||||
|
<field name="name" />
|
||||||
|
<field name="company_ids" />
|
||||||
|
<field name="host" />
|
||||||
|
<field name="port" />
|
||||||
|
<field name="username" />
|
||||||
|
|
||||||
|
<group expand="0" string="Group By">
|
||||||
|
<filter string="Host"
|
||||||
|
domain=""
|
||||||
|
context="{'group_by': 'host'}" />
|
||||||
|
<filter string="Port"
|
||||||
|
domain=""
|
||||||
|
context="{'group_by': 'port'}" />
|
||||||
|
<filter string="Username"
|
||||||
|
domain=""
|
||||||
|
context="{'group_by': 'username'}" />
|
||||||
|
</group>
|
||||||
|
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="external_system_action" model="ir.actions.act_window">
|
||||||
|
<field name="name">External Systems</field>
|
||||||
|
<field name="res_model">external.system</field>
|
||||||
|
<field name="type">ir.actions.act_window</field>
|
||||||
|
<field name="view_type">form</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_external_system"
|
||||||
|
name="External Systems"
|
||||||
|
parent="base.menu_custom"
|
||||||
|
action="external_system_action"
|
||||||
|
sequence="50" />
|
||||||
|
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user