Skip to content

Commit 7abd05e

Browse files
committed
[IMP] base_external_system: make system extendable
1 parent f427d86 commit 7abd05e

20 files changed

Lines changed: 428 additions & 240 deletions

base_external_system/README.rst

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Base External System
1111
!! This file is generated by oca-gen-addon-readme !!
1212
!! changes will be overwritten. !!
1313
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
14-
!! source digest: sha256:2a506ad3e94464097ed6d1a165abccb0f8151ad22ebfa20bf7aa521d308dff01
14+
!! source digest: sha256:3e86769a6d488f4c2debd3cb5f076fa41da5b207404fab56d493718f66ba2473
1515
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1616
1717
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
@@ -50,15 +50,23 @@ Configuration
5050

5151
Configure external systems in Settings => Technical => External Systems
5252

53+
|Try me on Runbot|
54+
55+
.. |Try me on Runbot| image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
56+
:target: https://runbot.odoo-community.org/runbot/149/10.0
57+
5358
Usage
5459
=====
5560

61+
Implementation
62+
--------------
63+
5664
The credentials for systems are stored in the ``external.system`` model,
5765
and are to be configured by the user. This model is the unified
5866
interface for the underlying adapters.
5967

6068
Using the Interface
61-
-------------------
69+
~~~~~~~~~~~~~~~~~~~
6270

6371
Given an ``external.system`` singleton called ``external_system``, you
6472
would do the following to get the underlying system client:
@@ -77,14 +85,38 @@ implemented in other modules, such as the file system interface in
7785
`OCA/server-tools/external_file_location <https://github.com/OCA/server-tools/tree/9.0/external_file_location>`__.
7886

7987
Creating an Adapter
80-
-------------------
88+
~~~~~~~~~~~~~~~~~~~
8189

8290
Modules looking to add an external system adapter should inherit the
8391
``external.system.adapter`` model and override the following methods:
8492

85-
- ``external_get_client``: Returns a usable client for the system
86-
- ``external_destroy_client``: Destroy the connection, if applicable.
87-
Does not need to be defined if the connection destroys itself.
93+
- ``external_get_client``: Returns a usable client for the system
94+
- ``external_destroy_client``: Destroy the connection, if applicable.
95+
Does not need to be defined if the connection destroys itself.
96+
97+
Changelog
98+
=========
99+
100+
Version 18.0.1.0.0
101+
==================
102+
103+
Migration to 18.0
104+
105+
- All data will now be stored in the external.system model.
106+
- Interface adapters can more easily be extended with functionality.
107+
- Activated clients can be passed information from the main
108+
external.system model.
109+
110+
Version 16.0.1.0.0
111+
==================
112+
113+
Migration to 16.0
114+
115+
Version 10.0.2.0.0
116+
------------------
117+
118+
Model base.external.system will be the only regular model, containing
119+
all the data
88120

89121
Bug Tracker
90122
===========
@@ -103,16 +135,21 @@ Authors
103135
-------
104136

105137
* LasLabs
138+
* Therp BV
106139

107140
Contributors
108141
------------
109142

110-
- Dave Lasley <dave@laslabs.com>
111-
- Ronald Portier <ronald@therp.nl>
112-
- `Tecnativa <https://www.tecnativa.com>`__:
143+
- Dave Lasley dave@laslabs.com
144+
145+
- Therp BV https://therp.nl:
146+
147+
- Ronald Portier ronald@therp.nl
113148

114-
- Alexandre Díaz
115-
- César A. Sánchez
149+
- Tecnativa https://www.tecnativa.com:
150+
151+
- Alexandre Díaz
152+
- César A. Sánchez
116153

117154
Maintainers
118155
-----------
@@ -127,6 +164,14 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose
127164
mission is to support the collaborative development of Odoo features and
128165
promote its widespread use.
129166

167+
.. |maintainer-NL66278| image:: https://github.com/NL66278.png?size=40px
168+
:target: https://github.com/NL66278
169+
:alt: NL66278
170+
171+
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
172+
173+
|maintainer-NL66278|
174+
130175
This module is part of the `OCA/server-backend <https://github.com/OCA/server-backend/tree/18.0/base_external_system>`_ project on GitHub.
131176

132177
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

base_external_system/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# Copyright 2017 LasLabs Inc.
21
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
32

43
from . import models

