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
523 args = {
524 u'tag': tag,
525 u'identity': self.ID
526 }
527
528
529 cmd = u"SELECT pk FROM dem.identity_tag WHERE fk_tag = %(tag)s AND fk_identity = %(identity)s"
530 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
531 if len(rows) > 0:
532 return gmDemographicRecord.cIdentityTag(aPK_obj = rows[0]['pk'])
533
534
535 cmd = u"""
536 INSERT INTO dem.identity_tag (
537 fk_tag,
538 fk_identity
539 ) VALUES (
540 %(tag)s,
541 %(identity)s
542 )
543 RETURNING pk
544 """
545 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False)
546 return gmDemographicRecord.cIdentityTag(aPK_obj = rows[0]['pk'])
547
549 cmd = u"DELETE FROM dem.identity_tag WHERE pk = %(pk)s"
550 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': tag}}])
551
552
553
554
555
556
557
558
559
560
561 - def add_external_id(self, type_name=None, value=None, issuer=None, comment=None, pk_type=None):
562 """Adds an external ID to the patient.
563
564 creates ID type if necessary
565 """
566
567
568 if pk_type is not None:
569 cmd = u"""
570 select * from dem.v_external_ids4identity where
571 pk_identity = %(pat)s and
572 pk_type = %(pk_type)s and
573 value = %(val)s"""
574 else:
575
576 if issuer is None:
577 cmd = u"""
578 select * from dem.v_external_ids4identity where
579 pk_identity = %(pat)s and
580 name = %(name)s and
581 value = %(val)s"""
582 else:
583 cmd = u"""
584 select * from dem.v_external_ids4identity where
585 pk_identity = %(pat)s and
586 name = %(name)s and
587 value = %(val)s and
588 issuer = %(issuer)s"""
589 args = {
590 'pat': self.ID,
591 'name': type_name,
592 'val': value,
593 'issuer': issuer,
594 'pk_type': pk_type
595 }
596 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
597
598
599 if len(rows) == 0:
600
601 args = {
602 'pat': self.ID,
603 'val': value,
604 'type_name': type_name,
605 'pk_type': pk_type,
606 'issuer': issuer,
607 'comment': comment
608 }
609
610 if pk_type is None:
611 cmd = u"""insert into dem.lnk_identity2ext_id (external_id, fk_origin, comment, id_identity) values (
612 %(val)s,
613 (select dem.add_external_id_type(%(type_name)s, %(issuer)s)),
614 %(comment)s,
615 %(pat)s
616 )"""
617 else:
618 cmd = u"""insert into dem.lnk_identity2ext_id (external_id, fk_origin, comment, id_identity) values (
619 %(val)s,
620 %(pk_type)s,
621 %(comment)s,
622 %(pat)s
623 )"""
624
625 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
626
627
628 else:
629 row = rows[0]
630 if comment is not None:
631
632 if gmTools.coalesce(row['comment'], '').find(comment.strip()) == -1:
633 comment = '%s%s' % (gmTools.coalesce(row['comment'], '', '%s // '), comment.strip)
634 cmd = u"update dem.lnk_identity2ext_id set comment = %(comment)s where id=%(pk)s"
635 args = {'comment': comment, 'pk': row['pk_id']}
636 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
637
638 - def update_external_id(self, pk_id=None, type=None, value=None, issuer=None, comment=None):
639 """Edits an existing external ID.
640
641 Creates ID type if necessary.
642 """
643 cmd = u"""
644 UPDATE dem.lnk_identity2ext_id SET
645 fk_origin = (SELECT dem.add_external_id_type(%(type)s, %(issuer)s)),
646 external_id = %(value)s,
647 comment = gm.nullify_empty_string(%(comment)s)
648 WHERE
649 id = %(pk)s
650 """
651 args = {'pk': pk_id, 'value': value, 'type': type, 'issuer': issuer, 'comment': comment}
652 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
653
655 where_parts = ['pk_identity = %(pat)s']
656 args = {'pat': self.ID}
657
658 if id_type is not None:
659 where_parts.append(u'name = %(name)s')
660 args['name'] = id_type.strip()
661
662 if issuer is not None:
663 where_parts.append(u'issuer = %(issuer)s')
664 args['issuer'] = issuer.strip()
665
666 cmd = u"SELECT * FROM dem.v_external_ids4identity WHERE %s" % ' AND '.join(where_parts)
667 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
668
669 return rows
670
671 external_ids = property(get_external_ids, lambda x:x)
672
674 cmd = u"""
675 delete from dem.lnk_identity2ext_id
676 where id_identity = %(pat)s and id = %(pk)s"""
677 args = {'pat': self.ID, 'pk': pk_ext_id}
678 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
679
681 """Merge another identity into this one.
682
683 Keep this one. Delete other one."""
684
685 if other_identity.ID == self.ID:
686 return True, None
687
688 curr_pat = gmCurrentPatient()
689 if curr_pat.connected:
690 if other_identity.ID == curr_pat.ID:
691 return False, _('Cannot merge active patient into another patient.')
692
693 queries = []
694 args = {'old_pat': other_identity.ID, 'new_pat': self.ID}
695
696
697 queries.append ({
698 '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)',
699 'args': args
700 })
701
702
703
704 queries.append ({
705 'cmd': u'update dem.names set active = False where id_identity = %(old_pat)s',
706 'args': args
707 })
708
709
710 FKs = gmPG2.get_foreign_keys2column (
711 schema = u'dem',
712 table = u'identity',
713 column = u'pk'
714 )
715
716
717 cmd_template = u'update %s set %s = %%(new_pat)s where %s = %%(old_pat)s'
718 for FK in FKs:
719 queries.append ({
720 'cmd': cmd_template % (FK['referencing_table'], FK['referencing_column'], FK['referencing_column']),
721 'args': args
722 })
723
724
725 queries.append ({
726 'cmd': u'delete from dem.identity where pk = %(old_pat)s',
727 'args': args
728 })
729
730 _log.warning('identity [%s] is about to assimilate identity [%s]', self.ID, other_identity.ID)
731
732 gmPG2.run_rw_queries(link_obj = link_obj, queries = queries, end_tx = True)
733
734 self.add_external_id (
735 type_name = u'merged GNUmed identity primary key',
736 value = u'GNUmed::pk::%s' % other_identity.ID,
737 issuer = u'GNUmed'
738 )
739
740 return True, None
741
742
744 cmd = u"""
745 insert into clin.waiting_list (fk_patient, urgency, comment, area, list_position)
746 values (
747 %(pat)s,
748 %(urg)s,
749 %(cmt)s,
750 %(area)s,
751 (select coalesce((max(list_position) + 1), 1) from clin.waiting_list)
752 )"""
753 args = {'pat': self.ID, 'urg': urgency, 'cmt': comment, 'area': zone}
754 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], verbose = True)
755
757 cmd = u"""SELECT * FROM clin.v_waiting_list WHERE pk_identity = %(pat)s"""
758 args = {'pat': self.ID}
759 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
760 return rows
761
762 waiting_list_entries = property(get_waiting_list_entry, lambda x:x)
763
764 - def export_as_gdt(self, filename=None, encoding='iso-8859-15', external_id_type=None):
765
766 template = u'%s%s%s\r\n'
767
768 file = codecs.open (
769 filename = filename,
770 mode = 'wb',
771 encoding = encoding,
772 errors = 'strict'
773 )
774
775 file.write(template % (u'013', u'8000', u'6301'))
776 file.write(template % (u'013', u'9218', u'2.10'))
777 if external_id_type is None:
778 file.write(template % (u'%03d' % (9 + len(str(self.ID))), u'3000', self.ID))
779 else:
780 ext_ids = self.get_external_ids(id_type = external_id_type)
781 if len(ext_ids) > 0:
782 file.write(template % (u'%03d' % (9 + len(ext_ids[0]['value'])), u'3000', ext_ids[0]['value']))
783 file.write(template % (u'%03d' % (9 + len(self._payload[self._idx['lastnames']])), u'3101', self._payload[self._idx['lastnames']]))
784 file.write(template % (u'%03d' % (9 + len(self._payload[self._idx['firstnames']])), u'3102', self._payload[self._idx['firstnames']]))
785 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')))
786 file.write(template % (u'010', u'3110', gmXdtMappings.map_gender_gm2xdt[self._payload[self._idx['gender']]]))
787 file.write(template % (u'025', u'6330', 'GNUmed::9206::encoding'))
788 file.write(template % (u'%03d' % (9 + len(encoding)), u'6331', encoding))
789 if external_id_type is None:
790 file.write(template % (u'029', u'6332', u'GNUmed::3000::source'))
791 file.write(template % (u'017', u'6333', u'internal'))
792 else:
793 if len(ext_ids) > 0:
794 file.write(template % (u'029', u'6332', u'GNUmed::3000::source'))
795 file.write(template % (u'%03d' % (9 + len(external_id_type)), u'6333', external_id_type))
796
797 file.close()
798
799
800
803
805 """Link an occupation with a patient, creating the occupation if it does not exists.
806
807 @param occupation The name of the occupation to link the patient to.
808 """
809 if (activities is None) and (occupation is None):
810 return True
811
812 occupation = occupation.strip()
813 if len(occupation) == 0:
814 return True
815
816 if activities is not None:
817 activities = activities.strip()
818
819 args = {'act': activities, 'pat_id': self.pk_obj, 'job': occupation}
820
821 cmd = u"select activities from dem.v_person_jobs where pk_identity = %(pat_id)s and l10n_occupation = _(%(job)s)"
822 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
823
824 queries = []
825 if len(rows) == 0:
826 queries.append ({
827 'cmd': u"INSERT INTO dem.lnk_job2person (fk_identity, fk_occupation, activities) VALUES (%(pat_id)s, dem.create_occupation(%(job)s), %(act)s)",
828 'args': args
829 })
830 else:
831 if rows[0]['activities'] != activities:
832 queries.append ({
833 '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))",
834 'args': args
835 })
836
837 rows, idx = gmPG2.run_rw_queries(queries = queries)
838
839 return True
840
842 if occupation is None:
843 return True
844 occupation = occupation.strip()
845 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))"
846 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj, 'job': occupation}}])
847 return True
848
849
850
852 cmd = u"select * from dem.v_person_comms where pk_identity = %s"
853 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}], get_col_idx = True)
854
855 filtered = rows
856
857 if comm_medium is not None:
858 filtered = []
859 for row in rows:
860 if row['comm_type'] == comm_medium:
861 filtered.append(row)
862
863 return [ gmDemographicRecord.cCommChannel(row = {
864 'pk_field': 'pk_lnk_identity2comm',
865 'data': r,
866 'idx': idx
867 }) for r in filtered
868 ]
869
870 - def link_comm_channel(self, comm_medium=None, url=None, is_confidential=False, pk_channel_type=None):
871 """Link a communication medium with a patient.
872
873 @param comm_medium The name of the communication medium.
874 @param url The communication resource locator.
875 @type url A types.StringType instance.
876 @param is_confidential Wether the data must be treated as confidential.
877 @type is_confidential A types.BooleanType instance.
878 """
879 comm_channel = gmDemographicRecord.create_comm_channel (
880 comm_medium = comm_medium,
881 url = url,
882 is_confidential = is_confidential,
883 pk_channel_type = pk_channel_type,
884 pk_identity = self.pk_obj
885 )
886 return comm_channel
887
893
894
895
897
898 cmd = u"SELECT * FROM dem.v_pat_addresses WHERE pk_identity = %(pat)s"
899 args = {'pat': self.pk_obj}
900 if address_type is not None:
901 cmd = cmd + u" AND address_type = %(typ)s"
902 args['typ'] = address_type
903
904 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
905
906 return [
907 gmDemographicRecord.cPatientAddress(row = {'idx': idx, 'data': r, 'pk_field': 'pk_address'})
908 for r in rows
909 ]
910
911 - 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):
912 """Link an address with a patient, creating the address if it does not exists.
913
914 @param number The number of the address.
915 @param street The name of the street.
916 @param postcode The postal code of the address.
917 @param urb The name of town/city/etc.
918 @param state The code of the state.
919 @param country The code of the country.
920 @param id_type The primary key of the address type.
921 """
922 if address is None:
923
924 address = gmDemographicRecord.create_address (
925 country = country,
926 state = state,
927 urb = urb,
928 suburb = suburb,
929 postcode = postcode,
930 street = street,
931 number = number,
932 subunit = subunit
933 )
934
935 if address is None:
936 return None
937
938
939 cmd = u"SELECT * FROM dem.lnk_person_org_address WHERE id_identity = %(pat)s AND id_address = %(adr)s"
940 args = {'pat': self.pk_obj, 'adr': address['pk_address']}
941 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
942
943
944 if len(rows) == 0:
945 args = {'id': self.pk_obj, 'adr': address['pk_address'], 'type': id_type}
946 cmd = u"""
947 INSERT INTO dem.lnk_person_org_address(id_identity, id_address)
948 VALUES (%(id)s, %(adr)s)
949 RETURNING *"""
950 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True)
951
952
953 if id_type is not None:
954 r = rows[0]
955 if r['id_type'] != id_type:
956 cmd = "UPDATE dem.lnk_person_org_address SET id_type = %(type)s WHERE id = %(id)s"
957 args = {'type': id_type, 'id': r['id']}
958 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
959
960 return address
961
963 """Remove an address from the patient.
964
965 The address itself stays in the database.
966 The address can be either cAdress or cPatientAdress.
967 """
968 if pk_address is None:
969 args = {'person': self.pk_obj, 'adr': address['pk_address']}
970 else:
971 args = {'person': self.pk_obj, 'adr': pk_address}
972 cmd = u"DELETE FROM dem.lnk_person_org_address WHERE id_identity = %(person)s AND id_address = %(adr)s"
973 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
974
975
976
978 cmd = u"""
979 select
980 t.description,
981 vbp.pk_identity as id,
982 title,
983 firstnames,
984 lastnames,
985 dob,
986 cob,
987 gender,
988 karyotype,
989 pupic,
990 pk_marital_status,
991 marital_status,
992 xmin_identity,
993 preferred
994 from
995 dem.v_basic_person vbp, dem.relation_types t, dem.lnk_person2relative l
996 where
997 (
998 l.id_identity = %(pk)s and
999 vbp.pk_identity = l.id_relative and
1000 t.id = l.id_relation_type
1001 ) or (
1002 l.id_relative = %(pk)s and
1003 vbp.pk_identity = l.id_identity and
1004 t.inverse = l.id_relation_type
1005 )"""
1006 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}])
1007 if len(rows) == 0:
1008 return []
1009 return [(row[0], cIdentity(row = {'data': row[1:], 'idx':idx, 'pk_field': 'pk'})) for row in rows]
1010
1012
1013 id_new_relative = create_dummy_identity()
1014
1015 relative = cIdentity(aPK_obj=id_new_relative)
1016
1017
1018 relative.add_name( '**?**', self.get_names()['lastnames'])
1019
1020 if self._ext_cache.has_key('relatives'):
1021 del self._ext_cache['relatives']
1022 cmd = u"""
1023 insert into dem.lnk_person2relative (
1024 id_identity, id_relative, id_relation_type
1025 ) values (
1026 %s, %s, (select id from dem.relation_types where description = %s)
1027 )"""
1028 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': [self.ID, id_new_relative, rel_type ]}])
1029 return True
1030
1032
1033 self.set_relative(None, relation)
1034
1039
1040 emergency_contact_in_database = property(_get_emergency_contact_from_database, lambda x:x)
1041
1042
1043
1052
1093
1094 - def dob_in_range(self, min_distance=u'1 week', max_distance=u'1 week'):
1095 cmd = u'select dem.dob_is_in_range(%(dob)s, %(min)s, %(max)s)'
1096 rows, idx = gmPG2.run_ro_queries (
1097 queries = [{
1098 'cmd': cmd,
1099 'args': {'dob': self['dob'], 'min': min_distance, 'max': max_distance}
1100 }]
1101 )
1102 return rows[0][0]
1103
1104
1105
1107 cmd = u'select * from clin.v_most_recent_encounters where pk_patient=%s'
1108 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self._payload[self._idx['pk_identity']]]}])
1109 if len(rows) > 0:
1110 return rows[0]
1111 else:
1112 return None
1113
1116
1117 messages = property(_get_messages, lambda x:x)
1118
1121
1122 due_messages = property(_get_due_messages, lambda x:x)
1123
1126
1128 if self._payload[self._idx['pk_primary_provider']] is None:
1129 return None
1130 from Gnumed.business import gmStaff
1131 return gmStaff.cStaff(aPK_obj = self._payload[self._idx['pk_primary_provider']])
1132
1133 primary_provider = property(_get_primary_provider, lambda x:x)
1134
1135
1136
1138 """Format patient demographics into patient specific path name fragment."""
1139 return '%s-%s%s-%s' % (
1140 self._payload[self._idx['lastnames']].replace(u' ', u'_'),
1141 self._payload[self._idx['firstnames']].replace(u' ', u'_'),
1142 gmTools.coalesce(self._payload[self._idx['preferred']], u'', template_initial = u'-(%s)'),
1143 self.get_formatted_dob(format = '%Y-%m-%d', encoding = gmI18N.get_encoding())
1144 )
1145
1147 """Represents a person which is a patient.
1148
1149 - a specializing subclass of cIdentity turning it into a patient
1150 - its use is to cache subobjects like EMR and document folder
1151 """
1152 - def __init__(self, aPK_obj=None, row=None):
1153 cIdentity.__init__(self, aPK_obj=aPK_obj, row=row)
1154 self.__db_cache = {}
1155 self.__emr_access_lock = threading.Lock()
1156
1158 """Do cleanups before dying.
1159
1160 - note that this may be called in a thread
1161 """
1162 if self.__db_cache.has_key('clinical record'):
1163 self.__db_cache['clinical record'].cleanup()
1164 if self.__db_cache.has_key('document folder'):
1165 self.__db_cache['document folder'].cleanup()
1166 cIdentity.cleanup(self)
1167
1169
1170
1171
1172
1173
1174
1175
1176
1177 if not self.__emr_access_lock.acquire(False):
1178 raise AttributeError('cannot access EMR')
1179 try:
1180 emr = self.__db_cache['clinical record']
1181 self.__emr_access_lock.release()
1182 return emr
1183 except KeyError:
1184 pass
1185
1186 self.__db_cache['clinical record'] = gmClinicalRecord.cClinicalRecord(aPKey = self._payload[self._idx['pk_identity']])
1187 self.__emr_access_lock.release()
1188 return self.__db_cache['clinical record']
1189
1190 emr = property(get_emr, lambda x:x)
1191
1193 try:
1194 return self.__db_cache['document folder']
1195 except KeyError:
1196 pass
1197
1198 self.__db_cache['document folder'] = cDocumentFolder(aPKey = self._payload[self._idx['pk_identity']])
1199 return self.__db_cache['document folder']
1200
1201 document_folder = property(get_document_folder, lambda x:x)
1202
1204 """Patient Borg to hold currently active patient.
1205
1206 There may be many instances of this but they all share state.
1207 """
1208 - def __init__(self, patient=None, forced_reload=False):
1209 """Change or get currently active patient.
1210
1211 patient:
1212 * None: get currently active patient
1213 * -1: unset currently active patient
1214 * cPatient instance: set active patient if possible
1215 """
1216
1217 try:
1218 tmp = self.patient
1219 except AttributeError:
1220 self.patient = gmNull.cNull()
1221 self.__register_interests()
1222
1223
1224
1225 self.__lock_depth = 0
1226
1227 self.__pre_selection_callbacks = []
1228
1229
1230 if patient is None:
1231 return None
1232
1233
1234 if self.locked:
1235 _log.error('patient [%s] is locked, cannot change to [%s]' % (self.patient['pk_identity'], patient))
1236 return None
1237
1238
1239 if patient == -1:
1240 _log.debug('explicitly unsetting current patient')
1241 if not self.__run_pre_selection_callbacks():
1242 _log.debug('not unsetting current patient')
1243 return None
1244 self.__send_pre_selection_notification()
1245 self.patient.cleanup()
1246 self.patient = gmNull.cNull()
1247 self.__send_selection_notification()
1248 return None
1249
1250
1251 if not isinstance(patient, cPatient):
1252 _log.error('cannot set active patient to [%s], must be either None, -1 or cPatient instance' % str(patient))
1253 raise TypeError, 'gmPerson.gmCurrentPatient.__init__(): <patient> must be None, -1 or cPatient instance but is: %s' % str(patient)
1254
1255
1256 if (self.patient['pk_identity'] == patient['pk_identity']) and not forced_reload:
1257 return None
1258
1259
1260 _log.debug('patient change [%s] -> [%s] requested', self.patient['pk_identity'], patient['pk_identity'])
1261
1262
1263 if not self.__run_pre_selection_callbacks():
1264 _log.debug('not changing current patient')
1265 return None
1266 self.__send_pre_selection_notification()
1267 self.patient.cleanup()
1268 self.patient = patient
1269 self.patient.get_emr()
1270 self.__send_selection_notification()
1271
1272 return None
1273
1277
1281
1282
1283
1285 if not callable(callback):
1286 raise TypeError(u'callback [%s] not callable' % callback)
1287
1288 self.__pre_selection_callbacks.append(callback)
1289
1292
1294 raise AttributeError(u'invalid to set <connected> state')
1295
1296 connected = property(_get_connected, _set_connected)
1297
1299 return (self.__lock_depth > 0)
1300
1302 if locked:
1303 self.__lock_depth = self.__lock_depth + 1
1304 gmDispatcher.send(signal='patient_locked')
1305 else:
1306 if self.__lock_depth == 0:
1307 _log.error('lock/unlock imbalance, trying to refcount lock depth below 0')
1308 return
1309 else:
1310 self.__lock_depth = self.__lock_depth - 1
1311 gmDispatcher.send(signal='patient_unlocked')
1312
1313 locked = property(_get_locked, _set_locked)
1314
1316 _log.info('forced patient unlock at lock depth [%s]' % self.__lock_depth)
1317 self.__lock_depth = 0
1318 gmDispatcher.send(signal='patient_unlocked')
1319
1320
1321
1323 if isinstance(self.patient, gmNull.cNull):
1324 return True
1325
1326 for call_back in self.__pre_selection_callbacks:
1327 try:
1328 successful = call_back()
1329 except:
1330 _log.exception('callback [%s] failed', call_back)
1331 print "*** pre-selection callback failed ***"
1332 print type(call_back)
1333 print call_back
1334 return False
1335
1336 if not successful:
1337 _log.debug('callback [%s] returned False', call_back)
1338 return False
1339
1340 return True
1341
1343 """Sends signal when another patient is about to become active.
1344
1345 This does NOT wait for signal handlers to complete.
1346 """
1347 kwargs = {
1348 'signal': u'pre_patient_selection',
1349 'sender': id(self.__class__),
1350 'pk_identity': self.patient['pk_identity']
1351 }
1352 gmDispatcher.send(**kwargs)
1353
1355 """Sends signal when another patient has actually been made active."""
1356 kwargs = {
1357 'signal': u'post_patient_selection',
1358 'sender': id(self.__class__),
1359 'pk_identity': self.patient['pk_identity']
1360 }
1361 gmDispatcher.send(**kwargs)
1362
1363
1364
1366 if attribute == 'patient':
1367 raise AttributeError
1368 if not isinstance(self.patient, gmNull.cNull):
1369 return getattr(self.patient, attribute)
1370
1371
1372
1374 """Return any attribute if known how to retrieve it by proxy.
1375 """
1376 return self.patient[attribute]
1377
1380
1381
1382
1385 gmMatchProvider.cMatchProvider_SQL2.__init__(
1386 self,
1387 queries = [
1388 u"""SELECT
1389 pk_staff AS data,
1390 short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS list_label,
1391 short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS field_label
1392 FROM dem.v_staff
1393 WHERE
1394 is_active AND (
1395 short_alias %(fragment_condition)s OR
1396 firstnames %(fragment_condition)s OR
1397 lastnames %(fragment_condition)s OR
1398 db_user %(fragment_condition)s
1399 )
1400 """
1401 ]
1402 )
1403 self.setThresholds(1, 2, 3)
1404
1405
1406
1407 -def create_name(pk_person, firstnames, lastnames, active=False):
1408 queries = [{
1409 'cmd': u"select dem.add_name(%s, %s, %s, %s)",
1410 'args': [pk_person, firstnames, lastnames, active]
1411 }]
1412 rows, idx = gmPG2.run_rw_queries(queries=queries, return_data=True)
1413 name = cPersonName(aPK_obj = rows[0][0])
1414 return name
1415
1416 -def create_identity(gender=None, dob=None, lastnames=None, firstnames=None):
1417
1418 cmd1 = u"""INSERT INTO dem.identity (gender, dob) VALUES (%s, %s)"""
1419 cmd2 = u"""
1420 INSERT INTO dem.names (
1421 id_identity, lastnames, firstnames
1422 ) VALUES (
1423 currval('dem.identity_pk_seq'), coalesce(%s, 'xxxDEFAULTxxx'), coalesce(%s, 'xxxDEFAULTxxx')
1424 ) RETURNING id_identity"""
1425 rows, idx = gmPG2.run_rw_queries (
1426 queries = [
1427 {'cmd': cmd1, 'args': [gender, dob]},
1428 {'cmd': cmd2, 'args': [lastnames, firstnames]}
1429 ],
1430 return_data = True
1431 )
1432 ident = cIdentity(aPK_obj=rows[0][0])
1433 gmHooks.run_hook_script(hook = u'post_person_creation')
1434 return ident
1435
1443
1470
1471
1472
1483
1484 map_gender2mf = {
1485 'm': u'm',
1486 'f': u'f',
1487 'tf': u'f',
1488 'tm': u'm',
1489 'h': u'mf'
1490 }
1491
1492
1493 map_gender2symbol = {
1494 'm': u'\u2642',
1495 'f': u'\u2640',
1496 'tf': u'\u26A5\u2640',
1497 'tm': u'\u26A5\u2642',
1498 'h': u'\u26A5'
1499
1500
1501
1502 }
1503
1523
1544
1546 """Try getting the gender for the given first name."""
1547
1548 if firstnames is None:
1549 return None
1550
1551 rows, idx = gmPG2.run_ro_queries(queries = [{
1552 'cmd': u"select gender from dem.name_gender_map where name ilike %(fn)s limit 1",
1553 'args': {'fn': firstnames}
1554 }])
1555
1556 if len(rows) == 0:
1557 return None
1558
1559 return rows[0][0]
1560
1562 return [ cIdentity(aPK_obj = pk) for pk in pks ]
1563
1567
1571
1572
1573
1574 if __name__ == '__main__':
1575
1576 if len(sys.argv) == 1:
1577 sys.exit()
1578
1579 if sys.argv[1] != 'test':
1580 sys.exit()
1581
1582 import datetime
1583
1584 gmI18N.activate_locale()
1585 gmI18N.install_domain()
1586 gmDateTime.init()
1587
1588
1609
1611 dto = cDTO_person()
1612 dto.firstnames = 'Sepp'
1613 dto.lastnames = 'Herberger'
1614 dto.gender = 'male'
1615 dto.dob = pyDT.datetime.now(tz=gmDateTime.gmCurrentLocalTimezone)
1616 print dto
1617
1618 print dto['firstnames']
1619 print dto['lastnames']
1620 print dto['gender']
1621 print dto['dob']
1622
1623 for key in dto.keys():
1624 print key
1625
1627
1628 print '\n\nCreating identity...'
1629 new_identity = create_identity(gender='m', dob='2005-01-01', lastnames='test lastnames', firstnames='test firstnames')
1630 print 'Identity created: %s' % new_identity
1631
1632 print '\nSetting title and gender...'
1633 new_identity['title'] = 'test title';
1634 new_identity['gender'] = 'f';
1635 new_identity.save_payload()
1636 print 'Refetching identity from db: %s' % cIdentity(aPK_obj=new_identity['pk_identity'])
1637
1638 print '\nGetting all names...'
1639 for a_name in new_identity.get_names():
1640 print a_name
1641 print 'Active name: %s' % (new_identity.get_active_name())
1642 print 'Setting nickname...'
1643 new_identity.set_nickname(nickname='test nickname')
1644 print 'Refetching all names...'
1645 for a_name in new_identity.get_names():
1646 print a_name
1647 print 'Active name: %s' % (new_identity.get_active_name())
1648
1649 print '\nIdentity occupations: %s' % new_identity['occupations']
1650 print 'Creating identity occupation...'
1651 new_identity.link_occupation('test occupation')
1652 print 'Identity occupations: %s' % new_identity['occupations']
1653
1654 print '\nIdentity addresses: %s' % new_identity.get_addresses()
1655 print 'Creating identity address...'
1656
1657 new_identity.link_address (
1658 number = 'test 1234',
1659 street = 'test street',
1660 postcode = 'test postcode',
1661 urb = 'test urb',
1662 state = 'SN',
1663 country = 'DE'
1664 )
1665 print 'Identity addresses: %s' % new_identity.get_addresses()
1666
1667 print '\nIdentity communications: %s' % new_identity.get_comm_channels()
1668 print 'Creating identity communication...'
1669 new_identity.link_comm_channel('homephone', '1234566')
1670 print 'Identity communications: %s' % new_identity.get_comm_channels()
1671
1677
1679 genders, idx = get_gender_list()
1680 print "\n\nRetrieving gender enum (tag, label, weight):"
1681 for gender in genders:
1682 print "%s, %s, %s" % (gender[idx['tag']], gender[idx['l10n_label']], gender[idx['sort_weight']])
1683
1684
1685
1686
1687
1688
1689 test_gender_list()
1690
1691
1692
1693
1694
1695
1696
1697
1698