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 __version__ = "$Revision: 1.60 $"
26 __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
27
28
29 import sys, types, cPickle, decimal, logging, re as regex
30
31
32
33 if __name__ == '__main__':
34 sys.path.insert(0, '../../')
35 from Gnumed.pycommon import gmPG2, gmTools
36
37
38 _log = logging.getLogger('gm.cfg')
39 _log.info(__version__)
40
41
42
43 cfg_DEFAULT = "xxxDEFAULTxxx"
44
46
47 if order_by is None:
48 order_by = u''
49 else:
50 order_by = u'ORDER BY %s' % order_by
51
52 cmd = u"""
53 SELECT * FROM (
54
55 SELECT
56 vco.*,
57 cs.value
58 FROM
59 cfg.v_cfg_options vco
60 JOIN cfg.cfg_string cs ON (vco.pk_cfg_item = cs.fk_item)
61
62 UNION ALL
63
64 SELECT
65 vco.*,
66 cn.value::text
67 FROM
68 cfg.v_cfg_options vco
69 JOIN cfg.cfg_numeric cn ON (vco.pk_cfg_item = cn.fk_item)
70
71 UNION ALL
72
73 SELECT
74 vco.*,
75 csa.value::text
76 FROM
77 cfg.v_cfg_options vco
78 JOIN cfg.cfg_str_array csa ON (vco.pk_cfg_item = csa.fk_item)
79
80 UNION ALL
81
82 SELECT
83 vco.*,
84 cd.value::text
85 FROM
86 cfg.v_cfg_options vco
87 JOIN cfg.cfg_data cd ON (vco.pk_cfg_item = cd.fk_item)
88
89 ) as option_list
90 %s""" % order_by
91
92 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = False)
93
94 return rows
95
96
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 = u"select type from cfg.cfg_template where name=%(opt)s"
142 rows, idx = gmPG2.run_ro_queries(link_obj=self.ro_conn, 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 == u'data':
164 sql_return_type = u''
165 else:
166 sql_return_type = gmTools.coalesce (
167 initial = sql_return_type,
168 instead = u'',
169 template_initial = u'::%s'
170 )
171
172
173 where_parts = [
174 u'vco.owner = CURRENT_USER',
175 u'vco.workplace = %(wp)s',
176 u'vco.option = %(opt)s'
177 ]
178 where_parts.append(gmTools.coalesce (
179 initial = cookie,
180 instead = u'vco.cookie is null',
181 template_initial = u'vco.cookie = %(cookie)s'
182 ))
183 cmd = u"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 u' and '.join(where_parts)
187 )
188 rows, idx = gmPG2.run_ro_queries(link_obj=self.ro_conn, queries = [{'cmd': cmd, 'args': args}])
189 if len(rows) > 0:
190 if cfg_table_type_suffix == u'data':
191 return cPickle.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 u'vco.option = %(opt)s',
201 u'vco.owner = CURRENT_USER',
202 ]
203 else:
204
205 where_parts = [
206 u'vco.option = %(opt)s',
207 u'vco.workplace = %(wp)s'
208 ]
209 where_parts.append(gmTools.coalesce (
210 initial = cookie,
211 instead = u'vco.cookie is null',
212 template_initial = u'vco.cookie = %(cookie)s'
213 ))
214 cmd = u"select vco.value%s from cfg.v_cfg_opts_%s vco where %s" % (
215 sql_return_type,
216 cfg_table_type_suffix,
217 u' and '.join(where_parts)
218 )
219 rows, idx = gmPG2.run_ro_queries(link_obj=self.ro_conn, 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 == u'data':
229 return cPickle.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 u'vco.owner = %(def)s',
237 u'vco.workplace = %(def)s',
238 u'vco.option = %(opt)s'
239 ]
240 cmd = u"select vco.value%s from cfg.v_cfg_opts_%s vco where %s" % (
241 sql_return_type,
242 cfg_table_type_suffix,
243 u' and '.join(where_parts)
244 )
245 rows, idx = gmPG2.run_ro_queries(link_obj=self.ro_conn, 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 == u'data':
255 return cPickle.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 = u"""
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(link_obj=self.ro_conn, 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 = u''
337 if isinstance(value, basestring):
338 sql_type_cast = u'::text'
339 elif isinstance(value, types.BooleanType):
340 opt_value = int(opt_value)
341 elif isinstance(value, (types.FloatType, types.IntType, types.LongType, decimal.Decimal, types.BooleanType)):
342 sql_type_cast = u'::numeric'
343 elif isinstance(value, types.ListType):
344
345 pass
346 elif isinstance(value, types.BufferType):
347
348 pass
349 else:
350 try:
351 opt_value = gmPG2.dbapi.Binary(cPickle.dumps(value))
352 sql_type_cast = '::bytea'
353 except cPickle.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 = u'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
380 """Get names of all stored parameters for a given workplace/(user)/cookie-key.
381 This will be used by the ConfigEditor object to create a parameter tree.
382 """
383
384 where_snippets = [
385 u'cfg_template.pk=cfg_item.fk_template',
386 u'cfg_item.workplace=%(wplace)s'
387 ]
388 where_args = {'wplace': workplace}
389
390
391 if user is None:
392 where_snippets.append(u'cfg_item.owner=CURRENT_USER')
393 else:
394 where_snippets.append(u'cfg_item.owner=%(usr)s')
395 where_args['usr'] = user
396
397 where_clause = u' and '.join(where_snippets)
398
399 cmd = u"""
400 select name, cookie, owner, type, description
401 from cfg.cfg_template, cfg.cfg_item
402 where %s""" % where_clause
403
404
405 rows, idx = gmPG2.run_ro_queries(link_obj=self.ro_conn, queries = [{'cmd': cmd, 'args': where_args}], return_data=True)
406 return rows
407
408 - def delete(self, conn=None, pk_option=None):
409 if conn is None:
410
411 cmd = u"DELETE FROM cfg.cfg_item WHERE pk = %(pk)s AND owner = CURRENT_USER"
412 else:
413 cmd = u"DELETE FROM cfg.cfg_item WHERE pk = %(pk)s"
414 args = {'pk': pk_option}
415 gmPG2.run_rw_queries(link_obj = conn, queries = [{'cmd': cmd, 'args': args}], end_tx = True)
416
417 - def delete_old(self, workplace = None, cookie = None, option = None):
418 """
419 Deletes an option or a whole group.
420 Note you have to call store() in order to save
421 the changes.
422 """
423 if option is None:
424 raise ValueError('<option> cannot be None')
425
426 if cookie is None:
427 cmd = u"""
428 delete from cfg.cfg_item where
429 fk_template=(select pk from cfg.cfg_template where name = %(opt)s) and
430 owner = CURRENT_USER and
431 workplace = %(wp)s and
432 cookie is Null
433 """
434 else:
435 cmd = u"""
436 delete from cfg.cfg_item where
437 fk_template=(select pk from cfg.cfg_template where name = %(opt)s) and
438 owner = CURRENT_USER and
439 workplace = %(wp)s and
440 cookie = %(cookie)s
441 """
442 args = {'opt': option, 'wp': workplace, 'cookie': cookie}
443 gmPG2.run_rw_queries(queries=[{'cmd': cmd, 'args': args}])
444 return True
445
447 return '%s-%s-%s-%s' % (workplace, user, cookie, option)
448
449 -def getDBParam(workplace = None, cookie = None, option = None):
450 """Convenience function to get config value from database.
451
452 will search for context dependant match in this order:
453 - CURRENT_USER_CURRENT_WORKPLACE
454 - CURRENT_USER_DEFAULT_WORKPLACE
455 - DEFAULT_USER_CURRENT_WORKPLACE
456 - DEFAULT_USER_DEFAULT_WORKPLACE
457
458 We assume that the config tables are found on service "default".
459 That way we can handle the db connection inside this function.
460
461 Returns (value, set) of first match.
462 """
463
464
465
466 if option is None:
467 return (None, None)
468
469
470 dbcfg = cCfgSQL()
471
472
473 sets2search = []
474 if workplace is not None:
475 sets2search.append(['CURRENT_USER_CURRENT_WORKPLACE', None, workplace])
476 sets2search.append(['CURRENT_USER_DEFAULT_WORKPLACE', None, None])
477 if workplace is not None:
478 sets2search.append(['DEFAULT_USER_CURRENT_WORKPLACE', cfg_DEFAULT, workplace])
479 sets2search.append(['DEFAULT_USER_DEFAULT_WORKPLACE', cfg_DEFAULT, None])
480
481 matchingSet = None
482 result = None
483 for set in sets2search:
484 result = dbcfg.get(
485 workplace = set[2],
486 user = set[1],
487 option = option,
488 cookie = cookie
489 )
490 if result is not None:
491 matchingSet = set[0]
492 break
493 _log.debug('[%s] not found for [%s@%s]' % (option, set[1], set[2]))
494
495
496 if matchingSet is None:
497 _log.warning('no config data for [%s]' % option)
498 return (result, matchingSet)
499
500 -def setDBParam(workplace = None, user = None, cookie = None, option = None, value = None):
501 """Convenience function to store config values in database.
502
503 We assume that the config tables are found on service "default".
504 That way we can handle the db connection inside this function.
505
506 Omitting any parameter (or setting to None) will store database defaults for it.
507
508 - returns True/False
509 """
510
511 dbcfg = cCfgSQL()
512
513 success = dbcfg.set(
514 workplace = workplace,
515 user = user,
516 option = option,
517 value = value
518 )
519
520 if not success:
521 return False
522 return True
523
524
525
526 if __name__ == "__main__":
527
528 if len(sys.argv) < 2:
529 sys.exit()
530
531 if sys.argv[1] != 'test':
532 sys.exit()
533
534 root = logging.getLogger()
535 root.setLevel(logging.DEBUG)
536
538 for opt in get_all_options():
539 print u'%s (%s): %s (%s@%s)' % (opt['option'], opt['type'], opt['value'], opt['owner'], opt['workplace'])
540
541
542
544 print "testing database config"
545 print "======================="
546
547 myDBCfg = cCfgSQL()
548
549 print "delete() works:", myDBCfg.delete(option='font name', workplace = 'test workplace')
550 print "font is initially:", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user')
551 print "set() works:", myDBCfg.set(option='font name', value="Times New Roman", workplace = 'test workplace')
552 print "font after set():", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user')
553 print "delete() works:", myDBCfg.delete(option='font name', workplace = 'test workplace')
554 print "font after delete():", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user')
555 print "font after get() with default:", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user', default = 'WingDings')
556 print "font right after get() with another default:", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user', default = 'default: Courier')
557 print "set() works:", myDBCfg.set(option='font name', value="Times New Roman", workplace = 'test workplace')
558 print "font after set() on existing option:", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user')
559
560 print "setting array option"
561 print "array now:", myDBCfg.get2(option = 'test array', workplace = 'test workplace', bias = 'user')
562 aList = ['val 1', 'val 2']
563 print "set():", myDBCfg.set(option='test array', value = aList, workplace = 'test workplace')
564 print "array now:", myDBCfg.get2(option = 'test array', workplace = 'test workplace', bias = 'user')
565 aList = ['val 11', 'val 12']
566 print "set():", myDBCfg.set(option='test array', value = aList, workplace = 'test workplace')
567 print "array now:", myDBCfg.get2(option = 'test array', workplace = 'test workplace', bias = 'user')
568 print "delete() works:", myDBCfg.delete(option='test array', workplace='test workplace')
569 print "array now:", myDBCfg.get2(option = 'test array', workplace = 'test workplace', bias = 'user')
570
571 print "setting complex option"
572 data = {1: 'line 1', 2: 'line2', 3: {1: 'line3.1', 2: 'line3.2'}, 4: 1234}
573 print "set():", myDBCfg.set(option = "complex option test", value = data, workplace = 'test workplace')
574 print "complex option now:", myDBCfg.get2(workplace = 'test workplace', option = "complex option test", bias = 'user')
575 print "delete() works:", myDBCfg.delete(option = "complex option test", workplace = 'test workplace')
576 print "complex option now:", myDBCfg.get2(workplace = 'test workplace', option = "complex option test", bias = 'user')
577
578
579 test_get_all_options()
580
581
582
583
584
585
586
587