diff --git a/server_environment/__manifest__.py b/server_environment/__manifest__.py index b6872be..ef036da 100644 --- a/server_environment/__manifest__.py +++ b/server_environment/__manifest__.py @@ -5,18 +5,12 @@ { "name": "server configuration environment files", "version": "13.0.2.0.0", - "depends": [ - "base", - "base_sparse_field", - ], + "depends": ["base", "base_sparse_field"], "author": "Camptocamp,Odoo Community Association (OCA)", "summary": "move some configurations out of the database", "website": "http://github.com/OCA/server-env", "license": "GPL-3 or any later version", "category": "Tools", - "data": [ - 'security/res_groups.xml', - 'serv_config.xml', - ], - 'installable': True, + "data": ["security/res_groups.xml", "serv_config.xml"], + "installable": True, } diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index 0227bfa..81868c5 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -2,12 +2,12 @@ # License GPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging - from functools import partialmethod from lxml import etree from odoo import api, fields, models + from ..server_env import serv_config _logger = logging.getLogger(__name__) @@ -96,19 +96,20 @@ class ServerEnvMixin(models.AbstractModel): ``keychain.backend``. """ - _name = 'server.env.mixin' - _description = 'Mixin to add server environment in existing models' + + _name = "server.env.mixin" + _description = "Mixin to add server environment in existing models" server_env_defaults = fields.Serialized() _server_env_getter_mapping = { - 'integer': 'getint', - 'float': 'getfloat', - 'monetary': 'getfloat', - 'boolean': 'getboolean', - 'char': 'get', - 'selection': 'get', - 'text': 'get', + "integer": "getint", + "float": "getfloat", + "monetary": "getfloat", + "boolean": "getboolean", + "char": "get", + "selection": "get", + "text": "get", } @property @@ -180,16 +181,13 @@ class ServerEnvMixin(models.AbstractModel): # _server_env_has_key_defined so we are sure that the value is # either in the global or the record config getter = getattr(serv_config, config_getter) - if (section_name in serv_config - and field_name in serv_config[section_name]): + if section_name in serv_config and field_name in serv_config[section_name]: value = getter(section_name, field_name) else: value = getter(global_section_name, field_name) except Exception: _logger.exception( - "error trying to read field %s in section %s", - field_name, - section_name, + "error trying to read field %s in section %s", field_name, section_name ) return False return value @@ -203,34 +201,31 @@ class ServerEnvMixin(models.AbstractModel): and field_name in serv_config[global_section_name] ) has_config = ( - section_name in serv_config - and field_name in serv_config[section_name] + section_name in serv_config and field_name in serv_config[section_name] ) return has_global_config or has_config def _compute_server_env_from_config(self, field_name, options): - getter_name = options.get('getter') if options else None + 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 - ) + _logger.warning( + "server.env.mixin is used on a field of type %s, " + "which may not be supported properly" + ) + getter_name = "get" + value = self._server_env_read_from_config(field_name, getter_name) self[field_name] = value def _compute_server_env_from_default(self, field_name, options): - if options and options.get('compute_default'): - getattr(self, options['compute_default'])() + if options and options.get("compute_default"): + getattr(self, options["compute_default"])() else: - default_field = self._server_env_default_fieldname( - field_name - ) + default_field = self._server_env_default_fieldname(field_name) if default_field: self[field_name] = self[default_field] else: @@ -248,9 +243,7 @@ class ServerEnvMixin(models.AbstractModel): record._compute_server_env_from_config(field_name, options) else: - record._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] @@ -262,8 +255,8 @@ class ServerEnvMixin(models.AbstractModel): # we update the default value in database if record[is_editable_field]: - if options and options.get('inverse_default'): - getattr(record, options['inverse_default'])() + if options and options.get("inverse_default"): + getattr(record, options["inverse_default"])() elif default_field: record[default_field] = record[field_name] @@ -278,12 +271,8 @@ class ServerEnvMixin(models.AbstractModel): # in ``_inverse_server_env`` it would reset the value of the field for record in self: for field_name in self._server_env_fields: - is_editable_field = self._server_env_is_editable_fieldname( - field_name - ) - is_editable = not record._server_env_has_key_defined( - field_name - ) + is_editable_field = self._server_env_is_editable_fieldname(field_name) + is_editable = not record._server_env_has_key_defined(field_name) record[is_editable_field] = is_editable def _server_env_view_set_readonly(self, view_arch): @@ -293,37 +282,32 @@ class ServerEnvMixin(models.AbstractModel): for elem in view_arch.findall(field_xpath % field): # set env-computed fields to readonly if the configuration # files have a key set for this field - elem.set('attrs', - str({'readonly': [(is_editable_field, '=', False)]})) + 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" - ) + 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): + 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_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu ) - view_arch = etree.fromstring(view_data['arch'].encode('utf-8')) + 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') + view_data["arch"] = etree.tostring(view_arch, encoding="unicode") return view_data def _server_env_default_fieldname(self, base_field_name): """Return the name of the field with default value""" options = self._server_env_fields[base_field_name] - if options and options.get('no_default_field'): - return '' - return '%s_env_default' % (base_field_name,) + if options and options.get("no_default_field"): + return "" + return "{}_env_default".format(base_field_name) def _server_env_is_editable_fieldname(self, base_field_name): """Return the name of the field for "is editable" @@ -331,16 +315,14 @@ class ServerEnvMixin(models.AbstractModel): This is the field used to tell if the env-computed field can be edited. """ - return '%s_env_is_editable' % (base_field_name,) + return "{}_env_is_editable".format(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' + field.compute = "_compute_server_env" - inverse_method_name = '_inverse_server_env_%s' % field.name - inverse_method = partialmethod( - type(self)._inverse_server_env, field.name - ) + inverse_method_name = "_inverse_server_env_%s" % field.name + inverse_method = partialmethod(type(self)._inverse_server_env, field.name) setattr(type(self), inverse_method_name, inverse_method) field.inverse = inverse_method_name field.store = False @@ -360,7 +342,7 @@ class ServerEnvMixin(models.AbstractModel): # (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', + compute="_compute_server_env_is_editable", automatic=True, # this is required to be able to edit fields # on new records @@ -385,14 +367,11 @@ class ServerEnvMixin(models.AbstractModel): if fieldname not in self._fields or self._fields[fieldname].inherited: base_field_cls = base_field.__class__ field_args = base_field.args.copy() - field_args.pop('_sequence', None) - field_args.update({ - 'sparse': 'server_env_defaults', - 'automatic': True, - }) + 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 + if hasattr(base_field, "selection"): + field_args["selection"] = base_field.selection field = base_field_cls(**field_args) self._add_field(fieldname, field) diff --git a/server_environment/server_env.py b/server_environment/server_env.py index a18bd22..06106c3 100644 --- a/server_environment/server_env.py +++ b/server_environment/server_env.py @@ -18,13 +18,14 @@ # ############################################################################## +import configparser import logging import os -import configparser -from lxml import etree from itertools import chain -from odoo import api, models, fields +from lxml import etree + +from odoo import api, fields, models from odoo.tools.config import config as system_base_config from .system_info import get_server_environment @@ -33,19 +34,29 @@ _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') + _logger.info( + "not using server_environment_files for configuration," " no directory found" + ) _dir = None -ENV_VAR_NAMES = ('SERVER_ENV_CONFIG', 'SERVER_ENV_CONFIG_SECRET') +ENV_VAR_NAMES = ("SERVER_ENV_CONFIG", "SERVER_ENV_CONFIG_SECRET") # Same dict as RawConfigParser._boolean_states -_boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True, - '0': False, 'no': False, 'false': False, 'off': False} +_boolean_states = { + "1": True, + "yes": True, + "true": True, + "on": True, + "0": False, + "no": False, + "false": False, + "off": False, +} -if not system_base_config.get('running_env', False): +if not system_base_config.get("running_env", False): raise Exception( "The parameter 'running_env' has not be set neither in base config " "file option -c or in openerprc.\n" @@ -56,7 +67,7 @@ if not system_base_config.get('running_env', False): ck_path = None if _dir: - ck_path = os.path.join(_dir, system_base_config['running_env']) + ck_path = os.path.join(_dir, system_base_config["running_env"]) if not os.path.exists(ck_path): raise Exception( @@ -77,25 +88,29 @@ def setboolean(obj, attr, _bool=None): # Borrowed from MarkupSafe def _escape(s): """Convert the characters &<>'" in string s to HTML-safe sequences.""" - return (str(s).replace('&', '&') - .replace('>', '>') - .replace('<', '<') - .replace("'", ''') - .replace('"', '"')) + return ( + str(s) + .replace("&", "&") + .replace(">", ">") + .replace("<", "<") + .replace("'", "'") + .replace('"', """) + ) def _listconf(env_path): """List configuration files in a folder.""" - files = [os.path.join(env_path, name) - for name in sorted(os.listdir(env_path)) - if name.endswith('.conf')] + files = [ + os.path.join(env_path, name) + for name in sorted(os.listdir(env_path)) + if name.endswith(".conf") + ] return files 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']) + default = os.path.join(_dir, "default") + running_env = os.path.join(_dir, system_base_config["running_env"]) if os.path.isdir(default): conf_files = _listconf(default) + _listconf(running_env) else: @@ -104,12 +119,12 @@ def _load_config_from_server_env_files(config_p): try: config_p.read(conf_files) except Exception as e: - raise Exception('Cannot read config files "%s": %s' % (conf_files, e)) + raise Exception('Cannot read config files "{}": {}'.format(conf_files, e)) def _load_config_from_rcfile(config_p): config_p.read(system_base_config.rcfile) - config_p.remove_section('options') + config_p.remove_section("options") def _load_config_from_env(config_p): @@ -120,8 +135,7 @@ def _load_config_from_env(config_p): config_p.read_string(env_config) except configparser.Error as err: raise Exception( - '%s content could not be parsed: %s' - % (varname, err,) + "{} content could not be parsed: {}".format(varname, err) ) @@ -147,13 +161,15 @@ class _Defaults(dict): def __setitem__(self, key, value): def func(*a): return str(value) + return dict.__setitem__(self, key, func) class ServerConfiguration(models.TransientModel): """Display server configuration.""" - _name = 'server.config' - _description = 'Display server configuration' + + _name = "server.config" + _description = "Display server configuration" _conf_defaults = _Defaults() @classmethod @@ -164,20 +180,20 @@ class ServerConfiguration(models.TransientModel): """ ModelClass = super(ServerConfiguration, cls)._build_model(pool, cr) ModelClass._add_columns() - ModelClass.running_env = system_base_config['running_env'] + ModelClass.running_env = system_base_config["running_env"] # Only show passwords in development - ModelClass.show_passwords = ModelClass.running_env in ('dev',) + ModelClass.show_passwords = ModelClass.running_env in ("dev",) ModelClass._arch = None ModelClass._build_osv() return ModelClass @classmethod def _format_key(cls, section, key): - return '%s_I_%s' % (section, key) + return "{}_I_{}".format(section, key) @classmethod def _format_key_display_name(cls, key_name): - return key_name.replace('_I_', ' | ') + return key_name.replace("_I_", " | ") @classmethod def _add_columns(cls): @@ -185,16 +201,17 @@ class ServerConfiguration(models.TransientModel): cols = chain( list(cls._get_base_cols().items()), list(cls._get_env_cols().items()), - list(cls._get_system_cols().items()) + list(cls._get_system_cols().items()), ) for col, value in cols: - col_name = col.replace('.', '_') - setattr(ServerConfiguration, - col_name, - fields.Char( - string=cls._format_key_display_name(col_name), - readonly=True) - ) + col_name = col.replace(".", "_") + setattr( + ServerConfiguration, + col_name, + fields.Char( + string=cls._format_key_display_name(col_name), readonly=True + ), + ) cls._conf_defaults[col_name] = value @classmethod @@ -202,7 +219,7 @@ class ServerConfiguration(models.TransientModel): """ Compute base fields""" res = {} for col, item in list(system_base_config.options.items()): - key = cls._format_key('odoo', col) + key = cls._format_key("odoo", col) res[key] = item return res @@ -222,7 +239,7 @@ class ServerConfiguration(models.TransientModel): """ Compute system fields""" res = {} for col, item in get_server_environment(): - key = cls._format_key('system', col) + key = cls._format_key("system", col) res[key] = item return res @@ -232,17 +249,19 @@ class ServerConfiguration(models.TransientModel): names = [] for key in sorted(items): - names.append(key.replace('.', '_')) - return ('' + - ''.join(['' % - _escape(name) for name in names]) + - '') + names.append(key.replace(".", "_")) + return ( + '' + + "".join( + ['' % _escape(name) for name in names] + ) + + "" + ) @classmethod def _build_osv(cls): """Build the view for the current configuration.""" - arch = ('
' - '') + arch = '' '' # Odoo server configuration rcfile = system_base_config.rcfile @@ -265,23 +284,23 @@ class ServerConfiguration(models.TransientModel): arch += cls._group(cls._get_system_cols()) arch += '' - arch += '' + arch += "
" cls._arch = etree.fromstring(arch) @api.model - def fields_view_get(self, view_id=None, view_type='form', toolbar=False, - submenu=False): + def fields_view_get( + self, view_id=None, view_type="form", toolbar=False, submenu=False + ): """Overwrite the default method to render the custom view.""" - res = super(ServerConfiguration, self).fields_view_get(view_id, - view_type, - toolbar) - View = self.env['ir.ui.view'] - if view_type == 'form': + res = super(ServerConfiguration, self).fields_view_get( + view_id, view_type, toolbar + ) + View = self.env["ir.ui.view"] + if view_type == "form": arch_node = self._arch - xarch, xfields = View.postprocess_and_fields( - self._name, arch_node, view_id) - res['arch'] = xarch - res['fields'] = xfields + xarch, xfields = View.postprocess_and_fields(self._name, arch_node, view_id) + res["arch"] = xarch + res["fields"] = xfields return res @api.model @@ -291,18 +310,19 @@ class ServerConfiguration(models.TransientModel): should be secret. :return: list of secret keywords """ - secret_keys = ['passw', 'key', 'secret', 'token'] + secret_keys = ["passw", "key", "secret", "token"] return any(secret_key in key for secret_key in secret_keys) @api.model def default_get(self, fields_list): res = {} if not self.env.user.has_group( - 'server_environment.has_server_configuration_access'): + "server_environment.has_server_configuration_access" + ): return res for key in self._conf_defaults: if not self.show_passwords and self._is_secret(key=key): - res[key] = '**********' + res[key] = "**********" else: res[key] = self._conf_defaults[key]() return res diff --git a/server_environment/system_info.py b/server_environment/system_info.py index 6e77f78..7b08ce1 100644 --- a/server_environment/system_info.py +++ b/server_environment/system_info.py @@ -28,38 +28,39 @@ from odoo.tools.config import config def _get_output(cmd): - bindir = config['root_path'] - p = subprocess.Popen(cmd, shell=True, cwd=bindir, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) + bindir = config["root_path"] + p = subprocess.Popen( + cmd, shell=True, cwd=bindir, stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) return p.communicate()[0].rstrip() def get_server_environment(): # inspired by server/bin/service/web_services.py try: - rev_id = 'git:%s' % _get_output('git rev-parse HEAD') + rev_id = "git:%s" % _get_output("git rev-parse HEAD") except Exception: try: - rev_id = 'bzr: %s' % _get_output('bzr revision-info') + rev_id = "bzr: %s" % _get_output("bzr revision-info") except Exception: - rev_id = 'Can not retrieve revison from git or bzr' + rev_id = "Can not retrieve revison from git or bzr" - os_lang = '.'.join([x for x in locale.getdefaultlocale() if x]) + os_lang = ".".join([x for x in locale.getdefaultlocale() if x]) if not os_lang: - os_lang = 'NOT SET' - if os.name == 'posix' and platform.system() == 'Linux': - lsbinfo = _get_output('lsb_release -a') + os_lang = "NOT SET" + if os.name == "posix" and platform.system() == "Linux": + lsbinfo = _get_output("lsb_release -a") else: - lsbinfo = 'not lsb compliant' + lsbinfo = "not lsb compliant" return ( - ('platform', platform.platform()), - ('os.name', os.name), - ('lsb_release', lsbinfo), - ('release', platform.release()), - ('version', platform.version()), - ('architecture', platform.architecture()[0]), - ('locale', os_lang), - ('python', platform.python_version()), - ('odoo', release.version), - ('revision', rev_id), + ("platform", platform.platform()), + ("os.name", os.name), + ("lsb_release", lsbinfo), + ("release", platform.release()), + ("version", platform.version()), + ("architecture", platform.architecture()[0]), + ("locale", os_lang), + ("python", platform.python_version()), + ("odoo", release.version), + ("revision", rev_id), ) diff --git a/server_environment/tests/common.py b/server_environment/tests/common.py index d74a133..b145b7a 100644 --- a/server_environment/tests/common.py +++ b/server_environment/tests/common.py @@ -6,29 +6,27 @@ from contextlib import contextmanager from unittest.mock import patch from odoo.tests import common -from odoo.addons.server_environment import server_env from odoo.tools.config import config -import odoo.addons.server_environment.models.server_env_mixin as \ - server_env_mixin +import odoo.addons.server_environment.models.server_env_mixin as server_env_mixin +from odoo.addons.server_environment import server_env class ServerEnvironmentCase(common.SavepointCase): - def setUp(self): super().setUp() - self._original_running_env = config.get('running_env') - config['running_env'] = 'testing' + self._original_running_env = config.get("running_env") + config["running_env"] = "testing" def tearDown(self): super().tearDown() - config['running_env'] = self._original_running_env + 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) + path = os.path.join(os.path.dirname(__file__), path) server_env._dir = path try: yield @@ -39,18 +37,17 @@ class ServerEnvironmentCase(common.SavepointCase): def set_env_variables(self, public=None, secret=None): newkeys = {} if public: - newkeys['SERVER_ENV_CONFIG'] = public + newkeys["SERVER_ENV_CONFIG"] = public if secret: - newkeys['SERVER_ENV_CONFIG_SECRET'] = secret - with patch.dict('os.environ', newkeys): + 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): + with self.set_config_dir(None), self.set_env_variables(public, secret): parser = server_env._load_config() server_env_mixin.serv_config = parser yield diff --git a/server_environment/tests/test_environment_variable.py b/server_environment/tests/test_environment_variable.py index 469bc18..1747406 100644 --- a/server_environment/tests/test_environment_variable.py +++ b/server_environment/tests/test_environment_variable.py @@ -3,43 +3,25 @@ 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): + 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.assertEqual(list(parser.keys()), ["DEFAULT", "section"]) self.assertDictEqual( - dict(parser['section'].items()), - {'alice': 'bob', - 'bar': 'foo', - 'foo': 'bar'} + 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): + 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') + 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 ee33798..0c30869 100644 --- a/server_environment/tests/test_server_environment.py +++ b/server_environment/tests/test_server_environment.py @@ -1,34 +1,33 @@ # Copyright 2018 Camptocamp (https://www.camptocamp.com). # License GPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo.addons.server_environment import server_env + from . import common class TestEnv(common.ServerEnvironmentCase): - def test_view(self): - model = self.env['server.config'] + model = self.env["server.config"] view = model.fields_view_get() self.assertTrue(view) def test_default(self): - model = self.env['server.config'] + model = self.env["server.config"] rec = model.create({}) defaults = rec.default_get([]) self.assertTrue(defaults) self.assertIsInstance(defaults, dict) pass_checked = False for default in defaults: - if 'passw' in default: - self.assertNotEqual(defaults[default], - '**********') + if "passw" in default: + self.assertNotEqual(defaults[default], "**********") pass_checked = True self.assertTrue(pass_checked) def test_value_retrival(self): - with self.set_config_dir('testfiles'): + 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') + val = parser.get("external_service.ftp", "user") + self.assertEqual(val, "testing") + val = parser.get("external_service.ftp", "host") + self.assertEqual(val, "sftp.example.com")