Merge PR #36 into 13.0

Signed-off-by yvaucher
This commit is contained in:
OCA-git-bot 2019-12-16 15:38:44 +00:00
commit 007e51f931
15 changed files with 745 additions and 232 deletions

291
.eslintrc Normal file
View File

@ -0,0 +1,291 @@
{
"globals": {
"$": false,
"_": false,
"fuzzy": false,
"jQuery": false,
"moment": false,
"odoo": false,
"openerp": false,
"self": false
},
"env": {
"browser": true
},
"rules": {
"no-alert": "warn",
"no-array-constructor": "warn",
"no-bitwise": "off",
"no-caller": "warn",
"no-case-declarations": "warn",
"no-catch-shadow": "warn",
"no-class-assign": "warn",
"no-cond-assign": "warn",
"no-confusing-arrow": "warn",
"no-console": "off",
"no-const-assign": "warn",
"no-constant-condition": "warn",
"no-continue": "off",
"no-control-regex": "warn",
"no-debugger": "warn",
"no-delete-var": "warn",
"no-div-regex": "warn",
"no-dupe-args": "warn",
"no-dupe-class-members": "warn",
"no-dupe-keys": "warn",
"no-duplicate-case": "warn",
"no-duplicate-imports": "warn",
"no-else-return": "warn",
"no-empty": "warn",
"no-empty-character-class": "warn",
"no-empty-function": "warn",
"no-empty-pattern": "warn",
"no-eq-null": "warn",
"no-eval": "warn",
"no-ex-assign": "warn",
"no-extend-native": "warn",
"no-extra-bind": "warn",
"no-extra-boolean-cast": "warn",
"no-extra-label": "warn",
"no-extra-parens": "warn",
"no-extra-semi": "warn",
"no-fallthrough": "warn",
"no-floating-decimal": "warn",
"no-func-assign": "warn",
"no-implicit-coercion": ["warn", {
"allow": ["~"]
}],
"no-implicit-globals": "warn",
"no-implied-eval": "warn",
"no-inline-comments": "warn",
"no-inner-declarations": "warn",
"no-invalid-regexp": "warn",
"no-invalid-this": "off",
"no-irregular-whitespace": "warn",
"no-iterator": "warn",
"no-label-var": "warn",
"no-labels": "warn",
"no-lone-blocks": "warn",
"no-lonely-if": "warn",
"no-loop-func": "off",
"no-magic-numbers": "off",
"no-mixed-operators": "warn",
"no-mixed-requires": "warn",
"no-mixed-spaces-and-tabs": "warn",
"no-multi-spaces": "warn",
"no-multi-str": "warn",
"no-multiple-empty-lines": "warn",
"no-native-reassign": "warn",
"no-negated-condition": "warn",
"no-negated-in-lhs": "warn",
"no-nested-ternary": "off",
"no-new": "warn",
"no-new-func": "warn",
"no-new-object": "warn",
"no-new-require": "warn",
"no-new-symbol": "warn",
"no-new-wrappers": "warn",
"no-obj-calls": "warn",
"no-octal": "warn",
"no-octal-escape": "warn",
"no-param-reassign": "warn",
"no-path-concat": "warn",
"no-plusplus": "off",
"no-process-env": "warn",
"no-process-exit": "warn",
"no-proto": "warn",
"no-prototype-builtins": "warn",
"no-redeclare": "warn",
"no-regex-spaces": "warn",
"no-restricted-globals": "warn",
"no-restricted-imports": "warn",
"no-restricted-modules": "warn",
"no-restricted-syntax": "warn",
"no-return-assign": "warn",
"no-script-url": "warn",
"no-self-assign": "warn",
"no-self-compare": "warn",
"no-sequences": "warn",
"no-shadow": "warn",
"no-shadow-restricted-names": "warn",
"no-whitespace-before-property": "warn",
"no-spaced-func": "warn",
"no-sparse-arrays": "warn",
"no-sync": "warn",
"no-tabs": "warn",
"no-ternary": "off",
"no-trailing-spaces": "warn",
"no-this-before-super": "warn",
"no-throw-literal": "warn",
"no-undef": "warn",
"no-undef-init": "warn",
"no-undefined": "off",
"no-unexpected-multiline": "warn",
"no-underscore-dangle": "off",
"no-unmodified-loop-condition": "warn",
"no-unneeded-ternary": "warn",
"no-unreachable": "warn",
"no-unsafe-finally": "warn",
"no-unused-expressions": "warn",
"no-unused-labels": "warn",
"no-unused-vars": "warn",
"no-use-before-define": "warn",
"no-useless-call": "warn",
"no-useless-computed-key": "warn",
"no-useless-concat": "warn",
"no-useless-constructor": "warn",
"no-useless-escape": "warn",
"no-useless-rename": "warn",
"no-void": "warn",
"no-var": "off",
"no-warning-comments": "off",
"no-with": "warn",
"array-bracket-spacing": "off",
"array-callback-return": "warn",
"arrow-body-style": "warn",
"arrow-parens": "warn",
"arrow-spacing": "off",
"accessor-pairs": "warn",
"block-scoped-var": "off",
"block-spacing": ["warn", "always"],
"brace-style": "warn",
"callback-return": "warn",
"camelcase": "off",
"capitalized-comments": ["warn", "always", {
"ignoreConsecutiveComments": true,
"ignoreInlineComments": true
}],
"comma-dangle": ["warn", "always-multiline"],
"comma-spacing": ["warn", {
"before": false,
"after": true
}],
"comma-style": "warn",
"complexity": [
"warn",
15
],
"computed-property-spacing": "off",
"consistent-return": "off",
"consistent-this": "off",
"constructor-super": "warn",
"curly": "warn",
"default-case": "off",
"dot-location": ["warn", "property"],
"dot-notation": "warn",
"eol-last": "warn",
"eqeqeq": "warn",
"func-names": "off",
"func-style": "off",
"generator-star-spacing": "off",
"global-require": "warn",
"guard-for-in": "off",
"handle-callback-err": "warn",
"id-blacklist": "warn",
"id-length": "off",
"id-match": "warn",
"indent": "warn",
"init-declarations": "warn",
"jsx-quotes": "warn",
"key-spacing": "off",
"keyword-spacing": "warn",
"linebreak-style": [
"warn",
"unix"
],
"lines-around-comment": "warn",
"max-depth": "warn",
"max-len": ["warn", {
"code": 88,
"ignorePattern": "odoo\\.define\\(",
"tabWidth": 4
}],
"max-lines": "off",
"max-nested-callbacks": "warn",
"max-params": "off",
"max-statements": "off",
"max-statements-per-line": "warn",
"multiline-ternary": "off",
"new-cap": "off",
"new-parens": "warn",
"newline-after-var": "off",
"newline-before-return": "off",
"newline-per-chained-call": "off",
"object-curly-newline": ["warn", { "consistent": true }],
"object-curly-spacing": ["warn", "never"],
"object-property-newline": ["warn", {
"allowAllPropertiesOnSameLine": true
}],
"object-shorthand": "off",
"one-var": "off",
"one-var-declaration-per-line": "off",
"operator-assignment": "warn",
"operator-linebreak": "warn",
"padded-blocks": "off",
"prefer-arrow-callback": "off",
"prefer-const": "warn",
"prefer-reflect": "off",
"prefer-rest-params": "off",
"prefer-spread": "off",
"prefer-template": "off",
"quote-props": "off",
"quotes": "off",
"radix": "warn",
"require-yield": "warn",
"rest-spread-spacing": "off",
"semi": [
"warn",
"always"
],
"semi-spacing": "warn",
"sort-imports": "warn",
"sort-vars": "off",
"space-before-blocks": "warn",
"space-before-function-paren": "warn",
"space-in-parens": "off",
"space-infix-ops": "off",
"space-unary-ops": "off",
"spaced-comment": ["warn", "always"],
"strict": ["warn", "function"],
"template-curly-spacing": "off",
"unicode-bom": "warn",
"use-isnan": "warn",
"valid-jsdoc": ["warn", {
"prefer": {
"arg": "param",
"argument": "param",
"augments": "extends",
"constructor": "class",
"exception": "throws",
"func": "function",
"method": "function",
"prop": "property",
"return": "returns",
"virtual": "abstract",
"yield": "yields"
},
"preferType": {
"array": "Array",
"bool": "Boolean",
"boolean": "Boolean",
"number": "Number",
"object": "Object",
"str": "String",
"string": "String"
},
"requireParamDescription": false,
"requireReturn": false,
"requireReturnDescription": false,
"requireReturnType": false
}],
"valid-typeof": "warn",
"vars-on-top": "off",
"wrap-iife": "warn",
"wrap-regex": "warn",
"yield-star-spacing": "off",
"yoda": "warn"
},
"parserOptions": {
"ecmaVersion": 2017
}
}

