Package Gnumed :: Package business :: Module gmForms
[frames] | no frames]

Source Code for Module Gnumed.business.gmForms

   1  # -*- coding: latin-1 -*- 
   2  """GNUmed forms classes 
   3   
   4  Business layer for printing all manners of forms, letters, scripts etc. 
   5    
   6  license: GPL 
   7  """ 
   8  #============================================================ 
   9  __version__ = "$Revision: 1.79 $" 
  10  __author__ ="Ian Haywood <ihaywood@gnu.org>, karsten.hilbert@gmx.net" 
  11   
  12   
  13  import os, sys, time, os.path, logging, codecs, re as regex, shutil, random, platform, subprocess 
  14  #, libxml2, libxslt 
  15   
  16   
  17  if __name__ == '__main__': 
  18          sys.path.insert(0, '../../') 
  19  from Gnumed.pycommon import gmTools, gmBorg, gmMatchProvider, gmExceptions, gmDispatcher 
  20  from Gnumed.pycommon import gmPG2, gmBusinessDBObject, gmCfg, gmShellAPI, gmMimeLib, gmLog2 
  21  from Gnumed.business import gmPerson, gmSurgery 
  22   
  23   
  24  _log = logging.getLogger('gm.forms') 
  25  _log.info(__version__) 
  26   
  27  #============================================================ 
  28  # this order is also used in choice boxes for the engine 
  29  form_engine_abbrevs = [u'O', u'L', u'I'] 
  30   
  31  form_engine_names = { 
  32          u'O': 'OpenOffice', 
  33          u'L': 'LaTeX', 
  34          u'I': 'Image editor' 
  35  } 
  36   
  37  form_engine_template_wildcards = { 
  38          u'O': u'*.o?t', 
  39          u'L': u'*.tex' 
  40  } 
  41   
  42  # is filled in further below after each engine is defined 
  43  form_engines = {} 
  44   
  45  #============================================================ 
  46  # match providers 
  47  #============================================================ 
