1 """This module encapsulates a document stored in a GNUmed database.
2
3 @copyright: GPL v2 or later
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"""
374 SELECT
375 *,
376 COALESCE (
377 (SELECT array_agg(seq_idx) FROM blobs.doc_obj b_do WHERE b_do.fk_doc = b_vdm.pk_doc),
378 ARRAY[]::integer[]
379 )
380 AS seq_idx_list
381 FROM
382 blobs.v_doc_med b_vdm
383 WHERE
384 %s
385 """
386
387 -class cDocument(gmBusinessDBObject.cBusinessDBObject):
388 """Represents one medical document."""
389
390 _cmd_fetch_payload = _sql_fetch_document_fields % u"pk_doc = %s"
391 _cmds_store_payload = [
392 u"""update blobs.doc_med set
393 fk_type = %(pk_type)s,
394 fk_episode = %(pk_episode)s,
395 fk_encounter = %(pk_encounter)s,
396 clin_when = %(clin_when)s,
397 comment = gm.nullify_empty_string(%(comment)s),
398 ext_ref = gm.nullify_empty_string(%(ext_ref)s)
399 where
400 pk = %(pk_doc)s and
401 xmin = %(xmin_doc_med)s""",
402 u"""select xmin_doc_med from blobs.v_doc_med where pk_doc = %(pk_doc)s"""
403 ]
404
405 _updatable_fields = [
406 'pk_type',
407 'comment',
408 'clin_when',
409 'ext_ref',
410 'pk_episode',
411 'pk_encounter'
412 ]
413
415 try: del self.__has_unreviewed_parts
416 except AttributeError: pass
417
418 return super(cDocument, self).refetch_payload(ignore_changes = ignore_changes)
419
421 """Get document descriptions.
422
423 - will return a list of rows
424 """
425 if max_lng is None:
426 cmd = u"SELECT pk, text FROM blobs.doc_desc WHERE fk_doc = %s"
427 else:
428 cmd = u"SELECT pk, substring(text from 1 for %s) FROM blobs.doc_desc WHERE fk_doc=%%s" % max_lng
429 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}])
430 return rows
431
436
438 cmd = u"update blobs.doc_desc set text = %(desc)s where fk_doc = %(doc)s and pk = %(pk_desc)s"
439 gmPG2.run_rw_queries(queries = [
440 {'cmd': cmd, 'args': {'doc': self.pk_obj, 'pk_desc': pk, 'desc': description}}
441 ])
442 return True
443
445 cmd = u"delete from blobs.doc_desc where fk_doc = %(doc)s and pk = %(desc)s"
446 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'doc': self.pk_obj, 'desc': pk}}])
447 return True
448
453
454 parts = property(_get_parts, lambda x:x)
455
457 """Add a part to the document."""
458
459 cmd = u"""
460 insert into blobs.doc_obj (
461 fk_doc, data, seq_idx
462 ) VALUES (
463 %(doc_id)s,
464 ''::bytea,
465 (select coalesce(max(seq_idx)+1, 1) from blobs.doc_obj where fk_doc=%(doc_id)s)
466 )"""
467 rows, idx = gmPG2.run_rw_queries (
468 queries = [
469 {'cmd': cmd, 'args': {'doc_id': self.pk_obj}},
470 {'cmd': u"select currval('blobs.doc_obj_pk_seq')"}
471 ],
472 return_data = True
473 )
474
475 pk_part = rows[0][0]
476 new_part = cDocumentPart(aPK_obj = pk_part)
477 if not new_part.update_data_from_file(fname=file):
478 _log.error('cannot import binary data from [%s] into document part' % file)
479 gmPG2.run_rw_queries (
480 queries = [
481 {'cmd': u"delete from blobs.doc_obj where pk = %s", 'args': [pk_part]}
482 ]
483 )
484 return None
485 new_part['filename'] = file
486 new_part.save_payload()
487
488 return new_part
489
491
492 new_parts = []
493
494 for filename in files:
495 new_part = self.add_part(file = filename)
496 if new_part is None:
497 msg = 'cannot instantiate document part object'
498 _log.error(msg)
499 return (False, msg, filename)
500 new_parts.append(new_part)
501
502 if reviewer is not None:
503 new_part['pk_intended_reviewer'] = reviewer
504 success, data = new_part.save_payload()
505 if not success:
506 msg = 'cannot set reviewer to [%s]' % reviewer
507 _log.error(msg)
508 _log.error(str(data))
509 return (False, msg, filename)
510
511 return (True, '', new_parts)
512
514 fnames = []
515 for part in self.parts:
516
517 fname = os.path.basename(gmTools.coalesce (
518 part['filename'],
519 u'%s%s%s_%s' % (part['l10n_type'], gmTools.coalesce(part['ext_ref'], '-', '-%s-'), _('part'), part['seq_idx'])
520 ))
521 if export_dir is not None:
522 fname = os.path.join(export_dir, fname)
523 fnames.append(part.export_to_file(aChunkSize = chunksize, filename = fname))
524 return fnames
525
527 try:
528 return self.__has_unreviewed_parts
529 except AttributeError:
530 pass
531
532 cmd = u"SELECT EXISTS(SELECT 1 FROM blobs.v_obj4doc_no_data WHERE pk_doc = %(pk)s AND reviewed IS FALSE)"
533 args = {'pk': self.pk_obj}
534 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
535 self.__has_unreviewed_parts = rows[0][0]
536
537 return self.__has_unreviewed_parts
538
539 has_unreviewed_parts = property(_get_has_unreviewed_parts, lambda x:x)
540
541 - def set_reviewed(self, technically_abnormal=None, clinically_relevant=None):
542
543 for part in self.parts:
544 if not part.set_reviewed(technically_abnormal, clinically_relevant):
545 return False
546 return True
547
549 for part in self.parts:
550 part['pk_intended_reviewer'] = reviewer
551 success, data = part.save_payload()
552 if not success:
553 _log.error('cannot set reviewer to [%s]' % reviewer)
554 _log.error(str(data))
555 return False
556 return True
557
559 """Returns new document instance or raises an exception.
560 """
561 cmd = u"""INSERT INTO blobs.doc_med (fk_type, fk_encounter, fk_episode) VALUES (%(type)s, %(enc)s, %(epi)s) RETURNING pk"""
562 try:
563 int(document_type)
564 except ValueError:
565 cmd = u"""
566 INSERT INTO blobs.doc_med (
567 fk_type,
568 fk_encounter,
569 fk_episode
570 ) VALUES (
571 coalesce (
572 (SELECT pk from blobs.doc_type bdt where bdt.name = %(type)s),
573 (SELECT pk from blobs.doc_type bdt where _(bdt.name) = %(type)s)
574 ),
575 %(enc)s,
576 %(epi)s
577 ) RETURNING pk"""
578
579 args = {'type': document_type, 'enc': encounter, 'epi': episode}
580 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True)
581 doc = cDocument(aPK_obj = rows[0][0])
582 return doc
583
585 """Searches for documents with the given patient and type ID.
586
587 No type ID returns all documents for the patient.
588 """
589
590 if patient_id is None:
591 raise ValueError('need patient id to search for document')
592
593 args = {'pat_id': patient_id, 'type_id': type_id}
594 if type_id is None:
595 cmd = u"SELECT pk_doc from blobs.v_doc_med WHERE pk_patient = %(pat_id)s"
596 else:
597 cmd = u"SELECT pk_doc from blobs.v_doc_med WHERE pk_patient = %(pat_id)s and pk_type = %(type_id)s"
598
599 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
600
601 docs = []
602 for row in rows:
603 docs.append(cDocument(row[0]))
604 return docs
605
607
608 cmd = u"select blobs.delete_document(%(pk)s, %(enc)s)"
609 args = {'pk': document_id, 'enc': encounter_id}
610 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
611 return
612
614
615 _log.debug('reclassifying documents by type')
616 _log.debug('original: %s', original_type)
617 _log.debug('target: %s', target_type)
618
619 if target_type['pk_doc_type'] == original_type['pk_doc_type']:
620 return True
621
622 cmd = u"""
623 update blobs.doc_med set
624 fk_type = %(new_type)s
625 where
626 fk_type = %(old_type)s
627 """
628 args = {u'new_type': target_type['pk_doc_type'], u'old_type': original_type['pk_doc_type']}
629
630 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
631
632 return True
633
634
636 """Represents a document type."""
637 _cmd_fetch_payload = u"""select * from blobs.v_doc_type where pk_doc_type=%s"""
638 _cmds_store_payload = [
639 u"""update blobs.doc_type set
640 name = %(type)s
641 where
642 pk=%(pk_obj)s and
643 xmin=%(xmin_doc_type)s""",
644 u"""select xmin_doc_type from blobs.v_doc_type where pk_doc_type = %(pk_obj)s"""
645 ]
646 _updatable_fields = ['type']
647
649
650 if translation.strip() == '':
651 return False
652
653 if translation.strip() == self._payload[self._idx['l10n_type']].strip():
654 return True
655
656 rows, idx = gmPG2.run_rw_queries (
657 queries = [
658 {'cmd': u'select i18n.i18n(%s)', 'args': [self._payload[self._idx['type']]]},
659 {'cmd': u'select i18n.upd_tx((select i18n.get_curr_lang()), %(orig)s, %(tx)s)',
660 'args': {
661 'orig': self._payload[self._idx['type']],
662 'tx': translation
663 }
664 }
665 ],
666 return_data = True
667 )
668 if not rows[0][0]:
669 _log.error('cannot set translation to [%s]' % translation)
670 return False
671
672 return self.refetch_payload()
673
674
676 rows, idx = gmPG2.run_ro_queries (
677 queries = [{'cmd': u"SELECT * FROM blobs.v_doc_type"}],
678 get_col_idx = True
679 )
680 doc_types = []
681 for row in rows:
682 row_def = {
683 'pk_field': 'pk_doc_type',
684 'idx': idx,
685 'data': row
686 }
687 doc_types.append(cDocumentType(row = row_def))
688 return doc_types
689
691
692 cmd = u'select pk from blobs.doc_type where name = %s'
693 rows, idx = gmPG2.run_ro_queries (
694 queries = [{'cmd': cmd, 'args': [document_type]}]
695 )
696 if len(rows) == 0:
697 cmd1 = u"insert into blobs.doc_type (name) values (%s)"
698 cmd2 = u"select currval('blobs.doc_type_pk_seq')"
699 rows, idx = gmPG2.run_rw_queries (
700 queries = [
701 {'cmd': cmd1, 'args': [document_type]},
702 {'cmd': cmd2}
703 ],
704 return_data = True
705 )
706 return cDocumentType(aPK_obj = rows[0][0])
707
709 if document_type['is_in_use']:
710 return False
711 gmPG2.run_rw_queries (
712 queries = [{
713 'cmd': u'delete from blobs.doc_type where pk=%s',
714 'args': [document_type['pk_doc_type']]
715 }]
716 )
717 return True
718
720 """This needs *considerably* more smarts."""
721 dirname = gmTools.get_unique_filename (
722 prefix = '',
723 suffix = time.strftime(".%Y%m%d-%H%M%S", time.localtime())
724 )
725
726 path, doc_ID = os.path.split(dirname)
727 return doc_ID
728
729
730
731 if __name__ == '__main__':
732
733 if len(sys.argv) < 2:
734 sys.exit()
735
736 if sys.argv[1] != u'test':
737 sys.exit()
738
739
741
742 print "----------------------"
743 print "listing document types"
744 print "----------------------"
745
746 for dt in get_document_types():
747 print dt
748
749 print "------------------------------"
750 print "testing document type handling"
751 print "------------------------------"
752
753 dt = create_document_type(document_type = 'dummy doc type for unit test 1')
754 print "created:", dt
755
756 dt['type'] = 'dummy doc type for unit test 2'
757 dt.save_payload()
758 print "changed base name:", dt
759
760 dt.set_translation(translation = 'Dummy-Dokumenten-Typ fuer Unit-Test')
761 print "translated:", dt
762
763 print "deleted:", delete_document_type(document_type = dt)
764
765 return
766
768
769 print "-----------------------"
770 print "testing document import"
771 print "-----------------------"
772
773 docs = search_for_document(patient_id=12)
774 doc = docs[0]
775 print "adding to doc:", doc
776
777 fname = sys.argv[1]
778 print "adding from file:", fname
779 part = doc.add_part(file=fname)
780 print "new part:", part
781
782 return
783
794
795
796 from Gnumed.pycommon import gmI18N
797 gmI18N.activate_locale()
798 gmI18N.install_domain()
799
800
801
802 test_get_documents()
803
804
805
806
807