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 try:
351 opt_value = gmPG2.dbapi.Binary(pickle.dumps(value))
352 sql_type_cast = '::bytea'
353 except pickle.PicklingError:
354 _log.error("cannot pickle option of type [%s] (key: %s, value: %s)", type(value), alias, str(value))
355 raise
356 except:
357 _log.error("don't know how to store option of type [%s] (key: %s, value: %s)", type(value), alias, str(value))
358 raise
359
360 cmd = 'select cfg.set_option(%%(opt)s, %%(val)s%s, %%(wp)s, %%(cookie)s, NULL)' % sql_type_cast
361 args = {
362 'opt': option,
363 'val': opt_value,
364 'wp': workplace,
365 'cookie': cookie
366 }
367 try:
368 rows, idx = gmPG2.run_rw_queries(link_obj=rw_conn, queries=[{'cmd': cmd, 'args': args}], return_data=True)
369 result = rows[0][0]
370 except:
371 _log.exception('cannot set option')
372 result = False
373
374 rw_conn.commit()
375 rw_conn.close()
376
377 return result
378
379
381 """Get names of all stored parameters for a given workplace/(user)/cookie-key.
382 This will be used by the ConfigEditor object to create a parameter tree.
383 """
384
385 where_snippets = [
386 'cfg_template.pk=cfg_item.fk_template',
387 'cfg_item.workplace=%(wplace)s'
388 ]
389 where_args = {'wplace': workplace}
390
391
392 if user is None:
393 where_snippets.append('cfg_item.owner=CURRENT_USER')
394 else:
395 where_snippets.append('cfg_item.owner=%(usr)s')
396 where_args['usr'] = user
397
398 where_clause = ' and '.join(where_snippets)
399
400 cmd = """
401 select name, cookie, owner, type, description
402 from cfg.cfg_template, cfg.cfg_item
403 where %s""" % where_clause
404
405
406 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': where_args}], return_data=True)
407 return rows
408
409
410 - def delete(self, conn=None, pk_option=None):
411 if conn is None:
412
413 cmd = "DELETE FROM cfg.cfg_item WHERE pk = %(pk)s AND owner = CURRENT_USER"
414 else:
415 cmd = "DELETE FROM cfg.cfg_item WHERE pk = %(pk)s"
416 args = {'pk': pk_option}
417 gmPG2.run_rw_queries(link_obj = conn, queries = [{'cmd': cmd, 'args': args}], end_tx = True)
418
419 - def delete_old(self, workplace = None, cookie = None, option = None):
420 """
421 Deletes an option or a whole group.
422 Note you have to call store() in order to save
423 the changes.
424 """
425 if option is None:
426 raise ValueError('<option> cannot be None')
427
428 if cookie is None:
429 cmd = """
430 delete from cfg.cfg_item where
431 fk_template=(select pk from cfg.cfg_template where name = %(opt)s) and
432 owner = CURRENT_USER and
433 workplace = %(wp)s and
434 cookie is Null
435 """
436 else:
437 cmd = """
438 delete from cfg.cfg_item where
439 fk_template=(select pk from cfg.cfg_template where name = %(opt)s) and
440 owner = CURRENT_USER and
441 workplace = %(wp)s and
442 cookie = %(cookie)s
443 """
444 args = {'opt': option, 'wp': workplace, 'cookie': cookie}
445 gmPG2.run_rw_queries(queries=[{'cmd': cmd, 'args': args}])
446 return True
447
449 return '%s-%s-%s-%s' % (workplace, user, cookie, option)
450
451 -def getDBParam(workplace = None, cookie = None, option = None):
452 """Convenience function to get config value from database.
453
454 will search for context dependant match in this order:
455 - CURRENT_USER_CURRENT_WORKPLACE
456 - CURRENT_USER_DEFAULT_WORKPLACE
457 - DEFAULT_USER_CURRENT_WORKPLACE
458 - DEFAULT_USER_DEFAULT_WORKPLACE
459
460 We assume that the config tables are found on service "default".
461 That way we can handle the db connection inside this function.
462
463 Returns (value, set) of first match.
464 """
465
466
467
468 if option is None:
469 return (None, None)
470
471
472 dbcfg = cCfgSQL()
473
474
475 sets2search = []
476 if workplace is not None:
477 sets2search.append(['CURRENT_USER_CURRENT_WORKPLACE', None, workplace])
478 sets2search.append(['CURRENT_USER_DEFAULT_WORKPLACE', None, None])
479 if workplace is not None:
480 sets2search.append(['DEFAULT_USER_CURRENT_WORKPLACE', cfg_DEFAULT, workplace])
481 sets2search.append(['DEFAULT_USER_DEFAULT_WORKPLACE', cfg_DEFAULT, None])
482
483 matchingSet = None
484 result = None
485 for set in sets2search:
486 result = dbcfg.get(
487 workplace = set[2],
488 user = set[1],
489 option = option,
490 cookie = cookie
491 )
492 if result is not None:
493 matchingSet = set[0]
494 break
495 _log.debug('[%s] not found for [%s@%s]' % (option, set[1], set[2]))
496
497
498 if matchingSet is None:
499 _log.warning('no config data for [%s]' % option)
500 return (result, matchingSet)
501
502 -def setDBParam(workplace = None, user = None, cookie = None, option = None, value = None):
503 """Convenience function to store config values in database.
504
505 We assume that the config tables are found on service "default".
506 That way we can handle the db connection inside this function.
507
508 Omitting any parameter (or setting to None) will store database defaults for it.
509
510 - returns True/False
511 """
512
513 dbcfg = cCfgSQL()
514
515 success = dbcfg.set(
516 workplace = workplace,
517 user = user,
518 option = option,
519 value = value
520 )
521
522 if not success:
523 return False
524 return True
525
526
527
528 if __name__ == "__main__":
529
530 if len(sys.argv) < 2:
531 sys.exit()
532
533 if sys.argv[1] != 'test':
534 sys.exit()
535
536 root = logging.getLogger()
537 root.setLevel(logging.DEBUG)
538
540 for opt in get_all_options():
541 print('%s (%s): %s (%s@%s)' % (opt['option'], opt['type'], opt['value'], opt['owner'], opt['workplace']))
542
543
544
546 print("testing database config")
547 print("=======================")
548
549 myDBCfg = cCfgSQL()
550
551 print("delete() works:", myDBCfg.delete(option='font name', workplace = 'test workplace'))
552 print("font is initially:", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user'))
553 print("set() works:", myDBCfg.set(option='font name', value="Times New Roman", workplace = 'test workplace'))
554 print("font after set():", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user'))
555 print("delete() works:", myDBCfg.delete(option='font name', workplace = 'test workplace'))
556 print("font after delete():", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user'))
557 print("font after get() with default:", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user', default = 'WingDings'))
558 print("font right after get() with another default:", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user', default = 'default: Courier'))
559 print("set() works:", myDBCfg.set(option='font name', value="Times New Roman", workplace = 'test workplace'))
560 print("font after set() on existing option:", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user'))
561
562 print("setting array option")
563 print("array now:", myDBCfg.get2(option = 'test array', workplace = 'test workplace', bias = 'user'))
564 aList = ['val 1', 'val 2']
565 print("set():", myDBCfg.set(option='test array', value = aList, workplace = 'test workplace'))
566 print("array now:", myDBCfg.get2(option = 'test array', workplace = 'test workplace', bias = 'user'))
567 aList = ['val 11', 'val 12']
568 print("set():", myDBCfg.set(option='test array', value = aList, workplace = 'test workplace'))
569 print("array now:", myDBCfg.get2(option = 'test array', workplace = 'test workplace', bias = 'user'))
570 print("delete() works:", myDBCfg.delete(option='test array', workplace='test workplace'))
571 print("array now:", myDBCfg.get2(option = 'test array', workplace = 'test workplace', bias = 'user'))
572
573 print("setting complex option")
574 data = {1: 'line 1', 2: 'line2', 3: {1: 'line3.1', 2: 'line3.2'}, 4: 1234}
575 print("set():", myDBCfg.set(option = "complex option test", value = data, workplace = 'test workplace'))
576 print("complex option now:", myDBCfg.get2(workplace = 'test workplace', option = "complex option test", bias = 'user'))
577 print("delete() works:", myDBCfg.delete(option = "complex option test", workplace = 'test workplace'))
578 print("complex option now:", myDBCfg.get2(workplace = 'test workplace', option = "complex option test", bias = 'user'))
579
580
581 test_get_all_options()
582
583
584
585
586
587
588
589