1 """GNUmed configuration handling.
2
3 This source of configuration information is supported:
4
5 - database tables
6
7 Theory of operation:
8
9 It is helpful to have a solid log target set up before importing this
10 module in your code. This way you will be able to see even those log
11 messages generated during module import.
12
13 Once your software has established database connectivity you can
14 set up a config source from the database. You can limit the option
15 applicability by the constraints "workplace", "user", and "cookie".
16
17 The basic API for handling items is get()/set().
18 The database config objects auto-sync with the backend.
19
20 @copyright: GPL v2 or later
21 """
22
23
24
25 __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
26
27
28 import sys, pickle, decimal, logging, re as regex
29
30
31
32 if __name__ == '__main__':
33 sys.path.insert(0, '../../')
34 from Gnumed.pycommon import gmPG2, gmTools
35
36
37 _log = logging.getLogger('gm.cfg')
38
39
40
41 cfg_DEFAULT = "xxxDEFAULTxxx"
42
44
45 if order_by is None:
46 order_by = ''
47 else:
48 order_by = 'ORDER BY %s' % order_by
49
50 cmd = """
51 SELECT * FROM (
52
53 SELECT
54 vco.*,
55 cs.value
56 FROM
57 cfg.v_cfg_options vco
58 JOIN cfg.cfg_string cs ON (vco.pk_cfg_item = cs.fk_item)
59
60 UNION ALL
61
62 SELECT
63 vco.*,
64 cn.value::text
65 FROM
66 cfg.v_cfg_options vco
67 JOIN cfg.cfg_numeric cn ON (vco.pk_cfg_item = cn.fk_item)
68
69 UNION ALL
70
71 SELECT
72 vco.*,
73 csa.value::text
74 FROM
75 cfg.v_cfg_options vco
76 JOIN cfg.cfg_str_array csa ON (vco.pk_cfg_item = csa.fk_item)
77
78 UNION ALL
79
80 SELECT
81 vco.*,
82 cd.value::text
83 FROM
84 cfg.v_cfg_options vco
85 JOIN cfg.cfg_data cd ON (vco.pk_cfg_item = cd.fk_item)
86
87 ) as option_list
88 %s""" % order_by
89
90 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = False)
91
92 return rows
93
94
96
97
98
99
100
101
102
103 - def get(self, option=None, workplace=None, cookie=None, bias=None, default=None, sql_return_type=None):
104 return self.get2 (
105 option = option,
106 workplace = workplace,
107 cookie = cookie,
108 bias = bias,
109 default = default,
110 sql_return_type = sql_return_type
111 )
112
113 - def get2(self, option=None, workplace=None, cookie=None, bias=None, default=None, sql_return_type=None):
114 """Retrieve configuration option from backend.
115
116 @param bias: Determine the direction into which to look for config options.
117
118 'user': When no value is found for "current_user/workplace" look for a value
119 for "current_user" regardless of workspace. The corresponding concept is:
120
121 "Did *I* set this option anywhere on this site ? If so, reuse the value."
122
123 'workplace': When no value is found for "current_user/workplace" look for a value
124 for "workplace" regardless of user. The corresponding concept is:
125
126 "Did anyone set this option for *this workplace* ? If so, reuse that value."
127
128 @param default: if no value is found for the option this value is returned
129 instead, also the option is set to this value in the backend, if <None>
130 a missing option will NOT be created in the backend
131 @param sql_return_type: a PostgreSQL type the value of the option is to be
132 cast to before returning, if None no cast will be applied, you will
133 want to make sure that sql_return_type and type(default) are compatible
134 """
135 if None in [option, workplace]:
136 raise ValueError('neither <option> (%s) nor <workplace> (%s) may be [None]' % (option, workplace))
137 if bias not in ['user', 'workplace']:
138 raise ValueError('<bias> must be "user" or "workplace"')
139
140
141 cmd = "select type from cfg.cfg_template where name=%(opt)s"
142 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'opt': option}}])
143 if len(rows) == 0:
144
145 if default is None:
146
147 return None
148 _log.info('creating option [%s] with default [%s]' % (option, default))
149 success = self.set(workplace = workplace, cookie = cookie, option = option, value = default)
150 if not success:
151
152 _log.error('creating option failed')
153 return default
154
155 cfg_table_type_suffix = rows[0][0]
156 args = {
157 'opt': option,
158 'wp': workplace,
159 'cookie': cookie,
160 'def': cfg_DEFAULT
161 }
162
163 if cfg_table_type_suffix == 'data':
164 sql_return_type = ''
165 else:
166 sql_return_type = gmTools.coalesce (
167 value2test = sql_return_type,
168 return_instead = '',
169 template4value = '::%s'
170 )
171
172
173 where_parts = [
174 'vco.owner = CURRENT_USER',
175 'vco.workplace = %(wp)s',
176 'vco.option = %(opt)s'
177 ]
178 where_parts.append(gmTools.coalesce (
179 value2test = cookie,
180 return_instead = 'vco.cookie is null',
181 template4value = 'vco.cookie = %(cookie)s'
182 ))
183 cmd = "select vco.value%s from cfg.v_cfg_opts_%s vco where %s limit 1" % (
184 sql_return_type,
185 cfg_table_type_suffix,
186 ' and '.join(where_parts)
187 )
188 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
189 if len(rows) > 0:
190 if cfg_table_type_suffix == 'data':
191 return pickle.loads(str(rows[0][0]))
192 return rows[0][0]
193
194 _log.warning('no user AND workplace specific value for option [%s] in config database' % option)
195
196
197 if bias == 'user':
198
199 where_parts = [
200 'vco.option = %(opt)s',
201 'vco.owner = CURRENT_USER',
202 ]
203 else:
204
205 where_parts = [
206 'vco.option = %(opt)s',
207 'vco.workplace = %(wp)s'
208 ]
209 where_parts.append(gmTools.coalesce (
210 value2test = cookie,
211 return_instead = 'vco.cookie is null',
212 template4value = 'vco.cookie = %(cookie)s'
213 ))
214 cmd = "select vco.value%s from cfg.v_cfg_opts_%s vco where %s" % (
215 sql_return_type,
216 cfg_table_type_suffix,
217 ' and '.join(where_parts)
218 )
219 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
220 if len(rows) > 0:
221
222 self.set (
223 workplace = workplace,
224 cookie = cookie,
225 option = option,
226 value = rows[0][0]
227 )
228 if cfg_table_type_suffix == 'data':
229 return pickle.loads(str(rows[0][0]))
230 return rows[0][0]
231
232 _log.warning('no user OR workplace specific value for option [%s] in config database' % option)
233
234
235 where_parts = [
236 'vco.owner = %(def)s',
237 'vco.workplace = %(def)s',
238 'vco.option = %(opt)s'
239 ]
240 cmd = "select vco.value%s from cfg.v_cfg_opts_%s vco where %s" % (
241 sql_return_type,
242 cfg_table_type_suffix,
243 ' and '.join(where_parts)
244 )
245 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
246 if len(rows) > 0:
247
248 self.set (
249 workplace = workplace,
250 cookie = cookie,
251 option = option,
252 value = rows[0]['value']
253 )
254 if cfg_table_type_suffix == 'data':
255 return pickle.loads(str(rows[0]['value']))
256 return rows[0]['value']
257
258 _log.warning('no default site policy value for option [%s] in config database' % option)
259
260
261 if default is None:
262 _log.warning('no default value for option [%s] supplied by caller' % option)
263 return None
264 _log.info('setting option [%s] to default [%s]' % (option, default))
265 success = self.set (
266 workplace = workplace,
267 cookie = cookie,
268 option = option,
269 value = default
270 )
271 if not success:
272 return None
273
274 return default
275
276 - def getID(self, workplace = None, cookie = None, option = None):
277 """Get config value from database.
278
279 - unset arguments are assumed to mean database defaults except for <cookie>
280 """
281
282 if option is None:
283 _log.error("Need to know which option to retrieve.")
284 return None
285
286 alias = self.__make_alias(workplace, 'CURRENT_USER', cookie, option)
287
288
289 where_parts = [
290 'vco.option=%(opt)s',
291 'vco.workplace=%(wplace)s'
292 ]
293 where_args = {
294 'opt': option,
295 'wplace': workplace
296 }
297 if workplace is None:
298 where_args['wplace'] = cfg_DEFAULT
299
300 where_parts.append('vco.owner=CURRENT_USER')
301
302 if cookie is not None:
303 where_parts.append('vco.cookie=%(cookie)s')
304 where_args['cookie'] = cookie
305 where_clause = ' and '.join(where_parts)
306 cmd = """
307 select vco.pk_cfg_item
308 from cfg.v_cfg_options vco
309 where %s
310 limit 1""" % where_clause
311
312 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': where_args}], return_data=True)
313 if len(rows) == 0:
314 _log.warning('option definition for [%s] not in config database' % alias)
315 return None
316 return rows[0][0]
317
318 - def set(self, workplace = None, cookie = None, option = None, value = None):
319 """Set (insert or update) option value in database.
320
321 Any parameter that is None will be set to the database default.
322
323 Note: you can't change the type of a parameter once it has been
324 created in the backend. If you want to change the type you will
325 have to delete the parameter and recreate it using the new type.
326 """
327
328 if None in [option, value]:
329 raise ValueError('invalid arguments (option=<%s>, value=<%s>)' % (option, value))
330
331 rw_conn = gmPG2.get_connection(readonly=False)
332
333 alias = self.__make_alias(workplace, 'CURRENT_USER', cookie, option)
334
335 opt_value = value
336 sql_type_cast = ''
337 if isinstance(value, str):
338 sql_type_cast = '::text'
339 elif isinstance(value, bool):
340 opt_value = int(opt_value)
341 elif isinstance(value, (float, int, decimal.Decimal, bool)):
342 sql_type_cast = '::numeric'
343 elif isinstance(value, list):
344
345 pass
346 elif isinstance(value, buffer):
347
348 pass
349 else:
350 opt_value = gmPG2.dbapi.Binary(pickle.dumps(value))
351 sql_type_cast = '::bytea'
352
353 cmd = 'select cfg.set_option(%%(opt)s, %%(val)s%s, %%(wp)s, %%(cookie)s, NULL)' % sql_type_cast
354 args = {
355 'opt': option,
356 'val': opt_value,
357 'wp': workplace,
358 'cookie': cookie
359 }
360 try:
361 rows, idx = gmPG2.run_rw_queries(link_obj=rw_conn, queries=[{'cmd': cmd, 'args': args}], return_data=True)
362 result = rows[0][0]
363 except Exception:
364 _log.exception('cannot set option')
365 result = False
366
367 rw_conn.commit()
368 rw_conn.close()
369
370 return result
371
372
374 """Get names of all stored parameters for a given workplace/(user)/cookie-key.
375 This will be used by the ConfigEditor object to create a parameter tree.
376 """
377
378 where_snippets = [
379 'cfg_template.pk=cfg_item.fk_template',
380 'cfg_item.workplace=%(wplace)s'
381 ]
382 where_args = {'wplace': workplace}
383
384
385 if user is None:
386 where_snippets.append('cfg_item.owner=CURRENT_USER')
387 else:
388 where_snippets.append('cfg_item.owner=%(usr)s')
389 where_args['usr'] = user
390
391 where_clause = ' and '.join(where_snippets)
392
393 cmd = """
394 select name, cookie, owner, type, description
395 from cfg.cfg_template, cfg.cfg_item
396 where %s""" % where_clause
397
398
399 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': where_args}], return_data=True)
400 return rows
401
402
403 - def delete(self, conn=None, pk_option=None):
404 if conn is None:
405
406 cmd = "DELETE FROM cfg.cfg_item WHERE pk = %(pk)s AND owner = CURRENT_USER"
407 else:
408 cmd = "DELETE FROM cfg.cfg_item WHERE pk = %(pk)s"
409 args = {'pk': pk_option}
410 gmPG2.run_rw_queries(link_obj = conn, queries = [{'cmd': cmd, 'args': args}], end_tx = True)
411
412 - def delete_old(self, workplace = None, cookie = None, option = None):
413 """
414 Deletes an option or a whole group.
415 Note you have to call store() in order to save
416 the changes.
417 """
418 if option is None:
419 raise ValueError('<option> cannot be None')
420
421 if cookie is None:
422 cmd = """
423 delete from cfg.cfg_item where
424 fk_template=(select pk from cfg.cfg_template where name = %(opt)s) and
425 owner = CURRENT_USER and
426 workplace = %(wp)s and
427 cookie is Null
428 """
429 else:
430 cmd = """
431 delete from cfg.cfg_item where
432 fk_template=(select pk from cfg.cfg_template where name = %(opt)s) and
433 owner = CURRENT_USER and
434 workplace = %(wp)s and
435 cookie = %(cookie)s
436 """
437 args = {'opt': option, 'wp': workplace, 'cookie': cookie}
438 gmPG2.run_rw_queries(queries=[{'cmd': cmd, 'args': args}])
439 return True
440
442 return '%s-%s-%s-%s' % (workplace, user, cookie, option)
443
444 -def getDBParam(workplace = None, cookie = None, option = None):
445 """Convenience function to get config value from database.
446
447 will search for context dependant match in this order:
448 - CURRENT_USER_CURRENT_WORKPLACE
449 - CURRENT_USER_DEFAULT_WORKPLACE
450 - DEFAULT_USER_CURRENT_WORKPLACE
451 - DEFAULT_USER_DEFAULT_WORKPLACE
452
453 We assume that the config tables are found on service "default".
454 That way we can handle the db connection inside this function.
455
456 Returns (value, set) of first match.
457 """
458
459
460
461 if option is None:
462 return (None, None)
463
464
465 dbcfg = cCfgSQL()
466
467
468 sets2search = []
469 if workplace is not None:
470 sets2search.append(['CURRENT_USER_CURRENT_WORKPLACE', None, workplace])
471 sets2search.append(['CURRENT_USER_DEFAULT_WORKPLACE', None, None])
472 if workplace is not None:
473 sets2search.append(['DEFAULT_USER_CURRENT_WORKPLACE', cfg_DEFAULT, workplace])
474 sets2search.append(['DEFAULT_USER_DEFAULT_WORKPLACE', cfg_DEFAULT, None])
475
476 matchingSet = None
477 result = None
478 for set in sets2search:
479 result = dbcfg.get(
480 workplace = set[2],
481 user = set[1],
482 option = option,
483 cookie = cookie
484 )
485 if result is not None:
486 matchingSet = set[0]
487 break
488 _log.debug('[%s] not found for [%s@%s]' % (option, set[1], set[2]))
489
490
491 if matchingSet is None:
492 _log.warning('no config data for [%s]' % option)
493 return (result, matchingSet)
494
495 -def setDBParam(workplace = None, user = None, cookie = None, option = None, value = None):
496 """Convenience function to store config values in database.
497
498 We assume that the config tables are found on service "default".
499 That way we can handle the db connection inside this function.
500
501 Omitting any parameter (or setting to None) will store database defaults for it.
502
503 - returns True/False
504 """
505
506 dbcfg = cCfgSQL()
507
508 success = dbcfg.set(
509 workplace = workplace,
510 user = user,
511 option = option,
512 value = value
513 )
514
515 if not success:
516 return False
517 return True
518
519
520
521 if __name__ == "__main__":
522
523 if len(sys.argv) < 2:
524 sys.exit()
525
526 if sys.argv[1] != 'test':
527 sys.exit()
528
529 root = logging.getLogger()
530 root.setLevel(logging.DEBUG)
531
533 for opt in get_all_options():
534 print('%s (%s): %s (%s@%s)' % (opt['option'], opt['type'], opt['value'], opt['owner'], opt['workplace']))
535
536
537
539 print("testing database config")
540 print("=======================")
541
542 myDBCfg = cCfgSQL()
543
544 print("delete() works:", myDBCfg.delete(option='font name', workplace = 'test workplace'))
545 print("font is initially:", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user'))
546 print("set() works:", myDBCfg.set(option='font name', value="Times New Roman", workplace = 'test workplace'))
547 print("font after set():", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user'))
548 print("delete() works:", myDBCfg.delete(option='font name', workplace = 'test workplace'))
549 print("font after delete():", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user'))
550 print("font after get() with default:", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user', default = 'WingDings'))
551 print("font right after get() with another default:", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user', default = 'default: Courier'))
552 print("set() works:", myDBCfg.set(option='font name', value="Times New Roman", workplace = 'test workplace'))
553 print("font after set() on existing option:", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user'))
554
555 print("setting array option")
556 print("array now:", myDBCfg.get2(option = 'test array', workplace = 'test workplace', bias = 'user'))
557 aList = ['val 1', 'val 2']
558 print("set():", myDBCfg.set(option='test array', value = aList, workplace = 'test workplace'))
559 print("array now:", myDBCfg.get2(option = 'test array', workplace = 'test workplace', bias = 'user'))
560 aList = ['val 11', 'val 12']
561 print("set():", myDBCfg.set(option='test array', value = aList, workplace = 'test workplace'))
562 print("array now:", myDBCfg.get2(option = 'test array', workplace = 'test workplace', bias = 'user'))
563 print("delete() works:", myDBCfg.delete(option='test array', workplace='test workplace'))
564 print("array now:", myDBCfg.get2(option = 'test array', workplace = 'test workplace', bias = 'user'))
565
566 print("setting complex option")
567 data = {1: 'line 1', 2: 'line2', 3: {1: 'line3.1', 2: 'line3.2'}, 4: 1234}
568 print("set():", myDBCfg.set(option = "complex option test", value = data, workplace = 'test workplace'))
569 print("complex option now:", myDBCfg.get2(workplace = 'test workplace', option = "complex option test", bias = 'user'))
570 print("delete() works:", myDBCfg.delete(option = "complex option test", workplace = 'test workplace'))
571 print("complex option now:", myDBCfg.get2(workplace = 'test workplace', option = "complex option test", bias = 'user'))
572
573
574 test_get_all_options()
575
576
577
578