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