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