10
.flake8 Normal file
View File

@ -0,0 +1,10 @@
[flake8]
max-line-length = 80
max-complexity = 16
# B = bugbear
# B9 = bugbear opinionated (incl line length)
select = C,E,F,W,B,B9
# E203: whitespace before ':' (black behaviour)
# E501: flake8 line length (covered by bugbear B950)
# W503: line break before binary operator (black behaviour)
ignore = E203,E501,W503

12
.isort.cfg Normal file
View File

@ -0,0 +1,12 @@
[settings]
; see https://github.com/psf/black
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
combine_as_imports=True
use_parentheses=True
line_length=88
known_odoo=odoo
known_odoo_addons=odoo.addons
sections=FUTURE,STDLIB,THIRDPARTY,ODOO,ODOO_ADDONS,FIRSTPARTY,LOCALFOLDER
known_third_party=lxml,setuptools

68
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,68 @@
exclude: "^setup/|/static/lib/|/static/src/lib/"
default_language_version:
python: python3
repos:
- repo: https://github.com/psf/black
rev: 19.3b0
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
hooks:
- id: trailing-whitespace
# exclude autogenerated files
exclude: /README\.rst$|\.pot?$
- id: end-of-file-fixer
# exclude autogenerated files
exclude: /README\.rst$|\.pot?$
- id: debug-statements
- id: flake8
name: flake8 except __init__.py
exclude: /__init__\.py$
additional_dependencies: ["flake8-bugbear==19.8.0"]
- id: flake8
name: flake8 only __init__.py
args: ["--extend-ignore=F401"] # ignore unused imports in __init__.py
files: /__init__\.py$
additional_dependencies: ["flake8-bugbear==19.8.0"]
- id: fix-encoding-pragma
args: ["--remove"]
- id: check-case-conflict
- id: check-docstring-first
- id: check-executables-have-shebangs
- id: check-merge-conflict
- id: check-symlinks
- id: check-xml
- id: mixed-line-ending
args: ["--fix=lf"]
- repo: https://github.com/pre-commit/mirrors-pylint
rev: v2.3.1
hooks:
- id: pylint
name: pylint with optional checks
args: ["--rcfile=.pylintrc", "--exit-zero"]
verbose: true
additional_dependencies: ["pylint-odoo==3.0.3"]
- id: pylint
name: pylint with mandatory checks
args: ["--rcfile=.pylintrc-mandatory"]
additional_dependencies: ["pylint-odoo==3.0.3"]
- repo: https://github.com/asottile/pyupgrade
rev: v1.24.0
hooks:
- id: pyupgrade
- repo: https://github.com/asottile/seed-isort-config
rev: v1.9.3
hooks:
- id: seed-isort-config
- repo: https://github.com/pre-commit/mirrors-isort
rev: v4.3.21
hooks:
- id: isort
name: isort except __init__.py
exclude: /__init__\.py$
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v6.5.1
hooks:
- id: eslint
verbose: true

