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
41
42
44
50
51
52
54 return 'firstnames lastnames dob gender'.split()
55
58
60 """Generate generic queries.
61
62 - not locale dependant
63 - data -> firstnames, lastnames, dob, gender
64
65 shall we mogrify name parts ? probably not as external
66 sources should know what they do
67
68 finds by inactive name, too, but then shows
69 the corresponding active name ;-)
70
71 Returns list of matching identities (may be empty)
72 or None if it was told to create an identity but couldn't.
73 """
74 where_snippets = []
75 args = {}
76
77 where_snippets.append(u'firstnames = %(first)s')
78 args['first'] = self.firstnames
79
80 where_snippets.append(u'lastnames = %(last)s')
81 args['last'] = self.lastnames
82
83 if self.dob is not None:
84 where_snippets.append(u"dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %(dob)s)")
85 args['dob'] = self.dob.replace(hour = 23, minute = 59, second = 59)
86
87 if self.gender is not None:
88 where_snippets.append('gender = %(sex)s')
89 args['sex'] = self.gender
90
91 cmd = u"""
92 SELECT *, '%s' AS match_type
93 FROM dem.v_basic_person
94 WHERE
95 pk_identity IN (
96 SELECT pk_identity FROM dem.v_person_names WHERE %s
97 )
98 ORDER BY lastnames, firstnames, dob""" % (
99 _('external patient source (name, gender, date of birth)'),
100 ' AND '.join(where_snippets)
101 )
102
103 try:
104 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx=True)
105 except:
106 _log.error(u'cannot get candidate identities for dto "%s"' % self)
107 _log.exception('query %s' % cmd)
108 rows = []
109
110 if len(rows) == 0:
111 _log.debug('no candidate identity matches found')
112 if not can_create:
113 return []
114 ident = self.import_into_database()
115 if ident is None:
116 return None
117 identities = [ident]
118 else:
119 identities = [ cIdentity(row = {'pk_field': 'pk_identity', 'data': row, 'idx': idx}) for row in rows ]
120
121 return identities
122
124 """Imports self into the database."""
125
126 self.identity = create_identity (
127 firstnames = self.firstnames,
128 lastnames = self.lastnames,
129 gender = self.gender,
130 dob = self.dob
131 )
132
133 if self.identity is None:
134 return None
135
136 for ext_id in self.external_ids:
137 try:
138 self.identity.add_external_id (
139 type_name = ext_id['name'],
140 value = ext_id['value'],
141 issuer = ext_id['issuer'],
142 comment = ext_id['comment']
143 )
144 except StandardError:
145 _log.exception('cannot import <external ID> from external data source')
146 _log.log_stack_trace()
147
148 for comm in self.comm_channels:
149 try:
150 self.identity.link_comm_channel (
151 comm_medium = comm['channel'],
152 url = comm['url']
153 )
154 except StandardError:
155 _log.exception('cannot import <comm channel> from external data source')
156 _log.log_stack_trace()
157
158 for adr in self.addresses:
159 try:
160 self.identity.link_address (
161 number = adr['number'],
162 street = adr['street'],
163 postcode = adr['zip'],
164 urb = adr['urb'],
165 state = adr['region'],
166 country = adr['country']
167 )
168 except StandardError:
169 _log.exception('cannot import <address> from external data source')
170 _log.log_stack_trace()
171
172 return self.identity
173
176
178 value = value.strip()
179 if value == u'':
180 return
181 name = name.strip()
182 if name == u'':
183 raise ValueError(_('<name> cannot be empty'))
184 issuer = issuer.strip()
185 if issuer == u'':
186 raise ValueError(_('<issuer> cannot be empty'))
187 self.external_ids.append({'name': name, 'value': value, 'issuer': issuer, 'comment': comment})
188
190 url = url.strip()
191 if url == u'':
192 return
193 channel = channel.strip()
194 if channel == u'':
195 raise ValueError(_('<channel> cannot be empty'))
196 self.comm_channels.append({'channel': channel, 'url': url})
197
198 - def remember_address(self, number=None, street=None, urb=None, region=None, zip=None, country=None):
199 number = number.strip()
200 if number == u'':
201 raise ValueError(_('<number> cannot be empty'))
202 street = street.strip()
203 if street == u'':
204 raise ValueError(_('<street> cannot be empty'))
205 urb = urb.strip()
206 if urb == u'':
207 raise ValueError(_('<urb> cannot be empty'))
208 zip = zip.strip()
209 if zip == u'':
210 raise ValueError(_('<zip> cannot be empty'))
211 country = country.strip()
212 if country == u'':
213 raise ValueError(_('<country> cannot be empty'))
214 region = region.strip()
215 if region == u'':
216 region = u'??'
217 self.addresses.append ({
218 u'number': number,
219 u'street': street,
220 u'zip': zip,
221 u'urb': urb,
222 u'region': region,
223 u'country': country
224 })
225
226
227
229 return u'<%s @ %s: %s %s (%s) %s>' % (
230 self.__class__.__name__,
231 id(self),
232 self.firstnames,
233 self.lastnames,
234 self.gender,
235 self.dob
236 )
237
239 """Do some sanity checks on self.* access."""
240
241 if attr == 'gender':
242 glist, idx = get_gender_list()
243 for gender in glist:
244 if str(val) in [gender[0], gender[1], gender[2], gender[3]]:
245 val = gender[idx['tag']]
246 object.__setattr__(self, attr, val)
247 return
248 raise ValueError('invalid gender: [%s]' % val)
249
250 if attr == 'dob':
251 if val is not None:
252 if not isinstance(val, pyDT.datetime):
253 raise TypeError('invalid type for DOB (must be datetime.datetime): %s [%s]' % (type(val), val))
254 if val.tzinfo is None:
255 raise ValueError('datetime.datetime instance is lacking a time zone: [%s]' % val.isoformat())
256
257 object.__setattr__(self, attr, val)
258 return
259
261 return getattr(self, attr)
262
263 -class cPersonName(gmBusinessDBObject.cBusinessDBObject):
264 _cmd_fetch_payload = u"SELECT * FROM dem.v_person_names WHERE pk_name = %s"
265 _cmds_store_payload = [
266 u"""UPDATE dem.names SET
267 active = FALSE
268 WHERE
269 %(active_name)s IS TRUE -- act only when needed and only
270 AND
271 id_identity = %(pk_identity)s -- on names of this identity
272 AND
273 active IS TRUE -- which are active
274 AND
275 id != %(pk_name)s -- but NOT *this* name
276 """,
277 u"""update dem.names set
278 active = %(active_name)s,
279 preferred = %(preferred)s,
280 comment = %(comment)s
281 where
282 id = %(pk_name)s and
283 id_identity = %(pk_identity)s and -- belt and suspenders
284 xmin = %(xmin_name)s""",
285 u"""select xmin as xmin_name from dem.names where id = %(pk_name)s"""
286 ]
287 _updatable_fields = ['active_name', 'preferred', 'comment']
288
297
299 return '%(last)s, %(title)s %(first)s%(nick)s' % {
300 'last': self._payload[self._idx['lastnames']],
301 'title': gmTools.coalesce (
302 self._payload[self._idx['title']],
303 map_gender2salutation(self._payload[self._idx['gender']])
304 ),
305 'first': self._payload[self._idx['firstnames']],
306 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], u'', u" '%s'", u'%s')
307 }
308
309 description = property(_get_description, lambda x:x)
310
311 -class cIdentity(gmBusinessDBObject.cBusinessDBObject):
312 _cmd_fetch_payload = u"SELECT * FROM dem.v_basic_person WHERE pk_identity = %s"
313 _cmds_store_payload = [
314 u"""UPDATE dem.identity SET
315 gender = %(gender)s,
316 dob = %(dob)s,
317 tob = %(tob)s,
318 cob = gm.nullify_empty_string(%(cob)s),
319 title = gm.nullify_empty_string(%(title)s),
320 fk_marital_status = %(pk_marital_status)s,
321 karyotype = gm.nullify_empty_string(%(karyotype)s),
322 pupic = gm.nullify_empty_string(%(pupic)s),
323 deceased = %(deceased)s,
324 emergency_contact = gm.nullify_empty_string(%(emergency_contact)s),
325 fk_emergency_contact = %(pk_emergency_contact)s,
326 fk_primary_provider = %(pk_primary_provider)s,
327 comment = gm.nullify_empty_string(%(comment)s)
328 WHERE
329 pk = %(pk_identity)s and
330 xmin = %(xmin_identity)s
331 RETURNING
332 xmin AS xmin_identity"""
333 ]
334 _updatable_fields = [
335 "title",
336 "dob",
337 "tob",
338 "cob",
339 "gender",
340 "pk_marital_status",
341 "karyotype",
342 "pupic",
343 'deceased',
344 'emergency_contact',
345 'pk_emergency_contact',
346 'pk_primary_provider',
347 'comment'
348 ]
349
351 return self._payload[self._idx['pk_identity']]
353 raise AttributeError('setting ID of identity is not allowed')
354 ID = property(_get_ID, _set_ID)
355
357
358 if attribute == 'dob':
359 if value is not None:
360
361 if isinstance(value, pyDT.datetime):
362 if value.tzinfo is None:
363 raise ValueError('datetime.datetime instance is lacking a time zone: [%s]' % dt.isoformat())
364 else:
365 raise TypeError, '[%s]: type [%s] (%s) invalid for attribute [dob], must be datetime.datetime or None' % (self.__class__.__name__, type(value), value)
366
367
368 if self._payload[self._idx['dob']] is not None:
369 old_dob = gmDateTime.pydt_strftime (
370 self._payload[self._idx['dob']],
371 format = '%Y %m %d %H %M %S',
372 accuracy = gmDateTime.acc_seconds
373 )
374 new_dob = gmDateTime.pydt_strftime (
375 value,
376 format = '%Y %m %d %H %M %S',
377 accuracy = gmDateTime.acc_seconds
378 )
379 if new_dob == old_dob:
380 return
381
382 gmBusinessDBObject.cBusinessDBObject.__setitem__(self, attribute, value)
383
386
388 cmd = u"""
389 SELECT EXISTS (
390 SELECT 1
391 FROM clin.v_emr_journal
392 WHERE
393 pk_patient = %(pat)s
394 AND
395 soap_cat IS NOT NULL
396 )"""
397 args = {'pat': self._payload[self._idx['pk_identity']]}
398 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
399 return rows[0][0]
400
402 raise AttributeError('setting is_patient status of identity is not allowed')
403
404 is_patient = property(_get_is_patient, _set_is_patient)
405
407 cmd = u"SELECT pk FROM dem.staff WHERE fk_identity = %(pk)s"
408 args = {'pk': self._payload[self._idx['pk_identity']]}
409 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False)
410 if len(rows) == 0:
411 return None
412 return rows[0][0]
413
414 staff_id = property(_get_staff_id, lambda x:x)
415
416
417
420
421 gender_symbol = property(_get_gender_symbol, lambda x:x)
422
424 names = self.get_names(active_only = True)
425 if len(names) == 0:
426 _log.error('cannot retrieve active name for patient [%s]', self._payload[self._idx['pk_identity']])
427 return None
428 return names[0]
429
430 active_name = property(get_active_name, lambda x:x)
431
432 - def get_names(self, active_only=False, exclude_active=False):
433
434 args = {'pk_pat': self._payload[self._idx['pk_identity']]}
435 where_parts = [u'pk_identity = %(pk_pat)s']
436 if active_only:
437 where_parts.append(u'active_name is True')
438 if exclude_active:
439 where_parts.append(u'active_name is False')
440 cmd = u"""
441 SELECT *
442 FROM dem.v_person_names
443 WHERE %s
444 ORDER BY active_name DESC, lastnames, firstnames
445 """ % u' AND '.join(where_parts)
446 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True)
447
448 if len(rows) == 0:
449
450 return []
451
452 names = [ cPersonName(row = {'idx': idx, 'data': r, 'pk_field': 'pk_name'}) for r in rows ]
453 return names
454
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 - def export_as_gdt(self, filename=None, encoding='iso-8859-15', external_id_type=None):
763
764 template = u'%s%s%s\r\n'
765
766 file = codecs.open (
767 filename = filename,
768 mode = 'wb',
769 encoding = encoding,
770 errors = 'strict'
771 )
772
773 file.write(template % (u'013', u'8000', u'6301'))
774 file.write(template % (u'013', u'9218', u'2.10'))
775 if external_id_type is None:
776 file.write(template % (u'%03d' % (9 + len(str(self.ID))), u'3000', self.ID))
777 else:
778 ext_ids = self.get_external_ids(id_type = external_id_type)
779 if len(ext_ids) > 0:
780 file.write(template % (u'%03d' % (9 + len(ext_ids[0]['value'])), u'3000', ext_ids[0]['value']))
781 file.write(template % (u'%03d' % (9 + len(self._payload[self._idx['lastnames']])), u'3101', self._payload[self._idx['lastnames']]))
782 file.write(template % (u'%03d' % (9 + len(self._payload[self._idx['firstnames']])), u'3102', self._payload[self._idx['firstnames']]))
783 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')))
784 file.write(template % (u'010', u'3110', gmXdtMappings.map_gender_gm2xdt[self._payload[self._idx['gender']]]))
785 file.write(template % (u'025', u'6330', 'GNUmed::9206::encoding'))
786 file.write(template % (u'%03d' % (9 + len(encoding)), u'6331', encoding))
787 if external_id_type is None:
788 file.write(template % (u'029', u'6332', u'GNUmed::3000::source'))
789 file.write(template % (u'017', u'6333', u'internal'))
790 else:
791 if len(ext_ids) > 0:
792 file.write(template % (u'029', u'6332', u'GNUmed::3000::source'))
793 file.write(template % (u'%03d' % (9 + len(external_id_type)), u'6333', external_id_type))
794
795 file.close()
796
797
798
801
803 """Link an occupation with a patient, creating the occupation if it does not exists.
804
805 @param occupation The name of the occupation to link the patient to.
806 """
807 if (activities is None) and (occupation is None):
808 return True
809
810 occupation = occupation.strip()
811 if len(occupation) == 0:
812 return True
813
814 if activities is not None:
815 activities = activities.strip()
816
817 args = {'act': activities, 'pat_id': self.pk_obj, 'job': occupation}
818
819 cmd = u"select activities from dem.v_person_jobs where pk_identity = %(pat_id)s and l10n_occupation = _(%(job)s)"
820 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
821
822 queries = []
823 if len(rows) == 0:
824 queries.append ({
825 'cmd': u"INSERT INTO dem.lnk_job2person (fk_identity, fk_occupation, activities) VALUES (%(pat_id)s, dem.create_occupation(%(job)s), %(act)s)",
826 'args': args
827 })
828 else:
829 if rows[0]['activities'] != activities:
830 queries.append ({
831 '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))",
832 'args': args
833 })
834
835 rows, idx = gmPG2.run_rw_queries(queries = queries)
836
837 return True
838
840 if occupation is None:
841 return True
842 occupation = occupation.strip()
843 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))"
844 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj, 'job': occupation}}])
845 return True
846
847
848
850 cmd = u"select * from dem.v_person_comms where pk_identity = %s"
851 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}], get_col_idx = True)
852
853 filtered = rows
854
855 if comm_medium is not None:
856 filtered = []
857 for row in rows:
858 if row['comm_type'] == comm_medium:
859 filtered.append(row)
860
861 return [ gmDemographicRecord.cCommChannel(row = {
862 'pk_field': 'pk_lnk_identity2comm',
863 'data': r,
864 'idx': idx
865 }) for r in filtered
866 ]
867
868 - def link_comm_channel(self, comm_medium=None, url=None, is_confidential=False, pk_channel_type=None):
869 """Link a communication medium with a patient.
870
871 @param comm_medium The name of the communication medium.
872 @param url The communication resource locator.
873 @type url A types.StringType instance.
874 @param is_confidential Wether the data must be treated as confidential.
875 @type is_confidential A types.BooleanType instance.
876 """
877 comm_channel = gmDemographicRecord.create_comm_channel (
878 comm_medium = comm_medium,
879 url = url,
880 is_confidential = is_confidential,
881 pk_channel_type = pk_channel_type,
882 pk_identity = self.pk_obj
883 )
884 return comm_channel
885
891
892
893
895 cmd = u"select * from dem.v_pat_addresses where pk_identity=%s"
896 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}], get_col_idx=True)
897 addresses = []
898 for r in rows:
899 addresses.append(gmDemographicRecord.cPatientAddress(row={'idx': idx, 'data': r, 'pk_field': 'pk_address'}))
900
901 filtered = addresses
902
903 if address_type is not None:
904 filtered = []
905 for adr in addresses:
906 if adr['address_type'] == address_type:
907 filtered.append(adr)
908
909 return filtered
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
1072
1073 - def dob_in_range(self, min_distance=u'1 week', max_distance=u'1 week'):
1074 cmd = u'select dem.dob_is_in_range(%(dob)s, %(min)s, %(max)s)'
1075 rows, idx = gmPG2.run_ro_queries (
1076 queries = [{
1077 'cmd': cmd,
1078 'args': {'dob': self['dob'], 'min': min_distance, 'max': max_distance}
1079 }]
1080 )
1081 return rows[0][0]
1082
1083
1084
1086 cmd = u'select * from clin.v_most_recent_encounters where pk_patient=%s'
1087 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self._payload[self._idx['pk_identity']]]}])
1088 if len(rows) > 0:
1089 return rows[0]
1090 else:
1091 return None
1092
1095
1098
1099 messages = property(_get_messages, _set_messages)
1100
1103
1105 if self._payload[self._idx['pk_primary_provider']] is None:
1106 return None
1107 from Gnumed.business import gmStaff
1108 return gmStaff.cStaff(aPK_obj = self._payload[self._idx['pk_primary_provider']])
1109
1110 primary_provider = property(_get_primary_provider, lambda x:x)
1111
1112
1113
1115 """Format patient demographics into patient specific path name fragment."""
1116 return '%s-%s%s-%s' % (
1117 self._payload[self._idx['lastnames']].replace(u' ', u'_'),
1118 self._payload[self._idx['firstnames']].replace(u' ', u'_'),
1119 gmTools.coalesce(self._payload[self._idx['preferred']], u'', template_initial = u'-(%s)'),
1120 self.get_formatted_dob(format = '%Y-%m-%d', encoding = gmI18N.get_encoding())
1121 )
1122
1124 """Represents a person which is a patient.
1125
1126 - a specializing subclass of cIdentity turning it into a patient
1127 - its use is to cache subobjects like EMR and document folder
1128 """
1129 - def __init__(self, aPK_obj=None, row=None):
1130 cIdentity.__init__(self, aPK_obj=aPK_obj, row=row)
1131 self.__db_cache = {}
1132 self.__emr_access_lock = threading.Lock()
1133
1135 """Do cleanups before dying.
1136
1137 - note that this may be called in a thread
1138 """
1139 if self.__db_cache.has_key('clinical record'):
1140 self.__db_cache['clinical record'].cleanup()
1141 if self.__db_cache.has_key('document folder'):
1142 self.__db_cache['document folder'].cleanup()
1143 cIdentity.cleanup(self)
1144
1146
1147
1148
1149
1150
1151
1152
1153
1154 if not self.__emr_access_lock.acquire(False):
1155 raise AttributeError('cannot access EMR')
1156 try:
1157 emr = self.__db_cache['clinical record']
1158 self.__emr_access_lock.release()
1159 return emr
1160 except KeyError:
1161 pass
1162
1163 self.__db_cache['clinical record'] = gmClinicalRecord.cClinicalRecord(aPKey = self._payload[self._idx['pk_identity']])
1164 self.__emr_access_lock.release()
1165 return self.__db_cache['clinical record']
1166
1167 emr = property(get_emr, lambda x:x)
1168
1170 try:
1171 return self.__db_cache['document folder']
1172 except KeyError:
1173 pass
1174
1175 self.__db_cache['document folder'] = cDocumentFolder(aPKey = self._payload[self._idx['pk_identity']])
1176 return self.__db_cache['document folder']
1177
1178 document_folder = property(get_document_folder, lambda x:x)
1179
1181 """Patient Borg to hold currently active patient.
1182
1183 There may be many instances of this but they all share state.
1184 """
1185 - def __init__(self, patient=None, forced_reload=False):
1186 """Change or get currently active patient.
1187
1188 patient:
1189 * None: get currently active patient
1190 * -1: unset currently active patient
1191 * cPatient instance: set active patient if possible
1192 """
1193
1194 try:
1195 tmp = self.patient
1196 except AttributeError:
1197 self.patient = gmNull.cNull()
1198 self.__register_interests()
1199
1200
1201
1202 self.__lock_depth = 0
1203
1204 self.__pre_selection_callbacks = []
1205
1206
1207 if patient is None:
1208 return None
1209
1210
1211 if self.locked:
1212 _log.error('patient [%s] is locked, cannot change to [%s]' % (self.patient['pk_identity'], patient))
1213 return None
1214
1215
1216 if patient == -1:
1217 _log.debug('explicitly unsetting current patient')
1218 if not self.__run_pre_selection_callbacks():
1219 _log.debug('not unsetting current patient')
1220 return None
1221 self.__send_pre_selection_notification()
1222 self.patient.cleanup()
1223 self.patient = gmNull.cNull()
1224 self.__send_selection_notification()
1225 return None
1226
1227
1228 if not isinstance(patient, cPatient):
1229 _log.error('cannot set active patient to [%s], must be either None, -1 or cPatient instance' % str(patient))
1230 raise TypeError, 'gmPerson.gmCurrentPatient.__init__(): <patient> must be None, -1 or cPatient instance but is: %s' % str(patient)
1231
1232
1233 if (self.patient['pk_identity'] == patient['pk_identity']) and not forced_reload:
1234 return None
1235
1236
1237 _log.debug('patient change [%s] -> [%s] requested', self.patient['pk_identity'], patient['pk_identity'])
1238
1239
1240 if not self.__run_pre_selection_callbacks():
1241 _log.debug('not changing current patient')
1242 return None
1243 self.__send_pre_selection_notification()
1244 self.patient.cleanup()
1245 self.patient = patient
1246 self.patient.get_emr()
1247 self.__send_selection_notification()
1248
1249 return None
1250
1254
1258
1259
1260
1262 if not callable(callback):
1263 raise TypeError(u'callback [%s] not callable' % callback)
1264
1265 self.__pre_selection_callbacks.append(callback)
1266
1269
1271 raise AttributeError(u'invalid to set <connected> state')
1272
1273 connected = property(_get_connected, _set_connected)
1274
1276 return (self.__lock_depth > 0)
1277
1279 if locked:
1280 self.__lock_depth = self.__lock_depth + 1
1281 gmDispatcher.send(signal='patient_locked')
1282 else:
1283 if self.__lock_depth == 0:
1284 _log.error('lock/unlock imbalance, trying to refcount lock depth below 0')
1285 return
1286 else:
1287 self.__lock_depth = self.__lock_depth - 1
1288 gmDispatcher.send(signal='patient_unlocked')
1289
1290 locked = property(_get_locked, _set_locked)
1291
1293 _log.info('forced patient unlock at lock depth [%s]' % self.__lock_depth)
1294 self.__lock_depth = 0
1295 gmDispatcher.send(signal='patient_unlocked')
1296
1297
1298
1300 if isinstance(self.patient, gmNull.cNull):
1301 return True
1302
1303 for call_back in self.__pre_selection_callbacks:
1304 try:
1305 successful = call_back()
1306 except:
1307 _log.exception('callback [%s] failed', call_back)
1308 print "*** pre-selection callback failed ***"
1309 print type(call_back)
1310 print call_back
1311 return False
1312
1313 if not successful:
1314 _log.debug('callback [%s] returned False', call_back)
1315 return False
1316
1317 return True
1318
1320 """Sends signal when another patient is about to become active.
1321
1322 This does NOT wait for signal handlers to complete.
1323 """
1324 kwargs = {
1325 'signal': u'pre_patient_selection',
1326 'sender': id(self.__class__),
1327 'pk_identity': self.patient['pk_identity']
1328 }
1329 gmDispatcher.send(**kwargs)
1330
1332 """Sends signal when another patient has actually been made active."""
1333 kwargs = {
1334 'signal': u'post_patient_selection',
1335 'sender': id(self.__class__),
1336 'pk_identity': self.patient['pk_identity']
1337 }
1338 gmDispatcher.send(**kwargs)
1339
1340
1341
1343 if attribute == 'patient':
1344 raise AttributeError
1345 if not isinstance(self.patient, gmNull.cNull):
1346 return getattr(self.patient, attribute)
1347
1348
1349
1351 """Return any attribute if known how to retrieve it by proxy.
1352 """
1353 return self.patient[attribute]
1354
1357
1358
1359
1362 gmMatchProvider.cMatchProvider_SQL2.__init__(
1363 self,
1364 queries = [
1365 u"""SELECT
1366 pk_staff AS data,
1367 short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS list_label,
1368 short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS field_label
1369 FROM dem.v_staff
1370 WHERE
1371 is_active AND (
1372 short_alias %(fragment_condition)s OR
1373 firstnames %(fragment_condition)s OR
1374 lastnames %(fragment_condition)s OR
1375 db_user %(fragment_condition)s
1376 )
1377 """
1378 ]
1379 )
1380 self.setThresholds(1, 2, 3)
1381
1382
1383
1384 -def create_name(pk_person, firstnames, lastnames, active=False):
1385 queries = [{
1386 'cmd': u"select dem.add_name(%s, %s, %s, %s)",
1387 'args': [pk_person, firstnames, lastnames, active]
1388 }]
1389 rows, idx = gmPG2.run_rw_queries(queries=queries, return_data=True)
1390 name = cPersonName(aPK_obj = rows[0][0])
1391 return name
1392
1393 -def create_identity(gender=None, dob=None, lastnames=None, firstnames=None):
1394
1395 cmd1 = u"""INSERT INTO dem.identity (gender, dob) VALUES (%s, %s)"""
1396 cmd2 = u"""
1397 INSERT INTO dem.names (
1398 id_identity, lastnames, firstnames
1399 ) VALUES (
1400 currval('dem.identity_pk_seq'), coalesce(%s, 'xxxDEFAULTxxx'), coalesce(%s, 'xxxDEFAULTxxx')
1401 ) RETURNING id_identity"""
1402 rows, idx = gmPG2.run_rw_queries (
1403 queries = [
1404 {'cmd': cmd1, 'args': [gender, dob]},
1405 {'cmd': cmd2, 'args': [lastnames, firstnames]}
1406 ],
1407 return_data = True
1408 )
1409 ident = cIdentity(aPK_obj=rows[0][0])
1410 gmHooks.run_hook_script(hook = u'post_person_creation')
1411 return ident
1412
1420
1447
1448
1449
1460
1461 map_gender2mf = {
1462 'm': u'm',
1463 'f': u'f',
1464 'tf': u'f',
1465 'tm': u'm',
1466 'h': u'mf'
1467 }
1468
1469
1470 map_gender2symbol = {
1471 'm': u'\u2642',
1472 'f': u'\u2640',
1473 'tf': u'\u26A5\u2640',
1474 'tm': u'\u26A5\u2642',
1475 'h': u'\u26A5'
1476
1477
1478
1479 }
1480
1501
1503 """Try getting the gender for the given first name."""
1504
1505 if firstnames is None:
1506 return None
1507
1508 rows, idx = gmPG2.run_ro_queries(queries = [{
1509 'cmd': u"select gender from dem.name_gender_map where name ilike %(fn)s limit 1",
1510 'args': {'fn': firstnames}
1511 }])
1512
1513 if len(rows) == 0:
1514 return None
1515
1516 return rows[0][0]
1517
1519 return [ cIdentity(aPK_obj = pk) for pk in pks ]
1520
1524
1528
1529
1530
1531 if __name__ == '__main__':
1532
1533 if len(sys.argv) == 1:
1534 sys.exit()
1535
1536 if sys.argv[1] != 'test':
1537 sys.exit()
1538
1539 import datetime
1540
1541 gmI18N.activate_locale()
1542 gmI18N.install_domain()
1543 gmDateTime.init()
1544
1545
1566
1568 dto = cDTO_person()
1569 dto.firstnames = 'Sepp'
1570 dto.lastnames = 'Herberger'
1571 dto.gender = 'male'
1572 dto.dob = pyDT.datetime.now(tz=gmDateTime.gmCurrentLocalTimezone)
1573 print dto
1574
1575 print dto['firstnames']
1576 print dto['lastnames']
1577 print dto['gender']
1578 print dto['dob']
1579
1580 for key in dto.keys():
1581 print key
1582
1584
1585 print '\n\nCreating identity...'
1586 new_identity = create_identity(gender='m', dob='2005-01-01', lastnames='test lastnames', firstnames='test firstnames')
1587 print 'Identity created: %s' % new_identity
1588
1589 print '\nSetting title and gender...'
1590 new_identity['title'] = 'test title';
1591 new_identity['gender'] = 'f';
1592 new_identity.save_payload()
1593 print 'Refetching identity from db: %s' % cIdentity(aPK_obj=new_identity['pk_identity'])
1594
1595 print '\nGetting all names...'
1596 for a_name in new_identity.get_names():
1597 print a_name
1598 print 'Active name: %s' % (new_identity.get_active_name())
1599 print 'Setting nickname...'
1600 new_identity.set_nickname(nickname='test nickname')
1601 print 'Refetching all names...'
1602 for a_name in new_identity.get_names():
1603 print a_name
1604 print 'Active name: %s' % (new_identity.get_active_name())
1605
1606 print '\nIdentity occupations: %s' % new_identity['occupations']
1607 print 'Creating identity occupation...'
1608 new_identity.link_occupation('test occupation')
1609 print 'Identity occupations: %s' % new_identity['occupations']
1610
1611 print '\nIdentity addresses: %s' % new_identity.get_addresses()
1612 print 'Creating identity address...'
1613
1614 new_identity.link_address (
1615 number = 'test 1234',
1616 street = 'test street',
1617 postcode = 'test postcode',
1618 urb = 'test urb',
1619 state = 'SN',
1620 country = 'DE'
1621 )
1622 print 'Identity addresses: %s' % new_identity.get_addresses()
1623
1624 print '\nIdentity communications: %s' % new_identity.get_comm_channels()
1625 print 'Creating identity communication...'
1626 new_identity.link_comm_channel('homephone', '1234566')
1627 print 'Identity communications: %s' % new_identity.get_comm_channels()
1628
1634
1635
1636
1637
1638
1639 test_name()
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652