Merge pull request #13 from guewen/11.0-server-env-mixin
server env: add fallback on database default values
This commit is contained in:
commit
71d75f2e79
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Mail configuration with server_environment',
|
'name': 'Mail configuration with server_environment',
|
||||||
'version': '11.0.1.0.0',
|
'version': '11.0.1.1.0',
|
||||||
'category': 'Tools',
|
'category': 'Tools',
|
||||||
'summary': 'Configure mail servers with server_environment_files',
|
'summary': 'Configure mail servers with server_environment_files',
|
||||||
'author': "Camptocamp, Odoo Community Association (OCA)",
|
'author': "Camptocamp, Odoo Community Association (OCA)",
|
||||||
|
|
@ -13,7 +13,5 @@
|
||||||
'fetchmail',
|
'fetchmail',
|
||||||
'server_environment',
|
'server_environment',
|
||||||
],
|
],
|
||||||
'data': [
|
'data': [],
|
||||||
'views/fetchmail_server_views.xml',
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,58 +3,38 @@
|
||||||
|
|
||||||
import operator
|
import operator
|
||||||
from odoo import api, fields, models
|
from odoo import api, fields, models
|
||||||
from odoo.addons.server_environment import serv_config
|
|
||||||
|
|
||||||
|
|
||||||
class FetchmailServer(models.Model):
|
class FetchmailServer(models.Model):
|
||||||
"""Incoming POP/IMAP mail server account"""
|
"""Incoming POP/IMAP mail server account"""
|
||||||
_inherit = 'fetchmail.server'
|
_name = 'fetchmail.server'
|
||||||
|
_inherit = ["fetchmail.server", "server.env.mixin"]
|
||||||
|
|
||||||
server = fields.Char(compute='_compute_server_env',
|
@property
|
||||||
states={})
|
def _server_env_fields(self):
|
||||||
port = fields.Integer(compute='_compute_server_env',
|
base_fields = super()._server_env_fields
|
||||||
states={})
|
mail_fields = {
|
||||||
type = fields.Selection(compute='_compute_server_env',
|
"server": {},
|
||||||
search='_search_type',
|
"port": {},
|
||||||
states={})
|
"type": {},
|
||||||
user = fields.Char(compute='_compute_server_env',
|
"user": {},
|
||||||
states={})
|
"password": {},
|
||||||
password = fields.Char(compute='_compute_server_env',
|
"is_ssl": {},
|
||||||
states={})
|
"attach": {},
|
||||||
is_ssl = fields.Boolean(compute='_compute_server_env')
|
"original": {},
|
||||||
attach = fields.Boolean(compute='_compute_server_env')
|
}
|
||||||
original = fields.Boolean(compute='_compute_server_env')
|
mail_fields.update(base_fields)
|
||||||
|
return mail_fields
|
||||||
|
|
||||||
@api.depends()
|
type = fields.Selection(search='_search_type')
|
||||||
def _compute_server_env(self):
|
|
||||||
for fetchmail in self:
|
|
||||||
global_section_name = 'incoming_mail'
|
|
||||||
|
|
||||||
key_types = {'port': int,
|
@api.model
|
||||||
'is_ssl': lambda a: bool(int(a or 0)),
|
def _server_env_global_section_name(self):
|
||||||
'attach': lambda a: bool(int(a or 0)),
|
"""Name of the global section in the configuration files
|
||||||
'original': lambda a: bool(int(a or 0)),
|
|
||||||
}
|
|
||||||
|
|
||||||
# default vals
|
Can be customized in your model
|
||||||
config_vals = {'port': 993,
|
"""
|
||||||
'is_ssl': 0,
|
return 'incoming_mail'
|
||||||
'attach': 0,
|
|
||||||
'original': 0,
|
|
||||||
}
|
|
||||||
if serv_config.has_section(global_section_name):
|
|
||||||
config_vals.update(serv_config.items(global_section_name))
|
|
||||||
|
|
||||||
custom_section_name = '.'.join((global_section_name,
|
|
||||||
fetchmail.name))
|
|
||||||
if serv_config.has_section(custom_section_name):
|
|
||||||
config_vals.update(serv_config.items(custom_section_name))
|
|
||||||
|
|
||||||
for key, to_type in key_types.items():
|
|
||||||
if config_vals.get(key):
|
|
||||||
config_vals[key] = to_type(config_vals[key])
|
|
||||||
|
|
||||||
fetchmail.update(config_vals)
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _search_type(self, oper, value):
|
def _search_type(self, oper, value):
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,30 @@
|
||||||
# Copyright 2012-2018 Camptocamp SA
|
# Copyright 2012-2018 Camptocamp SA
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
from odoo import api, fields, models
|
from odoo import api, models
|
||||||
from odoo.addons.server_environment import serv_config
|
|
||||||
|
|
||||||
|
|
||||||
class IrMailServer(models.Model):
|
class IrMailServer(models.Model):
|
||||||
_inherit = "ir.mail_server"
|
_name = "ir.mail_server"
|
||||||
|
_inherit = ["ir.mail_server", "server.env.mixin"]
|
||||||
|
|
||||||
smtp_host = fields.Char(compute='_compute_server_env',
|
@property
|
||||||
required=False,
|
def _server_env_fields(self):
|
||||||
readonly=True)
|
base_fields = super()._server_env_fields
|
||||||
smtp_port = fields.Integer(compute='_compute_server_env',
|
mail_fields = {
|
||||||
required=False,
|
"smtp_host": {},
|
||||||
readonly=True)
|
"smtp_port": {},
|
||||||
smtp_user = fields.Char(compute='_compute_server_env',
|
"smtp_user": {},
|
||||||
required=False,
|
"smtp_pass": {},
|
||||||
readonly=True)
|
"smtp_encryption": {},
|
||||||
smtp_pass = fields.Char(compute='_compute_server_env',
|
}
|
||||||
required=False,
|
mail_fields.update(base_fields)
|
||||||
readonly=True)
|
return mail_fields
|
||||||
smtp_encryption = fields.Selection(compute='_compute_server_env',
|
|
||||||
required=False,
|
|
||||||
readonly=True)
|
|
||||||
|
|
||||||
@api.depends()
|
@api.model
|
||||||
def _compute_server_env(self):
|
def _server_env_global_section_name(self):
|
||||||
for server in self:
|
"""Name of the global section in the configuration files
|
||||||
global_section_name = 'outgoing_mail'
|
|
||||||
|
|
||||||
# default vals
|
Can be customized in your model
|
||||||
config_vals = {'smtp_port': 587}
|
"""
|
||||||
if serv_config.has_section(global_section_name):
|
return 'outgoing_mail'
|
||||||
config_vals.update((serv_config.items(global_section_name)))
|
|
||||||
|
|
||||||
custom_section_name = '.'.join((global_section_name, server.name))
|
|
||||||
if serv_config.has_section(custom_section_name):
|
|
||||||
config_vals.update(serv_config.items(custom_section_name))
|
|
||||||
|
|
||||||
if config_vals.get('smtp_port'):
|
|
||||||
config_vals['smtp_port'] = int(config_vals['smtp_port'])
|
|
||||||
|
|
||||||
server.update(config_vals)
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<odoo>
|
|
||||||
|
|
||||||
<record id="inherit_fetchmail" model="ir.ui.view">
|
|
||||||
<field name="model">fetchmail.server</field>
|
|
||||||
<field name="inherit_id" ref="fetchmail.view_email_server_form"/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<field name="server" position="attributes">
|
|
||||||
<attribute name="attrs" eval="False"/>
|
|
||||||
</field>
|
|
||||||
<field name="port" position="attributes">
|
|
||||||
<attribute name="attrs" eval="False"/>
|
|
||||||
</field>
|
|
||||||
<field name="user" position="attributes">
|
|
||||||
<attribute name="attrs" eval="False"/>
|
|
||||||
</field>
|
|
||||||
<field name="password" position="attributes">
|
|
||||||
<attribute name="attrs" eval="False"/>
|
|
||||||
</field>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
</odoo>
|
|
||||||
|
|
@ -3,17 +3,20 @@
|
||||||
:alt: License: GPL-3
|
:alt: License: GPL-3
|
||||||
|
|
||||||
==================
|
==================
|
||||||
server environment
|
Server Environment
|
||||||
==================
|
==================
|
||||||
|
|
||||||
This module provides a way to define an environment in the main Odoo
|
This module provides a way to define an environment in the main Odoo
|
||||||
configuration file and to read some configurations from files
|
configuration file and to read some configurations from files
|
||||||
depending on the configured environment: you define the environment in
|
depending on the configured environment: you define the environment in
|
||||||
the main configuration file, and the values for the various possible
|
the main configuration file, and the values for the various possible
|
||||||
environments are stored in the `server_environment_files` companion
|
environments are stored in the ``server_environment_files`` companion
|
||||||
module.
|
module.
|
||||||
|
|
||||||
All the settings will be read only and visible under the Configuration
|
The ``server_environment_files`` module is optional, the values can be set using
|
||||||
|
an environment variable with a fallback on default values in the database.
|
||||||
|
|
||||||
|
The configuration read from the files are visible under the Configuration
|
||||||
menu. If you are not in the 'dev' environment you will not be able to
|
menu. If you are not in the 'dev' environment you will not be able to
|
||||||
see the values contained in keys named '*passw*'.
|
see the values contained in keys named '*passw*'.
|
||||||
|
|
||||||
|
|
@ -21,14 +24,14 @@ Installation
|
||||||
============
|
============
|
||||||
|
|
||||||
By itself, this module does little. See for instance the
|
By itself, this module does little. See for instance the
|
||||||
`mail_environment` addon which depends on this one to allow configuring
|
``mail_environment`` addon which depends on this one to allow configuring
|
||||||
the incoming and outgoing mail servers depending on the environment.
|
the incoming and outgoing mail servers depending on the environment.
|
||||||
|
|
||||||
To install this module, you need to provide a companion module called
|
You can store your configuration values in a companion module called
|
||||||
`server_environment_files`. You can copy and customize the provided
|
``server_environment_files``. You can copy and customize the provided
|
||||||
`server_environment_files_sample` module for this purpose.
|
``server_environment_files_sample`` module for this purpose. Alternatively, you
|
||||||
You can provide additional options in environment variables
|
can provide them in environment variables ``SERVER_ENV_CONFIG`` and
|
||||||
``SERVER_ENV_CONFIG`` and ``SERVER_ENV_CONFIG_SECRET``.
|
``SERVER_ENV_CONFIG_SECRET``.
|
||||||
|
|
||||||
|
|
||||||
Configuration
|
Configuration
|
||||||
|
|
@ -65,7 +68,7 @@ Environment variable
|
||||||
You can define configuration in the environment variable ``SERVER_ENV_CONFIG``
|
You can define configuration in the environment variable ``SERVER_ENV_CONFIG``
|
||||||
and/or ``SERVER_ENV_CONFIG_SECRET``. The 2 variables are handled the exact same
|
and/or ``SERVER_ENV_CONFIG_SECRET``. The 2 variables are handled the exact same
|
||||||
way, this is only a convenience for the deployment where you can isolate the
|
way, this is only a convenience for the deployment where you can isolate the
|
||||||
secrets in a different, encrypted, file. This is a multi-line environment variable
|
secrets in a different, encrypted, file. They are multi-line environment variables
|
||||||
in the same configparser format than the files.
|
in the same configparser format than the files.
|
||||||
If you used options in ``server_environment_files``, the options set in the
|
If you used options in ``server_environment_files``, the options set in the
|
||||||
environment variable overrides them.
|
environment variable overrides them.
|
||||||
|
|
@ -75,7 +78,6 @@ the content of the variable must be set accordingly to the running environment.
|
||||||
|
|
||||||
Example of setup:
|
Example of setup:
|
||||||
|
|
||||||
|
|
||||||
A public file, containing that will contain public variables::
|
A public file, containing that will contain public variables::
|
||||||
|
|
||||||
# These variables are not odoo standard variables,
|
# These variables are not odoo standard variables,
|
||||||
|
|
@ -106,17 +108,44 @@ A second file which is encrypted and contains secrets::
|
||||||
sftp_password=xxxxxxxxx
|
sftp_password=xxxxxxxxx
|
||||||
"
|
"
|
||||||
|
|
||||||
|
Default values
|
||||||
|
--------------
|
||||||
|
|
||||||
|
When using the ``server.env.mixin`` mixin, for each env-computed field, a
|
||||||
|
companion field ``<field>_env_default`` is created. This field is not
|
||||||
|
environment-dependent. It's a fallback value used when no key is set in
|
||||||
|
configuration files / environment variable.
|
||||||
|
|
||||||
|
When the default field is used, the field is made editable on Odoo.
|
||||||
|
|
||||||
|
Note: empty environment keys always take precedence over default fields
|
||||||
|
|
||||||
|
|
||||||
|
Keychain integration
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
Read the documentation of the class `models/server_env_mixin.py
|
||||||
|
<models/server_env_mixin.py>`_.
|
||||||
|
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
=====
|
=====
|
||||||
|
|
||||||
To use this module, in your code, you can follow this example::
|
You can include a mixin in your model and configure the env-computed fields
|
||||||
|
by an override of ``_server_env_fields``.
|
||||||
|
|
||||||
from openerp.addons.server_environment import serv_config
|
::
|
||||||
for key, value in serv_config.items('external_service.ftp'):
|
|
||||||
print (key, value)
|
|
||||||
|
|
||||||
serv_config.get('external_service.ftp', 'tls')
|
class StorageBackend(models.Model):
|
||||||
|
_name = "storage.backend"
|
||||||
|
_inherit = ["storage.backend", "server.env.mixin"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _server_env_fields(self):
|
||||||
|
return {"directory_path": {}}
|
||||||
|
|
||||||
|
Read the documentation of the class and methods in `models/server_env_mixin.py
|
||||||
|
<models/server_env_mixin.py>`__.
|
||||||
|
|
||||||
|
|
||||||
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
||||||
|
|
|
||||||
|
|
@ -17,4 +17,11 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
from . import models
|
||||||
|
# TODO when migrating to 12, fix the import of serv_config by renaming the
|
||||||
|
# file?
|
||||||
|
# Add an alias to access to the 'serv_config' module as it is shadowed
|
||||||
|
# in the following line by an import of a variable with the same name.
|
||||||
|
# We can't change the import of serv_config for backward compatibility.
|
||||||
|
from . import serv_config as server_env
|
||||||
from .serv_config import serv_config, setboolean
|
from .serv_config import serv_config, setboolean
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,11 @@
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "server configuration environment files",
|
"name": "server configuration environment files",
|
||||||
"version": "11.0.1.0.1",
|
"version": "11.0.2.0.0",
|
||||||
"depends": ["base"],
|
"depends": [
|
||||||
|
"base",
|
||||||
|
"base_sparse_field",
|
||||||
|
],
|
||||||
"author": "Camptocamp,Odoo Community Association (OCA)",
|
"author": "Camptocamp,Odoo Community Association (OCA)",
|
||||||
"summary": "move some configurations out of the database",
|
"summary": "move some configurations out of the database",
|
||||||
"website": "http://odoo-community.org/",
|
"website": "http://odoo-community.org/",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import server_env_mixin
|
||||||
|
|
@ -0,0 +1,408 @@
|
||||||
|
# Copyright 2018 Camptocamp (https://www.camptocamp.com).
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from functools import partialmethod
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
from ..serv_config import serv_config
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ServerEnvMixin(models.AbstractModel):
|
||||||
|
"""Mixin to add server environment in existing models
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
class StorageBackend(models.Model):
|
||||||
|
_name = "storage.backend"
|
||||||
|
_inherit = ["storage.backend", "server.env.mixin"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _server_env_fields(self):
|
||||||
|
return {"directory_path": {}}
|
||||||
|
|
||||||
|
With the snippet above, the "storage.backend" model now uses a server
|
||||||
|
environment configuration for the field ``directory_path``.
|
||||||
|
|
||||||
|
Under the hood, this mixin automatically replaces the original field
|
||||||
|
by an env-computed field that reads from the configuration files.
|
||||||
|
|
||||||
|
By default, it looks for the configuration in a section named
|
||||||
|
``[model_name.Record Name]`` where ``model_name`` is the ``_name`` of the
|
||||||
|
model with ``.`` replaced by ``_``. Then in a global section which is only
|
||||||
|
the name of the model. They can be customized by overriding the method
|
||||||
|
:meth:`~_server_env_section_name` and
|
||||||
|
:meth:`~_server_env_global_section_name`.
|
||||||
|
|
||||||
|
For each field transformed to an env-computed field, a companion field
|
||||||
|
``<field>_env_default`` is automatically created. When its value is set
|
||||||
|
and the configuration files do not contain a key for that field, the
|
||||||
|
env-computed field uses the default value stored in database. If there is a
|
||||||
|
key for this field but it is empty, the env-computed field has an empty
|
||||||
|
value.
|
||||||
|
|
||||||
|
Env-computed fields are conditionally editable, based on the absence
|
||||||
|
of their key in environment configuration files. When edited, their
|
||||||
|
value is stored in the database.
|
||||||
|
|
||||||
|
Integration with keychain
|
||||||
|
-------------------------
|
||||||
|
The keychain addon is used account information, encrypting the password
|
||||||
|
with a key per environment.
|
||||||
|
|
||||||
|
The default behavior of server_environment is to store the default fields
|
||||||
|
in a serialized field, so the password would lend there unencrypted.
|
||||||
|
|
||||||
|
You can benefit from keychain by using custom compute/inverse methods to
|
||||||
|
get/set the password field:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
class StorageBackend(models.Model):
|
||||||
|
_name = 'storage.backend'
|
||||||
|
_inherit = ['keychain.backend', 'collection.base']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _server_env_fields(self):
|
||||||
|
base_fields = super()._server_env_fields
|
||||||
|
sftp_fields = {
|
||||||
|
"sftp_server": {},
|
||||||
|
"sftp_port": {},
|
||||||
|
"sftp_login": {},
|
||||||
|
"sftp_password": {
|
||||||
|
"no_default_field": True,
|
||||||
|
"compute_default": "_compute_password",
|
||||||
|
"inverse_default": "_inverse_password",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sftp_fields.update(base_fields)
|
||||||
|
return sftp_fields
|
||||||
|
|
||||||
|
* ``no_default_field`` means that no new (sparse) field need to be
|
||||||
|
created, it already is provided by keychain
|
||||||
|
* ``compute_default`` is the name of the compute method to get the default
|
||||||
|
value when no key is set in the configuration files.
|
||||||
|
``_compute_password`` is implemented by ``keychain.backend``.
|
||||||
|
* ``inverse_default`` is the name of the compute method to set the default
|
||||||
|
value when it is editable. ``_inverse_password`` is implemented by
|
||||||
|
``keychain.backend``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
_name = 'server.env.mixin'
|
||||||
|
|
||||||
|
server_env_defaults = fields.Serialized()
|
||||||
|
|
||||||
|
_server_env_getter_mapping = {
|
||||||
|
'integer': 'getint',
|
||||||
|
'float': 'getfloat',
|
||||||
|
'monetary': 'getfloat',
|
||||||
|
'boolean': 'getboolean',
|
||||||
|
'char': 'get',
|
||||||
|
'selection': 'get',
|
||||||
|
'text': 'get',
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _server_env_fields(self):
|
||||||
|
"""Dict of fields to replace by fields computed from env
|
||||||
|
|
||||||
|
To override in models. The dictionary is:
|
||||||
|
{'name_of_the_field': options}
|
||||||
|
|
||||||
|
Where ``options`` is a dictionary::
|
||||||
|
|
||||||
|
options = {
|
||||||
|
"getter": "getint",
|
||||||
|
"no_default_field": True,
|
||||||
|
"compute_default": "_compute_password",
|
||||||
|
"inverse_default": "_inverse_password",
|
||||||
|
}
|
||||||
|
|
||||||
|
* ``getter``: The configparser getter can be one of: get, getboolean,
|
||||||
|
getint, getfloat. The getter is automatically inferred from the
|
||||||
|
type of the field, so it shouldn't generally be needed to set it.
|
||||||
|
* ``no_default_field``: disable creation of a field for storing
|
||||||
|
the default value, must be used with ``compute_default`` and
|
||||||
|
``inverse_default``
|
||||||
|
* ``compute_default``: name of a compute method to get the default
|
||||||
|
value when no key is present in configuration files
|
||||||
|
* ``inverse_default``: name of an inverse method to set the default
|
||||||
|
value when the value is editable
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _server_env_fields(self):
|
||||||
|
base_fields = super()._server_env_fields
|
||||||
|
sftp_fields = {
|
||||||
|
"sftp_server": {},
|
||||||
|
"sftp_port": {},
|
||||||
|
"sftp_login": {},
|
||||||
|
"sftp_password": {},
|
||||||
|
}
|
||||||
|
sftp_fields.update(base_fields)
|
||||||
|
return sftp_fields
|
||||||
|
"""
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _server_env_global_section_name(self):
|
||||||
|
"""Name of the global section in the configuration files
|
||||||
|
|
||||||
|
Can be customized in your model
|
||||||
|
"""
|
||||||
|
return self._name.replace(".", "_")
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _server_env_section_name(self):
|
||||||
|
"""Name of the section in the configuration files
|
||||||
|
|
||||||
|
Can be customized in your model
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
base = self._server_env_global_section_name()
|
||||||
|
return ".".join((base, self.name))
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _server_env_read_from_config(self, field_name, config_getter):
|
||||||
|
self.ensure_one()
|
||||||
|
global_section_name = self._server_env_global_section_name()
|
||||||
|
section_name = self._server_env_section_name()
|
||||||
|
try:
|
||||||
|
# at this point we should have checked that we have a key with
|
||||||
|
# _server_env_has_key_defined so we are sure that the value is
|
||||||
|
# either in the global or the record config
|
||||||
|
getter = getattr(serv_config, config_getter)
|
||||||
|
if (section_name in serv_config
|
||||||
|
and field_name in serv_config[section_name]):
|
||||||
|
value = getter(section_name, field_name)
|
||||||
|
else:
|
||||||
|
value = getter(global_section_name, field_name)
|
||||||
|
except Exception:
|
||||||
|
_logger.exception(
|
||||||
|
"error trying to read field %s in section %s",
|
||||||
|
field_name,
|
||||||
|
section_name,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
return value
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _server_env_has_key_defined(self, field_name):
|
||||||
|
self.ensure_one()
|
||||||
|
global_section_name = self._server_env_global_section_name()
|
||||||
|
section_name = self._server_env_section_name()
|
||||||
|
has_global_config = (
|
||||||
|
global_section_name in serv_config
|
||||||
|
and field_name in serv_config[global_section_name]
|
||||||
|
)
|
||||||
|
has_config = (
|
||||||
|
section_name in serv_config
|
||||||
|
and field_name in serv_config[section_name]
|
||||||
|
)
|
||||||
|
return has_global_config or has_config
|
||||||
|
|
||||||
|
def _compute_server_env_from_config(self, field_name, options):
|
||||||
|
getter_name = options.get('getter') if options else None
|
||||||
|
if not getter_name:
|
||||||
|
field_type = self._fields[field_name].type
|
||||||
|
getter_name = self._server_env_getter_mapping.get(field_type)
|
||||||
|
if not getter_name:
|
||||||
|
# if you get this message and the field is working as expected,
|
||||||
|
# you may want to add the type in _server_env_getter_mapping
|
||||||
|
_logger.warning('server.env.mixin is used on a field of type %s, '
|
||||||
|
'which may not be supported properly')
|
||||||
|
getter_name = 'get'
|
||||||
|
value = self._server_env_read_from_config(
|
||||||
|
field_name, getter_name
|
||||||
|
)
|
||||||
|
self[field_name] = value
|
||||||
|
|
||||||
|
def _compute_server_env_from_default(self, field_name, options):
|
||||||
|
if options and options.get('compute_default'):
|
||||||
|
getattr(self, options['compute_default'])()
|
||||||
|
else:
|
||||||
|
default_field = self._server_env_default_fieldname(
|
||||||
|
field_name
|
||||||
|
)
|
||||||
|
if default_field:
|
||||||
|
self[field_name] = self[default_field]
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _compute_server_env(self):
|
||||||
|
"""Read values from environment configuration files
|
||||||
|
|
||||||
|
If an env-computed field has no key in configuration files,
|
||||||
|
read from the ``<field>_env_default`` field from database.
|
||||||
|
"""
|
||||||
|
for record in self:
|
||||||
|
for field_name, options in self._server_env_fields.items():
|
||||||
|
if record._server_env_has_key_defined(field_name):
|
||||||
|
record._compute_server_env_from_config(field_name, options)
|
||||||
|
|
||||||
|
else:
|
||||||
|
record._compute_server_env_from_default(
|
||||||
|
field_name, options
|
||||||
|
)
|
||||||
|
|
||||||
|
def _inverse_server_env(self, field_name):
|
||||||
|
options = self._server_env_fields[field_name]
|
||||||
|
default_field = self._server_env_default_fieldname(field_name)
|
||||||
|
is_editable_field = self._server_env_is_editable_fieldname(field_name)
|
||||||
|
|
||||||
|
for record in self:
|
||||||
|
# when we write in an env-computed field, if it is editable
|
||||||
|
# we update the default value in database
|
||||||
|
|
||||||
|
if record[is_editable_field]:
|
||||||
|
if options and options.get('inverse_default'):
|
||||||
|
getattr(record, options['inverse_default'])()
|
||||||
|
elif default_field:
|
||||||
|
record[default_field] = record[field_name]
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _compute_server_env_is_editable(self):
|
||||||
|
"""Compute <field>_is_editable values
|
||||||
|
|
||||||
|
We can edit an env-computed filed only if there is no key
|
||||||
|
in any environment configuration file. If there is an empty
|
||||||
|
key, it's an empty value so we can't edit the env-computed field.
|
||||||
|
"""
|
||||||
|
# we can't group it with _compute_server_env otherwise when called
|
||||||
|
# in ``_inverse_server_env`` it would reset the value of the field
|
||||||
|
for record in self:
|
||||||
|
for field_name in self._server_env_fields:
|
||||||
|
is_editable_field = self._server_env_is_editable_fieldname(
|
||||||
|
field_name
|
||||||
|
)
|
||||||
|
is_editable = not record._server_env_has_key_defined(
|
||||||
|
field_name
|
||||||
|
)
|
||||||
|
record[is_editable_field] = is_editable
|
||||||
|
|
||||||
|
def _server_env_view_set_readonly(self, view_arch):
|
||||||
|
field_xpath = './/field[@name="%s"]'
|
||||||
|
for field in self._server_env_fields:
|
||||||
|
is_editable_field = self._server_env_is_editable_fieldname(field)
|
||||||
|
for elem in view_arch.findall(field_xpath % field):
|
||||||
|
# set env-computed fields to readonly if the configuration
|
||||||
|
# files have a key set for this field
|
||||||
|
elem.set('attrs',
|
||||||
|
str({'readonly': [(is_editable_field, '=', False)]}))
|
||||||
|
if not view_arch.findall(field_xpath % is_editable_field):
|
||||||
|
# add the _is_editable fields in the view for the 'attrs'
|
||||||
|
# domain
|
||||||
|
view_arch.append(
|
||||||
|
etree.Element(
|
||||||
|
'field',
|
||||||
|
name=is_editable_field,
|
||||||
|
invisible="1"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return view_arch
|
||||||
|
|
||||||
|
def _fields_view_get(self, view_id=None, view_type='form', toolbar=False,
|
||||||
|
submenu=False):
|
||||||
|
view_data = super()._fields_view_get(
|
||||||
|
view_id=view_id, view_type=view_type,
|
||||||
|
toolbar=toolbar, submenu=submenu
|
||||||
|
)
|
||||||
|
view_arch = etree.fromstring(view_data['arch'].encode('utf-8'))
|
||||||
|
view_arch = self._server_env_view_set_readonly(view_arch)
|
||||||
|
view_data['arch'] = etree.tostring(view_arch, encoding='unicode')
|
||||||
|
return view_data
|
||||||
|
|
||||||
|
def _server_env_default_fieldname(self, base_field_name):
|
||||||
|
"""Return the name of the field with default value"""
|
||||||
|
options = self._server_env_fields[base_field_name]
|
||||||
|
if options and options.get('no_default_field'):
|
||||||
|
return ''
|
||||||
|
return '%s_env_default' % (base_field_name,)
|
||||||
|
|
||||||
|
def _server_env_is_editable_fieldname(self, base_field_name):
|
||||||
|
"""Return the name of the field for "is editable"
|
||||||
|
|
||||||
|
This is the field used to tell if the env-computed field can
|
||||||
|
be edited.
|
||||||
|
"""
|
||||||
|
return '%s_env_is_editable' % (base_field_name,)
|
||||||
|
|
||||||
|
def _server_env_transform_field_to_read_from_env(self, field):
|
||||||
|
"""Transform the original field in a computed field"""
|
||||||
|
field.compute = '_compute_server_env'
|
||||||
|
|
||||||
|
inverse_method_name = '_inverse_server_env_%s' % field.name
|
||||||
|
inverse_method = partialmethod(
|
||||||
|
type(self)._inverse_server_env, field.name
|
||||||
|
)
|
||||||
|
setattr(type(self), inverse_method_name, inverse_method)
|
||||||
|
field.inverse = inverse_method_name
|
||||||
|
field.store = False
|
||||||
|
field.required = False
|
||||||
|
field.copy = False
|
||||||
|
field.sparse = None
|
||||||
|
field.prefetch = False
|
||||||
|
|
||||||
|
def _server_env_add_is_editable_field(self, base_field):
|
||||||
|
"""Add a field indicating if we can edit the env-computed fields
|
||||||
|
|
||||||
|
It is used in the inverse function of the env-computed field
|
||||||
|
and in the views to add 'readonly' on the fields.
|
||||||
|
"""
|
||||||
|
fieldname = self._server_env_is_editable_fieldname(base_field.name)
|
||||||
|
# if the field is inherited, it's a related to its delegated model
|
||||||
|
# (inherits), we want to override it with a new one
|
||||||
|
if fieldname not in self._fields or self._fields[fieldname].inherited:
|
||||||
|
field = fields.Boolean(
|
||||||
|
compute='_compute_server_env_is_editable',
|
||||||
|
automatic=True,
|
||||||
|
# this is required to be able to edit fields
|
||||||
|
# on new records
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
self._add_field(fieldname, field)
|
||||||
|
|
||||||
|
def _server_env_add_default_field(self, base_field):
|
||||||
|
"""Add a field storing the default value
|
||||||
|
|
||||||
|
The default value is used when there is no key for an env-computed
|
||||||
|
field in the configuration files.
|
||||||
|
|
||||||
|
The field is a sparse field stored in the serialized (json) field
|
||||||
|
``server_env_defaults``.
|
||||||
|
"""
|
||||||
|
fieldname = self._server_env_default_fieldname(base_field.name)
|
||||||
|
if not fieldname:
|
||||||
|
return
|
||||||
|
# if the field is inherited, it's a related to its delegated model
|
||||||
|
# (inherits), we want to override it with a new one
|
||||||
|
if fieldname not in self._fields or self._fields[fieldname].inherited:
|
||||||
|
base_field_cls = base_field.__class__
|
||||||
|
field_args = base_field.args.copy()
|
||||||
|
field_args.pop('_sequence', None)
|
||||||
|
field_args.update({
|
||||||
|
'sparse': 'server_env_defaults',
|
||||||
|
'automatic': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
if hasattr(base_field, 'selection'):
|
||||||
|
field_args['selection'] = base_field.selection
|
||||||
|
field = base_field_cls(**field_args)
|
||||||
|
self._add_field(fieldname, field)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _setup_base(self):
|
||||||
|
super()._setup_base()
|
||||||
|
for fieldname in self._server_env_fields:
|
||||||
|
field = self._fields[fieldname]
|
||||||
|
self._server_env_add_default_field(field)
|
||||||
|
self._server_env_transform_field_to_read_from_env(field)
|
||||||
|
self._server_env_add_is_editable_field(field)
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
#
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import configparser
|
import configparser
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
@ -28,8 +29,15 @@ from odoo.tools.config import config as system_base_config
|
||||||
|
|
||||||
from .system_info import get_server_environment
|
from .system_info import get_server_environment
|
||||||
|
|
||||||
from odoo.addons import server_environment_files
|
_logger = logging.getLogger(__name__)
|
||||||
_dir = os.path.dirname(server_environment_files.__file__)
|
|
||||||
|
try:
|
||||||
|
from odoo.addons import server_environment_files
|
||||||
|
_dir = os.path.dirname(server_environment_files.__file__)
|
||||||
|
except ImportError:
|
||||||
|
_logger.info('not using server_environment_files for configuration,'
|
||||||
|
' no directory found')
|
||||||
|
_dir = None
|
||||||
|
|
||||||
ENV_VAR_NAMES = ('SERVER_ENV_CONFIG', 'SERVER_ENV_CONFIG_SECRET')
|
ENV_VAR_NAMES = ('SERVER_ENV_CONFIG', 'SERVER_ENV_CONFIG_SECRET')
|
||||||
|
|
||||||
|
|
@ -46,13 +54,15 @@ if not system_base_config.get('running_env', False):
|
||||||
"[options]\nrunning_env = dev"
|
"[options]\nrunning_env = dev"
|
||||||
)
|
)
|
||||||
|
|
||||||
ck_path = os.path.join(_dir, system_base_config['running_env'])
|
ck_path = None
|
||||||
|
if _dir:
|
||||||
|
ck_path = os.path.join(_dir, system_base_config['running_env'])
|
||||||
|
|
||||||
if not os.path.exists(ck_path):
|
if not os.path.exists(ck_path):
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"Provided server environment does not exist, "
|
"Provided server environment does not exist, "
|
||||||
"please add a folder %s" % ck_path
|
"please add a folder %s" % ck_path
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setboolean(obj, attr, _bool=None):
|
def setboolean(obj, attr, _bool=None):
|
||||||
|
|
@ -82,8 +92,7 @@ def _listconf(env_path):
|
||||||
return files
|
return files
|
||||||
|
|
||||||
|
|
||||||
def _load_config():
|
def _load_config_from_server_env_files(config_p):
|
||||||
"""Load the configuration and return a ConfigParser instance."""
|
|
||||||
default = os.path.join(_dir, 'default')
|
default = os.path.join(_dir, 'default')
|
||||||
running_env = os.path.join(_dir,
|
running_env = os.path.join(_dir,
|
||||||
system_base_config['running_env'])
|
system_base_config['running_env'])
|
||||||
|
|
@ -92,17 +101,18 @@ def _load_config():
|
||||||
else:
|
else:
|
||||||
conf_files = _listconf(running_env)
|
conf_files = _listconf(running_env)
|
||||||
|
|
||||||
config_p = configparser.SafeConfigParser()
|
|
||||||
# options are case-sensitive
|
|
||||||
config_p.optionxform = str
|
|
||||||
try:
|
try:
|
||||||
config_p.read(conf_files)
|
config_p.read(conf_files)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception('Cannot read config files "%s": %s' % (conf_files, e))
|
raise Exception('Cannot read config files "%s": %s' % (conf_files, e))
|
||||||
|
|
||||||
|
|
||||||
|
def _load_config_from_rcfile(config_p):
|
||||||
config_p.read(system_base_config.rcfile)
|
config_p.read(system_base_config.rcfile)
|
||||||
config_p.remove_section('options')
|
config_p.remove_section('options')
|
||||||
|
|
||||||
|
|
||||||
|
def _load_config_from_env(config_p):
|
||||||
for varname in ENV_VAR_NAMES:
|
for varname in ENV_VAR_NAMES:
|
||||||
env_config = os.getenv(varname)
|
env_config = os.getenv(varname)
|
||||||
if env_config:
|
if env_config:
|
||||||
|
|
@ -114,6 +124,17 @@ def _load_config():
|
||||||
% (varname, err,)
|
% (varname, err,)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_config():
|
||||||
|
"""Load the configuration and return a ConfigParser instance."""
|
||||||
|
config_p = configparser.SafeConfigParser()
|
||||||
|
# options are case-sensitive
|
||||||
|
config_p.optionxform = str
|
||||||
|
|
||||||
|
if _dir:
|
||||||
|
_load_config_from_server_env_files(config_p)
|
||||||
|
_load_config_from_rcfile(config_p)
|
||||||
|
_load_config_from_env(config_p)
|
||||||
return config_p
|
return config_p
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,3 +18,4 @@
|
||||||
#
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
from . import test_server_environment
|
from . import test_server_environment
|
||||||
|
from . import test_environment_variable
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Copyright 2018 Camptocamp (https://www.camptocamp.com).
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
import os
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from odoo.tests import common
|
||||||
|
from odoo.addons.server_environment import server_env
|
||||||
|
from odoo.tools.config import config
|
||||||
|
|
||||||
|
import odoo.addons.server_environment.models.server_env_mixin as \
|
||||||
|
server_env_mixin
|
||||||
|
|
||||||
|
|
||||||
|
class ServerEnvironmentCase(common.SavepointCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self._original_running_env = config.get('running_env')
|
||||||
|
config['running_env'] = 'testing'
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
config['running_env'] = self._original_running_env
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def set_config_dir(self, path):
|
||||||
|
original_dir = server_env._dir
|
||||||
|
if path and not os.path.isabs(path):
|
||||||
|
path = os.path.join(os.path.dirname(__file__,), path)
|
||||||
|
server_env._dir = path
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
server_env._dir = original_dir
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def set_env_variables(self, public=None, secret=None):
|
||||||
|
newkeys = {}
|
||||||
|
if public:
|
||||||
|
newkeys['SERVER_ENV_CONFIG'] = public
|
||||||
|
if secret:
|
||||||
|
newkeys['SERVER_ENV_CONFIG_SECRET'] = secret
|
||||||
|
with patch.dict('os.environ', newkeys):
|
||||||
|
yield
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def load_config(self, public=None, secret=None):
|
||||||
|
original_serv_config = server_env_mixin.serv_config
|
||||||
|
try:
|
||||||
|
with self.set_config_dir(None), \
|
||||||
|
self.set_env_variables(public, secret):
|
||||||
|
parser = server_env._load_config()
|
||||||
|
server_env_mixin.serv_config = parser
|
||||||
|
yield
|
||||||
|
|
||||||
|
finally:
|
||||||
|
server_env_mixin.serv_config = original_serv_config
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Copyright 2018 Camptocamp (https://www.camptocamp.com).
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
|
||||||
|
from odoo.addons.server_environment import server_env
|
||||||
|
from .common import ServerEnvironmentCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnvironmentVariables(ServerEnvironmentCase):
|
||||||
|
|
||||||
|
def test_env_variables(self):
|
||||||
|
public = (
|
||||||
|
"[section]\n"
|
||||||
|
"foo=bar\n"
|
||||||
|
"bar=baz\n"
|
||||||
|
)
|
||||||
|
secret = (
|
||||||
|
"[section]\n"
|
||||||
|
"bar=foo\n"
|
||||||
|
"alice=bob\n"
|
||||||
|
)
|
||||||
|
with self.set_config_dir(None), \
|
||||||
|
self.set_env_variables(public, secret):
|
||||||
|
parser = server_env._load_config()
|
||||||
|
self.assertEqual(
|
||||||
|
list(parser.keys()),
|
||||||
|
['DEFAULT', 'section']
|
||||||
|
)
|
||||||
|
self.assertDictEqual(
|
||||||
|
dict(parser['section'].items()),
|
||||||
|
{'alice': 'bob',
|
||||||
|
'bar': 'foo',
|
||||||
|
'foo': 'bar'}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_env_variables_override(self):
|
||||||
|
public = (
|
||||||
|
"[external_service.ftp]\n"
|
||||||
|
"user=foo\n"
|
||||||
|
)
|
||||||
|
with self.set_config_dir('testfiles'), \
|
||||||
|
self.set_env_variables(public):
|
||||||
|
parser = server_env._load_config()
|
||||||
|
val = parser.get('external_service.ftp', 'user')
|
||||||
|
self.assertEqual(val, 'foo')
|
||||||
|
|
@ -17,11 +17,11 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
from odoo.tests import common
|
from odoo.addons.server_environment import server_env
|
||||||
from odoo.addons.server_environment import serv_config
|
from . import common
|
||||||
|
|
||||||
|
|
||||||
class TestEnv(common.TransactionCase):
|
class TestEnv(common.ServerEnvironmentCase):
|
||||||
|
|
||||||
def test_view(self):
|
def test_view(self):
|
||||||
model = self.env['server.config']
|
model = self.env['server.config']
|
||||||
|
|
@ -43,5 +43,9 @@ class TestEnv(common.TransactionCase):
|
||||||
self.assertTrue(pass_checked)
|
self.assertTrue(pass_checked)
|
||||||
|
|
||||||
def test_value_retrival(self):
|
def test_value_retrival(self):
|
||||||
val = serv_config.get('external_service.ftp', 'user')
|
with self.set_config_dir('testfiles'):
|
||||||
self.assertEqual(val, 'toto')
|
parser = server_env._load_config()
|
||||||
|
val = parser.get('external_service.ftp', 'user')
|
||||||
|
self.assertEqual(val, 'testing')
|
||||||
|
val = parser.get('external_service.ftp', 'host')
|
||||||
|
self.assertEqual(val, 'sftp.example.com')
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
[external_service.ftp]
|
||||||
|
host = sftp.example.com
|
||||||
|
user = foo
|
||||||
|
password = bar
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
[external_service.ftp]
|
||||||
|
user = testing
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
|
||||||
|
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||||
|
:alt: License: AGPL-3
|
||||||
|
|
||||||
|
=======================
|
||||||
|
Test Server Environment
|
||||||
|
=======================
|
||||||
|
|
||||||
|
This addon is not meant to be used. It extends the Odoo Models
|
||||||
|
in order to run automated tests on the Server Environment module.
|
||||||
|
|
||||||
|
Same basic tests are integrated within the ``server_environment`` addon.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
=====
|
||||||
|
|
||||||
|
This module only contains Python tests.
|
||||||
|
|
||||||
|
Credits
|
||||||
|
=======
|
||||||
|
|
||||||
|
Images
|
||||||
|
------
|
||||||
|
|
||||||
|
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
|
||||||
|
|
||||||
|
Contributors
|
||||||
|
------------
|
||||||
|
|
||||||
|
* Guewen Baconnier <guewen.baconnier@camptocamp.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 @@
|
||||||
|
from . import models
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Copyright 2018 Camptocamp (https://www.camptocamp.com).
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Automated tests for server environment - technical",
|
||||||
|
"summary": "Used to run automated tests, do not install",
|
||||||
|
"version": "11.0.1.0.0",
|
||||||
|
"depends": [
|
||||||
|
"server_environment",
|
||||||
|
],
|
||||||
|
"author": "Camptocamp,Odoo Community Association (OCA)",
|
||||||
|
"website": "http://odoo-community.org/",
|
||||||
|
"license": "AGPL-3",
|
||||||
|
"category": "Tools",
|
||||||
|
"data": [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import server_env_test
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
# Copyright 2018 Camptocamp (https://www.camptocamp.com).
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
""" Models used for testing server_environment
|
||||||
|
|
||||||
|
Create models that will be used in tests.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class ServerEnvTest(models.Model):
|
||||||
|
_name = 'server.env.test'
|
||||||
|
_description = 'Server Environment Test Model'
|
||||||
|
|
||||||
|
name = fields.Char(required=True)
|
||||||
|
# if the original field is required, it must not
|
||||||
|
# be required anymore as we set it with config
|
||||||
|
host = fields.Char(required=True)
|
||||||
|
port = fields.Integer()
|
||||||
|
user = fields.Char()
|
||||||
|
password = fields.Char()
|
||||||
|
ssl = fields.Boolean()
|
||||||
|
|
||||||
|
# we'll use these ones to stress the custom
|
||||||
|
# compute/inverse for the default value
|
||||||
|
alias = fields.Char()
|
||||||
|
alias_default = fields.Char()
|
||||||
|
|
||||||
|
|
||||||
|
# Intentionally re-declares a class to stress the inclusion of the mixin
|
||||||
|
class ServerEnvTestWithMixin(models.Model):
|
||||||
|
_name = 'server.env.test'
|
||||||
|
_inherit = ['server.env.test', 'server.env.mixin']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _server_env_fields(self):
|
||||||
|
base_fields = super()._server_env_fields
|
||||||
|
sftp_fields = {
|
||||||
|
"host": {},
|
||||||
|
"port": {},
|
||||||
|
"user": {},
|
||||||
|
"password": {},
|
||||||
|
"ssl": {},
|
||||||
|
"alias": {
|
||||||
|
"no_default_field": True,
|
||||||
|
"compute_default": "_compute_alias_default",
|
||||||
|
"inverse_default": "_inverse_alias_default",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sftp_fields.update(base_fields)
|
||||||
|
return sftp_fields
|
||||||
|
|
||||||
|
def _compute_alias_default(self):
|
||||||
|
for record in self:
|
||||||
|
record.alias = record.alias_default
|
||||||
|
|
||||||
|
def _inverse_alias_default(self):
|
||||||
|
for record in self:
|
||||||
|
record.alias_default = record.alias
|
||||||
|
|
||||||
|
|
||||||
|
class ServerEnvTest2(models.Model):
|
||||||
|
_name = 'server.env.test2'
|
||||||
|
_description = 'Server Environment Test Model 2'
|
||||||
|
# applied directly on the model
|
||||||
|
_inherit = 'server.env.mixin'
|
||||||
|
|
||||||
|
name = fields.Char(required=True)
|
||||||
|
host = fields.Char()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _server_env_fields(self):
|
||||||
|
base_fields = super()._server_env_fields
|
||||||
|
sftp_fields = {
|
||||||
|
"host": {},
|
||||||
|
}
|
||||||
|
sftp_fields.update(base_fields)
|
||||||
|
return sftp_fields
|
||||||
|
|
||||||
|
|
||||||
|
class ServerEnvTestInherits1(models.Model):
|
||||||
|
_name = 'server.env.test.inherits1'
|
||||||
|
_description = 'Server Environment Test Model Inherits'
|
||||||
|
|
||||||
|
base_id = fields.Many2one(
|
||||||
|
comodel_name='server.env.test',
|
||||||
|
delegate=True,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
# host is not redefined, handled by the delegated model
|
||||||
|
|
||||||
|
|
||||||
|
class ServerEnvTestInherits2(models.Model):
|
||||||
|
_name = 'server.env.test.inherits2'
|
||||||
|
_description = 'Server Environment Test Model Inherits'
|
||||||
|
# if you want to benefit from mixin in an inherits,
|
||||||
|
# even if the parent includes it, you have to
|
||||||
|
# add the inheritance here as well
|
||||||
|
_inherit = 'server.env.mixin'
|
||||||
|
|
||||||
|
base_id = fields.Many2one(
|
||||||
|
comodel_name='server.env.test',
|
||||||
|
delegate=True,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
host = fields.Char()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _server_env_fields(self):
|
||||||
|
base_fields = super()._server_env_fields
|
||||||
|
sftp_fields = {
|
||||||
|
"host": {},
|
||||||
|
}
|
||||||
|
sftp_fields.update(base_fields)
|
||||||
|
return sftp_fields
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_server_env_test,access_server_env_test,model_server_env_test,,1,0,0,0
|
||||||
|
access_server_env_test2,access_server_env_test2,model_server_env_test2,,1,0,0,0
|
||||||
|
access_server_env_test_inherits1,access_server_env_test_inherits1,model_server_env_test_inherits1,,1,0,0,0
|
||||||
|
access_server_env_test_inherits2,access_server_env_test_inherits2,model_server_env_test_inherits2,,1,0,0,0
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import test_server_env_mixin
|
||||||
|
from . import test_server_env_mixin_inherit
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
# Copyright 2018 Camptocamp (https://www.camptocamp.com).
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo.addons.server_environment.tests.common import ServerEnvironmentCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestServerEnvMixin(ServerEnvironmentCase):
|
||||||
|
|
||||||
|
def test_env_computed_fields_read(self):
|
||||||
|
"""Read values from the config in env-computed fields"""
|
||||||
|
public = (
|
||||||
|
# global for all server.env.test records
|
||||||
|
"[server_env_test]\n"
|
||||||
|
"ssl=1\n"
|
||||||
|
# for our server.env.test test record now
|
||||||
|
"[server_env_test.foo]\n"
|
||||||
|
"host=test.example.com\n"
|
||||||
|
"port=21\n"
|
||||||
|
"user=foo\n"
|
||||||
|
)
|
||||||
|
secret = (
|
||||||
|
"[server_env_test.foo]\n"
|
||||||
|
"password=bar\n"
|
||||||
|
)
|
||||||
|
# we can create the record even if we didn't provide
|
||||||
|
# the field host which was required
|
||||||
|
foo = self.env['server.env.test'].create({
|
||||||
|
'name': 'foo',
|
||||||
|
})
|
||||||
|
with self.load_config(public, secret):
|
||||||
|
self.assertEqual(foo.name, 'foo')
|
||||||
|
self.assertEqual(foo.host, 'test.example.com')
|
||||||
|
self.assertEqual(foo.port, 21)
|
||||||
|
self.assertEqual(foo.user, 'foo')
|
||||||
|
self.assertEqual(foo.password, 'bar')
|
||||||
|
self.assertTrue(foo.ssl)
|
||||||
|
|
||||||
|
def test_env_computed_fields_read_multi(self):
|
||||||
|
"""Read values in env-computed fields on several records"""
|
||||||
|
public = (
|
||||||
|
"[server_env_test]\n"
|
||||||
|
"host=test.example.com\n"
|
||||||
|
"port=21\n"
|
||||||
|
"user=foo\n"
|
||||||
|
)
|
||||||
|
# we can create the record even if we didn't provide
|
||||||
|
# the field host which was required
|
||||||
|
foo = self.env['server.env.test'].create({
|
||||||
|
'name': 'foo',
|
||||||
|
})
|
||||||
|
foo2 = self.env['server.env.test'].create({
|
||||||
|
'name': 'foo2',
|
||||||
|
})
|
||||||
|
foos = foo + foo2
|
||||||
|
with self.load_config(public):
|
||||||
|
foos._compute_server_env()
|
||||||
|
|
||||||
|
def test_env_computed_fields_write(self):
|
||||||
|
"""Env-computed fields without key in config can be written"""
|
||||||
|
public = (
|
||||||
|
# for our server.env.test test record now
|
||||||
|
"[server_env_test.foo]\n"
|
||||||
|
"host=test.example.com\n"
|
||||||
|
"port=21\n"
|
||||||
|
)
|
||||||
|
secret = (
|
||||||
|
"[server_env_test.foo]\n"
|
||||||
|
"password=bar\n"
|
||||||
|
)
|
||||||
|
# we can create the record even if we didn't provide
|
||||||
|
# the field host which was required
|
||||||
|
foo = self.env['server.env.test'].create({
|
||||||
|
'name': 'foo',
|
||||||
|
})
|
||||||
|
with self.load_config(public, secret):
|
||||||
|
self.assertEqual(foo.host, 'test.example.com')
|
||||||
|
self.assertFalse(foo.host_env_is_editable)
|
||||||
|
self.assertEqual(foo.port, 21)
|
||||||
|
self.assertFalse(foo.port_env_is_editable)
|
||||||
|
self.assertEqual(foo.password, 'bar')
|
||||||
|
self.assertFalse(foo.password_env_is_editable)
|
||||||
|
|
||||||
|
self.assertFalse(foo.user)
|
||||||
|
self.assertTrue(foo.user_env_is_editable)
|
||||||
|
self.assertFalse(foo.ssl)
|
||||||
|
self.assertTrue(foo.ssl_env_is_editable)
|
||||||
|
|
||||||
|
# field set in config, no effect
|
||||||
|
foo.host = 'new.example.com'
|
||||||
|
self.assertFalse(foo.host_env_default)
|
||||||
|
|
||||||
|
# fields not set in config, written
|
||||||
|
foo.user = 'dummy'
|
||||||
|
self.assertEqual(foo.user_env_default, 'dummy')
|
||||||
|
foo.ssl = True
|
||||||
|
self.assertTrue(foo.ssl_env_default)
|
||||||
|
|
||||||
|
def test_env_computed_default(self):
|
||||||
|
"""Env-computed fields read from default fields"""
|
||||||
|
# we can create the record even if we didn't provide
|
||||||
|
# the field host which was required
|
||||||
|
foo = self.env['server.env.test'].create({
|
||||||
|
'name': 'foo',
|
||||||
|
})
|
||||||
|
# empty files
|
||||||
|
with self.load_config():
|
||||||
|
self.assertFalse(foo.host)
|
||||||
|
self.assertFalse(foo.port)
|
||||||
|
self.assertFalse(foo.password)
|
||||||
|
self.assertFalse(foo.user)
|
||||||
|
self.assertFalse(foo.ssl)
|
||||||
|
|
||||||
|
self.assertTrue(foo.host_env_is_editable)
|
||||||
|
self.assertTrue(foo.port_env_is_editable)
|
||||||
|
self.assertTrue(foo.password_env_is_editable)
|
||||||
|
self.assertTrue(foo.user_env_is_editable)
|
||||||
|
self.assertTrue(foo.ssl_env_is_editable)
|
||||||
|
|
||||||
|
foo.write({
|
||||||
|
'host_env_default': 'test.example.com',
|
||||||
|
'port_env_default': 21,
|
||||||
|
'password_env_default': 'bar',
|
||||||
|
'user_env_default': 'foo',
|
||||||
|
'ssl_env_default': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
# refresh env-computed fields, it should read from
|
||||||
|
# the default fields
|
||||||
|
foo.invalidate_cache()
|
||||||
|
self.assertEqual(foo.host, 'test.example.com')
|
||||||
|
self.assertEqual(foo.port, 21)
|
||||||
|
self.assertEqual(foo.user, 'foo')
|
||||||
|
self.assertEqual(foo.password, 'bar')
|
||||||
|
self.assertTrue(foo.ssl)
|
||||||
|
|
||||||
|
def test_env_custom_compute_method(self):
|
||||||
|
"""Can customize compute/inverse methods"""
|
||||||
|
foo = self.env['server.env.test'].create({
|
||||||
|
'name': 'foo',
|
||||||
|
})
|
||||||
|
self.assertNotIn('alias_env_default', foo._fields)
|
||||||
|
with self.load_config():
|
||||||
|
self.assertTrue(foo.alias_env_is_editable)
|
||||||
|
|
||||||
|
foo.alias = 'test'
|
||||||
|
self.assertEqual(foo.alias_default, 'test')
|
||||||
|
|
||||||
|
foo = self.env['server.env.test'].create({
|
||||||
|
'name': 'foo_with_default',
|
||||||
|
})
|
||||||
|
with self.load_config():
|
||||||
|
self.assertTrue(foo.alias_env_is_editable)
|
||||||
|
|
||||||
|
foo.alias_default = 'new_value'
|
||||||
|
self.assertEqual(foo.alias, 'new_value')
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
# Copyright 2018 Camptocamp (https://www.camptocamp.com).
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo.addons.server_environment.tests.common import ServerEnvironmentCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestServerEnvMixinSameFieldName(ServerEnvironmentCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.public = (
|
||||||
|
# global for all server.env.test records
|
||||||
|
"[server_env_test]\n"
|
||||||
|
"host=global_value\n"
|
||||||
|
# for our server.env.test test record now
|
||||||
|
"[server_env_test.foo]\n"
|
||||||
|
"host=foo_value\n"
|
||||||
|
# for our server.env.test2 test record now
|
||||||
|
"[server_env_test2.foo]\n"
|
||||||
|
"host=foo2_value\n"
|
||||||
|
)
|
||||||
|
cls.foo = cls.env['server.env.test'].create({'name': 'foo'})
|
||||||
|
cls.foo2 = cls.env['server.env.test2'].create({
|
||||||
|
'name': 'foo',
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_env_computed_fields_read(self):
|
||||||
|
"""Read values from the config in env-computed fields"""
|
||||||
|
with self.load_config(self.public):
|
||||||
|
self.assertEqual(self.foo.name, 'foo')
|
||||||
|
self.assertEqual(self.foo2.name, 'foo')
|
||||||
|
self.assertEqual(self.foo.host, 'foo_value')
|
||||||
|
self.assertEqual(self.foo2.host, 'foo2_value')
|
||||||
|
|
||||||
|
def test_env_computed_fields_not_editable(self):
|
||||||
|
"""Env-computed fields without key in config can be written"""
|
||||||
|
# we can create the record even if we didn't provide
|
||||||
|
# the field host which was required
|
||||||
|
with self.load_config(self.public):
|
||||||
|
self.assertEqual(self.foo.host, 'foo_value')
|
||||||
|
self.assertFalse(self.foo.host_env_is_editable)
|
||||||
|
self.assertEqual(self.foo2.host, 'foo2_value')
|
||||||
|
self.assertFalse(self.foo2.host_env_is_editable)
|
||||||
|
|
||||||
|
def test_env_computed_fields_editable(self):
|
||||||
|
"""Env-computed fields without key in config can be written"""
|
||||||
|
# we can create the record even if we didn't provide
|
||||||
|
# the field host which was required
|
||||||
|
with self.load_config():
|
||||||
|
self.assertFalse(self.foo.host)
|
||||||
|
self.assertTrue(self.foo.host_env_is_editable)
|
||||||
|
self.assertFalse(self.foo2.host)
|
||||||
|
self.assertTrue(self.foo2.host_env_is_editable)
|
||||||
|
|
||||||
|
self.foo.host_env_default = 'foo_value'
|
||||||
|
self.foo.invalidate_cache()
|
||||||
|
self.assertEqual(self.foo.host, 'foo_value')
|
||||||
|
|
||||||
|
self.foo2.host_env_default = 'foo2_value'
|
||||||
|
self.foo2.invalidate_cache()
|
||||||
|
self.assertEqual(self.foo2.host, 'foo2_value')
|
||||||
|
|
||||||
|
self.foo.host = 'foo_new_value'
|
||||||
|
self.foo.invalidate_cache()
|
||||||
|
self.assertEqual(self.foo.host, 'foo_new_value')
|
||||||
|
|
||||||
|
self.foo2.host = 'foo2_new_value'
|
||||||
|
self.foo2.invalidate_cache()
|
||||||
|
self.assertEqual(self.foo2.host, 'foo2_new_value')
|
||||||
|
|
||||||
|
|
||||||
|
class TestServerEnvMixinInherits(ServerEnvironmentCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.public = (
|
||||||
|
# global for all server.env.test records
|
||||||
|
"[server_env_test]\n"
|
||||||
|
"host=global_value\n"
|
||||||
|
# for our server.env.test test record now
|
||||||
|
"[server_env_test.foo]\n"
|
||||||
|
"host=foo_value\n"
|
||||||
|
# for our server.env.test.inherits1 test record now
|
||||||
|
"[server_env_test_inherits1.foo]\n"
|
||||||
|
"host=foo_inherits_value\n"
|
||||||
|
# for our server.env.test.inherits2 test record now
|
||||||
|
"[server_env_test_inherits2.foo]\n"
|
||||||
|
"host=foo_inherits_value\n"
|
||||||
|
)
|
||||||
|
cls.foo = cls.env['server.env.test'].create({'name': 'foo'})
|
||||||
|
cls.foo_inh1 = cls.env['server.env.test.inherits1'].create({
|
||||||
|
'name': 'foo'
|
||||||
|
})
|
||||||
|
cls.foo_inh2 = cls.env['server.env.test.inherits2'].create({
|
||||||
|
'name': 'foo'
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_env_computed_fields_read(self):
|
||||||
|
"""Read values from the config in env-computed fields"""
|
||||||
|
with self.load_config(self.public):
|
||||||
|
self.assertEqual(self.foo.name, 'foo')
|
||||||
|
self.assertEqual(self.foo_inh1.name, 'foo')
|
||||||
|
self.assertEqual(self.foo_inh2.name, 'foo')
|
||||||
|
self.assertEqual(self.foo.host, 'foo_value')
|
||||||
|
# inh1 does not redefine the host field so has the
|
||||||
|
# same value than the parent record (delegate)
|
||||||
|
self.assertEqual(self.foo_inh1.host, 'foo_value')
|
||||||
|
# inh2 redefines self.the host field so has its own value
|
||||||
|
self.assertEqual(self.foo_inh2.host, 'foo_inherits_value')
|
||||||
|
|
||||||
|
def test_env_computed_fields_not_editable(self):
|
||||||
|
"""Env-computed fields without key in config can be written"""
|
||||||
|
with self.load_config(self.public):
|
||||||
|
self.assertEqual(self.foo.host, 'foo_value')
|
||||||
|
self.assertFalse(self.foo.host_env_is_editable)
|
||||||
|
self.assertEqual(self.foo_inh1.host, 'foo_value')
|
||||||
|
self.assertFalse(self.foo_inh1.host_env_is_editable)
|
||||||
|
self.assertEqual(self.foo_inh2.host, 'foo_inherits_value')
|
||||||
|
self.assertFalse(self.foo_inh2.host_env_is_editable)
|
||||||
|
|
||||||
|
def test_env_computed_fields_editable(self):
|
||||||
|
"""Env-computed fields without key in config can be written"""
|
||||||
|
with self.load_config():
|
||||||
|
self.assertFalse(self.foo.host)
|
||||||
|
self.assertTrue(self.foo.host_env_is_editable)
|
||||||
|
self.assertFalse(self.foo_inh1.host)
|
||||||
|
self.assertTrue(self.foo_inh1.host_env_is_editable)
|
||||||
|
self.assertFalse(self.foo_inh2.host)
|
||||||
|
self.assertTrue(self.foo_inh2.host_env_is_editable)
|
||||||
|
|
||||||
|
self.foo.host_env_default = 'foo_value'
|
||||||
|
self.foo.invalidate_cache()
|
||||||
|
self.assertEqual(self.foo.host, 'foo_value')
|
||||||
|
|
||||||
|
self.foo.host = 'foo_new_value'
|
||||||
|
self.foo.invalidate_cache()
|
||||||
|
self.assertEqual(self.foo.host, 'foo_new_value')
|
||||||
|
|
||||||
|
self.foo_inh1.host_env_default = 'foo2_value'
|
||||||
|
self.foo_inh1.invalidate_cache()
|
||||||
|
self.assertEqual(self.foo_inh1.host, 'foo2_value')
|
||||||
|
self.assertEqual(self.foo_inh1.base_id.host, 'foo2_value')
|
||||||
|
|
||||||
|
self.foo_inh1.host = 'foo2_new_value'
|
||||||
|
self.foo_inh1.invalidate_cache()
|
||||||
|
self.assertEqual(self.foo_inh1.host, 'foo2_new_value')
|
||||||
|
self.assertEqual(self.foo_inh1.base_id.host, 'foo2_new_value')
|
||||||
|
|
||||||
|
self.foo_inh2.host_env_default = 'foo_inherits_value'
|
||||||
|
self.foo_inh2.base_id.host_env_default = 'bar_value'
|
||||||
|
self.foo_inh2.invalidate_cache()
|
||||||
|
self.foo_inh2.base_id.invalidate_cache()
|
||||||
|
self.assertEqual(self.foo_inh2.host, 'foo_inherits_value')
|
||||||
|
self.assertEqual(self.foo_inh2.base_id.host, 'bar_value')
|
||||||
|
|
||||||
|
self.foo_inh2.host = 'foo_inherits_new_value'
|
||||||
|
self.foo_inh2.base_id.host = 'bar_new_value'
|
||||||
|
self.foo_inh2.invalidate_cache()
|
||||||
|
self.foo_inh2.base_id.invalidate_cache()
|
||||||
|
self.assertEqual(self.foo_inh2.host, 'foo_inherits_new_value')
|
||||||
|
self.assertEqual(self.foo_inh2.base_id.host, 'bar_new_value')
|
||||||
Loading…
Reference in New Issue