From 59fab426e1c24588d3cf92d1da0e16b1d5ed87bb Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 19 Jul 2018 15:57:58 +0200 Subject: [PATCH 01/27] Add a server environment mixin To automatically convert fields into fields reading values from the environment. Until now, every module reimplements the same computed field. --- server_environment/__init__.py | 1 + server_environment/models/__init__.py | 1 + server_environment/models/server_env_mixin.py | 112 ++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 server_environment/models/__init__.py create mode 100644 server_environment/models/server_env_mixin.py diff --git a/server_environment/__init__.py b/server_environment/__init__.py index 8e7cc35..3824827 100644 --- a/server_environment/__init__.py +++ b/server_environment/__init__.py @@ -17,4 +17,5 @@ # along with this program. If not, see . # ############################################################################## +from . import models from .serv_config import serv_config, setboolean diff --git a/server_environment/models/__init__.py b/server_environment/models/__init__.py new file mode 100644 index 0000000..6bd869a --- /dev/null +++ b/server_environment/models/__init__.py @@ -0,0 +1 @@ +from . import server_env_mixin diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py new file mode 100644 index 0000000..c6770f9 --- /dev/null +++ b/server_environment/models/server_env_mixin.py @@ -0,0 +1,112 @@ +# Copyright 2018 Camptocamp (https://www.camptocamp.com). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import api, 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": 'get'} + + With the snippet above, the "storage.backend" model will now use a server + environment configuration for the field ``directory_path``. + + Under the hood, this mixin will automatically replaces the original field + by a computed field that reads from the configuration files. + + By default, it will look for the configuration in a section named + ``[model_name.Record Name]`` where ``model_name`` is the ``_name`` of the + model with ``.`` replaced by ``_``. It can be customized by overriding the + method :meth:`~server_env_section_name`. + """ + _name = 'server.env.mixin' + + @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': 'name_of_the_configparser_getter'} + + The configparser getter can be one of: get, getbool, getint + + Example:: + + @property + def _server_env_fields(self): + base_fields = super()._server_env_fields + sftp_fields = { + "sftp_server": "get", + "sftp_port": "getint", + "sftp_login": "get", + "sftp_password": "get", + } + sftp_fields.update(base_fields) + return sftp_fields + """ + return {} + + @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() + return ".".join( + (self._name.replace(".", "_"), self.name) + ) + + @api.multi + def _server_env_read_from_config(self, section_name, field_name, + config_getter): + self.ensure_one() + try: + getter = getattr(serv_config, config_getter) + value = getter(section_name, field_name) + except: + _logger.exception( + "error trying to read field %s in section %s", + field_name, + section_name, + ) + return False + return value + + @api.multi + def _compute_server_env(self): + for record in self: + for field_name, getter_name in self._server_env_fields.items(): + section_name = self._server_env_section_name() + value = self._server_env_read_from_config( + section_name, field_name, getter_name + ) + record[field_name] = value + + def _server_env_transform_field_to_read_from_env(self, field): + """Transform the original field in a computed field""" + field.compute = '_compute_server_env' + field.store = False + field.copy = False + field.sparse = None + + @api.model + def _setup_base(self): + super()._setup_base() + for fieldname in self._server_env_fields: + field = self._fields[fieldname] + self._server_env_transform_field_to_read_from_env(field) From ff91cc4a0f64ca14c7d2ea41dc690c6ca91a2397 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 19 Jul 2018 16:06:46 +0200 Subject: [PATCH 02/27] Read default values from database when no config is provided Automatically add _env_default for every field transformed to a "computed from env" field, so a default value can be set. It will be used when the configuration is not set in a configuration file (when the key is absent, not empty). --- server_environment/__manifest__.py | 6 ++- server_environment/models/server_env_mixin.py | 49 ++++++++++++++++--- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/server_environment/__manifest__.py b/server_environment/__manifest__.py index f41ebcf..3d9dc22 100644 --- a/server_environment/__manifest__.py +++ b/server_environment/__manifest__.py @@ -20,8 +20,10 @@ { "name": "server configuration environment files", - "version": "11.0.1.0.1", - "depends": ["base"], + "depends": [ + "base", + "base_sparse_field", + ], "author": "Camptocamp,Odoo Community Association (OCA)", "summary": "move some configurations out of the database", "website": "http://odoo-community.org/", diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index c6770f9..24ce90c 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -3,7 +3,7 @@ import logging -from odoo import api, models +from odoo import api, fields, models from ..serv_config import serv_config _logger = logging.getLogger(__name__) @@ -22,19 +22,27 @@ class ServerEnvMixin(models.AbstractModel): def _server_env_fields(self): return {"directory_path": 'get'} - With the snippet above, the "storage.backend" model will now use a server + With the snippet above, the "storage.backend" model now uses a server environment configuration for the field ``directory_path``. - Under the hood, this mixin will automatically replaces the original field + Under the hood, this mixin automatically replaces the original field by a computed field that reads from the configuration files. - By default, it will look for the configuration in a section named + 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 ``_``. It can be customized by overriding the method :meth:`~server_env_section_name`. + + For each field transformed to an env-computed field, a companion field + ``_env_default`` is automatically created. When it's value is set + and the configuration files do not contain a key, the env-computed field + uses the default value stored in database. If a key is empty, the + env-computed field has an empty value. """ _name = 'server.env.mixin' + server_env_defaults = fields.Serialized() + @property def _server_env_fields(self): """Dict of fields to replace by fields computed from env @@ -92,11 +100,24 @@ class ServerEnvMixin(models.AbstractModel): for record in self: for field_name, getter_name in self._server_env_fields.items(): section_name = self._server_env_section_name() - value = self._server_env_read_from_config( - section_name, field_name, getter_name - ) + if (section_name in serv_config + and field_name in serv_config[section_name]): + + value = self._server_env_read_from_config( + section_name, field_name, getter_name + ) + + else: + default_field = self._server_env_default_fieldname( + field_name + ) + value = record[default_field] + record[field_name] = value + def _server_env_default_fieldname(self, base_field_name): + return '%s_env_default' % (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' @@ -104,9 +125,23 @@ class ServerEnvMixin(models.AbstractModel): field.copy = False field.sparse = None + def _server_env_add_default_field(self, base_field): + fieldname = self._server_env_default_fieldname(base_field.name) + if fieldname not in self._fields: + base_field_cls = base_field.__class__ + field_args = base_field.args + field_args.pop('_sequence', None) + field_args.update({ + 'sparse': 'server_env_defaults', + 'automatic': True, + }) + 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_transform_field_to_read_from_env(field) + self._server_env_add_default_field(field) From 04bbfce412e7cc7e59b49e365656c4517fbbb2cc Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 19 Jul 2018 16:56:22 +0200 Subject: [PATCH 03/27] Allow to edit default values for env-computed fields When they don't have any key in the environment configuration files. In the UI, when a field is set in a configuration file, the field is readonly, if not the field is editable. Which means you can selectively choose which fields depend on the environment and which can use a "default" value stored in database. --- server_environment/models/server_env_mixin.py | 113 +++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index 24ce90c..e8d1d0d 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -3,6 +3,10 @@ import logging +from functools import partialmethod + +from lxml import etree + from odoo import api, fields, models from ..serv_config import serv_config @@ -26,7 +30,7 @@ class ServerEnvMixin(models.AbstractModel): environment configuration for the field ``directory_path``. Under the hood, this mixin automatically replaces the original field - by a computed field that reads from the configuration files. + 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 @@ -38,6 +42,10 @@ class ServerEnvMixin(models.AbstractModel): and the configuration files do not contain a key, the env-computed field uses the default value stored in database. If a key 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. """ _name = 'server.env.mixin' @@ -97,6 +105,11 @@ class ServerEnvMixin(models.AbstractModel): @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 ``_env_default`` field from database. + """ for record in self: for field_name, getter_name in self._server_env_fields.items(): section_name = self._server_env_section_name() @@ -115,17 +128,114 @@ class ServerEnvMixin(models.AbstractModel): record[field_name] = value + def _inverse_server_env(self, 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]: + record[default_field] = record[field_name] + + @api.multi + def _compute_server_env_is_editable(self): + """Compute _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 + ) + section_name = self._server_env_section_name() + is_editable = not (section_name in serv_config + and field_name in serv_config[section_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 + 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""" 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( + ServerEnvMixin._inverse_server_env, field.name + ) + setattr(ServerEnvMixin, inverse_method_name, inverse_method) + field.inverse = inverse_method_name field.store = False field.copy = False field.sparse = None + 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 fieldname not in self._fields: + field = fields.Boolean( + compute='_compute_server_env_is_editable', + automatic=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 stored in the serialized field ``server_env_defaults``. + """ fieldname = self._server_env_default_fieldname(base_field.name) if fieldname not in self._fields: base_field_cls = base_field.__class__ @@ -144,4 +254,5 @@ class ServerEnvMixin(models.AbstractModel): for fieldname in self._server_env_fields: field = self._fields[fieldname] self._server_env_transform_field_to_read_from_env(field) + self._server_env_add_is_editable_field(field) self._server_env_add_default_field(field) From 8d43652dbd7ce9bdc0af2915c82e72d70de7dbdf Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 19 Jul 2018 17:47:04 +0200 Subject: [PATCH 04/27] Fix a few small issues in mixin --- server_environment/models/server_env_mixin.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index e8d1d0d..6184f4b 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -35,7 +35,7 @@ class ServerEnvMixin(models.AbstractModel): 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 ``_``. It can be customized by overriding the - method :meth:`~server_env_section_name`. + method :meth:`~_server_env_section_name`. For each field transformed to an env-computed field, a companion field ``_env_default`` is automatically created. When it's value is set @@ -111,17 +111,17 @@ class ServerEnvMixin(models.AbstractModel): read from the ``_env_default`` field from database. """ for record in self: - for field_name, getter_name in self._server_env_fields.items(): - section_name = self._server_env_section_name() + for field_name, getter_name in record._server_env_fields.items(): + section_name = record._server_env_section_name() if (section_name in serv_config and field_name in serv_config[section_name]): - value = self._server_env_read_from_config( + value = record._server_env_read_from_config( section_name, field_name, getter_name ) else: - default_field = self._server_env_default_fieldname( + default_field = record._server_env_default_fieldname( field_name ) value = record[default_field] @@ -152,7 +152,8 @@ class ServerEnvMixin(models.AbstractModel): is_editable_field = self._server_env_is_editable_fieldname( field_name ) - section_name = self._server_env_section_name() + + section_name = record._server_env_section_name() is_editable = not (section_name in serv_config and field_name in serv_config[section_name]) record[is_editable_field] = is_editable @@ -211,6 +212,7 @@ class ServerEnvMixin(models.AbstractModel): setattr(ServerEnvMixin, inverse_method_name, inverse_method) field.inverse = inverse_method_name field.store = False + field.required = False field.copy = False field.sparse = None @@ -239,12 +241,15 @@ class ServerEnvMixin(models.AbstractModel): fieldname = self._server_env_default_fieldname(base_field.name) if fieldname not in self._fields: base_field_cls = base_field.__class__ - field_args = base_field.args + 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) @@ -253,6 +258,6 @@ class ServerEnvMixin(models.AbstractModel): 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) - self._server_env_add_default_field(field) From 64c3e29b7393b295f2067f114d67320cb7bd39ca Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 19 Jul 2018 18:01:19 +0200 Subject: [PATCH 05/27] Use a dictionary to configure the fields --- server_environment/models/server_env_mixin.py | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index 6184f4b..95f6959 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -24,7 +24,7 @@ class ServerEnvMixin(models.AbstractModel): @property def _server_env_fields(self): - return {"directory_path": 'get'} + return {"directory_path": {'getter': 'get'}} With the snippet above, the "storage.backend" model now uses a server environment configuration for the field ``directory_path``. @@ -56,9 +56,16 @@ class ServerEnvMixin(models.AbstractModel): """Dict of fields to replace by fields computed from env To override in models. The dictionary is: - {'name_of_the_field': 'name_of_the_configparser_getter'} + {'name_of_the_field': options} - The configparser getter can be one of: get, getbool, getint + Where ``options`` is a dictionary:: + + options = { + "getter": "getint", + } + + The configparser getter can be one of: get, getbool, getint. + If options is an empty dict, "get" is used. Example:: @@ -66,10 +73,14 @@ class ServerEnvMixin(models.AbstractModel): def _server_env_fields(self): base_fields = super()._server_env_fields sftp_fields = { - "sftp_server": "get", - "sftp_port": "getint", - "sftp_login": "get", - "sftp_password": "get", + "sftp_server": { + "getter": "get", + }, + "sftp_port": { + "getter": "getint", + },, + "sftp_login": {}, + "sftp_password": {}, } sftp_fields.update(base_fields) return sftp_fields @@ -111,11 +122,11 @@ class ServerEnvMixin(models.AbstractModel): read from the ``_env_default`` field from database. """ for record in self: - for field_name, getter_name in record._server_env_fields.items(): + for field_name, options in record._server_env_fields.items(): section_name = record._server_env_section_name() if (section_name in serv_config and field_name in serv_config[section_name]): - + getter_name = options.get('getter', 'get') value = record._server_env_read_from_config( section_name, field_name, getter_name ) From 60375bbf65eea68f080b68a696947a84a569f240 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 19 Jul 2018 18:20:44 +0200 Subject: [PATCH 06/27] Add global section --- server_environment/models/server_env_mixin.py | 60 ++++++++++++++----- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index 95f6959..37d4c16 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -34,8 +34,10 @@ class ServerEnvMixin(models.AbstractModel): 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 ``_``. It can be customized by overriding the - method :meth:`~_server_env_section_name`. + 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 ``_env_default`` is automatically created. When it's value is set @@ -87,6 +89,15 @@ class ServerEnvMixin(models.AbstractModel): """ return {} + @api.multi + def _server_env_global_section_name(self): + """Name of the global section in the configuration files + + Can be customized in your model + """ + self.ensure_one() + return self._name.replace(".", "_") + @api.multi def _server_env_section_name(self): """Name of the section in the configuration files @@ -99,12 +110,20 @@ class ServerEnvMixin(models.AbstractModel): ) @api.multi - def _server_env_read_from_config(self, section_name, field_name, - config_getter): + 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) - value = getter(section_name, field_name) + 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: _logger.exception( "error trying to read field %s in section %s", @@ -114,6 +133,21 @@ class ServerEnvMixin(models.AbstractModel): 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 + @api.multi def _compute_server_env(self): """Read values from environment configuration files @@ -122,13 +156,11 @@ class ServerEnvMixin(models.AbstractModel): read from the ``_env_default`` field from database. """ for record in self: - for field_name, options in record._server_env_fields.items(): - section_name = record._server_env_section_name() - if (section_name in serv_config - and field_name in serv_config[section_name]): + for field_name, options in self._server_env_fields.items(): + if record._server_env_has_key_defined(field_name): getter_name = options.get('getter', 'get') value = record._server_env_read_from_config( - section_name, field_name, getter_name + field_name, getter_name ) else: @@ -145,6 +177,7 @@ class ServerEnvMixin(models.AbstractModel): 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]: record[default_field] = record[field_name] @@ -163,10 +196,9 @@ class ServerEnvMixin(models.AbstractModel): is_editable_field = self._server_env_is_editable_fieldname( field_name ) - - section_name = record._server_env_section_name() - is_editable = not (section_name in serv_config - and field_name in serv_config[section_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): From 6b44590605b80a664b3a33421f816b0d3eced31e Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 19 Jul 2018 21:59:37 +0200 Subject: [PATCH 07/27] Disable prefetch on env-computed fields As in the inverse field that write the value into the _env_default we have to browse the record, the prefetch has the effect of calling compute on the env-computed field which resets the value to it's previous state before we have the occasion to store it. --- server_environment/models/server_env_mixin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index 37d4c16..89d39bb 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -258,6 +258,7 @@ class ServerEnvMixin(models.AbstractModel): 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 From 9ae51f3c689c628e9e65a7a5eeae4c7848dd25ee Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 19 Jul 2018 22:18:31 +0200 Subject: [PATCH 08/27] Use global section name as first part of the section --- server_environment/models/server_env_mixin.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index 89d39bb..cd15d9a 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -89,13 +89,12 @@ class ServerEnvMixin(models.AbstractModel): """ return {} - @api.multi + @api.model def _server_env_global_section_name(self): """Name of the global section in the configuration files Can be customized in your model """ - self.ensure_one() return self._name.replace(".", "_") @api.multi @@ -105,9 +104,8 @@ class ServerEnvMixin(models.AbstractModel): Can be customized in your model """ self.ensure_one() - return ".".join( - (self._name.replace(".", "_"), self.name) - ) + 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): From 9ee47dc5d2eb6fdca859e28fbc23a916d26629e3 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 19 Jul 2018 22:28:25 +0200 Subject: [PATCH 09/27] Allow to edit all fields on creation --- server_environment/models/server_env_mixin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index cd15d9a..8231e47 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -269,6 +269,9 @@ class ServerEnvMixin(models.AbstractModel): 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) From 9371d19fbfa77b722282910744b3d410feae2e57 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 19 Jul 2018 22:36:30 +0200 Subject: [PATCH 10/27] Use new server.env.mixin in mail_environment --- mail_environment/__manifest__.py | 6 +- mail_environment/models/fetchmail_server.py | 76 ++++++++----------- mail_environment/models/ir_mail_server.py | 58 ++++++-------- .../views/fetchmail_server_views.xml | 23 ------ 4 files changed, 57 insertions(+), 106 deletions(-) delete mode 100644 mail_environment/views/fetchmail_server_views.xml diff --git a/mail_environment/__manifest__.py b/mail_environment/__manifest__.py index 00983c1..46a4b82 100644 --- a/mail_environment/__manifest__.py +++ b/mail_environment/__manifest__.py @@ -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': [], } diff --git a/mail_environment/models/fetchmail_server.py b/mail_environment/models/fetchmail_server.py index 72cd926..ed33312 100644 --- a/mail_environment/models/fetchmail_server.py +++ b/mail_environment/models/fetchmail_server.py @@ -3,58 +3,46 @@ 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": { + "getter": "getint", + }, + "type": {}, + "user": {}, + "password": {}, + "is_ssl": { + "getter": "getbool", + }, + "attach": { + "getter": "getbool", + }, + "original": { + "getter": "getbool", + }, + } + 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): diff --git a/mail_environment/models/ir_mail_server.py b/mail_environment/models/ir_mail_server.py index 1f45fd3..b1d53ce 100644 --- a/mail_environment/models/ir_mail_server.py +++ b/mail_environment/models/ir_mail_server.py @@ -1,44 +1,32 @@ # 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": { + "getter": "getint", + }, + "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' diff --git a/mail_environment/views/fetchmail_server_views.xml b/mail_environment/views/fetchmail_server_views.xml deleted file mode 100644 index 0c7f113..0000000 --- a/mail_environment/views/fetchmail_server_views.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - fetchmail.server - - - - - - - - - - - - - - - - - - From d9ad47f40d3e5657b0112b7a877e1cc83569808a Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 20 Jul 2018 08:51:37 +0200 Subject: [PATCH 11/27] Make server_environment_files optional --- server_environment/serv_config.py | 46 ++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/server_environment/serv_config.py b/server_environment/serv_config.py index 17ba30e..9807e13 100644 --- a/server_environment/serv_config.py +++ b/server_environment/serv_config.py @@ -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,16 @@ 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 From 9b0bdba49524b06c86953011a078af3ae0aadd69 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 20 Jul 2018 09:45:28 +0200 Subject: [PATCH 12/27] Allow integration with keychain By adding options to change the compute and inverse methods for default fields --- server_environment/models/server_env_mixin.py | 105 +++++++++++++++--- 1 file changed, 90 insertions(+), 15 deletions(-) diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index 8231e47..7c3b717 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -16,7 +16,10 @@ _logger = logging.getLogger(__name__) class ServerEnvMixin(models.AbstractModel): """Mixin to add server environment in existing models - Usage:: + Usage + ----- + + :: class StorageBackend(models.Model): _name = "storage.backend" @@ -48,6 +51,49 @@ class ServerEnvMixin(models.AbstractModel): 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": {'getter': "getint"}, + "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' @@ -64,10 +110,20 @@ class ServerEnvMixin(models.AbstractModel): options = { "getter": "getint", + "no_default_field": True, + "compute_default": "_compute_password", + "inverse_default": "_inverse_password", } - The configparser getter can be one of: get, getbool, getint. - If options is an empty dict, "get" is used. + * ``getter``: The configparser getter can be one of: get, getbool, + getint. Default is "get". + * ``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:: @@ -146,6 +202,23 @@ class ServerEnvMixin(models.AbstractModel): ) return has_global_config or has_config + def _compute_server_env_from_config(self, field_name, options): + getter_name = options.get('getter', '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.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 @@ -156,28 +229,25 @@ class ServerEnvMixin(models.AbstractModel): for record in self: for field_name, options in self._server_env_fields.items(): if record._server_env_has_key_defined(field_name): - getter_name = options.get('getter', 'get') - value = record._server_env_read_from_config( - field_name, getter_name - ) + self._compute_server_env_from_config(field_name, options) else: - default_field = record._server_env_default_fieldname( - field_name - ) - value = record[default_field] - - record[field_name] = value + self._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]: - record[default_field] = record[field_name] + if 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): @@ -233,6 +303,9 @@ class ServerEnvMixin(models.AbstractModel): 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.get('no_default_field'): + return '' return '%s_env_default' % (base_field_name,) def _server_env_is_editable_fieldname(self, base_field_name): @@ -284,6 +357,8 @@ class ServerEnvMixin(models.AbstractModel): The field is stored in the serialized field ``server_env_defaults``. """ fieldname = self._server_env_default_fieldname(base_field.name) + if not fieldname: + return if fieldname not in self._fields: base_field_cls = base_field.__class__ field_args = base_field.args.copy() @@ -301,7 +376,7 @@ class ServerEnvMixin(models.AbstractModel): @api.model def _setup_base(self): super()._setup_base() - for fieldname in self._server_env_fields: + for fieldname, options in self._server_env_fields.items(): field = self._fields[fieldname] self._server_env_add_default_field(field) self._server_env_transform_field_to_read_from_env(field) From c83da6071c11cc60db79a9aaee609c5675e7d50f Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 20 Jul 2018 10:31:18 +0200 Subject: [PATCH 13/27] Update documentation of server_environment, bump --- server_environment/README.rst | 60 ++++++++++++++++++++++-------- server_environment/__manifest__.py | 1 + 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/server_environment/README.rst b/server_environment/README.rst index e1b9749..ca0bae7 100644 --- a/server_environment/README.rst +++ b/server_environment/README.rst @@ -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 variable ``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,43 @@ 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 ``_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: a present key with an empty value do not fallback on the default field. + +Keychain integration +-------------------- + +Read the documentation of the class `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": {'getter': 'get'}} + +Read the documentation of the class and methods in `models/server_env_mixin.py +`__. .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas diff --git a/server_environment/__manifest__.py b/server_environment/__manifest__.py index 3d9dc22..bd30706 100644 --- a/server_environment/__manifest__.py +++ b/server_environment/__manifest__.py @@ -20,6 +20,7 @@ { "name": "server configuration environment files", + "version": "11.0.1.2.0", "depends": [ "base", "base_sparse_field", From edbefe2162d1a6d50284f9d615ca1205f57c12b8 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 20 Jul 2018 11:53:54 +0200 Subject: [PATCH 14/27] Add SERVER_ENV_CONFIG_SECRET alongside SERVER_ENV_CONFIG Allows to isolate the secrets in your deployment --- server_environment/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_environment/README.rst b/server_environment/README.rst index ca0bae7..cb216bc 100644 --- a/server_environment/README.rst +++ b/server_environment/README.rst @@ -30,7 +30,7 @@ the incoming and outgoing mail servers depending on the environment. 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 variable ``SERVER_ENV_CONFIG`` and +can provide them in environment variables ``SERVER_ENV_CONFIG`` and ``SERVER_ENV_CONFIG_SECRET``. From d3fe970be8352a001581e3ec4d6c6f9f05fe3dc9 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 23 Jul 2018 16:19:12 +0200 Subject: [PATCH 15/27] Reinforce server_environment base tests --- server_environment/__init__.py | 4 ++ server_environment/models/server_env_mixin.py | 2 +- server_environment/serv_config.py | 1 + server_environment/tests/__init__.py | 1 + server_environment/tests/common.py | 43 ++++++++++++++++++ .../tests/test_environment_variable.py | 45 +++++++++++++++++++ .../tests/test_server_environment.py | 14 +++--- .../tests/testfiles/default/base.conf | 4 ++ .../tests/testfiles/testing/base.conf | 2 + 9 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 server_environment/tests/common.py create mode 100644 server_environment/tests/test_environment_variable.py create mode 100644 server_environment/tests/testfiles/default/base.conf create mode 100644 server_environment/tests/testfiles/testing/base.conf diff --git a/server_environment/__init__.py b/server_environment/__init__.py index 3824827..0ff0f53 100644 --- a/server_environment/__init__.py +++ b/server_environment/__init__.py @@ -18,4 +18,8 @@ # ############################################################################## from . import models +# Add an alias to access to the 'serv_config' module as it is shadowed +# the next 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 diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index 7c3b717..09c5264 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -178,7 +178,7 @@ class ServerEnvMixin(models.AbstractModel): value = getter(section_name, field_name) else: value = getter(global_section_name, field_name) - except: + except Exception: _logger.exception( "error trying to read field %s in section %s", field_name, diff --git a/server_environment/serv_config.py b/server_environment/serv_config.py index 9807e13..4af4061 100644 --- a/server_environment/serv_config.py +++ b/server_environment/serv_config.py @@ -130,6 +130,7 @@ def _load_config(): 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) diff --git a/server_environment/tests/__init__.py b/server_environment/tests/__init__.py index 990f3cb..4c7aa90 100644 --- a/server_environment/tests/__init__.py +++ b/server_environment/tests/__init__.py @@ -18,3 +18,4 @@ # ############################################################################## from . import test_server_environment +from . import test_environment_variable diff --git a/server_environment/tests/common.py b/server_environment/tests/common.py new file mode 100644 index 0000000..37f3fb0 --- /dev/null +++ b/server_environment/tests/common.py @@ -0,0 +1,43 @@ +# 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 + + +class ServerEnvironmentCase(common.TransactionCase): + + 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 diff --git a/server_environment/tests/test_environment_variable.py b/server_environment/tests/test_environment_variable.py new file mode 100644 index 0000000..6d29dc5 --- /dev/null +++ b/server_environment/tests/test_environment_variable.py @@ -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') diff --git a/server_environment/tests/test_server_environment.py b/server_environment/tests/test_server_environment.py index 012f93d..2212a6c 100644 --- a/server_environment/tests/test_server_environment.py +++ b/server_environment/tests/test_server_environment.py @@ -17,11 +17,11 @@ # along with this program. If not, see . # ############################################################################## -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') diff --git a/server_environment/tests/testfiles/default/base.conf b/server_environment/tests/testfiles/default/base.conf new file mode 100644 index 0000000..566de2b --- /dev/null +++ b/server_environment/tests/testfiles/default/base.conf @@ -0,0 +1,4 @@ +[external_service.ftp] +host = sftp.example.com +user = foo +password = bar \ No newline at end of file diff --git a/server_environment/tests/testfiles/testing/base.conf b/server_environment/tests/testfiles/testing/base.conf new file mode 100644 index 0000000..544e95b --- /dev/null +++ b/server_environment/tests/testfiles/testing/base.conf @@ -0,0 +1,2 @@ +[external_service.ftp] +user = testing From ea30313c4375591c9c982c38d88f3fb17e4481ef Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 23 Jul 2018 17:34:00 +0200 Subject: [PATCH 16/27] Add tests for the server env mixin --- server_environment/models/server_env_mixin.py | 3 +- test_server_environment/README.rst | 46 ++++++ test_server_environment/__init__.py | 1 + test_server_environment/__manifest__.py | 19 +++ test_server_environment/models/__init__.py | 1 + .../models/server_env_test.py | 45 ++++++ .../security/ir.model.access.csv | 2 + test_server_environment/tests/__init__.py | 1 + .../tests/test_server_env_mixin.py | 133 ++++++++++++++++++ 9 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 test_server_environment/README.rst create mode 100644 test_server_environment/__init__.py create mode 100644 test_server_environment/__manifest__.py create mode 100644 test_server_environment/models/__init__.py create mode 100644 test_server_environment/models/server_env_test.py create mode 100644 test_server_environment/security/ir.model.access.csv create mode 100644 test_server_environment/tests/__init__.py create mode 100644 test_server_environment/tests/test_server_env_mixin.py diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index 09c5264..2f56e1a 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -136,7 +136,7 @@ class ServerEnvMixin(models.AbstractModel): }, "sftp_port": { "getter": "getint", - },, + }, "sftp_login": {}, "sftp_password": {}, } @@ -319,6 +319,7 @@ class ServerEnvMixin(models.AbstractModel): 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( ServerEnvMixin._inverse_server_env, field.name diff --git a/test_server_environment/README.rst b/test_server_environment/README.rst new file mode 100644 index 0000000..107a17a --- /dev/null +++ b/test_server_environment/README.rst @@ -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 `_. + +Contributors +------------ + +* Guewen Baconnier + +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. + diff --git a/test_server_environment/__init__.py b/test_server_environment/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/test_server_environment/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/test_server_environment/__manifest__.py b/test_server_environment/__manifest__.py new file mode 100644 index 0000000..345336a --- /dev/null +++ b/test_server_environment/__manifest__.py @@ -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": "GPL-3 or any later version", + "category": "Tools", + "data": [ + 'security/ir.model.access.csv', + ], + 'installable': True, +} diff --git a/test_server_environment/models/__init__.py b/test_server_environment/models/__init__.py new file mode 100644 index 0000000..9caabd0 --- /dev/null +++ b/test_server_environment/models/__init__.py @@ -0,0 +1 @@ +from . import server_env_test diff --git a/test_server_environment/models/server_env_test.py b/test_server_environment/models/server_env_test.py new file mode 100644 index 0000000..2251839 --- /dev/null +++ b/test_server_environment/models/server_env_test.py @@ -0,0 +1,45 @@ +# 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() + + +# 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": { + "getter": "getint", + }, + "user": {}, + "password": {}, + "ssl": {}, + } + sftp_fields.update(base_fields) + return sftp_fields diff --git a/test_server_environment/security/ir.model.access.csv b/test_server_environment/security/ir.model.access.csv new file mode 100644 index 0000000..2d95da4 --- /dev/null +++ b/test_server_environment/security/ir.model.access.csv @@ -0,0 +1,2 @@ +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 diff --git a/test_server_environment/tests/__init__.py b/test_server_environment/tests/__init__.py new file mode 100644 index 0000000..4952af4 --- /dev/null +++ b/test_server_environment/tests/__init__.py @@ -0,0 +1 @@ +from . import test_server_env_mixin diff --git a/test_server_environment/tests/test_server_env_mixin.py b/test_server_environment/tests/test_server_env_mixin.py new file mode 100644 index 0000000..4af655d --- /dev/null +++ b/test_server_environment/tests/test_server_env_mixin.py @@ -0,0 +1,133 @@ +# Copyright 2018 Camptocamp (https://www.camptocamp.com). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from contextlib import contextmanager + +from odoo.addons.server_environment import server_env +from odoo.addons.server_environment.tests.common import ServerEnvironmentCase + +import odoo.addons.server_environment.models.server_env_mixin as \ + server_env_mixin + + +class TestServerEnvMixin(ServerEnvironmentCase): + + @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 + + 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_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) From 3dae9eeb7fcdfc029ec072e01007f5e97a1e59b1 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 24 Jul 2018 09:07:43 +0200 Subject: [PATCH 17/27] Add test for custom compute methods --- .../models/server_env_test.py | 18 ++++++++++++++++ .../tests/test_server_env_mixin.py | 21 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/test_server_environment/models/server_env_test.py b/test_server_environment/models/server_env_test.py index 2251839..13e47a0 100644 --- a/test_server_environment/models/server_env_test.py +++ b/test_server_environment/models/server_env_test.py @@ -23,6 +23,11 @@ class ServerEnvTest(models.Model): 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): @@ -40,6 +45,19 @@ class ServerEnvTestWithMixin(models.Model): "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 diff --git a/test_server_environment/tests/test_server_env_mixin.py b/test_server_environment/tests/test_server_env_mixin.py index 4af655d..7f9b8ba 100644 --- a/test_server_environment/tests/test_server_env_mixin.py +++ b/test_server_environment/tests/test_server_env_mixin.py @@ -131,3 +131,24 @@ class TestServerEnvMixin(ServerEnvironmentCase): 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') From b2c6a1ce4f523498cd92630a5b4a332eef969832 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 24 Jul 2018 11:50:06 +0200 Subject: [PATCH 18/27] fixup! Add tests for the server env mixin --- test_server_environment/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_server_environment/__manifest__.py b/test_server_environment/__manifest__.py index 345336a..ee9ff45 100644 --- a/test_server_environment/__manifest__.py +++ b/test_server_environment/__manifest__.py @@ -10,7 +10,7 @@ ], "author": "Camptocamp,Odoo Community Association (OCA)", "website": "http://odoo-community.org/", - "license": "GPL-3 or any later version", + "license": "AGPL-3", "category": "Tools", "data": [ 'security/ir.model.access.csv', From 303e5e637ffeed09cd8a0e92d9855835dbf4da4e Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 24 Jul 2018 13:39:22 +0200 Subject: [PATCH 19/27] Infer configparser getter from field type --- mail_environment/models/fetchmail_server.py | 6 ++-- server_environment/README.rst | 2 +- server_environment/models/server_env_mixin.py | 36 ++++++++++++++----- .../models/server_env_test.py | 4 +-- 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/mail_environment/models/fetchmail_server.py b/mail_environment/models/fetchmail_server.py index ed33312..2475df6 100644 --- a/mail_environment/models/fetchmail_server.py +++ b/mail_environment/models/fetchmail_server.py @@ -22,13 +22,13 @@ class FetchmailServer(models.Model): "user": {}, "password": {}, "is_ssl": { - "getter": "getbool", + "getter": "getboolean", }, "attach": { - "getter": "getbool", + "getter": "getboolean", }, "original": { - "getter": "getbool", + "getter": "getboolean", }, } mail_fields.update(base_fields) diff --git a/server_environment/README.rst b/server_environment/README.rst index cb216bc..4435ec8 100644 --- a/server_environment/README.rst +++ b/server_environment/README.rst @@ -141,7 +141,7 @@ by an override of ``_server_env_fields``. @property def _server_env_fields(self): - return {"directory_path": {'getter': 'get'}} + return {"directory_path": {}} Read the documentation of the class and methods in `models/server_env_mixin.py `__. diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index 2f56e1a..98a80d3 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -27,7 +27,7 @@ class ServerEnvMixin(models.AbstractModel): @property def _server_env_fields(self): - return {"directory_path": {'getter': 'get'}} + return {"directory_path": {}} With the snippet above, the "storage.backend" model now uses a server environment configuration for the field ``directory_path``. @@ -99,6 +99,16 @@ class ServerEnvMixin(models.AbstractModel): 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 @@ -115,8 +125,9 @@ class ServerEnvMixin(models.AbstractModel): "inverse_default": "_inverse_password", } - * ``getter``: The configparser getter can be one of: get, getbool, - getint. Default is "get". + * ``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`` @@ -203,14 +214,23 @@ class ServerEnvMixin(models.AbstractModel): return has_global_config or has_config def _compute_server_env_from_config(self, field_name, options): - getter_name = options.get('getter', 'get') + 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.get('compute_default'): + if options and options.get('compute_default'): getattr(self, options['compute_default'])() else: default_field = self._server_env_default_fieldname( @@ -244,7 +264,7 @@ class ServerEnvMixin(models.AbstractModel): # we update the default value in database if record[is_editable_field]: - if options.get('inverse_default'): + if options and options.get('inverse_default'): getattr(record, options['inverse_default'])() elif default_field: record[default_field] = record[field_name] @@ -304,12 +324,12 @@ class ServerEnvMixin(models.AbstractModel): 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.get('no_default_field'): + 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 + """Return the name of the field for "is editable" This is the field used to tell if the env-computed field can be edited. diff --git a/test_server_environment/models/server_env_test.py b/test_server_environment/models/server_env_test.py index 13e47a0..b18d1a1 100644 --- a/test_server_environment/models/server_env_test.py +++ b/test_server_environment/models/server_env_test.py @@ -39,9 +39,7 @@ class ServerEnvTestWithMixin(models.Model): base_fields = super()._server_env_fields sftp_fields = { "host": {}, - "port": { - "getter": "getint", - }, + "port": {}, "user": {}, "password": {}, "ssl": {}, From c472ca85041f034ea2e04821438f1c349391a153 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 24 Jul 2018 13:43:03 +0200 Subject: [PATCH 20/27] Fixes for review feedbacks --- server_environment/__init__.py | 4 +++- server_environment/models/server_env_mixin.py | 11 ++++++----- server_environment/tests/testfiles/default/base.conf | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/server_environment/__init__.py b/server_environment/__init__.py index 0ff0f53..aab9a42 100644 --- a/server_environment/__init__.py +++ b/server_environment/__init__.py @@ -18,8 +18,10 @@ # ############################################################################## 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 -# the next line by an import of a variable with the same name. +# 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 diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index 98a80d3..5805e88 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -43,10 +43,11 @@ class ServerEnvMixin(models.AbstractModel): :meth:`~_server_env_global_section_name`. For each field transformed to an env-computed field, a companion field - ``_env_default`` is automatically created. When it's value is set - and the configuration files do not contain a key, the env-computed field - uses the default value stored in database. If a key is empty, the - env-computed field has an empty value. + ``_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 @@ -295,7 +296,7 @@ class ServerEnvMixin(models.AbstractModel): 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 + # 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): diff --git a/server_environment/tests/testfiles/default/base.conf b/server_environment/tests/testfiles/default/base.conf index 566de2b..434afc2 100644 --- a/server_environment/tests/testfiles/default/base.conf +++ b/server_environment/tests/testfiles/default/base.conf @@ -1,4 +1,4 @@ [external_service.ftp] host = sftp.example.com user = foo -password = bar \ No newline at end of file +password = bar From f55e7e056135ef2fdf1464ba7fe99838fbf04126 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 24 Jul 2018 13:43:49 +0200 Subject: [PATCH 21/27] fixup! Update documentation of server_environment, bump --- server_environment/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_environment/__manifest__.py b/server_environment/__manifest__.py index bd30706..15a7510 100644 --- a/server_environment/__manifest__.py +++ b/server_environment/__manifest__.py @@ -20,7 +20,7 @@ { "name": "server configuration environment files", - "version": "11.0.1.2.0", + "version": "11.0.2.0.0", "depends": [ "base", "base_sparse_field", From 05885049d20159640f80b9a52415b2c211993bb4 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 25 Jul 2018 12:55:48 +0200 Subject: [PATCH 22/27] fixup! Infer configparser getter from field type --- mail_environment/models/fetchmail_server.py | 16 ++++------------ mail_environment/models/ir_mail_server.py | 4 +--- server_environment/models/server_env_mixin.py | 10 +++------- 3 files changed, 8 insertions(+), 22 deletions(-) diff --git a/mail_environment/models/fetchmail_server.py b/mail_environment/models/fetchmail_server.py index 2475df6..2e891c7 100644 --- a/mail_environment/models/fetchmail_server.py +++ b/mail_environment/models/fetchmail_server.py @@ -15,21 +15,13 @@ class FetchmailServer(models.Model): base_fields = super()._server_env_fields mail_fields = { "server": {}, - "port": { - "getter": "getint", - }, + "port": {}, "type": {}, "user": {}, "password": {}, - "is_ssl": { - "getter": "getboolean", - }, - "attach": { - "getter": "getboolean", - }, - "original": { - "getter": "getboolean", - }, + "is_ssl": {}, + "attach": {}, + "original": {}, } mail_fields.update(base_fields) return mail_fields diff --git a/mail_environment/models/ir_mail_server.py b/mail_environment/models/ir_mail_server.py index b1d53ce..7f3c7af 100644 --- a/mail_environment/models/ir_mail_server.py +++ b/mail_environment/models/ir_mail_server.py @@ -13,9 +13,7 @@ class IrMailServer(models.Model): base_fields = super()._server_env_fields mail_fields = { "smtp_host": {}, - "smtp_port": { - "getter": "getint", - }, + "smtp_port": {}, "smtp_user": {}, "smtp_pass": {}, "smtp_encryption": {}, diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index 5805e88..dd7bcd0 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -75,7 +75,7 @@ class ServerEnvMixin(models.AbstractModel): base_fields = super()._server_env_fields sftp_fields = { "sftp_server": {}, - "sftp_port": {'getter': "getint"}, + "sftp_port": {}, "sftp_login": {}, "sftp_password": { "no_default_field": True, @@ -143,12 +143,8 @@ class ServerEnvMixin(models.AbstractModel): def _server_env_fields(self): base_fields = super()._server_env_fields sftp_fields = { - "sftp_server": { - "getter": "get", - }, - "sftp_port": { - "getter": "getint", - }, + "sftp_server": {}, + "sftp_port": {}, "sftp_login": {}, "sftp_password": {}, } From 04e54e09976597c190a32cd23af7fb575c93a725 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 25 Jul 2018 13:04:24 +0200 Subject: [PATCH 23/27] fixup! Fixes for review feedbacks --- server_environment/README.rst | 3 ++- server_environment/models/server_env_mixin.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/server_environment/README.rst b/server_environment/README.rst index 4435ec8..5517d0e 100644 --- a/server_environment/README.rst +++ b/server_environment/README.rst @@ -118,7 +118,8 @@ configuration files / environment variable. When the default field is used, the field is made editable on Odoo. -Note: a present key with an empty value do not fallback on the default field. +Note: empty environment keys always take precedence over default fields + Keychain integration -------------------- diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index dd7bcd0..c4f4503 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -372,7 +372,8 @@ class ServerEnvMixin(models.AbstractModel): The default value is used when there is no key for an env-computed field in the configuration files. - The field is stored in the serialized field ``server_env_defaults``. + 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: @@ -394,7 +395,7 @@ class ServerEnvMixin(models.AbstractModel): @api.model def _setup_base(self): super()._setup_base() - for fieldname, options in self._server_env_fields.items(): + 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) From a6ae304e491603ad863a0f1f3c11f8e73251998d Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 25 Jul 2018 17:23:46 +0200 Subject: [PATCH 24/27] Add tests and support of _inherits --- server_environment/models/server_env_mixin.py | 12 +- server_environment/tests/common.py | 16 ++ .../models/server_env_test.py | 54 ++++++ .../security/ir.model.access.csv | 3 + test_server_environment/tests/__init__.py | 1 + .../tests/test_server_env_mixin.py | 19 --- .../tests/test_server_env_mixin_inherit.py | 161 ++++++++++++++++++ 7 files changed, 243 insertions(+), 23 deletions(-) create mode 100644 test_server_environment/tests/test_server_env_mixin_inherit.py diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index c4f4503..72eb23b 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -339,9 +339,9 @@ class ServerEnvMixin(models.AbstractModel): inverse_method_name = '_inverse_server_env_%s' % field.name inverse_method = partialmethod( - ServerEnvMixin._inverse_server_env, field.name + type(self)._inverse_server_env, field.name ) - setattr(ServerEnvMixin, inverse_method_name, inverse_method) + setattr(type(self), inverse_method_name, inverse_method) field.inverse = inverse_method_name field.store = False field.required = False @@ -356,7 +356,9 @@ class ServerEnvMixin(models.AbstractModel): and in the views to add 'readonly' on the fields. """ fieldname = self._server_env_is_editable_fieldname(base_field.name) - if fieldname not in self._fields: + # 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, @@ -378,7 +380,9 @@ class ServerEnvMixin(models.AbstractModel): fieldname = self._server_env_default_fieldname(base_field.name) if not fieldname: return - if fieldname not in self._fields: + # 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) diff --git a/server_environment/tests/common.py b/server_environment/tests/common.py index 37f3fb0..7ea1256 100644 --- a/server_environment/tests/common.py +++ b/server_environment/tests/common.py @@ -9,6 +9,9 @@ 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.TransactionCase): @@ -41,3 +44,16 @@ class ServerEnvironmentCase(common.TransactionCase): 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 diff --git a/test_server_environment/models/server_env_test.py b/test_server_environment/models/server_env_test.py index b18d1a1..0cbc793 100644 --- a/test_server_environment/models/server_env_test.py +++ b/test_server_environment/models/server_env_test.py @@ -59,3 +59,57 @@ class ServerEnvTestWithMixin(models.Model): 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, + ) + # 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, + ) + 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 diff --git a/test_server_environment/security/ir.model.access.csv b/test_server_environment/security/ir.model.access.csv index 2d95da4..a2d18e6 100644 --- a/test_server_environment/security/ir.model.access.csv +++ b/test_server_environment/security/ir.model.access.csv @@ -1,2 +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 diff --git a/test_server_environment/tests/__init__.py b/test_server_environment/tests/__init__.py index 4952af4..4b0c4a6 100644 --- a/test_server_environment/tests/__init__.py +++ b/test_server_environment/tests/__init__.py @@ -1 +1,2 @@ from . import test_server_env_mixin +from . import test_server_env_mixin_inherit diff --git a/test_server_environment/tests/test_server_env_mixin.py b/test_server_environment/tests/test_server_env_mixin.py index 7f9b8ba..cc7b9cc 100644 --- a/test_server_environment/tests/test_server_env_mixin.py +++ b/test_server_environment/tests/test_server_env_mixin.py @@ -1,30 +1,11 @@ # Copyright 2018 Camptocamp (https://www.camptocamp.com). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from contextlib import contextmanager - -from odoo.addons.server_environment import server_env from odoo.addons.server_environment.tests.common import ServerEnvironmentCase -import odoo.addons.server_environment.models.server_env_mixin as \ - server_env_mixin - class TestServerEnvMixin(ServerEnvironmentCase): - @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 - def test_env_computed_fields_read(self): """Read values from the config in env-computed fields""" public = ( diff --git a/test_server_environment/tests/test_server_env_mixin_inherit.py b/test_server_environment/tests/test_server_env_mixin_inherit.py new file mode 100644 index 0000000..3b93bdf --- /dev/null +++ b/test_server_environment/tests/test_server_env_mixin_inherit.py @@ -0,0 +1,161 @@ +# 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): + + def setUp(self): + super().setUp() + self.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" + ) + self.foo = self.env['server.env.test'].create({'name': 'foo'}) + self.foo2 = self.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): + + def setUp(self): + super().setUp() + self.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" + ) + self.foo = self.env['server.env.test'].create({'name': 'foo'}) + self.foo_inh1 = self.env['server.env.test.inherits1'].create({ + 'name': 'foo' + }) + self.foo_inh2 = self.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') From 1f80d13773cd045c698424c2fcabead12eb984e0 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 26 Jul 2018 16:45:21 +0200 Subject: [PATCH 25/27] fixup! Add tests and support of _inherits --- test_server_environment/models/server_env_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test_server_environment/models/server_env_test.py b/test_server_environment/models/server_env_test.py index 0cbc793..106d6e9 100644 --- a/test_server_environment/models/server_env_test.py +++ b/test_server_environment/models/server_env_test.py @@ -87,6 +87,7 @@ class ServerEnvTestInherits1(models.Model): base_id = fields.Many2one( comodel_name='server.env.test', delegate=True, + required=True, ) # host is not redefined, handled by the delegated model @@ -102,6 +103,7 @@ class ServerEnvTestInherits2(models.Model): base_id = fields.Many2one( comodel_name='server.env.test', delegate=True, + required=True, ) host = fields.Char() From 3eeae22838084fe732562b086cccf549f803f1a7 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 27 Jul 2018 11:38:24 +0200 Subject: [PATCH 26/27] Use SavepointCase instead of TransactionCase It means less records to create for each test --- server_environment/tests/common.py | 2 +- .../tests/test_server_env_mixin_inherit.py | 24 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/server_environment/tests/common.py b/server_environment/tests/common.py index 7ea1256..ddf1b11 100644 --- a/server_environment/tests/common.py +++ b/server_environment/tests/common.py @@ -13,7 +13,7 @@ import odoo.addons.server_environment.models.server_env_mixin as \ server_env_mixin -class ServerEnvironmentCase(common.TransactionCase): +class ServerEnvironmentCase(common.SavepointCase): def setUp(self): super().setUp() diff --git a/test_server_environment/tests/test_server_env_mixin_inherit.py b/test_server_environment/tests/test_server_env_mixin_inherit.py index 3b93bdf..e3ef959 100644 --- a/test_server_environment/tests/test_server_env_mixin_inherit.py +++ b/test_server_environment/tests/test_server_env_mixin_inherit.py @@ -6,9 +6,10 @@ from odoo.addons.server_environment.tests.common import ServerEnvironmentCase class TestServerEnvMixinSameFieldName(ServerEnvironmentCase): - def setUp(self): - super().setUp() - self.public = ( + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.public = ( # global for all server.env.test records "[server_env_test]\n" "host=global_value\n" @@ -19,8 +20,8 @@ class TestServerEnvMixinSameFieldName(ServerEnvironmentCase): "[server_env_test2.foo]\n" "host=foo2_value\n" ) - self.foo = self.env['server.env.test'].create({'name': 'foo'}) - self.foo2 = self.env['server.env.test2'].create({ + cls.foo = cls.env['server.env.test'].create({'name': 'foo'}) + cls.foo2 = cls.env['server.env.test2'].create({ 'name': 'foo', }) @@ -71,9 +72,10 @@ class TestServerEnvMixinSameFieldName(ServerEnvironmentCase): class TestServerEnvMixinInherits(ServerEnvironmentCase): - def setUp(self): - super().setUp() - self.public = ( + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.public = ( # global for all server.env.test records "[server_env_test]\n" "host=global_value\n" @@ -87,11 +89,11 @@ class TestServerEnvMixinInherits(ServerEnvironmentCase): "[server_env_test_inherits2.foo]\n" "host=foo_inherits_value\n" ) - self.foo = self.env['server.env.test'].create({'name': 'foo'}) - self.foo_inh1 = self.env['server.env.test.inherits1'].create({ + cls.foo = cls.env['server.env.test'].create({'name': 'foo'}) + cls.foo_inh1 = cls.env['server.env.test.inherits1'].create({ 'name': 'foo' }) - self.foo_inh2 = self.env['server.env.test.inherits2'].create({ + cls.foo_inh2 = cls.env['server.env.test.inherits2'].create({ 'name': 'foo' }) From fa1110698e178c40216d225a51091c717381bc8f Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 27 Jul 2018 11:45:08 +0200 Subject: [PATCH 27/27] Fix iteration on records --- server_environment/models/server_env_mixin.py | 6 ++++-- .../tests/test_server_env_mixin.py | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index 72eb23b..bde7b37 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -246,10 +246,12 @@ class ServerEnvMixin(models.AbstractModel): for record in self: for field_name, options in self._server_env_fields.items(): if record._server_env_has_key_defined(field_name): - self._compute_server_env_from_config(field_name, options) + record._compute_server_env_from_config(field_name, options) else: - self._compute_server_env_from_default(field_name, options) + record._compute_server_env_from_default( + field_name, options + ) def _inverse_server_env(self, field_name): options = self._server_env_fields[field_name] diff --git a/test_server_environment/tests/test_server_env_mixin.py b/test_server_environment/tests/test_server_env_mixin.py index cc7b9cc..5465689 100644 --- a/test_server_environment/tests/test_server_env_mixin.py +++ b/test_server_environment/tests/test_server_env_mixin.py @@ -35,6 +35,26 @@ class TestServerEnvMixin(ServerEnvironmentCase): 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 = (