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