1
2 """GNUmed patient objects.
3
4 This is a patient object intended to let a useful client-side
5 API crystallize from actual use in true XP fashion.
6 """
7
8 __version__ = "$Revision: 1.198 $"
9 __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
10 __license__ = "GPL"
11
12
13 import sys, os.path, time, re as regex, string, types, datetime as pyDT, codecs, threading, logging
14
15
16
17 if __name__ == '__main__':
18 sys.path.insert(0, '../../')
19 from Gnumed.pycommon import gmExceptions, gmDispatcher, gmBorg, gmI18N, gmNull, gmBusinessDBObject, gmTools
20 from Gnumed.pycommon import gmPG2
21 from Gnumed.pycommon import gmDateTime
22 from Gnumed.pycommon import gmMatchProvider
23 from Gnumed.pycommon import gmLog2
24 from Gnumed.pycommon import gmHooks
25
26 from Gnumed.business import gmDemographicRecord
27 from Gnumed.business import gmClinicalRecord
28 from Gnumed.business import gmXdtMappings
29 from Gnumed.business import gmProviderInbox
30 from Gnumed.business.gmDocuments import cDocumentFolder
31
32
33 _log = logging.getLogger('gm.person')
34 _log.info(__version__)
35
36 __gender_list = None
37 __gender_idx = None
38
39 __gender2salutation_map = None
40 __gender2string_map = None
41
42
43
45
51
52
53
55 return 'firstnames lastnames dob gender'.split()
56
59
61 """Generate generic queries.
62
63 - not locale dependant
64 - data -> firstnames, lastnames, dob, gender
65
66 shall we mogrify name parts ? probably not as external
67 sources should know what they do
68
69 finds by inactive name, too, but then shows
70 the corresponding active name ;-)
71
72 Returns list of matching identities (may be empty)
73 or None if it was told to create an identity but couldn't.
74 """
75 where_snippets = []
76 args = {}
77
78 where_snippets.append(u'firstnames = %(first)s')
79 args['first'] = self.firstnames
80
81 where_snippets.append(u'lastnames = %(last)s')
82 args['last'] = self.lastnames
83
84 if self.dob is not None:
85 where_snippets.append(u"dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %(dob)s)")
86 args['dob'] = self.dob.replace(hour = 23, minute = 59, second = 59)
87
88 if self.gender is not None:
89 where_snippets.append('gender = %(sex)s')
90 args['sex'] = self.gender
91
92 cmd = u"""
93 SELECT *, '%s' AS match_type
94 FROM dem.v_basic_person
95 WHERE
96 pk_identity IN (
97 SELECT pk_identity FROM dem.v_person_names WHERE %s
98 )
99 ORDER BY lastnames, firstnames, dob""" % (
100 _('external patient source (name, gender, date of birth)'),
101 ' AND '.join(where_snippets)
102 )
103
104 try:
105 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx=True)
106 except:
107 _log.error(u'cannot get candidate identities for dto "%s"' % self)
108 _log.exception('query %s' % cmd)
109 rows = []
110
111 if len(rows) == 0:
112 _log.debug('no candidate identity matches found')
113 if not can_create:
114 return []
115 ident = self.import_into_database()
116 if ident is None:
117 return None
118 identities = [ident]
119 else:
120 identities = [ cIdentity(row = {'pk_field': 'pk_identity', 'data': row, 'idx': idx}) for row in rows ]
121
122 return identities
123
125 """Imports self into the database."""
126
127 self.identity = create_identity (
128 firstnames = self.firstnames,
129 lastnames = self.lastnames,
130 gender = self.gender,
131 dob = self.dob
132 )
133
134 if self.identity is None:
135 return None
136
137 for ext_id in self.external_ids:
138 try:
139 self.identity.add_external_id (
140 type_name = ext_id['name'],
141 value = ext_id['value'],
142 issuer = ext_id['issuer'],
143 comment = ext_id['comment']
144 )
145 except StandardError:
146 _log.exception('cannot import <external ID> from external data source')
147 _log.log_stack_trace()
148
149 for comm in self.comm_channels:
150 try:
151 self.identity.link_comm_channel (
152 comm_medium = comm['channel'],
153 url = comm['url']
154 )
155 except StandardError:
156 _log.exception('cannot import <comm channel> from external data source')
157 _log.log_stack_trace()
158
159 for adr in self.addresses:
160 try:
161 self.identity.link_address (
162 number = adr['number'],
163 street = adr['street'],
164 postcode = adr['zip'],
165 urb = adr['urb'],
166 state = adr['region'],
167 country = adr['country']
168 )
169 except StandardError:
170 _log.exception('cannot import <address> from external data source')
171 _log.log_stack_trace()
172
173 return self.identity
174
177
179 value = value.strip()
180 if value == u'':
181 return
182 name = name.strip()
183 if name == u'':
184 raise ValueError(_('<name> cannot be empty'))
185 issuer = issuer.strip()
186 if issuer == u'':
187 raise ValueError(_('<issuer> cannot be empty'))
188 self.external_ids.append({'name': name, 'value': value, 'issuer': issuer, 'comment': comment})
189
191 url = url.strip()
192 if url == u'':
193 return
194 channel = channel.strip()
195 if channel == u'':
196 raise ValueError(_('<channel> cannot be empty'))
197 self.comm_channels.append({'channel': channel, 'url': url})
198
199 - def remember_address(self, number=None, street=None, urb=None, region=None, zip=None, country=None):
200 number = number.strip()
201 if number == u'':
202 raise ValueError(_('<number> cannot be empty'))
203 street = street.strip()
204 if street == u'':
205 raise ValueError(_('<street> cannot be empty'))
206 urb = urb.strip()
207 if urb == u'':
208 raise ValueError(_('<urb> cannot be empty'))
209 zip = zip.strip()
210 if zip == u'':
211 raise ValueError(_('<zip> cannot be empty'))
212 country = country.strip()
213 if country == u'':
214 raise ValueError(_('<country> cannot be empty'))
215 region = region.strip()
216 if region == u'':
217 region = u'??'
218 self.addresses.append ({
219 u'number': number,
220 u'street': street,
221 u'zip': zip,
222 u'urb': urb,
223 u'region': region,
224 u'country': country
225 })
226
227
228
230 return u'<%s @ %s: %s %s (%s) %s>' % (
231 self.__class__.__name__,
232 id(self),
233 self.firstnames,
234 self.lastnames,
235 self.gender,
236 self.dob
237 )
238
240 """Do some sanity checks on self.* access."""
241
242 if attr == 'gender':
243 glist, idx = get_gender_list()
244 for gender in glist:
245 if str(val) in [gender[0], gender[1], gender[2], gender[3]]:
246 val = gender[idx['tag']]
247 object.__setattr__(self, attr, val)
248 return
249 raise ValueError('invalid gender: [%s]' % val)
250
251 if attr == 'dob':
252 if val is not None:
253 if not isinstance(val, pyDT.datetime):
254 raise TypeError('invalid type for DOB (must be datetime.datetime): %s [%s]' % (type(val), val))
255 if val.tzinfo is None:
256 raise ValueError('datetime.datetime instance is lacking a time zone: [%s]' % val.isoformat())
257
258 object.__setattr__(self, attr, val)
259 return
260
262 return getattr(self, attr)
263
264 -class cPersonName(gmBusinessDBObject.cBusinessDBObject):
265 _cmd_fetch_payload = u"SELECT * FROM dem.v_person_names WHERE pk_name = %s"
266 _cmds_store_payload = [
267 u"""UPDATE dem.names SET
268 active = FALSE
269 WHERE
270 %(active_name)s IS TRUE -- act only when needed and only
271 AND
272 id_identity = %(pk_identity)s -- on names of this identity
273 AND
274 active IS TRUE -- which are active
275 AND
276 id != %(pk_name)s -- but NOT *this* name
277 """,
278 u"""update dem.names set
279 active = %(active_name)s,
280 preferred = %(preferred)s,
281 comment = %(comment)s
282 where
283 id = %(pk_name)s and
284 id_identity = %(pk_identity)s and -- belt and suspenders
285 xmin = %(xmin_name)s""",
286 u"""select xmin as xmin_name from dem.names where id = %(pk_name)s"""
287 ]
288 _updatable_fields = ['active_name', 'preferred', 'comment']
289
298
300 return '%(last)s, %(title)s %(first)s%(nick)s' % {
301 'last': self._payload[self._idx['lastnames']],
302 'title': gmTools.coalesce (
303 self._payload[self._idx['title']],
304 map_gender2salutation(self._payload[self._idx['gender']])
305 ),
306 'first': self._payload[self._idx['firstnames']],
307 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], u'', u" '%s'", u'%s')
308 }
309
310 description = property(_get_description, lambda x:x)
311
312 -class cIdentity(gmBusinessDBObject.cBusinessDBObject):
313 _cmd_fetch_payload = u"SELECT * FROM dem.v_basic_person WHERE pk_identity = %s"
314 _cmds_store_payload = [
315 u"""UPDATE dem.identity SET
316 gender = %(gender)s,
317 dob = %(dob)s,
318 dob_is_estimated = %(dob_is_estimated)s,
319 tob = %(tob)s,
320 cob = gm.nullify_empty_string(%(cob)s),
321 title = gm.nullify_empty_string(%(title)s),
322 fk_marital_status = %(pk_marital_status)s,
323 karyotype = gm.nullify_empty_string(%(karyotype)s),
324 pupic = gm.nullify_empty_string(%(pupic)s),
325 deceased = %(deceased)s,
326 emergency_contact = gm.nullify_empty_string(%(emergency_contact)s),
327 fk_emergency_contact = %(pk_emergency_contact)s,
328 fk_primary_provider = %(pk_primary_provider)s,
329 comment = gm.nullify_empty_string(%(comment)s)
330 WHERE
331 pk = %(pk_identity)s and
332 xmin = %(xmin_identity)s
333 RETURNING
334 xmin AS xmin_identity"""
335 ]
336 _updatable_fields = [
337 "title",
338 "dob",
339 "tob",
340 "cob",
341 "gender",
342 "pk_marital_status",
343 "karyotype",
344 "pupic",
345 'deceased',
346 'emergency_contact',
347 'pk_emergency_contact',
348 'pk_primary_provider',
349 'comment',
350 'dob_is_estimated'
351 ]
352
354 return self._payload[self._idx['pk_identity']]
356 raise AttributeError('setting ID of identity is not allowed')
357 ID = property(_get_ID, _set_ID)
358
360
361 if attribute == 'dob':
362 if value is not None:
363
364 if isinstance(value, pyDT.datetime):
365 if value.tzinfo is None:
366 raise ValueError('datetime.datetime instance is lacking a time zone: [%s]' % dt.isoformat())
367 else:
368 raise TypeError, '[%s]: type [%s] (%s) invalid for attribute [dob], must be datetime.datetime or None' % (self.__class__.__name__, type(value), value)
369
370
371 if self._payload[self._idx['dob']] is not None:
372 old_dob = gmDateTime.pydt_strftime (
373 self._payload[self._idx['dob']],
374 format = '%Y %m %d %H %M %S',
375 accuracy = gmDateTime.acc_seconds
376 )
377 new_dob = gmDateTime.pydt_strftime (
378 value,
379 format = '%Y %m %d %H %M %S',
380 accuracy = gmDateTime.acc_seconds
381 )
382 if new_dob == old_dob:
383 return
384
385 gmBusinessDBObject.cBusinessDBObject.__setitem__(self, attribute, value)
386
389
391 cmd = u"""
392 SELECT EXISTS (
393 SELECT 1
394 FROM clin.v_emr_journal
395 WHERE
396 pk_patient = %(pat)s
397 AND
398 soap_cat IS NOT NULL
399 )"""
400 args = {'pat': self._payload[self._idx['pk_identity']]}
401 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
402 return rows[0][0]
403
405 raise AttributeError('setting is_patient status of identity is not allowed')
406
407 is_patient = property(_get_is_patient, _set_is_patient)
408
410 cmd = u"SELECT pk FROM dem.staff WHERE fk_identity = %(pk)s"
411 args = {'pk': self._payload[self._idx['pk_identity']]}
412 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
413 if len(rows) == 0:
414 return None
415 return rows[0][0]
416
417 staff_id = property(_get_staff_id, lambda x:x)
418
419
420
423
424 gender_symbol = property(_get_gender_symbol, lambda x:x)
425
428
429 gender_string = property(_get_gender_string, lambda x:x)
430
432 names = self.get_names(active_only = True)
433 if len(names) == 0:
434 _log.error('cannot retrieve active name for patient [%s]', self._payload[self._idx['pk_identity']])
435 return None
436 return names[0]
437
438 active_name = property(get_active_name, lambda x:x)
439
440 - def get_names(self, active_only=False, exclude_active=False):
441
442 args = {'pk_pat': self._payload[self._idx['pk_identity']]}
443 where_parts = [u'pk_identity = %(pk_pat)s']
444 if active_only:
445 where_parts.append(u'active_name is True')
446 if exclude_active:
447 where_parts.append(u'active_name is False')
448 cmd = u"""
449 SELECT *
450 FROM dem.v_person_names
451 WHERE %s
452 ORDER BY active_name DESC, lastnames, firstnames
453 """ % u' AND '.join(where_parts)
454 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
455
456 if len(rows) == 0:
457
458 return []
459
460 names = [ cPersonName(row = {'idx': idx, 'data': r, 'pk_field': 'pk_name'}) for r in rows ]
461 return names
462
464 return _(u'%(last)s,%(title)s %(first)s%(nick)s (%(sex)s)') % {
465 'last': self._payload[self._idx['lastnames']],
466 'title': gmTools.coalesce(self._payload[self._idx['title']], u'', u' %s'),
467 'first': self._payload[self._idx['firstnames']],
468 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], u'', u" '%s'"),
469 'sex': self.gender_symbol
470 }
471
473 return _(u'%(last)s,%(title)s %(first)s%(nick)s') % {
474 'last': self._payload[self._idx['lastnames']],
475 'title': gmTools.coalesce(self._payload[self._idx['title']], u'', u' %s'),
476 'first': self._payload[self._idx['firstnames']],
477 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], u'', u" '%s'")
478 }
479
480 - def add_name(self, firstnames, lastnames, active=True):
481 """Add a name.
482
483 @param firstnames The first names.
484 @param lastnames The last names.
485 @param active When True, the new name will become the active one (hence setting other names to inactive)
486 @type active A types.BooleanType instance
487 """
488 name = create_name(self.ID, firstnames, lastnames, active)
489 if active:
490 self.refetch_payload()
491 return name
492
494 cmd = u"delete from dem.names where id = %(name)s and id_identity = %(pat)s"
495 args = {'name': name['pk_name'], 'pat': self.ID}
496 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
497
498
499
500
502 """
503 Set the nickname. Setting the nickname only makes sense for the currently
504 active name.
505 @param nickname The preferred/nick/warrior name to set.
506 """
507 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': u"select dem.set_nickname(%s, %s)", 'args': [self.ID, nickname]}])
508 self.refetch_payload()
509 return True
510
521
522 tags = property(get_tags, lambda x:x)
523
525 args = {
526 u'tag': tag,
527 u'identity': self.ID
528 }
529
530
531 cmd = u"SELECT pk FROM dem.identity_tag WHERE fk_tag = %(tag)s AND fk_identity = %(identity)s"
532 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
533 if len(rows) > 0:
534 return gmDemographicRecord.cIdentityTag(aPK_obj = rows[0]['pk'])
535
536
537 cmd = u"""
538 INSERT INTO dem.identity_tag (
539 fk_tag,
540 fk_identity
541 ) VALUES (
542 %(tag)s,
543 %(identity)s
544 )
545 RETURNING pk
546 """
547 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
548 return gmDemographicRecord.cIdentityTag(aPK_obj = rows[0]['pk'])
549
551 cmd = u"DELETE FROM dem.identity_tag WHERE pk = %(pk)s"
552 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': tag}}])
553
554
555
556
557
558
559
560
561
562
563 - def add_external_id(self, type_name=None, value=None, issuer=None, comment=None, pk_type=None):
564 """Adds an external ID to the patient.
565
566 creates ID type if necessary
567 """
568
569
570 if pk_type is not None:
571 cmd = u"""
572 select * from dem.v_external_ids4identity where
573 pk_identity = %(pat)s and
574 pk_type = %(pk_type)s and
575 value = %(val)s"""
576 else:
577
578 if issuer is None:
579 cmd = u"""
580 select * from dem.v_external_ids4identity where
581 pk_identity = %(pat)s and
582 name = %(name)s and
583 value = %(val)s"""
584 else:
585 cmd = u"""
586 select * from dem.v_external_ids4identity where
587 pk_identity = %(pat)s and
588 name = %(name)s and
589 value = %(val)s and
590 issuer = %(issuer)s"""
591 args = {
592 'pat': self.ID,
593 'name': type_name,
594 'val': value,
595 'issuer': issuer,
596 'pk_type': pk_type
597 }
598 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
599
600
601 if len(rows) == 0:
602
603 args = {
604 'pat': self.ID,
605 'val': value,
606 'type_name': type_name,
607 'pk_type': pk_type,
608 'issuer': issuer,
609 'comment': comment
610 }
611
612 if pk_type is None:
613 cmd = u"""insert into dem.lnk_identity2ext_id (external_id, fk_origin, comment, id_identity) values (
614 %(val)s,
615 (select dem.add_external_id_type(%(type_name)s, %(issuer)s)),
616 %(comment)s,
617 %(pat)s
618 )"""
619 else:
620 cmd = u"""insert into dem.lnk_identity2ext_id (external_id, fk_origin, comment, id_identity) values (
621 %(val)s,
622 %(pk_type)s,
623 %(comment)s,
624 %(pat)s
625 )"""
626
627 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
628
629
630 else:
631 row = rows[0]
632 if comment is not None:
633
634 if gmTools.coalesce(row['comment'], '').find(comment.strip()) == -1:
635 comment = '%s%s' % (gmTools.coalesce(row['comment'], '', '%s // '), comment.strip)
636 cmd = u"update dem.lnk_identity2ext_id set comment = %(comment)s where id=%(pk)s"
637 args = {'comment': comment, 'pk': row['pk_id']}
638 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
639
640 - def update_external_id(self, pk_id=None, type=None, value=None, issuer=None, comment=None):
641 """Edits an existing external ID.
642
643 Creates ID type if necessary.
644 """
645 cmd = u"""
646 UPDATE dem.lnk_identity2ext_id SET
647 fk_origin = (SELECT dem.add_external_id_type(%(type)s, %(issuer)s)),
648 external_id = %(value)s,
649 comment = gm.nullify_empty_string(%(comment)s)
650 WHERE
651 id = %(pk)s
652 """
653 args = {'pk': pk_id, 'value': value, 'type': type, 'issuer': issuer, 'comment': comment}
654 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
655
657 where_parts = ['pk_identity = %(pat)s']
658 args = {'pat': self.ID}
659
660 if id_type is not None:
661 where_parts.append(u'name = %(name)s')
662 args['name'] = id_type.strip()
663
664 if issuer is not None:
665 where_parts.append(u'issuer = %(issuer)s')
666 args['issuer'] = issuer.strip()
667
668 cmd = u"SELECT * FROM dem.v_external_ids4identity WHERE %s" % ' AND '.join(where_parts)
669 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
670
671 return rows
672
673 external_ids = property(get_external_ids, lambda x:x)
674
676 cmd = u"""
677 delete from dem.lnk_identity2ext_id
678 where id_identity = %(pat)s and id = %(pk)s"""
679 args = {'pat': self.ID, 'pk': pk_ext_id}
680 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
681
683 """Merge another identity into this one.
684
685 Keep this one. Delete other one."""
686
687 if other_identity.ID == self.ID:
688 return True, None
689
690 curr_pat = gmCurrentPatient()
691 if curr_pat.connected:
692 if other_identity.ID == curr_pat.ID:
693 return False, _('Cannot merge active patient into another patient.')
694
695 queries = []
696 args = {'old_pat': other_identity.ID, 'new_pat': self.ID}
697
698
699 queries.append ({
700 'cmd': u'delete from clin.allergy_state where pk = (select pk_allergy_state from clin.v_pat_allergy_state where pk_patient = %(old_pat)s)',
701 'args': args
702 })
703
704
705
706 queries.append ({
707 'cmd': u'update dem.names set active = False where id_identity = %(old_pat)s',
708 'args': args
709 })
710
711
712 FKs = gmPG2.get_foreign_keys2column (
713 schema = u'dem',
714 table = u'identity',
715 column = u'pk'
716 )
717
718
719 cmd_template = u'update %s set %s = %%(new_pat)s where %s = %%(old_pat)s'
720 for FK in FKs:
721 queries.append ({
722 'cmd': cmd_template % (FK['referencing_table'], FK['referencing_column'], FK['referencing_column']),
723 'args': args
724 })
725
726
727 queries.append ({
728 'cmd': u'delete from dem.identity where pk = %(old_pat)s',
729 'args': args
730 })
731
732 _log.warning('identity [%s] is about to assimilate identity [%s]', self.ID, other_identity.ID)
733
734 gmPG2.run_rw_queries(link_obj = link_obj, queries = queries, end_tx = True)
735
736 self.add_external_id (
737 type_name = u'merged GNUmed identity primary key',
738 value = u'GNUmed::pk::%s' % other_identity.ID,
739 issuer = u'GNUmed'
740 )
741
742 return True, None
743
744
746 cmd = u"""
747 insert into clin.waiting_list (fk_patient, urgency, comment, area, list_position)
748 values (
749 %(pat)s,
750 %(urg)s,
751 %(cmt)s,
752 %(area)s,
753 (select coalesce((max(list_position) + 1), 1) from clin.waiting_list)
754 )"""
755 args = {'pat': self.ID, 'urg': urgency, 'cmt': comment, 'area': zone}
756 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], verbose = True)
757
759 cmd = u"""SELECT * FROM clin.v_waiting_list WHERE pk_identity = %(pat)s"""
760 args = {'pat': self.ID}
761 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
762 return rows
763
764 waiting_list_entries = property(get_waiting_list_entry, lambda x:x)
765
766 - def export_as_gdt(self, filename=None, encoding='iso-8859-15', external_id_type=None):
767
768 template = u'%s%s%s\r\n'
769
770 file = codecs.open (
771 filename = filename,
772 mode = 'wb',
773 encoding = encoding,
774 errors = 'strict'
775 )
776
777 file.write(template % (u'013', u'8000', u'6301'))
778 file.write(template % (u'013', u'9218', u'2.10'))
779 if external_id_type is None:
780 file.write(template % (u'%03d' % (9 + len(str(self.ID))), u'3000', self.ID))
781 else:
782 ext_ids = self.get_external_ids(id_type = external_id_type)
783 if len(ext_ids) > 0:
784 file.write(template % (u'%03d' % (9 + len(ext_ids[0]['value'])), u'3000', ext_ids[0]['value']))
785 file.write(template % (u'%03d' % (9 + len(self._payload[self._idx['lastnames']])), u'3101', self._payload[self._idx['lastnames']]))
786 file.write(template % (u'%03d' % (9 + len(self._payload[self._idx['firstnames']])), u'3102', self._payload[self._idx['firstnames']]))
787 file.write(template % (u'%03d' % (9 + len(self._payload[self._idx['dob']].strftime('%d%m%Y'))), u'3103', self._payload[self._idx['dob']].strftime('%d%m%Y')))
788 file.write(template % (u'010', u'3110', gmXdtMappings.map_gender_gm2xdt[self._payload[self._idx['gender']]]))
789 file.write(template % (u'025', u'6330', 'GNUmed::9206::encoding'))
790 file.write(template % (u'%03d' % (9 + len(encoding)), u'6331', encoding))
791 if external_id_type is None:
792 file.write(template % (u'029', u'6332', u'GNUmed::3000::source'))
793 file.write(template % (u'017', u'6333', u'internal'))
794 else:
795 if len(ext_ids) > 0:
796 file.write(template % (u'029', u'6332', u'GNUmed::3000::source'))
797 file.write(template % (u'%03d' % (9 + len(external_id_type)), u'6333', external_id_type))
798
799 file.close()
800
801
802
805
807 """Link an occupation with a patient, creating the occupation if it does not exists.
808
809 @param occupation The name of the occupation to link the patient to.
810 """
811 if (activities is None) and (occupation is None):
812 return True
813
814 occupation = occupation.strip()
815 if len(occupation) == 0:
816 return True
817
818 if activities is not None:
819 activities = activities.strip()
820
821 args = {'act': activities, 'pat_id': self.pk_obj, 'job': occupation}
822
823 cmd = u"select activities from dem.v_person_jobs where pk_identity = %(pat_id)s and l10n_occupation = _(%(job)s)"
824 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
825
826 queries = []
827 if len(rows) == 0:
828 queries.append ({
829 'cmd': u"INSERT INTO dem.lnk_job2person (fk_identity, fk_occupation, activities) VALUES (%(pat_id)s, dem.create_occupation(%(job)s), %(act)s)",
830 'args': args
831 })
832 else:
833 if rows[0]['activities'] != activities:
834 queries.append ({
835 'cmd': u"update dem.lnk_job2person set activities=%(act)s where fk_identity=%(pat_id)s and fk_occupation=(select id from dem.occupation where _(name) = _(%(job)s))",
836 'args': args
837 })
838
839 rows, idx = gmPG2.run_rw_queries(queries = queries)
840
841 return True
842
844 if occupation is None:
845 return True
846 occupation = occupation.strip()
847 cmd = u"delete from dem.lnk_job2person where fk_identity=%(pk)s and fk_occupation in (select id from dem.occupation where _(name) = _(%(job)s))"
848 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj, 'job': occupation}}])
849 return True
850
851
852
854 cmd = u"select * from dem.v_person_comms where pk_identity = %s"
855 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}], get_col_idx = True)
856
857 filtered = rows
858
859 if comm_medium is not None:
860 filtered = []
861 for row in rows:
862 if row['comm_type'] == comm_medium:
863 filtered.append(row)
864
865 return [ gmDemographicRecord.cCommChannel(row = {
866 'pk_field': 'pk_lnk_identity2comm',
867 'data': r,
868 'idx': idx
869 }) for r in filtered
870 ]
871
872 - def link_comm_channel(self, comm_medium=None, url=None, is_confidential=False, pk_channel_type=None):
873 """Link a communication medium with a patient.
874
875 @param comm_medium The name of the communication medium.
876 @param url The communication resource locator.
877 @type url A types.StringType instance.
878 @param is_confidential Wether the data must be treated as confidential.
879 @type is_confidential A types.BooleanType instance.
880 """
881 comm_channel = gmDemographicRecord.create_comm_channel (
882 comm_medium = comm_medium,
883 url = url,
884 is_confidential = is_confidential,
885 pk_channel_type = pk_channel_type,
886 pk_identity = self.pk_obj
887 )
888 return comm_channel
889
895
896
897
899
900 cmd = u"SELECT * FROM dem.v_pat_addresses WHERE pk_identity = %(pat)s"
901 args = {'pat': self.pk_obj}
902 if address_type is not None:
903 cmd = cmd + u" AND address_type = %(typ)s"
904 args['typ'] = address_type
905
906 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
907
908 return [
909 gmDemographicRecord.cPatientAddress(row = {'idx': idx, 'data': r, 'pk_field': 'pk_address'})
910 for r in rows
911 ]
912
913 - def link_address(self, number=None, street=None, postcode=None, urb=None, state=None, country=None, subunit=None, suburb=None, id_type=None, address=None):
914 """Link an address with a patient, creating the address if it does not exists.
915
916 @param number The number of the address.
917 @param street The name of the street.
918 @param postcode The postal code of the address.
919 @param urb The name of town/city/etc.
920 @param state The code of the state.
921 @param country The code of the country.
922 @param id_type The primary key of the address type.
923 """
924 if address is None:
925
926 address = gmDemographicRecord.create_address (
927 country = country,
928 state = state,
929 urb = urb,
930 suburb = suburb,
931 postcode = postcode,
932 street = street,
933 number = number,
934 subunit = subunit
935 )
936
937 if address is None:
938 return None
939
940
941 cmd = u"SELECT * FROM dem.lnk_person_org_address WHERE id_identity = %(pat)s AND id_address = %(adr)s"
942 args = {'pat': self.pk_obj, 'adr': address['pk_address']}
943 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
944
945
946 if len(rows) == 0:
947 args = {'id': self.pk_obj, 'adr': address['pk_address'], 'type': id_type}
948 cmd = u"""
949 INSERT INTO dem.lnk_person_org_address(id_identity, id_address)
950 VALUES (%(id)s, %(adr)s)
951 RETURNING *"""
952 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True)
953
954
955 if id_type is not None:
956 r = rows[0]
957 if r['id_type'] != id_type:
958 cmd = "UPDATE dem.lnk_person_org_address SET id_type = %(type)s WHERE id = %(id)s"
959 args = {'type': id_type, 'id': r['id']}
960 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
961
962 return address
963
965 """Remove an address from the patient.
966
967 The address itself stays in the database.
968 The address can be either cAdress or cPatientAdress.
969 """
970 if pk_address is None:
971 args = {'person': self.pk_obj, 'adr': address['pk_address']}
972 else:
973 args = {'person': self.pk_obj, 'adr': pk_address}
974 cmd = u"DELETE FROM dem.lnk_person_org_address WHERE id_identity = %(person)s AND id_address = %(adr)s"
975 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
976
977
978
980 cmd = u"""
981 select
982 t.description,
983 vbp.pk_identity as id,
984 title,
985 firstnames,
986 lastnames,
987 dob,
988 cob,
989 gender,
990 karyotype,
991 pupic,
992 pk_marital_status,
993 marital_status,
994 xmin_identity,
995 preferred
996 from
997 dem.v_basic_person vbp, dem.relation_types t, dem.lnk_person2relative l
998 where
999 (
1000 l.id_identity = %(pk)s and
1001 vbp.pk_identity = l.id_relative and
1002 t.id = l.id_relation_type
1003 ) or (
1004 l.id_relative = %(pk)s and
1005 vbp.pk_identity = l.id_identity and
1006 t.inverse = l.id_relation_type
1007 )"""
1008 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}])
1009 if len(rows) == 0:
1010 return []
1011 return [(row[0], cIdentity(row = {'data': row[1:], 'idx':idx, 'pk_field': 'pk'})) for row in rows]
1012
1014
1015 id_new_relative = create_dummy_identity()
1016
1017 relative = cIdentity(aPK_obj=id_new_relative)
1018
1019
1020 relative.add_name( '**?**', self.get_names()['lastnames'])
1021
1022 if self._ext_cache.has_key('relatives'):
1023 del self._ext_cache['relatives']
1024 cmd = u"""
1025 insert into dem.lnk_person2relative (
1026 id_identity, id_relative, id_relation_type
1027 ) values (
1028 %s, %s, (select id from dem.relation_types where description = %s)
1029 )"""
1030 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': [self.ID, id_new_relative, rel_type ]}])
1031 return True
1032
1034
1035 self.set_relative(None, relation)
1036
1041
1042 emergency_contact_in_database = property(_get_emergency_contact_from_database, lambda x:x)
1043
1044
1045
1054
1095
1096 - def dob_in_range(self, min_distance=u'1 week', max_distance=u'1 week'):
1097 cmd = u'select dem.dob_is_in_range(%(dob)s, %(min)s, %(max)s)'
1098 rows, idx = gmPG2.run_ro_queries (
1099 queries = [{
1100 'cmd': cmd,
1101 'args': {'dob': self['dob'], 'min': min_distance, 'max': max_distance}
1102 }]
1103 )
1104 return rows[0][0]
1105
1106
1107
1109 cmd = u'select * from clin.v_most_recent_encounters where pk_patient=%s'
1110 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self._payload[self._idx['pk_identity']]]}])
1111 if len(rows) > 0:
1112 return rows[0]
1113 else:
1114 return None
1115
1118
1119 messages = property(_get_messages, lambda x:x)
1120
1123
1124 due_messages = property(_get_due_messages, lambda x:x)
1125
1128
1131
1132 dynamic_hints = property(_get_dynamic_hints, lambda x:x)
1133
1135 if self._payload[self._idx['pk_primary_provider']] is None:
1136 return None
1137 from Gnumed.business import gmStaff
1138 return gmStaff.cStaff(aPK_obj = self._payload[self._idx['pk_primary_provider']])
1139
1140 primary_provider = property(_get_primary_provider, lambda x:x)
1141
1142
1143
1145 """Format patient demographics into patient specific path name fragment."""
1146 return '%s-%s%s-%s' % (
1147 self._payload[self._idx['lastnames']].replace(u' ', u'_'),
1148 self._payload[self._idx['firstnames']].replace(u' ', u'_'),
1149 gmTools.coalesce(self._payload[self._idx['preferred']], u'', template_initial = u'-(%s)'),
1150 self.get_formatted_dob(format = '%Y-%m-%d', encoding = gmI18N.get_encoding())
1151 )
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1170 """Represents a person which is a patient.
1171
1172 - a specializing subclass of cIdentity turning it into a patient
1173 - its use is to cache subobjects like EMR and document folder
1174 """
1175 - def __init__(self, aPK_obj=None, row=None):
1176 cIdentity.__init__(self, aPK_obj=aPK_obj, row=row)
1177 self.__db_cache = {}
1178 self.__emr_access_lock = threading.Lock()
1179
1181 """Do cleanups before dying.
1182
1183 - note that this may be called in a thread
1184 """
1185 if self.__db_cache.has_key('clinical record'):
1186 self.__db_cache['clinical record'].cleanup()
1187 if self.__db_cache.has_key('document folder'):
1188 self.__db_cache['document folder'].cleanup()
1189 cIdentity.cleanup(self)
1190
1192 if not self.__emr_access_lock.acquire(False):
1193
1194 _log.debug('failed to acquire EMR access lock, sleeping for 500ms')
1195 time.sleep(0.5)
1196 if not self.__emr_access_lock.acquire(False):
1197 _log.debug('still failed to acquire EMR access lock, aborting')
1198 raise AttributeError('cannot lock access to EMR')
1199 try:
1200 self.__db_cache['clinical record']
1201 except KeyError:
1202 self.__db_cache['clinical record'] = gmClinicalRecord.cClinicalRecord(aPKey = self._payload[self._idx['pk_identity']])
1203 self.__emr_access_lock.release()
1204 return self.__db_cache['clinical record']
1205
1206 emr = property(get_emr, lambda x:x)
1207
1209 try:
1210 return self.__db_cache['document folder']
1211 except KeyError:
1212 pass
1213
1214 self.__db_cache['document folder'] = cDocumentFolder(aPKey = self._payload[self._idx['pk_identity']])
1215 return self.__db_cache['document folder']
1216
1217 document_folder = property(get_document_folder, lambda x:x)
1218
1220 """Patient Borg to hold currently active patient.
1221
1222 There may be many instances of this but they all share state.
1223 """
1224 - def __init__(self, patient=None, forced_reload=False):
1225 """Change or get currently active patient.
1226
1227 patient:
1228 * None: get currently active patient
1229 * -1: unset currently active patient
1230 * cPatient instance: set active patient if possible
1231 """
1232
1233 try:
1234 tmp = self.patient
1235 except AttributeError:
1236 self.patient = gmNull.cNull()
1237 self.__register_interests()
1238
1239
1240
1241 self.__lock_depth = 0
1242
1243 self.__pre_selection_callbacks = []
1244
1245
1246 if patient is None:
1247 return None
1248
1249
1250 if self.locked:
1251 _log.error('patient [%s] is locked, cannot change to [%s]' % (self.patient['pk_identity'], patient))
1252 return None
1253
1254
1255 if patient == -1:
1256 _log.debug('explicitly unsetting current patient')
1257 if not self.__run_pre_selection_callbacks():
1258 _log.debug('not unsetting current patient')
1259 return None
1260 self.__send_pre_selection_notification()
1261 self.patient.cleanup()
1262 self.patient = gmNull.cNull()
1263 self.__send_selection_notification()
1264 return None
1265
1266
1267 if not isinstance(patient, cPatient):
1268 _log.error('cannot set active patient to [%s], must be either None, -1 or cPatient instance' % str(patient))
1269 raise TypeError, 'gmPerson.gmCurrentPatient.__init__(): <patient> must be None, -1 or cPatient instance but is: %s' % str(patient)
1270
1271
1272 if (self.patient['pk_identity'] == patient['pk_identity']) and not forced_reload:
1273 return None
1274
1275
1276 _log.debug('patient change [%s] -> [%s] requested', self.patient['pk_identity'], patient['pk_identity'])
1277
1278
1279 if not self.__run_pre_selection_callbacks():
1280 _log.debug('not changing current patient')
1281 return None
1282 self.__send_pre_selection_notification()
1283 self.patient.cleanup()
1284 self.patient = patient
1285 self.patient.get_emr()
1286 self.__send_selection_notification()
1287
1288 return None
1289
1293
1297
1298
1299
1301 if not callable(callback):
1302 raise TypeError(u'callback [%s] not callable' % callback)
1303
1304 self.__pre_selection_callbacks.append(callback)
1305
1308
1310 raise AttributeError(u'invalid to set <connected> state')
1311
1312 connected = property(_get_connected, _set_connected)
1313
1315 return (self.__lock_depth > 0)
1316
1318 if locked:
1319 self.__lock_depth = self.__lock_depth + 1
1320 gmDispatcher.send(signal='patient_locked')
1321 else:
1322 if self.__lock_depth == 0:
1323 _log.error('lock/unlock imbalance, trying to refcount lock depth below 0')
1324 return
1325 else:
1326 self.__lock_depth = self.__lock_depth - 1
1327 gmDispatcher.send(signal='patient_unlocked')
1328
1329 locked = property(_get_locked, _set_locked)
1330
1332 _log.info('forced patient unlock at lock depth [%s]' % self.__lock_depth)
1333 self.__lock_depth = 0
1334 gmDispatcher.send(signal='patient_unlocked')
1335
1336
1337
1339 if isinstance(self.patient, gmNull.cNull):
1340 return True
1341
1342 for call_back in self.__pre_selection_callbacks:
1343 try:
1344 successful = call_back()
1345 except:
1346 _log.exception('callback [%s] failed', call_back)
1347 print "*** pre-selection callback failed ***"
1348 print type(call_back)
1349 print call_back
1350 return False
1351
1352 if not successful:
1353 _log.debug('callback [%s] returned False', call_back)
1354 return False
1355
1356 return True
1357
1359 """Sends signal when another patient is about to become active.
1360
1361 This does NOT wait for signal handlers to complete.
1362 """
1363 kwargs = {
1364 'signal': u'pre_patient_selection',
1365 'sender': id(self.__class__),
1366 'pk_identity': self.patient['pk_identity']
1367 }
1368 gmDispatcher.send(**kwargs)
1369
1371 """Sends signal when another patient has actually been made active."""
1372 kwargs = {
1373 'signal': u'post_patient_selection',
1374 'sender': id(self.__class__),
1375 'pk_identity': self.patient['pk_identity']
1376 }
1377 gmDispatcher.send(**kwargs)
1378
1379
1380
1382 if attribute == 'patient':
1383 raise AttributeError
1384 if not isinstance(self.patient, gmNull.cNull):
1385 return getattr(self.patient, attribute)
1386
1387
1388
1390 """Return any attribute if known how to retrieve it by proxy.
1391 """
1392 return self.patient[attribute]
1393
1396
1397
1398
1401 gmMatchProvider.cMatchProvider_SQL2.__init__(
1402 self,
1403 queries = [
1404 u"""SELECT
1405 pk_staff AS data,
1406 short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS list_label,
1407 short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS field_label
1408 FROM dem.v_staff
1409 WHERE
1410 is_active AND (
1411 short_alias %(fragment_condition)s OR
1412 firstnames %(fragment_condition)s OR
1413 lastnames %(fragment_condition)s OR
1414 db_user %(fragment_condition)s
1415 )
1416 """
1417 ]
1418 )
1419 self.setThresholds(1, 2, 3)
1420
1421
1422
1423 -def create_name(pk_person, firstnames, lastnames, active=False):
1424 queries = [{
1425 'cmd': u"select dem.add_name(%s, %s, %s, %s)",
1426 'args': [pk_person, firstnames, lastnames, active]
1427 }]
1428 rows, idx = gmPG2.run_rw_queries(queries=queries, return_data=True)
1429 name = cPersonName(aPK_obj = rows[0][0])
1430 return name
1431
1432 -def create_identity(gender=None, dob=None, lastnames=None, firstnames=None):
1433
1434 cmd1 = u"""INSERT INTO dem.identity (gender, dob) VALUES (%s, %s)"""
1435 cmd2 = u"""
1436 INSERT INTO dem.names (
1437 id_identity, lastnames, firstnames
1438 ) VALUES (
1439 currval('dem.identity_pk_seq'), coalesce(%s, 'xxxDEFAULTxxx'), coalesce(%s, 'xxxDEFAULTxxx')
1440 ) RETURNING id_identity"""
1441 rows, idx = gmPG2.run_rw_queries (
1442 queries = [
1443 {'cmd': cmd1, 'args': [gender, dob]},
1444 {'cmd': cmd2, 'args': [lastnames, firstnames]}
1445 ],
1446 return_data = True
1447 )
1448 ident = cIdentity(aPK_obj=rows[0][0])
1449 gmHooks.run_hook_script(hook = u'post_person_creation')
1450 return ident
1451
1459
1486
1487
1488
1499
1500 map_gender2mf = {
1501 'm': u'm',
1502 'f': u'f',
1503 'tf': u'f',
1504 'tm': u'm',
1505 'h': u'mf'
1506 }
1507
1508
1509 map_gender2symbol = {
1510 'm': u'\u2642',
1511 'f': u'\u2640',
1512 'tf': u'\u26A5\u2640',
1513 'tm': u'\u26A5\u2642',
1514 'h': u'\u26A5'
1515
1516
1517
1518 }
1519
1539
1560
1562 """Try getting the gender for the given first name."""
1563
1564 if firstnames is None:
1565 return None
1566
1567 rows, idx = gmPG2.run_ro_queries(queries = [{
1568 'cmd': u"select gender from dem.name_gender_map where name ilike %(fn)s limit 1",
1569 'args': {'fn': firstnames}
1570 }])
1571
1572 if len(rows) == 0:
1573 return None
1574
1575 return rows[0][0]
1576
1578 return [ cIdentity(aPK_obj = pk) for pk in pks ]
1579
1583
1587
1588
1589
1590 if __name__ == '__main__':
1591
1592 if len(sys.argv) == 1:
1593 sys.exit()
1594
1595 if sys.argv[1] != 'test':
1596 sys.exit()
1597
1598 import datetime
1599
1600 gmI18N.activate_locale()
1601 gmI18N.install_domain()
1602 gmDateTime.init()
1603
1604
1625
1627 dto = cDTO_person()
1628 dto.firstnames = 'Sepp'
1629 dto.lastnames = 'Herberger'
1630 dto.gender = 'male'
1631 dto.dob = pyDT.datetime.now(tz=gmDateTime.gmCurrentLocalTimezone)
1632 print dto
1633
1634 print dto['firstnames']
1635 print dto['lastnames']
1636 print dto['gender']
1637 print dto['dob']
1638
1639 for key in dto.keys():
1640 print key
1641
1643
1644 print '\n\nCreating identity...'
1645 new_identity = create_identity(gender='m', dob='2005-01-01', lastnames='test lastnames', firstnames='test firstnames')
1646 print 'Identity created: %s' % new_identity
1647
1648 print '\nSetting title and gender...'
1649 new_identity['title'] = 'test title';
1650 new_identity['gender'] = 'f';
1651 new_identity.save_payload()
1652 print 'Refetching identity from db: %s' % cIdentity(aPK_obj=new_identity['pk_identity'])
1653
1654 print '\nGetting all names...'
1655 for a_name in new_identity.get_names():
1656 print a_name
1657 print 'Active name: %s' % (new_identity.get_active_name())
1658 print 'Setting nickname...'
1659 new_identity.set_nickname(nickname='test nickname')
1660 print 'Refetching all names...'
1661 for a_name in new_identity.get_names():
1662 print a_name
1663 print 'Active name: %s' % (new_identity.get_active_name())
1664
1665 print '\nIdentity occupations: %s' % new_identity['occupations']
1666 print 'Creating identity occupation...'
1667 new_identity.link_occupation('test occupation')
1668 print 'Identity occupations: %s' % new_identity['occupations']
1669
1670 print '\nIdentity addresses: %s' % new_identity.get_addresses()
1671 print 'Creating identity address...'
1672
1673 new_identity.link_address (
1674 number = 'test 1234',
1675 street = 'test street',
1676 postcode = 'test postcode',
1677 urb = 'test urb',
1678 state = 'SN',
1679 country = 'DE'
1680 )
1681 print 'Identity addresses: %s' % new_identity.get_addresses()
1682
1683 print '\nIdentity communications: %s' % new_identity.get_comm_channels()
1684 print 'Creating identity communication...'
1685 new_identity.link_comm_channel('homephone', '1234566')
1686 print 'Identity communications: %s' % new_identity.get_comm_channels()
1687
1693
1695 genders, idx = get_gender_list()
1696 print "\n\nRetrieving gender enum (tag, label, weight):"
1697 for gender in genders:
1698 print "%s, %s, %s" % (gender[idx['tag']], gender[idx['l10n_label']], gender[idx['sort_weight']])
1699
1700
1701
1702
1703
1704
1705 test_gender_list()
1706
1707
1708
1709
1710
1711
1712
1713
1714