Merge pull request #13 from guewen/11.0-server-env-mixin

server env: add fallback on database default values
This commit is contained in:
Maxime Chambreuil 2018-08-01 08:20:25 -05:00 committed by GitHub
commit 71d75f2e79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1176 additions and 142 deletions

View File

@ -3,7 +3,7 @@
{
'name': 'Mail configuration with server_environment',
'version': '11.0.1.0.0',
'version': '11.0.1.1.0',
'category': 'Tools',
'summary': 'Configure mail servers with server_environment_files',
'author': "Camptocamp, Odoo Community Association (OCA)",
@ -13,7 +13,5 @@
'fetchmail',
'server_environment',
],
'data': [
'views/fetchmail_server_views.xml',
],
'data': [],
}

View File

@ -3,58 +3,38 @@
import operator
from odoo import api, fields, models
from odoo.addons.server_environment import serv_config
class FetchmailServer(models.Model):
"""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',
states={})
port = fields.Integer(compute='_compute_server_env',
states={})
type = fields.Selection(compute='_compute_server_env',
search='_search_type',
states={})
user = fields.Char(compute='_compute_server_env',
states={})
password = fields.Char(compute='_compute_server_env',
states={})
is_ssl = fields.Boolean(compute='_compute_server_env')
attach = fields.Boolean(compute='_compute_server_env')
original = fields.Boolean(compute='_compute_server_env')
@property
def _server_env_fields(self):
base_fields = super()._server_env_fields
mail_fields = {
"server": {},
"port": {},
"type": {},
"user": {},
"password": {},
"is_ssl": {},
"attach": {},
"original": {},
}
mail_fields.update(base_fields)
return mail_fields
@api.depends()
def _compute_server_env(self):
for fetchmail in self:
global_section_name = 'incoming_mail'
type = fields.Selection(search='_search_type')
key_types = {'port': int,
'is_ssl': lambda a: bool(int(a or 0)),
'attach': lambda a: bool(int(a or 0)),
'original': lambda a: bool(int(a or 0)),
}
@api.model
def _server_env_global_section_name(self):
"""Name of the global section in the configuration files
# default vals
config_vals = {'port': 993,
'is_ssl': 0,
'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)
Can be customized in your model
"""
return 'incoming_mail'
@api.model
def _search_type(self, oper, value):

View File

@ -1,44 +1,30 @@
# Copyright 2012-2018 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
from odoo import api, fields, models
from odoo.addons.server_environment import serv_config
from odoo import api, models
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',
required=False,
readonly=True)
smtp_port = fields.Integer(compute='_compute_server_env',
required=False,
readonly=True)
smtp_user = fields.Char(compute='_compute_server_env',
required=False,
readonly=True)
smtp_pass = fields.Char(compute='_compute_server_env',
required=False,
readonly=True)
smtp_encryption = fields.Selection(compute='_compute_server_env',
required=False,
readonly=True)
@property
def _server_env_fields(self):
base_fields = super()._server_env_fields
mail_fields = {
"smtp_host": {},
"smtp_port": {},
"smtp_user": {},
"smtp_pass": {},
"smtp_encryption": {},
}
mail_fields.update(base_fields)
return mail_fields
@api.depends()
def _compute_server_env(self):
for server in self:
global_section_name = 'outgoing_mail'
@api.model
def _server_env_global_section_name(self):
"""Name of the global section in the configuration files
# default vals
config_vals = {'smtp_port': 587}
if serv_config.has_section(global_section_name):
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)
Can be customized in your model
"""
return 'outgoing_mail'

View File

@ -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>

View File

@ -3,17 +3,20 @@
:alt: License: GPL-3
==================
server environment
Server Environment
==================
This module provides a way to define an environment in the main Odoo
configuration file and to read some configurations from files
depending on the configured environment: you define the environment in
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.
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
see the values contained in keys named '*passw*'.
@ -21,14 +24,14 @@ Installation
============
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.
To install this module, you need to provide a companion module called
`server_environment_files`. You can copy and customize the provided
`server_environment_files_sample` module for this purpose.
You can provide additional options in environment variables
``SERVER_ENV_CONFIG`` and ``SERVER_ENV_CONFIG_SECRET``.
You can store your configuration values in a companion module called
``server_environment_files``. You can copy and customize the provided
``server_environment_files_sample`` module for this purpose. Alternatively, you
can provide them in environment variables ``SERVER_ENV_CONFIG`` and
``SERVER_ENV_CONFIG_SECRET``.
Configuration
@ -65,7 +68,7 @@ Environment variable
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
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.
If you used options in ``server_environment_files``, the options set in the
environment variable overrides them.
@ -75,7 +78,6 @@ the content of the variable must be set accordingly to the running environment.
Example of setup:
A public file, containing that will contain public variables::
# These variables are not odoo standard variables,
@ -106,17 +108,44 @@ A second file which is encrypted and contains secrets::
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
=====
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

View File

@ -17,4 +17,11 @@
# 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

View File

@ -20,8 +20,11 @@
{
"name": "server configuration environment files",
"version": "11.0.1.0.1",
"depends": ["base"],
"version": "11.0.2.0.0",
"depends": [
"base",
"base_sparse_field",
],
"author": "Camptocamp,Odoo Community Association (OCA)",
"summary": "move some configurations out of the database",
"website": "http://odoo-community.org/",

View File

@ -0,0 +1 @@
from . import server_env_mixin

View File

@ -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)

View File

@ -18,6 +18,7 @@
#
##############################################################################
import logging
import os
import configparser
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 odoo.addons import server_environment_files
_dir = os.path.dirname(server_environment_files.__file__)
_logger = logging.getLogger(__name__)
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')
@ -46,13 +54,15 @@ if not system_base_config.get('running_env', False):
"[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):
raise Exception(
"Provided server environment does not exist, "
"please add a folder %s" % ck_path
)
if not os.path.exists(ck_path):
raise Exception(
"Provided server environment does not exist, "
"please add a folder %s" % ck_path
)
def setboolean(obj, attr, _bool=None):
@ -82,8 +92,7 @@ def _listconf(env_path):
return files
def _load_config():
"""Load the configuration and return a ConfigParser instance."""
def _load_config_from_server_env_files(config_p):
default = os.path.join(_dir, 'default')
running_env = os.path.join(_dir,
system_base_config['running_env'])
@ -92,17 +101,18 @@ def _load_config():
else:
conf_files = _listconf(running_env)
config_p = configparser.SafeConfigParser()
# options are case-sensitive
config_p.optionxform = str
try:
config_p.read(conf_files)
except Exception as 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.remove_section('options')
def _load_config_from_env(config_p):
for varname in ENV_VAR_NAMES:
env_config = os.getenv(varname)
if env_config:
@ -114,6 +124,17 @@ def _load_config():
% (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

View File

@ -18,3 +18,4 @@
#
##############################################################################
from . import test_server_environment
from . import test_environment_variable

View File

@ -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

View File

@ -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')

View File

@ -17,11 +17,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from odoo.tests import common
from odoo.addons.server_environment import serv_config
from odoo.addons.server_environment import server_env
from . import common
class TestEnv(common.TransactionCase):
class TestEnv(common.ServerEnvironmentCase):
def test_view(self):
model = self.env['server.config']
@ -43,5 +43,9 @@ class TestEnv(common.TransactionCase):
self.assertTrue(pass_checked)
def test_value_retrival(self):
val = serv_config.get('external_service.ftp', 'user')
self.assertEqual(val, 'toto')
with self.set_config_dir('testfiles'):
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')

View File

@ -0,0 +1,4 @@
[external_service.ftp]
host = sftp.example.com
user = foo
password = bar

View File

@ -0,0 +1,2 @@
[external_service.ftp]
user = testing

View File

@ -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.

View File

@ -0,0 +1 @@
from . import models

View File

@ -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,
}

View File

@ -0,0 +1 @@
from . import server_env_test

View File

@ -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

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_server_env_test access_server_env_test model_server_env_test 1 0 0 0
3 access_server_env_test2 access_server_env_test2 model_server_env_test2 1 0 0 0
4 access_server_env_test_inherits1 access_server_env_test_inherits1 model_server_env_test_inherits1 1 0 0 0
5 access_server_env_test_inherits2 access_server_env_test_inherits2 model_server_env_test_inherits2 1 0 0 0

View File

@ -0,0 +1,2 @@
from . import test_server_env_mixin
from . import test_server_env_mixin_inherit

View File

@ -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')

View File

@ -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')