Package Gnumed :: Package business :: Module gmPerson
[frames] | no frames]

Source Code for Module Gnumed.business.gmPerson

   1  # -*- coding: utf8 -*- 
   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  __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>" 
   9  __license__ = "GPL" 
  10   
  11  # std lib 
  12  import sys 
  13  import os.path 
  14  import time 
  15  import re as regex 
  16  import datetime as pyDT 
  17  import codecs 
  18  import threading 
  19  import logging 
  20   
  21   
  22  # GNUmed 
  23  if __name__ == '__main__': 
  24          sys.path.insert(0, '../../') 
  25  from Gnumed.pycommon import gmExceptions 
  26  from Gnumed.pycommon import gmDispatcher 
  27  from Gnumed.pycommon import gmBorg 
  28  from Gnumed.pycommon import gmI18N 
  29  from Gnumed.pycommon import gmNull 
  30  from Gnumed.pycommon import gmBusinessDBObject 
  31  from Gnumed.pycommon import gmTools 
  32  from Gnumed.pycommon import gmPG2 
  33  from Gnumed.pycommon import gmDateTime 
  34  from Gnumed.pycommon import gmMatchProvider 
  35  from Gnumed.pycommon import gmLog2 
  36  from Gnumed.pycommon import gmHooks 
  37   
  38  from Gnumed.business import gmDemographicRecord 
  39  from Gnumed.business import gmClinicalRecord 
  40  from Gnumed.business import gmXdtMappings 
  41  from Gnumed.business import gmProviderInbox 
  42  from Gnumed.business.gmDocuments import cDocumentFolder 
  43   
  44   
  45  _log = logging.getLogger('gm.person') 
  46   
  47  __gender_list = None 
  48  __gender_idx = None 
  49   
  50  __gender2salutation_map = None 
  51  __gender2string_map = None 
  52   
  53   
  54  #============================================================ 