48 -class cFormTemplateNameLong_MatchProvider(gmMatchProvider.cMatchProvider_SQL2):
49
50 - def __init__(self):
51 52 query = u""" 53 select name_long, name_long 54 from ref.v_paperwork_templates 55 where name_long %(fragment_condition)s 56 order by name_long 57 """ 58 gmMatchProvider.cMatchProvider_SQL2.__init__(self, queries = [query])
59 #============================================================
60 -class cFormTemplateNameShort_MatchProvider(gmMatchProvider.cMatchProvider_SQL2):
61
62 - def __init__(self):
63 64 query = u""" 65 select name_short, name_short 66 from ref.v_paperwork_templates 67 where name_short %(fragment_condition)s 68 order by name_short 69 """ 70 gmMatchProvider.cMatchProvider_SQL2.__init__(self, queries = [query])
71 #============================================================
72 -class cFormTemplateType_MatchProvider(gmMatchProvider.cMatchProvider_SQL2):
73
74 - def __init__(self):
75 76 query = u""" 77 select * from ( 78 select pk, _(name) as l10n_name from ref.form_types 79 where _(name) %(fragment_condition)s 80 81 union 82 83 select pk, _(name) as l10n_name from ref.form_types 84 where name %(fragment_condition)s 85 ) as union_result 86 order by l10n_name 87 """ 88 gmMatchProvider.cMatchProvider_SQL2.__init__(self, queries = [query])
89 #============================================================
90 -class cFormTemplate(gmBusinessDBObject.cBusinessDBObject):
91 92 _cmd_fetch_payload = u'select * from ref.v_paperwork_templates where pk_paperwork_template = %s' 93 94 _cmds_store_payload = [ 95 u"""update ref.paperwork_templates set 96 name_short = %(name_short)s, 97 name_long = %(name_long)s, 98 fk_template_type = %(pk_template_type)s, 99 instance_type = %(instance_type)s, 100 engine = %(engine)s, 101 in_use = %(in_use)s, 102 filename = %(filename)s, 103 external_version = %(external_version)s 104 where 105 pk = %(pk_paperwork_template)s and 106 xmin = %(xmin_paperwork_template)s 107 """, 108 u"""select xmin_paperwork_template from ref.v_paperwork_templates where pk_paperwork_template = %(pk_paperwork_template)s""" 109 ] 110 111 _updatable_fields = [ 112 u'name_short', 113 u'name_long', 114 u'external_version', 115 u'pk_template_type', 116 u'instance_type', 117 u'engine', 118 u'in_use', 119 u'filename' 120 ] 121 122 _suffix4engine = { 123 u'O': u'.ott', 124 u'L': u'.tex', 125 u'T': u'.txt', 126 u'X': u'.xslt', 127 u'I': u'.img' 128 } 129 130 #--------------------------------------------------------
131 - def _get_template_data(self):
132 """The template itself better not be arbitrarily large unless you can handle that. 133 134 Note that the data type returned will be a buffer.""" 135 136 cmd = u'SELECT data FROM ref.paperwork_templates WHERE pk = %(pk)s' 137 rows, idx = gmPG2.run_ro_queries (queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}], get_col_idx = False) 138 139 if len(rows) == 0: 140 raise gmExceptions.NoSuchBusinessObjectError('cannot retrieve data for template pk = %s' % self.pk_obj) 141 142 return rows[0][0]
143 144 template_data = property(_get_template_data, lambda x:x) 145 #--------------------------------------------------------
146 - def export_to_file(self, filename=None, chunksize=0):
147 """Export form template from database into file.""" 148 149 if filename is None: 150 if self._payload[self._idx['filename']] is None: 151 suffix = self.__class__._suffix4engine[self._payload[self._idx['engine']]] 152 else: 153 suffix = os.path.splitext(self._payload[self._idx['filename']].strip())[1].strip() 154 if suffix in [u'', u'.']: 155 suffix = self.__class__._suffix4engine[self._payload[self._idx['engine']]] 156 157 filename = gmTools.get_unique_filename ( 158 prefix = 'gm-%s-Template-' % self._payload[self._idx['engine']], 159 suffix = suffix, 160 tmp_dir = os.path.expanduser(os.path.join('~', '.gnumed', 'tmp')) 161 ) 162 163 data_query = { 164 'cmd': u'SELECT substring(data from %(start)s for %(size)s) FROM ref.paperwork_templates WHERE pk = %(pk)s', 165 'args': {'pk': self.pk_obj} 166 } 167 168 data_size_query = { 169 'cmd': u'select octet_length(data) from ref.paperwork_templates where pk = %(pk)s', 170 'args': {'pk': self.pk_obj} 171 } 172 173 result = gmPG2.bytea2file ( 174 data_query = data_query, 175 filename = filename, 176 data_size_query = data_size_query, 177 chunk_size = chunksize 178 ) 179 if result is False: 180 return None 181 182 return filename
183 #--------------------------------------------------------
184 - def update_template_from_file(self, filename=None):
185 gmPG2.file2bytea ( 186 filename = filename, 187 query = u'update ref.paperwork_templates set data = %(data)s::bytea where pk = %(pk)s and xmin = %(xmin)s', 188 args = {'pk': self.pk_obj, 'xmin': self._payload[self._idx['xmin_paperwork_template']]} 189 ) 190 # adjust for xmin change 191 self.refetch_payload()
192 #--------------------------------------------------------
193 - def instantiate(self):
194 fname = self.export_to_file() 195 engine = form_engines[self._payload[self._idx['engine']]] 196 return engine(template_file = fname)
197 #============================================================
198 -def get_form_template(name_long=None, external_version=None):
199 cmd = u'select pk from ref.paperwork_templates where name_long = %(lname)s and external_version = %(ver)s' 200 args = {'lname': name_long, 'ver': external_version} 201 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 202 203 if len(rows) == 0: 204 _log.error('cannot load form template [%s - %s]', name_long, external_version) 205 return None 206 207 return cFormTemplate(aPK_obj = rows[0]['pk'])
208 #------------------------------------------------------------
209 -def get_form_templates(engine=None, active_only=False):
210 """Load form templates.""" 211 212 args = {'eng': engine, 'in_use': active_only} 213 214 where_parts = [] 215 if engine is not None: 216 where_parts.append(u'engine = %(eng)s') 217 218 if active_only: 219 where_parts.append(u'in_use is True') 220 221 if len(where_parts) == 0: 222 cmd = u"select * from ref.v_paperwork_templates order by in_use desc, name_long" 223 else: 224 cmd = u"select * from ref.v_paperwork_templates where %s order by in_use desc, name_long" % u'and'.join(where_parts) 225 226 rows, idx = gmPG2.run_ro_queries ( 227 queries = [{'cmd': cmd, 'args': args}], 228 get_col_idx = True 229 ) 230 templates = [ cFormTemplate(row = {'pk_field': 'pk_paperwork_template', 'data': r, 'idx': idx}) for r in rows ] 231 232 return templates
233 #------------------------------------------------------------
234 -def create_form_template(template_type=None, name_short=None, name_long=None):
235 236 cmd = u'insert into ref.paperwork_templates (fk_template_type, name_short, name_long, external_version) values (%(type)s, %(nshort)s, %(nlong)s, %(ext_version)s)' 237 rows, idx = gmPG2.run_rw_queries ( 238 queries = [ 239 {'cmd': cmd, 'args': {'type': template_type, 'nshort': name_short, 'nlong': name_long, 'ext_version': 'new'}}, 240 {'cmd': u"select currval(pg_get_serial_sequence('ref.paperwork_templates', 'pk'))"} 241 ], 242 return_data = True 243 ) 244 template = cFormTemplate(aPK_obj = rows[0][0]) 245 return template
246 #------------------------------------------------------------
247 -def delete_form_template(template=None):
248 rows, idx = gmPG2.run_rw_queries ( 249 queries = [ 250 {'cmd': u'delete from ref.paperwork_templates where pk=%(pk)s', 'args': {'pk': template['pk_paperwork_template']}} 251 ] 252 ) 253 return True
254 #============================================================ 255 # OpenOffice API 256 #============================================================ 257 uno = None 258 cOOoDocumentCloseListener = None 259 260 #-----------------------------------------------------------
261 -def __configure_path_to_UNO():
262 263 try: 264 which = subprocess.Popen ( 265 args = ('which', 'soffice'), 266 stdout = subprocess.PIPE, 267 stdin = subprocess.PIPE, 268 stderr = subprocess.PIPE, 269 universal_newlines = True 270 ) 271 except (OSError, ValueError, subprocess.CalledProcessError): 272 _log.exception('there was a problem executing [%s]', cmd) 273 return 274 275 soffice_path, err = which.communicate() 276 soffice_path = soffice_path.strip('\n') 277 uno_path = os.path.abspath ( os.path.join ( 278 os.path.dirname(os.path.realpath(soffice_path)), 279 '..', 280 'basis-link', 281 'program' 282 )) 283 284 _log.info('UNO should be at [%s], appending to sys.path', uno_path) 285 286 sys.path.append(uno_path)
287 #-----------------------------------------------------------
288 -def init_ooo():
289 """FIXME: consider this: 290 291 try: 292 import uno 293 except: 294 print "This Script needs to be run with the python from OpenOffice.org" 295 print "Example: /opt/OpenOffice.org/program/python %s" % ( 296 os.path.basename(sys.argv[0])) 297 print "Or you need to insert the right path at the top, where uno.py is." 298 print "Default: %s" % default_path 299 """ 300 global uno 301 if uno is not None: 302 return 303 304 try: 305 import uno 306 except ImportError: 307 __configure_path_to_UNO() 308 import uno 309 310 global unohelper, oooXCloseListener, oooNoConnectException, oooPropertyValue 311 312 import unohelper 313 from com.sun.star.util import XCloseListener as oooXCloseListener 314 from com.sun.star.connection import NoConnectException as oooNoConnectException 315 from com.sun.star.beans import PropertyValue as oooPropertyValue 316 317 #---------------------------------- 318 class _cOOoDocumentCloseListener(unohelper.Base, oooXCloseListener): 319 """Listens for events sent by OOo during the document closing 320 sequence and notifies the GNUmed client GUI so it can 321 import the closed document into the database. 322 """ 323 def __init__(self, document=None): 324 self.document = document
325 326 def queryClosing(self, evt, owner): 327 # owner is True/False whether I am the owner of the doc 328 pass 329 330 def notifyClosing(self, evt): 331 pass 332 333 def disposing(self, evt): 334 self.document.on_disposed_by_ooo() 335 self.document = None 336 #---------------------------------- 337 338 global cOOoDocumentCloseListener 339 cOOoDocumentCloseListener = _cOOoDocumentCloseListener 340 341 _log.debug('python UNO bridge successfully initialized') 342 343 #------------------------------------------------------------
344 -class gmOOoConnector(gmBorg.cBorg):
345 """This class handles the connection to OOo. 346 347 Its Singleton instance stays around once initialized. 348 """ 349 # FIXME: need to detect closure of OOo !
350 - def __init__(self):
351 352 init_ooo() 353 354 #self.ooo_start_cmd = 'oowriter -invisible -accept="socket,host=localhost,port=2002;urp;"' 355 #self.remote_context_uri = "uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext" 356 357 pipe_name = "uno-gm2ooo-%s" % str(random.random())[2:] 358 self.ooo_start_cmd = 'oowriter -invisible -norestore -accept="pipe,name=%s;urp"' % pipe_name 359 self.remote_context_uri = "uno:pipe,name=%s;urp;StarOffice.ComponentContext" % pipe_name 360 361 _log.debug('pipe name: %s', pipe_name) 362 _log.debug('startup command: %s', self.ooo_start_cmd) 363 _log.debug('remote context URI: %s', self.remote_context_uri) 364 365 self.resolver_uri = "com.sun.star.bridge.UnoUrlResolver" 366 self.desktop_uri = "com.sun.star.frame.Desktop" 367 368 self.local_context = uno.getComponentContext() 369 self.uri_resolver = self.local_context.ServiceManager.createInstanceWithContext(self.resolver_uri, self.local_context) 370 371 self.__desktop = None
372 #--------------------------------------------------------
373 - def cleanup(self, force=True):
374 if self.__desktop is None: 375 _log.debug('no desktop, no cleanup') 376 return 377 378 try: 379 self.__desktop.terminate() 380 except: 381 _log.exception('cannot terminate OOo desktop')
382 #--------------------------------------------------------
383 - def open_document(self, filename=None):
384 """<filename> must be absolute""" 385 386 if self.desktop is None: 387 _log.error('cannot access OOo desktop') 388 return None 389 390 filename = os.path.expanduser(filename) 391 filename = os.path.abspath(filename) 392 document_uri = uno.systemPathToFileUrl(filename) 393 394 _log.debug('%s -> %s', filename, document_uri) 395 396 doc = self.desktop.loadComponentFromURL(document_uri, "_blank", 0, ()) 397 return doc
398 #-------------------------------------------------------- 399 # internal helpers 400 #--------------------------------------------------------
401 - def __get_startup_settle_time(self):
402 # later factor this out ! 403 dbcfg = gmCfg.cCfgSQL() 404 self.ooo_startup_settle_time = dbcfg.get2 ( 405 option = u'external.ooo.startup_settle_time', 406 workplace = gmSurgery.gmCurrentPractice().active_workplace, 407 bias = u'workplace', 408 default = 3.0 409 )
410 #-------------------------------------------------------- 411 # properties 412 #--------------------------------------------------------
413 - def _get_desktop(self):
414 if self.__desktop is not None: 415 return self.__desktop 416 417 try: 418 self.remote_context = self.uri_resolver.resolve(self.remote_context_uri) 419 except oooNoConnectException: 420 _log.exception('cannot connect to OOo server') 421 _log.info('trying to start OOo server') 422 os.system(self.ooo_start_cmd) 423 self.__get_startup_settle_time() 424 _log.debug('waiting %s seconds for OOo to start up', self.ooo_startup_settle_time) 425 time.sleep(self.ooo_startup_settle_time) # OOo sometimes needs a bit 426 try: 427 self.remote_context = self.uri_resolver.resolve(self.remote_context_uri) 428 except oooNoConnectException: 429 _log.exception('cannot start (or connect to started) OOo server') 430 return None 431 432 self.__desktop = self.remote_context.ServiceManager.createInstanceWithContext(self.desktop_uri, self.remote_context) 433 _log.debug('connection seems established') 434 return self.__desktop
435 436 desktop = property(_get_desktop, lambda x:x)
437 #------------------------------------------------------------
438 -class cOOoLetter(object):
439
440 - def __init__(self, template_file=None, instance_type=None):
441 442 self.template_file = template_file 443 self.instance_type = instance_type 444 self.ooo_doc = None
445 #-------------------------------------------------------- 446 # external API 447 #--------------------------------------------------------
448 - def open_in_ooo(self):
449 # connect to OOo 450 ooo_srv = gmOOoConnector() 451 452 # open doc in OOo 453 self.ooo_doc = ooo_srv.open_document(filename = self.template_file) 454 if self.ooo_doc is None: 455 _log.error('cannot open document in OOo') 456 return False 457 458 # listen for close events 459 pat = gmPerson.gmCurrentPatient() 460 pat.locked = True 461 listener = cOOoDocumentCloseListener(document = self) 462 self.ooo_doc.addCloseListener(listener) 463 464 return True
465 #--------------------------------------------------------
466 - def show(self, visible=True):
467 self.ooo_doc.CurrentController.Frame.ContainerWindow.setVisible(visible)
468 #--------------------------------------------------------
469 - def replace_placeholders(self, handler=None, old_style_too = True):
470 471 # new style embedded, implicit placeholders 472 searcher = self.ooo_doc.createSearchDescriptor() 473 searcher.SearchCaseSensitive = False 474 searcher.SearchRegularExpression = True 475 searcher.SearchWords = True 476 searcher.SearchString = handler.placeholder_regex 477 478 placeholder_instance = self.ooo_doc.findFirst(searcher) 479 while placeholder_instance is not None: 480 try: 481 val = handler[placeholder_instance.String] 482 except: 483 _log.exception(val) 484 val = _('error with placeholder [%s]') % placeholder_instance.String 485 486 if val is None: 487 val = _('error with placeholder [%s]') % placeholder_instance.String 488 489 placeholder_instance.String = val 490 placeholder_instance = self.ooo_doc.findNext(placeholder_instance.End, searcher) 491 492 if not old_style_too: 493 return 494 495 # old style "explicit" placeholders 496 text_fields = self.ooo_doc.getTextFields().createEnumeration() 497 while text_fields.hasMoreElements(): 498 text_field = text_fields.nextElement() 499 500 # placeholder ? 501 if not text_field.supportsService('com.sun.star.text.TextField.JumpEdit'): 502 continue 503 # placeholder of type text ? 504 if text_field.PlaceHolderType != 0: 505 continue 506 507 replacement = handler[text_field.PlaceHolder] 508 if replacement is None: 509 continue 510 511 text_field.Anchor.setString(replacement)
512 #--------------------------------------------------------
513 - def save_in_ooo(self, filename=None):
514 if filename is not None: 515 target_url = uno.systemPathToFileUrl(os.path.abspath(os.path.expanduser(filename))) 516 save_args = ( 517 oooPropertyValue('Overwrite', 0, True, 0), 518 oooPropertyValue('FormatFilter', 0, 'swriter: StarOffice XML (Writer)', 0) 519 520 ) 521 # "store AS url" stores the doc, marks it unmodified and updates 522 # the internal media descriptor - as opposed to "store TO url" 523 self.ooo_doc.storeAsURL(target_url, save_args) 524 else: 525 self.ooo_doc.store()
526 #--------------------------------------------------------
527 - def close_in_ooo(self):
528 self.ooo_doc.dispose() 529 pat = gmPerson.gmCurrentPatient() 530 pat.locked = False 531 self.ooo_doc = None
532 #--------------------------------------------------------
533 - def on_disposed_by_ooo(self):
534 # get current file name from OOo, user may have used Save As 535 filename = uno.fileUrlToSystemPath(self.ooo_doc.URL) 536 # tell UI to import the file 537 gmDispatcher.send ( 538 signal = u'import_document_from_file', 539 filename = filename, 540 document_type = self.instance_type, 541 unlock_patient = True 542 ) 543 self.ooo_doc = None
544 #-------------------------------------------------------- 545 # internal helpers 546 #-------------------------------------------------------- 547 548 #============================================================
549 -class cFormEngine(object):
550 """Ancestor for forms.""" 551
552 - def __init__ (self, template_file=None):
553 self.template_filename = template_file
554 #--------------------------------------------------------
555 - def substitute_placeholders(self, data_source=None):
556 """Parse the template into an instance and replace placeholders with values.""" 557 raise NotImplementedError
558 #--------------------------------------------------------
559 - def edit(self):
560 """Allow editing the instance of the template.""" 561 raise NotImplementedError
562 #--------------------------------------------------------
563 - def generate_output(self, format=None):
564 """Generate output suitable for further processing outside this class, e.g. printing.""" 565 raise NotImplementedError
566 #--------------------------------------------------------
567 - def process (self, data_source=None):
568 """Merge values into the form template. 569 """ 570 pass
571 #--------------------------------------------------------
572 - def cleanup (self):
573 """ 574 A sop to TeX which can't act as a true filter: to delete temporary files 575 """ 576 pass
577 #--------------------------------------------------------
578 - def exe (self, command):
579 """ 580 Executes the provided command. 581 If command cotains %F. it is substituted with the filename 582 Otherwise, the file is fed in on stdin 583 """ 584 pass
585 #--------------------------------------------------------
586 - def store(self, params=None):
587 """Stores the parameters in the backend. 588 589 - link_obj can be a cursor, a connection or a service name 590 - assigning a cursor to link_obj allows the calling code to 591 group the call to store() into an enclosing transaction 592 (for an example see gmReferral.send_referral()...) 593 """ 594 # some forms may not have values ... 595 if params is None: 596 params = {} 597 patient_clinical = self.patient.get_emr() 598 encounter = patient_clinical.active_encounter['pk_encounter'] 599 # FIXME: get_active_episode is no more 600 #episode = patient_clinical.get_active_episode()['pk_episode'] 601 # generate "forever unique" name 602 cmd = "select name_short || ': <' || name_long || '::' || external_version || '>' from paperwork_templates where pk=%s"; 603 rows = gmPG.run_ro_query('reference', cmd, None, self.pk_def) 604 form_name = None 605 if rows is None: 606 _log.error('error retrieving form def for [%s]' % self.pk_def) 607 elif len(rows) == 0: 608 _log.error('no form def for [%s]' % self.pk_def) 609 else: 610 form_name = rows[0][0] 611 # we didn't get a name but want to store the form anyhow 612 if form_name is None: 613 form_name=time.time() # hopefully unique enough 614 # in one transaction 615 queries = [] 616 # - store form instance in form_instance 617 cmd = "insert into form_instances(fk_form_def, form_name, fk_episode, fk_encounter) values (%s, %s, %s, %s)" 618 queries.append((cmd, [self.pk_def, form_name, episode, encounter])) 619 # - store params in form_data 620 for key in params.keys(): 621 cmd = """ 622 insert into form_data(fk_instance, place_holder, value) 623 values ((select currval('form_instances_pk_seq')), %s, %s::text) 624 """ 625 queries.append((cmd, [key, params[key]])) 626 # - get inserted PK 627 queries.append(("select currval ('form_instances_pk_seq')", [])) 628 status, err = gmPG.run_commit('historica', queries, True) 629 if status is None: 630 _log.error('failed to store form [%s] (%s): %s' % (self.pk_def, form_name, err)) 631 return None 632 return status
633 634 #================================================================ 635 # OOo template forms 636 #----------------------------------------------------------------
637 -class cOOoForm(cFormEngine):
638 """A forms engine wrapping OOo.""" 639
640 - def __init__ (self, template_file=None):
641 super(self.__class__, self).__init__(template_file = template_file) 642 643 644 path, ext = os.path.splitext(self.template_filename) 645 if ext in [r'', r'.']: 646 ext = r'.tex' 647 self.instance_filename = r'%s-instance%s' % (path, ext)
648 649 #================================================================ 650 # LaTeX template forms 651 #----------------------------------------------------------------
652 -class cLaTeXForm(cFormEngine):
653 """A forms engine wrapping LaTeX.""" 654
655 - def __init__ (self, template_file=None):
656 super(self.__class__, self).__init__(template_file = template_file) 657 path, ext = os.path.splitext(self.template_filename) 658 if ext in [r'', r'.']: 659 ext = r'.tex' 660 self.instance_filename = r'%s-instance%s' % (path, ext)
661 #--------------------------------------------------------
662 - def substitute_placeholders(self, data_source=None):
663 664 template_file = codecs.open(self.template_filename, 'rU', 'utf8') 665 instance_file = codecs.open(self.instance_filename, 'wb', 'utf8') 666 667 for line in template_file: 668 669 if line.strip() in [u'', u'\r', u'\n', u'\r\n']: 670 instance_file.write(line) 671 continue 672 673 # 1) find placeholders in this line 674 placeholders_in_line = regex.findall(data_source.placeholder_regex, line, regex.IGNORECASE) 675 # 2) and replace them 676 for placeholder in placeholders_in_line: 677 #line = line.replace(placeholder, self._texify_string(data_source[placeholder])) 678 try: 679 val = data_source[placeholder] 680 except: 681 _log.exception(val) 682 val = _('error with placeholder [%s]') % placeholder 683 684 if val is None: 685 val = _('error with placeholder [%s]') % placeholder 686 687 line = line.replace(placeholder, val) 688 689 instance_file.write(line) 690 691 instance_file.close() 692 template_file.close() 693 694 return
695 #--------------------------------------------------------
696 - def edit(self):
697 698 mimetypes = [ 699 u'application/x-latex', 700 u'application/x-tex', 701 u'text/plain' 702 ] 703 704 for mimetype in mimetypes: 705 editor_cmd = gmMimeLib.get_editor_cmd(mimetype, self.instance_filename) 706 707 if editor_cmd is None: 708 editor_cmd = u'sensible-editor %s' % self.instance_filename 709 710 return gmShellAPI.run_command_in_shell(command = editor_cmd, blocking = True)
711 #--------------------------------------------------------
712 - def generate_output(self, instance_file = None, format=None, cleanup=True):
713 714 if instance_file is None: 715 instance_file = self.instance_filename 716 717 try: 718 open(instance_file, 'r').close() 719 except: 720 _log.exception('cannot access form instance file [%s]', instance_file) 721 gmLog2.log_stack_trace() 722 return None 723 724 self.instance_filename = instance_file 725 726 _log.debug('ignoring <format> directive [%s], generating PDF', format) 727 728 # create sandbox for LaTeX to play in 729 sandbox_dir = os.path.splitext(self.template_filename)[0] 730 _log.debug('LaTeX sandbox directory: [%s]', sandbox_dir) 731 732 old_cwd = os.getcwd() 733 _log.debug('CWD: [%s]', old_cwd) 734 735 gmTools.mkdir(sandbox_dir) 736 os.chdir(sandbox_dir) 737 738 sandboxed_instance_filename = os.path.join(sandbox_dir, os.path.split(self.instance_filename)[1]) 739 shutil.move(self.instance_filename, sandboxed_instance_filename) 740 741 # LaTeX can need up to three runs to get cross-references et al right 742 if platform.system() == 'Windows': 743 cmd = r'pdflatex.exe -interaction nonstopmode %s' % sandboxed_instance_filename 744 else: 745 cmd = r'pdflatex -interaction nonstopmode %s' % sandboxed_instance_filename 746 for run in [1, 2, 3]: 747 if not gmShellAPI.run_command_in_shell(command = cmd, blocking = True): 748 _log.error('problem running pdflatex, cannot generate form output') 749 gmDispatcher.send(signal = 'statustext', msg = _('Error running pdflatex. Cannot turn LaTeX template into PDF.'), beep = True) 750 return None 751 752 os.chdir(old_cwd) 753 pdf_name = u'%s.pdf' % os.path.splitext(sandboxed_instance_filename)[0] 754 shutil.move(pdf_name, os.path.split(self.instance_filename)[0]) 755 pdf_name = u'%s.pdf' % os.path.splitext(self.instance_filename)[0] 756 757 # cleanup LaTeX sandbox ? 758 if cleanup: 759 for fname in os.listdir(sandbox_dir): 760 os.remove(os.path.join(sandbox_dir, fname)) 761 os.rmdir(sandbox_dir) 762 763 try: 764 open(pdf_name, 'r').close() 765 return pdf_name 766 except IOError: 767 _log.exception('cannot open target PDF: %s', pdf_name) 768 769 gmDispatcher.send(signal = 'statustext', msg = _('PDF output file cannot be opened.'), beep = True) 770 return None
771 #--------------------------------------------------------
772 - def cleanup(self):
773 try: 774 os.remove(self.template_filename) 775 except: 776 _log.debug(u'cannot remove template file [%s]', self.template_filename)
777 #-------------------------------------------------------- 778 # internal helpers 779 #-------------------------------------------------------- 780 781 #------------------------------------------------------------ 782 form_engines[u'L'] = cLaTeXForm 783 #------------------------------------------------------------ 784 #------------------------------------------------------------
785 -class cIanLaTeXForm(cFormEngine):
786 """A forms engine wrapping LaTeX. 787 """
788 - def __init__ (self, id, template):
789 self.id = id 790 self.template = template
791
792 - def process (self,params={}):
793 try: 794 latex = Cheetah.Template.Template (self.template, filter=LaTeXFilter, searchList=[params]) 795 # create a 'sandbox' directory for LaTeX to play in 796 self.tmp = tempfile.mktemp () 797 os.makedirs (self.tmp) 798 self.oldcwd = os.getcwd () 799 os.chdir (self.tmp) 800 stdin = os.popen ("latex", "w", 2048) 801 stdin.write (str (latex)) #send text. LaTeX spits it's output into stdout 802 # FIXME: send LaTeX output to the logger 803 stdin.close () 804 if not gmShellAPI.run_command_in_shell("dvips texput.dvi -o texput.ps", blocking=True): 805 raise FormError ('DVIPS returned error') 806 except EnvironmentError, e: 807 _log.error(e.strerror) 808 raise FormError (e.strerror) 809 return file ("texput.ps")
810
811 - def xdvi (self):
812 """ 813 For testing purposes, runs Xdvi on the intermediate TeX output 814 WARNING: don't try this on Windows 815 """ 816 gmShellAPI.run_command_in_shell("xdvi texput.dvi", blocking=True)
817
818 - def exe (self, command):
819 if "%F" in command: 820 command.replace ("%F", "texput.ps") 821 else: 822 command = "%s < texput.ps" % command 823 try: 824 if not gmShellAPI.run_command_in_shell(command, blocking=True): 825 _log.error("external command %s returned non-zero" % command) 826 raise FormError ('external command %s returned error' % command) 827 except EnvironmentError, e: 828 _log.error(e.strerror) 829 raise FormError (e.strerror) 830 return True
831
832 - def printout (self):
833 command, set1 = gmCfg.getDBParam (workplace = self.workplace, option = 'main.comms.print') 834 self.exe (command)
835
836 - def cleanup (self):
837 """ 838 Delete all the LaTeX output iles 839 """ 840 for i in os.listdir ('.'): 841 os.unlink (i) 842 os.chdir (self.oldcwd) 843 os.rmdir (self.tmp)
844 845 846 847 848 #================================================================ 849 # define a class for HTML forms (for printing) 850 #================================================================
851 -class cXSLTFormEngine(cFormEngine):
852 """This class can create XML document from requested data, 853 then process it with XSLT template and display results 854 """ 855 856 # FIXME: make the path configurable ? 857 _preview_program = u'oowriter ' #this program must be in the system PATH 858
859 - def __init__ (self, template=None):
860 861 if template is None: 862 raise ValueError(u'%s: cannot create form instance without a template' % __name__) 863 864 cFormEngine.__init__(self, template = template) 865 866 self._FormData = None 867 868 # here we know/can assume that the template was stored as a utf-8 869 # encoded string so use that conversion to create unicode: 870 #self._XSLTData = unicode(str(template.template_data), 'UTF-8') 871 # but in fact, unicode() knows how to handle buffers, so simply: 872 self._XSLTData = unicode(self.template.template_data, 'UTF-8', 'strict') 873 874 # we must still devise a method of extracting the SQL query: 875 # - either by retrieving it from a particular tag in the XSLT or 876 # - by making the stored template actually be a dict which, unpickled, 877 # has the keys "xslt" and "sql" 878 self._SQL_query = u'select 1' #this sql query must output valid xml
879 #-------------------------------------------------------- 880 # external API 881 #--------------------------------------------------------
882 - def process(self, sql_parameters):
883 """get data from backend and process it with XSLT template to produce readable output""" 884 885 # extract SQL (this is wrong but displays what is intended) 886 xslt = libxml2.parseDoc(self._XSLTData) 887 root = xslt.children 888 for child in root: 889 if child.type == 'element': 890 self._SQL_query = child.content 891 break 892 893 # retrieve data from backend 894 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': self._SQL_query, 'args': sql_parameters}], get_col_idx = False) 895 896 __header = '<?xml version="1.0" encoding="UTF-8"?>\n' 897 __body = rows[0][0] 898 899 # process XML data according to supplied XSLT, producing HTML 900 self._XMLData =__header + __body 901 style = libxslt.parseStylesheetDoc(xslt) 902 xml = libxml2.parseDoc(self._XMLData) 903 html = style.applyStylesheet(xml, None) 904 self._FormData = html.serialize() 905 906 style.freeStylesheet() 907 xml.freeDoc() 908 html.freeDoc()
909 #--------------------------------------------------------
910 - def preview(self):
911 if self._FormData is None: 912 raise ValueError, u'Preview request for empty form. Make sure the form is properly initialized and process() was performed' 913 914 fname = gmTools.get_unique_filename(prefix = u'gm_XSLT_form-', suffix = u'.html') 915 #html_file = os.open(fname, 'wb') 916 #html_file.write(self._FormData.encode('UTF-8')) 917 html_file = codecs.open(fname, 'wb', 'utf8', 'strict') # or 'replace' ? 918 html_file.write(self._FormData) 919 html_file.close() 920 921 cmd = u'%s %s' % (self.__class__._preview_program, fname) 922 923 if not gmShellAPI.run_command_in_shell(command = cmd, blocking = False): 924 _log.error('%s: cannot launch report preview program' % __name__) 925 return False 926 927 #os.unlink(self.filename) #delete file 928 #FIXME: under Windows the temp file is deleted before preview program gets it (under Linux it works OK) 929 930 return True
931 #--------------------------------------------------------
932 - def print_directly(self):
933 #not so fast, look at it first 934 self.preview()
935 936 937 #===================================================== 938 engines = { 939 u'L': cLaTeXForm 940 } 941 #===================================================== 942 #class LaTeXFilter(Cheetah.Filters.Filter):
943 -class LaTeXFilter:
944 - def filter (self, item, table_sep= " \\\\\n", **kwds):
945 """ 946 Convience function to escape ISO-Latin-1 strings for TeX output 947 WARNING: not all ISO-Latin-1 characters are expressible in TeX 948 FIXME: nevertheless, there are a few more we could support 949 950 Also intelligently convert lists and tuples into TeX-style table lines 951 """ 952 if type (item) is types.UnicodeType or type (item) is types.StringType: 953 item = item.replace ("\\", "\\backslash") # I wonder about this, do we want users to be able to use raw TeX? 954 item = item.replace ("&", "\\&") 955 item = item.replace ("$", "\\$") 956 item = item.replace ('"', "") # okay, that's not right, but easiest solution for now 957 item = item.replace ("\n", "\\\\ ") 958 if len (item.strip ()) == 0: 959 item = "\\relax " # sometimes TeX really hates empty strings, this seems to mollify it 960 # FIXME: cover all of ISO-Latin-1 which can be expressed in TeX 961 if type (item) is types.UnicodeType: 962 item = item.encode ('latin-1', 'replace') 963 trans = {'ß':'\\ss{}', 'ä': '\\"{a}', 'Ä' :'\\"{A}', 'ö': '\\"{o}', 'Ö': '\\"{O}', 'ü': '\\"{u}', 'Ü': '\\"{U}', 964 '\x8a':'\\v{S}', '\x8a':'\\OE{}', '\x9a':'\\v{s}', '\x9c': '\\oe{}', '\a9f':'\\"{Y}', #Microsloth extensions 965 '\x86': '{\\dag}', '\x87': '{\\ddag}', '\xa7':'{\\S}', '\xb6': '{\\P}', '\xa9': '{\\copyright}', '\xbf': '?`', 966 '\xc0':'\\`{A}', '\xa1': "\\'{A}", '\xa2': '\\^{A}', '\xa3':'\\~{A}', '\\xc5': '{\AA}', 967 '\xc7':'\\c{C}', '\xc8':'\\`{E}', 968 '\xa1': '!`', 969 '\xb5':'$\mu$', '\xa3': '\pounds{}', '\xa2':'cent'} 970 for k, i in trans.items (): 971 item = item.replace (k, i) 972 elif type (item) is types.ListType or type (item) is types.TupleType: 973 item = string.join ([self.filter (i, ' & ') for i in item], table_sep) 974 elif item is None: 975 item = '\\relax % Python None\n' 976 elif type (item) is types.IntType or type (item) is types.FloatType: 977 item = str (item) 978 else: 979 item = str (item) 980 _log.warning("unknown type %s, string %s" % (type (item), item)) 981 return item
982 983 984 #===========================================================
985 -class cHL7Form (cFormEngine):
986 pass
987 988 #============================================================ 989 # convenience functions 990 #------------------------------------------------------------
991 -def get_form(id):
992 """ 993 Instantiates a FormEngine based on the form ID or name from the backend 994 """ 995 try: 996 # it's a number: match to form ID 997 id = int (id) 998 cmd = 'select template, engine, pk from paperwork_templates where pk = %s' 999 except ValueError: 1000 # it's a string, match to the form's name 1001 # FIXME: can we somehow OR like this: where name_short=%s OR name_long=%s ? 1002 cmd = 'select template, engine, flags, pk from paperwork_templates where name_short = %s' 1003 result = gmPG.run_ro_query ('reference', cmd, None, id) 1004 if result is None: 1005 _log.error('error getting form [%s]' % id) 1006 raise gmExceptions.FormError ('error getting form [%s]' % id) 1007 if len(result) == 0: 1008 _log.error('no form [%s] found' % id) 1009 raise gmExceptions.FormError ('no such form found [%s]' % id) 1010 if result[0][1] == 'L': 1011 return LaTeXForm (result[0][2], result[0][0]) 1012 elif result[0][1] == 'T': 1013 return TextForm (result[0][2], result[0][0]) 1014 else: 1015 _log.error('no form engine [%s] for form [%s]' % (result[0][1], id)) 1016 raise FormError ('no engine [%s] for form [%s]' % (result[0][1], id))
1017 #-------------------------------------------------------------
1018 -class FormError (Exception):
1019 - def __init__ (self, value):
1020 self.value = value
1021
1022 - def __str__ (self):
1023 return repr (self.value)
1024 #------------------------------------------------------------- 1025 1026 test_letter = """ 1027 \\documentclass{letter} 1028 \\address{ $DOCTOR \\\\ 1029 $DOCTORADDRESS} 1030 \\signature{$DOCTOR} 1031 1032 \\begin{document} 1033 \\begin{letter}{$RECIPIENTNAME \\\\ 1034 $RECIPIENTADDRESS} 1035 1036 \\opening{Dear $RECIPIENTNAME} 1037 1038 \\textbf{Re:} $PATIENTNAME, DOB: $DOB, $PATIENTADDRESS \\\\ 1039 1040 $TEXT 1041 1042 \\ifnum$INCLUDEMEDS>0 1043 \\textbf{Medications List} 1044 1045 \\begin{tabular}{lll} 1046 $MEDSLIST 1047 \\end{tabular} 1048 \\fi 1049 1050 \\ifnum$INCLUDEDISEASES>0 1051 \\textbf{Disease List} 1052 1053 \\begin{tabular}{l} 1054 $DISEASELIST 1055 \\end{tabular} 1056 \\fi 1057 1058 \\closing{$CLOSING} 1059 1060 \\end{letter} 1061 \\end{document} 1062 """ 1063 1064
1065 -def test_au():
1066 f = open('../../test-area/ian/terry-form.tex') 1067 params = { 1068 'RECIPIENT': "Dr. R. Terry\n1 Main St\nNewcastle", 1069 'DOCTORSNAME': 'Ian Haywood', 1070 'DOCTORSADDRESS': '1 Smith St\nMelbourne', 1071 'PATIENTNAME':'Joe Bloggs', 1072 'PATIENTADDRESS':'18 Fred St\nMelbourne', 1073 'REQUEST':'echocardiogram', 1074 'THERAPY':'on warfarin', 1075 'CLINICALNOTES':"""heard new murmur 1076 Here's some 1077 crap to demonstrate how it can cover multiple lines.""", 1078 'COPYADDRESS':'Karsten Hilbert\nLeipzig, Germany', 1079 'ROUTINE':1, 1080 'URGENT':0, 1081 'FAX':1, 1082 'PHONE':1, 1083 'PENSIONER':1, 1084 'VETERAN':0, 1085 'PADS':0, 1086 'INSTRUCTIONS':u'Take the blue pill, Neo' 1087 } 1088 form = LaTeXForm (1, f.read()) 1089 form.process (params) 1090 form.xdvi () 1091 form.cleanup ()
1092
1093 -def test_au2 ():
1094 form = LaTeXForm (2, test_letter) 1095 params = {'RECIPIENTNAME':'Dr. Richard Terry', 1096 'RECIPIENTADDRESS':'1 Main St\nNewcastle', 1097 'DOCTOR':'Dr. Ian Haywood', 1098 'DOCTORADDRESS':'1 Smith St\nMelbourne', 1099 'PATIENTNAME':'Joe Bloggs', 1100 'PATIENTADDRESS':'18 Fred St, Melbourne', 1101 'TEXT':"""This is the main text of the referral letter""", 1102 'DOB':'12/3/65', 1103 'INCLUDEMEDS':1, 1104 'MEDSLIST':[["Amoxycillin", "500mg", "TDS"], ["Perindopril", "4mg", "OD"]], 1105 'INCLUDEDISEASES':0, 'DISEASELIST':'', 1106 'CLOSING':'Yours sincerely,' 1107 } 1108 form.process (params) 1109 print os.getcwd () 1110 form.xdvi () 1111 form.cleanup ()
1112 #------------------------------------------------------------
1113 -def test_de():
1114 template = open('../../test-area/ian/Formularkopf-DE.tex') 1115 form = LaTeXForm(template=template.read()) 1116 params = { 1117 'PATIENT LASTNAME': 'Kirk', 1118 'PATIENT FIRSTNAME': 'James T.', 1119 'PATIENT STREET': 'Hauptstrasse', 1120 'PATIENT ZIP': '02999', 1121 'PATIENT TOWN': 'Gross Saerchen', 1122 'PATIENT DOB': '22.03.1931' 1123 } 1124 form.process(params) 1125 form.xdvi() 1126 form.cleanup()
1127 1128 #============================================================ 1129 # main 1130 #------------------------------------------------------------ 1131 if __name__ == '__main__': 1132 1133 if len(sys.argv) < 2: 1134 sys.exit() 1135 1136 if sys.argv[1] != 'test': 1137 sys.exit() 1138 1139 from Gnumed.pycommon import gmI18N, gmDateTime 1140 gmI18N.activate_locale() 1141 gmI18N.install_domain(domain='gnumed') 1142 gmDateTime.init() 1143 1144 #-------------------------------------------------------- 1145 # OOo 1146 #--------------------------------------------------------
1147 - def test_init_ooo():
1148 init_ooo()
1149 #--------------------------------------------------------
1150 - def test_ooo_connect():
1151 srv = gmOOoConnector() 1152 print srv 1153 print srv.desktop
1154 #--------------------------------------------------------
1155 - def test_open_ooo_doc_from_srv():
1156 srv = gmOOoConnector() 1157 doc = srv.open_document(filename = sys.argv[2]) 1158 print "document:", doc
1159 #--------------------------------------------------------
1160 - def test_open_ooo_doc_from_letter():
1161 doc = cOOoLetter(template_file = sys.argv[2]) 1162 doc.open_in_ooo() 1163 print "document:", doc 1164 raw_input('press <ENTER> to continue') 1165 doc.show() 1166 #doc.replace_placeholders() 1167 #doc.save_in_ooo('~/test_cOOoLetter.odt') 1168 # doc = None 1169 # doc.close_in_ooo() 1170 raw_input('press <ENTER> to continue')
1171 #--------------------------------------------------------
1172 - def play_with_ooo():
1173 try: 1174 doc = open_uri_in_ooo(filename=sys.argv[1]) 1175 except: 1176 _log.exception('cannot open [%s] in OOo' % sys.argv[1]) 1177 raise 1178 1179 class myCloseListener(unohelper.Base, oooXCloseListener): 1180 def disposing(self, evt): 1181 print "disposing:"
1182 def notifyClosing(self, evt): 1183 print "notifyClosing:" 1184 def queryClosing(self, evt, owner): 1185 # owner is True/False whether I am the owner of the doc 1186 print "queryClosing:" 1187 1188 l = myCloseListener() 1189 doc.addCloseListener(l) 1190 1191 tfs = doc.getTextFields().createEnumeration() 1192 print tfs 1193 print dir(tfs) 1194 while tfs.hasMoreElements(): 1195 tf = tfs.nextElement() 1196 if tf.supportsService('com.sun.star.text.TextField.JumpEdit'): 1197 print tf.getPropertyValue('PlaceHolder') 1198 print " ", tf.getPropertyValue('Hint') 1199 1200 # doc.close(True) # closes but leaves open the dedicated OOo window 1201 doc.dispose() # closes and disposes of the OOo window 1202 #--------------------------------------------------------
1203 - def test_cOOoLetter():
1204 pat = gmPerson.ask_for_patient() 1205 if pat is None: 1206 return 1207 gmPerson.set_active_patient(patient = pat) 1208 1209 doc = cOOoLetter(template_file = sys.argv[2]) 1210 doc.open_in_ooo() 1211 print doc 1212 doc.show() 1213 #doc.replace_placeholders() 1214 #doc.save_in_ooo('~/test_cOOoLetter.odt') 1215 doc = None 1216 # doc.close_in_ooo() 1217 raw_input('press <ENTER> to continue')
1218 #-------------------------------------------------------- 1219 # other 1220 #--------------------------------------------------------
1221 - def test_cFormTemplate():
1222 template = cFormTemplate(aPK_obj = sys.argv[2]) 1223 print template 1224 print template.export_to_file()
1225 #--------------------------------------------------------
1226 - def set_template_from_file():
1227 template = cFormTemplate(aPK_obj = sys.argv[2]) 1228 template.update_template_from_file(filename = sys.argv[3])
1229 #--------------------------------------------------------
1230 - def test_latex_form():
1231 pat = gmPerson.ask_for_patient() 1232 if pat is None: 1233 return 1234 gmPerson.set_active_patient(patient = pat) 1235 1236 gmPerson.gmCurrentProvider(provider = gmPerson.cStaff()) 1237 1238 path = os.path.abspath(sys.argv[2]) 1239 form = cLaTeXForm(template_file = path) 1240 1241 from Gnumed.wxpython import gmMacro 1242 ph = gmMacro.gmPlaceholderHandler() 1243 ph.debug = True 1244 instance_file = form.substitute_placeholders(data_source = ph) 1245 pdf_name = form.generate_output(instance_file = instance_file, cleanup = False) 1246 print "final PDF file is:", pdf_name
1247 1248 #-------------------------------------------------------- 1249 #-------------------------------------------------------- 1250 # now run the tests 1251 #test_au() 1252 #test_de() 1253 1254 # OOo 1255 #test_init_ooo() 1256 #test_ooo_connect() 1257 #test_open_ooo_doc_from_srv() 1258 #test_open_ooo_doc_from_letter() 1259 #play_with_ooo() 1260 #test_cOOoLetter() 1261 1262 #test_cFormTemplate() 1263 #set_template_from_file() 1264 test_latex_form() 1265 1266 #============================================================ 1267