1 """GNUmed export area
2
3 Think shopping cart in a web shop.
4
5 This is where you want to put documents for further
6 processing by you or someone else, like your secretary.
7 """
8
9 __license__ = "GPL v2 or later"
10 __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
11
12
13 import sys
14 import logging
15 import shutil
16 import os
17 import io
18 import platform
19
20
21 if __name__ == '__main__':
22 sys.path.insert(0, '../../')
23 from Gnumed.pycommon import gmI18N
24 gmI18N.activate_locale()
25 gmI18N.install_domain()
26 from Gnumed.pycommon import gmTools
27 from Gnumed.pycommon import gmBusinessDBObject
28 from Gnumed.pycommon import gmPG2
29 from Gnumed.pycommon import gmMimeLib
30 from Gnumed.pycommon import gmDateTime
31 from Gnumed.pycommon import gmCfg2
32 from Gnumed.pycommon import gmCrypto
33
34 from Gnumed.business import gmDocuments
35 from Gnumed.business import gmKeywordExpansion
36
37
38 _log = logging.getLogger('gm.exp_area')
39 _cfg = gmCfg2.gmCfgData()
40
41 PRINT_JOB_DESIGNATION = 'print'
42 DOCUMENTS_SUBDIR = 'documents'
43 DIRENTRY_README_NAME = '.README.GNUmed-DIRENTRY'
44
45
46
47
48 _SQL_get_export_items = "SELECT * FROM clin.v_export_items WHERE %s"
49
50 -class cExportItem(gmBusinessDBObject.cBusinessDBObject):
51 """Represents an item in the export area table"""
52
53 _cmd_fetch_payload = _SQL_get_export_items % "pk_export_item = %s"
54 _cmds_store_payload = [
55 """UPDATE clin.export_item SET
56 fk_identity = %(pk_identity)s,
57 created_by = gm.nullify_empty_string(%(created_by)s),
58 created_when = %(created_when)s,
59 designation = gm.nullify_empty_string(%(designation)s),
60 description = gm.nullify_empty_string(%(description)s),
61 fk_doc_obj = %(pk_doc_obj)s,
62 data = CASE
63 WHEN %(pk_doc_obj)s IS NULL THEN coalesce(data, 'to be replaced by real data')
64 ELSE NULL
65 END,
66 filename = CASE
67 WHEN %(pk_doc_obj)s IS NULL THEN gm.nullify_empty_string(%(filename)s)
68 ELSE NULL
69 END
70 WHERE
71 pk = %(pk_export_item)s
72 AND
73 xmin = %(xmin_export_item)s
74 """,
75 _SQL_get_export_items % 'pk_export_item = %(pk_export_item)s'
76 ]
77 _updatable_fields = [
78 'pk_identity',
79 'created_when',
80 'designation',
81 'description',
82 'pk_doc_obj',
83 'filename'
84 ]
85
86 - def __init__(self, aPK_obj=None, row=None, link_obj=None):
87 super(cExportItem, self).__init__(aPK_obj = aPK_obj, row = row, link_obj = link_obj)
88
89 if self._payload[self._idx['pk_identity_raw_needs_update']]:
90 _log.warning (
91 'auto-healing export item [%s] from identity [%s] to [%s] because of document part [%s] seems necessary',
92 self._payload[self._idx['pk_export_item']],
93 self._payload[self._idx['pk_identity_raw']],
94 self._payload[self._idx['pk_identity']],
95 self._payload[self._idx['pk_doc_obj']]
96 )
97 if self._payload[self._idx['pk_doc_obj']] is None:
98 _log.error('however, .fk_doc_obj is NULL, which should not happen, leaving things alone for manual inspection')
99 return
100
101
102 self._is_modified = True
103 self.save()
104 self.refetch_payload(ignore_changes = False, link_obj = link_obj)
105
106
107
108
109
111 assert (data is not None), '<data> must not be <None>'
112
113 SQL = """
114 UPDATE clin.export_item SET
115 data = %(data)s::bytea,
116 fk_doc_obj = NULL
117 WHERE pk = %(pk)s"""
118 args = {'pk': self.pk_obj, 'data': data}
119 gmPG2.run_rw_queries(queries = [{'cmd': SQL, 'args': args}], return_data = False, get_col_idx = False)
120
121 self.refetch_payload()
122 return True
123
124
126
127 if not (os.access(filename, os.R_OK) and os.path.isfile(filename)):
128 _log.error('[%s] is not a readable file' % filename)
129 return False
130
131 cmd = """
132 UPDATE clin.export_item SET
133 data = %(data)s::bytea,
134 fk_doc_obj = NULL,
135 filename = gm.nullify_empty_string(%(fname)s)
136 WHERE pk = %(pk)s"""
137 args = {'pk': self.pk_obj, 'fname': filename}
138 if not gmPG2.file2bytea(query = cmd, filename = filename, args = args):
139 return False
140
141
142 self.refetch_payload()
143 return True
144
145
146 - def save_to_file(self, aChunkSize=0, filename=None, directory=None, passphrase=None):
147
148
149 part_fname = self.__save_doc_obj(filename = filename, directory = directory, passphrase = passphrase)
150 if part_fname is False:
151 return None
152 if part_fname is not None:
153 return part_fname
154
155
156 if self.is_valid_DIRENTRY:
157 target_dir = self.__save_direntry(directory, passphrase = passphrase)
158 if target_dir is False:
159 return None
160 if target_dir is not None:
161 return target_dir
162
163 if self.is_DIRENTRY:
164
165 return None
166
167
168 if filename is None:
169 filename = self.get_useful_filename(directory = directory)
170 success = gmPG2.bytea2file (
171 data_query = {
172 'cmd': 'SELECT substring(data from %(start)s for %(size)s) FROM clin.export_item WHERE pk = %(pk)s',
173 'args': {'pk': self.pk_obj}
174 },
175 filename = filename,
176 chunk_size = aChunkSize,
177 data_size = self._payload[self._idx['size']]
178 )
179 if not success:
180 return None
181
182 if filename.endswith('.dat'):
183 filename = gmMimeLib.adjust_extension_by_mimetype(filename)
184 if passphrase is None:
185 return filename
186
187 enc_filename = gmCrypto.encrypt_file (
188 filename = filename,
189 passphrase = passphrase,
190 verbose = _cfg.get(option = 'debug'),
191 remove_unencrypted = True
192 )
193 removed = gmTools.remove_file(filename)
194 if enc_filename is None:
195 _log.error('cannot encrypt')
196 return None
197 if removed:
198 return enc_filename
199 _log.error('cannot remove unencrypted file')
200 gmTools.remove(enc_filename)
201 return None
202
203
205
206
207 if self._payload[self._idx['pk_doc_obj']] is not None:
208 return self.document_part.display_via_mime(chunksize = chunksize, block = block)
209
210
211 if self._payload[self._idx['filename']].startswith('DIR::'):
212
213 tag, node, path = self._payload[self._idx['filename']].split('::', 2)
214 if node != platform.node():
215 msg = _(
216 'This item points to a directory on the computer named:\n'
217 ' %s\n'
218 'You are, however, currently using another computer:\n'
219 ' %s\n'
220 'Directory items can only be viewed/saved/exported\n'
221 'on the computer they are pointing to.'
222 ) % (node, platform.node())
223 return False, msg
224 success, msg = gmMimeLib.call_viewer_on_file(path, block = block)
225 return success, msg
226
227
228 fname = self.save_to_file(aChunkSize = chunksize)
229 if fname is None:
230 return False, ''
231
232 success, msg = gmMimeLib.call_viewer_on_file(fname, block = block)
233 if not success:
234 return False, msg
235
236 return True, ''
237
238
240 patient_part = ''
241 if patient is not None:
242 patient_part = '-%s' % patient.subdir_name
243
244
245 suffix = '.dat'
246 if self._payload[self._idx['filename']] is not None:
247 tmp, suffix = os.path.splitext (
248 gmTools.fname_sanitize(self._payload[self._idx['filename']]).lower()
249 )
250 if suffix == '':
251 suffix = '.dat'
252 fname = gmTools.get_unique_filename (
253 prefix = 'gm-export_item%s-' % patient_part,
254 suffix = suffix,
255 tmp_dir = directory
256 )
257 return fname
258
259
260
261
262 - def __save_doc_obj(self, filename=None, directory=None, passphrase=None):
263 """Save doc object part into target.
264
265 None: not a doc obj
266 True: success
267 False: failure
268 """
269 if self._payload[self._idx['pk_doc_obj']] is None:
270 return None
271
272 part = self.document_part
273 if filename is None:
274 filename = part.get_useful_filename (
275 make_unique = False,
276 directory = directory,
277 include_gnumed_tag = False,
278 date_before_type = True,
279 name_first = False
280 )
281 part_fname = part.save_to_file (
282 aChunkSize = aChunkSize,
283 filename = filename,
284 ignore_conversion_problems = True,
285 adjust_extension = True
286 )
287 if part_fname is None:
288 _log.error('cannot save document part to file')
289 return False
290
291 if passphrase is None:
292 return part_fname
293
294 enc_filename = gmCrypto.encrypt_file (
295 filename = part_fname,
296 passphrase = passphrase,
297 verbose = _cfg.get(option = 'debug'),
298 remove_unencrypted = True
299 )
300 removed = gmTools.remove_file(filename)
301 if enc_filename is None:
302 _log.error('cannot encrypt')
303 return False
304 if removed:
305 return enc_filename
306 _log.error('cannot remove unencrypted file')
307 gmTools.remove(enc_filename)
308 return False
309
310
311 - def __save_direntry(self, directory=None, passphrase=None):
312 """Move DIRENTRY source into target.
313
314 None: not a DIRENTRY
315 True: success
316 False: failure
317 """
318
319 try:
320 tag, node, local_fs_path = self._payload[self._idx['filename']].split('::', 2)
321 except ValueError:
322 _log.exception('malformed DIRENTRY: [%s]', self._payload[self._idx['filename']])
323 return False
324
325 if directory is None:
326 directory = gmTools.mk_sandbox_dir(prefix = 'exp-')
327 if directory.startswith(local_fs_path):
328 _log.error('cannot dump DIRENTRY item [%s]: must not be subdirectory of target dir [%s]', self._payload[self._idx['filename']], directory)
329 return False
330 if local_fs_path.startswith(directory):
331 _log.error('cannot dump DIRENTRY item [%s]: target dir [%s] must not be subdirectory of DIRENTRY', self._payload[self._idx['filename']], directory)
332 return False
333
334 _log.debug('dumping DIRENTRY item [%s] into [%s]', self._payload[self._idx['filename']], directory)
335 sandbox_dir = gmTools.mk_sandbox_dir()
336 _log.debug('sandbox: %s', sandbox_dir)
337 tmp = gmTools.copy_tree_content(local_fs_path, sandbox_dir)
338 if tmp is None:
339 _log.error('cannot dump DIRENTRY item [%s] into [%s]: copy error', self._payload[self._idx['filename']], sandbox_dir)
340 return False
341
342 gmTools.remove_file(os.path.join(tmp, DIRENTRY_README_NAME))
343
344 if passphrase is not None:
345 _log.debug('encrypting sandbox: %s', sandbox_dir)
346 encrypted = gmCrypto.encrypt_directory_content (
347 directory = sandbox_dir,
348 passphrase = passphrase,
349 verbose = _cfg.get(option = 'debug'),
350 remove_unencrypted = True
351 )
352 if not encrypted:
353 _log.error('cannot dump DIRENTRY item [%s]: encryption problem in [%s]', self._payload[self._idx['filename']], sandbox_dir)
354 return False
355
356 tmp = gmTools.copy_tree_content(sandbox_dir, directory)
357 if tmp is None:
358 _log.debug('cannot dump DIRENTRY item [%s] into [%s]: copy error', self._payload[self._idx['filename']], directory)
359 return False
360
361 return directory
362
363
364
365
367 if self._payload[self._idx['pk_doc_obj']] is None:
368 return None
369 return gmDocuments.cDocumentPart(aPK_obj = self._payload[self._idx['pk_doc_obj']])
370
371 document_part = property(_get_doc_part, lambda x:x)
372
373
376
383
384 is_print_job = property(_get_is_print_job, _set_is_print_job)
385
386
387 - def _is_DIRENTRY(self):
388 """Check whether this item looks like a DIRENTRY."""
389 if self._payload[self._idx['filename']] is None:
390 return False
391 if not self._payload[self._idx['filename']].startswith('DIR::'):
392 return False
393 if len(self._payload[self._idx['filename']].split('::', 2)) != 3:
394 return False
395 return True
396
397 is_DIRENTRY = property(_is_DIRENTRY)
398
399
401 """Check whether this item is a _valid_ DIRENTRY."""
402 if not self.is_DIRENTRY:
403 return False
404
405 try:
406 tag, node, local_fs_path = self._payload[self._idx['filename']].split('::', 2)
407 except ValueError:
408
409
410 _log.exception('DIRENTRY [%s]: malformed', self._payload[self._idx['filename']])
411 return False
412
413 if node != platform.node():
414 _log.warning('DIRENTRY [%s]: not on this machine (%s)', self._payload[self._idx['filename']], platform.node())
415 return False
416
417 if not os.path.isdir(local_fs_path):
418 _log.warning('DIRENTRY [%s]: directory not found (old DIRENTRY ?)', self._payload[self._idx['filename']])
419 return False
420 return True
421
422 is_valid_DIRENTRY = property(_is_valid_DIRENTRY)
423
424
426 """Check whether this item points to a DICOMDIR."""
427 if not self._is_valid_DIRENTRY:
428 return False
429 tag, node, local_fs_path = self._payload[self._idx['filename']].split('::', 2)
430 found_DICOMDIR = False
431 for fs_entry in os.listdir(local_fs_path):
432
433 if os.path.isdir(os.path.join(local_fs_path, fs_entry)):
434
435 continue
436
437 if fs_entry != 'DICOMDIR':
438
439 return False
440
441 found_DICOMDIR = True
442 return found_DICOMDIR
443
444 is_DICOM_directory = property(_is_DICOM_directory)
445
446
448 """True if there are files in the root directory."""
449 tag, node, local_fs_path = self._payload[self._idx['filename']].split('::', 2)
450 for fs_entry in os.listdir(local_fs_path):
451 if os.path.isfile(fs_entry):
452 _log.debug('has files in top level: %s', local_fs_path)
453 return True
454 return False
455
456 has_files_in_root = property(_has_files_in_root)
457
458
459 -def get_export_items(order_by=None, pk_identity=None, designation=None, return_pks=False):
460
461 args = {
462 'pat': pk_identity,
463 'desig': gmTools.coalesce(designation, PRINT_JOB_DESIGNATION)
464 }
465 where_parts = []
466 if pk_identity is not None:
467 where_parts.append('pk_identity = %(pat)s')
468
469
470 if designation is None:
471 where_parts.append("designation IS DISTINCT FROM %(desig)s")
472 else:
473 where_parts.append('designation = %(desig)s')
474
475 if order_by is None:
476 order_by = ''
477 else:
478 order_by = ' ORDER BY %s' % order_by
479
480 cmd = (_SQL_get_export_items % ' AND '.join(where_parts)) + order_by
481 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
482 if return_pks:
483 return [ r['pk_export_item'] for r in rows ]
484 return [ cExportItem(row = {'data': r, 'idx': idx, 'pk_field': 'pk_export_item'}) for r in rows ]
485
486
489
490
491 -def create_export_item(description=None, pk_identity=None, pk_doc_obj=None, filename=None):
492
493 args = {
494 'desc': description,
495 'pk_obj': pk_doc_obj,
496 'pk_pat': pk_identity,
497 'fname': filename
498 }
499 cmd = """
500 INSERT INTO clin.export_item (
501 description,
502 fk_doc_obj,
503 fk_identity,
504 data,
505 filename
506 ) VALUES (
507 gm.nullify_empty_string(%(desc)s),
508 %(pk_obj)s,
509 %(pk_pat)s,
510 (CASE
511 WHEN %(pk_obj)s IS NULL THEN %(fname)s::bytea
512 ELSE NULL::bytea
513 END),
514 (CASE
515 WHEN %(pk_obj)s IS NULL THEN gm.nullify_empty_string(%(fname)s)
516 ELSE NULL
517 END)
518 )
519 RETURNING pk
520 """
521 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
522
523 return cExportItem(aPK_obj = rows[0]['pk'])
524
525
527 args = {'pk': pk_export_item}
528 cmd = "DELETE FROM clin.export_item WHERE pk = %(pk)s"
529 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
530 return True
531
532
533 _FRONTPAGE_HTML_CONTENT = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
534 "http://www.w3.org/TR/html4/loose.dtd">
535 <html>
536 <head>
537 <meta http-equiv="content-type" content="text/html; charset=UTF-8">
538 <link rel="icon" type="image/x-icon" href="gnumed.ico">
539 <title>%(html_title_header)s %(html_title_patient)s</title>
540 </head>
541 <body>
542
543 <h1>%(title)s</h1>
544
545 <p>
546 (%(date)s)<br>
547 </p>
548
549 <h2><a href="patient.vcf">Patient</a></h2>
550
551 <p>
552 %(pat_name)s<br>
553 %(pat_dob)s
554 </p>
555
556 <p><img src="%(mugshot_url)s" alt="%(mugshot_alt)s" title="%(mugshot_title)s" width="200" border="2"></p>
557
558 <h2>%(docs_title)s</h2>
559
560 <ul>
561 <li><a href="./">%(browse_root)s</a></li>
562 <li><a href="%(doc_subdir)s/">%(browse_docs)s</a></li>
563 %(browse_dicomdir)s
564 %(run_dicom_viewer)s
565 </ul>
566
567 <ul>
568 %(docs_list)s
569 </ul>
570
571 <h2><a href="praxis.vcf">Praxis</a></h2>
572
573 <p>
574 %(branch)s @ %(praxis)s
575 %(adr)s
576 </p>
577
578 <p>(<a href="http://www.gnumed.de">GNUmed</a> version %(gm_ver)s)</p>
579
580 </body>
581 </html>
582 """
583
584 _INDEX_HTML_CONTENT = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
585 "http://www.w3.org/TR/html4/loose.dtd">
586 <html>
587 <head>
588 <meta http-equiv="content-type" content="text/html; charset=UTF-8">
589 <link rel="icon" type="image/x-icon" href="gnumed.ico">
590 <title>%(html_title_header)s</title>
591 </head>
592 <body>
593
594 <h1>%(title)s</h1>
595
596 <p>
597 (%(date)s)<br>
598 </p>
599
600 This is an encrypted patient data excerpt created by the GNUmed Electronic Medical Record.
601
602 <p>
603 For decryption you will need to
604
605 <ul>
606 <li>install decryption software and</li>
607 <li>obtain relevant passwords from the creator or holder of this media</li>
608 </ul>
609
610 <h2>Decryption software</h2>
611
612 For files ending in
613
614 <ul>
615 <li>.asc: install <a href="https://gnupg.org">GNU Privacy Guard</a></li>
616 <li>.7z: install <a href="https://www.7-zip.org">7-zip</a> or <a href="https://www.winzip.com">WinZip</a></li>
617 </ul>
618
619
620 <h2>%(docs_title)s</h2>
621
622 <ul>
623 <li><a href="./frontpage.html">front page (after decryption)</a></li>
624 <li><a href="./%(frontpage_fname)s">front page (if decryption from browser is supported)</a></li>
625
626 <li><a href="./">%(browse_root)s</a></li>
627 <li><a href="%(doc_subdir)s/">%(browse_docs)s</a></li>
628 </ul>
629
630
631 <h2><a href="praxis.vcf">Praxis</a></h2>
632
633 <p>
634 %(branch)s @ %(praxis)s
635 %(adr)s
636 </p>
637
638 <p>(<a href="http://www.gnumed.de">GNUmed</a> version %(gm_ver)s)</p>
639
640 </body>
641 </html>
642 """
643
644
646
648 self.__pk_identity = pk_identity
649
650
669
670
677
678
679 - def add_path(self, path, comment=None):
680 """Add a DIR entry to the export area.
681
682 This sort of entry points to a certain directory on a
683 certain machine. The content of the the directory
684 will be included in exports, the directory *itself*
685 will not. For *that*, use a disposable top-level
686 directory into which you put the directory to include
687 as a subdirectory.
688 """
689 assert (os.path.isdir(path)), '<path> must exist: %s' % path
690
691 path_item_data = 'DIR::%s::%s/' % (platform.node(), path.rstrip('/'))
692 _log.debug('attempting to add path item [%s]', path_item_data)
693 item = self.path_item_exists(path_item_data)
694 if item is not None:
695 _log.debug('[%s] already in export area', path)
696 return item
697
698 if comment is None:
699 comment = _('path [%s/] on computer "%s"') % (
700 path.rstrip('/'),
701 platform.node()
702 )
703 else:
704 comment += _(' (on "%s")') % platform.node()
705
706 item = create_export_item (
707 description = comment,
708 pk_identity = self.__pk_identity,
709 filename = path_item_data
710 )
711 try:
712 README = open(os.path.join(path, DIRENTRY_README_NAME), mode = 'wt', encoding = 'utf8')
713 README.write('GNUmed DIRENTRY information\n')
714 README.write('created: %s\n' % gmDateTime.pydt_now_here())
715 README.write('machine: %s\n' % platform.node())
716 README.write('path: %s\n' % path)
717 README.close()
718 except OSError:
719 _log.exception('READONLY DIRENTRY [%s]', path)
720
721 return item
722
723
725
726 assert (path_item_data.startswith('DIR::')), 'invalid <path_item_data> [%s]' % path_item_data
727
728 where_parts = [
729 'pk_identity = %(pat)s',
730 'filename = %(fname)s',
731 'pk_doc_obj IS NULL'
732 ]
733 args = {
734 'pat': self.__pk_identity,
735 'fname': path_item_data
736 }
737 SQL = _SQL_get_export_items % ' AND '.join(where_parts)
738 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': SQL, 'args': args}], get_col_idx = True)
739 if len(rows) == 0:
740 return None
741
742 r = rows[0]
743 return cExportItem(row = {'data': r, 'idx': idx, 'pk_field': 'pk_export_item'})
744
745
746 - def add_file(self, filename=None, hint=None):
747 try:
748 open(filename).close()
749 except Exception:
750 _log.exception('cannot open file <%s>', filename)
751 return None
752
753 file_md5 = gmTools.file2md5(filename = filename, return_hex = True)
754 existing_item = self.md5_exists(md5 = file_md5, include_document_parts = False)
755 if existing_item is not None:
756 _log.debug('md5 match (%s): %s already in export area', file_md5, filename)
757 return existing_item
758
759 path, basename = os.path.split(filename)
760 item = create_export_item (
761 description = '%s: %s (%s/)' % (
762 gmTools.coalesce(hint, _('file'), '%s'),
763 basename,
764 path
765 ),
766 pk_identity = self.__pk_identity,
767 filename = filename
768 )
769
770 if item.update_data_from_file(filename = filename):
771 return item
772
773
774 delete_export_item(pk_export_item = item['pk_export_item'])
775 return None
776
777
778 - def add_files(self, filenames=None, hint=None):
779 all_ok = True
780 for fname in filenames:
781 all_ok = all_ok and (self.add_file(filename = fname, hint = hint) is not None)
782
783 return all_ok
784
785
787 for doc in documents:
788 doc_tag = _('%s (%s)%s') % (
789 doc['l10n_type'],
790 gmDateTime.pydt_strftime(doc['clin_when'], '%Y %b %d'),
791 gmTools.coalesce(doc['comment'], '', ' "%s"')
792 )
793 for obj in doc.parts:
794 if self.document_part_item_exists(pk_part = obj['pk_obj']):
795 continue
796 f_ext = ''
797 if obj['filename'] is not None:
798 f_ext = os.path.splitext(obj['filename'])[1].strip('.').strip()
799 if f_ext != '':
800 f_ext = ' .' + f_ext.upper()
801 obj_tag = _('part %s (%s%s)%s') % (
802 obj['seq_idx'],
803 gmTools.size2str(obj['size']),
804 f_ext,
805 gmTools.coalesce(obj['obj_comment'], '', ' "%s"')
806 )
807 create_export_item (
808 description = '%s - %s' % (doc_tag, obj_tag),
809 pk_doc_obj = obj['pk_obj']
810 )
811
812
814 cmd = "SELECT EXISTS (SELECT 1 FROM clin.export_item WHERE fk_doc_obj = %(pk_obj)s)"
815 args = {'pk_obj': pk_part}
816 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
817 return rows[0][0]
818
819
820 - def md5_exists(self, md5=None, include_document_parts=False):
821 where_parts = [
822 'pk_identity = %(pat)s',
823 'md5_sum = %(md5)s'
824 ]
825 args = {
826 'pat': self.__pk_identity,
827 'md5': md5
828 }
829
830 if not include_document_parts:
831 where_parts.append('pk_doc_obj IS NULL')
832
833 cmd = _SQL_get_export_items % ' AND '.join(where_parts)
834 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
835
836 if len(rows) == 0:
837 return None
838
839 r = rows[0]
840 return cExportItem(row = {'data': r, 'idx': idx, 'pk_field': 'pk_export_item'})
841
842
851
852
868
869
871 _log.debug('target dir: %s', base_dir)
872 dump_dir = self.dump_items_to_disk(base_dir = base_dir, items = items)
873 if dump_dir is None:
874 _log.error('cannot dump export area items')
875 return None
876 zip_file = gmTools.create_zip_archive_from_dir (
877 dump_dir,
878 comment = _('GNUmed Patient Media'),
879 overwrite = True,
880 passphrase = passphrase,
881 verbose = _cfg.get(option = 'debug')
882 )
883 if zip_file is None:
884 _log.error('cannot zip export area items dump')
885 return None
886 return zip_file
887
888
889 - def export(self, base_dir=None, items=None, passphrase=None):
890
891 if items is None:
892 items = self.items
893 if len(items) == 0:
894 return None
895
896 from Gnumed.business.gmPerson import cPatient
897 pat = cPatient(aPK_obj = self.__pk_identity)
898 target_base_dir = base_dir
899 if target_base_dir is None:
900 target_base_dir = gmTools.mk_sandbox_dir(prefix = '%s-' % pat.subdir_name)
901 gmTools.mkdir(target_base_dir)
902 _log.debug('patient media base dir: %s', target_base_dir)
903 if not gmTools.dir_is_empty(target_base_dir):
904 _log.error('patient media base dir is not empty')
905 return False
906
907 from Gnumed.business.gmPraxis import gmCurrentPraxisBranch
908 prax = gmCurrentPraxisBranch()
909
910 html_data = {}
911
912
913
914 sandbox_dir = gmTools.mk_sandbox_dir()
915 _log.debug('sandbox dir: %s', sandbox_dir)
916 doc_dir = os.path.join(sandbox_dir, DOCUMENTS_SUBDIR)
917 gmTools.mkdir(doc_dir)
918
919 mugshot = pat.document_folder.latest_mugshot
920 if mugshot is not None:
921 mugshot_fname = mugshot.save_to_file(directory = doc_dir, adjust_extension = True)
922 fname = os.path.split(mugshot_fname)[1]
923 html_data['mugshot_url'] = os.path.join(DOCUMENTS_SUBDIR, fname)
924 html_data['mugshot_alt'] =_('patient photograph from %s') % gmDateTime.pydt_strftime(mugshot['date_generated'], '%B %Y')
925 html_data['mugshot_title'] = gmDateTime.pydt_strftime(mugshot['date_generated'], '%B %Y')
926
927 pat.export_as_gdt(filename = os.path.join(sandbox_dir, 'patient.gdt'))
928 pat.export_as_xml_linuxmednews(filename = os.path.join(sandbox_dir, 'patient.xml'))
929 pat.export_as_vcard(filename = os.path.join(sandbox_dir, 'patient.vcf'))
930 pat.export_as_mecard(filename = os.path.join(sandbox_dir, u'patient.mcf'))
931
932 self._create_cd_inf(pat, sandbox_dir)
933
934 docs_list = []
935 for item in items:
936
937 if item.is_DICOM_directory:
938 _log.debug('exporting DICOMDIR DIRENTRY')
939
940 item_fname = item.save_to_file(directory = sandbox_dir)
941
942 continue
943 if item.is_valid_DIRENTRY:
944 _log.debug('exporting DIRENTRY')
945
946
947
948 if item.has_files_in_root:
949 tag, node, local_fs_path = item['filename'].split('::', 2)
950 subdir = local_fs_path.rstrip('/').split('/')[-1]
951 subdir = os.path.join(doc_dir, subdir)
952 gmTools.mkdir(subdir)
953 item_fname = item.save_to_file(directory = subdir)
954
955 else:
956 item_fname = item.save_to_file(directory = doc_dir)
957
958 fname = os.path.split(item_fname)[1]
959 docs_list.append([fname, gmTools.html_escape_string(item['description'])])
960 continue
961 if item.is_DIRENTRY:
962
963 continue
964
965 item_fname = item.save_to_file(directory = doc_dir)
966 fname = os.path.split(item_fname)[1]
967
968
969 docs_list.append([fname, gmTools.html_escape_string(item['description'])])
970
971 if 'DICOMDIR' in os.listdir(sandbox_dir):
972 has_dicomdir = True
973 html_data['browse_dicomdir'] = '<li><a href="./DICOMDIR">%s</a></li>' % _('show DICOMDIR file')
974 else:
975 has_dicomdir = False
976
977 if has_dicomdir:
978
979
980 dwv_sandbox_dir = self._clone_dwv()
981 if dwv_sandbox_dir is not None:
982 html_data['run_dicom_viewer'] = '<li><a href="./dwv/viewers/mobile-local/index.html">%s</a></li>' % _('run Radiology Images (DICOM) Viewer')
983
984 frontpage_fname = self._create_frontpage_html(pat, prax, sandbox_dir, html_data, docs_list)
985
986
987 start_fname = os.path.join(sandbox_dir, 'start.html')
988 try:
989 shutil.copy2(frontpage_fname, start_fname)
990 except Exception:
991 _log.exception('cannot copy %s to %s', frontpage_fname, start_fname)
992
993 start_fname = os.path.join(sandbox_dir, 'start.html')
994 try:
995 shutil.copy2(frontpage_fname, start_fname)
996 except Exception:
997 _log.exception('cannot copy %s to %s', frontpage_fname, start_fname)
998
999 index_fname = os.path.join(sandbox_dir, 'index.html')
1000 try:
1001 shutil.copy2(frontpage_fname, index_fname)
1002 except Exception:
1003 _log.exception('cannot copy %s to %s', frontpage_fname, index_fname)
1004
1005
1006 if passphrase is not None:
1007 encrypted = gmCrypto.encrypt_directory_content (
1008 directory = sandbox_dir,
1009 receiver_key_ids = None,
1010 passphrase = passphrase,
1011 comment = None,
1012 verbose = _cfg.get(option = 'debug'),
1013 remove_unencrypted = True
1014 )
1015 if not encrypted:
1016 _log.errror('cannot encrypt data in sandbox dir')
1017 return False
1018
1019
1020
1021
1022 if passphrase is None:
1023 self._create_autorun_inf(pat, sandbox_dir)
1024 self._create_readme(pat, sandbox_dir)
1025 else:
1026 self._create_autorun_inf(None, sandbox_dir)
1027 self._create_readme(None, sandbox_dir)
1028
1029 shutil.move(prax.vcf, os.path.join(sandbox_dir, 'praxis.vcf'))
1030 prax.export_as_mecard(filename = os.path.join(sandbox_dir, u'praxis.mcf'))
1031
1032 if has_dicomdir:
1033 self._clone_dwv(target_dir = sandbox_dir)
1034
1035 if passphrase is not None:
1036 index_fname = self._create_index_html(prax, sandbox_dir, html_data)
1037
1038
1039 target_dir = gmTools.copy_tree_content(sandbox_dir, target_base_dir)
1040 if target_dir is None:
1041 _log.error('cannot fill target base dir')
1042 return False
1043
1044 return target_dir
1045
1046
1047 - def export_as_zip(self, base_dir=None, items=None, passphrase=None):
1048 _log.debug('target dir: %s', base_dir)
1049 export_dir = self.export(base_dir = base_dir, items = items)
1050 if export_dir is None:
1051 _log.debug('cannot export items')
1052 return None
1053 if passphrase is None:
1054 zip_file = gmCrypto.create_zip_archive_from_dir (
1055 export_dir,
1056 comment = _('GNUmed Patient Media'),
1057 overwrite = True,
1058 verbose = _cfg.get(option = 'debug')
1059 )
1060 else:
1061 zip_file = gmCrypto.create_encrypted_zip_archive_from_dir (
1062 export_dir,
1063 comment = _('GNUmed Patient Media'),
1064 overwrite = True,
1065 passphrase = passphrase,
1066 verbose = _cfg.get(option = 'debug')
1067 )
1068 if zip_file is None:
1069 _log.debug('cannot create zip archive')
1070 return None
1071 return zip_file
1072
1073
1075
1076 _HTML_data = {
1077 'html_title_header': _('Patient data'),
1078 'title': _('Patient data excerpt'),
1079 'docs_title': _('Documents'),
1080 'browse_root': _('browse storage medium'),
1081 'browse_docs': _('browse documents area'),
1082 'doc_subdir': DOCUMENTS_SUBDIR,
1083 'date' : gmTools.html_escape_string(gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), format = '%Y %B %d'))
1084 }
1085 frontpage_fname_enc = 'frontpage.html.asc'
1086 if os.path.isfile(os.path.join(directory, frontpage_fname_enc)):
1087 _HTML_data['frontpage_fname'] = frontpage_fname_enc
1088 frontpage_fname_enc = 'frontpage.html.7z'
1089 if os.path.isfile(os.path.join(directory, frontpage_fname_enc)):
1090 _HTML_data['frontpage_fname'] = frontpage_fname_enc
1091
1092 lines = []
1093 adr = praxis.branch.org_unit.address
1094 if adr is not None:
1095 lines.extend(adr.format())
1096 for comm in praxis.branch.org_unit.comm_channels:
1097 if comm['is_confidential'] is True:
1098 continue
1099 lines.append('%s: %s' % (
1100 comm['l10n_comm_type'],
1101 comm['url']
1102 ))
1103 adr = ''
1104 if len(lines) > 0:
1105 adr = gmTools.html_escape_string('\n'.join(lines), replace_eol = True, keep_visual_eol = True)
1106 _HTML_data['branch'] = gmTools.html_escape_string(praxis['branch'])
1107 _HTML_data['praxis'] = gmTools.html_escape_string(praxis['praxis'])
1108 _HTML_data['gm_ver'] = gmTools.html_escape_string(gmTools.coalesce(_cfg.get(option = 'client_version'), 'git HEAD'))
1109 _HTML_data['adr'] = adr
1110
1111 index_fname = os.path.join(directory, 'index.html')
1112 index_file = io.open(index_fname, mode = 'wt', encoding = 'utf8')
1113 index_file.write(_INDEX_HTML_CONTENT % _HTML_data)
1114 index_file.close()
1115 return index_fname
1116
1117
1118 - def _create_frontpage_html(self, patient, praxis, directory, data, docs_list):
1119
1120 _HTML_LIST_ITEM = ' <li><a href="%s">%s</a></li>'
1121
1122 _HTML_data = {
1123 'html_title_header': _('Patient data for'),
1124 'html_title_patient': gmTools.html_escape_string(patient.get_description_gender(with_nickname = False) + ', ' + _('born') + ' ' + patient.get_formatted_dob('%Y %B %d')),
1125 'title': _('Patient data excerpt'),
1126 'pat_name': gmTools.html_escape_string(patient.get_description_gender(with_nickname = False)),
1127 'pat_dob': gmTools.html_escape_string(_('born') + ' ' + patient.get_formatted_dob('%Y %B %d')),
1128 'mugshot_url': 'documents/no-such-file.png',
1129 'mugshot_alt': _('no patient photograph available'),
1130 'mugshot_title': '',
1131 'docs_title': _('Documents'),
1132 'browse_root': _('browse storage medium'),
1133 'browse_docs': _('browse documents area'),
1134 'doc_subdir': DOCUMENTS_SUBDIR,
1135 'browse_dicomdir': '',
1136 'run_dicom_viewer': '',
1137 'date' : gmTools.html_escape_string(gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), format = '%Y %B %d'))
1138 }
1139 for key in data:
1140 _HTML_data[key] = data[key]
1141
1142
1143 _HTML_docs_list = []
1144 for doc in docs_list:
1145 subdir = os.path.join(directory, DOCUMENTS_SUBDIR, doc[0])
1146 if os.path.isdir(subdir):
1147 _HTML_docs_list.append(_HTML_LIST_ITEM % (os.path.join(DOCUMENTS_SUBDIR, doc[0]), _('DIRECTORY: %s/%s/') % (DOCUMENTS_SUBDIR, doc[0])))
1148 _HTML_docs_list.append(' <ul>')
1149 for fname in os.listdir(subdir):
1150 tmp = os.path.join(subdir, fname)
1151 if os.path.isdir(tmp):
1152 _HTML_docs_list.append(' <li><a href="%s">%s</a></li>' % (os.path.join(DOCUMENTS_SUBDIR, doc[0], fname), _('DIRECTORY: %s/%s/%s/') % (DOCUMENTS_SUBDIR, doc[0], fname)))
1153 else:
1154 _HTML_docs_list.append(' <li><a href="%s">%s</a></li>' % (os.path.join(DOCUMENTS_SUBDIR, doc[0], fname), fname))
1155 _HTML_docs_list.append(' </ul>')
1156 else:
1157 _HTML_docs_list.append(_HTML_LIST_ITEM % (doc[0], doc[1]))
1158 _HTML_data['docs_list'] = u'\n '.join(_HTML_docs_list)
1159
1160
1161 lines = []
1162 adr = praxis.branch.org_unit.address
1163 if adr is not None:
1164 lines.extend(adr.format())
1165 for comm in praxis.branch.org_unit.comm_channels:
1166 if comm['is_confidential'] is True:
1167 continue
1168 lines.append('%s: %s' % (
1169 comm['l10n_comm_type'],
1170 comm['url']
1171 ))
1172 adr = ''
1173 if len(lines) > 0:
1174 adr = gmTools.html_escape_string('\n'.join(lines), replace_eol = True, keep_visual_eol = True)
1175 _HTML_data['branch'] = gmTools.html_escape_string(praxis['branch'])
1176 _HTML_data['praxis'] = gmTools.html_escape_string(praxis['praxis'])
1177 _HTML_data['gm_ver'] = gmTools.html_escape_string(gmTools.coalesce(_cfg.get(option = 'client_version'), 'git HEAD'))
1178 _HTML_data['adr'] = adr
1179
1180 frontpage_fname = os.path.join(directory, 'frontpage.html')
1181 frontpage_file = io.open(frontpage_fname, mode = 'wt', encoding = 'utf8')
1182 frontpage_file.write(_FRONTPAGE_HTML_CONTENT % _HTML_data)
1183 frontpage_file.close()
1184 return frontpage_fname
1185
1186
1188 _log.debug('cloning dwv')
1189
1190 dwv_src_dir = os.path.join(gmTools.gmPaths().local_base_dir, 'resources', 'dwv4export')
1191 if not os.path.isdir(dwv_src_dir):
1192 _log.debug('[%s] not found', dwv_src_dir)
1193 dwv_src_dir = os.path.join(gmTools.gmPaths().system_app_data_dir, 'resources', 'dwv4export')
1194 if not os.path.isdir(dwv_src_dir):
1195 _log.debug('[%s] not found', dwv_src_dir)
1196 return None
1197
1198 if target_dir is None:
1199 target_dir = gmTools.mk_sandbox_dir()
1200 dwv_target_dir = os.path.join(target_dir, 'dwv')
1201 gmTools.rmdir(dwv_target_dir)
1202 try:
1203 shutil.copytree(dwv_src_dir, dwv_target_dir)
1204 except (shutil.Error, OSError):
1205 _log.exception('cannot include DWV, skipping')
1206 return None
1207
1208 return dwv_target_dir
1209
1210
1212 _README_CONTENT = (
1213 'This is a patient data excerpt created by the GNUmed Electronic Medical Record.\n'
1214 '\n'
1215 'Patient: %s\n'
1216 '\n'
1217 'Please display <frontpage.html> to browse patient data.\n'
1218 '\n'
1219 'Individual documents are stored in the subdirectory\n'
1220 '\n'
1221 ' documents/\n'
1222 '\n'
1223 '\n'
1224 'Data may need to be decrypted with either GNU Privacy\n'
1225 'Guard or 7zip/WinZip.\n'
1226 '\n'
1227 '.asc:\n'
1228 ' https://gnupg.org\n'
1229 '\n'
1230 '.7z:\n'
1231 ' https://www.7-zip.org\n'
1232 ' https://www.winzip.com\n'
1233 '\n'
1234 'To obtain any needed keys you will have to get in touch with\n'
1235 'the creator or the owner of this patient media excerpt.\n'
1236 )
1237 readme_fname = os.path.join(directory, 'README')
1238 readme_file = io.open(readme_fname, mode = 'wt', encoding = 'utf8')
1239 if patient is None:
1240 pat_str = _('<protected>')
1241 else:
1242 pat_str = patient.get_description_gender(with_nickname = False) + ', ' + _('born') + ' ' + patient.get_formatted_dob('%Y %B %d')
1243 readme_file.write(_README_CONTENT % pat_str)
1244 readme_file.close()
1245 return readme_fname
1246
1247
1249 _CD_INF_CONTENT = (
1250 '[Patient Info]\r\n'
1251 'PatientName=%s, %s\r\n'
1252 'Gender=%s\r\n'
1253 'BirthDate=%s\r\n'
1254 'CreationDate=%s\r\n'
1255 'PID=%s\r\n'
1256 'EMR=GNUmed\r\n'
1257 'Version=%s\r\n'
1258 '#StudyDate=\r\n'
1259 '#VNRInfo=<body part>\r\n'
1260 '\r\n'
1261 '# name format: lastnames, firstnames\r\n'
1262 '# date format: YYYY-MM-DD (ISO 8601)\r\n'
1263 '# gender format: %s\r\n'
1264 )
1265 fname = os.path.join(directory, 'CD.INF')
1266 cd_inf = io.open(fname, mode = 'wt', encoding = 'utf8')
1267 cd_inf.write(_CD_INF_CONTENT % (
1268 patient['lastnames'],
1269 patient['firstnames'],
1270 gmTools.coalesce(patient['gender'], '?'),
1271 patient.get_formatted_dob('%Y-%m-%d'),
1272 gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), format = '%Y-%m-%d'),
1273 patient.ID,
1274 _cfg.get(option = 'client_version'),
1275 ' / '.join([ '%s = %s (%s)' % (g['tag'], g['label'], g['l10n_label']) for g in patient.gender_list ])
1276 ))
1277 cd_inf.close()
1278 return fname
1279
1280
1282 _AUTORUN_INF_CONTENT = (
1283 '[AutoRun.Amd64]\r\n'
1284 'label=%(label)s\r\n'
1285 'shellexecute=index.html\r\n'
1286 'action=%(action)s\r\n'
1287 '%(icon)s\r\n'
1288 'UseAutoPlay=1\r\n'
1289 '\r\n'
1290 '[AutoRun]\r\n'
1291 'label=%(label)s\r\n'
1292 'shellexecute=index.html\r\n'
1293 'action=%(action)s\r\n'
1294 '%(icon)s\r\n'
1295 'UseAutoPlay=1\r\n'
1296 '\r\n'
1297 '[Content]\r\n'
1298 'PictureFiles=yes\r\n'
1299 'VideoFiles=yes\r\n'
1300 'MusicFiles=no\r\n'
1301 '\r\n'
1302 '[IgnoreContentPaths]\r\n'
1303 '\documents\r\n'
1304 '\r\n'
1305 '[unused]\r\n'
1306 'open=requires explicit executable\r\n'
1307 )
1308 autorun_dict = {
1309 'label': self._compute_autorun_inf_label(patient),
1310 'action': _('Browse patient data'),
1311 'icon': ''
1312 }
1313 media_icon_kwd = '$$gnumed_patient_media_export_icon'
1314 media_icon_kwd_exp = gmKeywordExpansion.get_expansion (
1315 keyword = media_icon_kwd,
1316 textual_only = False,
1317 binary_only = True
1318 )
1319 icon_tmp_fname = media_icon_kwd_exp.save_to_file (
1320 target_mime = 'image/x-icon',
1321 target_extension = '.ico',
1322 ignore_conversion_problems = True
1323 )
1324 if icon_tmp_fname is None:
1325 _log.error('cannot retrieve <%s>', media_icon_kwd)
1326 else:
1327 media_icon_fname = os.path.join(directory, 'gnumed.ico')
1328 try:
1329 shutil.copy2(icon_tmp_fname, media_icon_fname)
1330 autorun_dict['icon'] = 'icon=gnumed.ico'
1331 except Exception:
1332 _log.exception('cannot move %s to %s', icon_tmp_fname, media_icon_fname)
1333 autorun_fname = os.path.join(directory, 'AUTORUN.INF')
1334 autorun_file = io.open(autorun_fname, mode = 'wt', encoding = 'cp1252', errors = 'replace')
1335 autorun_file.write(_AUTORUN_INF_CONTENT % autorun_dict)
1336 autorun_file.close()
1337 return autorun_fname
1338
1339
1341 if patient is None:
1342
1343
1344
1345
1346 return _('GNUmed patient data excerpt')[:32]
1347
1348 LABEL_MAX_LEN = 32
1349 dob = patient.get_formatted_dob(format = ' %Y%m%d', none_string = '', honor_estimation = False)
1350 if dob == '':
1351 gender_template = ' (%s)'
1352 else:
1353 gender_template = ' %s'
1354 gender = gmTools.coalesce(patient['gender'], '', gender_template)
1355 name_max_len = LABEL_MAX_LEN - len(gender) - len(dob)
1356 name = patient.active_name
1357 last = name['lastnames'].strip()
1358 first = name['firstnames'].strip()
1359 len_last = len(last)
1360 len_first = len(first)
1361 while (len_last + len_first + 1) > name_max_len:
1362 if len_first > 6:
1363 len_first -= 1
1364 if first[len_first - 1] == ' ':
1365 len_first -= 1
1366 continue
1367 len_last -= 1
1368 if last[len_last - 1] == ' ':
1369 len_last -= 1
1370 last = last[:len_last].strip().upper()
1371 first = first[:len_first].strip()
1372
1373 label = (('%s %s%s%s' % (last, first, dob, gender)).strip())[:32]
1374 return label
1375
1376
1378 if passphrase is None:
1379 return filename
1380 enc_filename = gmCrypto.encrypt_file (
1381 filename = filename,
1382 passphrase = passphrase,
1383 verbose = _cfg.get(option = 'debug')
1384 )
1385 if enc_filename is None:
1386 _log.error('cannot encrypt')
1387 return None
1388 if not gmTools.remove_file(filename, log_error = True, force = True):
1389 _log.error('cannot remove unencrypted file')
1390 return None
1391 return enc_filename
1392
1393
1394
1395
1396 - def get_items(self, designation=None, order_by='designation, description'):
1397 return get_export_items(order_by = order_by, pk_identity = self.__pk_identity, designation = designation)
1398
1399 items = property(get_items, lambda x:x)
1400
1401
1403 return get_print_jobs(order_by = order_by, pk_identity = self.__pk_identity)
1404
1405 printouts = property(get_printouts, lambda x:x)
1406
1407
1408 if __name__ == '__main__':
1409
1410 if len(sys.argv) < 2:
1411 sys.exit()
1412
1413 if sys.argv[1] != 'test':
1414 sys.exit()
1415
1416 from Gnumed.pycommon import gmI18N
1417 gmI18N.activate_locale()
1418 gmI18N.install_domain()
1419
1420 from Gnumed.business import gmPraxis
1421
1422
1435
1436
1450
1451
1453
1454 from Gnumed.business.gmPerson import cPatient
1455 from Gnumed.business.gmPersonSearch import ask_for_patient
1456
1457
1458 pat_min = 1
1459 pat_max = 100
1460 try:
1461 pat_min = int(sys.argv[2])
1462 pat_max = int(sys.argv[3])
1463 except:
1464 pass
1465 cPatient(aPK_obj = pat_min)
1466 f = io.open('x-auto_inf_labels.txt', mode = 'w', encoding = 'utf8')
1467 f.write('--------------------------------\n')
1468 f.write('12345678901234567890123456789012\n')
1469 f.write('--------------------------------\n')
1470 for pat_id in range(pat_min, pat_max):
1471 try:
1472 exp_area = cExportArea(pat_id)
1473 pat = cPatient(aPK_obj = pat_id)
1474 except:
1475 continue
1476 f.write(exp_area._compute_autorun_inf_label(pat) + '\n')
1477 f.close()
1478 return
1479
1480
1481
1482 test_export_area()
1483
1484
1485 sys.exit(0)
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501