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