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