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