1 """This module encapsulates a document stored in a GNUmed database.
2
3 @copyright: GPL
4 """
5
6 __version__ = "$Revision: 1.118 $"
7 __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
8
9 import sys, os, shutil, os.path, types, time, logging
10 from cStringIO import StringIO
11 from pprint import pprint
12
13
14 if __name__ == '__main__':
15 sys.path.insert(0, '../../')
16 from Gnumed.pycommon import gmExceptions, gmBusinessDBObject, gmPG2, gmTools, gmMimeLib
17
18
19 _log = logging.getLogger('gm.docs')
20 _log.info(__version__)
21
22 MUGSHOT=26
23 DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE = u'visual progress note'
24 DOCUMENT_TYPE_PRESCRIPTION = u'prescription'
25
27 """Represents a folder with medical documents for a single patient."""
28
30 """Fails if
31
32 - patient referenced by aPKey does not exist
33 """
34 self.pk_patient = aPKey
35 if not self._pkey_exists():
36 raise gmExceptions.ConstructorError, "No patient with PK [%s] in database." % aPKey
37
38
39
40
41
42
43
44 _log.debug('instantiated document folder for patient [%s]' % self.pk_patient)
45
48
49
50
52 """Does this primary key exist ?
53
54 - true/false/None
55 """
56
57 rows, idx = gmPG2.run_ro_queries(queries = [
58 {'cmd': u"select exists(select pk from dem.identity where pk = %s)", 'args': [self.pk_patient]}
59 ])
60 if not rows[0][0]:
61 _log.error("patient [%s] not in demographic database" % self.pk_patient)
62 return None
63 return True
64
65
66
68 cmd = u"""
69 SELECT pk_doc
70 FROM blobs.v_doc_med
71 WHERE
72 pk_patient = %(pat)s
73 AND
74 type = %(typ)s
75 AND
76 ext_ref = %(ref)s
77 ORDER BY
78 clin_when DESC
79 LIMIT 1
80 """
81 args = {
82 'pat': self.pk_patient,
83 'typ': DOCUMENT_TYPE_PRESCRIPTION,
84 'ref': u'FreeDiams'
85 }
86 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
87 if len(rows) == 0:
88 _log.info('no FreeDiams prescription available for patient [%s]' % self.pk_patient)
89 return None
90 prescription = cDocument(aPK_obj = rows[0][0])
91 return prescription
92
94 cmd = u"select pk_obj from blobs.v_latest_mugshot where pk_patient=%s"
95 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient]}])
96 if len(rows) == 0:
97 _log.info('no mugshots available for patient [%s]' % self.pk_patient)
98 return None
99 mugshot = cDocumentPart(aPK_obj=rows[0][0])
100 return mugshot
101
103 if latest_only:
104 cmd = u"select pk_doc, pk_obj from blobs.v_latest_mugshot where pk_patient=%s"
105 else:
106 cmd = u"""
107 select
108 vdm.pk_doc as pk_doc,
109 dobj.pk as pk_obj
110 from
111 blobs.v_doc_med vdm
112 blobs.doc_obj dobj
113 where
114 vdm.pk_type = (select pk from blobs.doc_type where name = 'patient photograph')
115 and vdm.pk_patient = %s
116 and dobj.fk_doc = vdm.pk_doc
117 """
118 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_patient]}])
119 return rows
120
122 """return flat list of document IDs"""
123
124 args = {
125 'ID': self.pk_patient,
126 'TYP': doc_type
127 }
128
129 cmd = u"""
130 select vdm.pk_doc
131 from blobs.v_doc_med vdm
132 where
133 vdm.pk_patient = %%(ID)s
134 %s
135 order by vdm.clin_when"""
136
137 if doc_type is None:
138 cmd = cmd % u''
139 else:
140 try:
141 int(doc_type)
142 cmd = cmd % u'and vdm.pk_type = %(TYP)s'
143 except (TypeError, ValueError):
144 cmd = cmd % u'and vdm.pk_type = (select pk from blobs.doc_type where name = %(TYP)s)'
145
146 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
147 doc_ids = []
148 for row in rows:
149 doc_ids.append(row[0])
150 return doc_ids
151
158
159 - def get_documents(self, doc_type=None, episodes=None, encounter=None):
160 """Return list of documents."""
161
162 args = {
163 'pat': self.pk_patient,
164 'type': doc_type,
165 'enc': encounter
166 }
167 where_parts = [u'pk_patient = %(pat)s']
168
169 if doc_type is not None:
170 try:
171 int(doc_type)
172 where_parts.append(u'pk_type = %(type)s')
173 except (TypeError, ValueError):
174 where_parts.append(u'pk_type = (SELECT pk FROM blobs.doc_type WHERE name = %(type)s)')
175
176 if (episodes is not None) and (len(episodes) > 0):
177 where_parts.append(u'pk_episode IN %(epi)s')
178 args['epi'] = tuple(episodes)
179
180 if encounter is not None:
181 where_parts.append(u'pk_encounter = %(enc)s')
182
183 cmd = u"%s\nORDER BY clin_when" % (_sql_fetch_document_fields % u' AND '.join(where_parts))
184 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
185
186 return [ cDocument(row = {'pk_field': 'pk_doc', 'idx': idx, 'data': r}) for r in rows ]
187
188 - def add_document(self, document_type=None, encounter=None, episode=None):
189 return create_document(document_type = document_type, encounter = encounter, episode = episode)
190
191 _sql_fetch_document_part_fields = u"select * from blobs.v_obj4doc_no_data where %s"
192
194 """Represents one part of a medical document."""
195
196 _cmd_fetch_payload = _sql_fetch_document_part_fields % u"pk_obj = %s"
197 _cmds_store_payload = [
198 u"""update blobs.doc_obj set
199 seq_idx = %(seq_idx)s,
200 comment = gm.nullify_empty_string(%(obj_comment)s),
201 filename = gm.nullify_empty_string(%(filename)s),
202 fk_intended_reviewer = %(pk_intended_reviewer)s
203 where
204 pk=%(pk_obj)s and
205 xmin=%(xmin_doc_obj)s""",
206 u"""select xmin_doc_obj from blobs.v_obj4doc_no_data where pk_obj = %(pk_obj)s"""
207 ]
208 _updatable_fields = [
209 'seq_idx',
210 'obj_comment',
211 'pk_intended_reviewer',
212 'filename'
213 ]
214
215
216
217 - def export_to_file(self, aTempDir = None, aChunkSize = 0, filename=None):
218
219 if self._payload[self._idx['size']] == 0:
220 return None
221
222 if filename is None:
223 suffix = None
224
225 if self._payload[self._idx['filename']] is not None:
226 name, suffix = os.path.splitext(self._payload[self._idx['filename']])
227 suffix = suffix.strip()
228 if suffix == u'':
229 suffix = None
230
231 filename = gmTools.get_unique_filename (
232 prefix = 'gm-doc_obj-page_%s-' % self._payload[self._idx['seq_idx']],
233 suffix = suffix,
234 tmp_dir = aTempDir
235 )
236
237 success = gmPG2.bytea2file (
238 data_query = {
239 'cmd': u'SELECT substring(data from %(start)s for %(size)s) FROM blobs.doc_obj WHERE pk=%(pk)s',
240 'args': {'pk': self.pk_obj}
241 },
242 filename = filename,
243 chunk_size = aChunkSize,
244 data_size = self._payload[self._idx['size']]
245 )
246
247 if success:
248 return filename
249
250 return None
251
253 cmd = u"""
254 select
255 reviewer,
256 reviewed_when,
257 is_technically_abnormal,
258 clinically_relevant,
259 is_review_by_responsible_reviewer,
260 is_your_review,
261 coalesce(comment, '')
262 from blobs.v_reviewed_doc_objects
263 where pk_doc_obj = %s
264 order by
265 is_your_review desc,
266 is_review_by_responsible_reviewer desc,
267 reviewed_when desc
268 """
269 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}])
270 return rows
271
273 return cDocument(aPK_obj = self._payload[self._idx['pk_doc']])
274
275
276
278
279 if not (os.access(fname, os.R_OK) and os.path.isfile(fname)):
280 _log.error('[%s] is not a readable file' % fname)
281 return False
282
283 gmPG2.file2bytea (
284 query = u"UPDATE blobs.doc_obj SET data=%(data)s::bytea WHERE pk=%(pk)s",
285 filename = fname,
286 args = {'pk': self.pk_obj}
287 )
288
289
290 self.refetch_payload()
291 return True
292
293 - def set_reviewed(self, technically_abnormal=None, clinically_relevant=None):
294
295 cmd = u"""
296 select pk
297 from blobs.reviewed_doc_objs
298 where
299 fk_reviewed_row = %s and
300 fk_reviewer = (select pk from dem.staff where db_user = current_user)"""
301 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}])
302
303
304 if len(rows) == 0:
305 cols = [
306 u"fk_reviewer",
307 u"fk_reviewed_row",
308 u"is_technically_abnormal",
309 u"clinically_relevant"
310 ]
311 vals = [
312 u'%(fk_row)s',
313 u'%(abnormal)s',
314 u'%(relevant)s'
315 ]
316 args = {
317 'fk_row': self.pk_obj,
318 'abnormal': technically_abnormal,
319 'relevant': clinically_relevant
320 }
321 cmd = u"""
322 insert into blobs.reviewed_doc_objs (
323 %s
324 ) values (
325 (select pk from dem.staff where db_user=current_user),
326 %s
327 )""" % (', '.join(cols), ', '.join(vals))
328
329
330 if len(rows) == 1:
331 pk_row = rows[0][0]
332 args = {
333 'abnormal': technically_abnormal,
334 'relevant': clinically_relevant,
335 'pk_row': pk_row
336 }
337 cmd = u"""
338 update blobs.reviewed_doc_objs set
339 is_technically_abnormal = %(abnormal)s,
340 clinically_relevant = %(relevant)s
341 where
342 pk=%(pk_row)s"""
343 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
344
345 return True
346
348 if self._payload[self._idx['type']] != u'patient photograph':
349 return False
350
351 rows, idx = gmPG2.run_ro_queries (
352 queries = [{
353 'cmd': u'select coalesce(max(seq_idx)+1, 1) from blobs.doc_obj where fk_doc=%(doc_id)s',
354 'args': {'doc_id': self._payload[self._idx['pk_doc']]}
355 }]
356 )
357 self._payload[self._idx['seq_idx']] = rows[0][0]
358 self._is_modified = True
359 self.save_payload()
360
362
363 fname = self.export_to_file(aTempDir = tmpdir, aChunkSize = chunksize)
364 if fname is None:
365 return False, ''
366
367 success, msg = gmMimeLib.call_viewer_on_file(fname, block = block)
368 if not success:
369 return False, msg
370
371 return True, ''
372
373 _sql_fetch_document_fields = u"select * from blobs.v_doc_med where %s"
374
375 -class cDocument(gmBusinessDBObject.cBusinessDBObject):
376 """Represents one medical document."""
377
378 _cmd_fetch_payload = _sql_fetch_document_fields % u"pk_doc = %s"
379 _cmds_store_payload = [
380 u"""update blobs.doc_med set
381 fk_type = %(pk_type)s,
382 fk_episode = %(pk_episode)s,
383 fk_encounter = %(pk_encounter)s,
384 clin_when = %(clin_when)s,
385 comment = gm.nullify_empty_string(%(comment)s),
386 ext_ref = gm.nullify_empty_string(%(ext_ref)s)
387 where
388 pk = %(pk_doc)s and
389 xmin = %(xmin_doc_med)s""",
390 u"""select xmin_doc_med from blobs.v_doc_med where pk_doc = %(pk_doc)s"""
391 ]
392
393 _updatable_fields = [
394 'pk_type',
395 'comment',
396 'clin_when',
397 'ext_ref',
398 'pk_episode',
399 'pk_encounter'
400 ]
401
403 try: del self.__has_unreviewed_parts
404 except AttributeError: pass
405
406 return super(cDocument, self).refetch_payload(ignore_changes = ignore_changes)
407
409 """Get document descriptions.
410
411 - will return a list of rows
412 """
413 if max_lng is None:
414 cmd = u"SELECT pk, text FROM blobs.doc_desc WHERE fk_doc = %s"
415 else:
416 cmd = u"SELECT pk, substring(text from 1 for %s) FROM blobs.doc_desc WHERE fk_doc=%%s" % max_lng
417 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}])
418 return rows
419
424
426 cmd = u"update blobs.doc_desc set text = %(desc)s where fk_doc = %(doc)s and pk = %(pk_desc)s"
427 gmPG2.run_rw_queries(queries = [
428 {'cmd': cmd, 'args': {'doc': self.pk_obj, 'pk_desc': pk, 'desc': description}}
429 ])
430 return True
431
433 cmd = u"delete from blobs.doc_desc where fk_doc = %(doc)s and pk = %(desc)s"
434 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'doc': self.pk_obj, 'desc': pk}}])
435 return True
436
441
442 parts = property(_get_parts, lambda x:x)
443
445 """Add a part to the document."""
446
447 cmd = u"""
448 insert into blobs.doc_obj (
449 fk_doc, fk_intended_reviewer, data, seq_idx
450 ) VALUES (
451 %(doc_id)s,
452 (select pk_staff from dem.v_staff where db_user=CURRENT_USER),
453 ''::bytea,
454 (select coalesce(max(seq_idx)+1, 1) from blobs.doc_obj where fk_doc=%(doc_id)s)
455 )"""
456 rows, idx = gmPG2.run_rw_queries (
457 queries = [
458 {'cmd': cmd, 'args': {'doc_id': self.pk_obj}},
459 {'cmd': u"select currval('blobs.doc_obj_pk_seq')"}
460 ],
461 return_data = True
462 )
463
464 pk_part = rows[0][0]
465 new_part = cDocumentPart(aPK_obj = pk_part)
466 if not new_part.update_data_from_file(fname=file):
467 _log.error('cannot import binary data from [%s] into document part' % file)
468 gmPG2.run_rw_queries (
469 queries = [
470 {'cmd': u"delete from blobs.doc_obj where pk = %s", 'args': [pk_part]}
471 ]
472 )
473 return None
474 return new_part
475
477
478 new_parts = []
479
480 for filename in files:
481 new_part = self.add_part(file = filename)
482 if new_part is None:
483 msg = 'cannot instantiate document part object'
484 _log.error(msg)
485 return (False, msg, filename)
486 new_parts.append(new_part)
487
488 new_part['filename'] = filename
489 new_part['pk_intended_reviewer'] = reviewer
490
491 success, data = new_part.save_payload()
492 if not success:
493 msg = 'cannot set reviewer to [%s]' % reviewer
494 _log.error(msg)
495 _log.error(str(data))
496 return (False, msg, filename)
497
498 return (True, '', new_parts)
499
501 fnames = []
502 for part in self.parts:
503
504 fname = os.path.basename(gmTools.coalesce (
505 part['filename'],
506 u'%s%s%s_%s' % (part['l10n_type'], gmTools.coalesce(part['ext_ref'], '-', '-%s-'), _('part'), part['seq_idx'])
507 ))
508 if export_dir is not None:
509 fname = os.path.join(export_dir, fname)
510 fnames.append(part.export_to_file(aChunkSize = chunksize, filename = fname))
511 return fnames
512
514 try:
515 return self.__has_unreviewed_parts
516 except AttributeError:
517 pass
518
519 cmd = u"SELECT EXISTS(SELECT 1 FROM blobs.v_obj4doc_no_data WHERE pk_doc = %(pk)s AND reviewed IS FALSE)"
520 args = {'pk': self.pk_obj}
521 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
522 self.__has_unreviewed_parts = rows[0][0]
523
524 return self.__has_unreviewed_parts
525
526 has_unreviewed_parts = property(_get_has_unreviewed_parts, lambda x:x)
527
528 - def set_reviewed(self, technically_abnormal=None, clinically_relevant=None):
529
530 for part in self.parts:
531 if not part.set_reviewed(technically_abnormal, clinically_relevant):
532 return False
533 return True
534
536 for part in self.parts:
537 part['pk_intended_reviewer'] = reviewer
538 success, data = part.save_payload()
539 if not success:
540 _log.error('cannot set reviewer to [%s]' % reviewer)
541 _log.error(str(data))
542 return False
543 return True
544
546 """Returns new document instance or raises an exception.
547 """
548 cmd = u"""INSERT INTO blobs.doc_med (fk_type, fk_encounter, fk_episode) VALUES (%(type)s, %(enc)s, %(epi)s) RETURNING pk"""
549 try:
550 int(document_type)
551 except ValueError:
552 cmd = u"""
553 INSERT INTO blobs.doc_med (
554 fk_type,
555 fk_encounter,
556 fk_episode
557 ) VALUES (
558 coalesce (
559 (SELECT pk from blobs.doc_type bdt where bdt.name = %(type)s),
560 (SELECT pk from blobs.doc_type bdt where _(bdt.name) = %(type)s)
561 ),
562 %(enc)s,
563 %(epi)s
564 ) RETURNING pk"""
565
566 args = {'type': document_type, 'enc': encounter, 'epi': episode}
567 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True)
568 doc = cDocument(aPK_obj = rows[0][0])
569 return doc
570
572 """Searches for documents with the given patient and type ID.
573
574 No type ID returns all documents for the patient.
575 """
576
577 if patient_id is None:
578 raise ValueError('need patient id to search for document')
579
580 args = {'pat_id': patient_id, 'type_id': type_id}
581 if type_id is None:
582 cmd = u"SELECT pk_doc from blobs.v_doc_med WHERE pk_patient = %(pat_id)s"
583 else:
584 cmd = u"SELECT pk_doc from blobs.v_doc_med WHERE pk_patient = %(pat_id)s and pk_type = %(type_id)s"
585
586 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
587
588 docs = []
589 for row in rows:
590 docs.append(cDocument(row[0]))
591 return docs
592
594
595 cmd = u"select blobs.delete_document(%(pk)s, %(enc)s)"
596 args = {'pk': document_id, 'enc': encounter_id}
597 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
598 return
599
601
602 _log.debug('reclassifying documents by type')
603 _log.debug('original: %s', original_type)
604 _log.debug('target: %s', target_type)
605
606 if target_type['pk_doc_type'] == original_type['pk_doc_type']:
607 return True
608
609 cmd = u"""
610 update blobs.doc_med set
611 fk_type = %(new_type)s
612 where
613 fk_type = %(old_type)s
614 """
615 args = {u'new_type': target_type['pk_doc_type'], u'old_type': original_type['pk_doc_type']}
616
617 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
618
619 return True
620
621
623 """Represents a document type."""
624 _cmd_fetch_payload = u"""select * from blobs.v_doc_type where pk_doc_type=%s"""
625 _cmds_store_payload = [
626 u"""update blobs.doc_type set
627 name = %(type)s
628 where
629 pk=%(pk_obj)s and
630 xmin=%(xmin_doc_type)s""",
631 u"""select xmin_doc_type from blobs.v_doc_type where pk_doc_type = %(pk_obj)s"""
632 ]
633 _updatable_fields = ['type']
634
636
637 if translation.strip() == '':
638 return False
639
640 if translation.strip() == self._payload[self._idx['l10n_type']].strip():
641 return True
642
643 rows, idx = gmPG2.run_rw_queries (
644 queries = [
645 {'cmd': u'select i18n.i18n(%s)', 'args': [self._payload[self._idx['type']]]},
646 {'cmd': u'select i18n.upd_tx((select i18n.get_curr_lang()), %(orig)s, %(tx)s)',
647 'args': {
648 'orig': self._payload[self._idx['type']],
649 'tx': translation
650 }
651 }
652 ],
653 return_data = True
654 )
655 if not rows[0][0]:
656 _log.error('cannot set translation to [%s]' % translation)
657 return False
658
659 return self.refetch_payload()
660
661
663 rows, idx = gmPG2.run_ro_queries (
664 queries = [{'cmd': u"SELECT * FROM blobs.v_doc_type"}],
665 get_col_idx = True
666 )
667 doc_types = []
668 for row in rows:
669 row_def = {
670 'pk_field': 'pk_doc_type',
671 'idx': idx,
672 'data': row
673 }
674 doc_types.append(cDocumentType(row = row_def))
675 return doc_types
676
678
679 cmd = u'select pk from blobs.doc_type where name = %s'
680 rows, idx = gmPG2.run_ro_queries (
681 queries = [{'cmd': cmd, 'args': [document_type]}]
682 )
683 if len(rows) == 0:
684 cmd1 = u"insert into blobs.doc_type (name) values (%s)"
685 cmd2 = u"select currval('blobs.doc_type_pk_seq')"
686 rows, idx = gmPG2.run_rw_queries (
687 queries = [
688 {'cmd': cmd1, 'args': [document_type]},
689 {'cmd': cmd2}
690 ],
691 return_data = True
692 )
693 return cDocumentType(aPK_obj = rows[0][0])
694
696 if document_type['is_in_use']:
697 return False
698 gmPG2.run_rw_queries (
699 queries = [{
700 'cmd': u'delete from blobs.doc_type where pk=%s',
701 'args': [document_type['pk_doc_type']]
702 }]
703 )
704 return True
705
707 """This needs *considerably* more smarts."""
708 dirname = gmTools.get_unique_filename (
709 prefix = '',
710 suffix = time.strftime(".%Y%m%d-%H%M%S", time.localtime())
711 )
712
713 path, doc_ID = os.path.split(dirname)
714 return doc_ID
715
716
717
718 if __name__ == '__main__':
719
720 if len(sys.argv) < 2:
721 sys.exit()
722
723 if sys.argv[1] != u'test':
724 sys.exit()
725
726
728
729 print "----------------------"
730 print "listing document types"
731 print "----------------------"
732
733 for dt in get_document_types():
734 print dt
735
736 print "------------------------------"
737 print "testing document type handling"
738 print "------------------------------"
739
740 dt = create_document_type(document_type = 'dummy doc type for unit test 1')
741 print "created:", dt
742
743 dt['type'] = 'dummy doc type for unit test 2'
744 dt.save_payload()
745 print "changed base name:", dt
746
747 dt.set_translation(translation = 'Dummy-Dokumenten-Typ fuer Unit-Test')
748 print "translated:", dt
749
750 print "deleted:", delete_document_type(document_type = dt)
751
752 return
753
755
756 print "-----------------------"
757 print "testing document import"
758 print "-----------------------"
759
760 docs = search_for_document(patient_id=12)
761 doc = docs[0]
762 print "adding to doc:", doc
763
764 fname = sys.argv[1]
765 print "adding from file:", fname
766 part = doc.add_part(file=fname)
767 print "new part:", part
768
769 return
770
782
783 from Gnumed.pycommon import gmI18N
784 gmI18N.activate_locale()
785 gmI18N.install_domain()
786
787
788
789 test_get_documents()
790
791
792
793
794