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
430 tag, node, local_fs_path = self._payload[self._idx['filename']].split('::', 2)
431 found_DICOMDIR = False
432 for fs_entry in os.listdir(local_fs_path):
433
434 if os.path.isdir(os.path.join(local_fs_path, fs_entry)):
435
436 continue
437
438 if fs_entry != 'DICOMDIR':
439
440 return False
441
442
443 found_DICOMDIR = True
444 return found_DICOMDIR
445
446 is_DICOM_directory = property(_is_DICOM_directory)
447
448
450 """True if there are files in the root directory."""
451 tag, node, local_fs_path = self._payload[self._idx['filename']].split('::', 2)
452 for fs_entry in os.listdir(local_fs_path):
453 if os.path.isfile(fs_entry):
454 _log.debug('has files in top level: %s', local_fs_path)
455 return True
456 return False
457
458 has_files_in_root = property(_has_files_in_root)
459
460
461 -def get_export_items(order_by=None, pk_identity=None, designation=None, return_pks=False):
462
463 args = {
464 'pat': pk_identity,
465 'desig': gmTools.coalesce(designation, PRINT_JOB_DESIGNATION)
466 }
467 where_parts = []
468 if pk_identity is not None:
469 where_parts.append('pk_identity = %(pat)s')
470
471
472 if designation is None:
473 where_parts.append("designation IS DISTINCT FROM %(desig)s")
474 else:
475 where_parts.append('designation = %(desig)s')
476
477 if order_by is None:
478 order_by = ''
479 else:
480 order_by = ' ORDER BY %s' % order_by
481
482 cmd = (_SQL_get_export_items % ' AND '.join(where_parts)) + order_by
483 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
484 if return_pks:
485 return [ r['pk_export_item'] for r in rows ]
486 return [ cExportItem(row = {'data': r, 'idx': idx, 'pk_field': 'pk_export_item'}) for r in rows ]
487
488
491
492
493 -def create_export_item(description=None, pk_identity=None, pk_doc_obj=None, filename=None):
494
495 args = {
496 'desc': description,
497 'pk_obj': pk_doc_obj,
498 'pk_pat': pk_identity,
499 'fname': filename
500 }
501 cmd = """
502 INSERT INTO clin.export_item (
503 description,
504 fk_doc_obj,
505 fk_identity,
506 data,
507 filename
508 ) VALUES (
509 gm.nullify_empty_string(%(desc)s),
510 %(pk_obj)s,
511 %(pk_pat)s,
512 (CASE
513 WHEN %(pk_obj)s IS NULL THEN %(fname)s::bytea
514 ELSE NULL::bytea
515 END),
516 (CASE
517 WHEN %(pk_obj)s IS NULL THEN gm.nullify_empty_string(%(fname)s)
518 ELSE NULL
519 END)
520 )
521 RETURNING pk
522 """
523 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
524
525 return cExportItem(aPK_obj = rows[0]['pk'])
526
527
529 args = {'pk': pk_export_item}
530 cmd = "DELETE FROM clin.export_item WHERE pk = %(pk)s"
531 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
532 return True
533
534
535 _FRONTPAGE_HTML_CONTENT = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
536 "http://www.w3.org/TR/html4/loose.dtd">
537 <html>
538 <head>
539 <meta http-equiv="content-type" content="text/html; charset=UTF-8">
540 <link rel="icon" type="image/x-icon" href="gnumed.ico">
541 <title>%(html_title_header)s %(html_title_patient)s</title>
542 </head>
543 <body>
544
545 <h1>%(title)s</h1>
546
547 <p>
548 (%(date)s)<br>
549 </p>
550
551 <h2><a href="patient.vcf">Patient</a></h2>
552
553 <p>
554 %(pat_name)s<br>
555 %(pat_dob)s
556 </p>
557
558 <p><img src="%(mugshot_url)s" alt="%(mugshot_alt)s" title="%(mugshot_title)s" width="200" border="2"></p>
559
560 <h2>%(docs_title)s</h2>
561
562 <ul>
563 <li><a href="./">%(browse_root)s</a></li>
564 <li><a href="%(doc_subdir)s/">%(browse_docs)s</a></li>
565 %(browse_dicomdir)s
566 %(run_dicom_viewer)s
567 </ul>
568
569 <ul>
570 %(docs_list)s
571 </ul>
572
573 <h2><a href="praxis.vcf">Praxis</a></h2>
574
575 <p>
576 %(branch)s @ %(praxis)s
577 %(adr)s
578 </p>
579
580 <p>(<a href="http://www.gnumed.de">GNUmed</a> version %(gm_ver)s)</p>
581
582 </body>
583 </html>
584 """
585
586 _INDEX_HTML_CONTENT = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
587 "http://www.w3.org/TR/html4/loose.dtd">
588 <html>
589 <head>
590 <meta http-equiv="content-type" content="text/html; charset=UTF-8">
591 <link rel="icon" type="image/x-icon" href="gnumed.ico">
592 <title>%(html_title_header)s</title>
593 </head>
594 <body>
595
596 <h1>%(title)s</h1>
597
598 <p>
599 (%(date)s)<br>
600 </p>
601
602 This is an encrypted patient data excerpt created by the GNUmed Electronic Medical Record.
603
604 <p>
605 For decryption you will need to
606
607 <ul>
608 <li>install decryption software and</li>
609 <li>obtain relevant passwords from the creator or holder of this media</li>
610 </ul>
611
612 <h2>Decryption software</h2>
613
614 For files ending in
615
616 <ul>
617 <li>.asc: install <a href="https://gnupg.org">GNU Privacy Guard</a></li>
618 <li>.7z: install <a href="https://www.7-zip.org">7-zip</a> or <a href="https://www.winzip.com">WinZip</a></li>
619 </ul>
620
621
622 <h2>%(docs_title)s</h2>
623
624 <ul>
625 <li><a href="./frontpage.html">front page (after decryption)</a></li>
626 <li><a href="./%(frontpage_fname)s">front page (if decryption from browser is supported)</a></li>
627
628 <li><a href="./">%(browse_root)s</a></li>
629 <li><a href="%(doc_subdir)s/">%(browse_docs)s</a></li>
630 </ul>
631
632
633 <h2><a href="praxis.vcf">Praxis</a></h2>
634
635 <p>
636 %(branch)s @ %(praxis)s
637 %(adr)s
638 </p>
639
640 <p>(<a href="http://www.gnumed.de">GNUmed</a> version %(gm_ver)s)</p>
641
642 </body>
643 </html>
644 """
645
646
648
650 self.__pk_identity = pk_identity
651
652
671
672
679
680
681 - def add_path(self, path, comment=None):
682 """Add a DIR entry to the export area.
683
684 This sort of entry points to a certain directory on a
685 certain machine. The content of the the directory
686 will be included in exports, the directory *itself*
687 will not. For *that*, use a disposable top-level
688 directory into which you put the directory to include
689 as a subdirectory.
690 """
691 assert (os.path.isdir(path)), '<path> must exist: %s' % path
692
693 path_item_data = 'DIR::%s::%s/' % (platform.node(), path.rstrip('/'))
694 _log.debug('attempting to add path item [%s]', path_item_data)
695 item = self.path_item_exists(path_item_data)
696 if item is not None:
697 _log.debug('[%s] already in export area', path)
698 return item
699
700 if comment is None:
701 comment = _('path [%s/] on computer "%s"') % (
702 path.rstrip('/'),
703 platform.node()
704 )
705 else:
706 comment += _(' (on "%s")') % platform.node()
707
708 item = create_export_item (
709 description = comment,
710 pk_identity = self.__pk_identity,
711 filename = path_item_data
712 )
713 try:
714 README = open(os.path.join(path, DIRENTRY_README_NAME), mode = 'wt', encoding = 'utf8')
715 README.write('GNUmed DIRENTRY information\n')
716 README.write('created: %s\n' % gmDateTime.pydt_now_here())
717 README.write('machine: %s\n' % platform.node())
718 README.write('path: %s\n' % path)
719 README.close()
720 except OSError:
721 _log.exception('READONLY DIRENTRY [%s]', path)
722
723 return item
724
725
727
728 assert (path_item_data.startswith('DIR::')), 'invalid <path_item_data> [%s]' % path_item_data
729
730 where_parts = [
731 'pk_identity = %(pat)s',
732 'filename = %(fname)s',
733 'pk_doc_obj IS NULL'
734 ]
735 args = {
736 'pat': self.__pk_identity,
737 'fname': path_item_data
738 }
739 SQL = _SQL_get_export_items % ' AND '.join(where_parts)
740 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': SQL, 'args': args}], get_col_idx = True)
741 if len(rows) == 0:
742 return None
743
744 r = rows[0]
745 return cExportItem(row = {'data': r, 'idx': idx, 'pk_field': 'pk_export_item'})
746
747
748 - def add_file(self, filename=None, hint=None):
749 try:
750 open(filename).close()
751 except Exception:
752 _log.exception('cannot open file <%s>', filename)
753 return None
754
755 file_md5 = gmTools.file2md5(filename = filename, return_hex = True)
756 existing_item = self.md5_exists(md5 = file_md5, include_document_parts = False)
757 if existing_item is not None:
758 _log.debug('md5 match (%s): %s already in export area', file_md5, filename)
759 return existing_item
760
761 path, basename = os.path.split(filename)
762 item = create_export_item (
763 description = '%s: %s (%s/)' % (
764 gmTools.coalesce(hint, _('file'), '%s'),
765 basename,
766 path
767 ),
768 pk_identity = self.__pk_identity,
769 filename = filename
770 )
771
772 if item.update_data_from_file(filename = filename):
773 return item
774
775
776 delete_export_item(pk_export_item = item['pk_export_item'])
777 return None
778
779
780 - def add_files(self, filenames=None, hint=None):
781 all_ok = True
782 for fname in filenames:
783 all_ok = all_ok and (self.add_file(filename = fname, hint = hint) is not None)
784
785 return all_ok
786
787
789 for doc in documents:
790 doc_tag = _('%s (%s)%s') % (
791 doc['l10n_type'],
792 gmDateTime.pydt_strftime(doc['clin_when'], '%Y %b %d'),
793 gmTools.coalesce(doc['comment'], '', ' "%s"')
794 )
795 for obj in doc.parts:
796 if self.document_part_item_exists(pk_part = obj['pk_obj']):
797 continue
798 f_ext = ''
799 if obj['filename'] is not None:
800 f_ext = os.path.splitext(obj['filename'])[1].strip('.').strip()
801 if f_ext != '':
802 f_ext = ' .' + f_ext.upper()
803 obj_tag = _('part %s (%s%s)%s') % (
804 obj['seq_idx'],
805 gmTools.size2str(obj['size']),
806 f_ext,
807 gmTools.coalesce(obj['obj_comment'], '', ' "%s"')
808 )
809 create_export_item (
810 description = '%s - %s' % (doc_tag, obj_tag),
811 pk_doc_obj = obj['pk_obj']
812 )
813
814
816 cmd = "SELECT EXISTS (SELECT 1 FROM clin.export_item WHERE fk_doc_obj = %(pk_obj)s)"
817 args = {'pk_obj': pk_part}
818 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
819 return rows[0][0]
820
821
822 - def md5_exists(self, md5=None, include_document_parts=False):
823 where_parts = [
824 'pk_identity = %(pat)s',
825 'md5_sum = %(md5)s'
826 ]
827 args = {
828 'pat': self.__pk_identity,
829 'md5': md5
830 }
831
832 if not include_document_parts:
833 where_parts.append('pk_doc_obj IS NULL')
834
835 cmd = _SQL_get_export_items % ' AND '.join(where_parts)
836 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
837
838 if len(rows) == 0:
839 return None
840
841 r = rows[0]
842 return cExportItem(row = {'data': r, 'idx': idx, 'pk_field': 'pk_export_item'})
843
844
853
854
870
871
873 _log.debug('target dir: %s', base_dir)
874 dump_dir = self.dump_items_to_disk(base_dir = base_dir, items = items)
875 if dump_dir is None:
876 _log.error('cannot dump export area items')
877 return None
878 zip_file = gmTools.create_zip_archive_from_dir (
879 dump_dir,
880 comment = _('GNUmed Patient Media'),
881 overwrite = True,
882 passphrase = passphrase,
883 verbose = _cfg.get(option = 'debug')
884 )
885 if zip_file is None:
886 _log.error('cannot zip export area items dump')
887 return None
888 return zip_file
889
890
891 - def export(self, base_dir=None, items=None, passphrase=None):
892
893 if items is None:
894 items = self.items
895 if len(items) == 0:
896 return None
897
898 from Gnumed.business.gmPerson import cPatient
899 pat = cPatient(aPK_obj = self.__pk_identity)
900 target_base_dir = base_dir
901 if target_base_dir is None:
902 target_base_dir = gmTools.mk_sandbox_dir(prefix = '%s-' % pat.subdir_name)
903 gmTools.mkdir(target_base_dir)
904 _log.debug('patient media base dir: %s', target_base_dir)
905 if not gmTools.dir_is_empty(target_base_dir):
906 _log.error('patient media base dir is not empty')
907 return False
908
909 from Gnumed.business.gmPraxis import gmCurrentPraxisBranch
910 prax = gmCurrentPraxisBranch()
911
912 html_data = {}
913
914
915
916 sandbox_dir = gmTools.mk_sandbox_dir()
917 _log.debug('sandbox dir: %s', sandbox_dir)
918 doc_dir = os.path.join(sandbox_dir, DOCUMENTS_SUBDIR)
919 gmTools.mkdir(doc_dir)
920
921 mugshot = pat.document_folder.latest_mugshot
922 if mugshot is not None:
923 mugshot_fname = mugshot.save_to_file(directory = doc_dir, adjust_extension = True)
924 fname = os.path.split(mugshot_fname)[1]
925 html_data['mugshot_url'] = os.path.join(DOCUMENTS_SUBDIR, fname)
926 html_data['mugshot_alt'] =_('patient photograph from %s') % gmDateTime.pydt_strftime(mugshot['date_generated'], '%B %Y')
927 html_data['mugshot_title'] = gmDateTime.pydt_strftime(mugshot['date_generated'], '%B %Y')
928
929 pat.export_as_gdt(filename = os.path.join(sandbox_dir, 'patient.gdt'))
930 pat.export_as_xml_linuxmednews(filename = os.path.join(sandbox_dir, 'patient.xml'))
931 pat.export_as_vcard(filename = os.path.join(sandbox_dir, 'patient.vcf'))
932 pat.export_as_mecard(filename = os.path.join(sandbox_dir, u'patient.mcf'))
933
934 self._create_cd_inf(pat, sandbox_dir)
935
936 docs_list = []
937 for item in items:
938
939 if item.is_DICOM_directory:
940 _log.debug('exporting DICOMDIR DIRENTRY')
941
942 item_fname = item.save_to_file(directory = sandbox_dir)
943
944 continue
945 if item.is_valid_DIRENTRY:
946 _log.debug('exporting DIRENTRY')
947
948
949
950 if item.has_files_in_root:
951 tag, node, local_fs_path = item['filename'].split('::', 2)
952 subdir = local_fs_path.rstrip('/').split('/')[-1]
953 subdir = os.path.join(doc_dir, subdir)
954 gmTools.mkdir(subdir)
955 item_fname = item.save_to_file(directory = subdir)
956
957 else:
958 item_fname = item.save_to_file(directory = doc_dir)
959
960 fname = os.path.split(item_fname)[1]
961 docs_list.append([fname, gmTools.html_escape_string(item['description'])])
962 continue
963 if item.is_DIRENTRY:
964
965 continue
966
967 item_fname = item.save_to_file(directory = doc_dir)
968 fname = os.path.split(item_fname)[1]
969
970
971 docs_list.append([fname, gmTools.html_escape_string(item['description'])])
972
973 if 'DICOMDIR' in os.listdir(sandbox_dir):
974 has_dicomdir = True
975 html_data['browse_dicomdir'] = '<li><a href="./DICOMDIR">%s</a></li>' % _('show DICOMDIR file')
976 else:
977 has_dicomdir = False
978
979 if has_dicomdir:
980
981
982 dwv_sandbox_dir = self._clone_dwv()
983 if dwv_sandbox_dir is not None:
984 html_data['run_dicom_viewer'] = '<li><a href="./dwv/viewers/mobile-local/index.html">%s</a></li>' % _('run Radiology Images (DICOM) Viewer')
985
986 frontpage_fname = self._create_frontpage_html(pat, prax, sandbox_dir, html_data, docs_list)
987
988
989 start_fname = os.path.join(sandbox_dir, 'start.html')
990 try:
991 shutil.copy2(frontpage_fname, start_fname)
992 except Exception:
993 _log.exception('cannot copy %s to %s', frontpage_fname, start_fname)
994
995
996 index_fname = os.path.join(sandbox_dir, 'index.html')
997 try:
998 shutil.copy2(frontpage_fname, index_fname)
999 except Exception:
1000 _log.exception('cannot copy %s to %s', frontpage_fname, index_fname)
1001
1002
1003 if passphrase is not None:
1004 encrypted = gmCrypto.encrypt_directory_content (
1005 directory = sandbox_dir,
1006 receiver_key_ids = None,
1007 passphrase = passphrase,
1008 comment = None,
1009 verbose = _cfg.get(option = 'debug'),
1010 remove_unencrypted = True
1011 )
1012 if not encrypted:
1013 _log.errror('cannot encrypt data in sandbox dir')
1014 return False
1015
1016
1017
1018
1019 if passphrase is None:
1020 self._create_autorun_inf(pat, sandbox_dir)
1021 self._create_readme(pat, sandbox_dir)
1022 else:
1023 self._create_autorun_inf(None, sandbox_dir)
1024 self._create_readme(None, sandbox_dir)
1025
1026 shutil.move(prax.vcf, os.path.join(sandbox_dir, 'praxis.vcf'))
1027 prax.export_as_mecard(filename = os.path.join(sandbox_dir, u'praxis.mcf'))
1028
1029 if has_dicomdir:
1030 self._clone_dwv(target_dir = sandbox_dir)
1031
1032 if passphrase is not None:
1033 index_fname = self._create_index_html(prax, sandbox_dir, html_data)
1034
1035
1036 target_dir = gmTools.copy_tree_content(sandbox_dir, target_base_dir)
1037 if target_dir is None:
1038 _log.error('cannot fill target base dir')
1039 return False
1040
1041 return target_dir
1042
1043
1044 - def export_as_zip(self, base_dir=None, items=None, passphrase=None):
1045 _log.debug('target dir: %s', base_dir)
1046 export_dir = self.export(base_dir = base_dir, items = items)
1047 if export_dir is None:
1048 _log.debug('cannot export items')
1049 return None
1050 if passphrase is None:
1051 zip_file = gmCrypto.create_zip_archive_from_dir (
1052 export_dir,
1053 comment = _('GNUmed Patient Media'),
1054 overwrite = True,
1055 verbose = _cfg.get(option = 'debug')
1056 )
1057 else:
1058 zip_file = gmCrypto.create_encrypted_zip_archive_from_dir (
1059 export_dir,
1060 comment = _('GNUmed Patient Media'),
1061 overwrite = True,
1062 passphrase = passphrase,
1063 verbose = _cfg.get(option = 'debug')
1064 )
1065 if zip_file is None:
1066 _log.debug('cannot create zip archive')
1067 return None
1068 return zip_file
1069
1070
1072
1073 _HTML_data = {
1074 'html_title_header': _('Patient data'),
1075 'title': _('Patient data excerpt'),
1076 'docs_title': _('Documents'),
1077 'browse_root': _('browse storage medium'),
1078 'browse_docs': _('browse documents area'),
1079 'doc_subdir': DOCUMENTS_SUBDIR,
1080 'date' : gmTools.html_escape_string(gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), format = '%Y %B %d'))
1081 }
1082 frontpage_fname_enc = 'frontpage.html.asc'
1083 if os.path.isfile(os.path.join(directory, frontpage_fname_enc)):
1084 _HTML_data['frontpage_fname'] = frontpage_fname_enc
1085 frontpage_fname_enc = 'frontpage.html.7z'
1086 if os.path.isfile(os.path.join(directory, frontpage_fname_enc)):
1087 _HTML_data['frontpage_fname'] = frontpage_fname_enc
1088
1089 lines = []
1090 adr = praxis.branch.org_unit.address
1091 if adr is not None:
1092 lines.extend(adr.format())
1093 for comm in praxis.branch.org_unit.comm_channels:
1094 if comm['is_confidential'] is True:
1095 continue
1096 lines.append('%s: %s' % (
1097 comm['l10n_comm_type'],
1098 comm['url']
1099 ))
1100 adr = ''
1101 if len(lines) > 0:
1102 adr = gmTools.html_escape_string('\n'.join(lines), replace_eol = True, keep_visual_eol = True)
1103 _HTML_data['branch'] = gmTools.html_escape_string(praxis['branch'])
1104 _HTML_data['praxis'] = gmTools.html_escape_string(praxis['praxis'])
1105 _HTML_data['gm_ver'] = gmTools.html_escape_string(gmTools.coalesce(_cfg.get(option = 'client_version'), 'git HEAD'))
1106 _HTML_data['adr'] = adr
1107
1108 index_fname = os.path.join(directory, 'index.html')
1109 index_file = io.open(index_fname, mode = 'wt', encoding = 'utf8')
1110 index_file.write(_INDEX_HTML_CONTENT % _HTML_data)
1111 index_file.close()
1112 return index_fname
1113
1114
1115 - def _create_frontpage_html(self, patient, praxis, directory, data, docs_list):
1116
1117 _HTML_LIST_ITEM = ' <li><a href="%s">%s</a></li>'
1118
1119 _HTML_data = {
1120 'html_title_header': _('Patient data for'),
1121 'html_title_patient': gmTools.html_escape_string(patient.get_description_gender(with_nickname = False) + ', ' + _('born') + ' ' + patient.get_formatted_dob('%Y %B %d')),
1122 'title': _('Patient data excerpt'),
1123 'pat_name': gmTools.html_escape_string(patient.get_description_gender(with_nickname = False)),
1124 'pat_dob': gmTools.html_escape_string(_('born') + ' ' + patient.get_formatted_dob('%Y %B %d')),
1125 'mugshot_url': 'documents/no-such-file.png',
1126 'mugshot_alt': _('no patient photograph available'),
1127 'mugshot_title': '',
1128 'docs_title': _('Documents'),
1129 'browse_root': _('browse storage medium'),
1130 'browse_docs': _('browse documents area'),
1131 'doc_subdir': DOCUMENTS_SUBDIR,
1132 'browse_dicomdir': '',
1133 'run_dicom_viewer': '',
1134 'date' : gmTools.html_escape_string(gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), format = '%Y %B %d'))
1135 }
1136 for key in data:
1137 _HTML_data[key] = data[key]
1138
1139
1140 _HTML_docs_list = []
1141 for doc in docs_list:
1142 subdir = os.path.join(directory, DOCUMENTS_SUBDIR, doc[0])
1143 if os.path.isdir(subdir):
1144 _HTML_docs_list.append(_HTML_LIST_ITEM % (os.path.join(DOCUMENTS_SUBDIR, doc[0]), _('DIRECTORY: %s/%s/') % (DOCUMENTS_SUBDIR, doc[0])))
1145 _HTML_docs_list.append(' <ul>')
1146 for fname in os.listdir(subdir):
1147 tmp = os.path.join(subdir, fname)
1148 if os.path.isdir(tmp):
1149 _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)))
1150 else:
1151 _HTML_docs_list.append(' <li><a href="%s">%s</a></li>' % (os.path.join(DOCUMENTS_SUBDIR, doc[0], fname), fname))
1152 _HTML_docs_list.append(' </ul>')
1153 else:
1154 _HTML_docs_list.append(_HTML_LIST_ITEM % (os.path.join(DOCUMENTS_SUBDIR, doc[0]), doc[1]))
1155 _HTML_data['docs_list'] = u'\n '.join(_HTML_docs_list)
1156
1157
1158 lines = []
1159 adr = praxis.branch.org_unit.address
1160 if adr is not None:
1161 lines.extend(adr.format())
1162 for comm in praxis.branch.org_unit.comm_channels:
1163 if comm['is_confidential'] is True:
1164 continue
1165 lines.append('%s: %s' % (
1166 comm['l10n_comm_type'],
1167 comm['url']
1168 ))
1169 adr = ''
1170 if len(lines) > 0:
1171 adr = gmTools.html_escape_string('\n'.join(lines), replace_eol = True, keep_visual_eol = True)
1172 _HTML_data['branch'] = gmTools.html_escape_string(praxis['branch'])
1173 _HTML_data['praxis'] = gmTools.html_escape_string(praxis['praxis'])
1174 _HTML_data['gm_ver'] = gmTools.html_escape_string(gmTools.coalesce(_cfg.get(option = 'client_version'), 'git HEAD'))
1175 _HTML_data['adr'] = adr
1176
1177 frontpage_fname = os.path.join(directory, 'frontpage.html')
1178 frontpage_file = io.open(frontpage_fname, mode = 'wt', encoding = 'utf8')
1179 frontpage_file.write(_FRONTPAGE_HTML_CONTENT % _HTML_data)
1180 frontpage_file.close()
1181 return frontpage_fname
1182
1183
1185 _log.debug('cloning dwv')
1186
1187 dwv_src_dir = os.path.join(gmTools.gmPaths().local_base_dir, 'resources', 'dwv4export')
1188 if not os.path.isdir(dwv_src_dir):
1189 _log.debug('[%s] not found', dwv_src_dir)
1190 dwv_src_dir = os.path.join(gmTools.gmPaths().system_app_data_dir, 'resources', 'dwv4export')
1191 if not os.path.isdir(dwv_src_dir):
1192 _log.debug('[%s] not found', dwv_src_dir)
1193 return None
1194
1195 if target_dir is None:
1196 target_dir = gmTools.mk_sandbox_dir()
1197 dwv_target_dir = os.path.join(target_dir, 'dwv')
1198 gmTools.rmdir(dwv_target_dir)
1199 try:
1200 shutil.copytree(dwv_src_dir, dwv_target_dir)
1201 except (shutil.Error, OSError):
1202 _log.exception('cannot include DWV, skipping')
1203 return None
1204
1205 return dwv_target_dir
1206
1207
1209 _README_CONTENT = (
1210 'This is a patient data excerpt created by the GNUmed Electronic Medical Record.\n'
1211 '\n'
1212 'Patient: %s\n'
1213 '\n'
1214 'Please display <frontpage.html> to browse patient data.\n'
1215 '\n'
1216 'Individual documents are stored in the subdirectory\n'
1217 '\n'
1218 ' documents/\n'
1219 '\n'
1220 '\n'
1221 'Data may need to be decrypted with either GNU Privacy\n'
1222 'Guard or 7zip/WinZip.\n'
1223 '\n'
1224 '.asc:\n'
1225 ' https://gnupg.org\n'
1226 '\n'
1227 '.7z:\n'
1228 ' https://www.7-zip.org\n'
1229 ' https://www.winzip.com\n'
1230 '\n'
1231 'To obtain any needed keys you will have to get in touch with\n'
1232 'the creator or the owner of this patient media excerpt.\n'
1233 )
1234 readme_fname = os.path.join(directory, 'README')
1235 readme_file = io.open(readme_fname, mode = 'wt', encoding = 'utf8')
1236 if patient is None:
1237 pat_str = _('<protected>')
1238 else:
1239 pat_str = patient.get_description_gender(with_nickname = False) + ', ' + _('born') + ' ' + patient.get_formatted_dob('%Y %B %d')
1240 readme_file.write(_README_CONTENT % pat_str)
1241 readme_file.close()
1242 return readme_fname
1243
1244
1246 _CD_INF_CONTENT = (
1247 '[Patient Info]\r\n'
1248 'PatientName=%s, %s\r\n'
1249 'Gender=%s\r\n'
1250 'BirthDate=%s\r\n'
1251 'CreationDate=%s\r\n'
1252 'PID=%s\r\n'
1253 'EMR=GNUmed\r\n'
1254 'Version=%s\r\n'
1255 '#StudyDate=\r\n'
1256 '#VNRInfo=<body part>\r\n'
1257 '\r\n'
1258 '# name format: lastnames, firstnames\r\n'
1259 '# date format: YYYY-MM-DD (ISO 8601)\r\n'
1260 '# gender format: %s\r\n'
1261 )
1262 fname = os.path.join(directory, 'CD.INF')
1263 cd_inf = io.open(fname, mode = 'wt', encoding = 'utf8')
1264 cd_inf.write(_CD_INF_CONTENT % (
1265 patient['lastnames'],
1266 patient['firstnames'],
1267 gmTools.coalesce(patient['gender'], '?'),
1268 patient.get_formatted_dob('%Y-%m-%d'),
1269 gmDateTime.pydt_strftime(gmDateTime.pydt_now_here(), format = '%Y-%m-%d'),
1270 patient.ID,
1271 _cfg.get(option = 'client_version'),
1272 ' / '.join([ '%s = %s (%s)' % (g['tag'], g['label'], g['l10n_label']) for g in patient.gender_list ])
1273 ))
1274 cd_inf.close()
1275 return fname
1276
1277
1279 _AUTORUN_INF_CONTENT = (
1280 '[AutoRun.Amd64]\r\n'
1281 'label=%(label)s\r\n'
1282 'shellexecute=index.html\r\n'
1283 'action=%(action)s\r\n'
1284 '%(icon)s\r\n'
1285 'UseAutoPlay=1\r\n'
1286 '\r\n'
1287 '[AutoRun]\r\n'
1288 'label=%(label)s\r\n'
1289 'shellexecute=index.html\r\n'
1290 'action=%(action)s\r\n'
1291 '%(icon)s\r\n'
1292 'UseAutoPlay=1\r\n'
1293 '\r\n'
1294 '[Content]\r\n'
1295 'PictureFiles=yes\r\n'
1296 'VideoFiles=yes\r\n'
1297 'MusicFiles=no\r\n'
1298 '\r\n'
1299 '[IgnoreContentPaths]\r\n'
1300 '\documents\r\n'
1301 '\r\n'
1302 '[unused]\r\n'
1303 'open=requires explicit executable\r\n'
1304 )
1305 autorun_dict = {
1306 'label': self._compute_autorun_inf_label(patient),
1307 'action': _('Browse patient data'),
1308 'icon': ''
1309 }
1310 media_icon_kwd = '$$gnumed_patient_media_export_icon'
1311 media_icon_kwd_exp = gmKeywordExpansion.get_expansion (
1312 keyword = media_icon_kwd,
1313 textual_only = False,
1314 binary_only = True
1315 )
1316 icon_tmp_fname = media_icon_kwd_exp.save_to_file (
1317 target_mime = 'image/x-icon',
1318 target_extension = '.ico',
1319 ignore_conversion_problems = True
1320 )
1321 if icon_tmp_fname is None:
1322 _log.error('cannot retrieve <%s>', media_icon_kwd)
1323 else:
1324 media_icon_fname = os.path.join(directory, 'gnumed.ico')
1325 try:
1326 shutil.copy2(icon_tmp_fname, media_icon_fname)
1327 autorun_dict['icon'] = 'icon=gnumed.ico'
1328 except Exception:
1329 _log.exception('cannot move %s to %s', icon_tmp_fname, media_icon_fname)
1330 autorun_fname = os.path.join(directory, 'AUTORUN.INF')
1331 autorun_file = io.open(autorun_fname, mode = 'wt', encoding = 'cp1252', errors = 'replace')
1332 autorun_file.write(_AUTORUN_INF_CONTENT % autorun_dict)
1333 autorun_file.close()
1334 return autorun_fname
1335
1336
1338 if patient is None:
1339
1340
1341
1342
1343 return _('GNUmed patient data excerpt')[:32]
1344
1345 LABEL_MAX_LEN = 32
1346 dob = patient.get_formatted_dob(format = ' %Y%m%d', none_string = '', honor_estimation = False)
1347 if dob == '':
1348 gender_template = ' (%s)'
1349 else:
1350 gender_template = ' %s'
1351 gender = gmTools.coalesce(patient['gender'], '', gender_template)
1352 name_max_len = LABEL_MAX_LEN - len(gender) - len(dob)
1353 name = patient.active_name
1354 last = name['lastnames'].strip()
1355 first = name['firstnames'].strip()
1356 len_last = len(last)
1357 len_first = len(first)
1358 while (len_last + len_first + 1) > name_max_len:
1359 if len_first > 6:
1360 len_first -= 1
1361 if first[len_first - 1] == ' ':
1362 len_first -= 1
1363 continue
1364 len_last -= 1
1365 if last[len_last - 1] == ' ':
1366 len_last -= 1
1367 last = last[:len_last].strip().upper()
1368 first = first[:len_first].strip()
1369
1370 label = (('%s %s%s%s' % (last, first, dob, gender)).strip())[:32]
1371 return label
1372
1373
1375 if passphrase is None:
1376 return filename
1377 enc_filename = gmCrypto.encrypt_file (
1378 filename = filename,
1379 passphrase = passphrase,
1380 verbose = _cfg.get(option = 'debug')
1381 )
1382 if enc_filename is None:
1383 _log.error('cannot encrypt')
1384 return None
1385 if not gmTools.remove_file(filename, log_error = True, force = True):
1386 _log.error('cannot remove unencrypted file')
1387 return None
1388 return enc_filename
1389
1390
1391
1392
1393 - def get_items(self, designation=None, order_by='designation, description'):
1394 return get_export_items(order_by = order_by, pk_identity = self.__pk_identity, designation = designation)
1395
1396 items = property(get_items, lambda x:x)
1397
1398
1400 return get_print_jobs(order_by = order_by, pk_identity = self.__pk_identity)
1401
1402 printouts = property(get_printouts, lambda x:x)
1403
1404
1405 if __name__ == '__main__':
1406
1407 if len(sys.argv) < 2:
1408 sys.exit()
1409
1410 if sys.argv[1] != 'test':
1411 sys.exit()
1412
1413 from Gnumed.pycommon import gmI18N
1414 gmI18N.activate_locale()
1415 gmI18N.install_domain()
1416
1417 from Gnumed.business import gmPraxis
1418
1419
1432
1433
1447
1448
1450
1451 from Gnumed.business.gmPerson import cPatient
1452 from Gnumed.business.gmPersonSearch import ask_for_patient
1453
1454
1455 pat_min = 1
1456 pat_max = 100
1457 try:
1458 pat_min = int(sys.argv[2])
1459 pat_max = int(sys.argv[3])
1460 except Exception:
1461 pass
1462 cPatient(aPK_obj = pat_min)
1463 f = io.open('x-auto_inf_labels.txt', mode = 'w', encoding = 'utf8')
1464 f.write('--------------------------------\n')
1465 f.write('12345678901234567890123456789012\n')
1466 f.write('--------------------------------\n')
1467 for pat_id in range(pat_min, pat_max):
1468 try:
1469 exp_area = cExportArea(pat_id)
1470 pat = cPatient(aPK_obj = pat_id)
1471 except Exception:
1472 continue
1473 f.write(exp_area._compute_autorun_inf_label(pat) + '\n')
1474 f.close()
1475 return
1476
1477
1478
1479 test_export_area()
1480
1481
1482 sys.exit(0)
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498