87
.pylintrc Normal file
View File

@ -0,0 +1,87 @@
[MASTER]
load-plugins=pylint_odoo
score=n
[ODOOLINT]
readme_template_url="https://github.com/OCA/maintainer-tools/blob/master/template/module/README.rst"
manifest_required_authors=Odoo Community Association (OCA)
manifest_required_keys=license
manifest_deprecated_keys=description,active
license_allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3
valid_odoo_versions=13.0
[MESSAGES CONTROL]
disable=all
# This .pylintrc contains optional AND mandatory checks and is meant to be
# loaded in an IDE to have it check everything, in the hope this will make
# optional checks more visible to contributors who otherwise never look at a
# green travis to see optional checks that failed.
# .pylintrc-mandatory containing only mandatory checks is used the pre-commit
# config as a blocking check.
enable=anomalous-backslash-in-string,
api-one-deprecated,
api-one-multi-together,
assignment-from-none,
attribute-deprecated,
class-camelcase,
dangerous-default-value,
dangerous-view-replace-wo-priority,
duplicate-id-csv,
duplicate-key,
duplicate-xml-fields,
duplicate-xml-record-id,
eval-referenced,
eval-used,
incoherent-interpreter-exec-perm,
license-allowed,
manifest-author-string,
manifest-deprecated-key,
manifest-required-author,
manifest-required-key,
manifest-version-format,
method-compute,
method-inverse,
method-required-super,
method-search,
missing-import-error,
missing-manifest-dependency,
openerp-exception-warning,
pointless-statement,
pointless-string-statement,
print-used,
redundant-keyword-arg,
redundant-modulename-xml,
reimported,
relative-import,
return-in-init,
rst-syntax-error,
sql-injection,
too-few-format-args,
translation-field,
translation-required,
unreachable,
use-vim-comment,
wrong-tabs-instead-of-spaces,
xml-syntax-error,
# messages that do not cause the lint step to fail
consider-merging-classes-inherited,
create-user-wo-reset-password,
dangerous-filter-wo-user,
deprecated-module,
file-not-used,
invalid-commit,
missing-newline-extrafiles,
missing-readme,
no-utf8-coding-comment,
odoo-addons-relative-import,
old-api7-method-defined,
redefined-builtin,
too-complex,
unnecessary-utf8-coding-comment
[REPORTS]
msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}
output-format=colorized
reports=no

65
.pylintrc-mandatory Normal file
View File

@ -0,0 +1,65 @@
[MASTER]
load-plugins=pylint_odoo
score=n
[ODOOLINT]
readme_template_url="https://github.com/OCA/maintainer-tools/blob/master/template/module/README.rst"
manifest_required_authors=Odoo Community Association (OCA)
manifest_required_keys=license
manifest_deprecated_keys=description,active
license_allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3
valid_odoo_versions=13.0
[MESSAGES CONTROL]
disable=all
enable=anomalous-backslash-in-string,
api-one-deprecated,
api-one-multi-together,
assignment-from-none,
attribute-deprecated,
class-camelcase,
dangerous-default-value,
dangerous-view-replace-wo-priority,
duplicate-id-csv,
duplicate-key,
duplicate-xml-fields,
duplicate-xml-record-id,
eval-referenced,
eval-used,
incoherent-interpreter-exec-perm,
license-allowed,
manifest-author-string,
manifest-deprecated-key,
manifest-required-author,
manifest-required-key,
manifest-version-format,
method-compute,
method-inverse,
method-required-super,
method-search,
missing-import-error,
missing-manifest-dependency,
openerp-exception-warning,
pointless-statement,
pointless-string-statement,
print-used,
redundant-keyword-arg,
redundant-modulename-xml,
reimported,
relative-import,
return-in-init,
rst-syntax-error,
sql-injection,
too-few-format-args,
translation-field,
translation-required,
unreachable,
use-vim-comment,
wrong-tabs-instead-of-spaces,
xml-syntax-error
[REPORTS]
msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}
output-format=colorized
reports=no

View File

@ -1,36 +1,47 @@
language: python language: python
sudo: false cache:
cache: pip directories:
- $HOME/.cache/pip
- $HOME/.cache/pre-commit
python: python:
- "3.5" - "3.6"
addons: addons:
postgresql: "9.6" postgresql: "9.6"
apt: apt:
packages: packages:
- expect-dev # provides unbuffer utility - expect-dev # provides unbuffer utility
- python-lxml # because pip installation is slow
stages:
- linting
- test
jobs:
include:
- stage: linting
name: "pre-commit"
before_install:
install: pip install pre-commit
script: pre-commit run --all --show-diff-on-failure
after_success:
- stage: test
env:
- TESTS="1" ODOO_REPO="odoo/odoo" MAKEPOT="1"
- stage: test
env:
- TESTS="1" ODOO_REPO="OCA/OCB"
env: env:
global: global:
- VERSION="13.0" TESTS="0" LINT_CHECK="0" MAKEPOT="0" - VERSION="13.0" TESTS="0" LINT_CHECK="0" MAKEPOT="0"
matrix:
- LINT_CHECK="1"
- TESTS="1" ODOO_REPO="OCA/OCB"
- TESTS="1" ODOO_REPO="odoo/odoo" MAKEPOT="1"
install: install:
- pip install -q unidecode
- pip install unicodecsv
- git clone --depth=1 https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools - git clone --depth=1 https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools
- export PATH=${HOME}/maintainer-quality-tools/travis:${PATH} - export PATH=${HOME}/maintainer-quality-tools/travis:${PATH}
- travis_install_nightly - travis_install_nightly
# Requirements to test server_environment modules # Requirements to test server_environment modules
- printf '[options]\n\nrunning_env = dev\n' > ${HOME}/.openerp_serverrc - printf '[options]\n\nrunning_env = dev\n' > ${HOME}/.openerp_serverrc
- ln -s ${TRAVIS_BUILD_DIR}/server_environment_files_sample ${TRAVIS_BUILD_DIR}/server_environment_files
script: script:
- travis_run_tests - travis_run_tests

