forked from OCA/server-env
-
Notifications
You must be signed in to change notification settings - Fork 0
/
server_env_mixin.py
395 lines (329 loc) · 15.7 KB
/
server_env_mixin.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# Copyright 2018 Camptocamp (https://www.camptocamp.com).
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html)
import logging
from functools import partialmethod
from lxml import etree
from odoo import api, fields, models
from odoo.addons.base_sparse_field.models.fields import Serialized
from ..server_env import serv_config
_logger = logging.getLogger(__name__)
class ServerEnvMixin(models.AbstractModel):
"""Mixin to add server environment in existing models
Usage
-----
::
class StorageBackend(models.Model):
_name = "storage.backend"
_inherit = ["storage.backend", "server.env.mixin"]
@property
def _server_env_fields(self):
return {"directory_path": {}}
With the snippet above, the "storage.backend" model now uses a server
environment configuration for the field ``directory_path``.
Under the hood, this mixin automatically replaces the original field
by an env-computed field that reads from the configuration files.
By default, it looks for the configuration in a section named
``[model_name.Record Name]`` where ``model_name`` is the ``_name`` of the
model with ``.`` replaced by ``_``. Then in a global section which is only
the name of the model. They can be customized by overriding the method
:meth:`~_server_env_section_name` and
:meth:`~_server_env_global_section_name`.
For each field transformed to an env-computed field, a companion field
``<field>_env_default`` is automatically created. When its value is set
and the configuration files do not contain a key for that field, the
env-computed field uses the default value stored in database. If there is a
key for this field but it is empty, the env-computed field has an empty
value.
Env-computed fields are conditionally editable, based on the absence
of their key in environment configuration files. When edited, their
value is stored in the database.
Integration with keychain
-------------------------
The keychain addon is used account information, encrypting the password
with a key per environment.
The default behavior of server_environment is to store the default fields
in a serialized field, so the password would lend there unencrypted.
You can benefit from keychain by using custom compute/inverse methods to
get/set the password field:
::
class StorageBackend(models.Model):
_name = 'storage.backend'
_inherit = ['keychain.backend', 'collection.base']
@property
def _server_env_fields(self):
base_fields = super()._server_env_fields
sftp_fields = {
"sftp_server": {},
"sftp_port": {},
"sftp_login": {},
"sftp_password": {
"no_default_field": True,
"compute_default": "_compute_password",
"inverse_default": "_inverse_password",
},
}
sftp_fields.update(base_fields)
return sftp_fields
* ``no_default_field`` means that no new (sparse) field need to be
created, it already is provided by keychain
* ``compute_default`` is the name of the compute method to get the default
value when no key is set in the configuration files.
``_compute_password`` is implemented by ``keychain.backend``.
* ``inverse_default`` is the name of the compute method to set the default
value when it is editable. ``_inverse_password`` is implemented by
``keychain.backend``.
"""
_name = "server.env.mixin"
_description = "Mixin to add server environment in existing models"
server_env_defaults = Serialized()
_server_env_getter_mapping = {
"integer": "getint",
"float": "getfloat",
"monetary": "getfloat",
"boolean": "getboolean",
"char": "get",
"selection": "get",
"text": "get",
}
@property
def _server_env_fields(self):
"""Dict of fields to replace by fields computed from env
To override in models. The dictionary is:
{'name_of_the_field': options}
Where ``options`` is a dictionary::
options = {
"getter": "getint",
"no_default_field": True,
"compute_default": "_compute_password",
"inverse_default": "_inverse_password",
}
* ``getter``: The configparser getter can be one of: get, getboolean,
getint, getfloat. The getter is automatically inferred from the
type of the field, so it shouldn't generally be needed to set it.
* ``no_default_field``: disable creation of a field for storing
the default value, must be used with ``compute_default`` and
``inverse_default``
* ``compute_default``: name of a compute method to get the default
value when no key is present in configuration files
* ``inverse_default``: name of an inverse method to set the default
value when the value is editable
Example::
@property
def _server_env_fields(self):
base_fields = super()._server_env_fields
sftp_fields = {
"sftp_server": {},
"sftp_port": {},
"sftp_login": {},
"sftp_password": {},
}
sftp_fields.update(base_fields)
return sftp_fields
"""
return {}
@api.model
def _server_env_global_section_name(self):
"""Name of the global section in the configuration files
Can be customized in your model
"""
return self._name.replace(".", "_")
_server_env_section_name_field = "name"
def _server_env_section_name(self):
"""Name of the section in the configuration files
Can be customized in your model
"""
self.ensure_one()
val = self[self._server_env_section_name_field]
if not val:
# special case: we have onchanges relying on tech_name
# and we are testing them using `tests.common.Form`.
# when the for is initialized there's no value yet.
return
base = self._server_env_global_section_name()
return ".".join((base, val))
def _server_env_read_from_config(self, field_name, config_getter):
self.ensure_one()
global_section_name = self._server_env_global_section_name()
section_name = self._server_env_section_name()
try:
# at this point we should have checked that we have a key with
# _server_env_has_key_defined so we are sure that the value is
# either in the global or the record config
getter = getattr(serv_config, config_getter)
if section_name in serv_config and field_name in serv_config[section_name]:
value = getter(section_name, field_name)
else:
value = getter(global_section_name, field_name)
except Exception:
_logger.exception(
"error trying to read field %s in section %s", field_name, section_name
)
return False
return value
def _server_env_has_key_defined(self, field_name):
self.ensure_one()
global_section_name = self._server_env_global_section_name()
section_name = self._server_env_section_name()
has_global_config = (
global_section_name in serv_config
and field_name in serv_config[global_section_name]
)
has_config = (
section_name in serv_config and field_name in serv_config[section_name]
)
return has_global_config or has_config
def _compute_server_env_from_config(self, field_name, options):
getter_name = options.get("getter") if options else None
if not getter_name:
field_type = self._fields[field_name].type
getter_name = self._server_env_getter_mapping.get(field_type)
if not getter_name:
# if you get this message and the field is working as expected,
# you may want to add the type in _server_env_getter_mapping
_logger.warning(
"server.env.mixin is used on a field of type %s, "
"which may not be supported properly"
)
getter_name = "get"
value = self._server_env_read_from_config(field_name, getter_name)
self[field_name] = value
def _compute_server_env_from_default(self, field_name, options):
if options and options.get("compute_default"):
getattr(self, options["compute_default"])()
else:
default_field = self._server_env_default_fieldname(field_name)
if default_field:
self[field_name] = self[default_field]
else:
self[field_name] = False
def _compute_server_env(self):
"""Read values from environment configuration files
If an env-computed field has no key in configuration files,
read from the ``<field>_env_default`` field from database.
"""
for record in self:
for field_name, options in self._server_env_fields.items():
if record._server_env_has_key_defined(field_name):
record._compute_server_env_from_config(field_name, options)
else:
record._compute_server_env_from_default(field_name, options)
def _inverse_server_env(self, field_name):
options = self._server_env_fields[field_name]
default_field = self._server_env_default_fieldname(field_name)
is_editable_field = self._server_env_is_editable_fieldname(field_name)
for record in self:
# when we write in an env-computed field, if it is editable
# we update the default value in database
if record[is_editable_field]:
if options and options.get("inverse_default"):
getattr(record, options["inverse_default"])()
elif default_field:
record[default_field] = record[field_name]
def _compute_server_env_is_editable(self):
"""Compute <field>_is_editable values
We can edit an env-computed filed only if there is no key
in any environment configuration file. If there is an empty
key, it's an empty value so we can't edit the env-computed field.
"""
# we can't group it with _compute_server_env otherwise when called
# in ``_inverse_server_env`` it would reset the value of the field
for record in self:
for field_name in self._server_env_fields:
is_editable_field = self._server_env_is_editable_fieldname(field_name)
is_editable = not record._server_env_has_key_defined(field_name)
record[is_editable_field] = is_editable
def _server_env_view_set_readonly(self, view_arch):
field_xpath = './/field[@name="%s"]'
for field in self._server_env_fields:
is_editable_field = self._server_env_is_editable_fieldname(field)
for elem in view_arch.findall(field_xpath % field):
# set env-computed fields to readonly if the configuration
# files have a key set for this field
elem.set("attrs", str({"readonly": [(is_editable_field, "=", False)]}))
if not view_arch.findall(field_xpath % is_editable_field):
# add the _is_editable fields in the view for the 'attrs'
# domain
view_arch.append(
etree.Element("field", name=is_editable_field, invisible="1")
)
return view_arch
def _fields_view_get(
self, view_id=None, view_type="form", toolbar=False, submenu=False
):
view_data = super()._fields_view_get(
view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu
)
view_arch = etree.fromstring(view_data["arch"].encode("utf-8"))
view_arch = self._server_env_view_set_readonly(view_arch)
view_data["arch"] = etree.tostring(view_arch, encoding="unicode")
return view_data
def _server_env_default_fieldname(self, base_field_name):
"""Return the name of the field with default value"""
options = self._server_env_fields[base_field_name]
if options and options.get("no_default_field"):
return ""
return "{}_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"
This is the field used to tell if the env-computed field can
be edited.
"""
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"
inverse_method_name = "_inverse_server_env_%s" % field.name
inverse_method = partialmethod(type(self)._inverse_server_env, field.name)
setattr(type(self), inverse_method_name, inverse_method)
field.inverse = inverse_method_name
field.store = False
field.required = False
field.copy = False
field.sparse = None
field.prefetch = False
def _server_env_add_is_editable_field(self, base_field):
"""Add a field indicating if we can edit the env-computed fields
It is used in the inverse function of the env-computed field
and in the views to add 'readonly' on the fields.
"""
fieldname = self._server_env_is_editable_fieldname(base_field.name)
# if the field is inherited, it's a related to its delegated model
# (inherits), we want to override it with a new one
if fieldname not in self._fields or self._fields[fieldname].inherited:
field = fields.Boolean(
compute="_compute_server_env_is_editable",
automatic=True,
# this is required to be able to edit fields
# on new records
default=True,
)
self._add_field(fieldname, field)
def _server_env_add_default_field(self, base_field):
"""Add a field storing the default value
The default value is used when there is no key for an env-computed
field in the configuration files.
The field is a sparse field stored in the serialized (json) field
``server_env_defaults``.
"""
fieldname = self._server_env_default_fieldname(base_field.name)
if not fieldname:
return
# if the field is inherited, it's a related to its delegated model
# (inherits), we want to override it with a new one
if fieldname not in self._fields or self._fields[fieldname].inherited:
base_field_cls = base_field.__class__
field_args = base_field.args.copy()
field_args.pop("_sequence", None)
field_args.update({"sparse": "server_env_defaults", "automatic": True})
if hasattr(base_field, "selection"):
field_args["selection"] = base_field.selection
field = base_field_cls(**field_args)
self._add_field(fieldname, field)
@api.model
def _setup_base(self):
super()._setup_base()
for fieldname in self._server_env_fields:
field = self._fields[fieldname]
self._server_env_add_default_field(field)
self._server_env_transform_field_to_read_from_env(field)
self._server_env_add_is_editable_field(field)