Browse Source
[ADD] base_external_system: Implement interface/adapter (#993)
[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 notepull/1035/head
Dave Lasley
7 years ago
committed by
GitHub
16 changed files with 661 additions and 0 deletions
-
96base_external_system/README.rst
-
5base_external_system/__init__.py
-
23base_external_system/__manifest__.py
-
17base_external_system/demo/external_system_os_demo.xml
-
4base_external_system/models/__init__.py
-
125base_external_system/models/external_system.py
-
71base_external_system/models/external_system_adapter.py
-
43base_external_system/models/external_system_os.py
-
3base_external_system/security/ir.model.access.csv
-
BINbase_external_system/static/description/icon.png
-
5base_external_system/tests/__init__.py
-
22base_external_system/tests/common.py
-
54base_external_system/tests/test_external_system.py
-
45base_external_system/tests/test_external_system_adapter.py
-
40base_external_system/tests/test_external_system_os.py
-
108base_external_system/views/external_system_view.xml
@ -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. |
@ -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 |
@ -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', |
||||
|
], |
||||
|
} |
@ -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> |
@ -0,0 +1,4 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
from . import external_system |
||||
|
from . import external_system_adapter |
||||
|
from . import external_system_os |
@ -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() |
@ -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.')) |
@ -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 |
@ -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 |
After Width: 128 | Height: 128 | Size: 9.2 KiB |
@ -0,0 +1,5 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
|
||||
|
from . import test_external_system |
||||
|
from . import test_external_system_adapter |
||||
|
from . import test_external_system_os |
@ -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) |
@ -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() |
@ -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() |
@ -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) |
@ -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> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue