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
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
131
132 - def get_documents(self, doc_type=None, episodes=None, encounter=None):
133 """Return list of documents."""
134
135 args = {
136 'pat': self.pk_patient,
137 'type': doc_type,
138 'enc': encounter
139 }
140 where_parts = [u'pk_patient = %(pat)s']
141
142 if doc_type is not None:
143 try:
144 int(doc_type)
145 where_parts.append(u'pk_type = %(type)s')
146 except (TypeError, ValueError):
147 where_parts.append(u'pk_type = (SELECT pk FROM blobs.doc_type WHERE name = %(type)s)')
148
149 if (episodes is not None) and (len(episodes) > 0):
150 where_parts.append(u'pk_episode IN %(epi)s')
151 args['epi'] = tuple(episodes)
152
153 if encounter is not None:
154 where_parts.append(u'pk_encounter = %(enc)s')
155
156 cmd = u"%s\nORDER BY clin_when" % (_sql_fetch_document_fields % u' AND '.join(where_parts))
157 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
158
159 return [ cMedDoc(row = {'pk_field': 'pk_doc', 'idx': idx, 'data': r}) for r in rows ]
160
161 - def add_document(self, document_type=None, encounter=None, episode=None):
162 return create_document(document_type = document_type, encounter = encounter, episode = episode)
163
164 _sql_fetch_document_part_fields = u"select * from blobs.v_obj4doc_no_data where %s"
165
166 -class cMedDocPart(gmBusinessDBObject.cBusinessDBObject):
167 """Represents one part of a medical document."""
168
169 _cmd_fetch_payload = _sql_fetch_document_part_fields % u"pk_obj = %s"
170 _cmds_store_payload = [
171 u"""update blobs.doc_obj set
172 seq_idx = %(seq_idx)s,
173 comment = gm.nullify_empty_string(%(obj_comment)s),
174 filename = gm.nullify_empty_string(%(filename)s),
175 fk_intended_reviewer = %(pk_intended_reviewer)s
176 where
177 pk=%(pk_obj)s and
178 xmin=%(xmin_doc_obj)s""",
179 u"""select xmin_doc_obj from blobs.v_obj4doc_no_data where pk_obj = %(pk_obj)s"""
180 ]
181 _updatable_fields = [
182 'seq_idx',
183 'obj_comment',
184 'pk_intended_reviewer',
185 'filename'
186 ]
187
188
189
190 - def export_to_file(self, aTempDir = None, aChunkSize = 0, filename=None):
191
192 if self._payload[self._idx['size']] == 0:
193 return None
194
195 if filename is None:
196 suffix = None
197
198 if self._payload[self._idx['filename']] is not None:
199 name, suffix = os.path.splitext(self._payload[self._idx['filename']])
200 suffix = suffix.strip()
201 if suffix == u'':
202 suffix = None
203
204 filename = gmTools.get_unique_filename (
205 prefix = 'gm-doc_obj-page_%s-' % self._payload[self._idx['seq_idx']],
206 suffix = suffix,
207 tmp_dir = aTempDir
208 )
209
210 success = gmPG2.bytea2file (
211 data_query = {
212 'cmd': u'SELECT substring(data from %(start)s for %(size)s) FROM blobs.doc_obj WHERE pk=%(pk)s',
213 'args': {'pk': self.pk_obj}
214 },
215 filename = filename,
216 chunk_size = aChunkSize,
217 data_size = self._payload[self._idx['size']]
218 )
219
220 if success:
221 return filename
222
223 return None
224
226 cmd = u"""
227 select
228 reviewer,
229 reviewed_when,
230 is_technically_abnormal,
231 clinically_relevant,
232 is_review_by_responsible_reviewer,
233 is_your_review,
234 coalesce(comment, '')
235 from blobs.v_reviewed_doc_objects
236 where pk_doc_obj = %s
237 order by
238 is_your_review desc,
239 is_review_by_responsible_reviewer desc,
240 reviewed_when desc
241 """
242 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}])
243 return rows
244
246 return cMedDoc(aPK_obj = self._payload[self._idx['pk_doc']])
247
248
249
251
252 if not (os.access(fname, os.R_OK) and os.path.isfile(fname)):
253 _log.error('[%s] is not a readable file' % fname)
254 return False
255
256 gmPG2.file2bytea (
257 query = u"UPDATE blobs.doc_obj SET data=%(data)s::bytea WHERE pk=%(pk)s",
258 filename = fname,
259 args = {'pk': self.pk_obj}
260 )
261
262
263 self.refetch_payload()
264 return True
265
266 - def set_reviewed(self, technically_abnormal=None, clinically_relevant=None):
267
268 cmd = u"""
269 select pk
270 from blobs.reviewed_doc_objs
271 where
272 fk_reviewed_row = %s and
273 fk_reviewer = (select pk from dem.staff where db_user = current_user)"""
274 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}])
275
276
277 if len(rows) == 0:
278 cols = [
279 u"fk_reviewer",
280 u"fk_reviewed_row",
281 u"is_technically_abnormal",
282 u"clinically_relevant"
283 ]
284 vals = [
285 u'%(fk_row)s',
286 u'%(abnormal)s',
287 u'%(relevant)s'
288 ]
289 args = {
290 'fk_row': self.pk_obj,
291 'abnormal': technically_abnormal,
292 'relevant': clinically_relevant
293 }
294 cmd = u"""
295 insert into blobs.reviewed_doc_objs (
296 %s
297 ) values (
298 (select pk from dem.staff where db_user=current_user),
299 %s
300 )""" % (', '.join(cols), ', '.join(vals))
301
302
303 if len(rows) == 1:
304 pk_row = rows[0][0]
305 args = {
306 'abnormal': technically_abnormal,
307 'relevant': clinically_relevant,
308 'pk_row': pk_row
309 }
310 cmd = u"""
311 update blobs.reviewed_doc_objs set
312 is_technically_abnormal = %(abnormal)s,
313 clinically_relevant = %(relevant)s
314 where
315 pk=%(pk_row)s"""
316 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
317
318 return True
319
321 if self._payload[self._idx['type']] != u'patient photograph':
322 return False
323
324 rows, idx = gmPG2.run_ro_queries (
325 queries = [{
326 'cmd': u'select coalesce(max(seq_idx)+1, 1) from blobs.doc_obj where fk_doc=%(doc_id)s',
327 'args': {'doc_id': self._payload[self._idx['pk_doc']]}
328 }]
329 )
330 self._payload[self._idx['seq_idx']] = rows[0][0]
331 self._is_modified = True
332 self.save_payload()
333
335
336 fname = self.export_to_file(aTempDir = tmpdir, aChunkSize = chunksize)
337 if fname is None:
338 return False, ''
339
340 success, msg = gmMimeLib.call_viewer_on_file(fname, block = block)
341 if not success:
342 return False, msg
343
344 return True, ''
345
346 _sql_fetch_document_fields = u"select * from blobs.v_doc_med where %s"
347
348 -class cMedDoc(gmBusinessDBObject.cBusinessDBObject):
349 """Represents one medical document."""
350
351 _cmd_fetch_payload = _sql_fetch_document_fields % u"pk_doc = %s"
352 _cmds_store_payload = [
353 u"""update blobs.doc_med set
354 fk_type = %(pk_type)s,
355 fk_episode = %(pk_episode)s,
356 clin_when = %(clin_when)s,
357 comment = gm.nullify_empty_string(%(comment)s),
358 ext_ref = gm.nullify_empty_string(%(ext_ref)s)
359 where
360 pk = %(pk_doc)s and
361 xmin = %(xmin_doc_med)s""",
362 u"""select xmin_doc_med from blobs.v_doc_med where pk_doc = %(pk_doc)s"""
363 ]
364
365 _updatable_fields = [
366 'pk_type',
367 'comment',
368 'clin_when',
369 'ext_ref',
370 'pk_episode'
371 ]
372
374 try: del self.__has_unreviewed_parts
375 except AttributeError: pass
376
377 return super(cMedDoc, self).refetch_payload(ignore_changes = ignore_changes)
378
380 """Get document descriptions.
381
382 - will return a list of rows
383 """
384 if max_lng is None:
385 cmd = u"SELECT pk, text FROM blobs.doc_desc WHERE fk_doc = %s"
386 else:
387 cmd = u"SELECT pk, substring(text from 1 for %s) FROM blobs.doc_desc WHERE fk_doc=%%s" % max_lng
388 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}])
389 return rows
390
395
397 cmd = u"update blobs.doc_desc set text = %(desc)s where fk_doc = %(doc)s and pk = %(pk_desc)s"
398 gmPG2.run_rw_queries(queries = [
399 {'cmd': cmd, 'args': {'doc': self.pk_obj, 'pk_desc': pk, 'desc': description}}
400 ])
401 return True
402
404 cmd = u"delete from blobs.doc_desc where fk_doc = %(doc)s and pk = %(desc)s"
405 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'doc': self.pk_obj, 'desc': pk}}])
406 return True
407
412
413 parts = property(_get_parts, lambda x:x)
414
416 """Add a part to the document."""
417
418 cmd = u"""
419 insert into blobs.doc_obj (
420 fk_doc, fk_intended_reviewer, data, seq_idx
421 ) VALUES (
422 %(doc_id)s,
423 (select pk_staff from dem.v_staff where db_user=CURRENT_USER),
424 ''::bytea,
425 (select coalesce(max(seq_idx)+1, 1) from blobs.doc_obj where fk_doc=%(doc_id)s)
426 )"""
427 rows, idx = gmPG2.run_rw_queries (
428 queries = [
429 {'cmd': cmd, 'args': {'doc_id': self.pk_obj}},
430 {'cmd': u"select currval('blobs.doc_obj_pk_seq')"}
431 ],
432 return_data = True
433 )
434
435 pk_part = rows[0][0]
436 new_part = cMedDocPart(aPK_obj = pk_part)
437 if not new_part.update_data_from_file(fname=file):
438 _log.error('cannot import binary data from [%s] into document part' % file)
439 gmPG2.run_rw_queries (
440 queries = [
441 {'cmd': u"delete from blobs.doc_obj where pk = %s", 'args': [pk_part]}
442 ]
443 )
444 return None
445 return new_part
446
448
449 new_parts = []
450
451 for filename in files:
452 new_part = self.add_part(file=filename)
453 if new_part is None:
454 msg = 'cannot instantiate document part object'
455 _log.error(msg)
456 return (False, msg, filename)
457 new_parts.append(new_part)
458
459 new_part['filename'] = filename
460 new_part['pk_intended_reviewer'] = reviewer
461
462 success, data = new_part.save_payload()
463 if not success:
464 msg = 'cannot set reviewer to [%s]' % reviewer
465 _log.error(msg)
466 _log.error(str(data))
467 return (False, msg, filename)
468
469 return (True, '', new_parts)
470
472 fnames = []
473 for part in self.parts:
474
475 fname = os.path.basename(gmTools.coalesce (
476 part['filename'],
477 u'%s%s%s_%s' % (part['l10n_type'], gmTools.coalesce(part['ext_ref'], '-', '-%s-'), _('part'), part['seq_idx'])
478 ))
479 if export_dir is not None:
480 fname = os.path.join(export_dir, fname)
481 fnames.append(part.export_to_file(aChunkSize = chunksize, filename = fname))
482 return fnames
483
485 try:
486 return self.__has_unreviewed_parts
487 except AttributeError:
488 pass
489
490 cmd = u"SELECT EXISTS(SELECT 1 FROM blobs.v_obj4doc_no_data WHERE pk_doc = %(pk)s AND reviewed IS FALSE)"
491 args = {'pk': self.pk_obj}
492 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
493 self.__has_unreviewed_parts = rows[0][0]
494
495 return self.__has_unreviewed_parts
496
497 has_unreviewed_parts = property(_get_has_unreviewed_parts, lambda x:x)
498
499 - def set_reviewed(self, technically_abnormal=None, clinically_relevant=None):
500
501 for part in self.parts:
502 if not part.set_reviewed(technically_abnormal, clinically_relevant):
503 return False
504 return True
505
507 for part in self.parts:
508 part['pk_intended_reviewer'] = reviewer
509 success, data = part.save_payload()
510 if not success:
511 _log.error('cannot set reviewer to [%s]' % reviewer)
512 _log.error(str(data))
513 return False
514 return True
515
517 """Returns new document instance or raises an exception.
518 """
519 cmd1 = u"""insert into blobs.doc_med (fk_type, fk_encounter, fk_episode) VALUES (%(type)s, %(enc)s, %(epi)s)"""
520 cmd2 = u"""select currval('blobs.doc_med_pk_seq')"""
521 rows, idx = gmPG2.run_rw_queries (
522 queries = [
523 {'cmd': cmd1, 'args': {'type': document_type, 'enc': encounter, 'epi': episode}},
524 {'cmd': cmd2}
525 ],
526 return_data = True
527 )
528 doc_id = rows[0][0]
529 doc = cMedDoc(aPK_obj = doc_id)
530 return doc
531
533 """Searches for documents with the given patient and type ID.
534
535 No type ID returns all documents for the patient.
536 """
537
538 if patient_id is None:
539 raise ValueError('need patient id to search for document')
540
541 args = {'pat_id': patient_id, 'type_id': type_id}
542 if type_id is None:
543 cmd = u"SELECT pk_doc from blobs.v_doc_med WHERE pk_patient = %(pat_id)s"
544 else:
545 cmd = u"SELECT pk_doc from blobs.v_doc_med WHERE pk_patient = %(pat_id)s and pk_type = %(type_id)s"
546
547 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
548
549 docs = []
550 for row in rows:
551 docs.append(cMedDoc(row[0]))
552 return docs
553
555
556 cmd = u"select blobs.delete_document(%(pk)s, %(enc)s)"
557 args = {'pk': document_id, 'enc': encounter_id}
558 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
559 return
560
562
563 _log.debug('reclassifying documents by type')
564 _log.debug('original: %s', original_type)
565 _log.debug('target: %s', target_type)
566
567 if target_type['pk_doc_type'] == original_type['pk_doc_type']:
568 return True
569
570 cmd = u"""
571 update blobs.doc_med set
572 fk_type = %(new_type)s
573 where
574 fk_type = %(old_type)s
575 """
576 args = {u'new_type': target_type['pk_doc_type'], u'old_type': original_type['pk_doc_type']}
577
578 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
579
580 return True
581
582
584 """Represents a document type."""
585 _cmd_fetch_payload = u"""select * from blobs.v_doc_type where pk_doc_type=%s"""
586 _cmds_store_payload = [
587 u"""update blobs.doc_type set
588 name = %(type)s
589 where
590 pk=%(pk_obj)s and
591 xmin=%(xmin_doc_type)s""",
592 u"""select xmin_doc_type from blobs.v_doc_type where pk_doc_type = %(pk_obj)s"""
593 ]
594 _updatable_fields = ['type']
595
597
598 if translation.strip() == '':
599 return False
600
601 if translation.strip() == self._payload[self._idx['l10n_type']].strip():
602 return True
603
604 rows, idx = gmPG2.run_rw_queries (
605 queries = [
606 {'cmd': u'select i18n.i18n(%s)', 'args': [self._payload[self._idx['type']]]},
607 {'cmd': u'select i18n.upd_tx((select i18n.get_curr_lang()), %(orig)s, %(tx)s)',
608 'args': {
609 'orig': self._payload[self._idx['type']],
610 'tx': translation
611 }
612 }
613 ],
614 return_data = True
615 )
616 if not rows[0][0]:
617 _log.error('cannot set translation to [%s]' % translation)
618 return False
619
620 return self.refetch_payload()
621
622
624 rows, idx = gmPG2.run_ro_queries (
625 queries = [{'cmd': u"SELECT * FROM blobs.v_doc_type"}],
626 get_col_idx = True
627 )
628 doc_types = []
629 for row in rows:
630 row_def = {
631 'pk_field': 'pk_doc_type',
632 'idx': idx,
633 'data': row
634 }
635 doc_types.append(cDocumentType(row = row_def))
636 return doc_types
637
639
640 cmd = u'select pk from blobs.doc_type where name = %s'
641 rows, idx = gmPG2.run_ro_queries (
642 queries = [{'cmd': cmd, 'args': [document_type]}]
643 )
644 if len(rows) == 0:
645 cmd1 = u"insert into blobs.doc_type (name) values (%s)"
646 cmd2 = u"select currval('blobs.doc_type_pk_seq')"
647 rows, idx = gmPG2.run_rw_queries (
648 queries = [
649 {'cmd': cmd1, 'args': [document_type]},
650 {'cmd': cmd2}
651 ],
652 return_data = True
653 )
654 return cDocumentType(aPK_obj = rows[0][0])
655
657 if document_type['is_in_use']:
658 return False
659 gmPG2.run_rw_queries (
660 queries = [{
661 'cmd': u'delete from blobs.doc_type where pk=%s',
662 'args': [document_type['pk_doc_type']]
663 }]
664 )
665 return True
666
668 """This needs *considerably* more smarts."""
669 dirname = gmTools.get_unique_filename (
670 prefix = '',
671 suffix = time.strftime(".%Y%m%d-%H%M%S", time.localtime())
672 )
673
674 path, doc_ID = os.path.split(dirname)
675 return doc_ID
676
677
678
679 if __name__ == '__main__':
680
681 if len(sys.argv) < 2:
682 sys.exit()
683
684 if sys.argv[1] != u'test':
685 sys.exit()
686
687
689
690 print "----------------------"
691 print "listing document types"
692 print "----------------------"
693
694 for dt in get_document_types():
695 print dt
696
697 print "------------------------------"
698 print "testing document type handling"
699 print "------------------------------"
700
701 dt = create_document_type(document_type = 'dummy doc type for unit test 1')
702 print "created:", dt
703
704 dt['type'] = 'dummy doc type for unit test 2'
705 dt.save_payload()
706 print "changed base name:", dt
707
708 dt.set_translation(translation = 'Dummy-Dokumenten-Typ fuer Unit-Test')
709 print "translated:", dt
710
711 print "deleted:", delete_document_type(document_type = dt)
712
713 return
714
716
717 print "-----------------------"
718 print "testing document import"
719 print "-----------------------"
720
721 docs = search_for_document(patient_id=12)
722 doc = docs[0]
723 print "adding to doc:", doc
724
725 fname = sys.argv[1]
726 print "adding from file:", fname
727 part = doc.add_part(file=fname)
728 print "new part:", part
729
730 return
731
743
744 from Gnumed.pycommon import gmI18N
745 gmI18N.activate_locale()
746 gmI18N.install_domain()
747
748
749
750 test_get_documents()
751
752
753
754
755