base_external_system/__manifest__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Copyright 2017 LasLabs Inc.
2+
# Copyright 2024 Therp BV <https://therp.nl>.
23
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
34

45
{
@@ -7,7 +8,8 @@
78
"version": "18.0.1.0.0",
89
"category": "Base",
910
"website": "https://github.com/OCA/server-backend",
10-
"author": "LasLabs, " "Odoo Community Association (OCA)",
11+
"author": "LasLabs, Therp BV, Odoo Community Association (OCA)",
12+
"maintainers": ["NL66278"],
1113
"license": "LGPL-3",
1214
"application": False,
1315
"installable": True,
@@ -16,5 +18,6 @@
1618
"demo/external_system_os_demo.xml",
1719
"security/ir.model.access.csv",
1820
"views/external_system_view.xml",
21+
"views/ir_ui_menu.xml",
1922
],
2023
}

base_external_system/demo/external_system_os_demo.xml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
<?xml version="1.0" encoding="utf-8" ?>
22
<!--
33
Copyright 2017 LasLabs Inc.
4+
Copyright 2023 Therp BV.
45
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
56
-->
67
<odoo>
7-
<record id="external_system_os" model="external.system.os">
8-
<field name="name">Example OS Connection</field>
9-
<field name="system_type">external.system.os</field>
8+
<record id="external_system_os_demo" model="external.system">
9+
<field name="name">Example OS Connection for Testing</field>
10+
<field name="system_type">external.system.adapter.os</field>
1011
<field name="remote_path">/tmp</field>
1112
<field name="company_ids" eval="[(5, 0), (4, ref('base.main_company'))]" />
1213
</record>
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
12
from . import external_system
23
from . import external_system_adapter
3-
from . import external_system_os
4+
from . import external_system_adapter_os
Lines changed: 86 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Copyright 2017 LasLabs Inc.
2+
# Copyright 2023-2024 Therp BV.
23
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
34

45
from contextlib import contextmanager
@@ -12,42 +13,47 @@ class ExternalSystem(models.Model):
1213
_description = "External System"
1314

1415
name = fields.Char(
15-
required=True,
16-
help="This is the canonical (humanized) name for the system.",
16+
required=True, help="This is the canonical (humanized) name for the system."
17+
)
18+
scheme = fields.Char(
19+
help="This is the protocol used to communicate with the system."
1720
)
1821
host = fields.Char(
19-
help="This is the domain or IP address that the system can be reached " "at.",
22+
help="This is the domain or IP address that the system can be reached at."
2023
)
2124
port = fields.Integer(
22-
help="This is the port number that the system is listening on.",
25+
help="This is the port number that the system is listening on."
2326
)
2427
username = fields.Char(
25-
help="This is the username that is used for authenticating to this "
26-
"system, if applicable.",
28+
help="This is the username that is used for authenticating to this"
29+
" system, if applicable."
2730
)
2831
password = fields.Char(
29-
help="This is the password that is used for authenticating to this "
30-
"system, if applicable.",
32+
help="This is the password that is used for authenticating to this"
33+
" system, if applicable."
3134
)
3235
private_key = fields.Text(
33-
help="This is the private key that is used for authenticating to "
34-
"this system, if applicable.",
36+
help="This is the private key that is used for authenticating to"
37+
" this system, if applicable."
3538
)
3639
private_key_password = fields.Text(
37-
help="This is the password to unlock the private key that was "
38-
"provided for this sytem.",
40+
help="This is the password to unlock the private key that was"
41+
" provided for this sytem."
42+
)
43+
private_key_thumbprint = fields.Text(
44+
help="The thumbprint generated by AAD when you upload your public cert"
3945
)
4046
fingerprint = fields.Text(
41-
help="This is the fingerprint that is advertised by this system in "
42-
"order to validate its identity.",
47+
help="This is the fingerprint that is advertised by this system in"
48+
" order to validate its identity."
4349
)
4450
ignore_fingerprint = fields.Boolean(
4551
default=True,
46-
help="Set this to `True` in order to ignore an invalid/unknown "
47-
"fingerprint from the system.",
52+
help="Set this to `True` in order to ignore an invalid/unknown"
53+
" fingerprint from the system.",
4854
)
4955
remote_path = fields.Char(
50-
help="Restrict to this directory path on the remote, if applicable.",
56+
help="Restrict to this directory path on the remote, if applicable."
5157
)
5258
company_ids = fields.Many2many(
5359
string="Companies",
@@ -57,25 +63,29 @@ class ExternalSystem(models.Model):
5763
help="Access to this system is restricted to these companies.",
5864
)
5965
system_type = fields.Selection(
60-
selection="_get_system_types",
66+
# Use lambda selection, otherwise subclasses loaded later will not be found.
67+
selection=lambda self: self._get_system_types(),
6168
required=True,
6269
)
63-
interface = fields.Reference(
64-
selection="_get_system_types",
65-
readonly=True,
66-
help="This is the interface that this system represents. It is "
67-
"created automatically upon creation of the external system.",
68-
)
6970

