1 """This module encapsulates a document stored in a GNUmed database.
2
3 @copyright: GPL
4 """
5
6
7
8 __version__ = "$Revision: 1.118 $"
9 __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
10
11 import sys, os, shutil, os.path, types, time, logging
12 from cStringIO import StringIO
13
14
15 if __name__ == '__main__':
16 sys.path.insert(0, '../../')
17 from Gnumed.pycommon import gmExceptions, gmBusinessDBObject, gmPG2, gmTools, gmMimeLib
18
19
20 _log = logging.getLogger('gm.docs')
21 _log.info(__version__)
22
23 MUGSHOT=26
24
26 """Represents a folder with medical documents for a single patient."""
27
29 """Fails if
30
31 - patient referenced by aPKey does not exist
32 """
33 self.pk_patient = aPKey
34 if not self._pkey_exists():
35 raise gmExceptions.ConstructorError, "No patient with PK [%s] in database." % aPKey
36
37
38
39
40
41
42
43 _log.debug('instantiated document folder for patient [%s]' % self.pk_patient)
44
47
48
49
51 """Does this primary key exist ?
52
53 - true/false/None
54 """
55
56 rows, idx = gmPG2.run_ro_queries(queries = [
57 {'cmd': u"select exists(select pk from dem.identity where pk = %s)", 'args': [self.pk_patient]}
58 ])
59 if not rows[0][0]:
60 _log.error("patient [%s] not in demographic database" % self.pk_patient)
61 return None
62 return True
63
64
65
67 cmd = u"select pk_obj from blobs.v_latest_mugshot where pk_patient=%s"
68 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient]}])
69 if len(rows) == 0:
70 _log.info('no mugshots available for patient [%s]' % self.pk_patient)
71 return None
72 mugshot = cMedDocPart(aPK_obj=rows[0][0])
73 return mugshot
74
76 if latest_only:
77 cmd = u"select pk_doc, pk_obj from blobs.v_latest_mugshot where pk_patient=%s"
78 else:
79 cmd = u"""
80 select
81 vdm.pk_doc as pk_doc,
82 dobj.pk as pk_obj
83 from
84 blobs.v_doc_med vdm
85 blobs.doc_obj dobj
86 where
87 vdm.pk_type = (select pk from blobs.doc_type where name = 'patient photograph')
88 and vdm.pk_patient = %s
89 and dobj.fk_doc = vdm.pk_doc
90 """
91 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient]}])
92 return rows
93
95 """return flat list of document IDs"""
96
97 args = {
98 'ID': self.pk_patient,
99 'TYP': doc_type
100 }
101
102 cmd = u"""
103 select vdm.pk_doc
104 from blobs.v_doc_med vdm
105 where
106 vdm.pk_patient = %%(ID)s
107 %s
108 order by vdm.clin_when"""
109
110 if doc_type is None:
111 cmd = cmd % u''
112 else:
113 try:
114 int(doc_type)
115 cmd = cmd % u'and vdm.pk_type = %(TYP)s'
116 except (TypeError, ValueError):
117 cmd = cmd % u'and vdm.pk_type = (select pk from blobs.doc_type where name = %(TYP)s)'
118
119 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
120 doc_ids = []
121 for row in rows:
122 doc_ids.append(row[0])
123 return doc_ids
124
125 - def get_documents(self, doc_type=None, episodes=None, encounter=None):
126 """Return list of documents."""
127 doc_ids = self.get_doc_list(doc_type=doc_type)
128
129 docs = []
130 for doc_id in doc_ids:
131 try:
132 docs.append(cMedDoc(aPK_obj=doc_id))
133 except gmExceptions.ConstructorError:
134 _log.exception('document error on [%s] for patient [%s]' % (doc_id, self.pk_patient))
135 continue
136
137 if episodes is not None:
138 docs = [ d for d in docs if d['pk_episode'] in episodes ]
139
140 if encounter is not None:
141 docs = [ d for d in docs if d['pk_encounter'] == encounter ]
142
143 return docs
144
145 - def add_document(self, document_type=None, encounter=None, episode=None):
146 return create_document(document_type = document_type, encounter = encounter, episode = episode)
147
148 -class cMedDocPart(gmBusinessDBObject.cBusinessDBObject):
149 """Represents one part of a medical document."""
150
151 _cmd_fetch_payload = u"""select * from blobs.v_obj4doc_no_data where pk_obj=%s"""
152 _cmds_store_payload = [
153 u"""update blobs.doc_obj set
154 seq_idx = %(seq_idx)s,
155 comment = gm.nullify_empty_string(%(obj_comment)s),
156 filename = gm.nullify_empty_string(%(filename)s),
157 fk_intended_reviewer = %(pk_intended_reviewer)s
158 where
159 pk=%(pk_obj)s and
160 xmin=%(xmin_doc_obj)s""",
161 u"""select xmin_doc_obj from blobs.v_obj4doc_no_data where pk_obj = %(pk_obj)s"""
162 ]
163 _updatable_fields = [
164 'seq_idx',
165 'obj_comment',
166 'pk_intended_reviewer',
167 'filename'
168 ]
169
170
171
172 - def export_to_file(self, aTempDir = None, aChunkSize = 0, filename=None):
173
174 if self._payload[self._idx['size']] == 0:
175 return None
176
177 if filename is None:
178 suffix = None
179
180 if self._payload[self._idx['filename']] is not None:
181 name, suffix = os.path.splitext(self._payload[self._idx['filename']])
182 suffix = suffix.strip()
183 if suffix == u'':
184 suffix = None
185
186 filename = gmTools.get_unique_filename (
187 prefix = 'gm-doc_obj-page_%s-' % self._payload[self._idx['seq_idx']],
188 suffix = suffix,
189 tmp_dir = aTempDir
190 )
191
192 success = gmPG2.bytea2file (
193 data_query = {
194 'cmd': u'SELECT substring(data from %(start)s for %(size)s) FROM blobs.doc_obj WHERE pk=%(pk)s',
195 'args': {'pk': self.pk_obj}
196 },
197 filename = filename,
198 chunk_size = aChunkSize,
199 data_size = self._payload[self._idx['size']]
200 )
201
202 if success:
203 return filename
204
205 return None
206
208 cmd = u"""
209 select
210 reviewer,
211 reviewed_when,
212 is_technically_abnormal,
213 clinically_relevant,
214 is_review_by_responsible_reviewer,
215 is_your_review,
216 coalesce(comment, '')
217 from blobs.v_reviewed_doc_objects
218 where pk_doc_obj = %s
219 order by
220 is_your_review desc,
221 is_review_by_responsible_reviewer desc,
222 reviewed_when desc
223 """
224 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}])
225 return rows
226
228 return cMedDoc(aPK_obj = self._payload[self._idx['pk_doc']])
229
230
231
233
234 if not (os.access(fname, os.R_OK) and os.path.isfile(fname)):
235 _log.error('[%s] is not a readable file' % fname)
236 return False
237
238 gmPG2.file2bytea (
239 query = u"UPDATE blobs.doc_obj SET data=%(data)s::bytea WHERE pk=%(pk)s",
240 filename = fname,
241 args = {'pk': self.pk_obj}
242 )
243
244
245 self.refetch_payload()
246 return True
247
248 - def set_reviewed(self, technically_abnormal=None, clinically_relevant=None):
249
250 cmd = u"""
251 select pk
252 from blobs.reviewed_doc_objs
253 where
254 fk_reviewed_row = %s and
255 fk_reviewer = (select pk from dem.staff where db_user=current_user)"""
256 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}])
257
258
259 if len(rows) == 0:
260 cols = [
261 u"fk_reviewer",
262 u"fk_reviewed_row",
263 u"is_technically_abnormal",
264 u"clinically_relevant"
265 ]
266 vals = [
267 u'%(fk_row)s',
268 u'%(abnormal)s',
269 u'%(relevant)s'
270 ]
271 args = {
272 'fk_row': self.pk_obj,
273 'abnormal': technically_abnormal,
274 'relevant': clinically_relevant
275 }
276 cmd = u"""
277 insert into blobs.reviewed_doc_objs (
278 %s
279 ) values (
280 (select pk from dem.staff where db_user=current_user),
281 %s
282 )""" % (', '.join(cols), ', '.join(vals))
283
284
285 if len(rows) == 1:
286 pk_row = rows[0][0]
287 args = {
288 'abnormal': technically_abnormal,
289 'relevant': clinically_relevant,
290 'pk_row': pk_row
291 }
292 cmd = u"""
293 update blobs.reviewed_doc_objs set
294 is_technically_abnormal = %(abnormal)s,
295 clinically_relevant = %(relevant)s
296 where
297 pk=%(pk_row)s"""
298 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
299
300 return True
301
303 if self._payload[self._idx['type']] != u'patient photograph':
304 return False
305
306 rows, idx = gmPG2.run_ro_queries (
307 queries = [{
308 'cmd': u'select coalesce(max(seq_idx)+1, 1) from blobs.doc_obj where fk_doc=%(doc_id)s',
309 'args': {'doc_id': self._payload[self._idx['pk_doc']]}
310 }]
311 )
312 self._payload[self._idx['seq_idx']] = rows[0][0]
313 self._is_modified = True
314 self.save_payload()
315
317
318 fname = self.export_to_file(aTempDir = tmpdir, aChunkSize = chunksize)
319 if fname is None:
320 return False, ''
321
322 success, msg = gmMimeLib.call_viewer_on_file(fname, block = block)
323 if not success:
324 return False, msg
325
326 return True, ''
327
328 -class cMedDoc(gmBusinessDBObject.cBusinessDBObject):
329 """Represents one medical document."""
330
331 _cmd_fetch_payload = u"""select * from blobs.v_doc_med where pk_doc=%s"""
332 _cmds_store_payload = [
333 u"""update blobs.doc_med set
334 fk_type = %(pk_type)s,
335 fk_episode = %(pk_episode)s,
336 clin_when = %(clin_when)s,
337 comment = gm.nullify_empty_string(%(comment)s),
338 ext_ref = gm.nullify_empty_string(%(ext_ref)s)
339 where
340 pk = %(pk_doc)s and
341 xmin = %(xmin_doc_med)s""",
342 u"""select xmin_doc_med from blobs.v_doc_med where pk_doc = %(pk_doc)s"""
343 ]
344
345 _updatable_fields = [
346 'pk_type',
347 'comment',
348 'clin_when',
349 'ext_ref',
350 'pk_episode'
351 ]
352
354 """Get document descriptions.
355
356 - will return a list of rows
357 """
358 if max_lng is None:
359 cmd = u"SELECT pk, text FROM blobs.doc_desc WHERE fk_doc = %s"
360 else:
361 cmd = u"SELECT pk, substring(text from 1 for %s) FROM blobs.doc_desc WHERE fk_doc=%%s" % max_lng
362 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}])
363 return rows
364
369
371 cmd = u"update blobs.doc_desc set text = %(desc)s where fk_doc = %(doc)s and pk = %(pk_desc)s"
372 gmPG2.run_rw_queries(queries = [
373 {'cmd': cmd, 'args': {'doc': self.pk_obj, 'pk_desc': pk, 'desc': description}}
374 ])
375 return True
376
378 cmd = u"delete from blobs.doc_desc where fk_doc = %(doc)s and pk = %(desc)s"
379 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'doc': self.pk_obj, 'desc': pk}}])
380 return True
381
383 cmd = u"select pk_obj from blobs.v_obj4doc_no_data where pk_doc=%s"
384 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}])
385 parts = []
386 for row in rows:
387 try:
388 parts.append(cMedDocPart(aPK_obj=row[0]))
389 except ConstructorError, msg:
390 _log.exception(msg)
391 continue
392 return parts
393
395 """Add a part to the document."""
396
397 cmd = u"""
398 insert into blobs.doc_obj (
399 fk_doc, fk_intended_reviewer, data, seq_idx
400 ) VALUES (
401 %(doc_id)s,
402 (select pk_staff from dem.v_staff where db_user=CURRENT_USER),
403 ''::bytea,
404 (select coalesce(max(seq_idx)+1, 1) from blobs.doc_obj where fk_doc=%(doc_id)s)
405 )"""
406 rows, idx = gmPG2.run_rw_queries (
407 queries = [
408 {'cmd': cmd, 'args': {'doc_id': self.pk_obj}},
409 {'cmd': u"select currval('blobs.doc_obj_pk_seq')"}
410 ],
411 return_data = True
412 )
413
414 pk_part = rows[0][0]
415 new_part = cMedDocPart(aPK_obj = pk_part)
416 if not new_part.update_data_from_file(fname=file):
417 _log.error('cannot import binary data from [%s] into document part' % file)
418 gmPG2.run_rw_queries (
419 queries = [
420 {'cmd': u"delete from blobs.doc_obj where pk = %s", 'args': [pk_part]}
421 ]
422 )
423 return None
424 return new_part
425
427
428 new_parts = []
429
430 for filename in files:
431 new_part = self.add_part(file=filename)
432 if new_part is None:
433 msg = 'cannot instantiate document part object'
434 _log.error(msg)
435 return (False, msg, filename)
436 new_parts.append(new_part)
437
438 new_part['filename'] = filename
439 new_part['pk_intended_reviewer'] = reviewer
440
441 success, data = new_part.save_payload()
442 if not success:
443 msg = 'cannot set reviewer to [%s]' % reviewer
444 _log.error(msg)
445 _log.error(str(data))
446 return (False, msg, filename)
447
448 return (True, '', new_parts)
449
451 fnames = []
452 for part in self.get_parts():
453
454 fname = os.path.basename(gmTools.coalesce (
455 part['filename'],
456 u'%s%s%s_%s' % (part['l10n_type'], gmTools.coalesce(part['ext_ref'], '-', '-%s-'), _('part'), part['seq_idx'])
457 ))
458 if export_dir is not None:
459 fname = os.path.join(export_dir, fname)
460 fnames.append(part.export_to_file(aChunkSize = chunksize, filename = fname))
461 return fnames
462
464 cmd = u"select exists(select 1 from blobs.v_obj4doc_no_data where pk_doc=%s and not reviewed)"
465 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}])
466 return rows[0][0]
467
468 - def set_reviewed(self, technically_abnormal=None, clinically_relevant=None):
469
470 for part in self.get_parts():
471 if not part.set_reviewed(technically_abnormal, clinically_relevant):
472 return False
473 return True
474
476 for part in self.get_parts():
477 part['pk_intended_reviewer'] = reviewer
478 success, data = part.save_payload()
479 if not success:
480 _log.error('cannot set reviewer to [%s]' % reviewer)
481 _log.error(str(data))
482 return False
483 return True
484
485
487 """Returns new document instance or raises an exception.
488 """
489 cmd1 = u"""insert into blobs.doc_med (fk_type, fk_encounter, fk_episode) VALUES (%(type)s, %(enc)s, %(epi)s)"""
490 cmd2 = u"""select currval('blobs.doc_med_pk_seq')"""
491 rows, idx = gmPG2.run_rw_queries (
492 queries = [
493 {'cmd': cmd1, 'args': {'type': document_type, 'enc': encounter, 'epi': episode}},
494 {'cmd': cmd2}
495 ],
496 return_data = True
497 )
498 doc_id = rows[0][0]
499 doc = cMedDoc(aPK_obj = doc_id)
500 return doc
501
503 """Searches for documents with the given patient and type ID.
504
505 No type ID returns all documents for the patient.
506 """
507
508 if patient_id is None:
509 raise ValueError('need patient id to search for document')
510
511 args = {'pat_id': patient_id, 'type_id': type_id}
512 if type_id is None:
513 cmd = u"SELECT pk_doc from blobs.v_doc_med WHERE pk_patient = %(pat_id)s"
514 else:
515 cmd = u"SELECT pk_doc from blobs.v_doc_med WHERE pk_patient = %(pat_id)s and pk_type = %(type_id)s"
516
517 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
518
519 docs = []
520 for row in rows:
521 docs.append(cMedDoc(row[0]))
522 return docs
523
525
526 cmd = u"select blobs.delete_document(%(pk)s, %(enc)s)"
527 args = {'pk': document_id, 'enc': encounter_id}
528 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
529 return
530
532
533 _log.debug('reclassifying documents by type')
534 _log.debug('original: %s', original_type)
535 _log.debug('target: %s', target_type)
536
537 if target_type['pk_doc_type'] == original_type['pk_doc_type']:
538 return True
539
540 cmd = u"""
541 update blobs.doc_med set
542 fk_type = %(new_type)s
543 where
544 fk_type = %(old_type)s
545 """
546 args = {u'new_type': target_type['pk_doc_type'], u'old_type': original_type['pk_doc_type']}
547
548 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
549
550 return True
551
552
554 """Represents a document type."""
555 _cmd_fetch_payload = u"""select * from blobs.v_doc_type where pk_doc_type=%s"""
556 _cmds_store_payload = [
557 u"""update blobs.doc_type set
558 name = %(type)s
559 where
560 pk=%(pk_obj)s and
561 xmin=%(xmin_doc_type)s""",
562 u"""select xmin_doc_type from blobs.v_doc_type where pk_doc_type = %(pk_obj)s"""
563 ]
564 _updatable_fields = ['type']
565
567
568 if translation.strip() == '':
569 return False
570
571 if translation.strip() == self._payload[self._idx['l10n_type']].strip():
572 return True
573
574 rows, idx = gmPG2.run_rw_queries (
575 queries = [
576 {'cmd': u'select i18n.i18n(%s)', 'args': [self._payload[self._idx['type']]]},
577 {'cmd': u'select i18n.upd_tx((select i18n.get_curr_lang()), %(orig)s, %(tx)s)',
578 'args': {
579 'orig': self._payload[self._idx['type']],
580 'tx': translation
581 }
582 }
583 ],
584 return_data = True
585 )
586 if not rows[0][0]:
587 _log.error('cannot set translation to [%s]' % translation)
588 return False
589
590 return self.refetch_payload()
591
592
594 rows, idx = gmPG2.run_ro_queries (
595 queries = [{'cmd': u"SELECT * FROM blobs.v_doc_type"}],
596 get_col_idx = True
597 )
598 doc_types = []
599 for row in rows:
600 row_def = {
601 'pk_field': 'pk_doc_type',
602 'idx': idx,
603 'data': row
604 }
605 doc_types.append(cDocumentType(row = row_def))
606 return doc_types
607
609
610 cmd = u'select pk from blobs.doc_type where name = %s'
611 rows, idx = gmPG2.run_ro_queries (
612 queries = [{'cmd': cmd, 'args': [document_type]}]
613 )
614 if len(rows) == 0:
615 cmd1 = u"insert into blobs.doc_type (name) values (%s)"
616 cmd2 = u"select currval('blobs.doc_type_pk_seq')"
617 rows, idx = gmPG2.run_rw_queries (
618 queries = [
619 {'cmd': cmd1, 'args': [document_type]},
620 {'cmd': cmd2}
621 ],
622 return_data = True
623 )
624 return cDocumentType(aPK_obj = rows[0][0])
625
627 if document_type['is_in_use']:
628 return False
629 gmPG2.run_rw_queries (
630 queries = [{
631 'cmd': u'delete from blobs.doc_type where pk=%s',
632 'args': [document_type['pk_doc_type']]
633 }]
634 )
635 return True
636
638 """This needs *considerably* more smarts."""
639 dirname = gmTools.get_unique_filename (
640 prefix = '',
641 suffix = time.strftime(".%Y%m%d-%H%M%S", time.localtime())
642 )
643
644 path, doc_ID = os.path.split(dirname)
645 return doc_ID
646
647
648
649 if __name__ == '__main__':
650
651
653
654 print "----------------------"
655 print "listing document types"
656 print "----------------------"
657
658 for dt in get_document_types():
659 print dt
660
661 print "------------------------------"
662 print "testing document type handling"
663 print "------------------------------"
664
665 dt = create_document_type(document_type = 'dummy doc type for unit test 1')
666 print "created:", dt
667
668 dt['type'] = 'dummy doc type for unit test 2'
669 dt.save_payload()
670 print "changed base name:", dt
671
672 dt.set_translation(translation = 'Dummy-Dokumenten-Typ fuer Unit-Test')
673 print "translated:", dt
674
675 print "deleted:", delete_document_type(document_type = dt)
676
677 return
678
680
681 print "-----------------------"
682 print "testing document import"
683 print "-----------------------"
684
685 docs = search_for_document(patient_id=12)
686 doc = docs[0]
687 print "adding to doc:", doc
688
689 fname = sys.argv[1]
690 print "adding from file:", fname
691 part = doc.add_part(file=fname)
692 print "new part:", part
693
694 return
695
696 from Gnumed.pycommon import gmI18N
697 gmI18N.activate_locale()
698 gmI18N.install_domain()
699
700 test_doc_types()
701 test_adding_doc_part()
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111