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