7071
_sql_constraints = [
71-
("name_uniq", "UNIQUE(name)", "Connection name must be unique."),
72+
("name_uniq", "UNIQUE(name)", "Connection name must be unique.")
7273
]
7374

7475
@api.model
7576
def _get_system_types(self):
7677
"""Return the adapter interface models that are installed."""
7778
adapter = self.env["external.system.adapter"]
78-
return [(m, self.env[m]._description) for m in adapter._inherit_children]
79+
subclasses = set()
80+
work = [adapter]
81+
while work:
82+
parent = work.pop()
83+
for child in parent._inherit_children:
84+
subclass = self.env[child]
85+
if subclass not in subclasses:
86+
subclasses.add(subclass)
87+
work.append(subclass)
88+
return [(m._name, m._description) for m in subclasses]
7989

8090
@api.constrains("fingerprint", "ignore_fingerprint")
8191
def check_fingerprint_ignore_fingerprint(self):
@@ -84,8 +94,8 @@ def check_fingerprint_ignore_fingerprint(self):
8494
if not record.ignore_fingerprint and not record.fingerprint:
8595
raise ValidationError(
8696
_(
87-
"Fingerprint cannot be empty when Ignore Fingerprint is "
88-
"not checked.",
97+
"Fingerprint cannot be empty when Ignore Fingerprint is"
98+
" not checked."
8999
)
90100
)
91101

@@ -100,24 +110,55 @@ def client(self):
100110
mixed: An object representing the client connection to the remote
101111
system.
102112
"""
103-
with self.interface.client() as client:
113+
self.ensure_one()
114+
adapter = None
115+
client = None
116+
try:
117+
adapter = self._get_adapter()
118+
client = adapter.external_get_client()
104119
yield client
105-
106-
@api.model_create_multi
107-
def create(self, vals_list):
108-
"""Create the interface for the record and assign to ``interface``."""
109-
records = self.browse([])
110-
for vals in vals_list:
111-
record = super().create(vals)
112-
if not self.env.context.get("no_create_interface"):
113-
interface = self.env[vals["system_type"]].create(
114-
{"system_id": record.id}
115-
)
116-
record.interface = interface
117-
records += record
118-
return records
120+
finally:
121+
if client:
122+
adapter.external_destroy_client(client)
119123

120124
def action_test_connection(self):
121-
"""Test the connection to the external system."""
125+
"""Test the connection to the external system.
126+
127+
Any unexpected exception will be transformed into a
128+
ValidationError. A ValidationError will also be raised
129+
if no client is returned.
130+
"""
122131
self.ensure_one()
123-
self.interface.external_test_connection()
132+
try:
133+
with self.client() as client:
134+
if client is None:
135+
raise ValidationError(
136+
_("Client connection failed for system %s") % self.name
137+
)
138+
return True
139+
except Exception as exc:
140+
raise ValidationError(
141+
_(
142+
"Unexpected error %(exception)s"
143+
" when connecting to %(system_name)s"
144+
)
145+
% {
146+
"exception": exc,
147+
"system_name": self.name,
148+
}
149+
) from None
150+
151+
def _get_adapter(self):
152+
"""Trivial method to get adapter from system type.
153+
154+
Adding the system to the context allows the AbstractModel for the adapter,
155+
that can not be instantiated, to still hold information, through a class
156+
property, that can be accessed by all methods in derived classes.
157+
158+
An alternative would be to use standard python classes, but that would take
159+
away the possibility to extend them in the Odoo way.
160+
161+
To even further extend the possibility of the adapter to have its own
162+
runtime memory, we add an (at first) empty dictionary to the context.
163+
"""
164+
return self.with_context(system=self, adapter_memory={}).env[self.system_type]

0 commit comments

Comments
 (0)