55 -def external_id_exists(pk_issuer, value):
56 cmd = u'SELECT COUNT(1) FROM dem.lnk_identity2ext_id WHERE fk_origin = %(issuer)s AND external_id = %(val)s' 57 args = {'issuer': pk_issuer, 'val': value} 58 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 59 return rows[0][0]
60 61 #============================================================
62 -def person_exists(lastnames, dob, firstnames=None):
63 args = { 64 'last': lastnames, 65 'dob': dob 66 } 67 where_parts = [ 68 u"lastnames = %(last)s", 69 u"dem.date_trunc_utc('day', dob) = dem.date_trunc_utc('day', %(dob)s)" 70 ] 71 if firstnames is not None: 72 if firstnames.strip() != u'': 73 #where_parts.append(u"position(%(first)s in firstnames) = 1") 74 where_parts.append(u"firstnames ~* %(first)s") 75 args['first'] = u'\\m' + firstnames 76 cmd = u"""SELECT COUNT(1) FROM dem.v_basic_person WHERE %s""" % u' AND '.join(where_parts) 77 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 78 return rows[0][0]
79 80 #============================================================ 81 # FIXME: make this work as a mapping type, too
82 -class cDTO_person(object):
83
84 - def __init__(self):
85 self.identity = None 86 self.external_ids = [] 87 self.comm_channels = [] 88 self.addresses = []
89 #-------------------------------------------------------- 90 # external API 91 #--------------------------------------------------------
92 - def keys(self):
93 return 'firstnames lastnames dob gender'.split()
94 #--------------------------------------------------------
95 - def delete_from_source(self):
96 pass
97 #--------------------------------------------------------
98 - def get_candidate_identities(self, can_create=False):
99 """Generate generic queries. 100 101 - not locale dependant 102 - data -> firstnames, lastnames, dob, gender 103 104 shall we mogrify name parts ? probably not as external 105 sources should know what they do 106 107 finds by inactive name, too, but then shows 108 the corresponding active name ;-) 109 110 Returns list of matching identities (may be empty) 111 or None if it was told to create an identity but couldn't. 112 """ 113 where_snippets = [] 114 args = {} 115 116 where_snippets.append(u'firstnames = %(first)s') 117 args['first'] = self.firstnames 118 119 where_snippets.append(u'lastnames = %(last)s') 120 args['last'] = self.lastnames 121 122 if self.dob is not None: 123 where_snippets.append(u"dem.date_trunc_utc('day'::text, dob) = dem.date_trunc_utc('day'::text, %(dob)s)") 124 args['dob'] = self.dob.replace(hour = 23, minute = 59, second = 59) 125 126 if self.gender is not None: 127 where_snippets.append('gender = %(sex)s') 128 args['sex'] = self.gender 129 130 cmd = u""" 131 SELECT *, '%s' AS match_type 132 FROM dem.v_basic_person 133 WHERE 134 pk_identity IN ( 135 SELECT pk_identity FROM dem.v_person_names WHERE %s 136 ) 137 ORDER BY lastnames, firstnames, dob""" % ( 138 _('external patient source (name, gender, date of birth)'), 139 ' AND '.join(where_snippets) 140 ) 141 142 try: 143 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx=True) 144 except: 145 _log.error(u'cannot get candidate identities for dto "%s"' % self) 146 _log.exception('query %s' % cmd) 147 rows = [] 148 149 if len(rows) == 0: 150 _log.debug('no candidate identity matches found') 151 if not can_create: 152 return [] 153 ident = self.import_into_database() 154 if ident is None: 155 return None 156 identities = [ident] 157 else: 158 identities = [ cIdentity(row = {'pk_field': 'pk_identity', 'data': row, 'idx': idx}) for row in rows ] 159 160 return identities
161 #--------------------------------------------------------
162 - def import_into_database(self):
163 """Imports self into the database.""" 164 165 self.identity = create_identity ( 166 firstnames = self.firstnames, 167 lastnames = self.lastnames, 168 gender = self.gender, 169 dob = self.dob 170 ) 171 172 if self.identity is None: 173 return None 174 175 for ext_id in self.external_ids: 176 try: 177 self.identity.add_external_id ( 178 type_name = ext_id['name'], 179 value = ext_id['value'], 180 issuer = ext_id['issuer'], 181 comment = ext_id['comment'] 182 ) 183 except StandardError: 184 _log.exception('cannot import <external ID> from external data source') 185 _log.log_stack_trace() 186 187 for comm in self.comm_channels: 188 try: 189 self.identity.link_comm_channel ( 190 comm_medium = comm['channel'], 191 url = comm['url'] 192 ) 193 except StandardError: 194 _log.exception('cannot import <comm channel> from external data source') 195 _log.log_stack_trace() 196 197 for adr in self.addresses: 198 try: 199 self.identity.link_address ( 200 number = adr['number'], 201 street = adr['street'], 202 postcode = adr['zip'], 203 urb = adr['urb'], 204 state = adr['region'], 205 country = adr['country'] 206 ) 207 except StandardError: 208 _log.exception('cannot import <address> from external data source') 209 _log.log_stack_trace() 210 211 return self.identity
212 #--------------------------------------------------------
213 - def import_extra_data(self, *args, **kwargs):
214 pass
215 #--------------------------------------------------------
216 - def remember_external_id(self, name=None, value=None, issuer=None, comment=None):
217 value = value.strip() 218 if value == u'': 219 return 220 name = name.strip() 221 if name == u'': 222 raise ValueError(_('<name> cannot be empty')) 223 issuer = issuer.strip() 224 if issuer == u'': 225 raise ValueError(_('<issuer> cannot be empty')) 226 self.external_ids.append({'name': name, 'value': value, 'issuer': issuer, 'comment': comment})
227 #--------------------------------------------------------
228 - def remember_comm_channel(self, channel=None, url=None):
229 url = url.strip() 230 if url == u'': 231 return 232 channel = channel.strip() 233 if channel == u'': 234 raise ValueError(_('<channel> cannot be empty')) 235 self.comm_channels.append({'channel': channel, 'url': url})
236 #--------------------------------------------------------
237 - def remember_address(self, number=None, street=None, urb=None, region=None, zip=None, country=None):
238 number = number.strip() 239 if number == u'': 240 raise ValueError(_('<number> cannot be empty')) 241 street = street.strip() 242 if street == u'': 243 raise ValueError(_('<street> cannot be empty')) 244 urb = urb.strip() 245 if urb == u'': 246 raise ValueError(_('<urb> cannot be empty')) 247 zip = zip.strip() 248 if zip == u'': 249 raise ValueError(_('<zip> cannot be empty')) 250 country = country.strip() 251 if country == u'': 252 raise ValueError(_('<country> cannot be empty')) 253 region = region.strip() 254 if region == u'': 255 region = u'??' 256 self.addresses.append ({ 257 u'number': number, 258 u'street': street, 259 u'zip': zip, 260 u'urb': urb, 261 u'region': region, 262 u'country': country 263 })
264 #-------------------------------------------------------- 265 # customizing behaviour 266 #--------------------------------------------------------
267 - def __str__(self):
268 return u'<%s @ %s: %s %s (%s) %s>' % ( 269 self.__class__.__name__, 270 id(self), 271 self.firstnames, 272 self.lastnames, 273 self.gender, 274 self.dob 275 )
276 #--------------------------------------------------------
277 - def __setattr__(self, attr, val):
278 """Do some sanity checks on self.* access.""" 279 280 if attr == 'gender': 281 glist, idx = get_gender_list() 282 for gender in glist: 283 if str(val) in [gender[0], gender[1], gender[2], gender[3]]: 284 val = gender[idx['tag']] 285 object.__setattr__(self, attr, val) 286 return 287 raise ValueError('invalid gender: [%s]' % val) 288 289 if attr == 'dob': 290 if val is not None: 291 if not isinstance(val, pyDT.datetime): 292 raise TypeError('invalid type for DOB (must be datetime.datetime): %s [%s]' % (type(val), val)) 293 if val.tzinfo is None: 294 raise ValueError('datetime.datetime instance is lacking a time zone: [%s]' % val.isoformat()) 295 296 object.__setattr__(self, attr, val) 297 return
298 #--------------------------------------------------------
299 - def __getitem__(self, attr):
300 return getattr(self, attr)
301 #============================================================
302 -class cPersonName(gmBusinessDBObject.cBusinessDBObject):
303 _cmd_fetch_payload = u"SELECT * FROM dem.v_person_names WHERE pk_name = %s" 304 _cmds_store_payload = [ 305 u"""UPDATE dem.names SET 306 active = FALSE 307 WHERE 308 %(active_name)s IS TRUE -- act only when needed and only 309 AND 310 id_identity = %(pk_identity)s -- on names of this identity 311 AND 312 active IS TRUE -- which are active 313 AND 314 id != %(pk_name)s -- but NOT *this* name 315 """, 316 u"""update dem.names set 317 active = %(active_name)s, 318 preferred = %(preferred)s, 319 comment = %(comment)s 320 where 321 id = %(pk_name)s and 322 id_identity = %(pk_identity)s and -- belt and suspenders 323 xmin = %(xmin_name)s""", 324 u"""select xmin as xmin_name from dem.names where id = %(pk_name)s""" 325 ] 326 _updatable_fields = ['active_name', 'preferred', 'comment'] 327 #--------------------------------------------------------
328 - def __setitem__(self, attribute, value):
329 if attribute == 'active_name': 330 # cannot *directly* deactivate a name, only indirectly 331 # by activating another one 332 # FIXME: should be done at DB level 333 if self._payload[self._idx['active_name']] is True: 334 return 335 gmBusinessDBObject.cBusinessDBObject.__setitem__(self, attribute, value)
336 #--------------------------------------------------------
337 - def _get_description(self):
338 return '%(last)s, %(title)s %(first)s%(nick)s' % { 339 'last': self._payload[self._idx['lastnames']], 340 'title': gmTools.coalesce ( 341 self._payload[self._idx['title']], 342 map_gender2salutation(self._payload[self._idx['gender']]) 343 ), 344 'first': self._payload[self._idx['firstnames']], 345 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], u'', u" '%s'", u'%s') 346 }
347 348 description = property(_get_description, lambda x:x)
349 #============================================================
350 -class cIdentity(gmBusinessDBObject.cBusinessDBObject):
351 _cmd_fetch_payload = u"SELECT * FROM dem.v_basic_person WHERE pk_identity = %s" 352 _cmds_store_payload = [ 353 u"""UPDATE dem.identity SET 354 gender = %(gender)s, 355 dob = %(dob)s, 356 dob_is_estimated = %(dob_is_estimated)s, 357 tob = %(tob)s, 358 cob = gm.nullify_empty_string(%(cob)s), 359 title = gm.nullify_empty_string(%(title)s), 360 fk_marital_status = %(pk_marital_status)s, 361 karyotype = gm.nullify_empty_string(%(karyotype)s), 362 pupic = gm.nullify_empty_string(%(pupic)s), 363 deceased = %(deceased)s, 364 emergency_contact = gm.nullify_empty_string(%(emergency_contact)s), 365 fk_emergency_contact = %(pk_emergency_contact)s, 366 fk_primary_provider = %(pk_primary_provider)s, 367 comment = gm.nullify_empty_string(%(comment)s) 368 WHERE 369 pk = %(pk_identity)s and 370 xmin = %(xmin_identity)s 371 RETURNING 372 xmin AS xmin_identity""" 373 ] 374 _updatable_fields = [ 375 "title", 376 "dob", 377 "tob", 378 "cob", 379 "gender", 380 "pk_marital_status", 381 "karyotype", 382 "pupic", 383 'deceased', 384 'emergency_contact', 385 'pk_emergency_contact', 386 'pk_primary_provider', 387 'comment', 388 'dob_is_estimated' 389 ] 390 #--------------------------------------------------------
391 - def _get_ID(self):
392 return self._payload[self._idx['pk_identity']]
393 - def _set_ID(self, value):
394 raise AttributeError('setting ID of identity is not allowed')
395 ID = property(_get_ID, _set_ID) 396 #--------------------------------------------------------
397 - def __setitem__(self, attribute, value):
398 399 if attribute == 'dob': 400 if value is not None: 401 402 if isinstance(value, pyDT.datetime): 403 if value.tzinfo is None: 404 raise ValueError('datetime.datetime instance is lacking a time zone: [%s]' % dt.isoformat()) 405 else: 406 raise TypeError, '[%s]: type [%s] (%s) invalid for attribute [dob], must be datetime.datetime or None' % (self.__class__.__name__, type(value), value) 407 408 # compare DOB at seconds level 409 if self._payload[self._idx['dob']] is not None: 410 old_dob = gmDateTime.pydt_strftime ( 411 self._payload[self._idx['dob']], 412 format = '%Y %m %d %H %M %S', 413 accuracy = gmDateTime.acc_seconds 414 ) 415 new_dob = gmDateTime.pydt_strftime ( 416 value, 417 format = '%Y %m %d %H %M %S', 418 accuracy = gmDateTime.acc_seconds 419 ) 420 if new_dob == old_dob: 421 return 422 423 gmBusinessDBObject.cBusinessDBObject.__setitem__(self, attribute, value)
424 #--------------------------------------------------------
425 - def cleanup(self):
426 pass
427 #--------------------------------------------------------
428 - def _get_is_patient(self):
429 cmd = u""" 430 SELECT EXISTS ( 431 SELECT 1 432 FROM clin.v_emr_journal 433 WHERE 434 pk_patient = %(pat)s 435 AND 436 soap_cat IS NOT NULL 437 )""" 438 args = {'pat': self._payload[self._idx['pk_identity']]} 439 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 440 return rows[0][0]
441
442 - def _set_is_patient(self, value):
443 raise AttributeError('setting is_patient status of identity is not allowed')
444 445 is_patient = property(_get_is_patient, _set_is_patient) 446 #--------------------------------------------------------
447 - def _get_staff_id(self):
448 cmd = u"SELECT pk FROM dem.staff WHERE fk_identity = %(pk)s" 449 args = {'pk': self._payload[self._idx['pk_identity']]} 450 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 451 if len(rows) == 0: 452 return None 453 return rows[0][0]
454 455 staff_id = property(_get_staff_id, lambda x:x) 456 #-------------------------------------------------------- 457 # identity API 458 #--------------------------------------------------------
459 - def _get_gender_symbol(self):
460 return map_gender2symbol[self._payload[self._idx['gender']]]
461 462 gender_symbol = property(_get_gender_symbol, lambda x:x) 463 #--------------------------------------------------------
464 - def _get_gender_string(self):
465 return map_gender2string(gender = self._payload[self._idx['gender']])
466 467 gender_string = property(_get_gender_string, lambda x:x) 468 #--------------------------------------------------------
469 - def get_active_name(self):
470 names = self.get_names(active_only = True) 471 if len(names) == 0: 472 _log.error('cannot retrieve active name for patient [%s]', self._payload[self._idx['pk_identity']]) 473 return None 474 return names[0]
475 476 active_name = property(get_active_name, lambda x:x) 477 #--------------------------------------------------------
478 - def get_names(self, active_only=False, exclude_active=False):
479 480 args = {'pk_pat': self._payload[self._idx['pk_identity']]} 481 where_parts = [u'pk_identity = %(pk_pat)s'] 482 if active_only: 483 where_parts.append(u'active_name is True') 484 if exclude_active: 485 where_parts.append(u'active_name is False') 486 cmd = u""" 487 SELECT * 488 FROM dem.v_person_names 489 WHERE %s 490 ORDER BY active_name DESC, lastnames, firstnames 491 """ % u' AND '.join(where_parts) 492 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 493 494 if len(rows) == 0: 495 # no names registered for patient 496 return [] 497 498 names = [ cPersonName(row = {'idx': idx, 'data': r, 'pk_field': 'pk_name'}) for r in rows ] 499 return names
500 #--------------------------------------------------------
501 - def get_description_gender(self):
502 return _(u'%(last)s,%(title)s %(first)s%(nick)s (%(sex)s)') % { 503 'last': self._payload[self._idx['lastnames']], 504 'title': gmTools.coalesce(self._payload[self._idx['title']], u'', u' %s'), 505 'first': self._payload[self._idx['firstnames']], 506 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], u'', u" '%s'"), 507 'sex': self.gender_symbol 508 }
509 #--------------------------------------------------------
510 - def get_description(self):
511 return _(u'%(last)s,%(title)s %(first)s%(nick)s') % { 512 'last': self._payload[self._idx['lastnames']], 513 'title': gmTools.coalesce(self._payload[self._idx['title']], u'', u' %s'), 514 'first': self._payload[self._idx['firstnames']], 515 'nick': gmTools.coalesce(self._payload[self._idx['preferred']], u'', u" '%s'") 516 }
517 #--------------------------------------------------------
518 - def add_name(self, firstnames, lastnames, active=True):
519 """Add a name. 520 521 @param firstnames The first names. 522 @param lastnames The last names. 523 @param active When True, the new name will become the active one (hence setting other names to inactive) 524 @type active A types.BooleanType instance 525 """ 526 name = create_name(self.ID, firstnames, lastnames, active) 527 if active: 528 self.refetch_payload() 529 return name
530 #--------------------------------------------------------
531 - def delete_name(self, name=None):
532 cmd = u"delete from dem.names where id = %(name)s and id_identity = %(pat)s" 533 args = {'name': name['pk_name'], 'pat': self.ID} 534 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
535 # can't have been the active name as that would raise an 536 # exception (since no active name would be left) so no 537 # data refetch needed 538 #--------------------------------------------------------
539 - def set_nickname(self, nickname=None):
540 """ 541 Set the nickname. Setting the nickname only makes sense for the currently 542 active name. 543 @param nickname The preferred/nick/warrior name to set. 544 """ 545 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': u"select dem.set_nickname(%s, %s)", 'args': [self.ID, nickname]}]) 546 self.refetch_payload() 547 return True
548 #--------------------------------------------------------
549 - def get_tags(self, order_by=None):
550 if order_by is None: 551 order_by = u'' 552 else: 553 order_by = u'ORDER BY %s' % order_by 554 555 cmd = gmDemographicRecord._SQL_get_identity_tags % (u'pk_identity = %%(pat)s %s' % order_by) 556 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {u'pat': self.ID}}], get_col_idx = True) 557 558 return [ gmDemographicRecord.cIdentityTag(row = {'data': r, 'idx': idx, 'pk_field': 'pk_identity_tag'}) for r in rows ]
559 560 tags = property(get_tags, lambda x:x) 561 #--------------------------------------------------------
562 - def add_tag(self, tag):
563 args = { 564 u'tag': tag, 565 u'identity': self.ID 566 } 567 568 # already exists ? 569 cmd = u"SELECT pk FROM dem.identity_tag WHERE fk_tag = %(tag)s AND fk_identity = %(identity)s" 570 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = False) 571 if len(rows) > 0: 572 return gmDemographicRecord.cIdentityTag(aPK_obj = rows[0]['pk']) 573 574 # no, add 575 cmd = u""" 576 INSERT INTO dem.identity_tag ( 577 fk_tag, 578 fk_identity 579 ) VALUES ( 580 %(tag)s, 581 %(identity)s 582 ) 583 RETURNING pk 584 """ 585 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], return_data = True, get_col_idx = False) 586 return gmDemographicRecord.cIdentityTag(aPK_obj = rows[0]['pk'])
587 #--------------------------------------------------------
588 - def remove_tag(self, tag):
589 cmd = u"DELETE FROM dem.identity_tag WHERE pk = %(pk)s" 590 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': {'pk': tag}}])
591 #-------------------------------------------------------- 592 # external ID API 593 # 594 # since external IDs are not treated as first class 595 # citizens (classes in their own right, that is), we 596 # handle them *entirely* within cIdentity, also they 597 # only make sense with one single person (like names) 598 # and are not reused (like addresses), so they are 599 # truly added/deleted, not just linked/unlinked 600 #--------------------------------------------------------
601 - def add_external_id(self, type_name=None, value=None, issuer=None, comment=None, pk_type=None):
602 """Adds an external ID to the patient. 603 604 creates ID type if necessary 605 """ 606 607 # check for existing ID 608 if pk_type is not None: 609 cmd = u""" 610 select * from dem.v_external_ids4identity where 611 pk_identity = %(pat)s and 612 pk_type = %(pk_type)s and 613 value = %(val)s""" 614 else: 615 # by type/value/issuer 616 if issuer is None: 617 cmd = u""" 618 select * from dem.v_external_ids4identity where 619 pk_identity = %(pat)s and 620 name = %(name)s and 621 value = %(val)s""" 622 else: 623 cmd = u""" 624 select * from dem.v_external_ids4identity where 625 pk_identity = %(pat)s and 626 name = %(name)s and 627 value = %(val)s and 628 issuer = %(issuer)s""" 629 args = { 630 'pat': self.ID, 631 'name': type_name, 632 'val': value, 633 'issuer': issuer, 634 'pk_type': pk_type 635 } 636 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}]) 637 638 # create new ID if not found 639 if len(rows) == 0: 640 641 args = { 642 'pat': self.ID, 643 'val': value, 644 'type_name': type_name, 645 'pk_type': pk_type, 646 'issuer': issuer, 647 'comment': comment 648 } 649 650 if pk_type is None: 651 cmd = u"""insert into dem.lnk_identity2ext_id (external_id, fk_origin, comment, id_identity) values ( 652 %(val)s, 653 (select dem.add_external_id_type(%(type_name)s, %(issuer)s)), 654 %(comment)s, 655 %(pat)s 656 )""" 657 else: 658 cmd = u"""insert into dem.lnk_identity2ext_id (external_id, fk_origin, comment, id_identity) values ( 659 %(val)s, 660 %(pk_type)s, 661 %(comment)s, 662 %(pat)s 663 )""" 664 665 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}]) 666 667 # or update comment of existing ID 668 else: 669 row = rows[0] 670 if comment is not None: 671 # comment not already there ? 672 if gmTools.coalesce(row['comment'], '').find(comment.strip()) == -1: 673 comment = '%s%s' % (gmTools.coalesce(row['comment'], '', '%s // '), comment.strip) 674 cmd = u"update dem.lnk_identity2ext_id set comment = %(comment)s where id=%(pk)s" 675 args = {'comment': comment, 'pk': row['pk_id']} 676 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
677 #--------------------------------------------------------
678 - def update_external_id(self, pk_id=None, type=None, value=None, issuer=None, comment=None):
679 """Edits an existing external ID. 680 681 Creates ID type if necessary. 682 """ 683 cmd = u""" 684 UPDATE dem.lnk_identity2ext_id SET 685 fk_origin = (SELECT dem.add_external_id_type(%(type)s, %(issuer)s)), 686 external_id = %(value)s, 687 comment = gm.nullify_empty_string(%(comment)s) 688 WHERE 689 id = %(pk)s 690 """ 691 args = {'pk': pk_id, 'value': value, 'type': type, 'issuer': issuer, 'comment': comment} 692 rows, idx = gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
693 #--------------------------------------------------------
694 - def get_external_ids(self, id_type=None, issuer=None):
695 where_parts = ['pk_identity = %(pat)s'] 696 args = {'pat': self.ID} 697 698 if id_type is not None: 699 where_parts.append(u'name = %(name)s') 700 args['name'] = id_type.strip() 701 702 if issuer is not None: 703 where_parts.append(u'issuer = %(issuer)s') 704 args['issuer'] = issuer.strip() 705 706 cmd = u"SELECT * FROM dem.v_external_ids4identity WHERE %s" % ' AND '.join(where_parts) 707 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}]) 708 709 return rows
710 711 external_ids = property(get_external_ids, lambda x:x) 712 #--------------------------------------------------------
713 - def delete_external_id(self, pk_ext_id=None):
714 cmd = u""" 715 delete from dem.lnk_identity2ext_id 716 where id_identity = %(pat)s and id = %(pk)s""" 717 args = {'pat': self.ID, 'pk': pk_ext_id} 718 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}])
719 #--------------------------------------------------------
720 - def assimilate_identity(self, other_identity=None, link_obj=None):
721 """Merge another identity into this one. 722 723 Keep this one. Delete other one.""" 724 725 if other_identity.ID == self.ID: 726 return True, None 727 728 curr_pat = gmCurrentPatient() 729 if curr_pat.connected: 730 if other_identity.ID == curr_pat.ID: 731 return False, _('Cannot merge active patient into another patient.') 732 733 queries = [] 734 args = {'pat2del': other_identity.ID, 'pat2keep': self.ID} 735 736 # merge allergy state 737 queries.append ({ 738 'cmd': u""" 739 UPDATE clin.allergy_state SET 740 has_allergy = greatest ( 741 (SELECT has_allergy FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2del)s), 742 (SELECT has_allergy FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2keep)s) 743 ) 744 WHERE 745 pk = (SELECT pk_allergy_state FROM clin.v_pat_allergy_state WHERE pk_patient = %(pat2keep)s) 746 """, 747 'args': args 748 }) 749 # delete old allergy state 750 queries.append ({ 751 'cmd': u'delete from clin.allergy_state where pk = (select pk_allergy_state from clin.v_pat_allergy_state where pk_patient = %(pat2del)s)', 752 'args': args 753 }) 754 755 # transfer names 756 # 1) disambiguate names in old pat 757 queries.append ({ 758 'cmd': u""" 759 UPDATE dem.names d_n1 SET 760 lastnames = lastnames || ' (%s)' 761 WHERE 762 d_n1.id_identity = %%(pat2del)s 763 AND 764 EXISTS ( 765 SELECT 1 FROM dem.names d_n2 766 WHERE 767 d_n2.id_identity = %%(pat2keep)s 768 AND 769 d_n2.lastnames = d_n1.lastnames 770 AND 771 d_n2.firstnames = d_n1.firstnames 772 )""" % _('assimilated'), 773 'args': args 774 }) 775 # 2) move inactive ones (but beware of dupes) 776 queries.append ({ 777 'cmd': u""" 778 UPDATE dem.names SET 779 id_identity = %(pat2keep)s 780 WHERE id_identity = %(pat2del)s AND active IS false""", 781 'args': args 782 }) 783 # 3) copy active ones 784 queries.append ({ 785 'cmd': u""" 786 INSERT INTO dem.names ( 787 id_identity, active, lastnames, firstnames, preferred, comment 788 ) SELECT 789 %(pat2keep)s, false, lastnames, firstnames, preferred, comment 790 FROM dem.names d_n 791 WHERE d_n.id_identity = %(pat2del)s AND d_n.active IS true""", 792 'args': args 793 }) 794 795 # find FKs pointing to identity 796 FKs = gmPG2.get_foreign_keys2column ( 797 schema = u'dem', 798 table = u'identity', 799 column = u'pk' 800 ) 801 802 # disambiguate potential dupes 803 # - same-url comm channels 804 queries.append ({ 805 'cmd': u""" 806 UPDATE dem.lnk_identity2comm 807 SET url = url || ' (%s %s)' 808 WHERE 809 fk_identity = %%(pat2del)s 810 AND 811 EXISTS ( 812 SELECT 1 FROM dem.lnk_identity2comm d_li2c 813 WHERE d_li2c.fk_identity = %%(pat2keep)s AND d_li2c.url = url 814 ) 815 """ % (_('merged'), gmDateTime.pydt_strftime()), 816 'args': args 817 }) 818 # - same-value external IDs 819 queries.append ({ 820 'cmd': u""" 821 UPDATE dem.lnk_identity2ext_id 822 SET external_id = external_id || ' (%s %s)' 823 WHERE 824 id_identity = %%(pat2del)s 825 AND 826 EXISTS ( 827 SELECT 1 FROM dem.lnk_identity2ext_id d_li2e 828 WHERE 829 d_li2e.id_identity = %%(pat2keep)s 830 AND 831 d_li2e.external_id = external_id 832 AND 833 d_li2e.fk_origin = fk_origin 834 ) 835 """ % (_('merged'), gmDateTime.pydt_strftime()), 836 'args': args 837 }) 838 # - same addresses 839 queries.append ({ 840 'cmd': u""" 841 DELETE FROM dem.lnk_person_org_address 842 WHERE 843 id_identity = %(pat2del)s 844 AND 845 id_address IN ( 846 SELECT id_address FROM dem.lnk_person_org_address d_lpoa 847 WHERE d_lpoa.id_identity = %(pat2keep)s 848 ) 849 """, 850 'args': args 851 }) 852 853 # generate UPDATEs 854 cmd_template = u'UPDATE %s SET %s = %%(pat2keep)s WHERE %s = %%(pat2del)s' 855 for FK in FKs: 856 if FK['referencing_table'] == u'dem.names': 857 continue 858 queries.append ({ 859 'cmd': cmd_template % (FK['referencing_table'], FK['referencing_column'], FK['referencing_column']), 860 'args': args 861 }) 862 863 # remove old identity entry 864 queries.append ({ 865 'cmd': u'delete from dem.identity where pk = %(pat2del)s', 866 'args': args 867 }) 868 869 _log.warning('identity [%s] is about to assimilate identity [%s]', self.ID, other_identity.ID) 870 871 gmPG2.run_rw_queries(link_obj = link_obj, queries = queries, end_tx = True) 872 873 self.add_external_id ( 874 type_name = u'merged GNUmed identity primary key', 875 value = u'GNUmed::pk::%s' % other_identity.ID, 876 issuer = u'GNUmed' 877 ) 878 879 return True, None
880 #-------------------------------------------------------- 881 #--------------------------------------------------------
882 - def put_on_waiting_list(self, urgency=0, comment=None, zone=None):
883 cmd = u""" 884 insert into clin.waiting_list (fk_patient, urgency, comment, area, list_position) 885 values ( 886 %(pat)s, 887 %(urg)s, 888 %(cmt)s, 889 %(area)s, 890 (select coalesce((max(list_position) + 1), 1) from clin.waiting_list) 891 )""" 892 args = {'pat': self.ID, 'urg': urgency, 'cmt': comment, 'area': zone} 893 gmPG2.run_rw_queries(queries = [{'cmd': cmd, 'args': args}], verbose = True)
894 #--------------------------------------------------------
895 - def get_waiting_list_entry(self):
896 cmd = u"""SELECT * FROM clin.v_waiting_list WHERE pk_identity = %(pat)s""" 897 args = {'pat': self.ID} 898 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}]) 899 return rows
900 901 waiting_list_entries = property(get_waiting_list_entry, lambda x:x) 902 #--------------------------------------------------------
903 - def _get_export_tray(self):
904 #return gmExportTray.cExportTray(self.tray_dir_name) 905 return None
906 907 export_tray = property(_get_export_tray, lambda x:x) 908 #--------------------------------------------------------
909 - def export_as_gdt(self, filename=None, encoding='iso-8859-15', external_id_type=None):
910 911 template = u'%s%s%s\r\n' 912 913 file = codecs.open ( 914 filename = filename, 915 mode = 'wb', 916 encoding = encoding, 917 errors = 'strict' 918 ) 919 920 file.write(template % (u'013', u'8000', u'6301')) 921 file.write(template % (u'013', u'9218', u'2.10')) 922 if external_id_type is None: 923 file.write(template % (u'%03d' % (9 + len(str(self.ID))), u'3000', self.ID)) 924 else: 925 ext_ids = self.get_external_ids(id_type = external_id_type) 926 if len(ext_ids) > 0: 927 file.write(template % (u'%03d' % (9 + len(ext_ids[0]['value'])), u'3000', ext_ids[0]['value'])) 928 file.write(template % (u'%03d' % (9 + len(self._payload[self._idx['lastnames']])), u'3101', self._payload[self._idx['lastnames']])) 929 file.write(template % (u'%03d' % (9 + len(self._payload[self._idx['firstnames']])), u'3102', self._payload[self._idx['firstnames']])) 930 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'))) 931 file.write(template % (u'010', u'3110', gmXdtMappings.map_gender_gm2xdt[self._payload[self._idx['gender']]])) 932 file.write(template % (u'025', u'6330', 'GNUmed::9206::encoding')) 933 file.write(template % (u'%03d' % (9 + len(encoding)), u'6331', encoding)) 934 if external_id_type is None: 935 file.write(template % (u'029', u'6332', u'GNUmed::3000::source')) 936 file.write(template % (u'017', u'6333', u'internal')) 937 else: 938 if len(ext_ids) > 0: 939 file.write(template % (u'029', u'6332', u'GNUmed::3000::source')) 940 file.write(template % (u'%03d' % (9 + len(external_id_type)), u'6333', external_id_type)) 941 942 file.close()
943 #-------------------------------------------------------- 944 # occupations API 945 #--------------------------------------------------------
946 - def get_occupations(self):
947 return gmDemographicRecord.get_occupations(pk_identity = self.pk_obj)
948 #-------------------------------------------------------- 985 #-------------------------------------------------------- 993 #-------------------------------------------------------- 994 # comms API 995 #--------------------------------------------------------
996 - def get_comm_channels(self, comm_medium=None):
997 cmd = u"select * from dem.v_person_comms where pk_identity = %s" 998 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self.pk_obj]}], get_col_idx = True) 999 1000 filtered = rows 1001 1002 if comm_medium is not None: 1003 filtered = [] 1004 for row in rows: 1005 if row['comm_type'] == comm_medium: 1006 filtered.append(row) 1007 1008 return [ gmDemographicRecord.cCommChannel(row = { 1009 'pk_field': 'pk_lnk_identity2comm', 1010 'data': r, 1011 'idx': idx 1012 }) for r in filtered 1013 ]
1014 #-------------------------------------------------------- 1032 #-------------------------------------------------------- 1038 #-------------------------------------------------------- 1039 # contacts API 1040 #--------------------------------------------------------
1041 - def get_addresses(self, address_type=None):
1042 1043 cmd = u"SELECT * FROM dem.v_pat_addresses WHERE pk_identity = %(pat)s" 1044 args = {'pat': self.pk_obj} 1045 if address_type is not None: 1046 cmd = cmd + u" AND address_type = %(typ)s" 1047 args['typ'] = address_type 1048 1049 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}], get_col_idx = True) 1050 1051 return [ 1052 gmDemographicRecord.cPatientAddress(row = {'idx': idx, 'data': r, 'pk_field': 'pk_address'}) 1053 for r in rows 1054 ]
1055 #-------------------------------------------------------- 1098 #---------------------------------------------------------------------- 1111 #---------------------------------------------------------------------- 1112 # relatives API 1113 #----------------------------------------------------------------------
1114 - def get_relatives(self):
1115 cmd = u""" 1116 select 1117 t.description, 1118 vbp.pk_identity as id, 1119 title, 1120 firstnames, 1121 lastnames, 1122 dob, 1123 cob, 1124 gender, 1125 karyotype, 1126 pupic, 1127 pk_marital_status, 1128 marital_status, 1129 xmin_identity, 1130 preferred 1131 from 1132 dem.v_basic_person vbp, dem.relation_types t, dem.lnk_person2relative l 1133 where 1134 ( 1135 l.id_identity = %(pk)s and 1136 vbp.pk_identity = l.id_relative and 1137 t.id = l.id_relation_type 1138 ) or ( 1139 l.id_relative = %(pk)s and 1140 vbp.pk_identity = l.id_identity and 1141 t.inverse = l.id_relation_type 1142 )""" 1143 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'pk': self.pk_obj}}]) 1144 if len(rows) == 0: 1145 return [] 1146 return [(row[0], cIdentity(row = {'data': row[1:], 'idx':idx, 'pk_field': 'pk'})) for row in rows]
1147 #-------------------------------------------------------- 1167 #----------------------------------------------------------------------
1168 - def delete_relative(self, relation):
1169 # unlink only, don't delete relative itself 1170 self.set_relative(None, relation)
1171 #--------------------------------------------------------
1173 if self._payload[self._idx['pk_emergency_contact']] is None: 1174 return None 1175 return cIdentity(aPK_obj = self._payload[self._idx['pk_emergency_contact']])
1176 1177 emergency_contact_in_database = property(_get_emergency_contact_from_database, lambda x:x) 1178 #---------------------------------------------------------------------- 1179 # age/dob related 1180 #----------------------------------------------------------------------
1181 - def get_formatted_dob(self, format='%Y %b %d', encoding=None, none_string=None):
1182 return gmDateTime.format_dob ( 1183 self._payload[self._idx['dob']], 1184 format = format, 1185 encoding = encoding, 1186 none_string = none_string, 1187 dob_is_estimated = self._payload[self._idx['dob_is_estimated']] 1188 )
1189 #----------------------------------------------------------------------
1190 - def get_medical_age(self):
1191 dob = self['dob'] 1192 1193 if dob is None: 1194 return u'??' 1195 1196 if dob > gmDateTime.pydt_now_here(): 1197 return _('invalid age: DOB in the future') 1198 1199 death = self['deceased'] 1200 1201 if death is None: 1202 return u'%s%s' % ( 1203 gmTools.bool2subst ( 1204 self._payload[self._idx['dob_is_estimated']], 1205 gmTools.u_almost_equal_to, 1206 u'' 1207 ), 1208 gmDateTime.format_apparent_age_medically ( 1209 age = gmDateTime.calculate_apparent_age(start = dob) 1210 ) 1211 ) 1212 1213 if dob > death: 1214 return _('invalid age: DOB after death') 1215 1216 return u'%s%s%s' % ( 1217 gmTools.u_latin_cross, 1218 gmTools.bool2subst ( 1219 self._payload[self._idx['dob_is_estimated']], 1220 gmTools.u_almost_equal_to, 1221 u'' 1222 ), 1223 gmDateTime.format_apparent_age_medically ( 1224 age = gmDateTime.calculate_apparent_age ( 1225 start = dob, 1226 end = self['deceased'] 1227 ) 1228 ) 1229 )
1230 #----------------------------------------------------------------------
1231 - def dob_in_range(self, min_distance=u'1 week', max_distance=u'1 week'):
1232 cmd = u'select dem.dob_is_in_range(%(dob)s, %(min)s, %(max)s)' 1233 rows, idx = gmPG2.run_ro_queries ( 1234 queries = [{ 1235 'cmd': cmd, 1236 'args': {'dob': self['dob'], 'min': min_distance, 'max': max_distance} 1237 }] 1238 ) 1239 return rows[0][0]
1240 #---------------------------------------------------------------------- 1241 # practice related 1242 #----------------------------------------------------------------------
1243 - def get_last_encounter(self):
1244 cmd = u'select * from clin.v_most_recent_encounters where pk_patient=%s' 1245 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': [self._payload[self._idx['pk_identity']]]}]) 1246 if len(rows) > 0: 1247 return rows[0] 1248 else: 1249 return None
1250 #--------------------------------------------------------
1251 - def get_messages(self, order_by=None):
1252 return gmProviderInbox.get_inbox_messages(pk_patient = self._payload[self._idx['pk_identity']], order_by = order_by)
1253 1254 messages = property(get_messages, lambda x:x) 1255 #--------------------------------------------------------
1256 - def _get_overdue_messages(self):
1257 return gmProviderInbox.get_overdue_messages(pk_patient = self._payload[self._idx['pk_identity']])
1258 1259 overdue_messages = property(_get_overdue_messages, lambda x:x) 1260 #--------------------------------------------------------
1261 - def delete_message(self, pk=None):
1262 return gmProviderInbox.delete_inbox_message(inbox_message = pk)
1263 #--------------------------------------------------------
1264 - def _get_dynamic_hints(self):
1265 return gmProviderInbox.get_hints_for_patient(pk_identity = self._payload[self._idx['pk_identity']])
1266 1267 dynamic_hints = property(_get_dynamic_hints, lambda x:x) 1268 #--------------------------------------------------------
1269 - def _get_primary_provider(self):
1270 if self._payload[self._idx['pk_primary_provider']] is None: 1271 return None 1272 from Gnumed.business import gmStaff 1273 return gmStaff.cStaff(aPK_obj = self._payload[self._idx['pk_primary_provider']])
1274 1275 primary_provider = property(_get_primary_provider, lambda x:x) 1276 #---------------------------------------------------------------------- 1277 # convenience 1278 #----------------------------------------------------------------------
1279 - def get_dirname(self):
1280 """Format patient demographics into patient specific path name fragment.""" 1281 return (u'%s-%s%s-%s' % ( 1282 self._payload[self._idx['lastnames']].replace(u' ', u'_'), 1283 self._payload[self._idx['firstnames']].replace(u' ', u'_'), 1284 gmTools.coalesce(self._payload[self._idx['preferred']], u'', template_initial = u'-(%s)').replace(u' ', u'_'), 1285 self.get_formatted_dob(format = '%Y-%m-%d', encoding = gmI18N.get_encoding()) 1286 )).replace ( 1287 u"'", u"" 1288 ).replace ( 1289 u'"', u'' 1290 ).replace ( 1291 u'/', u'_' 1292 ).replace ( 1293 u'\\', u'_' 1294 ).replace ( 1295 u'~', u'' 1296 ).replace ( 1297 u'|', u'_' 1298 ).replace ( 1299 u'*', u'' 1300 ).replace ( 1301 u'\u2248', u'' # "approximately", having been added by dob_is_estimated 1302 )
1303 1304 dirname = property(get_dirname, lambda x:x) 1305 #----------------------------------------------------------------------
1306 - def _get_tray_dir_name(self):
1307 paths = gmTools.gmPaths() 1308 return os.path.join(paths.tmp_dir, self.dirname)
1309 1310 tray_dir_name = property(_get_tray_dir_name, lambda x:x)
1311 #============================================================ 1312 # helper functions 1313 #------------------------------------------------------------ 1314 #_spin_on_emr_access = None 1315 # 1316 #def set_emr_access_spinner(func=None): 1317 # if not callable(func): 1318 # _log.error('[%] not callable, not setting _spin_on_emr_access', func) 1319 # return False 1320 # 1321 # _log.debug('setting _spin_on_emr_access to [%s]', func) 1322 # 1323 # global _spin_on_emr_access 1324 # _spin_on_emr_access = func 1325 1326 #============================================================
1327 -class cPatient(cIdentity):
1328 """Represents a person which is a patient. 1329 1330 - a specializing subclass of cIdentity turning it into a patient 1331 - its use is to cache subobjects like EMR and document folder 1332 """
1333 - def __init__(self, aPK_obj=None, row=None):
1334 cIdentity.__init__(self, aPK_obj=aPK_obj, row=row) 1335 self.__db_cache = {} 1336 self.__emr_access_lock = threading.Lock()
1337 #--------------------------------------------------------
1338 - def cleanup(self):
1339 """Do cleanups before dying. 1340 1341 - note that this may be called in a thread 1342 """ 1343 if self.__db_cache.has_key('clinical record'): 1344 self.__db_cache['clinical record'].cleanup() 1345 if self.__db_cache.has_key('document folder'): 1346 self.__db_cache['document folder'].cleanup() 1347 cIdentity.cleanup(self)
1348 #----------------------------------------------------------
1349 - def get_emr(self, allow_user_interaction=True):
1350 if not self.__emr_access_lock.acquire(False): 1351 # maybe something slow is happening on the machine 1352 _log.debug('failed to acquire EMR access lock, sleeping for 500ms') 1353 time.sleep(0.5) 1354 if not self.__emr_access_lock.acquire(False): 1355 _log.debug('still failed to acquire EMR access lock, aborting') 1356 raise AttributeError('cannot lock access to EMR') 1357 try: 1358 self.__db_cache['clinical record'] 1359 except KeyError: 1360 self.__db_cache['clinical record'] = gmClinicalRecord.cClinicalRecord(aPKey = self._payload[self._idx['pk_identity']], allow_user_interaction = allow_user_interaction) 1361 self.__emr_access_lock.release() 1362 return self.__db_cache['clinical record']
1363 1364 emr = property(get_emr, lambda x:x) 1365 #--------------------------------------------------------
1366 - def get_document_folder(self):
1367 try: 1368 return self.__db_cache['document folder'] 1369 except KeyError: 1370 pass 1371 1372 self.__db_cache['document folder'] = cDocumentFolder(aPKey = self._payload[self._idx['pk_identity']]) 1373 return self.__db_cache['document folder']
1374 1375 document_folder = property(get_document_folder, lambda x:x)
1376 #============================================================
1377 -class gmCurrentPatient(gmBorg.cBorg):
1378 """Patient Borg to hold currently active patient. 1379 1380 There may be many instances of this but they all share state. 1381 """
1382 - def __init__(self, patient=None, forced_reload=False):
1383 """Change or get currently active patient. 1384 1385 patient: 1386 * None: get currently active patient 1387 * -1: unset currently active patient 1388 * cPatient instance: set active patient if possible 1389 """ 1390 # make sure we do have a patient pointer 1391 try: 1392 tmp = self.patient 1393 except AttributeError: 1394 self.patient = gmNull.cNull() 1395 self.__register_interests() 1396 # set initial lock state, 1397 # this lock protects against activating another patient 1398 # when we are controlled from a remote application 1399 self.__lock_depth = 0 1400 # initialize callback state 1401 self.__pre_selection_callbacks = [] 1402 1403 # user wants copy of current patient 1404 if patient is None: 1405 return None 1406 1407 # do nothing if patient is locked 1408 if self.locked: 1409 _log.error('patient [%s] is locked, cannot change to [%s]' % (self.patient['pk_identity'], patient)) 1410 return None 1411 1412 # user wants to explicitly unset current patient 1413 if patient == -1: 1414 _log.debug('explicitly unsetting current patient') 1415 if not self.__run_pre_selection_callbacks(): 1416 _log.debug('not unsetting current patient') 1417 return None 1418 self.__send_pre_selection_notification() 1419 self.patient.cleanup() 1420 self.patient = gmNull.cNull() 1421 self.__send_selection_notification() 1422 return None 1423 1424 # must be cPatient instance, then 1425 if not isinstance(patient, cPatient): 1426 _log.error('cannot set active patient to [%s], must be either None, -1 or cPatient instance' % str(patient)) 1427 raise TypeError, 'gmPerson.gmCurrentPatient.__init__(): <patient> must be None, -1 or cPatient instance but is: %s' % str(patient) 1428 1429 # same ID, no change needed 1430 if (self.patient['pk_identity'] == patient['pk_identity']) and not forced_reload: 1431 return None 1432 1433 # user wants different patient 1434 _log.debug('patient change [%s] -> [%s] requested', self.patient['pk_identity'], patient['pk_identity']) 1435 1436 # everything seems swell 1437 if not self.__run_pre_selection_callbacks(): 1438 _log.debug('not changing current patient') 1439 return None 1440 self.__send_pre_selection_notification() 1441 self.patient.cleanup() 1442 self.patient = patient 1443 self.patient.get_emr() 1444 self.__send_selection_notification() 1445 1446 return None
1447 #--------------------------------------------------------
1448 - def __register_interests(self):
1449 gmDispatcher.connect(signal = u'dem.identity_mod_db', receiver = self._on_identity_change) 1450 gmDispatcher.connect(signal = u'dem.names_mod_db', receiver = self._on_identity_change)
1451 #--------------------------------------------------------
1452 - def _on_identity_change(self):
1453 """Listen for patient *data* change.""" 1454 self.patient.refetch_payload()
1455 #-------------------------------------------------------- 1456 # external API 1457 #--------------------------------------------------------
1458 - def register_pre_selection_callback(self, callback=None):
1459 if not callable(callback): 1460 raise TypeError(u'callback [%s] not callable' % callback) 1461 1462 self.__pre_selection_callbacks.append(callback)
1463 #--------------------------------------------------------
1464 - def _get_connected(self):
1465 return (not isinstance(self.patient, gmNull.cNull))
1466
1467 - def _set_connected(self):
1468 raise AttributeError(u'invalid to set <connected> state')
1469 1470 connected = property(_get_connected, _set_connected) 1471 #--------------------------------------------------------
1472 - def _get_locked(self):
1473 return (self.__lock_depth > 0)
1474
1475 - def _set_locked(self, locked):
1476 if locked: 1477 self.__lock_depth = self.__lock_depth + 1 1478 gmDispatcher.send(signal='patient_locked') 1479 else: 1480 if self.__lock_depth == 0: 1481 _log.error('lock/unlock imbalance, trying to refcount lock depth below 0') 1482 return 1483 else: 1484 self.__lock_depth = self.__lock_depth - 1 1485 gmDispatcher.send(signal='patient_unlocked')
1486 1487 locked = property(_get_locked, _set_locked) 1488 #--------------------------------------------------------
1489 - def force_unlock(self):
1490 _log.info('forced patient unlock at lock depth [%s]' % self.__lock_depth) 1491 self.__lock_depth = 0 1492 gmDispatcher.send(signal='patient_unlocked')
1493 #-------------------------------------------------------- 1494 # patient change handling 1495 #--------------------------------------------------------
1497 if isinstance(self.patient, gmNull.cNull): 1498 return True 1499 1500 for call_back in self.__pre_selection_callbacks: 1501 try: 1502 successful = call_back() 1503 except: 1504 _log.exception('callback [%s] failed', call_back) 1505 print "*** pre-selection callback failed ***" 1506 print type(call_back) 1507 print call_back 1508 return False 1509 1510 if not successful: 1511 _log.debug('callback [%s] returned False', call_back) 1512 return False 1513 1514 return True
1515 #--------------------------------------------------------
1517 """Sends signal when another patient is about to become active. 1518 1519 This does NOT wait for signal handlers to complete. 1520 """ 1521 kwargs = { 1522 'signal': u'pre_patient_selection', 1523 'sender': id(self.__class__), 1524 'pk_identity': self.patient['pk_identity'] 1525 } 1526 gmDispatcher.send(**kwargs)
1527 #--------------------------------------------------------
1529 """Sends signal when another patient has actually been made active.""" 1530 kwargs = { 1531 'signal': u'post_patient_selection', 1532 'sender': id(self.__class__), 1533 'pk_identity': self.patient['pk_identity'] 1534 } 1535 gmDispatcher.send(**kwargs)
1536 #-------------------------------------------------------- 1537 # __getattr__ handling 1538 #--------------------------------------------------------
1539 - def __getattr__(self, attribute):
1540 if attribute == 'patient': 1541 raise AttributeError 1542 if not isinstance(self.patient, gmNull.cNull): 1543 return getattr(self.patient, attribute)
1544 #-------------------------------------------------------- 1545 # __get/setitem__ handling 1546 #--------------------------------------------------------
1547 - def __getitem__(self, attribute = None):
1548 """Return any attribute if known how to retrieve it by proxy. 1549 """ 1550 return self.patient[attribute]
1551 #--------------------------------------------------------
1552 - def __setitem__(self, attribute, value):
1553 self.patient[attribute] = value
1554 #============================================================ 1555 # match providers 1556 #============================================================
1557 -class cMatchProvider_Provider(gmMatchProvider.cMatchProvider_SQL2):
1558 - def __init__(self):
1559 gmMatchProvider.cMatchProvider_SQL2.__init__( 1560 self, 1561 queries = [ 1562 u"""SELECT 1563 pk_staff AS data, 1564 short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS list_label, 1565 short_alias || ' (' || coalesce(title, '') || ' ' || firstnames || ' ' || lastnames || ')' AS field_label 1566 FROM dem.v_staff 1567 WHERE 1568 is_active AND ( 1569 short_alias %(fragment_condition)s OR 1570 firstnames %(fragment_condition)s OR 1571 lastnames %(fragment_condition)s OR 1572 db_user %(fragment_condition)s 1573 ) 1574 """ 1575 ] 1576 ) 1577 self.setThresholds(1, 2, 3)
1578 #============================================================ 1579 # convenience functions 1580 #============================================================
1581 -def create_name(pk_person, firstnames, lastnames, active=False):
1582 queries = [{ 1583 'cmd': u"select dem.add_name(%s, %s, %s, %s)", 1584 'args': [pk_person, firstnames, lastnames, active] 1585 }] 1586 rows, idx = gmPG2.run_rw_queries(queries=queries, return_data=True) 1587 name = cPersonName(aPK_obj = rows[0][0]) 1588 return name
1589 #============================================================
1590 -def create_identity(gender=None, dob=None, lastnames=None, firstnames=None):
1591 1592 cmd1 = u"""INSERT INTO dem.identity (gender, dob) VALUES (%s, %s)""" 1593 cmd2 = u""" 1594 INSERT INTO dem.names ( 1595 id_identity, lastnames, firstnames 1596 ) VALUES ( 1597 currval('dem.identity_pk_seq'), coalesce(%s, 'xxxDEFAULTxxx'), coalesce(%s, 'xxxDEFAULTxxx') 1598 ) RETURNING id_identity""" 1599 rows, idx = gmPG2.run_rw_queries ( 1600 queries = [ 1601 {'cmd': cmd1, 'args': [gender, dob]}, 1602 {'cmd': cmd2, 'args': [lastnames, firstnames]} 1603 ], 1604 return_data = True 1605 ) 1606 ident = cIdentity(aPK_obj=rows[0][0]) 1607 gmHooks.run_hook_script(hook = u'post_person_creation') 1608 return ident
1609 #============================================================
1610 -def create_dummy_identity():
1611 cmd = u"INSERT INTO dem.identity(gender) VALUES ('xxxDEFAULTxxx') RETURNING pk" 1612 rows, idx = gmPG2.run_rw_queries ( 1613 queries = [{'cmd': cmd}], 1614 return_data = True 1615 ) 1616 return gmDemographicRecord.cIdentity(aPK_obj = rows[0][0])
1617 #============================================================
1618 -def set_active_patient(patient=None, forced_reload=False):
1619 """Set active patient. 1620 1621 If patient is -1 the active patient will be UNset. 1622 """ 1623 if isinstance(patient, cPatient): 1624 pat = patient 1625 elif isinstance(patient, cIdentity): 1626 pat = cPatient(aPK_obj = patient['pk_identity']) 1627 # elif isinstance(patient, cStaff): 1628 # pat = cPatient(aPK_obj=patient['pk_identity']) 1629 elif isinstance(patient, gmCurrentPatient): 1630 pat = patient.patient 1631 elif patient == -1: 1632 pat = patient 1633 else: 1634 # maybe integer ? 1635 success, pk = gmTools.input2int(initial = patient, minval = 1) 1636 if not success: 1637 raise ValueError('<patient> must be either -1, >0, or a cPatient, cIdentity or gmCurrentPatient instance, is: %s' % patient) 1638 # but also valid patient ID ? 1639 try: 1640 pat = cPatient(aPK_obj = pk) 1641 except: 1642 _log.exception('error changing active patient to [%s]' % patient) 1643 return False 1644 1645 # attempt to switch 1646 try: 1647 gmCurrentPatient(patient = pat, forced_reload = forced_reload) 1648 except: 1649 _log.exception('error changing active patient to [%s]' % patient) 1650 return False 1651 1652 return True
1653 #============================================================ 1654 # gender related 1655 #------------------------------------------------------------
1656 -def get_gender_list():
1657 """Retrieves the list of known genders from the database.""" 1658 global __gender_idx 1659 global __gender_list 1660 1661 if __gender_list is None: 1662 cmd = u"select tag, l10n_tag, label, l10n_label, sort_weight from dem.v_gender_labels order by sort_weight desc" 1663 __gender_list, __gender_idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = True) 1664 1665 return (__gender_list, __gender_idx)
1666 #------------------------------------------------------------ 1667 map_gender2mf = { 1668 'm': u'm', 1669 'f': u'f', 1670 'tf': u'f', 1671 'tm': u'm', 1672 'h': u'mf' 1673 } 1674 #------------------------------------------------------------ 1675 # Maps GNUmed related i18n-aware gender specifiers to a unicode symbol. 1676 map_gender2symbol = { 1677 'm': u'\u2642', 1678 'f': u'\u2640', 1679 'tf': u'\u26A5\u2640', 1680 'tm': u'\u26A5\u2642', 1681 'h': u'\u26A5' 1682 # 'tf': u'\u2642\u2640-\u2640', 1683 # 'tm': u'\u2642\u2640-\u2642', 1684 # 'h': u'\u2642\u2640' 1685 } 1686 #------------------------------------------------------------
1687 -def map_gender2string(gender=None):
1688 """Maps GNUmed related i18n-aware gender specifiers to a human-readable string.""" 1689 1690 global __gender2string_map 1691 1692 if __gender2string_map is None: 1693 genders, idx = get_gender_list() 1694 __gender2string_map = { 1695 'm': _('male'), 1696 'f': _('female'), 1697 'tf': u'', 1698 'tm': u'', 1699 'h': u'' 1700 } 1701 for g in genders: 1702 __gender2string_map[g[idx['l10n_tag']]] = g[idx['l10n_label']] 1703 __gender2string_map[g[idx['tag']]] = g[idx['l10n_label']] 1704 1705 return __gender2string_map[gender]
1706 #------------------------------------------------------------
1707 -def map_gender2salutation(gender=None):
1708 """Maps GNUmed related i18n-aware gender specifiers to a human-readable salutation.""" 1709 1710 global __gender2salutation_map 1711 1712 if __gender2salutation_map is None: 1713 genders, idx = get_gender_list() 1714 __gender2salutation_map = { 1715 'm': _('Mr'), 1716 'f': _('Mrs'), 1717 'tf': u'', 1718 'tm': u'', 1719 'h': u'' 1720 } 1721 for g in genders: 1722 __gender2salutation_map[g[idx['l10n_tag']]] = __gender2salutation_map[g[idx['tag']]] 1723 __gender2salutation_map[g[idx['label']]] = __gender2salutation_map[g[idx['tag']]] 1724 __gender2salutation_map[g[idx['l10n_label']]] = __gender2salutation_map[g[idx['tag']]] 1725 1726 return __gender2salutation_map[gender]
1727 #------------------------------------------------------------
1728 -def map_firstnames2gender(firstnames=None):
1729 """Try getting the gender for the given first name.""" 1730 1731 if firstnames is None: 1732 return None 1733 1734 rows, idx = gmPG2.run_ro_queries(queries = [{ 1735 'cmd': u"select gender from dem.name_gender_map where name ilike %(fn)s limit 1", 1736 'args': {'fn': firstnames} 1737 }]) 1738 1739 if len(rows) == 0: 1740 return None 1741 1742 return rows[0][0]
1743 #============================================================
1744 -def get_persons_from_pks(pks=None):
1745 return [ cIdentity(aPK_obj = pk) for pk in pks ]
1746 #============================================================
1747 -def get_person_from_xdt(filename=None, encoding=None, dob_format=None):
1748 from Gnumed.business import gmXdtObjects 1749 return gmXdtObjects.read_person_from_xdt(filename=filename, encoding=encoding, dob_format=dob_format)
1750 #============================================================
1751 -def get_persons_from_pracsoft_file(filename=None, encoding='ascii'):
1752 from Gnumed.business import gmPracSoftAU 1753 return gmPracSoftAU.read_persons_from_pracsoft_file(filename=filename, encoding=encoding)
1754 #============================================================ 1755 # main/testing 1756 #============================================================ 1757 if __name__ == '__main__': 1758 1759 if len(sys.argv) == 1: 1760 sys.exit() 1761 1762 if sys.argv[1] != 'test': 1763 sys.exit() 1764 1765 import datetime 1766 1767 gmI18N.activate_locale() 1768 gmI18N.install_domain() 1769 gmDateTime.init() 1770 1771 #--------------------------------------------------------
1772 - def test_set_active_pat():
1773 1774 ident = cIdentity(1) 1775 print "setting active patient with", ident 1776 set_active_patient(patient=ident) 1777 1778 patient = cPatient(12) 1779 print "setting active patient with", patient 1780 set_active_patient(patient=patient) 1781 1782 pat = gmCurrentPatient() 1783 print pat['dob'] 1784 #pat['dob'] = 'test' 1785 1786 # staff = cStaff() 1787 # print "setting active patient with", staff 1788 # set_active_patient(patient=staff) 1789 1790 print "setting active patient with -1" 1791 set_active_patient(patient=-1)
1792 #--------------------------------------------------------
1793 - def test_dto_person():
1794 dto = cDTO_person() 1795 dto.firstnames = 'Sepp' 1796 dto.lastnames = 'Herberger' 1797 dto.gender = 'male' 1798 dto.dob = pyDT.datetime.now(tz=gmDateTime.gmCurrentLocalTimezone) 1799 print dto 1800 1801 print dto['firstnames'] 1802 print dto['lastnames'] 1803 print dto['gender'] 1804 print dto['dob'] 1805 1806 for key in dto.keys(): 1807 print key
1808 #--------------------------------------------------------
1809 - def test_identity():
1810 # create patient 1811 print '\n\nCreating identity...' 1812 new_identity = create_identity(gender='m', dob='2005-01-01', lastnames='test lastnames', firstnames='test firstnames') 1813 print 'Identity created: %s' % new_identity 1814 1815 print '\nSetting title and gender...' 1816 new_identity['title'] = 'test title'; 1817 new_identity['gender'] = 'f'; 1818 new_identity.save_payload() 1819 print 'Refetching identity from db: %s' % cIdentity(aPK_obj=new_identity['pk_identity']) 1820 1821 print '\nGetting all names...' 1822 for a_name in new_identity.get_names(): 1823 print a_name 1824 print 'Active name: %s' % (new_identity.get_active_name()) 1825 print 'Setting nickname...' 1826 new_identity.set_nickname(nickname='test nickname') 1827 print 'Refetching all names...' 1828 for a_name in new_identity.get_names(): 1829 print a_name 1830 print 'Active name: %s' % (new_identity.get_active_name()) 1831 1832 print '\nIdentity occupations: %s' % new_identity['occupations'] 1833 print 'Creating identity occupation...' 1834 new_identity.link_occupation('test occupation') 1835 print 'Identity occupations: %s' % new_identity['occupations'] 1836 1837 print '\nIdentity addresses: %s' % new_identity.get_addresses() 1838 print 'Creating identity address...' 1839 # make sure the state exists in the backend 1840 new_identity.link_address ( 1841 number = 'test 1234', 1842 street = 'test street', 1843 postcode = 'test postcode', 1844 urb = 'test urb', 1845 state = 'SN', 1846 country = 'DE' 1847 ) 1848 print 'Identity addresses: %s' % new_identity.get_addresses() 1849 1850 print '\nIdentity communications: %s' % new_identity.get_comm_channels() 1851 print 'Creating identity communication...' 1852 new_identity.link_comm_channel('homephone', '1234566') 1853 print 'Identity communications: %s' % new_identity.get_comm_channels()
1854 #--------------------------------------------------------
1855 - def test_name():
1856 for pk in range(1,16): 1857 name = cPersonName(aPK_obj=pk) 1858 print name.description 1859 print ' ', name
1860 #--------------------------------------------------------
1861 - def test_gender_list():
1862 genders, idx = get_gender_list() 1863 print "\n\nRetrieving gender enum (tag, label, weight):" 1864 for gender in genders: 1865 print "%s, %s, %s" % (gender[idx['tag']], gender[idx['l10n_label']], gender[idx['sort_weight']])
1866 #-------------------------------------------------------- 1867 #test_dto_person() 1868 #test_identity() 1869 #test_set_active_pat() 1870 #test_search_by_dto() 1871 #test_name() 1872 test_gender_list() 1873 1874 #map_gender2salutation('m') 1875 # module functions 1876 1877 #comms = get_comm_list() 1878 #print "\n\nRetrieving communication media enum (id, description): %s" % comms 1879 1880 #============================================================ 1881