View File

@ -658,4 +658,4 @@ specific requirements.
You should also get your employer (if you work as a programmer) or school, You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>. <http://www.gnu.org/licenses/>.

View File

@ -5,18 +5,12 @@
{ {
"name": "server configuration environment files", "name": "server configuration environment files",
"version": "13.0.2.0.0", "version": "13.0.2.0.0",
"depends": [ "depends": ["base", "base_sparse_field"],
"base",
"base_sparse_field",
],
"author": "Camptocamp,Odoo Community Association (OCA)", "author": "Camptocamp,Odoo Community Association (OCA)",
"summary": "move some configurations out of the database", "summary": "move some configurations out of the database",
"website": "http://github.com/OCA/server-env", "website": "http://github.com/OCA/server-env",
"license": "GPL-3 or any later version", "license": "GPL-3 or any later version",
"category": "Tools", "category": "Tools",
"data": [ "data": ["security/res_groups.xml", "serv_config.xml"],
'security/res_groups.xml', "installable": True,
'serv_config.xml',
],
'installable': True,
} }

View File

@ -2,12 +2,12 @@
# License GPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License GPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging import logging
from functools import partialmethod from functools import partialmethod
from lxml import etree from lxml import etree
from odoo import api, fields, models from odoo import api, fields, models
from ..server_env import serv_config from ..server_env import serv_config
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -96,19 +96,20 @@ class ServerEnvMixin(models.AbstractModel):
``keychain.backend``. ``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_defaults = fields.Serialized()
_server_env_getter_mapping = { _server_env_getter_mapping = {
'integer': 'getint', "integer": "getint",
'float': 'getfloat', "float": "getfloat",
'monetary': 'getfloat', "monetary": "getfloat",
'boolean': 'getboolean', "boolean": "getboolean",
'char': 'get', "char": "get",
'selection': 'get', "selection": "get",
'text': 'get', "text": "get",
} }
@property @property
@ -180,16 +181,13 @@ class ServerEnvMixin(models.AbstractModel):
# _server_env_has_key_defined so we are sure that the value is # _server_env_has_key_defined so we are sure that the value is
# either in the global or the record config # either in the global or the record config
getter = getattr(serv_config, config_getter) getter = getattr(serv_config, config_getter)
if (section_name in serv_config if section_name in serv_config and field_name in serv_config[section_name]:
and field_name in serv_config[section_name]):
value = getter(section_name, field_name) value = getter(section_name, field_name)
else: else:
value = getter(global_section_name, field_name) value = getter(global_section_name, field_name)
except Exception: except Exception:
_logger.exception( _logger.exception(
"error trying to read field %s in section %s", "error trying to read field %s in section %s", field_name, section_name
field_name,
section_name,
) )
return False return False
return value return value
@ -203,34 +201,31 @@ class ServerEnvMixin(models.AbstractModel):
and field_name in serv_config[global_section_name] and field_name in serv_config[global_section_name]
) )
has_config = ( has_config = (
section_name in serv_config section_name in serv_config and field_name in serv_config[section_name]
and field_name in serv_config[section_name]
) )
return has_global_config or has_config return has_global_config or has_config
def _compute_server_env_from_config(self, field_name, options): 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: if not getter_name:
field_type = self._fields[field_name].type field_type = self._fields[field_name].type
getter_name = self._server_env_getter_mapping.get(field_type) getter_name = self._server_env_getter_mapping.get(field_type)
if not getter_name: if not getter_name:
# if you get this message and the field is working as expected, # if you get this message and the field is working as expected,
# you may want to add the type in _server_env_getter_mapping # 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, ' _logger.warning(
'which may not be supported properly') "server.env.mixin is used on a field of type %s, "
getter_name = 'get' "which may not be supported properly"
value = self._server_env_read_from_config( )
field_name, getter_name getter_name = "get"
) value = self._server_env_read_from_config(field_name, getter_name)
self[field_name] = value self[field_name] = value
def _compute_server_env_from_default(self, field_name, options): def _compute_server_env_from_default(self, field_name, options):
if options and options.get('compute_default'): if options and options.get("compute_default"):
getattr(self, options['compute_default'])() getattr(self, options["compute_default"])()
else: else:
default_field = self._server_env_default_fieldname( default_field = self._server_env_default_fieldname(field_name)
field_name
)
if default_field: if default_field:
self[field_name] = self[default_field] self[field_name] = self[default_field]
else: else:
@ -248,9 +243,7 @@ class ServerEnvMixin(models.AbstractModel):
record._compute_server_env_from_config(field_name, options) record._compute_server_env_from_config(field_name, options)
else: else:
record._compute_server_env_from_default( record._compute_server_env_from_default(field_name, options)
field_name, options
)
def _inverse_server_env(self, field_name): def _inverse_server_env(self, field_name):
options = self._server_env_fields[field_name] options = self._server_env_fields[field_name]
@ -262,8 +255,8 @@ class ServerEnvMixin(models.AbstractModel):
# we update the default value in database # we update the default value in database
if record[is_editable_field]: if record[is_editable_field]:
if options and options.get('inverse_default'): if options and options.get("inverse_default"):
getattr(record, options['inverse_default'])() getattr(record, options["inverse_default"])()
elif default_field: elif default_field:
record[default_field] = record[field_name] 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 # in ``_inverse_server_env`` it would reset the value of the field
for record in self: for record in self:
for field_name in self._server_env_fields: for field_name in self._server_env_fields:
is_editable_field = self._server_env_is_editable_fieldname( is_editable_field = self._server_env_is_editable_fieldname(field_name)
field_name is_editable = not record._server_env_has_key_defined(field_name)
)
is_editable = not record._server_env_has_key_defined(
field_name
)
record[is_editable_field] = is_editable record[is_editable_field] = is_editable
def _server_env_view_set_readonly(self, view_arch): 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): for elem in view_arch.findall(field_xpath % field):
# set env-computed fields to readonly if the configuration # set env-computed fields to readonly if the configuration
# files have a key set for this field # files have a key set for this field
elem.set('attrs', elem.set("attrs", str({"readonly": [(is_editable_field, "=", False)]}))
str({'readonly': [(is_editable_field, '=', False)]}))
if not view_arch.findall(field_xpath % is_editable_field): if not view_arch.findall(field_xpath % is_editable_field):
# add the _is_editable fields in the view for the 'attrs' # add the _is_editable fields in the view for the 'attrs'
# domain # domain
view_arch.append( view_arch.append(
etree.Element( etree.Element("field", name=is_editable_field, invisible="1")
'field',
name=is_editable_field,
invisible="1"
)
) )
return view_arch return view_arch
def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, def _fields_view_get(
submenu=False): self, view_id=None, view_type="form", toolbar=False, submenu=False
):
view_data = super()._fields_view_get( view_data = super()._fields_view_get(
view_id=view_id, view_type=view_type, view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu
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_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 return view_data
def _server_env_default_fieldname(self, base_field_name): def _server_env_default_fieldname(self, base_field_name):
"""Return the name of the field with default value""" """Return the name of the field with default value"""
options = self._server_env_fields[base_field_name] options = self._server_env_fields[base_field_name]
if options and options.get('no_default_field'): if options and options.get("no_default_field"):
return '' return ""
return '%s_env_default' % (base_field_name,) return "{}_env_default".format(base_field_name)
def _server_env_is_editable_fieldname(self, 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"
@ -331,16 +315,14 @@ class ServerEnvMixin(models.AbstractModel):
This is the field used to tell if the env-computed field can This is the field used to tell if the env-computed field can
be edited. 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): def _server_env_transform_field_to_read_from_env(self, field):
"""Transform the original field in a computed 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_name = "_inverse_server_env_%s" % field.name
inverse_method = partialmethod( inverse_method = partialmethod(type(self)._inverse_server_env, field.name)
type(self)._inverse_server_env, field.name
)
setattr(type(self), inverse_method_name, inverse_method) setattr(type(self), inverse_method_name, inverse_method)
field.inverse = inverse_method_name field.inverse = inverse_method_name
field.store = False field.store = False
@ -360,7 +342,7 @@ class ServerEnvMixin(models.AbstractModel):
# (inherits), we want to override it with a new one # (inherits), we want to override it with a new one
if fieldname not in self._fields or self._fields[fieldname].inherited: if fieldname not in self._fields or self._fields[fieldname].inherited:
field = fields.Boolean( field = fields.Boolean(
compute='_compute_server_env_is_editable', compute="_compute_server_env_is_editable",
automatic=True, automatic=True,
# this is required to be able to edit fields # this is required to be able to edit fields
# on new records # on new records
@ -385,14 +367,11 @@ class ServerEnvMixin(models.AbstractModel):
if fieldname not in self._fields or self._fields[fieldname].inherited: if fieldname not in self._fields or self._fields[fieldname].inherited:
base_field_cls = base_field.__class__ base_field_cls = base_field.__class__
field_args = base_field.args.copy() field_args = base_field.args.copy()
field_args.pop('_sequence', None) field_args.pop("_sequence", None)
field_args.update({ field_args.update({"sparse": "server_env_defaults", "automatic": True})
'sparse': 'server_env_defaults',
'automatic': True,
})
if hasattr(base_field, 'selection'): if hasattr(base_field, "selection"):
field_args['selection'] = base_field.selection field_args["selection"] = base_field.selection
field = base_field_cls(**field_args) field = base_field_cls(**field_args)
self._add_field(fieldname, field) self._add_field(fieldname, field)

View File

@ -18,13 +18,14 @@
# #
############################################################################## ##############################################################################
import configparser
import logging import logging
import os import os
import configparser
from lxml import etree
from itertools import chain 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 odoo.tools.config import config as system_base_config
from .system_info import get_server_environment from .system_info import get_server_environment
@ -33,19 +34,29 @@ _logger = logging.getLogger(__name__)
try: try:
from odoo.addons import server_environment_files from odoo.addons import server_environment_files
_dir = os.path.dirname(server_environment_files.__file__) _dir = os.path.dirname(server_environment_files.__file__)
except ImportError: except ImportError:
_logger.info('not using server_environment_files for configuration,' _logger.info(
' no directory found') "not using server_environment_files for configuration," " no directory found"
)
_dir = None _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 # Same dict as RawConfigParser._boolean_states
_boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True, _boolean_states = {
'0': False, 'no': False, 'false': False, 'off': False} "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( raise Exception(
"The parameter 'running_env' has not be set neither in base config " "The parameter 'running_env' has not be set neither in base config "
"file option -c or in openerprc.\n" "file option -c or in openerprc.\n"
@ -56,7 +67,7 @@ if not system_base_config.get('running_env', False):
ck_path = None ck_path = None
if _dir: 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): if not os.path.exists(ck_path):
raise Exception( raise Exception(
@ -77,25 +88,29 @@ def setboolean(obj, attr, _bool=None):
# Borrowed from MarkupSafe # Borrowed from MarkupSafe
def _escape(s): def _escape(s):
"""Convert the characters &<>'" in string s to HTML-safe sequences.""" """Convert the characters &<>'" in string s to HTML-safe sequences."""
return (str(s).replace('&', '&amp;') return (
.replace('>', '&gt;') str(s)
.replace('<', '&lt;') .replace("&", "&amp;")
.replace("'", '&#39;') .replace(">", "&gt;")
.replace('"', '&#34;')) .replace("<", "&lt;")
.replace("'", "&#39;")
.replace('"', "&#34;")
)
def _listconf(env_path): def _listconf(env_path):
"""List configuration files in a folder.""" """List configuration files in a folder."""
files = [os.path.join(env_path, name) files = [
for name in sorted(os.listdir(env_path)) os.path.join(env_path, name)
if name.endswith('.conf')] for name in sorted(os.listdir(env_path))
if name.endswith(".conf")
]
return files return files
def _load_config_from_server_env_files(config_p): def _load_config_from_server_env_files(config_p):
default = os.path.join(_dir, 'default') default = os.path.join(_dir, "default")
running_env = os.path.join(_dir, running_env = os.path.join(_dir, system_base_config["running_env"])
system_base_config['running_env'])
if os.path.isdir(default): if os.path.isdir(default):
conf_files = _listconf(default) + _listconf(running_env) conf_files = _listconf(default) + _listconf(running_env)
else: else:
@ -104,12 +119,12 @@ def _load_config_from_server_env_files(config_p):
try: try:
config_p.read(conf_files) config_p.read(conf_files)
except Exception as e: except Exception as e:
raise Exception('Cannot read config files "%s": %s' % (conf_files, e)) raise Exception('Cannot read config files "{}": {}'.format(conf_files, e))
def _load_config_from_rcfile(config_p): def _load_config_from_rcfile(config_p):
config_p.read(system_base_config.rcfile) config_p.read(system_base_config.rcfile)
config_p.remove_section('options') config_p.remove_section("options")
def _load_config_from_env(config_p): def _load_config_from_env(config_p):
@ -120,8 +135,7 @@ def _load_config_from_env(config_p):
config_p.read_string(env_config) config_p.read_string(env_config)
except configparser.Error as err: except configparser.Error as err:
raise Exception( raise Exception(
'%s content could not be parsed: %s' "{} content could not be parsed: {}".format(varname, err)
% (varname, err,)
) )
@ -147,13 +161,15 @@ class _Defaults(dict):
def __setitem__(self, key, value): def __setitem__(self, key, value):
def func(*a): def func(*a):
return str(value) return str(value)
return dict.__setitem__(self, key, func) return dict.__setitem__(self, key, func)
class ServerConfiguration(models.TransientModel): class ServerConfiguration(models.TransientModel):
"""Display server configuration.""" """Display server configuration."""
_name = 'server.config'
_description = 'Display server configuration' _name = "server.config"
_description = "Display server configuration"
_conf_defaults = _Defaults() _conf_defaults = _Defaults()
@classmethod @classmethod
@ -164,20 +180,20 @@ class ServerConfiguration(models.TransientModel):
""" """
ModelClass = super(ServerConfiguration, cls)._build_model(pool, cr) ModelClass = super(ServerConfiguration, cls)._build_model(pool, cr)
ModelClass._add_columns() ModelClass._add_columns()
ModelClass.running_env = system_base_config['running_env'] ModelClass.running_env = system_base_config["running_env"]
# Only show passwords in development # 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._arch = None
ModelClass._build_osv() ModelClass._build_osv()
return ModelClass return ModelClass
@classmethod @classmethod
def _format_key(cls, section, key): def _format_key(cls, section, key):
return '%s_I_%s' % (section, key) return "{}_I_{}".format(section, key)
@classmethod @classmethod
def _format_key_display_name(cls, key_name): def _format_key_display_name(cls, key_name):
return key_name.replace('_I_', ' | ') return key_name.replace("_I_", " | ")
@classmethod @classmethod
def _add_columns(cls): def _add_columns(cls):
@ -185,16 +201,17 @@ class ServerConfiguration(models.TransientModel):
cols = chain( cols = chain(
list(cls._get_base_cols().items()), list(cls._get_base_cols().items()),
list(cls._get_env_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: for col, value in cols:
col_name = col.replace('.', '_') col_name = col.replace(".", "_")
setattr(ServerConfiguration, setattr(
col_name, ServerConfiguration,
fields.Char( col_name,
string=cls._format_key_display_name(col_name), fields.Char(
readonly=True) string=cls._format_key_display_name(col_name), readonly=True
) ),
)
cls._conf_defaults[col_name] = value cls._conf_defaults[col_name] = value
@classmethod @classmethod
@ -202,7 +219,7 @@ class ServerConfiguration(models.TransientModel):
""" Compute base fields""" """ Compute base fields"""
res = {} res = {}
for col, item in list(system_base_config.options.items()): 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 res[key] = item
return res return res
@ -222,7 +239,7 @@ class ServerConfiguration(models.TransientModel):
""" Compute system fields""" """ Compute system fields"""
res = {} res = {}
for col, item in get_server_environment(): for col, item in get_server_environment():
key = cls._format_key('system', col) key = cls._format_key("system", col)
res[key] = item res[key] = item
return res return res
@ -232,17 +249,19 @@ class ServerConfiguration(models.TransientModel):
names = [] names = []
for key in sorted(items): for key in sorted(items):
names.append(key.replace('.', '_')) names.append(key.replace(".", "_"))
return ('<group col="2" colspan="4">' + return (
''.join(['<field name="%s" readonly="1"/>' % '<group col="2" colspan="4">'
_escape(name) for name in names]) + + "".join(
'</group>') ['<field name="%s" readonly="1"/>' % _escape(name) for name in names]
)
+ "</group>"
)
@classmethod @classmethod
def _build_osv(cls): def _build_osv(cls):
"""Build the view for the current configuration.""" """Build the view for the current configuration."""
arch = ('<form string="Configuration Form">' arch = '<form string="Configuration Form">' '<notebook colspan="4">'
'<notebook colspan="4">')
# Odoo server configuration # Odoo server configuration
rcfile = system_base_config.rcfile rcfile = system_base_config.rcfile
@ -265,23 +284,21 @@ class ServerConfiguration(models.TransientModel):
arch += cls._group(cls._get_system_cols()) arch += cls._group(cls._get_system_cols())
arch += '<separator colspan="4"/></page>' arch += '<separator colspan="4"/></page>'
arch += '</notebook></form>' arch += "</notebook></form>"
cls._arch = etree.fromstring(arch) cls._arch = etree.fromstring(arch)
@api.model @api.model
def fields_view_get(self, view_id=None, view_type='form', toolbar=False, def fields_view_get(
submenu=False): self, view_id=None, view_type="form", toolbar=False, submenu=False
):
"""Overwrite the default method to render the custom view.""" """Overwrite the default method to render the custom view."""
res = super(ServerConfiguration, self).fields_view_get(view_id, res = super().fields_view_get(view_id, view_type, toolbar)
view_type, View = self.env["ir.ui.view"]
toolbar) if view_type == "form":
View = self.env['ir.ui.view']
if view_type == 'form':
arch_node = self._arch arch_node = self._arch
xarch, xfields = View.postprocess_and_fields( xarch, xfields = View.postprocess_and_fields(self._name, arch_node, view_id)
self._name, arch_node, view_id) res["arch"] = xarch
res['arch'] = xarch res["fields"] = xfields
res['fields'] = xfields
return res return res
@api.model @api.model
@ -291,18 +308,19 @@ class ServerConfiguration(models.TransientModel):
should be secret. should be secret.
:return: list of secret keywords :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) return any(secret_key in key for secret_key in secret_keys)
@api.model @api.model
def default_get(self, fields_list): def default_get(self, fields_list):
res = {} res = super().default_get(fields_list)
if not self.env.user.has_group( if not self.env.user.has_group(
'server_environment.has_server_configuration_access'): "server_environment.has_server_configuration_access"
):
return res return res
for key in self._conf_defaults: for key in self._conf_defaults:
if not self.show_passwords and self._is_secret(key=key): if not self.show_passwords and self._is_secret(key=key):
res[key] = '**********' res[key] = "**********"
else: else:
res[key] = self._conf_defaults[key]() res[key] = self._conf_defaults[key]()
return res return res

View File

@ -28,38 +28,39 @@ from odoo.tools.config import config
def _get_output(cmd): def _get_output(cmd):
bindir = config['root_path'] bindir = config["root_path"]
p = subprocess.Popen(cmd, shell=True, cwd=bindir, stdout=subprocess.PIPE, p = subprocess.Popen(
stderr=subprocess.STDOUT) cmd, shell=True, cwd=bindir, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
return p.communicate()[0].rstrip() return p.communicate()[0].rstrip()
def get_server_environment(): def get_server_environment():
# inspired by server/bin/service/web_services.py # inspired by server/bin/service/web_services.py
try: try:
rev_id = 'git:%s' % _get_output('git rev-parse HEAD') rev_id = "git:%s" % _get_output("git rev-parse HEAD")
except Exception: except Exception:
try: try:
rev_id = 'bzr: %s' % _get_output('bzr revision-info') rev_id = "bzr: %s" % _get_output("bzr revision-info")
except Exception: 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: if not os_lang:
os_lang = 'NOT SET' os_lang = "NOT SET"
if os.name == 'posix' and platform.system() == 'Linux': if os.name == "posix" and platform.system() == "Linux":
lsbinfo = _get_output('lsb_release -a') lsbinfo = _get_output("lsb_release -a")
else: else:
lsbinfo = 'not lsb compliant' lsbinfo = "not lsb compliant"
return ( return (
('platform', platform.platform()), ("platform", platform.platform()),
('os.name', os.name), ("os.name", os.name),
('lsb_release', lsbinfo), ("lsb_release", lsbinfo),
('release', platform.release()), ("release", platform.release()),
('version', platform.version()), ("version", platform.version()),
('architecture', platform.architecture()[0]), ("architecture", platform.architecture()[0]),
('locale', os_lang), ("locale", os_lang),
('python', platform.python_version()), ("python", platform.python_version()),
('odoo', release.version), ("odoo", release.version),
('revision', rev_id), ("revision", rev_id),
) )

View File

@ -6,29 +6,27 @@ from contextlib import contextmanager
from unittest.mock import patch from unittest.mock import patch
from odoo.tests import common from odoo.tests import common
from odoo.addons.server_environment import server_env
from odoo.tools.config import config from odoo.tools.config import config
import odoo.addons.server_environment.models.server_env_mixin as \ import odoo.addons.server_environment.models.server_env_mixin as server_env_mixin
server_env_mixin from odoo.addons.server_environment import server_env
class ServerEnvironmentCase(common.SavepointCase): class ServerEnvironmentCase(common.SavepointCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self._original_running_env = config.get('running_env') self._original_running_env = config.get("running_env")
config['running_env'] = 'testing' config["running_env"] = "testing"
def tearDown(self): def tearDown(self):
super().tearDown() super().tearDown()
config['running_env'] = self._original_running_env config["running_env"] = self._original_running_env
@contextmanager @contextmanager
def set_config_dir(self, path): def set_config_dir(self, path):
original_dir = server_env._dir original_dir = server_env._dir
if path and not os.path.isabs(path): 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 server_env._dir = path
try: try:
yield yield
@ -39,18 +37,17 @@ class ServerEnvironmentCase(common.SavepointCase):
def set_env_variables(self, public=None, secret=None): def set_env_variables(self, public=None, secret=None):
newkeys = {} newkeys = {}
if public: if public:
newkeys['SERVER_ENV_CONFIG'] = public newkeys["SERVER_ENV_CONFIG"] = public
if secret: if secret:
newkeys['SERVER_ENV_CONFIG_SECRET'] = secret newkeys["SERVER_ENV_CONFIG_SECRET"] = secret
with patch.dict('os.environ', newkeys): with patch.dict("os.environ", newkeys):
yield yield
@contextmanager @contextmanager
def load_config(self, public=None, secret=None): def load_config(self, public=None, secret=None):
original_serv_config = server_env_mixin.serv_config original_serv_config = server_env_mixin.serv_config
try: try:
with self.set_config_dir(None), \ with self.set_config_dir(None), self.set_env_variables(public, secret):
self.set_env_variables(public, secret):
parser = server_env._load_config() parser = server_env._load_config()
server_env_mixin.serv_config = parser server_env_mixin.serv_config = parser
yield yield

View File

@ -3,43 +3,25 @@
from odoo.addons.server_environment import server_env from odoo.addons.server_environment import server_env
from .common import ServerEnvironmentCase from .common import ServerEnvironmentCase
class TestEnvironmentVariables(ServerEnvironmentCase): class TestEnvironmentVariables(ServerEnvironmentCase):
def test_env_variables(self): def test_env_variables(self):
public = ( public = "[section]\n" "foo=bar\n" "bar=baz\n"
"[section]\n" secret = "[section]\n" "bar=foo\n" "alice=bob\n"
"foo=bar\n" with self.set_config_dir(None), self.set_env_variables(public, secret):
"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() parser = server_env._load_config()
self.assertEqual( self.assertEqual(list(parser.keys()), ["DEFAULT", "section"])
list(parser.keys()),
['DEFAULT', 'section']
)
self.assertDictEqual( self.assertDictEqual(
dict(parser['section'].items()), dict(parser["section"].items()),
{'alice': 'bob', {"alice": "bob", "bar": "foo", "foo": "bar"},
'bar': 'foo',
'foo': 'bar'}
) )
def test_env_variables_override(self): def test_env_variables_override(self):
public = ( public = "[external_service.ftp]\n" "user=foo\n"
"[external_service.ftp]\n" with self.set_config_dir("testfiles"), self.set_env_variables(public):
"user=foo\n"
)
with self.set_config_dir('testfiles'), \
self.set_env_variables(public):
parser = server_env._load_config() parser = server_env._load_config()
val = parser.get('external_service.ftp', 'user') val = parser.get("external_service.ftp", "user")
self.assertEqual(val, 'foo') self.assertEqual(val, "foo")

View File

@ -1,34 +1,32 @@
# Copyright 2018 Camptocamp (https://www.camptocamp.com). # Copyright 2018 Camptocamp (https://www.camptocamp.com).
# License GPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License GPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.addons.server_environment import server_env from .. import server_env
from . import common from . import common
class TestEnv(common.ServerEnvironmentCase): class TestEnv(common.ServerEnvironmentCase):
def test_view(self): def test_view(self):
model = self.env['server.config'] model = self.env["server.config"]
view = model.fields_view_get() view = model.fields_view_get()
self.assertTrue(view) self.assertTrue(view)
def test_default(self): def test_default(self):
model = self.env['server.config'] model = self.env["server.config"]
rec = model.create({}) rec = model.create({})
defaults = rec.default_get([]) defaults = rec.default_get([])
self.assertTrue(defaults) self.assertTrue(defaults)
self.assertIsInstance(defaults, dict) self.assertIsInstance(defaults, dict)
pass_checked = False pass_checked = False
for default in defaults: for default in defaults:
if 'passw' in default: if "passw" in default:
self.assertNotEqual(defaults[default], self.assertNotEqual(defaults[default], "**********")
'**********')
pass_checked = True pass_checked = True
self.assertTrue(pass_checked) self.assertTrue(pass_checked)
def test_value_retrival(self): def test_value_retrival(self):
with self.set_config_dir('testfiles'): with self.set_config_dir("testfiles"):
parser = server_env._load_config() parser = server_env._load_config()
val = parser.get('external_service.ftp', 'user') val = parser.get("external_service.ftp", "user")
self.assertEqual(val, 'testing') self.assertEqual(val, "testing")
val = parser.get('external_service.ftp', 'host') val = parser.get("external_service.ftp", "host")
self.assertEqual(val, 'sftp.example.com') self.assertEqual(val, "sftp.example.com")